diff options
Diffstat (limited to 'addons/web/static/src/js/control_panel/search_utils.js')
| -rw-r--r-- | addons/web/static/src/js/control_panel/search_utils.js | 477 |
1 files changed, 428 insertions, 49 deletions
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, }; }); |
