summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/control_panel
diff options
context:
space:
mode:
authorMathieu Duckerts-Antoine <dam@odoo.com>2020-05-25 18:11:02 +0000
committerMathieu Duckerts-Antoine <dam@odoo.com>2020-06-04 19:32:42 +0000
commitbe5a51a345b8986da0e19627260f64f984e11ae9 (patch)
tree4a0b1e5b0bfdad3124718ccf8e67164b5bbe07dd /addons/web/static/src/js/control_panel
parent884ef9f7ee345ab1e940dbf88f4432b7e10f36e3 (diff)
[IMP] web: Comparison menu
This commit replaces the "time range menu" in the control panel by a new component: the "comparison menu". Its purpose is roughly the same: compare a period of data in a reporting view with the previous period or the previous year. Its implementation is a bit simpler: it will only appear if at least one date filter is selected in the search view, and will suggest to compare each selected date option with the previous period or the previous year. Task ID: 2245719 Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Julien Mougenot <jum@odoo.com>
Diffstat (limited to 'addons/web/static/src/js/control_panel')
-rw-r--r--addons/web/static/src/js/control_panel/comparison_menu.js63
-rw-r--r--addons/web/static/src/js/control_panel/control_panel.js4
-rw-r--r--addons/web/static/src/js/control_panel/control_panel_model.js555
-rw-r--r--addons/web/static/src/js/control_panel/search_utils.js477
-rw-r--r--addons/web/static/src/js/control_panel/time_range_menu.js109
5 files changed, 752 insertions, 456 deletions
diff --git a/addons/web/static/src/js/control_panel/comparison_menu.js b/addons/web/static/src/js/control_panel/comparison_menu.js
new file mode 100644
index 00000000000..4504370e7c4
--- /dev/null
+++ b/addons/web/static/src/js/control_panel/comparison_menu.js
@@ -0,0 +1,63 @@
+odoo.define("web.ComparisonMenu", function (require) {
+ "use strict";
+
+ const DropdownMenu = require("web.DropdownMenu");
+ const { FACET_ICONS } = require("web.searchUtils");
+ const { useModel } = require("web.model");
+
+ /**
+ * "Comparison" menu
+ *
+ * Displays a set of comparison options related to the currently selected
+ * date filters.
+ * @extends DropdownMenu
+ */
+ class ComparisonMenu extends DropdownMenu {
+ constructor() {
+ super(...arguments);
+ this.model = useModel("controlPanelModel");
+ }
+
+ //---------------------------------------------------------------------
+ // Getters
+ //---------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ get icon() {
+ return FACET_ICONS.comparison;
+ }
+
+ /**
+ * @override
+ */
+ get items() {
+ return this.model.getFiltersOfType("comparison");
+ }
+
+ /**
+ * @override
+ */
+ get title() {
+ return this.env._t("Comparison");
+ }
+
+ //---------------------------------------------------------------------
+ // Handlers
+ //---------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {OwlEvent} ev
+ */
+ _onItemSelected(ev) {
+ ev.stopPropagation();
+ const { item } = ev.detail;
+ this.model.dispatch("toggleComparison", item.id);
+ }
+
+ }
+
+ return ComparisonMenu;
+});
diff --git a/addons/web/static/src/js/control_panel/control_panel.js b/addons/web/static/src/js/control_panel/control_panel.js
index 5d42cfc2fbe..554912175bb 100644
--- a/addons/web/static/src/js/control_panel/control_panel.js
+++ b/addons/web/static/src/js/control_panel/control_panel.js
@@ -3,13 +3,13 @@ odoo.define('web.ControlPanel', function (require) {
const ActionMenus = require('web.ActionMenus');
const ControlPanelModel = require('web.ControlPanelModel');
+ const ComparisonMenu = require('web.ComparisonMenu');
const FavoriteMenu = require('web.FavoriteMenu');
const FilterMenu = require('web.FilterMenu');
const GroupByMenu = require('web.GroupByMenu');
const patchMixin = require('web.patchMixin');
const Pager = require('web.Pager');
const SearchBar = require('web.SearchBar');
- const TimeRangeMenu = require('web.TimeRangeMenu');
const { useModel } = require('web.model');
const { Component, hooks } = owl;
@@ -191,7 +191,7 @@ odoo.define('web.ControlPanel', function (require) {
ControlPanel.components = {
SearchBar,
ActionMenus, Pager,
- FilterMenu, GroupByMenu, TimeRangeMenu, FavoriteMenu,
+ ComparisonMenu, FilterMenu, GroupByMenu, FavoriteMenu,
};
ControlPanel.defaultProps = {
breadcrumbs: [],
diff --git a/addons/web/static/src/js/control_panel/control_panel_model.js b/addons/web/static/src/js/control_panel/control_panel_model.js
index 034c4cd22c7..e1eb9c8da1a 100644
--- a/addons/web/static/src/js/control_panel/control_panel_model.js
+++ b/addons/web/static/src/js/control_panel/control_panel_model.js
@@ -6,11 +6,9 @@ odoo.define('web.ControlPanelModel', function (require) {
const { parseArch } = require('web.viewUtils');
const pyUtils = require('web.py_utils');
- const { COMPARISON_TIME_RANGE_OPTIONS,
- DEFAULT_INTERVAL, DEFAULT_PERIOD, FACET_ICONS,
- INTERVAL_OPTIONS, OPTION_GENERATORS,
- TIME_RANGE_OPTIONS, YEAR_OPTIONS,
- rankPeriod, rankInterval } = require('web.searchUtils');
+ const { DEFAULT_INTERVAL, DEFAULT_PERIOD, FACET_ICONS,
+ getComparisonOptions, getIntervalOptions, getPeriodOptions,
+ constructDateDomain, rankInterval, yearSelected } = require('web.searchUtils');
const FAVORITE_PRIVATE_GROUP = 1;
const FAVORITE_SHARED_GROUP = 2;
@@ -41,10 +39,15 @@ odoo.define('web.ControlPanelModel', function (require) {
* @prop {number} groupId the id of some group, actually the group itself,
* the (active) 'groups' are reconstructed in _getGroups.
* @prop {string} description the description of the filter
- * @prop {string} type 'filter'|'groupBy'|'timeRange'|'field'|'favorite'
+ * @prop {string} type 'filter'|'groupBy'|'comparison'|'field'|'favorite'
*
* Other properties can be present according to the corresponding filter type:
*
+ * • type 'comparison':
+ * @prop {string} comparisonOptionId option identifier (@see COMPARISON_OPTIONS).
+ * @prop {string} dateFilterId the id of a date filter (filter of type 'filter'
+ * with isDateFilter=true)
+ *
* • type 'filter':
* @prop {number} groupNumber used to separate items in the 'Filters' menu
* @prop {string} [context] context
@@ -53,15 +56,15 @@ odoo.define('web.ControlPanelModel', function (require) {
* @if isDefault = true:
* > @prop {number} [defaultRank=-5] used to determine the order of
* > activation of default filters
- * @prop {boolean} [hasOptions] true if the filter comes from an arch node
+ * @prop {boolean} [isDateFilter] true if the filter comes from an arch node
* with a valid 'date' attribute.
- * @if hasOptions = true
+ * @if isDateFilter = true
+ * > @prop {boolean} [hasOptions=true]
* > @prop {string} defaultOptionId option identifier determined by
- * > default_period attribute (@see OPTION_GENERATORS).
+ * > default_period attribute (@see PERIOD_OPTIONS).
* > Default set to DEFAULT_PERIOD.
* > @prop {string} fieldName determined by the value of 'date' attribute
* > @prop {string} fieldType 'date' or 'datetime', type of the corresponding field
- * > @prop {Objecŧ[]} basicDomains of the form { description, domain }[]
* @else
* > @prop {string} domain
*
@@ -79,9 +82,6 @@ odoo.define('web.ControlPanelModel', function (require) {
* > @prop {string} defaultOptionId option identifier (see INTERVAL_OPTIONS)
* default set to DEFAULT_INTERVAL.
*
- * • type 'timeRange':
- * no extra key, a single filter has that type
- *
* • type 'field':
* @prop {string} fieldName
* @prop {string} fieldType
@@ -97,6 +97,8 @@ odoo.define('web.ControlPanelModel', function (require) {
* > @prop {Object} defaultAutocompleteValue of the form { value, label, operator }
*
* • type: 'favorite':
+ * @prop {Object} [comparison] of the form {comparisonId, fieldName, fieldDescription,
+ * range, rangeDescription, comparisonRange, comparisonRangeDescription, }
* @prop {Object} context
* @prop {string} domain
* @prop {string[]} groupBys
@@ -106,7 +108,6 @@ odoo.define('web.ControlPanelModel', function (require) {
* @prop {number} serverSideId
* @prop {number} userId
* @prop {boolean} [isDefault]
- * @prop {Object} [timeRanges] of the form { fieldName, rangeId[, comparisonRangeId] }
*
*-------------------------------------------------------------------------
* Query
@@ -120,17 +121,17 @@ odoo.define('web.ControlPanelModel', function (require) {
*
* Other properties must be defined according to the corresponding filter type.
*
+ * • type 'comparison':
+ * @prop {string} dateFilterId the id of a date filter (filter of type 'filter'
+ * with hasOptions=true)
+ * @prop {string} type 'comparison', help when searching if a comparison is active
+ *
* • type 'filter' with hasOptions=true:
- * @prop {string} optionId option identifier (@see OPTION_GENERATORS)
+ * @prop {string} optionId option identifier (@see PERIOD_OPTIONS)
*
* • type 'groupBy' with hasOptions=true:
* @prop {string} optionId option identifier (@see INTERVAL_OPTIONS)
*
- * • type 'timeRange':
- * @prop {string} fieldName name of a field
- * @prop {string} rangeId option identifier (see TIME_RANGE_OPTIONS)
- * @prop {string} [comparisonRangeId] option identifier (see COMPARISON_TIME_RANGE_OPTIONS)
- *
* • type 'field':
* @prop {string} label description put in the facet (can be temporarilly missing)
* @prop {(string|number)} value used as the value of the generated domain
@@ -173,6 +174,17 @@ odoo.define('web.ControlPanelModel', function (require) {
}
//---------------------------------------------------------------------
+ // Getters
+ //---------------------------------------------------------------------
+
+ /**
+ * @returns {(Object | undefined)}
+ */
+ get activeComparison() {
+ return this.state.query.find(queryElem => queryElem.type === 'comparison');
+ }
+
+ //---------------------------------------------------------------------
// Public
//---------------------------------------------------------------------
@@ -196,30 +208,6 @@ odoo.define('web.ControlPanelModel', function (require) {
}
/**
- * Activate the unique filter of type 'timeRange' with provided 'options' fieldName, rangeId,
- * and optional comparisonRangeId.
- * @param {string} fieldName
- * @param {string} rangeId
- * @param {string} [comparisonRangeId]
- */
- activateTimeRange(fieldName, rangeId, comparisonRangeId) {
- const filter = Object.values(this.state.filters).find(f => f.type === 'timeRange');
- const detail = { fieldName, rangeId };
- if (comparisonRangeId) {
- detail.comparisonRangeId = comparisonRangeId;
- }
- const queryElem = this.state.query.find(queryElem => queryElem.filterId === filter.id);
- if (queryElem) {
- Object.assign(queryElem, detail);
- if (!comparisonRangeId) {
- delete queryElem.comparisonRangeId;
- }
- } else {
- this.state.query.push(Object.assign({ groupId: filter.groupId, filterId: filter.id }, detail));
- }
- }
-
- /**
* Remove all the query elements from query.
*/
clearQuery() {
@@ -311,6 +299,7 @@ odoo.define('web.ControlPanelModel', function (require) {
this.state.query = this.state.query.filter(
queryElem => queryElem.groupId !== groupId
);
+ this._checkComparisonStatus();
}
/**
@@ -347,7 +336,7 @@ odoo.define('web.ControlPanelModel', function (require) {
*/
getFacets() {
const accept = type => {
- if (type === 'groupBy' && !this.searchMenuTypes.includes('groupBy')) {
+ if (['groupBy', 'comparison'].includes(type) && !this.searchMenuTypes.includes(type)) {
return false;
}
return true;
@@ -384,11 +373,13 @@ odoo.define('web.ControlPanelModel', function (require) {
const filters = Object.values(this.state.filters).reduce(
(filters, filter) => {
if (filter.type === type && !filter.invisible) {
- const activities = this.state.query.filter(
+ const filterQueryElements = this.state.query.filter(
queryElem => queryElem.filterId === filter.id
);
- const enrichedFilter = this._enrichFilterCopy(filter, activities);
- filters.push(enrichedFilter);
+ const enrichedFilter = this._enrichFilterCopy(filter, filterQueryElements);
+ if (enrichedFilter) {
+ filters.push(enrichedFilter);
+ }
}
return filters;
},
@@ -419,7 +410,7 @@ odoo.define('web.ControlPanelModel', function (require) {
} else {
query.groupBy = [];
}
- if (this.searchMenuTypes.includes('timeRange')) {
+ if (this.searchMenuTypes.includes('comparison')) {
const timeRanges = this._getTimeRanges(requireEvaluation);
query.timeRanges = timeRanges || {};
}
@@ -442,6 +433,24 @@ odoo.define('web.ControlPanelModel', function (require) {
search() { }
/**
+ * Activate/Deactivate a filter of type 'comparison' with provided id.
+ * At most one filter of type 'comparison' can be activated at every time.
+ * @param {string} filterId
+ */
+ toggleComparison(filterId) {
+ const { groupId, dateFilterId } = this.state.filters[filterId];
+ const queryElem = this.state.query.find(queryElem =>
+ queryElem.type === 'comparison' &&
+ queryElem.filterId === filterId
+ );
+ // make sure only one comparison can be active
+ this.state.query = this.state.query.filter(queryElem => queryElem.type !== 'comparison');
+ if (!queryElem) {
+ this.state.query.push({ groupId, filterId, dateFilterId, type: 'comparison', });
+ }
+ }
+
+ /**
* Activate or deactivate the simple filter with given filterId, i.e.
* add or remove a corresponding query element.
* @param {string} filterId
@@ -471,22 +480,15 @@ odoo.define('web.ControlPanelModel', function (require) {
toggleFilterWithOptions(filterId, optionId) {
const filter = this.state.filters[filterId];
optionId = optionId || filter.defaultOptionId;
-
- const noYearSelected = (filterId) => !this.state.query.some(queryElem => {
- return queryElem.filterId === filterId && YEAR_OPTIONS[queryElem.optionId];
- });
-
- const defaultYearId = (optionId) => {
- const year = this.optionGenerators.find(o => o.id === optionId).defaultYear;
- return this.optionGenerators.find(o => o.setParam.year === year).id;
- };
+ const option = this.optionGenerators.find(o => o.id === optionId);
const index = this.state.query.findIndex(
queryElem => queryElem.filterId === filterId && queryElem.optionId === optionId
);
+
if (index >= 0) {
this.state.query.splice(index, 1);
- if (filter.type === 'filter' && noYearSelected(filterId)) {
+ if (filter.type === 'filter' && !yearSelected(this._getSelectedOptionIds(filterId))) {
// This is the case where optionId was the last option
// of type 'year' to be there before being removed above.
// Since other options of type 'month' or 'quarter' do
@@ -497,16 +499,19 @@ odoo.define('web.ControlPanelModel', function (require) {
}
} else {
this.state.query.push({ groupId: filter.groupId, filterId, optionId });
- if (filter.type === 'filter' && noYearSelected(filterId)) {
+ if (filter.type === 'filter' && !yearSelected(this._getSelectedOptionIds(filterId))) {
// Here we add 'this_year' as options if no option of type
// year is already selected.
this.state.query.push({
groupId: filter.groupId,
filterId,
- optionId: defaultYearId(optionId),
+ optionId: option.defaultYearId,
});
}
}
+ if (filter.type === 'filter') {
+ this._checkComparisonStatus();
+ }
}
/**
@@ -535,16 +540,6 @@ odoo.define('web.ControlPanelModel', function (require) {
//---------------------------------------------------------------------
/**
- * Activate the filter of type timeRange with the options set in the key
- * time_ranges of actionContext (if any).
- * @private
- */
- _activateDefaultTimeRanges() {
- const { field, range, comparisonRange } = this.actionContext.time_ranges;
- this.activateTimeRange(field, range, comparisonRange);
- }
-
- /**
* Activate the default favorite (if any) or all default filters.
* @private
*/
@@ -583,25 +578,109 @@ odoo.define('web.ControlPanelModel', function (require) {
}
});
}
- if (this.actionContext.time_ranges) {
- this._activateDefaultTimeRanges();
- }
}
/**
* This function populates the 'filters' object at initialization.
* The filters come from:
- * - config.viewInfo.arch (types 'filter', 'groupBy', 'field'),
+ * - config.viewInfo.arch (types 'comparison', 'filter', 'groupBy', 'field'),
* - config.dynamicFilters (type 'filter'),
* - config.viewInfo.favoriteFilters (type 'favorite'),
- * - code itself (type 'timeRange')
* @private
*/
_addFilters() {
this._createGroupOfFiltersFromArch();
this._createGroupOfDynamicFilters();
this._createGroupOfFavorites();
- this._createGroupOfTimeRanges();
+ }
+
+ /**
+ * If a comparison is active, check if it should become inactive.
+ * The comparison should become inactive if the corresponding date filter has become
+ * inactive.
+ * @private
+ */
+ _checkComparisonStatus() {
+ const activeComparison = this.activeComparison;
+ if (!activeComparison) {
+ return;
+ }
+ const { dateFilterId } = activeComparison;
+ const dateFilterIsActive = this.state.query.some(
+ queryElem => queryElem.filterId === dateFilterId
+ );
+ if (!dateFilterIsActive) {
+ this.state.query = this.state.query.filter(
+ queryElem => queryElem.type !== 'comparison'
+ );
+ }
+ }
+
+ /**
+ * Returns the active comparison timeRanges object.
+ * @private
+ * @param {Object} comparisonFilter
+ * @returns {Object | undefined}
+ */
+ _computeTimeRanges(comparisonFilter) {
+ const { filterId } = this.activeComparison;
+ if (filterId !== comparisonFilter.id) {
+ return;
+ }
+ const { dateFilterId, comparisonOptionId } = comparisonFilter;
+ const {
+ fieldName,
+ fieldType,
+ description: dateFilterDescription,
+ } = this.state.filters[dateFilterId];
+
+ const selectedOptionIds = this._getSelectedOptionIds(dateFilterId);
+
+ // compute range and range description
+ const { domain: range, description: rangeDescription } = constructDateDomain(
+ this.referenceMoment, fieldName, fieldType, selectedOptionIds,
+ );
+
+ // compute comparisonRange and comparisonRange description
+ const {
+ domain: comparisonRange,
+ description: comparisonRangeDescription,
+ } = constructDateDomain(
+ this.referenceMoment, fieldName, fieldType, selectedOptionIds, comparisonOptionId
+ );
+
+ return {
+ comparisonId: comparisonOptionId,
+ fieldName,
+ fieldDescription: dateFilterDescription,
+ range,
+ rangeDescription,
+ comparisonRange,
+ comparisonRangeDescription,
+ };
+ }
+
+ /**
+ * Starting from the array of date filters, create the filters of type
+ * 'comparison'.
+ * @private
+ * @param {Object[]} dateFilters
+ */
+ _createGroupOfComparisons(dateFilters) {
+ const preFilters = [];
+ for (const dateFilter of dateFilters) {
+ for (const comparisonOption of this.comparisonOptions) {
+ const { id: dateFilterId, description } = dateFilter;
+ const preFilter = {
+ type: 'comparison',
+ comparisonOptionId: comparisonOption.id,
+ description: `${description}: ${comparisonOption.description}`,
+ dateFilterId,
+ };
+ preFilters.push(preFilter);
+ }
+ }
+ this._createGroupOfFilters(preFilters);
}
/**
@@ -717,15 +796,12 @@ odoo.define('web.ControlPanelModel', function (require) {
if (pregroupOfGroupBys.length) {
this._createGroupOfFilters(pregroupOfGroupBys);
}
- }
-
- /**
- * Add a single filter of type 'timeRange' to filters.
- * @private
- */
- _createGroupOfTimeRanges() {
- const pregroup = [{ type: 'timeRange' }];
- this._createGroupOfFilters(pregroup);
+ const dateFilters = Object.values(this.state.filters).filter(
+ (filter) => filter.isDateFilter
+ );
+ if (dateFilters.length) {
+ this._createGroupOfComparisons(dateFilters);
+ }
}
/**
@@ -734,19 +810,21 @@ odoo.define('web.ControlPanelModel', function (require) {
* mechanism different from __notifyComponents. Here we use the fact
* that the controlPanelModel is an (owl) EventBus to communicate with
* the parent.
+ * @private
*/
_dispatch() {
this.trigger('search', this.getQuery());
}
/**
- * Returns a copy of the provided filter with additional information
+ * Returns undefined or a copy of the provided filter with additional information
* used only outside of the control panel model, like in search bar or in the
- * various menus.
+ * various menus. The value undefined is returned if the filter should not appear
+ * for some reason.
* @private
* @param {Object} filter
* @param {Object[]} filterQueryElements
- * @returns {Object}
+ * @returns {Object|undefined}
*/
_enrichFilterCopy(filter, filterQueryElements) {
const isActive = Boolean(filterQueryElements.length);
@@ -761,6 +839,16 @@ odoo.define('web.ControlPanelModel', function (require) {
}
switch (f.type) {
+ case 'comparison': {
+ const { dateFilterId } = filter;
+ const dateFilterIsActive = this.state.query.some(
+ queryElem => queryElem.filterId === dateFilterId
+ );
+ if (!dateFilterIsActive) {
+ return;
+ }
+ break;
+ }
case 'filter':
if (f.hasOptions) {
f.options = _enrichOptions(this.optionGenerators);
@@ -776,12 +864,6 @@ odoo.define('web.ControlPanelModel', function (require) {
({ label, value, operator }) => ({ label, value, operator })
);
break;
- case 'timeRange':
- if (filterQueryElements.length) {
- const timeRange = this._extractTimeRange(filterQueryElements[0]);
- Object.assign(f, timeRange);
- }
- break;
}
return f;
}
@@ -842,11 +924,11 @@ odoo.define('web.ControlPanelModel', function (require) {
filter.context = attrs.context;
}
if (attrs.date) {
+ filter.isDateFilter = true;
filter.hasOptions = true;
filter.fieldName = attrs.date;
filter.fieldType = this.fields[attrs.date].type;
filter.defaultOptionId = attrs.default_period || DEFAULT_PERIOD;
- filter.basicDomains = this._getDateFilterBasicDomains(filter);
} else {
filter.domain = attrs.domain || '[]';
}
@@ -865,7 +947,7 @@ odoo.define('web.ControlPanelModel', function (require) {
filter.defaultRank = attrs.defaultRank;
}
break;
- case 'field':
+ case 'field': {
const field = this.fields[attrs.name];
filter.fieldName = attrs.name;
filter.fieldType = field.type;
@@ -885,6 +967,7 @@ odoo.define('web.ControlPanelModel', function (require) {
filter.defaultAutocompleteValue = attrs.defaultAutocompleteValue;
}
break;
+ }
}
if (filter.fieldName && !attrs.string) {
const { string } = this.fields[filter.fieldName];
@@ -893,76 +976,6 @@ odoo.define('web.ControlPanelModel', function (require) {
}
/**
- * For filter of type 'filter' with hasOptions=true,
- * returns an array of domains or descriptions according to key.
- * @param {Object} filter
- * @param {Object[]} filterQueryElements
- * @param {'domain'|'description'} key
- * @returns {string[]}
- */
- _extractInfoFromBasicDomains(filter, filterQueryElements, key) {
- const results = [];
- const yearIds = [];
- const otherOptionIds = [];
- filterQueryElements.forEach(({ optionId }) => {
- if (YEAR_OPTIONS[optionId]) {
- yearIds.push(optionId);
- } else {
- otherOptionIds.push(optionId);
- }
- });
-
- const sortOptionIds = (a, b) => rankPeriod(a) - rankPeriod(b);
- yearIds.sort(sortOptionIds);
- otherOptionIds.sort(sortOptionIds);
-
- // the following case corresponds to years selected only
- if (otherOptionIds.length === 0) {
- yearIds.forEach(yearId => {
- const d = filter.basicDomains[yearId];
- results.push(d[key]);
- });
- } else {
- otherOptionIds.forEach(optionId => {
- yearIds.forEach(yearId => {
- const d = filter.basicDomains[`${yearId}__${optionId}`];
- results.push(d[key]);
- });
- });
- }
- return results;
- }
-
- /**
- * Construct a timeRange object from the given fieldName, rangeId, comparisonRangeId
- * parameters.
- * @private
- * @param {string} fieldName
- * @param {string} rangeId
- * @param {string} comparisonRangeId
- * @returns {Object}
- */
- _extractTimeRange({ fieldName, rangeId, comparisonRangeId }) {
- const field = this.fields[fieldName];
- const timeRange = {
- fieldName,
- fieldDescription: field.string || fieldName,
- rangeId,
- range: Domain.prototype.constructDomain(fieldName, rangeId, field.type),
- rangeDescription: TIME_RANGE_OPTIONS[rangeId].description.toString(),
- };
- if (comparisonRangeId) {
- timeRange.comparisonRangeId = comparisonRangeId;
- timeRange.comparisonRange = Domain.prototype.constructDomain(
- fieldName, rangeId, field.type, comparisonRangeId
- );
- const { description } = COMPARISON_TIME_RANGE_OPTIONS[comparisonRangeId];
- timeRange.comparisonRangeDescription = description.toString();
- }
- return timeRange;
- }
-
- /**
* Returns an object irFilter serving to create an ir_filte in db
* starting from a filter of type 'favorite'.
* @private
@@ -1003,13 +1016,8 @@ odoo.define('web.ControlPanelModel', function (require) {
if ('groupBys' in favorite) {
context.group_by = favorite.groupBys;
}
- if ('timeRanges' in favorite) {
- const { fieldName, rangeId, comparisonRangeId } = favorite.timeRanges;
- context.time_ranges = {
- field: fieldName,
- range: rangeId,
- comparisonRange: comparisonRangeId,
- };
+ if ('comparison' in favorite) {
+ context.comparison = favorite.comparison;
}
if (Object.keys(context).length) {
irFilter.context = context;
@@ -1078,56 +1086,21 @@ odoo.define('web.ControlPanelModel', function (require) {
}
/**
- * Construct an object containing domains based on this.referenceMoment and
- * the field associated with the provided date filter.
- * @private
- * @param {Object} filter
- * @returns {Object}
- */
- _getDateFilterBasicDomains({ fieldName, fieldType }) {
- const _constructBasicDomain = (y, o) => {
- const setParam = Object.assign({}, y.setParam, o ? o.setParam : {});
- const granularity = o ? o.granularity : y.granularity;
- const date = this.referenceMoment.clone().set(setParam);
- let leftBound = date.clone().startOf(granularity).locale('en');
- let rightBound = date.clone().endOf(granularity).locale('en');
- if (fieldType === 'date') {
- leftBound = leftBound.format('YYYY-MM-DD');
- rightBound = rightBound.format('YYYY-MM-DD');
- } else {
- leftBound = leftBound.utc().format('YYYY-MM-DD HH:mm:ss');
- rightBound = rightBound.utc().format('YYYY-MM-DD HH:mm:ss');
- }
- const domain = Domain.prototype.arrayToString([
- '&',
- [fieldName, '>=', leftBound],
- [fieldName, '<=', rightBound]
- ]);
- const description = o ? o.description + " " + y.description : y.description;
- return { domain, description };
- };
-
- const domains = {};
- this.optionGenerators.filter(y => y.groupNumber === 2).forEach(y => {
- domains[y.id] = _constructBasicDomain(y);
- this.optionGenerators.filter(y => y.groupNumber === 1).forEach(o => {
- domains[y.id + '__' + o.id] = _constructBasicDomain(y, o);
- });
- });
- return domains;
- }
-
- /**
- * Compute the string representation of the current domain associated to a date filter
- * starting from its corresponding query elements.
+ * Compute the string representation or the description of the current domain associated
+ * with a date filter starting from its corresponding query elements.
* @private
* @param {Object} filter
- * @param {Objec[]} filterQueryElements
+ * @param {Object[]} filterQueryElements
+ * @param {'domain'|'description'} [key='domain']
* @returns {string}
*/
- _getDateFilterDomain(filter, filterQueryElements) {
- const domains = this._extractInfoFromBasicDomains(filter, filterQueryElements, 'domain');
- return pyUtils.assembleDomains(domains, 'OR');
+ _getDateFilterDomain(filter, filterQueryElements, key = 'domain') {
+ const { fieldName, fieldType } = filter;
+ const selectedOptionIds = filterQueryElements.map(queryElem => queryElem.optionId);
+ const dateFilterRange = constructDateDomain(
+ this.referenceMoment, fieldName, fieldType, selectedOptionIds,
+ );
+ return dateFilterRange[key];
}
/**
@@ -1205,6 +1178,10 @@ odoo.define('web.ControlPanelModel', function (require) {
*/
_getFilterDomain(filter, filterQueryElements) {
if (filter.type === 'filter' && filter.hasOptions) {
+ const { dateFilterId } = this.activeComparison || {};
+ if (this.searchMenuTypes.includes('comparison') && dateFilterId === filter.id) {
+ return "[]";
+ }
return this._getDateFilterDomain(filter, filterQueryElements);
} else if (filter.type === 'field') {
return this._getAutoCompletionFilterDomain(filter, filterQueryElements);
@@ -1307,7 +1284,7 @@ odoo.define('web.ControlPanelModel', function (require) {
/**
* Reconstruct the (active) groups from the query elements.
* @private
- * @returns {Objec[]}
+ * @returns {Object[]}
*/
_getGroups() {
const groups = this.state.query.reduce(
@@ -1356,37 +1333,20 @@ odoo.define('web.ControlPanelModel', function (require) {
}
/**
- * Returns the last timeRanges object found in the query.
- * TimeRanges objects can be associated with filters of type 'favorite'
- * or 'timeRange'.
+ * Starting from the id of a date filter, returns the array of option ids currently selected
+ * for the corresponding filter.
* @private
- * @param {boolean} [evaluation=false]
- * @returns {(Object|undefined)}
+ * @param {string} dateFilterId
+ * @returns {string[]}
*/
- _getTimeRanges(evaluation = false) {
- let timeRanges;
- for (const queryElem of this.state.query.slice().reverse()) {
- const filter = this.state.filters[queryElem.filterId];
- if (filter.type === 'timeRange') {
- timeRanges = this._extractTimeRange(queryElem);
- break;
- } else if (filter.type === 'favorite' && filter.timeRanges) {
- // we want to make sure that last is not observed! (it is change below in case of evaluation)
- timeRanges = this._extractTimeRange(filter.timeRanges);
- break;
- }
- }
- if (timeRanges) {
- if (evaluation) {
- timeRanges.range = Domain.prototype.stringToArray(timeRanges.range);
- if (timeRanges.comparisonRangeId) {
- timeRanges.comparisonRange = Domain.prototype.stringToArray(
- timeRanges.comparisonRange
- );
- }
+ _getSelectedOptionIds(dateFilterId) {
+ const selectedOptionIds = [];
+ for (const queryElem of this.state.query) {
+ if (queryElem.filterId === dateFilterId) {
+ selectedOptionIds.push(queryElem.optionId);
}
- return timeRanges;
}
+ return selectedOptionIds;
}
/**
@@ -1407,15 +1367,10 @@ odoo.define('web.ControlPanelModel', function (require) {
groupBys = context.group_by;
delete context.group_by;
}
- let timeRanges;
- if (context.time_ranges) {
- const { field, range, comparisonRange } = context.time_ranges;
- timeRanges = this._extractTimeRange({
- fieldName: field,
- rangeId: range,
- comparisonRangeId: comparisonRange,
- });
- delete context.time_ranges;
+ let comparison;
+ if (context.comparison) {
+ comparison = context.comparison;
+ delete context.comparison;
}
let sort;
try {
@@ -1460,8 +1415,8 @@ odoo.define('web.ControlPanelModel', function (require) {
if (irFilter.is_default) {
favorite.isDefault = irFilter.is_default;
}
- if (timeRanges) {
- favorite.timeRanges = timeRanges;
+ if (comparison) {
+ favorite.comparison = comparison;
}
return favorite;
}
@@ -1470,6 +1425,7 @@ odoo.define('web.ControlPanelModel', function (require) {
* Group the query elements in group.activities by qe -> qe.filterId
* and changes the form of group.activities to make it more suitable for further
* computations.
+ * @private
* @param {Object} group
*/
_mergeActivities(group) {
@@ -1477,7 +1433,7 @@ odoo.define('web.ControlPanelModel', function (require) {
let res = [];
switch (type) {
case 'filter':
- case 'groupBy':
+ case 'groupBy': {
for (const activity of activities) {
const { filterId } = activity;
let a = res.find(({ filter }) => filter.id === filterId);
@@ -1491,9 +1447,10 @@ odoo.define('web.ControlPanelModel', function (require) {
a.filterQueryElements.push(activity);
}
break;
+ }
case 'favorite':
case 'field':
- case 'timeRange':
+ case 'comparison': {
// all activities in the group have same filterId
const { filterId } = group.activities[0];
const filter = this.state.filters[filterId];
@@ -1502,6 +1459,7 @@ odoo.define('web.ControlPanelModel', function (require) {
filterQueryElements: group.activities
});
break;
+ }
}
if (type === 'groupBy') {
res.forEach(activity => {
@@ -1608,7 +1566,7 @@ odoo.define('web.ControlPanelModel', function (require) {
userId,
});
if (timeRanges) {
- preFilter.timeRanges = timeRanges;
+ preFilter.comparison = timeRanges;
}
const irFilter = this._favoriteToIrFilter(preFilter);
const serverSideId = await this.env.dataManager.create_filter(irFilter);
@@ -1648,20 +1606,13 @@ odoo.define('web.ControlPanelModel', function (require) {
} else {
let facetDescription;
for (const { filter, filterQueryElements } of activities) {
- if (type === 'timeRange') {
- const tr = this._extractTimeRange(filterQueryElements[0]);
- facetDescription = `${tr.fieldDescription}: ${tr.rangeDescription}`;
- if (tr.comparisonRangeDescription) {
- facetDescription += ` / ${tr.comparisonRangeDescription}`;
- }
- } else { // filter and favorite
- facetDescription = filter.description;
- if (filter.hasOptions) {
- const descriptions = this._extractInfoFromBasicDomains(
- filter, filterQueryElements, 'description'
- );
- facetDescription += `: ${descriptions.join(" / ")}`;
- }
+ // filter, favorite and comparison
+ facetDescription = filter.description;
+ if (filter.isDateFilter) {
+ const description = this._getDateFilterDomain(
+ filter, filterQueryElements, 'description'
+ );
+ facetDescription += `: ${description}`;
}
facetDescriptions.push(facetDescription);
}
@@ -1670,6 +1621,35 @@ odoo.define('web.ControlPanelModel', function (require) {
}
/**
+ * Returns the last timeRanges object found in the query.
+ * TimeRanges objects can be associated with filters of type 'favorite'
+ * or 'comparison'.
+ * @private
+ * @param {boolean} [evaluation=false]
+ * @returns {(Object|undefined)}
+ */
+ _getTimeRanges(evaluation) {
+ let timeRanges;
+ for (const queryElem of this.state.query.slice().reverse()) {
+ const filter = this.state.filters[queryElem.filterId];
+ if (filter.type === 'comparison') {
+ timeRanges = this._computeTimeRanges(filter);
+ break;
+ } else if (filter.type === 'favorite' && filter.comparison) {
+ timeRanges = filter.comparison;
+ break;
+ }
+ }
+ if (timeRanges) {
+ if (evaluation) {
+ timeRanges.range = Domain.prototype.stringToArray(timeRanges.range);
+ timeRanges.comparisonRange = Domain.prototype.stringToArray(timeRanges.comparisonRange);
+ }
+ return timeRanges;
+ }
+ }
+
+ /**
* Using the constructor parameter object config, set most of the properties
* of the control panel model.
* @private
@@ -1713,26 +1693,9 @@ odoo.define('web.ControlPanelModel', function (require) {
this.dynamicFilters = config.dynamicFilters || [];
this.referenceMoment = moment();
- const setDescriptions = options => {
- return Object.values(options).map(o => {
- const oClone = JSON.parse(JSON.stringify(o));
- const description = o.description ?
- o.description.toString() :
- this.referenceMoment.clone().add(o.addParam).format(o.format);
- return Object.assign(oClone, { description });
- });
- };
- const process = (options) => {
- return options.map(o => {
- const date = this.referenceMoment.clone().set(o.setParam).add(o.addParam);
- delete o.addParam;
- o.setParam[o.granularity] = date[o.granularity]();
- o.defaultYear = date.year();
- return o;
- });
- };
- this.optionGenerators = process(setDescriptions(OPTION_GENERATORS));
- this.intervalOptions = setDescriptions(INTERVAL_OPTIONS);
+ this.optionGenerators = getPeriodOptions(this.referenceMoment);
+ this.intervalOptions = getIntervalOptions();
+ this.comparisonOptions = getComparisonOptions();
}
}
diff --git a/addons/web/static/src/js/control_panel/search_utils.js b/addons/web/static/src/js/control_panel/search_utils.js
index 3a605faa2b8..cd4ae869651 100644
--- a/addons/web/static/src/js/control_panel/search_utils.js
+++ b/addons/web/static/src/js/control_panel/search_utils.js
@@ -1,7 +1,13 @@
odoo.define('web.searchUtils', function (require) {
"use strict";
- const { _lt } = require('web.core');
+ const { _lt, _t } = require('web.core');
+ const Domain = require('web.Domain');
+ const pyUtils = require('web.py_utils');
+
+ //-------------------------------------------------------------------------
+ // Constants
+ //-------------------------------------------------------------------------
// Filter menu parameters
const FIELD_OPERATORS = {
@@ -10,7 +16,7 @@ odoo.define('web.searchUtils', function (require) {
{ symbol: "!=", description: _lt("is false"), value: true },
],
char: [
- { symbol: "ilike", description:_lt("contains") },
+ { symbol: "ilike", description: _lt("contains") },
{ symbol: "not ilike", description: _lt("doesn't contain") },
{ symbol: "=", description: _lt("is equal to") },
{ symbol: "!=", description: _lt("is not equal to") },
@@ -40,7 +46,7 @@ odoo.define('web.searchUtils', function (require) {
{ symbol: "=", description: _lt("is not set"), value: false },
],
id: [
- { symbol: "=", description:_lt("is")},
+ { symbol: "=", description: _lt("is") },
],
number: [
{ symbol: "=", description: _lt("is equal to") },
@@ -53,7 +59,7 @@ odoo.define('web.searchUtils', function (require) {
{ symbol: "=", description: _lt("is not set"), value: false },
],
selection: [
- { symbol: "=", description:_lt("is")},
+ { symbol: "=", description: _lt("is") },
{ symbol: "!=", description: _lt("is not") },
{ symbol: "!=", description: _lt("is set"), value: false },
{ symbol: "=", description: _lt("is not set"), value: false },
@@ -76,30 +82,70 @@ odoo.define('web.searchUtils', function (require) {
selection: 'selection',
};
const DEFAULT_PERIOD = 'this_month';
+ const QUARTERS = {
+ 1: { description: _lt("Q1"), coveredMonths: [0, 1, 2] },
+ 2: { description: _lt("Q2"), coveredMonths: [3, 4, 5] },
+ 3: { description: _lt("Q3"), coveredMonths: [6, 7, 8] },
+ 4: { description: _lt("Q4"), coveredMonths: [9, 10, 11] },
+ };
const MONTH_OPTIONS = {
- this_month: { id: 'this_month', groupNumber: 1, format: 'MMMM', addParam: {}, setParam: {}, granularity: 'month' },
- last_month: { id: 'last_month', groupNumber: 1, format: 'MMMM', addParam: { months: -1 }, setParam: {}, granularity: 'month' },
- antepenultimate_month: { id: 'antepenultimate_month', groupNumber: 1, format: 'MMMM', addParam: { months: -2 }, setParam: {}, granularity: 'month' },
+ this_month: {
+ id: 'this_month', groupNumber: 1, format: 'MMMM',
+ addParam: {}, granularity: 'month',
+ },
+ last_month: {
+ id: 'last_month', groupNumber: 1, format: 'MMMM',
+ addParam: { months: -1 }, granularity: 'month',
+ },
+ antepenultimate_month: {
+ id: 'antepenultimate_month', groupNumber: 1, format: 'MMMM',
+ addParam: { months: -2 }, granularity: 'month',
+ },
};
const QUARTER_OPTIONS = {
- fourth_quarter: { id: 'fourth_quarter', groupNumber: 1, description: _lt("Q4"), addParam: {}, setParam: { quarter: 4 }, granularity: 'quarter' },
- third_quarter: { id: 'third_quarter', groupNumber: 1, description: _lt("Q3"), addParam: {}, setParam: { quarter: 3 }, granularity: 'quarter' },
- second_quarter: { id: 'second_quarter', groupNumber: 1, description: _lt("Q2"), addParam: {}, setParam: { quarter: 2 }, granularity: 'quarter' },
- first_quarter: { id: 'first_quarter', groupNumber: 1, description: _lt("Q1"), addParam: {}, setParam: { quarter: 1 }, granularity: 'quarter' },
+ fourth_quarter: {
+ id: 'fourth_quarter', groupNumber: 1, description: QUARTERS[4].description,
+ setParam: { quarter: 4 }, granularity: 'quarter',
+ },
+ third_quarter: {
+ id: 'third_quarter', groupNumber: 1, description: QUARTERS[3].description,
+ setParam: { quarter: 3 }, granularity: 'quarter',
+ },
+ second_quarter: {
+ id: 'second_quarter', groupNumber: 1, description: QUARTERS[2].description,
+ setParam: { quarter: 2 }, granularity: 'quarter',
+ },
+ first_quarter: {
+ id: 'first_quarter', groupNumber: 1, description: QUARTERS[1].description,
+ setParam: { quarter: 1 }, granularity: 'quarter',
+ },
};
const YEAR_OPTIONS = {
- this_year: { id: 'this_year', groupNumber: 2, format: 'YYYY', addParam: {}, setParam: {}, granularity: 'year' },
- last_year: { id: 'last_year', groupNumber: 2, format: 'YYYY', addParam: { years: -1 }, setParam: {}, granularity: 'year' },
- antepenultimate_year: { id: 'antepenultimate_year', groupNumber: 2, format: 'YYYY', addParam: { years: -2 }, setParam: {}, granularity: 'year' },
+ this_year: {
+ id: 'this_year', groupNumber: 2, format: 'YYYY',
+ addParam: {}, granularity: 'year',
+ },
+ last_year: {
+ id: 'last_year', groupNumber: 2, format: 'YYYY',
+ addParam: { years: -1 }, granularity: 'year',
+ },
+ antepenultimate_year: {
+ id: 'antepenultimate_year', groupNumber: 2, format: 'YYYY',
+ addParam: { years: -2 }, granularity: 'year',
+ },
};
- const OPTION_GENERATORS = Object.assign({}, MONTH_OPTIONS, QUARTER_OPTIONS, YEAR_OPTIONS);
-
- function rankPeriod(oId) {
- return Object.keys(OPTION_GENERATORS).indexOf(oId);
- }
+ const PERIOD_OPTIONS = Object.assign({}, MONTH_OPTIONS, QUARTER_OPTIONS, YEAR_OPTIONS);
// GroupBy menu parameters
- const GROUPABLE_TYPES = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime', 'integer'];
+ const GROUPABLE_TYPES = [
+ 'boolean',
+ 'char',
+ 'date',
+ 'datetime',
+ 'integer',
+ 'many2one',
+ 'selection',
+ ];
const DEFAULT_INTERVAL = 'month';
const INTERVAL_OPTIONS = {
year: { description: _lt("Year"), id: 'year', groupNumber: 1 },
@@ -108,42 +154,373 @@ odoo.define('web.searchUtils', function (require) {
week: { description: _lt("Week"), id: 'week', groupNumber: 1 },
day: { description: _lt("Day"), id: 'day', groupNumber: 1 }
};
- function rankInterval(oId) {
- return Object.keys(INTERVAL_OPTIONS).indexOf(oId);
- }
- // TimeRange menu parameters
- const TIME_RANGE_OPTIONS = {
- last_7_days: { description: _lt("Last 7 Days"), id: 'last_7_days', groupNumber: 1 },
- last_30_days: { description: _lt("Last 30 Days"), id: 'last_30_days', groupNumber: 1 },
- last_365_days: { description: _lt("Last 365 Days"), id: 'last_365_days', groupNumber: 1 },
- last_5_years: { description: _lt("Last 5 Years"), id: 'last_5_years', groupNumber: 1 },
- today: { description: _lt("Today"), id: 'today', groupNumber: 2 },
- this_week: { description: _lt("This Week"), id: 'this_week', groupNumber: 2 },
- this_month: { description: _lt("This Month"), id: 'this_month', groupNumber: 2 },
- this_quarter: { description: _lt("This Quarter"), id: 'this_quarter', groupNumber: 2 },
- this_year: { description: _lt("This Year"), id: 'this_year', groupNumber: 2 },
- yesterday: { description: _lt("Yesterday"), id: 'yesterday', groupNumber: 3 },
- last_week: { description: _lt("Last Week"), id: 'last_week', groupNumber: 3 },
- last_month: { description: _lt("Last Month"), id: 'last_month', groupNumber: 3 },
- last_quarter: { description: _lt("Last Quarter"), id: 'last_quarter', groupNumber: 3 },
- last_year: { description: _lt("Last Year"), id: 'last_year', groupNumber: 3 },
+ // Comparison menu parameters
+ const COMPARISON_OPTIONS = {
+ previous_period: {
+ description: _lt("Previous Period"), id: 'previous_period',
+ },
+ previous_year: {
+ description: _lt("Previous Year"), id: 'previous_year', addParam: { years: -1 },
+ },
};
- const COMPARISON_TIME_RANGE_OPTIONS = {
- previous_period: { description: _lt("Previous Period"), id: 'previous_period' },
- previous_year: { description: _lt("Previous Year"), id: 'previous_year' },
+ const PER_YEAR = {
+ year: 1,
+ quarter: 4,
+ month: 12,
};
-
// Search bar
const FACET_ICONS = {
filter: 'fa fa-filter',
groupBy: 'fa fa-bars',
favorite: 'fa fa-star',
- timeRange: 'fa fa-calendar',
+ comparison: 'fa fa-adjust',
};
+ //-------------------------------------------------------------------------
+ // Functions
+ //-------------------------------------------------------------------------
+
+ /**
+ * Constructs the string representation of a domain and its description. The
+ * domain is of the form:
+ * ['|',..., '|', d_1,..., d_n]
+ * where d_i is a time range of the form
+ * ['&', [fieldName, >=, leftBound_i], [fieldName, <=, rightBound_i]]
+ * where leftBound_i and rightBound_i are date or datetime computed accordingly
+ * to the given options and reference moment.
+ * (@see constructDateRange).
+ * @param {moment} referenceMoment
+ * @param {string} fieldName
+ * @param {string} fieldType
+ * @param {string[]} selectedOptionIds
+ * @param {string} [comparisonOptionId]
+ * @returns {{ domain: string, description: string }}
+ */
+ function constructDateDomain(
+ referenceMoment,
+ fieldName,
+ fieldType,
+ selectedOptionIds,
+ comparisonOptionId
+ ) {
+ let addParam;
+ let selectedOptions;
+ if (comparisonOptionId) {
+ [addParam, selectedOptions] = getComparisonParams(
+ referenceMoment,
+ selectedOptionIds,
+ comparisonOptionId);
+ } else {
+ selectedOptions = getSelectedOptions(referenceMoment, selectedOptionIds);
+ }
+
+ const yearOptions = selectedOptions.year;
+ const otherOptions = [
+ ...(selectedOptions.quarter || []),
+ ...(selectedOptions.month || [])
+ ];
+
+ sortPeriodOptions(yearOptions);
+ sortPeriodOptions(otherOptions);
+
+ const ranges = [];
+ for (const yearOption of yearOptions) {
+ const constructRangeParams = {
+ referenceMoment,
+ fieldName,
+ fieldType,
+ addParam,
+ };
+ if (otherOptions.length) {
+ for (const option of otherOptions) {
+ const setParam = Object.assign({},
+ yearOption.setParam,
+ option ? option.setParam : {}
+ );
+ const { granularity } = option;
+ const range = constructDateRange(Object.assign(
+ { granularity, setParam },
+ constructRangeParams
+ ));
+ ranges.push(range);
+ }
+ } else {
+ const { granularity, setParam } = yearOption;
+ const range = constructDateRange(Object.assign(
+ { granularity, setParam },
+ constructRangeParams
+ ));
+ ranges.push(range);
+ }
+ }
+
+ const domain = pyUtils.assembleDomains(ranges.map(range => range.domain), 'OR');
+ const description = ranges.map(range => range.description).join("/");
+
+ return { domain, description };
+ }
+
+ /**
+ * Constructs the string representation of a domain and its description. The
+ * domain is a time range of the form:
+ * ['&', [fieldName, >=, leftBound],[fieldName, <=, rightBound]]
+ * where leftBound and rightBound are some date or datetime determined by setParam,
+ * addParam, granularity and the reference moment.
+ * @param {Object} params
+ * @param {moment} params.referenceMoment
+ * @param {string} params.fieldName
+ * @param {string} params.fieldType
+ * @param {string} params.granularity
+ * @param {Object} params.setParam
+ * @param {Object} [params.addParam]
+ * @returns {{ domain: string, description: string }}
+ */
+ function constructDateRange({
+ referenceMoment,
+ fieldName,
+ fieldType,
+ granularity,
+ setParam,
+ addParam,
+ }) {
+ const date = referenceMoment.clone().set(setParam).add(addParam || {});
+
+ // compute domain
+ let leftBound = date.clone().startOf(granularity);
+ let rightBound = date.clone().endOf(granularity);
+ if (fieldType === 'date') {
+ leftBound = leftBound.format('YYYY-MM-DD');
+ rightBound = rightBound.format('YYYY-MM-DD');
+ } else {
+ leftBound = leftBound.utc().format('YYYY-MM-DD HH:mm:ss');
+ rightBound = rightBound.utc().format('YYYY-MM-DD HH:mm:ss');
+ }
+ const domain = Domain.prototype.arrayToString([
+ '&',
+ [fieldName, '>=', leftBound],
+ [fieldName, '<=', rightBound]
+ ]);
+
+ // compute description
+ const descriptions = [date.format("YYYY")];
+ const method = _t.database.parameters.direction === "rtl" ? "push" : "unshift";
+ if (granularity === "month") {
+ descriptions[method](date.format("MMMM"));
+ } else if (granularity === "quarter") {
+ descriptions[method](QUARTERS[date.quarter()].description);
+ }
+ const description = descriptions.join(" ");
+
+ return { domain, description, };
+ }
+
+ /**
+ * Returns a version of the options in COMPARISON_OPTIONS with translated descriptions.
+ * @see getOptionsWithDescriptions
+ */
+ function getComparisonOptions() {
+ return getOptionsWithDescriptions(COMPARISON_OPTIONS);
+ }
+
+ /**
+ * Returns the params addParam and selectedOptions necessary for the computation
+ * of a comparison domain.
+ * @param {moment} referenceMoment
+ * @param {string{}} selectedOptionIds
+ * @param {string} comparisonOptionId
+ * @returns {Object[]}
+ */
+ function getComparisonParams(referenceMoment, selectedOptionIds, comparisonOptionId) {
+ const comparisonOption = COMPARISON_OPTIONS[comparisonOptionId];
+ const selectedOptions = getSelectedOptions(referenceMoment, selectedOptionIds);
+ let addParam = comparisonOption.addParam;
+ if (addParam) {
+ return [addParam, selectedOptions];
+ }
+ addParam = {};
+
+ let globalGranularity = 'year';
+ if (selectedOptions.month) {
+ globalGranularity = 'month';
+ } else if (selectedOptions.quarter) {
+ globalGranularity = 'quarter';
+ }
+ const granularityFactor = PER_YEAR[globalGranularity];
+ const years = selectedOptions.year.map(o => o.setParam.year);
+ const yearMin = Math.min(...years);
+ const yearMax = Math.max(...years);
+
+ let optionMin = 0;
+ let optionMax = 0;
+ if (selectedOptions.quarter) {
+ const quarters = selectedOptions.quarter.map(o => o.setParam.quarter);
+ if (globalGranularity === 'month') {
+ delete selectedOptions.quarter;
+ for (const quarter of quarters) {
+ for (const month of QUARTERS[quarter].coveredMonths) {
+ const monthOption = selectedOptions.month.find(
+ o => o.setParam.month === month
+ );
+ if (!monthOption) {
+ selectedOptions.month.push({
+ setParam: { month, }, granularity: 'month',
+ });
+ }
+ }
+ }
+ } else {
+ optionMin = Math.min(...quarters);
+ optionMax = Math.max(...quarters);
+ }
+ }
+ if (selectedOptions.month) {
+ const months = selectedOptions.month.map(o => o.setParam.month);
+ optionMin = Math.min(...months);
+ optionMax = Math.max(...months);
+ }
+
+ addParam[globalGranularity] = -1 +
+ granularityFactor * (yearMin - yearMax) +
+ optionMin - optionMax;
+
+ return [addParam, selectedOptions];
+ }
+
+ /**
+ * Returns a version of the options in INTERVAL_OPTIONS with translated descriptions.
+ * @see getOptionsWithDescriptions
+ */
+ function getIntervalOptions() {
+ return getOptionsWithDescriptions(INTERVAL_OPTIONS);
+ }
+
+ /**
+ * Returns a version of the options in PERIOD_OPTIONS with translated descriptions
+ * and a key defautlYearId used in the control panel model when toggling a period option.
+ * @param {moment} referenceMoment
+ * @returns {Object[]}
+ */
+ function getPeriodOptions(referenceMoment) {
+ const options = [];
+ for (const option of Object.values(PERIOD_OPTIONS)) {
+ const { id, groupNumber, description, } = option;
+ const res = { id, groupNumber, };
+ const date = referenceMoment.clone().set(option.setParam).add(option.addParam);
+ if (description) {
+ res.description = description.toString();
+ } else {
+ res.description = date.format(option.format.toString());
+ }
+ res.setParam = getSetParam(option, referenceMoment);
+ res.defaultYear = date.year();
+ options.push(res);
+ }
+ for (const option of options) {
+ const yearOption = options.find(
+ o => o.setParam && o.setParam.year === option.defaultYear
+ );
+ option.defaultYearId = yearOption.id;
+ delete option.defaultYear;
+ delete option.setParam;
+ }
+ return options;
+ }
+
+ /**
+ * Returns a version of the options in OPTIONS with translated descriptions (if any).
+ * @param {Object{}} OPTIONS
+ * @returns {Object[]}
+ */
+ function getOptionsWithDescriptions(OPTIONS) {
+ const options = [];
+ for (const option of Object.values(OPTIONS)) {
+ const { id, groupNumber, description, } = option;
+ const res = { id, };
+ if (description) {
+ res.description = description.toString();
+ }
+ if (groupNumber) {
+ res.groupNumber = groupNumber;
+ }
+ options.push(res);
+ }
+ return options;
+ }
+
+ /**
+ * Returns a version of the period options whose ids are in selectedOptionIds
+ * partitioned by granularity.
+ * @param {moment} referenceMoment
+ * @param {string[]} selectedOptionIds
+ * @param {Object}
+ */
+ function getSelectedOptions(referenceMoment, selectedOptionIds) {
+ const selectedOptions = { year: [] };
+ for (const optionId of selectedOptionIds) {
+ const option = PERIOD_OPTIONS[optionId];
+ const setParam = getSetParam(option, referenceMoment);
+ const granularity = option.granularity;
+ if (!selectedOptions[granularity]) {
+ selectedOptions[granularity] = [];
+ }
+ selectedOptions[granularity].push({ granularity, setParam });
+ }
+ return selectedOptions;
+ }
+
+ /**
+ * Returns the setParam object associated with the given periodOption and
+ * referenceMoment.
+ * @param {Object} periodOption
+ * @param {moment} referenceMoment
+ * @returns {Object}
+ */
+ function getSetParam(periodOption, referenceMoment) {
+ if (periodOption.setParam) {
+ return periodOption.setParam;
+ }
+ const date = referenceMoment.clone().add(periodOption.addParam);
+ const setParam = {};
+ setParam[periodOption.granularity] = date[periodOption.granularity]();
+ return setParam;
+ }
+
+ /**
+ * @param {string} intervalOptionId
+ * @returns {number} index
+ */
+ function rankInterval(intervalOptionId) {
+ return Object.keys(INTERVAL_OPTIONS).indexOf(intervalOptionId);
+ }
+
+ /**
+ * Sorts in place an array of 'period' options.
+ * @param {Object[]} options supposed to be of the form:
+ * { granularity, setParam, }
+ */
+ function sortPeriodOptions(options) {
+ options.sort((o1, o2) => {
+ const granularity1 = o1.granularity;
+ const granularity2 = o2.granularity;
+ if (granularity1 === granularity2) {
+ return o1.setParam[granularity1] - o2.setParam[granularity1];
+ }
+ return granularity1 < granularity2 ? -1 : 1;
+ });
+ }
+
+ /**
+ * Checks if a year id is among the given array of period option ids.
+ * @param {string[]} selectedOptionIds
+ * @returns {boolean}
+ */
+ function yearSelected(selectedOptionIds) {
+ return selectedOptionIds.some(optionId => !!YEAR_OPTIONS[optionId]);
+ }
+
return {
- COMPARISON_TIME_RANGE_OPTIONS,
+ COMPARISON_OPTIONS,
DEFAULT_INTERVAL,
DEFAULT_PERIOD,
FACET_ICONS,
@@ -151,11 +528,13 @@ odoo.define('web.searchUtils', function (require) {
FIELD_TYPES,
GROUPABLE_TYPES,
INTERVAL_OPTIONS,
- OPTION_GENERATORS,
- TIME_RANGE_OPTIONS,
- YEAR_OPTIONS,
+ PERIOD_OPTIONS,
+ constructDateDomain,
+ getComparisonOptions,
+ getIntervalOptions,
+ getPeriodOptions,
rankInterval,
- rankPeriod,
+ yearSelected,
};
});
diff --git a/addons/web/static/src/js/control_panel/time_range_menu.js b/addons/web/static/src/js/control_panel/time_range_menu.js
deleted file mode 100644
index b18b376d40b..00000000000
--- a/addons/web/static/src/js/control_panel/time_range_menu.js
+++ /dev/null
@@ -1,109 +0,0 @@
-odoo.define('web.TimeRangeMenu', function (require) {
- "use strict";
-
- const { COMPARISON_TIME_RANGE_OPTIONS, DEFAULT_PERIOD, TIME_RANGE_OPTIONS } = require('web.searchUtils');
- const DropdownMenu = require('web.DropdownMenu');
- const { useModel } = require('web.model');
-
- // Used to provide unique ids to its template elements.
- let timeRangeMenuId = 0;
-
- /**
- * 'Time ranges' menu
- *
- * Component used to create a time range from a field, a given range and optionally
- * another field to make a comparison.
- *
- * The component template overrides the dropdownmenu to keep the basic behaviours
- * (opening/closing, layout). The template itself iis a set of labels/inputs
- * used to select the field and range. There is also a checkbox used to determine
- * whether to render the comparison range field selection (input).
- * @extends DropdownMenu
- */
- class TimeRangeMenu extends DropdownMenu {
- constructor() {
- super(...arguments);
- this.model = useModel('controlPanelModel');
-
- this.domId = timeRangeMenuId++;
- this.fields = Object.values(this.props.fields)
- .filter(field => this._validateField(field));
- this.periodOptions = TIME_RANGE_OPTIONS;
- this.comparisonTimeRangeOptions = COMPARISON_TIME_RANGE_OPTIONS;
- this.periodGroups = Object.values(this.periodOptions).reduce((acc, o) => {
- if (!acc.includes(o.groupNumber)) {
- acc.push(o.groupNumber);
- }
- return acc;
- }, []);
- const activeTimeRange = this.model.getFiltersOfType('timeRange').find(
- timeRange => timeRange.isActive
- );
- Object.assign(this.state, {
- fieldName: this.fields[0] && this.fields[0].name,
- rangeId: DEFAULT_PERIOD,
- }, activeTimeRange);
- }
-
- //---------------------------------------------------------------------
- // Getters
- //---------------------------------------------------------------------
-
- /**
- * @override
- */
- get title() {
- return this.env._t("Time Ranges");
- }
-
- //---------------------------------------------------------------------
- // Private
- //---------------------------------------------------------------------
-
- /**
- * @private
- * @param {Object} field
- * @returns {boolean}
- */
- _validateField(field) {
- return field.sortable &&
- ['date', 'datetime'].includes(field.type);
- }
-
- //---------------------------------------------------------------------
- // Handlers
- //---------------------------------------------------------------------
-
- /**
- * @private
- */
- _onApply() {
- this.model.dispatch('activateTimeRange',
- this.state.fieldName,
- this.state.rangeId,
- this.state.comparisonRangeId
- );
- }
-
- /**
- * @private
- */
- _onCheckboxClick() {
- if (!this.state.comparisonRangeId) {
- this.state.comparisonRangeId = 'previous_period'; // default
- } else {
- delete this.state.comparisonRangeId;
- }
- }
- }
-
- TimeRangeMenu.defaultProps = Object.assign({}, DropdownMenu.defaultProps, {
- icon: 'fa fa-calendar',
- });
- TimeRangeMenu.props = Object.assign({}, DropdownMenu.props, {
- fields: Object,
- });
- TimeRangeMenu.template = 'web.TimeRangeMenu';
-
- return TimeRangeMenu;
-});