From bd6b98a65fca59e804faefea129c9e18acb196a6 Mon Sep 17 00:00:00 2001 From: FrancoisGe Date: Mon, 8 Aug 2022 12:53:07 +0000 Subject: [PATCH] [FIX] web: save a record with an invisible required field Before this commit, it was impossible to correctly save a record containing an invisible required field in its template. Problem: When a record tries to be saved or switched to readonly, it will check the validity of all its fields and if any of them are invalid, it will cancel the save or switch to readonly. In our case, we have an invisible required field. If this field is invalid, it is impossible for the user to modify it because it is invisible. So he cannot make it valid. So it is not possible to save this record or to switch it to readonly mode. Solution: We no longer check the validity of invisible fields. How to reproduce: - Go into a list view with a template containing a field that is invisible and required - Click on Create - Click on Save after editing the record Before this commit: The record doesn't save itself and doesn't go into readonly mode After this commit: The record is saved and in readonly mode We have the same problem in the form view. closes odoo/odoo#97610 Signed-off-by: Lucas Perais (lpe) --- .../src/views/basic_relational_model.js | 6 +- addons/web/static/src/views/fields/field.js | 4 +- .../web/static/src/views/relational_model.js | 10 ++- .../tests/views/form/form_view_tests.js | 72 +++++++++++++++++++ .../web/static/tests/views/list_view_tests.js | 30 ++++++++ 5 files changed, 118 insertions(+), 4 deletions(-) diff --git a/addons/web/static/src/views/basic_relational_model.js b/addons/web/static/src/views/basic_relational_model.js index c7c46747da8..8ec8deb572a 100644 --- a/addons/web/static/src/views/basic_relational_model.js +++ b/addons/web/static/src/views/basic_relational_model.js @@ -351,7 +351,11 @@ export class Record extends DataPoint { for (const fieldName in this.activeFields) { const fieldType = this.fields[fieldName].type; if (fieldName in this._requiredFields) { - if (!evalDomain(this._requiredFields[fieldName], this.evalContext)) { + const activeField = this.activeFields[fieldName]; + if ( + !evalDomain(this._requiredFields[fieldName], this.evalContext) || + (activeField && activeField.alwaysInvisible) + ) { this._removeInvalidFields([fieldName]); continue; } diff --git a/addons/web/static/src/views/fields/field.js b/addons/web/static/src/views/fields/field.js index a8611e37790..c23b667f212 100644 --- a/addons/web/static/src/views/fields/field.js +++ b/addons/web/static/src/views/fields/field.js @@ -196,6 +196,7 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) { const widget = node.getAttribute("widget"); const fields = models[modelName]; const field = fields[name]; + const modifiers = JSON.parse(node.getAttribute("modifiers") || "{}"); const fieldInfo = { name, viewType, @@ -203,7 +204,7 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) { domain: node.getAttribute("domain") || "[]", string: node.getAttribute("string") || field.string, widget, - modifiers: JSON.parse(node.getAttribute("modifiers") || "{}"), + modifiers, onChange: archParseBoolean(node.getAttribute("on_change")), FieldComponent: getFieldClassFromRegistry(fields[name].type, widget, viewType, jsClass), forceSave: archParseBoolean(node.getAttribute("force_save")), @@ -212,6 +213,7 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) { props: {}, rawAttrs: {}, options: evaluateExpr(node.getAttribute("options") || "{}"), + alwaysInvisible: modifiers.invisible === true || modifiers.column_invisible === true, }; for (const attribute of node.attributes) { if (attribute.name in Field.forbiddenAttributeNames) { diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js index 11cd7912650..df67830ff4a 100644 --- a/addons/web/static/src/views/relational_model.js +++ b/addons/web/static/src/views/relational_model.js @@ -503,7 +503,9 @@ export class Record extends DataPoint { } get dirtyFields() { - if (!this.isDirty) return []; + if (!this.isDirty) { + return []; + } return this._changes.map((change) => this.activeFields[change]); } @@ -569,7 +571,11 @@ export class Record extends DataPoint { _checkValidity() { for (const fieldName in this._requiredFields) { const fieldType = this.fields[fieldName].type; - if (!evalDomain(this._requiredFields[fieldName], this.evalContext)) { + const activeField = this.activeFields[fieldName]; + if ( + !evalDomain(this._requiredFields[fieldName], this.evalContext) || + (activeField && activeField.alwaysInvisible) + ) { this._removeInvalidFields([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 806f2282475..9fefb804c36 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -3,6 +3,7 @@ import { registerCleanup } from "@web/../tests/helpers/cleanup"; import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; import { + addRow, click, clickEdit, clickSave, @@ -11542,6 +11543,77 @@ QUnit.module("Views", (hooks) => { assert.containsOnce(target, ".o_form_editable"); assert.containsOnce(target, ".o_form_button_save"); assert.containsOnce(target, ".o_form_button_cancel"); + }); + + QUnit.test("save a form view with an invisible required field", async function (assert) { + serverData.models.partner.fields.text = { string: "Text", type: "char", required: 1 }; + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` +
+ + + + +
`, + mockRPC(route, args) { + assert.step(args.method); + if (args.method === "create") { + assert.deepEqual(args.args, [ + { + int_field: 0, + text: false, + }, + ]); + } + }, + }); + + await clickSave(target); + assert.verifySteps(["get_views", "onchange", "create", "read"]); + }); + + QUnit.test( + "save a form view with an invisible required field in a x2many", + async function (assert) { + serverData.models.partner.fields.text = { string: "Text", type: "char", required: 1 }; + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` +
+ + + + + + + + +
`, + mockRPC(route, args) { + assert.step(args.method); + if (args.method === "create") { + assert.deepEqual(args.args[0].p[0][2], { int_field: 1, text: false }); + } + }, + }); + + await addRow(target); + await editInput(target, "[name='int_field'] input", 1); + await click(target, ".o_form_view"); + assert.containsNone(target, "[name='int_field'] input"); + + await clickSave(target); + assert.containsOnce(target, ".o_list_renderer .o_data_row"); + assert.strictEqual( + target.querySelector(".o_list_renderer .o_data_row [name='int_field']").textContent, + "1" + ); + assert.verifySteps(["get_views", "onchange", "onchange", "create", "read", "read"]); } ); }); diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index 7165ad23623..7680a04d910 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -1002,6 +1002,36 @@ QUnit.module("Views", (hooks) => { } ); + QUnit.test("save a record with an invisible required field ", async function (assert) { + serverData.models.foo.fields.foo.required = true; + + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: ` + + + + `, + mockRPC(route, args) { + assert.step(args.method); + if (args.method === "create") { + assert.deepEqual(args.args, [{ int_field: 1 }]); + } + }, + }); + assert.containsN(target, ".o_data_row", 4); + assert.verifySteps(["get_views", "web_search_read"]); + + await click(target.querySelector(".o_list_button_add")); + await editInput(target, "[name='int_field'] input", 1); + await click(target, ".o_list_view"); + assert.containsN(target, ".o_data_row", 5); + assert.strictEqual(target.querySelector(".o_data_row [name='int_field']").textContent, "1"); + assert.verifySteps(["onchange", "create", "read"]); + }); + QUnit.test("boolean field has no title (data-tooltip)", async function (assert) { await makeView({ type: "list",