diff --git a/web_widget_date_interval/DESCRIPTION.rst b/web_widget_date_interval/DESCRIPTION.rst new file mode 100644 index 00000000..dd5baa8e --- /dev/null +++ b/web_widget_date_interval/DESCRIPTION.rst @@ -0,0 +1 @@ +This module was written to add support for a date interval widget. diff --git a/web_widget_date_interval/DEVELOP.rst b/web_widget_date_interval/DEVELOP.rst new file mode 100644 index 00000000..3d4f7081 --- /dev/null +++ b/web_widget_date_interval/DEVELOP.rst @@ -0,0 +1,28 @@ +Search views +============ + +On search views, this addon allows developers to show a predefined range of intervals. Use ``widget="date_interval"`` and an options dictionary to configure the addon, at least a type of interval. Currently, the only supported interval type is `iso_week`: ``options="{'type': 'iso_week'}"``. + +Configuration options +--------------------- + + type + Possible values are ``iso_week`` (shows iso week numbers) + + date + The reference date from which to calculate intervals if different from `now` + + lookahead + The amount of intervals after `date` to offer to the user + + lookbehind + The amount of intervals before `date` to offer to the user + + exclusive + If truthy, selecting one interval will unselect all already selected intervals. If not set (default), selecting multiple intervals will yield a domain for all selected intervals + + cycle + If truthy, the currently selected interval will be used for setting the reference date. The effect is that you can cycle through intervals rapidly in combination with `exclusive = 1` and `dropdown = 1` + + dropdown + If truthy (default), the intervals to choose are shown as a dropdown menu diff --git a/web_widget_date_interval/README.rst b/web_widget_date_interval/README.rst new file mode 100644 index 00000000..b498fd49 --- /dev/null +++ b/web_widget_date_interval/README.rst @@ -0,0 +1 @@ +/ diff --git a/web_widget_date_interval/ROADMAP.rst b/web_widget_date_interval/ROADMAP.rst new file mode 100644 index 00000000..dc7a9ffa --- /dev/null +++ b/web_widget_date_interval/ROADMAP.rst @@ -0,0 +1,4 @@ +- migrate the work done in https://github.com/OCA/web/pull/575 to 10 for the form view part +- support tree and kanban views +- support much more interval types (days, months, quarters...) +- invent some textual way to fill in some interval (-1w+3w for now - one week to now + three weeks) diff --git a/web_widget_date_interval/__init__.py b/web_widget_date_interval/__init__.py new file mode 100644 index 00000000..8c501711 --- /dev/null +++ b/web_widget_date_interval/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). diff --git a/web_widget_date_interval/__manifest__.py b/web_widget_date_interval/__manifest__.py new file mode 100644 index 00000000..68e04723 --- /dev/null +++ b/web_widget_date_interval/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Date interval widget", + "version": "10.0.1.0.0", + "author": "Therp BV,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Hidden/Dependency", + "summary": "Widget for date intervals", + "depends": [ + 'web', + ], + "demo": [ + "demo/res_users.xml", + ], + "data": [ + 'views/templates.xml', + ], + "qweb": [ + 'static/src/xml/web_widget_date_interval.xml', + ], +} diff --git a/web_widget_date_interval/demo/res_users.xml b/web_widget_date_interval/demo/res_users.xml new file mode 100644 index 00000000..474bcd46 --- /dev/null +++ b/web_widget_date_interval/demo/res_users.xml @@ -0,0 +1,13 @@ + + + + res.users + + + + + + + + + diff --git a/web_widget_date_interval/static/description/icon.png b/web_widget_date_interval/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/web_widget_date_interval/static/description/icon.png differ diff --git a/web_widget_date_interval/static/src/css/web_widget_date_interval.css b/web_widget_date_interval/static/src/css/web_widget_date_interval.css new file mode 100644 index 00000000..ef9b6bf4 --- /dev/null +++ b/web_widget_date_interval/static/src/css/web_widget_date_interval.css @@ -0,0 +1,8 @@ +div.o_date_interval > button > a +{ + color: #4c4c4c; +} +div.o_date_interval > button > a.selected +{ + font-weight: bold; +} diff --git a/web_widget_date_interval/static/src/js/web_widget_date_interval.js b/web_widget_date_interval/static/src/js/web_widget_date_interval.js new file mode 100644 index 00000000..6fc87ceb --- /dev/null +++ b/web_widget_date_interval/static/src/js/web_widget_date_interval.js @@ -0,0 +1,245 @@ +//-*- coding: utf-8 -*- +//Copyright 2018 Therp BV +//License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +odoo.define('web_widget_date_interval', function(require) { + var core = require('web.core'), + pyeval = require('web.pyeval'), + Widget = require('web.Widget'), + SearchView = require('web.SearchView'); + + var SearchWidgetDateInterval = Widget.extend({ + template: 'SearchView.DateInterval', + events: { + 'click [data-id]': 'on_click_interval', + }, + _option_defaults: { + iso_week: { + lookbehind: 1, + lookahead: 10, + exclusive: false, + cycle: false, + }, + }, + options: { + type: 'iso_week', + dropdown: true, + }, + init: function(parent, field_name, string, options) { + this.searchview = parent; + this.string = string; + this.options = _.extend( + {}, this.options, this._option_defaults[options.type] || {}, + options + ); + this.field = field_name; + return this._super.apply(this, arguments); + }, + start: function() { + this.searchview.query.on( + 'add change remove reset', this.proxy('on_searchview_change') + ); + return this._super.apply(this, arguments); + }, + get_intervals: function() { + var interval_function = _.str.sprintf( + 'get_intervals_%s', this.options.type + ); + if(!this[interval_function]) { + throw new Error('Unknown interval type given'); + } + return this[interval_function](); + }, + get_intervals_iso_week: function() { + var existing = this.searchview.query.findWhere({ + category: this.string, + }), + reference_date = moment( + !existing || !this.options.cycle + ? this.options.date + : existing.get('field').get_domain().slice(-2, -1)[0][2] + ), + start_date = reference_date.clone().startOf('isoWeek') + .subtract(this.options.lookbehind, 'weeks'), + stop_date = reference_date.clone().endOf('isoWeek') + .add(this.options.lookahead, 'weeks'), + current_date = start_date.clone(), + result = []; + + while(current_date.isBefore(stop_date)) { + result.push({ + name: current_date.year() === moment().year() + ? _.str.sprintf(core._t('%s'), current_date.isoWeek()) + : _.str.sprintf( + core._t('%s / %s'), current_date.isoWeek(), + current_date.isoWeekYear() + ), + _id: _.str.sprintf( + '%s-%s', this.field, current_date.format('YYYY-MM-DD') + ), + start: current_date.format('YYYY-MM-DD'), + stop: current_date.add(1, 'weeks').format('YYYY-MM-DD'), + }); + } + return result; + }, + on_click_interval: function(e) { + var $this = jQuery(e.currentTarget); + return this._update_searchview( + $this.data('start'), $this.data('stop'), $this.text().trim() + ); + }, + _create_facet: function(date_start, date_stop, label) { + var self = this; + + return { + field: { + get_domain: function() { + return [ + '&', + [self.field, '>=', date_start], + [self.field, '<', date_stop], + ]; + }, + // eslint-disable-next-line no-empty-function + get_context: function() {}, + // eslint-disable-next-line no-empty-function + get_groupby: function() {}, + }, + category: this.string, + icon: 'fa-calendar', + values: [ + { + label: _.str.sprintf( + '%s: %s', this.string, label + ), + value: null, + }, + ], + _id: _.str.sprintf( + '%s-%s', self.field, date_start + ), + }; + }, + _update_searchview: function(date_start, date_stop, label, options) { + var facet = this._create_facet(date_start, date_stop, label), + existing = this.searchview.query.findWhere({ + category: this.string, + }); + if(existing) { + var is_removal = existing.get('_id').includes(facet._id); + + this.searchview.query.remove(existing, {silent: !is_removal}); + + if(!this.options.exclusive) { + // concatenate existing facet with ours + var domain = [].concat( + ['|'], facet.field.get_domain(), + existing.get('field').get_domain() + ); + facet._id = _.str.sprintf( + '%s %s', facet._id, existing.get('_id') + ); + facet.values = facet.values.concat(existing.get('values')); + facet.field.get_domain = function() { + return domain; + }; + } + + if(is_removal) { + return; + } + } + this.searchview.query.add(facet, options); + }, + on_searchview_change: function() { + var self = this; + this.$('[data-id]').removeClass('selected'); + if(this.options.cycle) { + this.renderElement(); + } + this.searchview.query.each(function(facet) { + if(facet.get('category') === self.string) { + self.$( + _.map( + facet.get('_id').split(' '), function(x) { + return _.str.sprintf('[data-id="%s"]', x); + } + ).join(',') + ).addClass('selected'); + } + }); + }, + facet_for_defaults: function(values) { + var self = this, + default_value = values[_.str.sprintf( + 'date_interval_%s', this.field + )], + facet = false; + + if(!default_value) { + return; + } + + default_value = moment(default_value); + + _(this.get_intervals()).each(function(interval) { + if( + !moment(interval.start).isBefore(default_value) || + !moment(interval.stop).isAfter(default_value) + ) { + return; + } + facet = self._create_facet( + interval.start, interval.stop, interval.name + ); + }); + return facet; + }, + visible: function() { + return false; + }, + }); + + SearchView.include({ + init: function() { + this._super.apply(this, arguments); + this.date_intervals = []; + }, + start: function() { + var self = this, + deferreds = [this._super.apply(this, arguments)]; + + if(this.$buttons && !this.options.disable_date_interval) { + _(this.date_intervals).each(function(widget) { + deferreds.push(widget.appendTo(self.$buttons)); + }); + } + + return jQuery.when.apply(jQuery, deferreds); + }, + prepare_search_inputs: function() { + this._super.apply(this, arguments); + + var self = this; + + _.chain(this.fields_view.arch.children) + .filter(function(x) { + return x.tag === 'field' && x.attrs.widget === 'date_interval'; + }) + .each(function(x) { + var widget = new SearchWidgetDateInterval( + self, x.attrs.name, core._t(x.attrs.string) || + self.ViewManager.search_fields_view.fields[x.attrs.name] + .string, pyeval.py_eval(x.attrs.options || '{}') + ); + self.date_intervals.push(widget); + self.search_fields.push(widget); + }); + }, + }); + + return { + search_widget_date_interval: SearchWidgetDateInterval, + }; +}); diff --git a/web_widget_date_interval/static/src/xml/web_widget_date_interval.xml b/web_widget_date_interval/static/src/xml/web_widget_date_interval.xml new file mode 100644 index 00000000..c7622c87 --- /dev/null +++ b/web_widget_date_interval/static/src/xml/web_widget_date_interval.xml @@ -0,0 +1,21 @@ + +
+ + +
+
diff --git a/web_widget_date_interval/views/templates.xml b/web_widget_date_interval/views/templates.xml new file mode 100644 index 00000000..2e2db9e0 --- /dev/null +++ b/web_widget_date_interval/views/templates.xml @@ -0,0 +1,9 @@ + + + +