diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index ab6f226a66e..16e928ea288 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -403,6 +403,18 @@ var AbstractField = Widget.extend({ _getClassFromDecoration: function (decoration) { return `text-${decoration.split('-')[1]}`; }, + /** + * Compares the given value with the last value that has been set. + * Note that we compare unparsed values. Handles the special case where no + * value has been set yet, and the given value is the empty string. + * + * @private + * @param {any} value + * @returns {boolean} true iff values are the same + */ + _isLastSetValue: function (value) { + return this.lastSetValue === value || (this.value === false && value === ''); + }, /** * This method check if a value is the same as the current value of the * field. For example, a fieldDate widget might want to use the moment @@ -499,9 +511,8 @@ var AbstractField = Widget.extend({ * @returns {Promise} */ _setValue: function (value, options) { - // we try to avoid doing useless work, if the value given has not - // changed. Note that we compare the unparsed values. - if (this.lastSetValue === value || (this.value === false && value === '')) { + // we try to avoid doing useless work, if the value given has not changed. + if (this._isLastSetValue(value)) { return Promise.resolve(); } this.lastSetValue = value; diff --git a/addons/web/static/src/js/fields/abstract_field_owl.js b/addons/web/static/src/js/fields/abstract_field_owl.js index f97faae35ed..43e3d468d78 100644 --- a/addons/web/static/src/js/fields/abstract_field_owl.js +++ b/addons/web/static/src/js/fields/abstract_field_owl.js @@ -453,6 +453,18 @@ odoo.define('web.AbstractFieldOwl', function (require) { _getClassFromDecoration(decoration) { return `text-${decoration.split('-')[1]}`; } + /** + * Compares the given value with the last value that has been set. + * Note that we compare unparsed values. Handles the special case where no + * value has been set yet, and the given value is the empty string. + * + * @private + * @param {any} value + * @returns {boolean} true iff values are the same + */ + _isLastSetValue(value) { + return this._lastSetValue === value || (this.value === false && value === ''); + } /** * This method check if a value is the same as the current value of the * field. For example, a fieldDate component might want to use the moment @@ -498,9 +510,8 @@ odoo.define('web.AbstractFieldOwl', function (require) { * @returns {Promise} */ _setValue(value, options) { - // we try to avoid doing useless work, if the value given has not - // changed. Note that we compare the unparsed values. - if (this._lastSetValue === value || (this.value === false && value === '')) { + // we try to avoid doing useless work, if the value given has not changed. + if (this._isLastSetValue(value)) { return Promise.resolve(); } this._lastSetValue = value; diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index bf225d7ea99..d5f1f1074d4 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -326,7 +326,7 @@ var InputField = DebouncedField.extend({ * @private */ _onInput: function () { - this.isDirty = true; + this.isDirty = !this._isLastSetValue(this.$input.val()); this._doDebouncedAction(); }, /** diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index c7040a98744..3d98ea984e7 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1743,6 +1743,54 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test('input field: set and remove value, then wait for onchange', async function (assert) { + assert.expect(2); + + this.data.partner.onchanges = { + product_id(obj) { + obj.foo = obj.product_id ? "onchange value" : false; + }, + }; + + let def; + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + +
`, + async mockRPC(route, args) { + const result = this._super(...arguments); + if (args.method === "onchange") { + await Promise.resolve(def); + } + return result; + }, + fieldDebounce: 1000, // needed to accurately mock what really happens + }); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + assert.strictEqual(form.$('input[name="foo"]').val(), ""); + + await testUtils.fields.editInput(form.$('input[name="foo"]'), "test"); // set value for foo + await testUtils.fields.editInput(form.$('input[name="foo"]'), ""); // remove value for foo + + // trigger the onchange by setting a product + await testUtils.fields.many2one.clickOpenDropdown('product_id'); + await testUtils.fields.many2one.clickHighlightedItem('product_id'); + assert.strictEqual(form.$('input[name="foo"]').val(), 'onchange value', + 'input should contain correct value after onchange'); + + form.destroy(); + }); + QUnit.module('UrlWidget'); QUnit.test('url widget in form view', async function (assert) {