[FIX] web: prevent empty json field from passing the required check

Steps to reproduce
==================

- Enable analytic accounting
- Go to customer invoices
- Open a record
- Open studio
- Click on the Invoice lines > Edit list view
- Set the analytic field as required
- Close studio
- Create a new record
- Add a new line
- Click on the analytic field
- Close the popup
- Save the record

=> Even though we didn't set a value for the field, it is saved

Cause of the issue
==================

The actual field value is an empty object.
The validity of a field is checked here [0].
An empty object is considered a truthy value.

When setting the required attribute directly on the python field
declaration, an empty object does not pass the check

Solution
========

Add a specific check for json fields. When the value is either false
(not set) or an empty object, the field is considered invalid.

---

[0]: https://github.com/odoo/odoo/blob/23f01c533512cbfb33e90509e22cd3c3c4f99d40/addons/web/static/src/views/basic_relational_model.js#L284

opw-3659763

closes odoo/odoo#150677

X-original-commit: 51b538b3f678298f27bbbf458de5696706c18349
Signed-off-by: Francois Georis (fge) <fge@odoo.com>
Signed-off-by: Hubert Van De Walle <huvw@odoo.com>
This commit is contained in:
Hubert Van De Walle
2024-01-24 00:16:50 +00:00
parent a5d169f1a8
commit a76641cb06
3 changed files with 45 additions and 0 deletions
@@ -402,6 +402,15 @@ export class Record extends DataPoint {
}
break;
}
case "json": {
if (
this._isRequired(fieldName) &&
(!this.data[fieldName] || !Object.keys(this.data[fieldName]).length)
) {
unsetRequiredFields.push(fieldName);
}
break;
}
default:
if (!this.data[fieldName] && this._isRequired(fieldName)) {
unsetRequiredFields.push(fieldName);
@@ -55,6 +55,7 @@ const DEFAULT_FIELD_VALUES = {
selection: false,
reference: false,
properties: [],
json: false,
};
// -----------------------------------------------------------------------------
@@ -55,6 +55,7 @@ import { Field } from "@web/views/fields/field";
import { IntegerField } from "@web/views/fields/integer/integer_field";
import { useSpecialData } from "@web/views/fields/relational_utils";
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { FormController } from "@web/views/form/form_controller";
import { companyService } from "@web/webclient/company_service";
@@ -14541,4 +14542,38 @@ QUnit.module("Views", (hooks) => {
".modal:not(.o_inactive_modal) .modal-footer button[name='someothername']"
);
});
QUnit.test("an empty json object does not pass the required check", async function (assert) {
assert.expect(3);
serverData.models.partner.fields.json_field = { type: "json" };
class JsonField extends Component {
onChange(ev) {
this.props.record.update({ [this.props.name]: JSON.parse(ev.target.value) });
}
}
JsonField.props = standardFieldProps;
JsonField.supportedTypes = ["json"];
JsonField.template = xml`<span><input t-on-change="onChange"/></span>`;
fieldRegistry.add("json", { component: JsonField });
const form = await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `<form><field name="json_field" widget="json" required="1"/></form>`,
});
patchWithCleanup(form.env.services.notification, {
add: (message, params) => {
assert.strictEqual(message.toString(), "<ul><li>json_field</li></ul>");
assert.deepEqual(params, { title: "Invalid fields: ", type: "danger" });
},
});
await editInput(target, ".o_field_widget[name=json_field] input", "{}")
await clickSave(target);
assert.hasClass(
target.querySelector(".o_field_widget[name=json_field]"),
"o_field_invalid"
);
});
});