diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 767a6511ce3..43d604eac0b 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -173,15 +173,20 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { // may have to validate them before notifying them, so we ask them to // commit their current value before saving. This has to be done outside // of the mutex protection of saving because commitChanges will trigger - // changes and these are also protected. So the actual saving has to be + // changes and these are also protected. However, we must wait for the + // mutex to be idle to ensure that onchange RPCs returned before asking + // field widgets to commit their value (and validate it, for instance + // for one2many with required fields). So the actual saving has to be // done after these changes. Also the commitChanges operation might not // be synchronous for other reason (e.g. the x2m fields will ask the // user if some discarding has to be made). This operation must also be // mutex-protected as commitChanges function of x2m has to be aware of // all final changes made to a row. var self = this; - return this.renderer.commitChanges(recordID || this.handle).then(function () { - return self.mutex.exec(self._saveRecord.bind(self, recordID, options)); + return this.mutex.getUnlockedDef().then(function () { + return self.renderer.commitChanges(recordID || self.handle).then(function () { + return self.mutex.exec(self._saveRecord.bind(self, recordID, options)); + }); }); }, /** diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 1d89375c547..0cfc37daff5 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -7107,6 +7107,68 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('save record with onchange on one2many with required field', function (assert) { + // in this test, we have a one2many with a required field, whose value is + // set by an onchange on another field ; we manually set the value of that + // first field, and directly click on Save (before the onchange RPC returns + // and sets the value of the required field) + assert.expect(6); + + this.data.partner.fields.foo.default = undefined; + this.data.partner.onchanges = { + display_name: function (obj) { + obj.foo = obj.display_name ? 'foo value' : undefined; + }, + }; + + var onchangeDef; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
', + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'onchange') { + return $.when(onchangeDef).then(_.constant(result)); + } + if (args.method === 'create') { + assert.step('create'); + assert.strictEqual(args.args[0].p[0][2].foo, 'foo value', + "should have wait for the onchange to return before saving"); + } + return result; + }, + }); + + form.$('.o_field_x2many_list_row_add a').click(); + + assert.strictEqual(form.$('.o_field_widget[name=display_name]').val(), '', + "display_name should be the empty string by default"); + assert.strictEqual(form.$('.o_field_widget[name=foo]').val(), '', + "foo should be the empty string by default"); + + onchangeDef = $.Deferred(); // delay the onchange + + form.$('.o_field_widget[name=display_name]').val('some value').trigger('input'); + + form.$buttons.find('.o_form_button_save').click(); + + assert.step('resolve'); + onchangeDef.resolve(); + + assert.verifySteps(['resolve', 'create']); + + form.destroy(); + }); + QUnit.module('FormViewTABMainButtons'); QUnit.test('using tab in an empty required string field should not move to the next field',function(assert) {