', {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 @@
+
+
+
+
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 222d109be56..75266cf8f2a 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -110,6 +110,7 @@
+
@@ -239,6 +240,8 @@
+
+