diff --git a/addons/event_booth_sale/static/src/js/sale_product_field.js b/addons/event_booth_sale/static/src/js/sale_product_field.js index 333c706b939..4b0648a8e0a 100644 --- a/addons/event_booth_sale/static/src/js/sale_product_field.js +++ b/addons/event_booth_sale/static/src/js/sale_product_field.js @@ -1,7 +1,8 @@ /** @odoo-module **/ -import { patch } from "@web/core/utils/patch"; import { SaleOrderLineProductField } from '@sale/js/sale_product_field'; +import { x2ManyCommands } from "@web/core/orm_service"; +import { patch } from "@web/core/utils/patch"; patch(SaleOrderLineProductField.prototype, 'event_booth_sale', { @@ -60,13 +61,10 @@ patch(SaleOrderLineProductField.prototype, 'event_booth_sale', { } else { const { event_id, event_booth_category_id, event_booth_pending_ids } = closeInfo.eventBoothConfiguration; - this.props.record.data.event_booth_pending_ids.replaceWith( - event_booth_pending_ids, - { silent: true } - ); this.props.record.update({ event_id, event_booth_category_id, + event_booth_pending_ids: [x2ManyCommands.replaceWith(event_booth_pending_ids)], }); } } diff --git a/addons/sale_product_configurator/static/src/js/sale_product_field.js b/addons/sale_product_configurator/static/src/js/sale_product_field.js index 7761df6c7fd..58b5feaab22 100644 --- a/addons/sale_product_configurator/static/src/js/sale_product_field.js +++ b/addons/sale_product_configurator/static/src/js/sale_product_field.js @@ -1,9 +1,10 @@ /** @odoo-module */ -import { patch } from "@web/core/utils/patch"; -import { useService } from "@web/core/utils/hooks"; import { SaleOrderLineProductField } from '@sale/js/sale_product_field'; import { serializeDateTime } from "@web/core/l10n/dates"; +import { x2ManyCommands } from "@web/core/orm_service"; +import { useService } from "@web/core/utils/hooks"; +import { patch } from "@web/core/utils/patch"; import { ProductConfiguratorDialog } from "./product_configurator_dialog/product_configurator_dialog"; async function applyProduct(record, product) { @@ -28,11 +29,11 @@ async function applyProduct(record, product) { ptal => ptal.create_variant === "no_variant" && ptal.attribute_values.length > 1 ).map(ptal => ptal.selected_attribute_value_id); - proms.push(record.data.product_no_variant_attribute_value_ids.replaceWith(noVariantPTAVIds, { silent: true })); await Promise.all(proms); await record.update({ product_id: [product.id, product.display_name], product_uom_qty: product.quantity, + product_no_variant_attribute_value_ids: [x2ManyCommands.replaceWith(noVariantPTAVIds)], }); }; diff --git a/addons/survey/static/src/question_page/question_page_one2many_field.js b/addons/survey/static/src/question_page/question_page_one2many_field.js index d6a5519b9a0..015e6a36862 100644 --- a/addons/survey/static/src/question_page/question_page_one2many_field.js +++ b/addons/survey/static/src/question_page/question_page_one2many_field.js @@ -95,17 +95,19 @@ class QuestionPageOneToManyField extends X2ManyField { updateRecord, }); this._openRecord = async (params) => { - if (!await self.props.record.save()) { + const { record, name } = this.props; + if (!await record.save()) { // do not open question form as it won't be savable either. return; } if (params.record) { + params.record = record.data[name].records.find(r => r.resId === params.record.resId); // Force synchronization of fields that depend on sequence // (allowed_triggering_question_ids, is_placed_before_trigger) // as records may have been re-ordered before opening this one. await params.record.load(); } - openRecord(params); + await openRecord(params); }; this.canOpenRecord = true; } diff --git a/addons/survey/static/tests/components/question_page_one2many_field_tests.js b/addons/survey/static/tests/components/question_page_one2many_field_tests.js index afc182029ce..5c2a4579b3b 100644 --- a/addons/survey/static/tests/components/question_page_one2many_field_tests.js +++ b/addons/survey/static/tests/components/question_page_one2many_field_tests.js @@ -167,7 +167,7 @@ QUnit.module("QuestionPageOneToManyField", (hooks) => { await click(target.querySelector(".o_data_row:nth-child(2) .o_data_cell")); // Edit content to trigger the expected actual save at row opening assert.verifySteps(["save parent form"]); - assert.containsNone(target, ".o_selected_row"); + assert.containsOnce(target, ".o_selected_row"); assert.containsOnce(target, ".modal .o_form_view"); }); diff --git a/addons/web/static/src/model/relational_model/record.js b/addons/web/static/src/model/relational_model/record.js index fcfbcb5da0f..4bed78e5ce1 100644 --- a/addons/web/static/src/model/relational_model/record.js +++ b/addons/web/static/src/model/relational_model/record.js @@ -5,6 +5,7 @@ import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; import { Domain, evalDomain } from "@web/core/domain"; import { serializeDate, serializeDateTime } from "@web/core/l10n/dates"; import { _t } from "@web/core/l10n/translation"; +import { x2ManyCommands } from "@web/core/orm_service"; import { pick } from "@web/core/utils/objects"; import { escape } from "@web/core/utils/strings"; import { DataPoint } from "./datapoint"; @@ -409,14 +410,12 @@ export class Record extends DataPoint { limit: limit || Number.MAX_SAFE_INTEGER, context: {}, // will be set afterwards, see "_updateContext" in "_setEvalContext" }; - let staticList; const options = { onUpdate: ({ withoutOnchange } = {}) => - this._update({ [fieldName]: staticList }, { withoutOnchange }), + this._update({ [fieldName]: [] }, { withoutOnchange }), parent: this, }; - staticList = new this.model.constructor.StaticList(this.model, config, data, options); - return staticList; + return new this.model.constructor.StaticList(this.model, config, data, options); } _discard() { @@ -630,6 +629,7 @@ export class Record extends DataPoint { await Promise.all([ this._preprocessMany2oneChanges(changes), this._preprocessReferenceChanges(changes), + this._preprocessX2manyChanges(changes), ]); } @@ -741,6 +741,28 @@ export class Record extends DataPoint { return Promise.all(proms); } + async _preprocessX2manyChanges(changes) { + for (const [fieldName, value] of Object.entries(changes)) { + if ( + this.fields[fieldName].type !== "one2many" && + this.fields[fieldName].type !== "many2many" + ) { + continue; + } + const list = this.data[fieldName]; + for (const command of value) { + switch (command[0]) { + case x2ManyCommands.REPLACE_WITH: + await list._replaceWith(command[2]); + break; + default: + await list._applyCommands([command]); + } + } + changes[fieldName] = list; + } + } + _removeInvalidFields(fieldNames) { for (const fieldName of fieldNames) { this._invalidFields.delete(fieldName); @@ -931,7 +953,7 @@ export class Record extends DataPoint { const initialChanges = pick(this._changes, ...Object.keys(changes)); this._applyChanges(changes); try { - await this._onUpdate(changes, { withoutParentUpdate }); + await this._onUpdate({ withoutParentUpdate }); } catch (e) { this._applyChanges(initialChanges); throw e; 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 d26cd5374ae..30f984ad000 100644 --- a/addons/web/static/src/model/relational_model/static_list.js +++ b/addons/web/static/src/model/relational_model/static_list.js @@ -350,29 +350,10 @@ export class StaticList extends DataPoint { return this.model.mutex.exec(() => this._sortBy(fieldName)); } - async replaceWith(ids, { reload = false, silent = false } = {}) { + async replaceWith(ids, { reload = false } = {}) { return this.model.mutex.exec(async () => { - const resIds = reload ? ids : ids.filter((id) => !this._cache[id]); - if (resIds.length) { - const records = await this.model._loadRecords({ - ...this.config, - resIds, - context: this.context, - }); - for (const record of records) { - this._createRecordDatapoint(record); - } - } - this.records = ids.map((id) => this._cache[id]); - const updateCommandsToKeep = this._commands.filter( - (c) => c[0] === x2ManyCommands.UPDATE && ids.includes(c[1]) - ); - this._commands = [x2ManyCommands.replaceWith(ids)].concat(updateCommandsToKeep); - this._currentIds = [...ids]; - this.count = this._currentIds.length; - if (!silent) { - await this._onUpdate(); - } + await this._replaceWith(ids, { reload }); + await this._onUpdate(); }); } @@ -693,7 +674,7 @@ export class StaticList extends DataPoint { const { CREATE, UPDATE } = x2ManyCommands; const options = { parentRecord: this._parent, - onUpdate: async (changes, { withoutParentUpdate }) => { + onUpdate: async ({ withoutParentUpdate }) => { if (!this.currentIds.includes(record.isNew ? record._virtualId : record.resId)) { // the record hasn't been added to the list yet (we're currently creating it // from a dialog) @@ -822,6 +803,27 @@ export class StaticList extends DataPoint { await this.model._updateConfig(this.config, { limit, offset, orderBy }, { noReload: true }); } + async _replaceWith(ids, { reload = false } = {}) { + const resIds = reload ? ids : ids.filter((id) => !this._cache[id]); + if (resIds.length) { + const records = await this.model._loadRecords({ + ...this.config, + resIds, + context: this.context, + }); + for (const record of records) { + this._createRecordDatapoint(record); + } + } + this.records = ids.map((id) => this._cache[id]); + const updateCommandsToKeep = this._commands.filter( + (c) => c[0] === x2ManyCommands.UPDATE && ids.includes(c[1]) + ); + this._commands = [x2ManyCommands.replaceWith(ids)].concat(updateCommandsToKeep); + this._currentIds = [...ids]; + this.count = this._currentIds.length; + } + async _resequence(movedId, targetId) { const records = [...this.records]; const order = this.orderBy.find((o) => o.name === this.handleField); diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js index c698430d469..42852e7c13d 100644 --- a/addons/web/static/src/views/list/list_renderer.js +++ b/addons/web/static/src/views/list/list_renderer.js @@ -876,6 +876,7 @@ export class ListRenderer extends Component { isCellReadonly(column, record) { return ( + this.isRecordReadonly(record) || (column.relatedPropertyField && record.selected && record.model.multiEdit) || evalDomain(column.modifiers.readonly, record.evalContext) ); 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 2638a6a3f81..0f3d6aa52b9 100644 --- a/addons/web/static/tests/views/fields/one2many_field_tests.js +++ b/addons/web/static/tests/views/fields/one2many_field_tests.js @@ -3818,7 +3818,7 @@ QUnit.module("Fields", (hooks) => { }); QUnit.test("one2many list (non editable): edition", async function (assert) { - assert.expect(10); + assert.expect(11); let nbWrite = 0; serverData.models.partner.records[0].p = [2, 4]; @@ -3862,6 +3862,10 @@ QUnit.module("Fields", (hooks) => { // edit first record await click(target.querySelector(".o_list_renderer .o_data_cell")); + assert.hasClass( + target.querySelector(".o_list_renderer .o_data_cell"), + "o_readonly_modifier" + ); await editInput(target, ".modal .o_form_editable input", "new name"); diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index 00afa023f68..eb27925d1c6 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -13400,4 +13400,64 @@ QUnit.module("Views", (hooks) => { ]); } ); + + QUnit.test("widget update several fields including an x2m", async function (assert) { + assert.expect(7); + + serverData.models.partner.onchanges = { + name() {}, + p() {}, + }; + class TestWidget extends Component { + static template = xml`
`; + + onClick() { + this.props.record.update({ name: "New Name", p: [[0, false, { name: "yop" }]] }); + } + } + widgetRegistry.add("test", { + component: TestWidget, + fieldDependencies: [ + { name: "name", type: "char" }, + { name: "p", type: "one2many", relation: "partner" }, + ], + }); + + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` + `, + resId: 2, + async mockRPC(route, args) { + if (args.method === "onchange2") { + assert.strictEqual(args.args[1].name, "New Name"); + assert.strictEqual(args.args[1].p.length, 1); + assert.deepEqual(args.args[1].p[0][2], { name: "yop" }); + } + }, + }); + + assert.strictEqual(target.querySelector("[name=name] input").value, "name"); + assert.deepEqual( + [...target.querySelectorAll(".o_data_row")].map((el) => el.textContent), + [] + ); + + await click(target, ".o_widget_test button"); + assert.strictEqual(target.querySelector("[name=name] input").value, "New Name"); + assert.deepEqual( + [...target.querySelectorAll(".o_data_row")].map((el) => el.textContent), + ["yop"] + ); + }); });