[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) <lpe@odoo.com>
This commit is contained in:
FrancoisGe
2022-08-09 10:15:48 +02:00
parent 384434f00d
commit bd6b98a65f
5 changed files with 118 additions and 4 deletions
@@ -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;
}
+3 -1
View File
@@ -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) {
@@ -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;
}
@@ -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: `
<form>
<sheet>
<field name="text" invisible="1"/>
<field name="int_field"/>
</sheet>
</form>`,
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: `
<form>
<sheet>
<field name="p">
<tree editable="top">
<field name="text" invisible="1"/>
<field name="int_field"/>
</tree>
</field>
</sheet>
</form>`,
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"]);
}
);
});
@@ -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: `
<tree editable="top">
<field name="foo" invisible="1"/>
<field name="int_field"/>
</tree>`,
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",