diff options
| author | Mathieu Duckerts-Antoine <dam@odoo.com> | 2018-06-12 17:11:13 +0200 |
|---|---|---|
| committer | Adrien Dieudonne <adr@odoo.com> | 2018-07-06 14:09:09 +0200 |
| commit | 097464de6f85020ad1e56bbc253837a7d60e47e4 (patch) | |
| tree | a33c5fbe25cedfd2b0ac378b13ac2d47f38f393b /addons | |
| parent | 9756109e670cef01b4d475c8cb6687fecdab2cd2 (diff) | |
[IMP] web: easy selection of a period in filters menu
This commit allow to create easily a range of periods (related to a field
of date or datetime type) to select in the filters menu.
Two new attributes on elements with tag 'filter' are now usable
in combination in a search view arch:
- date: string.
A valid field name with associated type date or datetime
- default_period: string (optional).
To choose in the following list:
- 'today'
- 'this_week'
- 'this_month' (default)
- 'this_quarter'
- 'this_year'
- 'yesterday'
- 'last_week'
- 'last_month'
- 'last_quarter'
- 'last_year'
- 'last_7_days'
- 'last_30_days'
- 'last_365_days'
Example:
<filter string="Creation Date" name="period_generator" date="create_date" default_period="this_quarter"/>
Note: the attribute 'domain' can not be used for such filters.
The generated domains are dynamic and can be saved as such
via the favorites menu.
Diffstat (limited to 'addons')
19 files changed, 660 insertions, 200 deletions
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 8b38ee39cf6..eed0b44ce05 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -667,6 +667,7 @@ ]"/> <separator/> + <filter string="Creation Date" name="creation_date" date="create_date" default_period="this_week"/> <filter string="Overdue Opportunities" name="overdue_opp" domain="[('date_deadline', '<', context_today().strftime('%Y-%m-%d')), ('date_closed', '=', False)]" help="Opportunities with a date of Expected Closing which is in the past"/> @@ -886,6 +887,7 @@ for rec in records: 'default_type': 'opportunity', 'default_user_id': uid, 'search_default_assigned_to_me': 1, + 'search_default_creation_date': 1, }</field> <field name="view_ids" eval="[(5, 0, 0), @@ -925,7 +927,7 @@ for rec in records: <field name="context">{ 'default_type': 'opportunity', 'default_user_id': uid, - 'search_default_activities_my': 1 + 'search_default_activities_my': 1, } </field> <field name="help" type="html"> diff --git a/addons/web/static/lib/py.js/lib/py_extras.js b/addons/web/static/lib/py.js/lib/py_extras.js index 2bc69ece598..47aaa33685e 100644 --- a/addons/web/static/lib/py.js/lib/py_extras.js +++ b/addons/web/static/lib/py.js/lib/py_extras.js @@ -444,6 +444,14 @@ datetime.datetime = py.type('datetime', null, { throw new Error('ValueError: No known conversion for ' + m); })); }, + to_utc: function () { + var d = new Date(this.year, this.month, this.day, this.hour, this.minute, this.second); + var offset = d.getTimezoneOffset(); + var kwargs = {minutes: py.float.fromJSON(offset)}; + var timedelta = py.PY_call(py.extras.datetime.timedelta,[],kwargs); + var s = tmxxx(this.year, this.month, this.day + timedelta.days, this.hour, this.minute, this.second + timedelta.seconds); + return datetime.datetime.fromJSON(s.year, s.month, s.day, s.hour, s.minute, s.second); + }, now: py.classmethod.fromJSON(function () { var d = new Date(); return py.PY_call(datetime.datetime, [ @@ -462,6 +470,7 @@ datetime.datetime = py.type('datetime', null, { d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds() * 1000]); }), + combine: py.classmethod.fromJSON(function () { var args = py.PY_parseArgs(arguments, 'date time'); return py.PY_call(datetime.datetime, [ diff --git a/addons/web/static/src/js/core/domain.js b/addons/web/static/src/js/core/domain.js index 86e1f85a634..e8f089dca76 100644 --- a/addons/web/static/src/js/core/domain.js +++ b/addons/web/static/src/js/core/domain.js @@ -228,6 +228,86 @@ 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 {boolean} forTooltip indicates if domain is used to print a tooltip + */ + constructDomain: function (fieldName, period, type, forTooltip) { + var leftBoundaryParams, rightBoundaryParams; + var t = forTooltip || false; + function makeInterval () { + if (type === 'date') { + return "['&'," + + "('" + fieldName + "', '>=', (context_today() + relativedelta(" + leftBoundaryParams + ")).strftime('%Y-%m-%d'))," + + "('" + fieldName + "', '<', (context_today() + relativedelta(" + rightBoundaryParams + ")).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'))," + + "('" + fieldName + "', '<', " + + "(datetime.datetime.combine(context_today() + relativedelta(" + rightBoundaryParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))"+ + "]"; + } + } + switch (period) { + case 'today': + leftBoundaryParams = ""; + rightBoundaryParams = t ? "" : "days=1"; + return makeInterval(); + case 'this_week': + leftBoundaryParams = "weeks=-1, days=1, weekday=0"; + rightBoundaryParams = t ? "weekday=6" : "days=1, weekday=0"; + return makeInterval(); + case 'this_month': + leftBoundaryParams = "day=1"; + rightBoundaryParams = (t ? "days=-1, " : "") + "day=1, 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"; + return makeInterval(); + case 'this_year': + leftBoundaryParams = "month=1, day=1"; + rightBoundaryParams = (t ? "days=-1, " : "") + "month=1, day=1, years=1"; + return makeInterval(); + case 'yesterday': + leftBoundaryParams = "days=-1"; + rightBoundaryParams = (t ? "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"; + return makeInterval(); + case 'last_month': + leftBoundaryParams = "months=-1, day=1"; + rightBoundaryParams = (t ? "days=-1 , " : "") + "day=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"; + return makeInterval(); + case 'last_year': + leftBoundaryParams = "month=1, day=1, years=-1"; + rightBoundaryParams = (t ? "days=-1 , " : "") + "month=1, day=1"; + return makeInterval(); + case 'last_7_days': + leftBoundaryParams = "days=-7"; + rightBoundaryParams = t ? "days=-1" : ""; + return makeInterval(); + case 'last_30_days': + leftBoundaryParams = "days=-30"; + rightBoundaryParams = t ? "days=-1" : ""; + return makeInterval(); + case 'last_365_days': + leftBoundaryParams = "days=-365"; + rightBoundaryParams = t ? "days=-1" : ""; + 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/search/favorites_menu.js b/addons/web/static/src/js/views/search/favorites_menu.js index bd622797c49..1e62ffb5ce3 100644 --- a/addons/web/static/src/js/views/search/favorites_menu.js +++ b/addons/web/static/src/js/views/search/favorites_menu.js @@ -4,6 +4,7 @@ odoo.define('web.FavoriteMenu', function (require) { var config = require('web.config'); var core = require('web.core'); var data_manager = require('web.data_manager'); +var Domain = require('web.Domain'); var pyUtils = require('web.py_utils'); var session = require('web.session'); var Widget = require('web.Widget'); @@ -98,7 +99,9 @@ return Widget.extend({ return; } var user_context = this.getSession().user_context; - var search = this.searchview.build_search_data(); + // get search params with domains not evaluated + var search = this.searchview.build_search_data(true); + var domain = pyUtils.assembleDomains(search.domains, 'AND'); var controllerContext; this.trigger_up('get_controller_context', { callback: function (ctx) { @@ -106,7 +109,7 @@ return Widget.extend({ }, }); var results = pyUtils.eval_domains_and_contexts({ - domains: search.domains, + domains: [], contexts: [user_context].concat(search.contexts.concat(controllerContext || [])), group_by_seq: search.groupbys || [], }); @@ -125,7 +128,7 @@ return Widget.extend({ user_id: shared_filter ? false : session.uid, model_id: this.target_model, context: results.context, - domain: results.domain, + domain: domain, sort: JSON.stringify(this.searchview.dataset._sort), is_default: default_filter, action_id: this.action_id, @@ -180,13 +183,23 @@ return Widget.extend({ * @return {Object} */ facet_for: function (filter) { + var self = this; return { category: _t("Custom Filter"), icon: 'fa-star', field: { get_context: function () { return filter.context; }, get_groupby: function () { return [filter.context]; }, - get_domain: function () { return filter.domain; } + // facet is not used + get_domain: function (facet, noEvaluation) { + noEvaluation = noEvaluation || false; + var userContext = self.getSession().user_context; + var domain = pyUtils.assembleDomains([filter.domain]); + if (!noEvaluation) { + return Domain.prototype.stringToArray(domain, userContext); + } + return domain; + } }, _id: filter.id, is_custom_filter: true, 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 778e199df44..221f98faa97 100644 --- a/addons/web/static/src/js/views/search/filters_menu.js +++ b/addons/web/static/src/js/views/search/filters_menu.js @@ -3,14 +3,15 @@ odoo.define('web.FiltersMenu', function (require) { var config = require('web.config'); var core = require('web.core'); +var Domain = require('web.Domain'); var DropdownMenu = require('web.DropdownMenu'); var search_filters = require('web.search_filters'); +var time = require('web.time'); var QWeb = core.qweb; var _t = core._t; - var FiltersMenu = DropdownMenu.extend({ custom_events: { remove_proposition: '_onRemoveProposition', @@ -20,6 +21,7 @@ var FiltersMenu = DropdownMenu.extend({ 'click .o_add_custom_filter': '_onAddCustomFilterClick', 'click .o_add_condition': '_appendProposition', 'click .o_apply_filter': '_onApplyClick', + ':hover .o_item_option': '_onOptionHover', }), /** @@ -29,6 +31,8 @@ var FiltersMenu = DropdownMenu.extend({ * interface IFilter { * itemId: string; unique id associated with the filter * domain: string; + * isPeriod: boolean + * fieldName: string; fieldName used to generate a domain in case isDate is true (and domain empty) * description: string; label printed on screen * groupId: string; * isActive: boolean; (optional) determines if the filter is considered active @@ -46,16 +50,21 @@ 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' - // TO DO: simplify optionIds below - this.intervalOptions = [ - {description: _t('Day'), optionId: '__interval__day__'}, - {description: _t('Week'), optionId: '__interval__week__'}, - {description: _t('Month'), optionId: '__interval__month__'}, - {description: _t('Year'), optionId: '__interval__year__'}, + 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}, ]; - // determines the default option used in case - // it has not been provided for a filter - this.defaultOptionId = "__interval__month__"; // determines when the 'Add custom filter' submenu is open this.generatorMenuIsOpen = false; this.propositions = []; @@ -111,8 +120,11 @@ var FiltersMenu = DropdownMenu.extend({ _commitSearch: function () { var self = this; var filters = _.invoke(this.propositions, 'get_filter'); + _.each(filters, function (filter) { + filter.attrs.domain = Domain.prototype.arrayToString(filter.attrs.domain); + }); var groupId = _.uniqueId('__group__'); - var toSendToSearchView = []; + var data = []; filters.forEach(function (filter) { var filterName = _.uniqueId('__filter__'); var filterItem = { @@ -122,7 +134,7 @@ var FiltersMenu = DropdownMenu.extend({ isActive: true, }; self._prepareItem(filterItem); - toSendToSearchView.push({ + data.push({ itemId: filterName, groupId: groupId, filter: filter, @@ -130,13 +142,59 @@ var FiltersMenu = DropdownMenu.extend({ self.items.push(filterItem); }); this._renderMenuItems(); - this.trigger_up('new_filter', toSendToSearchView); + this.trigger_up('new_filters', data); _.invoke(this.propositions, 'destroy'); this.propositions = []; - // this._appendProposition(); this._toggleCustomFilterMenu(); }, /** + * override + * + * @private + * @param {Object} item + */ + _prepareItem: function (item) { + if (item.isPeriod) { + item.options = this.periodOptions; + } + // super has to be called here because we need to add options to groupby + // before to call it since some keys in a groupby are initialized using + // the keys 'options' and 'defaultOptionId' + this._super.apply(this, arguments); + }, + /** + * override + * + * @private + */ + _renderMenuItems: function () { + var self= this; + this._super.apply(this, arguments); + // the following code adds tooltip on date options in order + // to alert the user of the meaning of intervals + var $options = this.$('ul.o_dropdown_menu .o_item_option'); + $options.each(function () { + var $option = $(this); + $option.tooltip({ + delay: { show: 500, hide: 0 }, + title: function () { + var itemId = $option.attr('data-item_id'); + var optionId = $option.attr('data-option_id'); + var fieldName = _.findWhere(self.items, {itemId: itemId}).fieldName; + var domain = Domain.prototype.constructDomain(fieldName, optionId, 'date', true); + var evaluatedDomain = Domain.prototype.stringToArray(domain); + var dateFormat = time.getLangDateFormat(); + var dateStart = moment(evaluatedDomain[1][2], "YYYY-MM-DD", 'en').format(dateFormat); + var dateEnd = moment(evaluatedDomain[2][2], "YYYY-MM-DD", 'en').format(dateFormat); + if (optionId === 'today' || optionId === 'yesterday') { + return dateStart; + } + return _.str.sprintf(_t('From %s To %s'), dateStart, dateEnd); + } + }); + }); + }, + /** * Hide and display the sub menu which allows adding custom filters * * @private diff --git a/addons/web/static/src/js/views/search/groupby_menu.js b/addons/web/static/src/js/views/search/groupby_menu.js index 4e3c4eb573d..f32cf314a33 100644 --- a/addons/web/static/src/js/views/search/groupby_menu.js +++ b/addons/web/static/src/js/views/search/groupby_menu.js @@ -9,6 +9,8 @@ var QWeb = core.qweb; var GROUPABLE_TYPES = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime']; +var DEFAULT_INTERVAL = 'month'; + var GroupByMenu = DropdownMenu.extend({ events: _.extend({}, DropdownMenu.prototype.events, { @@ -26,12 +28,12 @@ var GroupByMenu = DropdownMenu.extend({ * fieldName: string; field name without interval! * description: string; label printed on screen * groupId: string; + * isDate: boolean; * isActive: boolean; (optional) determines if the groupby is considered active * isOpen: boolean; (optional) in case there are options the submenu presenting the options * is opened or closed according to isOpen * isRemovable: boolean; (optional) can be removed from menu * options: array of objects with 'optionId' and 'description' keys; (optional) - * defaultOptionId: string refers to an optionId (optional) * currentOptionId: string refers to an optionId that is activated if item is active (optional) * } * @param {Object} fields 'field_get' of a model: mapping from field name @@ -46,15 +48,12 @@ var GroupByMenu = DropdownMenu.extend({ this.generatorMenuIsOpen = false; // determines list of options used by groupbys of type 'date' this.intervalOptions = [ - {description: 'Day', optionId: 'day'}, - {description: 'Week', optionId: 'week'}, - {description: 'Month', optionId: 'month'}, - {description: 'Quarter', optionId: 'quarter'}, - {description: 'Year', optionId: 'year'}, + {description: 'Day', optionId: 'day', groupId: 1}, + {description: 'Week', optionId: 'week', groupId: 1}, + {description: 'Month', optionId: 'month', groupId: 1}, + {description: 'Quarter', optionId: 'quarter', groupId: 1}, + {description: 'Year', optionId: 'year', groupId: 1}, ]; - // determines the default option used in case - // it has not been provided for a groupby - this.defaultOptionId = "month"; this.groupableFields = []; _.each(fields, function (field, name) { if (field.sortable && _.contains(GROUPABLE_TYPES, field.type)) { @@ -114,7 +113,7 @@ var GroupByMenu = DropdownMenu.extend({ * method called via the 'Add Custom Groupby' menu to create * a new groupby and add it (activated) to the menu * In case the field is of type date, it is activated using - * the option determined by the parameter 'this.defaultOptionId' + * the option determined by the parameter 'DEFAULT_INTERVAL' * * @private * @param {string} fieldName @@ -132,8 +131,10 @@ var GroupByMenu = DropdownMenu.extend({ var eventData = _.clone(groupby); this._prepareItem(groupby); if (groupby.hasOptions) { - groupby.currentOptionId = groupby.defaultOptionId; + groupby.currentOptionId = DEFAULT_INTERVAL; + groupby.isDate = true; eventData.optionId = groupby.currentOptionId; + eventData.isDate = true; } this.items.push(groupby); var fieldIndex = this.presentedFields.indexOf(field); @@ -151,7 +152,6 @@ var GroupByMenu = DropdownMenu.extend({ _prepareItem: function (item) { if (_.contains(['date', 'datetime'], this.fields[item.fieldName].type)) { item.options = this.intervalOptions; - item.defaultOptionId = item.defaultOptionId || this.defaultOptionId; } // the idea is that we have only one group in the groupby menu // like in the search view. diff --git a/addons/web/static/src/js/views/search/search_inputs.js b/addons/web/static/src/js/views/search/search_inputs.js index d3f36254819..ee179d7da74 100644 --- a/addons/web/static/src/js/views/search/search_inputs.js +++ b/addons/web/static/src/js/views/search/search_inputs.js @@ -518,7 +518,7 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ * is the current interval used * (necessarily the field is of type 'date' or 'datetime') */ - init: function (filters, parent, intervalMapping) { + init: function (filters, parent, intervalMapping, periodMapping) { // If all filters are group_by and we're not initializing a GroupbyGroup, // create a GroupbyGroup instead of the current FilterGroup if (!(this instanceof GroupbyGroup) && @@ -532,6 +532,7 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ this.filters = filters; this.searchview = parent; this.intervalMapping = intervalMapping; + this.periodMapping = periodMapping || []; this.searchview.query.on('add remove change reset', this.proxy('search_change')); }, start: function () { @@ -640,29 +641,45 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ * Handles domains-fetching for all the filters within it: groups them. * * @param {VS.model.SearchFacet} facet + * @param {boolean} noEvaluation determines if the domains are evaluated or not. + By default, domains are evaluated. * @return {*} combined domains of the enabled filters in this group */ - get_domain: function (facet) { + get_domain: function (facet, noEvaluation) { + noEvaluation = noEvaluation || false; + var self = this; var userContext = this.getSession().user_context; var domains = facet.values.chain() - .map(function (f) { return f.get('value').attrs.domain; }) + .map(function (f) { + var filter = f.get('value'); + var attributes = filter.attrs; + var domain = attributes.domain; + var fieldName = attributes.date; + if (!domain && fieldName) { + var couple = _.findWhere(self.periodMapping, {filter: filter}); + var period = couple ? couple.period: 'this_month'; + var type = couple ? couple.type: 'date'; + domain = Domain.prototype.constructDomain(fieldName, period, type); + } + return domain; + }) .without('[]') .reject(_.isEmpty) - .map(function (d) { - return Domain.prototype.stringToArray(d, userContext); + .map(function (domain) { + domain = domain.replace(/%%/g, '%'); + return domain; }) .value(); if (!domains.length) { return; } - if (domains.length === 1) { - return domains[0]; + // domains belong the same groups. We need to make a union. + var domain = pyUtils.assembleDomains(domains, 'OR'); + if (!noEvaluation) { + return Domain.prototype.stringToArray(domain, userContext); } - - _.each(domains, Domain.prototype.normalizeArray); - var ors = _.times(domains.length - 1, _.constant("|")); - return ors.concat.apply(ors, domains); + return domain; }, toggle_filter: function (e) { e.preventDefault(); @@ -705,13 +722,21 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ })); }, /* - * private - * * @param {Object} intervalMapping */ updateIntervalMapping: function (intervalMapping) { this.intervalMapping = intervalMapping; - } + }, + /* + * @param {Object} periodMapping + */ + updatePeriodMapping: function (periodMapping) { + this.intervalMapping = periodMapping; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- }); 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 e5a00434745..91075bc4972 100644 --- a/addons/web/static/src/js/views/search/search_view.js +++ b/addons/web/static/src/js/views/search/search_view.js @@ -4,6 +4,7 @@ odoo.define('web.SearchView', function (require) { var AutoComplete = require('web.AutoComplete'); var config = require('web.config'); var core = require('web.core'); +var Domain = require('web.Domain'); var FavoriteMenu = require('web.FavoriteMenu'); var FiltersMenu = require('web.FiltersMenu'); var GroupByMenu = require('web.GroupByMenu'); @@ -21,6 +22,9 @@ var FacetValues = Backbone.Collection.extend({ model: FacetValue }); +var DEFAULT_INTERVAL = 'month'; +var DEFAULT_PERIOD = 'this_month'; + var Facet = Backbone.Model.extend({ initialize: function (attrs) { var values = attrs.values; @@ -193,11 +197,15 @@ var FacetView = Widget.extend({ * @param {Object} intervalMapping, a key is a field name and the corresponding value * 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 + * (necessarily the field is of type 'date' or 'datetime') */ - init: function (parent, model, intervalMapping) { + init: function (parent, model, intervalMapping, periodMapping) { this._super(parent); this.model = model; this.intervalMapping = intervalMapping; + this.periodMapping = periodMapping; this.model.on('change', this.model_changed, this); }, destroy: function () { @@ -212,9 +220,21 @@ var FacetView = Widget.extend({ if (index > 0) { $('<span/>', {html: self.model.get('separator') || _t(" or ")}).addClass('o_facet_values_sep').appendTo($e); } - var couple = _.findWhere(self.intervalMapping, {groupby: value.attributes.value}); - var interval = couple ? couple.interval : undefined; - return new FacetValueView(self, value, interval).appendTo($e); + var couple; + var option; + if (value.attributes.value && value.attributes.value.attrs) { + if (value.attributes.value.attrs.isPeriod) { + couple = _.findWhere(self.periodMapping, {filter: value.attributes.value}); + option = couple ? couple.period : value.attributes.value.attrs.default_period; + } + if (value.attributes.value.attrs.isDate) { + couple = _.findWhere(self.intervalMapping, {groupby: value.attributes.value}); + option = couple ? + couple.interval : + (value.attributes.value.attrs.defaultInterval || DEFAULT_INTERVAL); + } + } + return new FacetValueView(self, value, option).appendTo($e); })); }); }, @@ -232,21 +252,34 @@ var FacetValueView = Widget.extend({ * corresponds to a groupby with an associated 'date' * 'datetime' field. */ - init: function (parent, model, interval) { + init: function (parent, model, option) { this._super(parent); this.model = model; - var intervalDescription = { + var optionDescription = _.extend({}, { day: 'Day', week: 'Week', month: 'Month', quarter: 'Quarter', year: 'Year', - }; - // to do: put a test on interval - if (interval) { - var intervalLabel = intervalDescription[interval]; - this.intervalLabel = _t(intervalLabel); + }, { + today: 'Today', + this_week: 'This Week', + this_month: 'This Month', + this_quarter: 'This Quarter', + this_year: 'This Year', + yesterday: 'Yesterday', + last_week: 'Last Week', + last_month: 'Last Month', + last_quarter: 'Last Quarter', + last_year: 'Last Year', + last_7_days: 'Last 7 Days', + last_30_days: 'Last 30 Days', + last_365_days: 'Last 365 Days', + }); + if (option) { + var optionLabel = optionDescription[option]; + this.optionLabel = _t(optionLabel); } this.model.on('change', this.model_changed, this); }, @@ -292,7 +325,7 @@ var SearchView = Widget.extend({ menu_item_toggled: '_onItemToggled', item_option_changed: '_onItemOptionChanged', new_groupby: '_onNewGroupby', - new_filter: '_onNewFilter', + new_filters: '_onNewFilters', }, defaults: _.extend({}, Widget.prototype.defaults, { hidden: false, @@ -326,6 +359,8 @@ var SearchView = Widget.extend({ this.action = this.options.action || {}; this.search_fields = []; + this.hasFavorites = false; + this.selectedGroupIds = { groupByCategory: [], filterCategory: [] @@ -334,6 +369,7 @@ var SearchView = Widget.extend({ this.groupbysMapping = []; this.filtersMapping = []; this.intervalMapping = []; + this.periodMapping = []; this.filters = []; this.groupbys = []; @@ -406,13 +442,15 @@ var SearchView = Widget.extend({ var self = this, default_custom_filter = this.$buttons && this.favorite_menu && this.favorite_menu.get_default_filter(); if (!self.options.disable_custom_filters && default_custom_filter) { + this.hasFavorites = true; return this.favorite_menu.toggle_filter(default_custom_filter, true); } if (!_.isEmpty(this.search_defaults)) { - var inputs = this.search_fields.concat(this.filters, this.groupbys), - search_defaults = _.invoke(inputs, 'facet_for_defaults', this.search_defaults); + var inputs = this.search_fields.concat(this.filters, this.groupbys); + var search_defaults = _.invoke(inputs, 'facet_for_defaults', this.search_defaults); return $.when.apply(null, search_defaults).then(function () { - self.query.reset(_(arguments).compact(), {preventSearch: true}); + var facets = _.compact(arguments); + self.query.reset(facets, {preventSearch: true}); }); } this.query.reset([], {preventSearch: true}); @@ -438,6 +476,9 @@ var SearchView = Widget.extend({ this.trigger_up('search', search); }, /** + * @param {boolean} noDomainEvaluation determines if domain are evaluated or not. + * By default, domains are evaluated. + * * Extract search data from the view's facets. * * Result is an object with 3 (own) properties: @@ -451,12 +492,13 @@ var SearchView = Widget.extend({ * * @return {Object} */ - build_search_data: function () { + build_search_data: function (noDomainEvaluation) { var domains = [], contexts = [], groupbys = []; - + noDomainEvaluation = noDomainEvaluation || false; this.query.each(function (facet) { + // field is actually a FilterGroup! var field = facet.get('field'); - var domain = field.get_domain(facet); + var domain = field.get_domain(facet, noDomainEvaluation); if (domain) { domains.push(domain); } @@ -470,7 +512,6 @@ var SearchView = Widget.extend({ } }); var intervalMappingNormalized = {}; - _.each(this.intervalMapping, function (couple) { var fieldName = couple.groupby.fieldName; var interval = couple.interval; @@ -589,7 +630,6 @@ var SearchView = Widget.extend({ groupByCategory: [], filterCategory: [], }; - this.query.each(function (facet) { if (facet.attributes.cat === "groupByCategory") { @@ -623,7 +663,7 @@ var SearchView = Widget.extend({ ); } - var f = new FacetView(this, facet, this.intervalMapping); + var f = new FacetView(this, facet, this.intervalMapping, this.periodMapping); started.push(f.appendTo(self.$('.o_searchview_input_container'))); self.input_subviews.push(f); }, this); @@ -687,10 +727,16 @@ var SearchView = Widget.extend({ if (context.group_by) { category = 'group_by'; item.attrs.fieldName = context.group_by.split(':')[0]; + item.attrs.isDate = _.contains(['date', 'datetime'], self.fields[item.attrs.fieldName].type); item.attrs.defaultInterval = context.group_by.split(':')[1]; } } catch (e) {} } + if (item.attrs.date) { + item.attrs.default_period = item.attrs.default_period || DEFAULT_PERIOD; + item.attrs.type = self.fields[item.attrs.date].type; + } + item.attrs.isPeriod = !!item.attrs.date; return { item: item, category: category, @@ -705,7 +751,7 @@ var SearchView = Widget.extend({ return current_group.push(new search_inputs.Filter(filter.item, self)); } if (current_group.length) { - var group = new search_inputs.FilterGroup(current_group, self, self.intervalMapping); + var group = new search_inputs.FilterGroup(current_group, self, self.intervalMapping, self.periodMapping); categories[current_category].push(group); current_group = []; } @@ -718,7 +764,6 @@ var SearchView = Widget.extend({ if (field.type === "many2one" && attrs.widget === "selection") { attrs.widget = undefined; } - var Obj = core.search_widgets_registry.getAny([attrs.widget, field.type]); if (Obj) { self.search_fields.push(new (Obj) (filter.item, field, self)); @@ -752,11 +797,15 @@ var SearchView = Widget.extend({ updateFilters: function (newFilters, filtersToRemove) { var self = this; var addedFilters = _.map(newFilters, function (filter) { + var domain = filter.domain; + if (domain instanceof Array) { + domain = Domain.prototype.arrayToString(domain); + } filter = { - attrs: {domain: filter.domain, help: filter.help}, + attrs: {domain: domain, help: filter.help}, }; var filterWidget = new search_inputs.Filter(filter); - var filterGroup = new search_inputs.FilterGroup([filterWidget], self, self.intervalMapping); + var filterGroup = new search_inputs.FilterGroup([filterWidget], self, self.intervalMapping, self.periodMapping); var facet = filterGroup.make_facet([filterGroup.make_value(filter)]); self.query.add([facet], {silent: true}); @@ -804,14 +853,23 @@ var SearchView = Widget.extend({ group.filters.forEach(function (filter) { if (!filter.attrs.invisible) { var filterId = _.uniqueId('__filter__'); + var isPeriod = filter.attrs.isPeriod; + var defaultPeriod = filter.attrs.default_period; filters.push({ - isActive: !!self.search_defaults[filter.attrs.name], - description: filter.attrs.string || filter.attrs.help || filter.attrs.name || filter.attrs.domain || 'Ω', itemId: filterId, + description: filter.attrs.string || filter.attrs.help || + filter.attrs.name || filter.attrs.domain || 'Ω', domain: filter.attrs.domain, + fieldName: filter.attrs.date, + isPeriod: isPeriod, + currentOptionId: defaultPeriod, + isActive: !self.hasFavorites && !!self.search_defaults[filter.attrs.name], groupId: groupId, }); self.filtersMapping.push({filterId: filterId, filter: filter, groupId: groupId}); + if (isPeriod) { + self.periodMapping.push({filter: filter, period: defaultPeriod, type: filter.attrs.type}); + } } }); self.groupsMapping.push({groupId: groupId, group: group, category: 'Filters'}); @@ -835,20 +893,22 @@ var SearchView = Widget.extend({ var groupId = _.uniqueId('__group__'); group.filters.forEach(function (groupby) { if (!groupby.attrs.invisible) { - var fieldName = groupby.attrs.fieldName; - var defaultOptionId = groupby.attrs.defaultInterval; var groupbyId = _.uniqueId('__groupby__'); + var fieldName = groupby.attrs.fieldName; + var isDate = groupby.attrs.isDate; + var defaultInterval = groupby.attrs.defaultInterval || DEFAULT_INTERVAL; groupbys.push({ - isActive: !!self.search_defaults[groupby.attrs.name], - description: groupby.attrs.string, itemId: groupbyId, + description: groupby.attrs.string || groupby.attrs.help || groupby.attrs.name + || groupby.attrs.fieldName || 'Ω', + isDate: isDate, fieldName: fieldName, + currentOptionId: defaultInterval, + isActive: !self.hasFavorites && !!self.search_defaults[groupby.attrs.name], groupId: groupId, - defaultOptionId: defaultOptionId, }); - if (_.contains(['date', 'datetime'], self.fields[fieldName].type)) { - self.intervalMapping.push({groupby: groupby, interval: defaultOptionId || 'month'}); - + if (isDate) { + self.intervalMapping.push({groupby: groupby, interval: defaultInterval}); } self.groupbysMapping.push({groupbyId: groupbyId, groupby: groupby, groupId: groupId}); } @@ -882,10 +942,12 @@ var SearchView = Widget.extend({ * @param {string[]]} selectedGroupIds list of group ids to deactivate */ _unsetUnusedGroupbys: function (selectedGroupIds) { - var groupIds = this.selectedGroupIds.groupByCategory.reduce( - function (acc, id) { - if (!_.contains(selectedGroupIds, id)) { - acc.push(id); + var groupIds = this.groupsMapping.reduce( + function (acc, triple) { + if (triple.category === 'Group By') { + if (!_.contains(selectedGroupIds, triple.groupId)) { + acc.push(triple.groupId); + } } return acc; }, @@ -902,10 +964,12 @@ var SearchView = Widget.extend({ * @param {string[]]} selectedGroupIds */ _unsetUnusedFilters: function (selectedGroupIds) { - var groupIds = this.selectedGroupIds.filterCategory.reduce( - function (acc, id) { - if (!_.contains(selectedGroupIds, id)) { - acc.push(id); + var groupIds = this.groupsMapping.reduce( + function (acc, triple) { + if (triple.category === 'Filters') { + if (!_.contains(selectedGroupIds, triple.groupId)) { + acc.push(triple.groupId); + } } return acc; }, @@ -944,6 +1008,11 @@ var SearchView = Widget.extend({ if (event.data.category === 'filterCategory') { var filter = _.findWhere(this.filtersMapping, {filterId: event.data.itemId}).filter; group = _.findWhere(this.groupsMapping, {groupId: event.data.groupId}).group; + if (event.data.optionId) { + var period = event.data.optionId; + _.findWhere(this.periodMapping, {filter: filter}).period = period; + group.updatePeriodMapping(this.periodMapping); + } group.toggle(filter); } }, @@ -956,18 +1025,19 @@ var SearchView = Widget.extend({ * @param {OdooEvent} event */ _onNewGroupby: function (event) { + var isDate = event.data.isDate; var attrs = { context:"{'group_by':'" + event.data.fieldName + "''}", name: event.data.description, fieldName: event.data.fieldName, + isDate: isDate, }; var groupby = new search_inputs.Filter({attrs: attrs}, this); if (event.data.optionId) { var interval = event.data.optionId; this.intervalMapping.push({groupby: groupby, interval: interval}); } - var group = new search_inputs.FilterGroup([groupby], this, this.intervalMapping); - group.toggle(groupby); + var group = new search_inputs.FilterGroup([groupby], this, this.intervalMapping, this.periodMapping); this.groupbysMapping.push({ groupbyId: event.data.itemId, groupby: groupby, @@ -978,6 +1048,7 @@ var SearchView = Widget.extend({ group: group, category: 'Group By', }); + group.toggle(groupby); }, /** * this function is called when a new filter has been added to the filters menu @@ -987,7 +1058,7 @@ var SearchView = Widget.extend({ * @private * @param {OdooEvent} event */ - _onNewFilter: function (event) { + _onNewFilters: function (event) { var self= this; var filter; var filters = []; @@ -1006,16 +1077,16 @@ var SearchView = Widget.extend({ groupId = filterDescription.groupId; } }); - var group = new search_inputs.FilterGroup(filters, this, this.intervalMapping); + var group = new search_inputs.FilterGroup(filters, this, this.intervalMapping, this.periodMapping); filters.forEach(function (filter) { group.toggle(filter, {silent: true}); }); - this.query.trigger('reset'); this.groupsMapping.push({ groupId: groupId, group: group, category: 'Filters', }); + this.query.trigger('reset'); }, /** * this function is called when the option related to an item (filter or groupby) has been @@ -1036,8 +1107,11 @@ var SearchView = Widget.extend({ } if (event.data.category === 'filterCategory') { var filter = _.findWhere(this.filtersMapping, {filterId: event.data.itemId}).filter; + var period = event.data.optionId; + _.findWhere(this.periodMapping, {filter: filter}).period = period; group = _.findWhere(this.groupsMapping, {groupId: event.data.groupId}).group; - group.toggle(filter); + group.updatePeriodMapping(this.periodMapping); + this.query.trigger('reset'); } }, }); diff --git a/addons/web/static/src/js/widgets/dropdown_menu.js b/addons/web/static/src/js/widgets/dropdown_menu.js index 2999d09bbf9..335eed80e0e 100644 --- a/addons/web/static/src/js/widgets/dropdown_menu.js +++ b/addons/web/static/src/js/widgets/dropdown_menu.js @@ -13,7 +13,6 @@ var DropdownMenu = Widget.extend({ events: { 'click li.o_menu_item': '_onItemClick', 'click li.o_item_option': '_onOptionClick', - 'click li.o_menu_item span.o_submenu_switcher': '_openItemOptions', 'click span.o_trash_button': '_onTrashClick', 'hidden.bs.dropdown': '_onBootstrapClose', }, @@ -30,7 +29,7 @@ var DropdownMenu = Widget.extend({ * - 'style' (button style) * @param {Object} items list of menu items (type IGMenuItem below) * interface IMenuItem { - * itemId: string; unique id associated with the item + * itemId: string; (optional) unique id associated with the item * description: string; label printed on screen * groupId: string; * isActive: boolean; (optional) determines if the item is considered active @@ -38,7 +37,6 @@ var DropdownMenu = Widget.extend({ * is opened or closed according to isOpen * isRemovable: boolean; (optional) can be removed from menu * options: array of objects with 'optionId' and 'description' keys; (optional) - * defaultOptionId: string refers to an optionId (optional) * currentOptionId: string refers to an optionId that is activated if item is active (optional) * } */ @@ -57,7 +55,9 @@ var DropdownMenu = Widget.extend({ this.items = _.sortBy(items, 'groupId'); _.each(this.items, this._prepareItem.bind(this)); }, - + /** + * override + */ start: function () { this.$menu = this.$('.o_dropdown_menu'); }, @@ -117,13 +117,14 @@ var DropdownMenu = Widget.extend({ * @param {Object} item */ _prepareItem: function (item) { + item.itemId = item.itemId || _.uniqueId('__item__'); item.isOpen = item.isOpen || false; item.isRemovable = item.isRemovable || false; - item.hasOptions = item.options && item.options.length !== 0 ? true : false; - item.defaultOptionId = item.hasOptions ? - (item.defaultOptionId || item.options[0].optionId) : - false; - item.currentOptionId = item.isActive ? (item.currentOptionId || item.defaultOptionId ) : false; + if (item.options && item.options.length !== 0) { + item.options = _.sortBy(item.options, 'groupId'); + item.hasOptions = true; + item.currentOptionId = item.isActive && item.currentOptionId ? item.currentOptionId : false; + } }, /** * @private @@ -131,23 +132,21 @@ var DropdownMenu = Widget.extend({ */ _toggleMenuItem: function (itemId) { var item = _.findWhere(this.items, {itemId: itemId}); - item.isActive = !item.isActive; - var eventData = { - category: this.dropdownCategory, - itemId: itemId, - isActive: item.isActive, - groupId: item.groupId, - }; + if (!item.hasOptions) { + item.isActive = !item.isActive; + var eventData = { + category: this.dropdownCategory, + itemId: itemId, + isActive: item.isActive, + groupId: item.groupId, + }; + this._renderMenuItems(); + this.trigger_up('menu_item_toggled', eventData); + } if (item.hasOptions) { - if (item.isActive) { - item.currentOptionId = item.defaultOptionId; - eventData.optionId = item.currentOptionId; - } else { - item.currentOptionId = false; - } + item.isOpen = !item.isOpen; + this._renderMenuItems(); } - this._renderMenuItems(); - this.trigger_up('menu_item_toggled', eventData); }, /** * @private @@ -178,15 +177,6 @@ var DropdownMenu = Widget.extend({ this.trigger_up('item_option_changed', eventData); } }, - /** - * @private - * @param {string} itemId - */ - _toggleMenuOptions: function (itemId) { - var item = _.findWhere(this.items, {itemId: itemId}); - item.isOpen = !item.isOpen; - this._renderMenuItems(); - }, //-------------------------------------------------------------------------- // Handlers @@ -235,18 +225,6 @@ var DropdownMenu = Widget.extend({ var itemId = $(event.currentTarget).data('id'); this._removeItem(itemId); }, - /** - * @private - * @param {MouseEvent} event - */ - _openItemOptions: function (event) { - // we preventdefault the event here to avoid changing the hash # in the - // url. - event.preventDefault(); - event.stopPropagation(); - var itemId = $(event.currentTarget).data('id'); - this._toggleMenuOptions(itemId); - }, }); return DropdownMenu; diff --git a/addons/web/static/src/scss/dropdown_menu.scss b/addons/web/static/src/scss/dropdown_menu.scss index c32779271bc..8024783d3c2 100644 --- a/addons/web/static/src/scss/dropdown_menu.scss +++ b/addons/web/static/src/scss/dropdown_menu.scss @@ -43,8 +43,16 @@ } } } + li.divider:first-child { + display: none; + } .o_item_option { + color: $o-main-text-color; padding-left: 20px; + &:hover { + color: darken($o-main-text-color, 10%); + background-color: $dropdown-link-hover-bg; + } } } } diff --git a/addons/web/static/src/scss/search_view.scss b/addons/web/static/src/scss/search_view.scss index 03fe29916fc..0ff5fd320e7 100644 --- a/addons/web/static/src/scss/search_view.scss +++ b/addons/web/static/src/scss/search_view.scss @@ -179,6 +179,10 @@ display: none; // Needed by the webclient @include o-search-options-dropdown-custom-li; } + .o_submenu_switcher { + position: absolute; + right: 0px; + } } diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index a4714e81ee5..758f5bcd416 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1055,14 +1055,19 @@ <a href="#"> <t t-esc="item.description"/> <span t-if="item.isRemovable" t-att-data-id="item.itemId" class="o_trash_button" > <span class="fa fa-trash-o"></span> </span> - <span t-if="item.hasOptions" t-att-data-id="item.itemId" class="o_submenu_switcher"> <span class="fa fa-caret-down"></span> </span> + <span t-if="item.hasOptions" t-att-data-id="item.itemId" class="o_submenu_switcher"> + <span t-att-class="item.isOpen ? 'fa fa-caret-down' : 'fa fa-caret-right'"></span> + </span> </a> <ul t-if="item.hasOptions && item.isOpen"> + <t t-set="subgroup" t-value="0"/> <t t-foreach="_.values(item.options)" t-as="option"> + <li t-if="subgroup !== option.groupId" class="divider" data-removable="1"/> <li t-att-class="'o_item_option ' + (item.currentOptionId === option.optionId ? 'selected': '')" t-att-data-item_id="item.itemId" t-att-data-option_id="option.optionId"> <a href="#"><t t-esc="option.description"/></a> </li> + <t t-set="subgroup" t-value="option.groupId"/> </t> </ul> </li> @@ -1145,7 +1150,7 @@ <div class="fa fa-sm fa-remove o_facet_remove" role="img" aria-label="Remove" title="Remove"/> </div> <span t-name="SearchView.FacetView.Value"> - <t t-esc="widget.model.get('label') + (widget.intervalLabel ? (': ' + widget.intervalLabel) : '')"/> + <t t-esc="widget.model.get('label') + (widget.optionLabel ? (': ' + widget.optionLabel) : '')"/> </span> <t t-name="SearchView.autocomplete"> <ul class="dropdown-menu o_searchview_autocomplete" role="menu"></ul> diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 3b8f320393b..3910e6d761f 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -2877,7 +2877,7 @@ QUnit.module('ActionManager', { intercepts: { create_filter: function (event) { var filter = event.data.filter; - assert.deepEqual(filter.domain, [['bar', '=', 1]], + assert.deepEqual(filter.domain, "[('bar', '=', 1)]", "should save the correct domain"); assert.deepEqual(filter.context, {shouldBeInFilterContext: true}, "should save the correct context"); diff --git a/addons/web/static/tests/core/py_utils_tests.js b/addons/web/static/tests/core/py_utils_tests.js index 1e1848eb778..df657789c71 100644 --- a/addons/web/static/tests/core/py_utils_tests.js +++ b/addons/web/static/tests/core/py_utils_tests.js @@ -33,9 +33,9 @@ QUnit.module('core', function () { QUnit.test('simple arithmetic', function(assert) { assert.expect(2); - var result = pyEval.py_eval("1 + 2"); + var result = pyUtils.py_eval("1 + 2"); assert.strictEqual(result, 3, "should properly evaluate sum"); - result = pyEval.py_eval("42 % 5"); + result = pyUtils.py_eval("42 % 5"); assert.strictEqual(result, 2, "should properly evaluate modulo operator"); }); @@ -1209,9 +1209,10 @@ QUnit.module('core', function () { }; QUnit.test("assemble domains", function (assert) { - assert.expect(6); + assert.expect(7); assert.checkAssemble([], '&', "[]"); + assert.checkAssemble(["[('a', '=', 1)]"], null, "[('a', '=', 1)]"); assert.checkAssemble( ["[('a', '=', '1'), ('b', '!=', 2)]"], 'AND', @@ -1238,7 +1239,5 @@ QUnit.module('core', function () { "[('user_id', '=', uid)]" ); }); - }); - }); diff --git a/addons/web/static/tests/views/search_filters_menu_tests.js b/addons/web/static/tests/views/search_filters_menu_tests.js index 84bc1df86d8..e6430527de7 100644 --- a/addons/web/static/tests/views/search_filters_menu_tests.js +++ b/addons/web/static/tests/views/search_filters_menu_tests.js @@ -129,7 +129,47 @@ QUnit.module('FiltersMenu', { filterMenu.destroy(); }); + QUnit.test('adding a date filter', function (assert) { + assert.expect(5); + var filters = [{ + itemId: 1, + isActive: false, + description: 'some filter', + isPeriod: true, + name: 'Filter Date', + fieldName: 'date_field', + groupId: 1, + }, + ]; + + var filterMenu = createFiltersMenu(filters, this.fields, { + intercepts: { + menu_item_toggled: function (ev) { + assert.strictEqual(ev.data.optionId, 'this_quarter', + "should trigger event with proper period"); + }, + }, + }); + // open menu dropdown + filterMenu.$('span.fa-filter').click(); + + assert.strictEqual(filterMenu.$('.o_menu_item ul').length, 0, + "there should not be a sub menu item list"); + + // open date sub menu + filterMenu.$('.o_submenu_switcher').click(); + assert.strictEqual(filterMenu.$('.o_menu_item ul').length, 1, + "there should be a sub menu item list"); + // click on This Quarter option + assert.notOk(filterMenu.$('.o_menu_item li[data-option_id="this_quarter"]').hasClass('selected'), + "menu item should not be selected"); + filterMenu.$('.o_menu_item li[data-option_id="this_quarter"] a').click(); + assert.ok(filterMenu.$('.o_menu_item li[data-option_id="this_quarter"]').hasClass('selected'), + "menu item should be selected"); + + filterMenu.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/search_groupby_menu_tests.js b/addons/web/static/tests/views/search_groupby_menu_tests.js index e403c08a97c..8dff7a581fe 100644 --- a/addons/web/static/tests/views/search_groupby_menu_tests.js +++ b/addons/web/static/tests/views/search_groupby_menu_tests.js @@ -93,14 +93,22 @@ QUnit.module('GroupByMenu', { groupByMenu.destroy(); }); - QUnit.test('click on groupby filter should activate it', function (assert) { + QUnit.test('click on a groupby filter (not of date type) should activate it', function (assert) { assert.expect(5); - var groupByMenu = createGroupByMenu(this.groupbys, - {fieldname: {sortable: true, string: 'Super Date', type: 'date', isDate: true}}, { + this.groupbys = [{ + isActive: false, + description: 'another group by', + fieldName: 'float_field', + itemId: 'green', + groupId: 1, + }]; + this.fields = {float_field: {sortable: true, string: 'Super Float', type: 'float'}}; + + var groupByMenu = createGroupByMenu(this.groupbys, this.fields, { intercepts: { menu_item_toggled: function(ev) { - assert.strictEqual(ev.data.itemId, 'red'); + assert.strictEqual(ev.data.itemId, 'green'); assert.strictEqual(ev.data.isActive, true); }, }, @@ -111,7 +119,21 @@ QUnit.module('GroupByMenu', { assert.ok(groupByMenu.$('.o_menu_item:first').hasClass('selected')); assert.ok(groupByMenu.$('.o_menu_item:first').is(':visible'), 'group by filter should still be visible'); + groupByMenu.destroy(); + }); + + QUnit.test('click on a groupby filter of date type should open menu option', function (assert) { + assert.expect(4); + var groupByMenu = createGroupByMenu(this.groupbys, + {fieldname: {sortable: true, string: 'Super Date', type: 'date', isDate: true}}); + groupByMenu.$('button:first').click(); + assert.ok(!groupByMenu.$('.o_menu_item:first').hasClass('selected')); + groupByMenu.$('.o_menu_item a').first().click(); + assert.ok(!groupByMenu.$('.o_menu_item:first').hasClass('selected')); + assert.ok(groupByMenu.$('.o_menu_item:first').is(':visible'), + 'group by filter should still be visible'); + assert.ok(groupByMenu.$('.o_item_option').length, 5); groupByMenu.destroy(); }); diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js index e4db69131f7..f01ea2f05af 100644 --- a/addons/web/static/tests/views/search_view_tests.js +++ b/addons/web/static/tests/views/search_view_tests.js @@ -10,17 +10,17 @@ QUnit.module('Search View', { partner: { fields: { date_field: {string: "Date", type: "date", store: true, sortable: true}, - Birthday: {string: "Birthday", type: "date", store: true, sortable: true}, - foo: {string: "Foo", type: "char"}, + birthday: {string: "Birthday", type: "date", store: true, sortable: true}, + foo: {string: "Foo", type: "char", store: true, sortable: true}, bar: {string: "Bar", type: "many2one", relation: 'partner'}, float_field: {string: "Float", type: "float"}, }, records: [ - {id: 1, display_name: "First record", foo: "yop", bar: 2, date_field: "2017-01-25", Birthday: "1983-07-15", float_field: 1}, - {id: 2, display_name: "Second record", foo: "blip", bar: 1, date_field: "2017-01-24", Birthday: "1982-06-04",float_field: 2}, - {id: 3, display_name: "Third record", foo: "gnap", bar: 1, date_field: "2017-01-13", Birthday: "1985-09-13",float_field: 1.618}, - {id: 4, display_name: "Fourth record", foo: "plop", bar: 2, date_field: "2017-02-25", Birthday: "1983-05-05",float_field: -1}, - {id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, date_field: "2016-01-25", Birthday: "1800-01-01",float_field: 13}, + {id: 1, display_name: "First record", foo: "yop", bar: 2, date_field: "2017-01-25", birthday: "1983-07-15", float_field: 1}, + {id: 2, display_name: "Second record", foo: "blip", bar: 1, date_field: "2017-01-24", birthday: "1982-06-04",float_field: 2}, + {id: 3, display_name: "Third record", foo: "gnap", bar: 1, date_field: "2017-01-13", birthday: "1985-09-13",float_field: 1.618}, + {id: 4, display_name: "Fourth record", foo: "plop", bar: 2, date_field: "2017-02-25", birthday: "1983-05-05",float_field: -1}, + {id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, date_field: "2016-01-25", birthday: "1800-01-01",float_field: 13}, ], }, pony: { @@ -69,6 +69,13 @@ QUnit.module('Search View', { type: 'ir.actions.act_window', views: [[2, 'list']], search_view_id: [4, 'search'], + }, { + id: 6, + name: 'Partners Action 6', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[2, 'list']], + search_view_id: [5, 'search'], } ]; @@ -90,21 +97,21 @@ QUnit.module('Search View', { // search views 'partner,false,search': '<search>'+ - '<field name="foo" string="Foo"/>' + '<filter string="candle" name="itsName" context="{\'group_by\': \'foo\'}"/>' + '</search>', 'partner,2,search': '<search>'+ - '<field name="date_field" string="Date"/>' + '<filter string="Date" name="coolName" context="{\'group_by\': \'date_field\'}"/>' + '</search>', 'partner,3,search': '<search>'+ - '<field name="date_field" string="Date"/>' + '<filter string="float" name="positive" domain="[(\'float_field\', \'>=\', 0)]"/>' + '</search>', 'partner,4,search': '<search>'+ - '<field name="date_field" string="Date"/>' + '<filter string="Date" name="coolName" context="{\'group_by\': \'date_field:day\'}"/>' + '</search>', + 'partner,5,search': '<search>'+ + '<filter string="Date Field Filter" name="positive" date="date_field"/>' + + '<filter string="Date Field Groupby" name="coolName" context="{\'group_by\': \'date_field:day\'}"/>' + + '</search>', }; }, }, function () { @@ -159,13 +166,14 @@ QUnit.module('Search View', { actionManager.doAction(2); $('span.fa-bars').click(); $('.o_submenu_switcher').click(); - $('.o_item_option:first').click(); + // Don't forget there is a hidden li.divider element at first place among children + $('.o_item_option:nth-child(2)').click(); assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 1, 'should have a facet'); - $('.o_item_option:nth-child(2)').click(); + $('.o_item_option:nth-child(3)').click(); assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 1, 'should have a facet'); - $('.o_item_option:nth-child(2)').click(); + $('.o_item_option:nth-child(3)').click(); assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 0, 'should have no facet'); actionManager.destroy(); @@ -213,12 +221,26 @@ QUnit.module('Search View', { }); QUnit.test('group by a date field using interval works', function (assert) { - assert.expect(9); + assert.expect(13); + + + var groupbys = [ + ["date_field:day"], + ["date_field:month"], + ["birthday:month"], + ["birthday:year"], + ]; var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/read_group') { + assert.deepEqual(args.kwargs.groupby, groupbys.shift()); + } + return this._super.apply(this, arguments); + }, }); actionManager.doAction(5); @@ -226,32 +248,31 @@ QUnit.module('Search View', { $('.o_search_options .fa-bars').click(); // Activate the groupby 'Date' $('.o_group_by_menu .o_menu_item').click(); - // by default, data should be grouped by the field 'Date' using the interval 'day' + // // select option 'day' + $('.o_group_by_menu .o_menu_item .o_item_option[data-option_id="day"]').click(); assert.strictEqual($('div.o_facet_values span').text().trim(),'Date: Day'); assert.strictEqual($('.o_content tr.o_group_header').length, 5); - // open submenu with interval options - $('.o_group_by_menu .o_menu_item .o_submenu_switcher').click(); - // select option 'month' - $('.o_group_by_menu .o_menu_item .o_item_option:nth-child(3)').click(); - // data should be grouped by the field 'Date' using the interval 'month' + // // select option 'month' + $('.o_group_by_menu .o_menu_item .o_item_option[data-option_id="month"]').click(); + // // data should be grouped by the field 'Date' using the interval 'month' assert.strictEqual($('div.o_facet_values span').text().trim(),'Date: Month'); assert.strictEqual($('.o_content tr.o_group_header').length, 3); - // deactivate option 'month' - $('.o_group_by_menu .o_menu_item .o_item_option:nth-child(3)').click(); - // no groupby is applied + // // deactivate option 'month' + $('.o_group_by_menu .o_menu_item .o_item_option[data-option_id="month"]').click(); + // // no groupby is applied assert.strictEqual($('div.o_facet_values span').length, 0); - // open 'Add custom Groupby' menu + // // open 'Add custom Groupby' menu $('.o_group_by_menu .o_add_custom_group a').click(); - // click on 'Apply' button + // // click on 'Apply' button $('.o_group_by_menu .o_generator_menu button').click(); - // data should be grouped by the field 'Birthday' using the interval 'month' + // // data should be grouped by the field 'Birthday' using the interval 'month' assert.strictEqual($('div.o_facet_values span').text().trim(),'Birthday: Month'); assert.strictEqual($('.o_content tr.o_group_header').length, 5); - // open submenu with interval options + // // open submenu with interval options $('.o_group_by_menu .o_menu_item .o_submenu_switcher').eq(1).click(); - // select option 'year' + // // select option 'year' $('.o_group_by_menu .o_menu_item .o_item_option').eq(9).click(); - // data should be grouped by the field 'Birthday' using the interval 'year' + // // data should be grouped by the field 'Birthday' using the interval 'year' assert.strictEqual($('div.o_facet_values span').text().trim(),'Birthday: Year'); assert.strictEqual($('.o_content tr.o_group_header').length, 4); actionManager.destroy(); @@ -295,5 +316,121 @@ QUnit.module('Search View', { 'no facet should be in the search view'); actionManager.destroy(); }); + + QUnit.test('filter by a date field using period works', function (assert) { + + assert.expect(14); + + this.archs['partner,4,search'] = '<search>'+ + '<filter string="AAA" name="some_filter" date="date_field" default_period="this_week"></filter>' + + '</search>'; + + var domains = [ + [], + // today + ['&', ["date_field", ">=", "2017-03-22"],["date_field", "<", "2017-03-23"]], + // this week + ['&', ["date_field", ">=", "2017-03-20"],["date_field", "<", "2017-03-27"]], + // this month + ['&', ["date_field", ">=", "2017-03-01"],["date_field", "<", "2017-04-01"]], + // this quarter + ['&', ["date_field", ">=", "2017-01-01"],["date_field", "<", "2017-04-01"]], + // this year + ['&', ["date_field", ">=", "2017-01-01"],["date_field", "<", "2018-01-01"]], + // yesterday + ['&', ["date_field", ">=", "2017-03-21"],["date_field", "<", "2017-03-22"]], + // last week + ['&', ["date_field", ">=", "2017-03-13"],["date_field", "<", "2017-03-20"]], + // last month + ['&', ["date_field", ">=", "2017-02-01"],["date_field", "<", "2017-03-01"]], + // last quarter + ['&', ["date_field", ">=", "2016-10-01"],["date_field", "<", "2017-01-01"]], + // last year + ['&', ["date_field", ">=", "2016-01-01"],["date_field", "<", "2017-01-01"]], + // last 7 days (whole days) + ['&', ["date_field", ">=", "2017-03-15"],["date_field", "<", "2017-03-22"]], + // last 30 days + ['&', ["date_field", ">=", "2017-02-20"],["date_field", "<", "2017-03-22"]], + // last 365 days + ['&', ["date_field", ">=", "2016-03-22"],["date_field", "<", "2017-03-22"]], + ]; + + var RealDate = window.Date; + + window.Date = function TestDate() { + // month are indexed from 0! + return new RealDate(2017,2,22); + }; + window.Date.now = function Test() { + return new Date(2017,2,22); + }; + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + if (route === '/web/dataset/search_read') { + assert.deepEqual(args.domain, domains.shift()); + } + return this._super.apply(this, arguments); + }, + }); + + actionManager.doAction(5); + + // open menu 'Filter' + $('.o_search_options .fa-filter').click(); + // open menu options + $('li.o_menu_item').click(); + $('li.o_menu_item .o_item_option[data-option_id="today"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="this_week"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="this_month"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="this_quarter"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="this_year"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="yesterday"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_week"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_month"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_quarter"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_year"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_7_days"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_30_days"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_365_days"]').click(); + + actionManager.destroy(); + window.Date = RealDate; + }); + + QUnit.module('Favorites Menu'); + + QUnit.test('dynamic filters are saved dynamic', function (assert) { + assert.expect(1); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + intercepts: { + create_filter: function (ev) { + console.log(ev.data); + assert.equal( + ev.data.filter.domain, + "['&', " + + "('date_field', '>=', (context_today() + relativedelta()).strftime('%Y-%m-%d')), " + + "('date_field', '<', (context_today() + relativedelta(days = 1)).strftime('%Y-%m-%d'))"+ + "]"); + }, + }, + }); + + actionManager.doAction(6); + $('span.fa-filter').click(); + $('.o_filters_menu li.o_menu_item a').click(); + $('.o_item_option:first').click(); + $('span.fa-star').click(); + $('.o_favorites_menu .o_save_search a').click(); + $('.o_favorites_menu .o_save_name button').click(); + actionManager.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index 2d6af1b5e14..14ec4402061 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -391,7 +391,7 @@ QUnit.module('Views', { intercepts: { create_filter: function (event) { var filter = event.data.filter; - assert.deepEqual(filter.domain, [['bar', '=', true]], + assert.deepEqual(filter.domain, "[('bar', '=', True)]", "should save the correct domain"); assert.deepEqual(filter.context, {shouldBeInFilterContext: true}, "should save the correct context"); diff --git a/addons/web/static/tests/widgets/dropdown_menu_tests.js b/addons/web/static/tests/widgets/dropdown_menu_tests.js index 92444c57075..0669b92c87f 100644 --- a/addons/web/static/tests/widgets/dropdown_menu_tests.js +++ b/addons/web/static/tests/widgets/dropdown_menu_tests.js @@ -98,14 +98,16 @@ QUnit.module('Web', { QUnit.test('options rendering', function (assert) { assert.expect(3); - this.items[0].options = [{optionId: 1, description: "First Option"}, {optionId: 2, description: "Second Option"}]; + this.items[0].options = [{optionId: 1, description: "First Option", groupId: 1}, {optionId: 2, description: "Second Option", groupId: 1}]; var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items); // open dropdown dropdownMenu.$('button:first').click(); assert.strictEqual(dropdownMenu.$('li').length, 4); - dropdownMenu.$('span.fa-caret-down').click(); - assert.strictEqual(dropdownMenu.$('li').length, 6); + // open options menu + dropdownMenu.$('span.fa-caret-right').click(); + assert.strictEqual(dropdownMenu.$('li').length, 7); + // close options menu dropdownMenu.$('span.fa-caret-down').click(); assert.strictEqual(dropdownMenu.$('li').length, 4); @@ -119,8 +121,9 @@ QUnit.module('Web', { var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items); dropdownMenu.$('button:first').click(); - dropdownMenu.$('span.fa-caret-down').click(); - assert.strictEqual(dropdownMenu.$('li').length, 6); + // open options menu + dropdownMenu.$('span.fa-caret-right').click(); + assert.strictEqual(dropdownMenu.$('li').length, 7); dropdownMenu.$('button:first').click(); dropdownMenu.$('button:first').click(); assert.strictEqual(dropdownMenu.$('li').length, 4); @@ -158,24 +161,27 @@ QUnit.module('Web', { }, }, }); + // open dropdown menu dropdownMenu.$('button:first').click(); - dropdownMenu.$('span.fa-caret-down').click(); - assert.strictEqual(dropdownMenu.$('li').length, 6); - assert.ok(!dropdownMenu.$('.o_menu_item:first').hasClass('selected')); - assert.ok(!dropdownMenu.$('.o_item_option:first').hasClass('selected')); + // open options menu + dropdownMenu.$('span.fa-caret-right').click(); + assert.strictEqual(dropdownMenu.$('li').length, 7); + // Don't forget there is a hidden li.divider element at first place among children + assert.ok(!dropdownMenu.$('.o_menu_item:nth-child(2)').hasClass('selected')); assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected')); + assert.ok(!dropdownMenu.$('.o_item_option:nth-child(3)').hasClass('selected')); dropdownMenu.$('li.o_item_option:first').click(); - assert.ok(dropdownMenu.$('.o_menu_item:first').hasClass('selected')); - assert.ok(dropdownMenu.$('.o_item_option:first').hasClass('selected')); - assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected')); - dropdownMenu.$('li.o_item_option:nth-child(2)').click(); - assert.ok(dropdownMenu.$('.o_menu_item:first').hasClass('selected')); - assert.ok(!dropdownMenu.$('.o_item_option:first').hasClass('selected')); + assert.ok(dropdownMenu.$('.o_menu_item:nth-child(2)').hasClass('selected')); assert.ok(dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected')); - dropdownMenu.$('li.o_item_option:nth-child(2)').click(); - assert.ok(!dropdownMenu.$('.o_menu_item:first').hasClass('selected')); - assert.ok(!dropdownMenu.$('.o_item_option:first').hasClass('selected')); + assert.ok(!dropdownMenu.$('.o_item_option:nth-child(3)').hasClass('selected')); + dropdownMenu.$('li.o_item_option:nth-child(3)').click(); + assert.ok(dropdownMenu.$('.o_menu_item:nth-child(2)').hasClass('selected')); + assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected')); + assert.ok(dropdownMenu.$('.o_item_option:nth-child(3)').hasClass('selected')); + dropdownMenu.$('li.o_item_option:nth-child(3)').click(); + assert.ok(!dropdownMenu.$('.o_menu_item:nth-child(2)').hasClass('selected')); assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected')); + assert.ok(!dropdownMenu.$('.o_item_option:nth-child(3)').hasClass('selected')); dropdownMenu.destroy(); }); |
