diff --git a/addons/web/static/src/legacy/js/fields/basic_fields.js b/addons/web/static/src/legacy/js/fields/basic_fields.js
index 5ce2d004537..58b36a195a0 100644
--- a/addons/web/static/src/legacy/js/fields/basic_fields.js
+++ b/addons/web/static/src/legacy/js/fields/basic_fields.js
@@ -1068,10 +1068,83 @@ const RemainingDays = AbstractField.extend({
description: _lt("Remaining Days"),
supportedFieldTypes: ['date', 'datetime'],
+ /**
+ * @override
+ */
+ init() {
+ this._super(...arguments);
+ // use the session timezone when formatting dates
+ this.formatOptions.timezone = true;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns the associated element.
+ *
+ * @override
+ */
+ getFocusableElement() {
+ return this.datewidget && this.datewidget.$input || $();
+ },
+
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
+ /**
+ * @override
+ * @returns {string} the content of the input
+ */
+ _getValue() {
+ let value = this.datewidget.getValue();
+ if (this.field.type === "datetime") {
+ value = value && value.add(-this.getSession().getTZOffset(value), 'minutes');
+ }
+ return value;
+ },
+ /**
+ * Instantiates a new DateWidget datepicker.
+ *
+ * @private
+ */
+ _makeDatePicker() {
+ this.datepickerOptions = _.defaults({}, { defaultDate: this.value });
+ if (this.field.type === "datetime" && this.value) {
+ const offset = this.getSession().getTZOffset(this.value);
+ const displayedValue = this.value.clone().add(offset, 'minutes');
+ this.datepickerOptions.defaultDate = displayedValue;
+ }
+ if (this.field.type === "date") {
+ return new datepicker.DateWidget(this, this.datepickerOptions);
+ }
+ return new datepicker.DateTimeWidget(this, this.datepickerOptions);
+ },
+ /**
+ * Displays date/datetime picker in edit mode.
+ *
+ * @override
+ */
+ async _renderEdit() {
+ await this._super(...arguments);
+ if (this.datewidget) {
+ this.datewidget.destroy();
+ }
+
+ this.datewidget = this._makeDatePicker();
+ this.datewidget.on('datetime_changed', this, () => {
+ const value = this._getValue();
+ if ((!value && this.value) || (value && !this._isSameValue(value))) {
+ this._setValue(value);
+ }
+ });
+
+ await this.datewidget.appendTo('
');
+ this.$el.append(this.datewidget.$el);
+ },
+
/**
* 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
@@ -1079,7 +1152,7 @@ const RemainingDays = AbstractField.extend({
*
* @override
*/
- _render() {
+ _renderReadonly() {
if (this.value === false) {
this.$el.removeClass('font-weight-bold text-danger text-warning');
return;
diff --git a/addons/web/static/tests/legacy/fields/basic_fields_tests.js b/addons/web/static/tests/legacy/fields/basic_fields_tests.js
index 668834bb189..0b74bd29627 100644
--- a/addons/web/static/tests/legacy/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/legacy/fields/basic_fields_tests.js
@@ -4958,8 +4958,93 @@ QUnit.module('basic_fields', {
unpatchDate();
});
+ QUnit.test('remaining_days widget on a date field in multi edit list view', async function (assert) {
+ assert.expect(7);
+
+ 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: 8, date: false },
+ ];
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ });
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
+ assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow');
+
+ // select two records and edit them
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first .o_data_cell'));
+ assert.containsOnce(list, 'input.o_datepicker_input', 'should have date picker input');
+ await testUtils.fields.editAndTrigger(list.$('.o_datepicker_input'), '10/10/2017', ['input', 'change', 'focusout']);
+
+ assert.containsOnce(document.body, '.modal');
+ assert.strictEqual($('.modal .o_field_widget').text(), "In 2 days",
+ "should have 'In 2 days' value to change");
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ assert.strictEqual(list.$('.o_data_row:first .o_data_cell').text(), "In 2 days",
+ "should have 'In 2 days' as date field value");
+ assert.strictEqual(list.$('.o_data_row:nth(1) .o_data_cell').text(), "In 2 days",
+ "should have 'In 2 days' as date field value");
+
+ list.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('remaining_days widget, enter wrong value manually in multi edit list 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
+ { id: 2, date: '2017-10-09' }, // tomorrow
+ { id: 8, date: false },
+ ];
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ });
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
+ assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow');
+
+ // select two records and edit them
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first .o_data_cell'));
+ assert.containsOnce(list, 'input.o_datepicker_input', 'should have date picker input');
+ await testUtils.fields.editAndTrigger(list.$('.o_datepicker_input'), 'blabla', ['input', 'change']);
+ await testUtils.dom.click(list.$el);
+
+ assert.containsNone(document.body, '.modal');
+ assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
+ assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow');
+
+ list.destroy();
+ unpatchDate();
+ });
+
QUnit.test('remaining_days widget on a date field in form view', async function (assert) {
- assert.expect(4);
+ assert.expect(8);
const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
this.data.partner.records = [
@@ -4977,11 +5062,64 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('.o_field_widget').text(), 'Today');
assert.hasClass(form.$('.o_field_widget'), 'font-weight-bold text-warning');
- // in edit mode, this widget should not be editable.
+ // in edit mode, this widget should be editable.
await testUtils.form.clickEdit(form);
assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
- assert.containsOnce(form, 'div.o_field_widget[name=date]');
+ assert.containsOnce(form, 'div.o_field_widget[name=date] .o_datepicker');
+
+ await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 1,
+ "datepicker should be opened");
+
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day[data-day="10/09/2017"]'));
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), 'Tomorrow');
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day[data-day="10/07/2017"]'));
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), 'Yesterday');
+ assert.hasClass(form.$('.o_field_widget'), 'text-danger');
+
+ form.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('remaining_days widget on a datetime 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, datetime: '2017-10-08 10:00:00' }, // 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-warning');
+
+ // in edit mode, this widget should be editable.
+ await testUtils.form.clickEdit(form);
+
+ assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
+ assert.containsOnce(form, 'div.o_field_widget[name=datetime] .o_datepicker');
+
+ await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 1,
+ "datepicker should be opened");
+
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day[data-day="10/09/2017"]'));
+ await testUtils.dom.click($('a[data-action="close"]'));
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), 'Tomorrow');
form.destroy();
unpatchDate();