diff --git a/addons/web/static/src/model/relational_model/record.js b/addons/web/static/src/model/relational_model/record.js index 940a66ea7e8..48fe6b01478 100644 --- a/addons/web/static/src/model/relational_model/record.js +++ b/addons/web/static/src/model/relational_model/record.js @@ -307,7 +307,7 @@ export class Record extends DataPoint { _askChanges() { const proms = []; this.model.bus.trigger("NEED_LOCAL_CHANGES", { proms }); - return Promise.all(proms); + return Promise.all([...proms, this.model.mutex.getUnlockedDef()]); } _checkValidity({ silent } = {}) { diff --git a/addons/web/static/src/model/relational_model/static_list.js b/addons/web/static/src/model/relational_model/static_list.js index 7f7e98e02f8..ad68f56d551 100644 --- a/addons/web/static/src/model/relational_model/static_list.js +++ b/addons/web/static/src/model/relational_model/static_list.js @@ -288,10 +288,12 @@ export class StaticList extends DataPoint { }); } - leaveEditMode({ discard, canAbandon, validate } = {}) { + async leaveEditMode({ discard, canAbandon, validate } = {}) { + if (this.editedRecord) { + await this.editedRecord._askChanges(false); + } return this.model.mutex.exec(async () => { if (this.editedRecord) { - await this.editedRecord._askChanges(); const isValid = this.editedRecord._checkValidity(); if (!isValid && validate) { return false; diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js index df2af077a5e..c698430d469 100644 --- a/addons/web/static/src/views/list/list_renderer.js +++ b/addons/web/static/src/views/list/list_renderer.js @@ -2024,7 +2024,7 @@ export class ListRenderer extends Component { * @param {HTMLElement} [params.previous] */ async sortDrop(dataRowId, { element, previous }) { - this.props.list.leaveEditMode(); + await this.props.list.leaveEditMode(); element.classList.remove("o_row_draggable"); const refId = previous ? previous.dataset.id : null; this.resequencePromise = this.props.list.resequence(dataRowId, refId, { diff --git a/addons/web/static/tests/views/fields/one2many_field_tests.js b/addons/web/static/tests/views/fields/one2many_field_tests.js index 67819ef45db..a8bf7015eb6 100644 --- a/addons/web/static/tests/views/fields/one2many_field_tests.js +++ b/addons/web/static/tests/views/fields/one2many_field_tests.js @@ -13177,4 +13177,62 @@ QUnit.module("Fields", (hooks) => { assert.containsOnce(target, ".o_list_view"); assert.verifySteps([]); }); + + QUnit.test( + "one2many: save a record before the onchange is complete in a form dialog", + async function (assert) { + serverData.models.turtle.onchanges = { + display_name: function () {}, + }; + + serverData.views = { + "turtle,false,form": ` +
`, + }; + + const def = makeDeferred(); + await makeView({ + type: "form", + resModel: "partner", + serverData, + resId: 1, + arch: ` + `, + async mockRPC(route, args) { + if ( + args.method === "onchange2" && + args.args[2].length === 1 && + args.args[2][0] === "display_name" + ) { + await def; + } + }, + }); + await addRow(target); + assert.containsOnce(target, ".modal"); + + await editInput(target, ".o_field_widget[name=display_name] input", "new name"); + await click(target, ".modal .o_form_button_save"); + assert.containsOnce(target, ".modal"); + + def.resolve(); + await nextTick(); + assert.containsNone(target, ".modal"); + assert.containsN(target, ".o_data_row", 2); + assert.deepEqual( + [...target.querySelectorAll(".o_data_row [name='display_name']")].map( + (el) => el.textContent + ), + ["donatello", "new name"] + ); + } + ); });