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",