diff --git a/addons/web/static/src/views/basic_relational_model.js b/addons/web/static/src/views/basic_relational_model.js index ef55c07b1eb..9a9c4b3ec43 100644 --- a/addons/web/static/src/views/basic_relational_model.js +++ b/addons/web/static/src/views/basic_relational_model.js @@ -108,6 +108,7 @@ function mapActiveFieldsToFieldsInfo(activeFields, fields, viewType) { domain, context: fieldDescr.context, fieldDependencies: {}, // ?? + force_save: fieldDescr.forceSave, mode: fieldDescr.viewMode, modifiers: fieldDescr.modifiers, name: fieldName, diff --git a/addons/web/static/src/views/fields/field.js b/addons/web/static/src/views/fields/field.js index c3d177aac3b..9282d2af818 100644 --- a/addons/web/static/src/views/fields/field.js +++ b/addons/web/static/src/views/fields/field.js @@ -202,6 +202,7 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) { modifiers: JSON.parse(node.getAttribute("modifiers") || "{}"), onChange: archParseBoolean(node.getAttribute("on_change")), FieldComponent: getFieldClassFromRegistry(fields[name].type, widget, viewType, jsClass), + forceSave: archParseBoolean(node.getAttribute("force_save")), decorations: {}, // populated below noLabel: archParseBoolean(node.getAttribute("nolabel")), props: {}, diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js index 1679d5e6e4c..f8ab91d4dac 100644 --- a/addons/web/static/src/views/relational_model.js +++ b/addons/web/static/src/views/relational_model.js @@ -641,7 +641,12 @@ export class Record extends DataPoint { getChanges(allFields = false, parentChanges = false) { const changes = { ...(allFields ? this.data : this._changes) }; for (const fieldName in changes) { - if (!allFields && fieldName in this.activeFields && this.isReadonly(fieldName)) { + if ( + !allFields && + fieldName in this.activeFields && + !this.activeFields[fieldName].forceSave && + this.isReadonly(fieldName) + ) { delete changes[fieldName]; continue; } 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 1e167f2b532..adcbdec4397 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -6119,6 +6119,45 @@ QUnit.module("Views", (hooks) => { ); }); + QUnit.test("readonly sub fields fields with force_save attribute", async function (assert) { + assert.expect(1); + + serverData.models.partner.fields.foo.readonly = true; + serverData.models.partner.fields.int_field.readonly = true; + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` +
`, + mockRPC(route, args) { + if (args.method === "create") { + // foo should be saved because of the "force_save" attribute + // qux should be saved because it isn't readonly + // int_field should not be saved as it is readonly + assert.deepEqual(args.args[0].p, [[0, 1, { foo: "some value", qux: 6.5 }]]); + } + if (args.method === "onchange") { + return { + value: { + p: [[5], [0, 1, { foo: "some value", int_field: 44, qux: 6.5 }]], + }, + }; + } + }, + }); + + await click(target.querySelector(".o_form_button_save")); + }); + QUnit.test("readonly set by modifier do not break many2many_tags", async function (assert) { serverData.models.partner.onchanges = { bar: function (obj) {