diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index 1ab4367de4c..2490cf0da57 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -528,15 +528,22 @@ ActionManager.include({ group_by_seq: groupbys || [], eval_context: this.userContext, }); + var groupBy = results.group_by.length ? + results.group_by : + (action.context.group_by || []); + groupBy = (typeof groupBy === 'string') ? [groupBy] : groupBy; + if (results.error) { throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s", JSON.stringify(results.error))); } - var groupBy = results.group_by.length ? results.group_by : (action.context.group_by || []); + + var context = _.omit(results.context, 'time_ranges'); + return { - context: results.context, + context: context, domain: results.domain, - groupBy: (typeof groupBy === 'string') ? [groupBy] : groupBy, + groupBy: groupBy, }; }, /** diff --git a/addons/web/static/src/js/chrome/control_panel.js b/addons/web/static/src/js/chrome/control_panel.js index fb265390f51..5d0601820d0 100644 --- a/addons/web/static/src/js/chrome/control_panel.js +++ b/addons/web/static/src/js/chrome/control_panel.js @@ -170,7 +170,7 @@ var ControlPanel = Widget.extend({ // Update the searchview and switch buttons if (status.searchview || options.clear) { - this._update_search_view(status.searchview, status.search_view_hidden, status.groupable); + this._update_search_view(status.searchview, status.search_view_hidden, status.groupable, status.enableTimeRangeMenu); } if (status.active_view_selector) { this._update_switch_buttons(status.active_view_selector); @@ -266,12 +266,15 @@ var ControlPanel = Widget.extend({ * @param {boolean} [groupable] visibility of the groupable menu (only * relevant if searchview is visible) */ - _update_search_view: function (searchview, isHidden, groupable) { + _update_search_view: function (searchview, isHidden, groupable, enableTimeRangeMenu) { if (searchview) { searchview.toggle_visibility(!isHidden); if (groupable !== undefined){ searchview.groupby_menu.do_toggle(groupable); } + if (enableTimeRangeMenu !== undefined){ + searchview.displayTimeRangeMenu(enableTimeRangeMenu); + } } this.nodes.$searchview.toggle(!isHidden); this.$el.toggleClass('o_breadcrumb_full', !!isHidden); diff --git a/addons/web/static/src/js/core/data_comparison_utils.js b/addons/web/static/src/js/core/data_comparison_utils.js new file mode 100644 index 00000000000..bdf74b56a24 --- /dev/null +++ b/addons/web/static/src/js/core/data_comparison_utils.js @@ -0,0 +1,83 @@ +odoo.define('web.dataComparisonUtils', function (require) { +"use strict"; + +var fieldUtils = require('web.field_utils'); + +/** + * @param {Number} value + * @param {Number} comparisonValue + * @returns {Object} + */ +function computeVariation (value, comparisonValue) { + var magnitude; + var signClass; + if (!isNaN(value) && !isNaN(comparisonValue)) { + if (comparisonValue === 0) { + if (value === 0) { + magnitude = 0; + } else if (value > 0){ + magnitude = 1; + } else { + magnitude = -1; + } + } else { + magnitude = Math.sign(comparisonValue) * (value - comparisonValue) / comparisonValue; + } + if (magnitude > 0) { + signClass = ' o_positive'; + } else if (magnitude < 0) { + signClass = ' o_negative'; + } else if (magnitude === 0) { + signClass = ' o_null'; + } + return {magnitude: magnitude, signClass: signClass}; + } else { + return {magnitude: NaN}; + } +} + +/** + * @param {Object} variation + * @param {Object} field + * @param {Object} options + * @returns {Object} + */ +function renderVariation (variation, field, options) { + var $variation; + if (!isNaN(variation.magnitude)) { + $variation = $('
', {class: 'o_variation' + variation.signClass}).html( + fieldUtils.format.percentage(variation.magnitude, field, options + )); + } else { + $variation = $('
', {class: 'o_variation'}).html('-'); + } + return $variation; +} + +/** + * @param {JQuery} $node + * @param {Number} value + * @param {Number} comparisonValue + * @param {Object} variation (with key 'magnitude' and 'signClass') + * @param {function} formatter + * @param {Object} field + * @param {Object} options + * @returns {Object} + */ +function renderComparison ($node, value, comparisonValue, variation, formatter, field, options) { + var $variation = renderVariation(variation, field, options); + $node.append($variation); + if (!isNaN(variation.magnitude)) { + $node.append( + $('
', {class: 'o_comparison'}) + .html(formatter(value, field, options) + ' vs ' + formatter(comparisonValue, field, options)) + ); + } +} + +return { + computeVariation: computeVariation, + renderComparison: renderComparison +}; + +}); diff --git a/addons/web/static/src/js/core/domain.js b/addons/web/static/src/js/core/domain.js index c4011b049b9..5ebc6bd22c9 100644 --- a/addons/web/static/src/js/core/domain.js +++ b/addons/web/static/src/js/core/domain.js @@ -234,78 +234,127 @@ var Domain = collections.Tree.extend({ * @param {string} period * @param {string} type ('date' or 'datetime') * @param {boolean} forTooltip indicates if domain is used to print a tooltip + * @param {string} comparisonPeriod + * @returns {string} a domain in string form */ - constructDomain: function (fieldName, period, type, forTooltip) { + constructDomain: function (fieldName, period, type, forTooltip, comparisonPeriod) { var leftBoundaryParams, rightBoundaryParams; + var offsetPeriodParams; var t = forTooltip || false; function makeInterval () { + switch (comparisonPeriod) { + case 'previous_period': + _.map(offsetPeriodParams, function (value, key) { + if (_.isNumber(value)) { + leftBoundaryParams[key] = value + (leftBoundaryParams[key] || 0); + } else { + leftBoundaryParams[key] = value + ' + ' + (leftBoundaryParams[key] || 0); + } + }); + _.map(offsetPeriodParams, function (value, key) { + if (_.isNumber(value)) { + rightBoundaryParams[key] = value + (rightBoundaryParams[key] || 0); + } else { + rightBoundaryParams[key] = value + ' + ' + (rightBoundaryParams[key] || 0); + } + }); + break; + case 'previous_year': + leftBoundaryParams.years = leftBoundaryParams.years ? leftBoundaryParams.years-- : -1; + rightBoundaryParams.years = rightBoundaryParams.years ? rightBoundaryParams.years-- : -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(" + leftBoundaryParams + ")).strftime('%Y-%m-%d'))," + - "('" + fieldName + "', '<', (context_today() + relativedelta(" + rightBoundaryParams + ")).strftime('%Y-%m-%d'))"+ + "('" + 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(" + leftBoundaryParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))," + + "(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(" + rightBoundaryParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))"+ + "(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 = t ? "" : "days=1"; + leftBoundaryParams = {}; + rightBoundaryParams = t ? {} : {days: 1}; + offsetPeriodParams = {days: -1}; return makeInterval(); case 'this_week': - leftBoundaryParams = "weeks=-1, days=1, weekday=0"; - rightBoundaryParams = t ? "weekday=6" : "days=1, weekday=0"; + leftBoundaryParams = {weeks: -1, days: 1, weekday: 0}; + rightBoundaryParams = t ? {weekday: 6} : {days: 1, weekday: 0}; + offsetPeriodParams = {weeks: -1}; return makeInterval(); case 'this_month': - leftBoundaryParams = "day=1"; - rightBoundaryParams = (t ? "days=-1, " : "") + "day=1, months=1"; + leftBoundaryParams = {day: 1}; + rightBoundaryParams = _.extend(t ? {days: -1} : {}, + {day: 1, months: 1}); + offsetPeriodParams = {months: -1}; return makeInterval(); case 'this_quarter': - leftBoundaryParams = "months= - (context_today().month - 1) % 3, day=1"; - rightBoundaryParams = (t ? "days=-1, " : "") + "months= 3 - (context_today().month -1 ) % 3, day=1"; + leftBoundaryParams = {months: '- (context_today().month - 1) % 3', day: 1}; + rightBoundaryParams = _.extend(t ? {days: -1} : {}, + {months: '3 - (context_today().month - 1) % 3', day: 1}); + offsetPeriodParams = {months: -3}; return makeInterval(); case 'this_year': - leftBoundaryParams = "month=1, day=1"; - rightBoundaryParams = (t ? "days=-1, " : "") + "month=1, day=1, years=1"; + leftBoundaryParams = {month: 1, day: 1}; + rightBoundaryParams = _.extend(t ? {days: -1} : {}, + {month: 1, day: 1, years: 1}); + offsetPeriodParams = {years: -1}; return makeInterval(); case 'yesterday': - leftBoundaryParams = "days=-1"; - rightBoundaryParams = (t ? "days=-1" : ""); + leftBoundaryParams = {days: -1}; + rightBoundaryParams = (t ? {days: -1} : {}); + offsetPeriodParams = {days: -1}; return makeInterval(); case 'last_week': - leftBoundaryParams = "weeks=-2, days=1, weekday=0"; - rightBoundaryParams = t ? "weeks=-1, weekday=6" : "weeks=-1, days=1, weekday=0"; + leftBoundaryParams = {weeks: -2, days: 1, weekday: 0}; + rightBoundaryParams = t ? {weeks: -1, weekday: 6} : {weeks: -1, days: 1, weekday: 0}; + offsetPeriodParams = {weeks: -1}; return makeInterval(); case 'last_month': - leftBoundaryParams = "months=-1, day=1"; - rightBoundaryParams = (t ? "days=-1 , " : "") + "day=1"; + leftBoundaryParams = {months: -1, day: 1}; + rightBoundaryParams = _.extend(t ? {days: -1} : {}, {day: 1}); + offsetPeriodParams = {months: -1}; return makeInterval(); case 'last_quarter': - leftBoundaryParams = "months= - 3 - (context_today().month - 1) % 3, day=1"; - rightBoundaryParams = (t ? "days=-1 , " : "") + "months= - (context_today().month - 1) % 3, day=1"; + leftBoundaryParams = {months: '- 3 - (context_today().month - 1) % 3', day: 1}; + rightBoundaryParams = _.extend(t ? {days: -1} : {}, + {months: '- (context_today().month - 1) % 3', day: 1}); + offsetPeriodParams = {months: -3}; return makeInterval(); case 'last_year': - leftBoundaryParams = "month=1, day=1, years=-1"; - rightBoundaryParams = (t ? "days=-1 , " : "") + "month=1, day=1"; + leftBoundaryParams = {month: 1, day: 1, years: -1}; + rightBoundaryParams = _.extend(t ? {days: -1} : {}, {month: 1, day: 1}); + offsetPeriodParams = {years: -1}; return makeInterval(); case 'last_7_days': - leftBoundaryParams = "days=-7"; - rightBoundaryParams = t ? "days=-1" : ""; + leftBoundaryParams = {days: -7}; + rightBoundaryParams = t ? {days: -1} : {}; + offsetPeriodParams = {days: -7}; return makeInterval(); case 'last_30_days': - leftBoundaryParams = "days=-30"; - rightBoundaryParams = t ? "days=-1" : ""; + leftBoundaryParams = {days: -30}; + rightBoundaryParams = t ? {days: -1} : {}; + offsetPeriodParams = {days: -30}; return makeInterval(); case 'last_365_days': - leftBoundaryParams = "days=-365"; - rightBoundaryParams = t ? "days=-1" : ""; + leftBoundaryParams = {days: -365}; + rightBoundaryParams = t ? {days: -1} : {}; + offsetPeriodParams = {days: -365}; return makeInterval(); } }, diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index aed09c363cc..4bcb21f7a4d 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -67,6 +67,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { this.searchView = params.searchView; this.searchViewHidden = params.searchViewHidden; this.groupable = params.groupable; + this.enableTimeRangeMenu = params.enableTimeRangeMenu; this.actionViews = params.actionViews; this.viewType = params.viewType; this.withControlPanel = params.withControlPanel !== false; @@ -428,6 +429,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { searchview: this.searchView, search_view_hidden: !this.searchable || this.searchviewHidden, groupable: this.groupable, + enableTimeRangeMenu: this.enableTimeRangeMenu, }); this._pushState(); diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index 7b1d21e6ea5..d061349b7c8 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -50,7 +50,7 @@ var AbstractView = Class.extend({ // groupby menu or not. This is useful for the views which do not support // grouping data. groupable: true, - + enableTimeRangeMenu: false, config: { Model: AbstractModel, Renderer: AbstractRenderer, @@ -74,6 +74,11 @@ var AbstractView = Class.extend({ * @param {number} [params.count] * @param {string} [params.controllerID] * @param {string[]} params.domain + * @param {string[][]} params.timeRange + * @param {string[][]} params.comparisonTimeRange + * @param {string} params.timeRangeDescription + * @param {string} params.comparisonTimeRangeDescription + * @param {boolean} params.compare * @param {string[]} params.groupBy * @param {number} [params.currentId] * @param {boolean} params.isEmbedded @@ -110,6 +115,22 @@ var AbstractView = Class.extend({ noContentHelp: params.action && params.action.help, }; + var timeRangeMenuData = params.context.timeRangeMenuData; + var timeRange = []; + var comparisonTimeRange = []; + var compare = false; + var timeRangeDescription = ""; + var comparisonTimeRangeDescription = ""; + if (this.enableTimeRangeMenu && timeRangeMenuData) { + timeRange = timeRangeMenuData.timeRange; + comparisonTimeRange = timeRangeMenuData.comparisonTimeRange; + compare = comparisonTimeRange.length > 0; + timeRangeDescription = timeRangeMenuData.timeRangeDescription; + comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription; + this.rendererParams.timeRangeDescription = timeRangeDescription; + this.rendererParams.comparisonTimeRangeDescription = comparisonTimeRangeDescription; + } + this.controllerParams = { modelName: params.modelName, activeActions: { @@ -119,6 +140,7 @@ var AbstractView = Class.extend({ duplicate: this.arch.attrs.duplicate ? JSON.parse(this.arch.attrs.duplicate) : true, }, groupable: this.groupable, + enableTimeRangeMenu: this.enableTimeRangeMenu, isEmbedded: this.isEmbedded, controllerID: params.controllerID, bannerRoute: this.arch.attrs.banner_route, @@ -144,6 +166,11 @@ var AbstractView = Class.extend({ count: params.count || ((this.controllerParams.ids !== undefined) && this.controllerParams.ids.length) || 0, domain: params.domain, + timeRange: timeRange, + timeRangeDescription: timeRangeDescription, + comparisonTimeRange: comparisonTimeRange, + comparisonTimeRangeDescription: comparisonTimeRangeDescription, + compare: compare, groupedBy: params.groupBy, modelName: params.modelName, res_id: params.currentId, diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index 153423fe115..0cbf095db70 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -18,10 +18,13 @@ odoo.define('web.PivotModel', function (require) { var AbstractModel = require('web.AbstractModel'); var concurrency = require('web.concurrency'); +var dataComparisonUtils = require('web.dataComparisonUtils'); var core = require('web.core'); var session = require('web.session'); var utils = require('web.utils'); +var computeVariation = dataComparisonUtils.computeVariation; + var _t = core._t; var PivotModel = AbstractModel.extend({ diff --git a/addons/web/static/src/js/views/search/filters_menu.js b/addons/web/static/src/js/views/search/filters_menu.js index af5425ec2d7..eb35a311fc9 100644 --- a/addons/web/static/src/js/views/search/filters_menu.js +++ b/addons/web/static/src/js/views/search/filters_menu.js @@ -7,10 +7,12 @@ var Domain = require('web.Domain'); var DropdownMenu = require('web.DropdownMenu'); var search_filters = require('web.search_filters'); var time = require('web.time'); +var TimeRangeMenuOptions = require('web.TimeRangeMenuOptions'); var QWeb = core.qweb; var _t = core._t; +var PERIOD_OPTIONS = TimeRangeMenuOptions.PeriodOptions; var FiltersMenu = DropdownMenu.extend({ custom_events: { @@ -43,6 +45,7 @@ var FiltersMenu = DropdownMenu.extend({ * defaultOptionId: string refers to an optionId (optional) * currentOptionId: string refers to an optionId that is activated if item is active (optional) * } + * @param {Object[]} periodOptions determines the options available for filter with date attribute * @param {Object} fields 'field_get' of a model: mapping from field name * to an object with its properties */ @@ -50,21 +53,7 @@ var FiltersMenu = DropdownMenu.extend({ // determines where the filter menu is displayed and its style this.isMobile = config.device.isMobile; // determines list of options used by filter of type 'date' - this.periodOptions = [ - {description: _t('Today'), optionId: 'today', groupId: 1}, - {description: _t('This Week'), optionId: 'this_week', groupId: 1}, - {description: _t('This Month'), optionId: 'this_month', groupId: 1}, - {description: _t('This Quarter'), optionId: 'this_quarter', groupId: 1}, - {description: _t('This Year'), optionId: 'this_year', groupId: 1}, - {description: _t('Yesterday'), optionId: 'yesterday', groupId: 2}, - {description: _t('Last Week'), optionId: 'last_week', groupId: 2}, - {description: _t('Last Month'), optionId: 'last_month', groupId: 2}, - {description: _t('Last Quarter'), optionId: 'last_quarter', groupId: 2}, - {description: _t('Last Year'), optionId: 'last_year', groupId: 2}, - {description: _t('Last 7 Days'), optionId: 'last_7_days', groupId: 3}, - {description: _t('Last 30 Days'), optionId: 'last_30_days', groupId: 3}, - {description: _t('Last 365 Days'), optionId: 'last_365_days', groupId: 3}, - ]; + this.periodOptions = PERIOD_OPTIONS; // determines when the 'Add custom filter' submenu is open this.generatorMenuIsOpen = false; this.propositions = []; diff --git a/addons/web/static/src/js/views/search/search_view.js b/addons/web/static/src/js/views/search/search_view.js index 2bac31ed27f..8e79b6b057e 100644 --- a/addons/web/static/src/js/views/search/search_view.js +++ b/addons/web/static/src/js/views/search/search_view.js @@ -10,9 +10,14 @@ var FiltersMenu = require('web.FiltersMenu'); var GroupByMenu = require('web.GroupByMenu'); var pyUtils = require('web.py_utils'); var search_inputs = require('web.search_inputs'); +var TimeRangeMenu = require('web.TimeRangeMenu'); +var TimeRangeMenuOptions = require('web.TimeRangeMenuOptions'); var utils = require('web.utils'); var Widget = require('web.Widget'); + var _t = core._t; +var ComparisonOptions = TimeRangeMenuOptions.ComparisonOptions; +var PeriodOptions = TimeRangeMenuOptions.PeriodOptions; var Backbone = window.Backbone; @@ -198,7 +203,7 @@ var FacetView = Widget.extend({ * is the current interval used * (necessarily the field is of type 'date' or 'datetime') * @param {Object} periodMapping, a key is a field name and the corresponding value - * is the current interval used + * is the current period used * (necessarily the field is of type 'date' or 'datetime') */ init: function (parent, model, intervalMapping, periodMapping) { @@ -248,9 +253,11 @@ var FacetValueView = Widget.extend({ /* * @param {Widget} parent * @param {Object} model - * @param {Object} interval (optional) is used in case the facet value + * @param {Object} option (optional) is used in case the facet value * corresponds to a groupby with an associated 'date' * 'datetime' field. + * @param {Object} comparison (optional) is used in case the facet value + * comes from the time range menu and comparison is active */ init: function (parent, model, option) { this._super(parent); @@ -326,13 +333,16 @@ var SearchView = Widget.extend({ item_option_changed: '_onItemOptionChanged', new_groupby: '_onNewGroupby', new_filters: '_onNewFilters', + time_range_modified: '_onTimeRangeModified', + time_range_removed: '_onTimeRangeRemoved', }, defaults: _.extend({}, Widget.prototype.defaults, { hidden: false, - disable_filters: false, + disable_custom_filters: false, disable_groupby: false, disable_favorites: false, - disable_custom_filters: false, + disable_filters: false, + disableTimeRangeMenu: true, }), template: "SearchView", @@ -361,6 +371,15 @@ var SearchView = Widget.extend({ this.hasFavorites = false; + this.noDateFields = true; + var field; + for (var key in this.fields) { + field = this.fields[key]; + if (_.contains(['date', 'datetime'], field.type) && field.sortable) { + this.noDateFields = false; + break; + } + } this.selectedGroupIds = { groupByCategory: [], filterCategory: [] @@ -373,6 +392,8 @@ var SearchView = Widget.extend({ this.filters = []; this.groupbys = []; + this.timeRanges = options.action && options.action.context ? + options.action.context.time_ranges : undefined; var visibleSearchMenu = this.call('local_storage', 'getItem', 'visible_search_menu'); this.visible_filters = (visibleSearchMenu !== 'false'); this.input_subviews = []; // for user input in searchbar @@ -421,6 +442,12 @@ var SearchView = Widget.extend({ .then(this.set_default_filters.bind(this)) .then(function () { var menu_defs = []; + self.timeRangeMenu = self._createTimeRangeMenu(); + menu_defs.push(self.timeRangeMenu.prependTo($buttons)); + self.timeRangeMenu.do_hide(); + var display = self.options.disableTimeRangeMenu !== undefined && + !self.options.disableTimeRangeMenu; + self.displayTimeRangeMenu(display); if (!self.options.disable_groupby) { self.groupby_menu = self._createGroupByMenu(); menu_defs.push(self.groupby_menu.prependTo($buttons)); @@ -432,6 +459,17 @@ var SearchView = Widget.extend({ return $.when.apply($, menu_defs); }); }, + /* + * + * @param {boolean} + */ + displayTimeRangeMenu: function (b) { + if (!b || this.noDateFields) { + this.timeRangeMenu.do_hide(); + } else { + this.timeRangeMenu.do_show(); + } + }, on_attach_callback: function () { this._focusInput(); }, @@ -448,6 +486,8 @@ var SearchView = Widget.extend({ if (!_.isEmpty(this.search_defaults)) { var inputs = this.search_fields.concat(this.filters, this.groupbys); var search_defaults = _.invoke(inputs, 'facet_for_defaults', this.search_defaults); + var defaultTimeRange = this._searchDefaultTimeRange(); + search_defaults.push(defaultTimeRange); return $.when.apply(null, search_defaults).then(function () { var facets = _.compact(arguments); self.query.reset(facets, {preventSearch: true}); @@ -502,7 +542,7 @@ var SearchView = Widget.extend({ if (domain) { domains.push(domain); } - var context = field.get_context(facet); + var context = field.get_context(facet, noDomainEvaluation); if (context) { contexts.push(context); } @@ -630,8 +670,8 @@ var SearchView = Widget.extend({ groupByCategory: [], filterCategory: [], }; + var timeRangeMenuIsActive; this.query.each(function (facet) { - var values = facet.get('values'); if (facet.attributes.cat === "groupByCategory") { selectedGroupIds.groupByCategory = _.uniq( @@ -665,7 +705,9 @@ var SearchView = Widget.extend({ ) ); } - + if (facet.attributes.cat === "timeRangeCategory") { + timeRangeMenuIsActive = true; + } var f = new FacetView(this, facet, this.intervalMapping, this.periodMapping); started.push(f.appendTo(self.$('.o_searchview_input_container'))); self.input_subviews.push(f); @@ -692,6 +734,9 @@ var SearchView = Widget.extend({ if (self.filters_menu) { self._unsetUnusedFilters(selectedGroupIds.filterCategory); } + if (self.timeRangeMenu && !timeRangeMenuIsActive) { + self.timeRangeMenu.deactivate(); + } }); }, childFocused: function () { @@ -747,7 +792,7 @@ var SearchView = Widget.extend({ } var current_group = [], current_category = 'filters', - categories = {filters: this.filters, group_by: this.groupbys}; + categories = {filters: this.filters, group_by: this.groupbys, timeRanges: this.timeRanges}; _.each(filters.concat({category:'filters', item: 'separator'}), function (filter) { if (filter.item.tag === 'filter' && filter.category === current_category) { @@ -921,6 +966,15 @@ var SearchView = Widget.extend({ }); return new GroupByMenu(this, groupbys, this.fields); }, + /** + * Create a time range menu. + * + * @private + * @returns {Widget} the range menu + */ + _createTimeRangeMenu: function () { + return new TimeRangeMenu(this, this.fields, this.timeRanges); + }, /** * Processes a fieldsView in place. In particular, parses its arch. * @@ -936,6 +990,81 @@ var SearchView = Widget.extend({ fv.arch = utils.xml_to_json(doc, true); return fv; }, + _searchDefaultTimeRange: function () { + if (this.timeRanges) { + var timeRange = "[]"; + var timeRangeDescription; + var comparisonTimeRange = "[]"; + var comparisonTimeRangeDescription; + + var dateField = { + name: this.timeRanges.field, + type: this.fields[this.timeRanges.field].type, + description: this.fields[this.timeRanges.field].string, + }; + + timeRange = Domain.prototype.constructDomain( + dateField.name, + this.timeRanges.range, + dateField.type + ); + timeRangeDescription = _.findWhere( + PeriodOptions, + {optionId: this.timeRanges.range} + ).description; + + if (this.timeRanges.comparison_range) { + comparisonTimeRange = Domain.prototype.constructDomain( + dateField.name, + this.timeRanges.range, + dateField.type, + null, + this.timeRanges.comparison_range + ); + comparisonTimeRangeDescription = _.findWhere( + ComparisonOptions, + {optionId: this.timeRanges.comparison_range} + ).description; + } + + return $.when({ + cat: 'timeRangeCategory', + category: _t("Time Range"), + icon: 'fa fa-calendar', + field: { + get_context: function (facet, noDomainEvaluation) { + if (!noDomainEvaluation) { + timeRange = Domain.prototype.stringToArray(timeRange); + comparisonTimeRange = Domain.prototype.stringToArray(comparisonTimeRange); + } + return { + timeRangeMenuData: { + timeRange: timeRange, + timeRangeDescription: timeRangeDescription, + comparisonTimeRange: comparisonTimeRange, + comparisonTimeRangeDescription: comparisonTimeRangeDescription, + } + }; + }, + get_groupby: function () {}, + get_domain: function () {} + }, + isRange: true, + values: [{ + label: dateField.description + ': ' + timeRangeDescription + + ( + comparisonTimeRangeDescription ? + (' / ' + comparisonTimeRangeDescription) : + '' + ), + value: null, + }], + }); + } else { + return $.when(); + } + }, + /** * This function ask the groupby menu to deactive all groupbys if no * groupby is used. @@ -1115,6 +1244,31 @@ var SearchView = Widget.extend({ this.query.trigger('reset'); } }, + /* + * @private + * @param {JQueryEvent} event + */ + _onTimeRangeModified: function () { + var facet = this.timeRangeMenu.facetFor(); + var current = this.query.find(function (facet) { + return facet.get('cat') === 'timeRangeCategory'; + }); + if (current) { + this.query.remove(current, {silent: true}); + } + this.query.add(facet); + }, + /* + * @private + */ + _onTimeRangeRemoved: function () { + var current = this.query.find(function (facet) { + return facet.get('cat') === 'timeRangeCategory'; + }); + if (current) { + this.query.remove(current); + } + }, }); _.extend(SearchView, { diff --git a/addons/web/static/src/js/views/search/time_range_menu.js b/addons/web/static/src/js/views/search/time_range_menu.js new file mode 100644 index 00000000000..7da0a7da8fe --- /dev/null +++ b/addons/web/static/src/js/views/search/time_range_menu.js @@ -0,0 +1,185 @@ +odoo.define('web.TimeRangeMenu', function (require) { +"use strict"; + +var core = require('web.core'); +var Domain = require('web.Domain'); +var TimeRangeMenuOptions = require('web.TimeRangeMenuOptions'); +var Widget = require('web.Widget'); + +var _t = core._t; +var ComparisonOptions = TimeRangeMenuOptions.ComparisonOptions; +var PeriodOptions = TimeRangeMenuOptions.PeriodOptions; + +var TimeRangeMenu = Widget.extend({ + template: 'web.TimeRangeMenu', + events: { + 'click .o_apply_range': '_onApplyButtonClick', + 'click .o_comparison_checkbox': '_onCheckBoxClick', + }, + + /** + * override + * @param {Widget} parent + * @param {Object} fields + * @param {Object} configuration + * + */ + init: function(parent, fields, configuration) { + var self = this; + this._super(parent); + this.dateFields = []; + _.each(fields, function (field, name) { + if (field.sortable && _.contains(['date', 'datetime'], field.type)) { + self.dateFields.push(_.extend({}, field, { + name: name, + })); + } + }); + this.periodOptions = PeriodOptions; + this.comparisonOptions = ComparisonOptions; + + // Following steps determine initial configuration + this.isActive = false; + this.timeRangeId = undefined; + this.comparisonIsSelected = false; + this.comparisonTimeRangeId = undefined; + this.dateField = {}; + if (configuration && configuration.field && configuration.range) { + this.isActive = true; + var dateField = _.findWhere(this.dateFields, {name: configuration.field}); + this.dateField = { + name: dateField.name, + description: dateField.string, + type: dateField.type, + }; + this.timeRangeId = configuration.range; + if (configuration.comparison_range) { + this.comparisonIsSelected = true; + this.comparisonTimeRangeId = configuration.comparison_range; + } + } + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + deactivate: function () { + this.isActive = false; + this.comparisonIsSelected = false; + this.renderElement(); + }, + /** + * Generates a :js:class:`~instance.web.search.Facet` descriptor from a + * filter descriptor + * + * @returns {Object} + */ + facetFor: function () { + var fieldDescription; + var timeRange = "[]"; + var timeRangeDescription; + var comparisonTimeRange = "[]"; + var comparisonTimeRangeDescription; + + if (this.isActive) { + fieldDescription = this.dateField.description; + if (this.timeRangeId !== 'custom') { + timeRange = Domain.prototype.constructDomain( + this.dateField.name, + this.timeRangeId, + this.dateField.type + ); + timeRangeDescription = _.findWhere( + this.periodOptions, + {optionId: this.timeRangeId} + ).description; + } + if (this.comparisonIsSelected) { + comparisonTimeRange = Domain.prototype.constructDomain( + this.dateField.name, + this.timeRangeId, + this.dateField.type, + null, + this.comparisonTimeRangeId + ); + comparisonTimeRangeDescription = _.findWhere( + this.comparisonOptions, + {optionId: this.comparisonTimeRangeId} + ).description; + } + } + + return { + cat: 'timeRangeCategory', + category: _t("Time Range"), + icon: 'fa fa-calendar', + field: { + get_context: function (facet, noDomainEvaluation) { + if (!noDomainEvaluation) { + timeRange = Domain.prototype.stringToArray(timeRange); + comparisonTimeRange = Domain.prototype.stringToArray(comparisonTimeRange); + } + return { + timeRangeMenuData: { + timeRange: timeRange, + timeRangeDescription: timeRangeDescription, + comparisonTimeRange: comparisonTimeRange, + comparisonTimeRangeDescription: comparisonTimeRangeDescription, + } + }; + }, + get_groupby: function () {}, + get_domain: function () {} + }, + isRange: true, + values: [{ + label: fieldDescription + ': ' + timeRangeDescription + + ( + comparisonTimeRangeDescription ? + (' / ' + comparisonTimeRangeDescription) : + '' + ), + value: null, + }], + }; + }, + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onApplyButtonClick: function () { + this.isActive = true; + var dateFieldName = this.$('.o_date_field_selector').val(); + this.timeRangeId = this.$('.o_time_range_selector').val(); + if (this.comparisonIsSelected) { + this.comparisonTimeRangeId = this.$('.o_comparison_time_range_selector').val(); + } + this.dateField = { + name: dateFieldName, + type: _.findWhere(this.dateFields, {name: dateFieldName}).type, + description: _.findWhere(this.dateFields, {name: dateFieldName}).string, + }; + + this.renderElement(); + this.trigger_up('time_range_modified'); + }, + /** + * @private + * + * @param {JQueryEvent} ev + */ + _onCheckBoxClick: function (ev) { + ev.stopPropagation(); + this.comparisonIsSelected = this.$('.o_comparison_checkbox').prop('checked'); + this.$('.o_comparison_time_range_selector').toggleClass('o_hidden'); + this.$el.addClass('open'); + } +}); + +return TimeRangeMenu; + +}); diff --git a/addons/web/static/src/js/views/search/time_range_menu_options.js b/addons/web/static/src/js/views/search/time_range_menu_options.js new file mode 100644 index 00000000000..1a5510ab6c4 --- /dev/null +++ b/addons/web/static/src/js/views/search/time_range_menu_options.js @@ -0,0 +1,33 @@ +odoo.define('web.TimeRangeMenuOptions', function (require) { +"use strict"; + +var core = require('web.core'); +var _lt = core._lt; + +var PeriodOptions = [ + {description: _lt('Today'), optionId: 'today', groupId: 1}, + {description: _lt('This Week'), optionId: 'this_week', groupId: 1}, + {description: _lt('This Month'), optionId: 'this_month', groupId: 1}, + {description: _lt('This Quarter'), optionId: 'this_quarter', groupId: 1}, + {description: _lt('This Year'), optionId: 'this_year', groupId: 1}, + {description: _lt('Yesterday'), optionId: 'yesterday', groupId: 2}, + {description: _lt('Last Week'), optionId: 'last_week', groupId: 2}, + {description: _lt('Last Month'), optionId: 'last_month', groupId: 2}, + {description: _lt('Last Quarter'), optionId: 'last_quarter', groupId: 2}, + {description: _lt('Last Year'), optionId: 'last_year', groupId: 2}, + {description: _lt('Last 7 Days'), optionId: 'last_7_days', groupId: 3}, + {description: _lt('Last 30 Days'), optionId: 'last_30_days', groupId: 3}, + {description: _lt('Last 365 Days'), optionId: 'last_365_days', groupId: 3}, +]; + +var ComparisonOptions = [ + {description: _lt('Previous Period'), optionId: 'previous_period'}, + {description: _lt('Previous Year'), optionId: 'previous_year'} +]; + +return { + PeriodOptions: PeriodOptions, + ComparisonOptions: ComparisonOptions, +}; + +}); diff --git a/addons/web/static/src/scss/dropdown_menu.scss b/addons/web/static/src/scss/dropdown_menu.scss index 8f11ba7c0a1..0ff7fd7f561 100644 --- a/addons/web/static/src/scss/dropdown_menu.scss +++ b/addons/web/static/src/scss/dropdown_menu.scss @@ -3,6 +3,7 @@ display: none; } + min-width: 150px; max-height: calc(100vh - 125px); // FIXME overflow: auto; diff --git a/addons/web/static/src/scss/search_view.scss b/addons/web/static/src/scss/search_view.scss index fc1d84d8247..d5149134304 100644 --- a/addons/web/static/src/scss/search_view.scss +++ b/addons/web/static/src/scss/search_view.scss @@ -142,11 +142,13 @@ } } -// Filters -.o_filters_menu { +.o_filters_menu, .o_group_by_menu, .o_time_range_menu { max-height: calc(100vh - 125px); overflow: auto; +} +// Filters +.o_filters_menu { .o_filter_condition { @include o-search-options-dropdown-custom-item; margin-bottom: 8px; @@ -182,8 +184,6 @@ } } - - // Favorites .o_favorites_menu { max-height: 75vh; @@ -211,3 +211,10 @@ @include o-search-options-dropdown-custom-item; } } + +// Time Ranges +.o_time_range_menu { + .dropdown-item-text > label, .custom-control-label { + font-weight: bold; + } +} diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 12464d2220e..c5581976e30 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1127,6 +1127,72 @@
+
+ + +