[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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user