From 4f8860e5038711c0da5b26584c4b72663908ddbc Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 29 Apr 2020 07:21:37 +0000 Subject: [PATCH] [IMP] web: add RemainingDays field widget This widget can be used on date and datetime fields. In readonly, it displays the delta (in days) between the value of the field and today. In edit, it behaves like a regular date(time) widget. Part of task 2195254 --- .../static/src/js/fields/abstract_field.js | 5 +- .../web/static/src/js/fields/basic_fields.js | 44 ++++ .../static/src/js/fields/field_registry.js | 1 + .../static/tests/fields/basic_fields_tests.js | 197 ++++++++++++++++++ doc/reference/javascript_reference.rst | 9 +- 5 files changed, 253 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index 5ed42e54e04..4767f54d998 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -379,11 +379,12 @@ var AbstractField = Widget.extend({ * * @private * @param {any} value (from the field type) + * @param {string} [formatType=this.formatType] the formatter to use * @returns {string} */ - _formatValue: function (value) { + _formatValue: function (value, formatType) { var options = _.extend({}, this.nodeOptions, { data: this.recordData }, this.formatOptions); - return field_utils.format[this.formatType](value, this.field, options); + return field_utils.format[formatType || this.formatType](value, this.field, options); }, /** * Returns the className corresponding to a given decoration. A diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 721d6618e83..4c5bef2c366 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -887,6 +887,49 @@ var FieldDateTime = FieldDate.extend({ }, }); +const RemainingDays = FieldDate.extend({ + description: _lt("Remaining Days"), + supportedFieldTypes: ['date', 'datetime'], + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Displays the delta (in days) between the value of the field and today. If + * the delta is larger than 99 days, displays the date as usual (without + * time). + * + * @override + */ + _renderReadonly() { + if (this.value === false) { + this.$el.removeClass('text-bf text-danger text-warning'); + return; + } + // compare the value (in the user timezone) with now (also in the user + // timezone), to get a meaningful delta for the user + const nowUTC = moment().utc(); + const nowUserTZ = nowUTC.clone().add(session.getTZOffset(nowUTC), 'minutes'); + const valueUserTZ = this.value.clone().add(session.getTZOffset(this.value), 'minutes'); + const diffDays = valueUserTZ.startOf('day').diff(nowUserTZ.startOf('day'), 'days'); + let text; + if (Math.abs(diffDays) > 99) { + text = this._formatValue(this.value, 'date'); + } else if (diffDays === 0) { + text = _t("Today"); + } else if (diffDays < 0) { + text = diffDays === -1 ? _t("Yesterday") : _t(`${-diffDays} days ago`); + } else { + text = diffDays === 1 ? _t("Tomorrow") : _t(`In ${diffDays} days`); + } + this.$el.text(text).attr('title', this._formatValue(this.value, 'date')); + this.$el.toggleClass('text-bf', diffDays <= 0); + this.$el.toggleClass('text-danger', diffDays < 0); + this.$el.toggleClass('text-warning', diffDays === 0); + }, +}); + var FieldMonetary = NumericField.extend({ description: _lt("Monetary"), className: 'o_field_monetary o_field_number', @@ -3437,6 +3480,7 @@ return { FieldDate: FieldDate, FieldDateTime: FieldDateTime, FieldDateRange: FieldDateRange, + RemainingDays: RemainingDays, FieldDomain: FieldDomain, FieldFloat: FieldFloat, FieldFloatTime: FieldFloatTime, diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js index b8b3fe61452..3cadf6cf6fa 100644 --- a/addons/web/static/src/js/fields/field_registry.js +++ b/addons/web/static/src/js/fields/field_registry.js @@ -34,6 +34,7 @@ registry .add('date', basic_fields.FieldDate) .add('datetime', basic_fields.FieldDateTime) .add('daterange', basic_fields.FieldDateRange) + .add('remaining_days', basic_fields.RemainingDays) .add('domain', basic_fields.FieldDomain) .add('text', basic_fields.FieldText) .add('list.text', basic_fields.ListFieldText) diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 1f78efeb7f2..5f4c4eb8dcb 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -15,6 +15,8 @@ var testUtilsDom = require('web.test_utils_dom'); var field_registry = require('web.field_registry'); var createView = testUtils.createView; +var patchDate = testUtils.mock.patchDate; + var DebouncedField = basicFields.DebouncedField; var JournalDashboardGraph = basicFields.JournalDashboardGraph; var _t = core._t; @@ -4193,6 +4195,201 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.module('RemainingDays'); + + QUnit.test('remaining_days widget on a date field in list view', async function (assert) { + assert.expect(16); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11 + this.data.partner.records = [ + { id: 1, date: '2017-10-08' }, // today + { id: 2, date: '2017-10-09' }, // tomorrow + { id: 3, date: '2017-10-07' }, // yesterday + { id: 4, date: '2017-10-10' }, // + 2 days + { id: 5, date: '2017-10-05' }, // - 3 days + { id: 6, date: '2018-02-08' }, // + 4 months (diff >= 100 days) + { id: 7, date: '2017-06-08' }, // - 4 months (diff >= 100 days) + { id: 8, date: false }, + ]; + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '', + }); + + assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Yesterday'); + assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'In 2 days'); + assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '3 days ago'); + assert.strictEqual(list.$('.o_data_cell:nth(5)').text(), '02/08/2018'); + assert.strictEqual(list.$('.o_data_cell:nth(6)').text(), '06/08/2017'); + assert.strictEqual(list.$('.o_data_cell:nth(7)').text(), ''); + + assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/08/2017'); + + assert.hasClass(list.$('.o_data_cell:nth(0) span'), 'text-bf text-warning'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(1) span'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(2) span'), 'text-bf text-danger'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(3) span'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(4) span'), 'text-bf text-danger'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(5) span'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(6) span'), 'text-bf text-danger'); + + list.destroy(); + unpatchDate(); + }); + + QUnit.test('remaining_days widget on a date field in form view', async function (assert) { + assert.expect(6); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11 + this.data.partner.records = [ + { id: 1, date: '2017-10-08' }, // today + ]; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
', + res_id: 1, + }); + + assert.strictEqual(form.$('.o_field_widget').text(), 'Today'); + assert.hasClass(form.$('.o_field_widget'), 'text-bf text-warning'); + + // in edit mode, this widget should behave like a regular date(time) widget + await testUtils.form.clickEdit(form); + + assert.hasClass(form.$('.o_form_view'), 'o_form_editable'); + assert.containsOnce(form, '.o_datepicker'); + assert.strictEqual(form.$('.o_datepicker_input').val(), '10/08/2017'); + + await testUtils.dom.openDatepicker(form.$('.o_datepicker')); + + assert.containsOnce(document.body, '.bootstrap-datetimepicker-widget:visible'); + + form.destroy(); + unpatchDate(); + }); + + QUnit.test('remaining_days widget on a datetime field in list view in UTC', async function (assert) { + assert.expect(16); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11 + this.data.partner.records = [ + { id: 1, datetime: '2017-10-08 20:00:00' }, // today + { id: 2, datetime: '2017-10-09 08:00:00' }, // tomorrow + { id: 3, datetime: '2017-10-07 18:00:00' }, // yesterday + { id: 4, datetime: '2017-10-10 22:00:00' }, // + 2 days + { id: 5, datetime: '2017-10-05 04:00:00' }, // - 3 days + { id: 6, datetime: '2018-02-08 04:00:00' }, // + 4 months (diff >= 100 days) + { id: 7, datetime: '2017-06-08 04:00:00' }, // - 4 months (diff >= 100 days) + { id: 6, datetime: false }, + ]; + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '', + session: { + getTZOffset: () => 0, + }, + }); + + assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Yesterday'); + assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'In 2 days'); + assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '3 days ago'); + assert.strictEqual(list.$('.o_data_cell:nth(5)').text(), '02/08/2018'); + assert.strictEqual(list.$('.o_data_cell:nth(6)').text(), '06/08/2017'); + assert.strictEqual(list.$('.o_data_cell:nth(7)').text(), ''); + + assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/08/2017'); + + assert.hasClass(list.$('.o_data_cell:nth(0) span'), 'text-bf text-warning'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(1) span'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(2) span'), 'text-bf text-danger'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(3) span'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(4) span'), 'text-bf text-danger'); + assert.doesNotHaveClass(list.$('.o_data_cell:nth(5) span'), 'text-bf text-warning text-danger'); + assert.hasClass(list.$('.o_data_cell:nth(6) span'), 'text-bf text-danger'); + + list.destroy(); + unpatchDate(); + }); + + QUnit.test('remaining_days widget on a datetime field in list view in UTC+6', async function (assert) { + assert.expect(6); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11, UTC+6 + this.data.partner.records = [ + { id: 1, datetime: '2017-10-08 20:00:00' }, // tomorrow + { id: 2, datetime: '2017-10-09 08:00:00' }, // tomorrow + { id: 3, datetime: '2017-10-07 18:30:00' }, // today + { id: 4, datetime: '2017-10-07 12:00:00' }, // yesterday + { id: 5, datetime: '2017-10-09 20:00:00' }, // + 2 days + ]; + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '', + session: { + getTZOffset: () => 360, + }, + }); + + assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'Yesterday'); + assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), 'In 2 days'); + + assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/09/2017'); + + list.destroy(); + unpatchDate(); + }); + + QUnit.test('remaining_days widget on a datetime field in list view in UTC-8', async function (assert) { + assert.expect(5); + + const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11, UTC-8 + this.data.partner.records = [ + { id: 1, datetime: '2017-10-08 20:00:00' }, // today + { id: 2, datetime: '2017-10-09 07:00:00' }, // today + { id: 3, datetime: '2017-10-09 10:00:00' }, // tomorrow + { id: 4, datetime: '2017-10-08 06:00:00' }, // yesterday + { id: 5, datetime: '2017-10-07 02:00:00' }, // - 2 days + ]; + + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '', + session: { + getTZOffset: () => -560, + }, + }); + + assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Today'); + assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Tomorrow'); + assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'Yesterday'); + assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '2 days ago'); + + list.destroy(); + unpatchDate(); + }); + QUnit.module('FieldMonetary'); QUnit.test('monetary field in form view', async function (assert) { diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index 7d5b6e6af35..92c6a292ff9 100644 --- a/doc/reference/javascript_reference.rst +++ b/doc/reference/javascript_reference.rst @@ -1686,7 +1686,7 @@ order. - daterange (FieldDateRange) - This widget allow user to select start and end date into single picker. + This widget allows the user to select start and end date into a single picker. - Supported field types: *date*, *datetime* @@ -1702,6 +1702,13 @@ order. +- remaining_days (RemainingDays) + This widget can be used on date and datetime fields. In readonly, it displays + the delta (in days) between the value of the field and today. It edit, it + behaves like a regular date(time) widget. + + - Supported field types: *date*, *datetime* + - monetary (FieldMonetary) This is the default field type for fields of type 'monetary'. It is used to display a currency. If there is a currency fields given in option, it will