summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js')
-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
-rw-r--r--addons/web/static/src/js/core/domain.js126
-rw-r--r--addons/web/static/src/js/views/graph/graph_model.js11
-rw-r--r--addons/web/static/src/js/views/graph/graph_view.js2
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_view.js2
9 files changed, 759 insertions, 590 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;
-});
diff --git a/addons/web/static/src/js/core/domain.js b/addons/web/static/src/js/core/domain.js
index 07d785b9292..ad0cfba9ae4 100644
--- a/addons/web/static/src/js/core/domain.js
+++ b/addons/web/static/src/js/core/domain.js
@@ -244,132 +244,6 @@ var Domain = collections.Tree.extend({
.replace(/false/g, "False")
.replace(/true/g, "True");
},
- /*
- * @param {string} fieldName
- * @param {string} period
- * @param {string} type ('date' or 'datetime')
- * @param {string} comparisonPeriod
- * @returns {string} a domain in string form
- */
- constructDomain: function (fieldName, period, type, comparisonPeriod) {
- var leftBoundaryParams, rightBoundaryParams;
- var offsetPeriodParams;
- function makeInterval () {
- switch (comparisonPeriod) {
- case 'previous_period':
- _.each(offsetPeriodParams, function (value, key) {
- if (!leftBoundaryParams[key] ||_.isNumber(leftBoundaryParams[key])) {
- leftBoundaryParams[key] = value + (leftBoundaryParams[key] || 0);
- } else {
- leftBoundaryParams[key] = value + ' + ' + leftBoundaryParams[key];
- }
- if (!rightBoundaryParams[key] || _.isNumber(rightBoundaryParams[key])) {
- rightBoundaryParams[key] = value + (rightBoundaryParams[key] || 0);
- } else {
- rightBoundaryParams[key] = value + ' + ' + rightBoundaryParams[key];
- }
- });
- break;
- case 'previous_year':
- leftBoundaryParams.years = leftBoundaryParams.years ? leftBoundaryParams.years - 1 : -1;
- rightBoundaryParams.years = rightBoundaryParams.years ? rightBoundaryParams.years - 1 : -1;
- break;
- }
-
- var stringifyParams = function (value, key) {
- return key + '=' + value;
- };
- var leftBoundaryStringifyParams = _.map(leftBoundaryParams, stringifyParams).join(', ');
- var rightBoundaryStringifyParams = _.map(rightBoundaryParams, stringifyParams).join(', ');
-
- if (type === 'date') {
- return "['&'," +
- "('" + fieldName + "', '>=', (context_today() + relativedelta(" + leftBoundaryStringifyParams + ")).strftime('%Y-%m-%d'))," +
- "('" + fieldName + "', '<', (context_today() + relativedelta(" + rightBoundaryStringifyParams + ")).strftime('%Y-%m-%d'))"+
- "]";
- }
- else {
- return "['&'," +
- "('" + fieldName + "', '>=', " +
- "(datetime.datetime.combine(context_today() + relativedelta(" + leftBoundaryStringifyParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))," +
- "('" + fieldName + "', '<', " +
- "(datetime.datetime.combine(context_today() + relativedelta(" + rightBoundaryStringifyParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))"+
- "]";
- }
- }
- switch (period) {
- case 'today':
- leftBoundaryParams = {};
- rightBoundaryParams = {days: 1};
- offsetPeriodParams = {days: -1};
- return makeInterval();
- case 'this_week':
- leftBoundaryParams = {weeks: -1, days: 1, weekday: 0};
- rightBoundaryParams = {days: 1, weekday: 0};
- offsetPeriodParams = {weeks: -1};
- return makeInterval();
- case 'this_month':
- leftBoundaryParams = {day: 1};
- rightBoundaryParams = {day: 1, months: 1};
- offsetPeriodParams = {months: -1};
- return makeInterval();
- case 'this_quarter':
- leftBoundaryParams = {months: '- (context_today().month - 1) % 3', day: 1};
- rightBoundaryParams = {months: '3 - (context_today().month - 1) % 3', day: 1};
- offsetPeriodParams = {months: -3};
- return makeInterval();
- case 'this_year':
- leftBoundaryParams = {month: 1, day: 1};
- rightBoundaryParams = {month: 1, day: 1, years: 1};
- offsetPeriodParams = {years: -1};
- return makeInterval();
- case 'yesterday':
- leftBoundaryParams = {days: -1};
- rightBoundaryParams = {};
- offsetPeriodParams = {days: -1};
- return makeInterval();
- case 'last_week':
- leftBoundaryParams = {weeks: -2, days: 1, weekday: 0};
- rightBoundaryParams = {weeks: -1, days: 1, weekday: 0};
- offsetPeriodParams = {weeks: -1};
- return makeInterval();
- case 'last_month':
- leftBoundaryParams = {months: -1, day: 1};
- rightBoundaryParams = {day: 1};
- offsetPeriodParams = {months: -1};
- return makeInterval();
- case 'last_quarter':
- leftBoundaryParams = {months: '- 3 - (context_today().month - 1) % 3', day: 1};
- rightBoundaryParams = {months: '- (context_today().month - 1) % 3', day: 1};
- offsetPeriodParams = {months: -3};
- return makeInterval();
- case 'last_year':
- leftBoundaryParams = {month: 1, day: 1, years: -1};
- rightBoundaryParams = {month: 1, day: 1};
- offsetPeriodParams = {years: -1};
- return makeInterval();
- case 'last_7_days':
- leftBoundaryParams = {days: -7};
- rightBoundaryParams = {};
- offsetPeriodParams = {days: -7};
- return makeInterval();
- case 'last_30_days':
- leftBoundaryParams = {days: -30};
- rightBoundaryParams = {};
- offsetPeriodParams = {days: -30};
- return makeInterval();
- case 'last_365_days':
- leftBoundaryParams = {days: -365};
- rightBoundaryParams = {};
- offsetPeriodParams = {days: -365};
- return makeInterval();
- case 'last_5_years':
- leftBoundaryParams = {years: -5};
- rightBoundaryParams = {};
- offsetPeriodParams = {years: -5};
- return makeInterval();
- }
- },
/**
* Converts a string representation of the Python prefix-array
* representation of a domain to a JS prefix-array representation of this
diff --git a/addons/web/static/src/js/views/graph/graph_model.js b/addons/web/static/src/js/views/graph/graph_model.js
index 6ee4d06651e..ccaa008f3f7 100644
--- a/addons/web/static/src/js/views/graph/graph_model.js
+++ b/addons/web/static/src/js/views/graph/graph_model.js
@@ -150,12 +150,11 @@ return AbstractModel.extend({
const { range, rangeDescription, comparisonRange, comparisonRangeDescription, fieldName } = this.chart.timeRanges;
if (range) {
- this.chart.domains = [this.chart.domain.concat(range)];
- this.chart.origins = [rangeDescription];
- if (comparisonRange) {
- this.chart.domains.push(this.chart.domain.concat(comparisonRange));
- this.chart.origins.push(comparisonRangeDescription);
- }
+ this.chart.domains = [
+ this.chart.domain.concat(range),
+ this.chart.domain.concat(comparisonRange),
+ ];
+ this.chart.origins = [rangeDescription, comparisonRangeDescription];
const groupBys = this.chart.processedGroupBy.map(function (gb) {
return gb.split(":")[0];
});
diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js
index dbfd9f0e211..5293409cb8b 100644
--- a/addons/web/static/src/js/views/graph/graph_view.js
+++ b/addons/web/static/src/js/views/graph/graph_view.js
@@ -31,7 +31,7 @@ var GraphView = AbstractView.extend({
Renderer: GraphRenderer,
}),
viewType: 'graph',
- searchMenuTypes: ['filter', 'groupBy', 'timeRange', 'favorite'],
+ searchMenuTypes: ['filter', 'groupBy', 'comparison', 'favorite'],
/**
* @override
diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js
index 1fca1e14487..7ff00d1fb77 100644
--- a/addons/web/static/src/js/views/pivot/pivot_view.js
+++ b/addons/web/static/src/js/views/pivot/pivot_view.js
@@ -30,7 +30,7 @@ odoo.define('web.PivotView', function (require) {
Renderer: PivotRenderer,
}),
viewType: 'pivot',
- searchMenuTypes: ['filter', 'groupBy', 'timeRange', 'favorite'],
+ searchMenuTypes: ['filter', 'groupBy', 'comparison', 'favorite'],
/**
* @override