From a76641cb066e46dcaa848c83a01329e841159bdb Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Wed, 17 Jan 2024 16:57:54 +0000 Subject: [PATCH] [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) Signed-off-by: Hubert Van De Walle --- .../src/model/relational_model/record.js | 9 +++++ .../web/static/tests/helpers/mock_server.js | 1 + .../tests/views/form/form_view_tests.js | 35 +++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/addons/web/static/src/model/relational_model/record.js b/addons/web/static/src/model/relational_model/record.js index 978b674008c..9ed2c67129c 100644 --- a/addons/web/static/src/model/relational_model/record.js +++ b/addons/web/static/src/model/relational_model/record.js @@ -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); diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 145ace05f26..e22fe96064e 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -55,6 +55,7 @@ const DEFAULT_FIELD_VALUES = { selection: false, reference: false, properties: [], + json: false, }; // ----------------------------------------------------------------------------- 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 5fae239f2a1..92a645e8046 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -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``; + + fieldRegistry.add("json", { component: JsonField }); + const form = await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: `
`, + }); + + patchWithCleanup(form.env.services.notification, { + add: (message, params) => { + assert.strictEqual(message.toString(), "
  • json_field
"); + 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" + ); + }); });