diff --git a/addons/web/static/src/views/record.js b/addons/web/static/src/views/record.js index 454bbd8a8cc..99c74b46436 100644 --- a/addons/web/static/src/views/record.js +++ b/addons/web/static/src/views/record.js @@ -3,6 +3,7 @@ import { useService } from "@web/core/utils/hooks"; import { pick } from "@web/core/utils/objects"; import { RelationalModel } from "@web/model/relational_model/relational_model"; +import { getFieldsSpec } from "@web/model/relational_model/utils"; import { Component, xml, onWillStart, onWillUpdateProps, useState } from "@odoo/owl"; const defaultActiveField = { attrs: {}, options: {}, domain: "[]", string: "" }; @@ -23,13 +24,15 @@ class StandaloneRelationalModel extends RelationalModel { class _Record extends Component { setup() { + this.orm = useService("orm"); const resModel = this.props.info.resModel; + const activeFields = this.getActiveFields(); const modelParams = { config: { resModel, fields: this.props.fields, isMonoRecord: true, - activeFields: this.getActiveFields(), + activeFields, resId: this.props.info.resId, mode: this.props.info.mode, }, @@ -44,11 +47,79 @@ class _Record extends Component { return [servName, useService(servName)]; }) ); - modelServices.orm = useService("orm"); + modelServices.orm = this.orm; this.model = useState(new StandaloneRelationalModel(this.env, modelParams, modelServices)); - const loadWithValues = (values) => { + const loadWithValues = async (values) => { values = pick(values, ...Object.keys(modelParams.config.activeFields)); + const proms = []; + for (const fieldName in values) { + if (["one2many", "many2many"].includes(this.props.fields[fieldName].type)) { + if (values[fieldName].length && typeof values[fieldName][0] === "number") { + const resModel = this.props.fields[fieldName].relation; + const resIds = values[fieldName]; + const activeField = modelParams.config.activeFields[fieldName]; + if (activeField.related) { + const { activeFields, fields } = activeField.related; + const fieldSpec = getFieldsSpec(activeFields, fields, {}); + const kwargs = { + context: activeField.context || {}, + specification: fieldSpec, + }; + proms.push( + this.orm + .call(resModel, "web_read", [resIds], kwargs) + .then((records) => { + values[fieldName] = records; + }) + ); + } + } + } + if (this.props.fields[fieldName].type === "many2one") { + const loadDisplayName = async (resId) => { + const resModel = this.props.fields[fieldName].relation; + const activeField = modelParams.config.activeFields[fieldName]; + const kwargs = { + context: activeField.context || {}, + specification: { display_name: {} }, + }; + const records = await this.orm.call( + resModel, + "web_read", + [[resId]], + kwargs + ); + return records[0].display_name; + }; + if (typeof values[fieldName] === "number") { + const prom = loadDisplayName(values[fieldName]); + prom.then((displayName) => { + values[fieldName] = { + id: values[fieldName], + display_name: displayName, + }; + }); + proms.push(prom); + } else if (Array.isArray(values[fieldName])) { + if (values[fieldName][1] === undefined) { + const prom = loadDisplayName(values[fieldName][0]); + prom.then((displayName) => { + values[fieldName] = { + id: values[fieldName][0], + display_name: displayName, + }; + }); + proms.push(prom); + } + values[fieldName] = { + id: values[fieldName][0], + display_name: values[fieldName][1], + }; + } + } + await Promise.all(proms); + } return this.model.load({ values }); }; onWillStart(() => { diff --git a/addons/web/static/tests/views/record_tests.js b/addons/web/static/tests/views/record_tests.js index 9973739d14d..fa5450ef2ce 100644 --- a/addons/web/static/tests/views/record_tests.js +++ b/addons/web/static/tests/views/record_tests.js @@ -3,6 +3,7 @@ import { browser } from "@web/core/browser/browser"; import { Field } from "@web/views/fields/field"; import { Many2OneField } from "@web/views/fields/many2one/many2one_field"; +import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field"; import { Record } from "@web/views/record"; import { makeTestEnv } from "@web/../tests/helpers/mock_env"; @@ -10,6 +11,7 @@ import { click, editInput, getFixture, + getNodesTextContent, mount, nextTick, patchWithCleanup, @@ -335,6 +337,167 @@ QUnit.module("Record Component", (hooks) => { assert.strictEqual(target.querySelector(".o_field_many2one_selection input").value, "abc"); }); + QUnit.test("handles many2one fields (2)", async function (assert) { + patchWithCleanup(browser, { + setTimeout: (fn) => fn(), + }); + + serverData.models = { + bar: { + records: [ + { id: 1, display_name: "bar1" }, + { id: 3, display_name: "abc" }, + ], + }, + }; + + class Parent extends Component { + setup() { + this.fields = { + foo: { + name: "foo", + type: "many2one", + relation: "bar", + }, + }; + this.values = { + foo: 1, + }; + } + + onRecordChanged(record, changes) { + assert.step("record changed"); + assert.deepEqual(changes, { foo: 3 }); + assert.deepEqual(record.data, { foo: [3, "abc"] }); + } + } + Parent.components = { Record, Many2OneField }; + Parent.template = xml` + + + + `; + + await mount(Parent, target, { + env: await makeTestEnv({ + serverData, + mockRPC(route, args) { + assert.step(route); + }, + }), + }); + assert.verifySteps(["/web/dataset/call_kw/bar/web_read"]); + assert.strictEqual(target.querySelector(".o_field_many2one_selection input").value, "bar1"); + }); + + QUnit.test("handles many2one fields (3)", async function (assert) { + patchWithCleanup(browser, { + setTimeout: (fn) => fn(), + }); + + serverData.models = { + bar: { + records: [ + { id: 1, display_name: "bar1" }, + { id: 3, display_name: "abc" }, + ], + }, + }; + + class Parent extends Component { + setup() { + this.fields = { + foo: { + name: "foo", + type: "many2one", + relation: "bar", + }, + }; + this.values = { + foo: [1], + }; + } + + onRecordChanged(record, changes) { + assert.step("record changed"); + assert.deepEqual(changes, { foo: 3 }); + assert.deepEqual(record.data, { foo: [3, "abc"] }); + } + } + Parent.components = { Record, Many2OneField }; + Parent.template = xml` + + + + `; + + await mount(Parent, target, { + env: await makeTestEnv({ + serverData, + mockRPC(route, args) { + assert.step(route); + }, + }), + }); + assert.verifySteps(["/web/dataset/call_kw/bar/web_read"]); + assert.strictEqual(target.querySelector(".o_field_many2one_selection input").value, "bar1"); + }); + + QUnit.test("handles x2many fields", async function (assert) { + serverData.models = { + tag: { + records: [ + { id: 1, display_name: "bug" }, + { id: 3, display_name: "ref" }, + ], + }, + }; + + class Parent extends Component { + setup() { + this.activeFields = { + tags: { + related: { + activeFields: { + display_name: {}, + }, + fields: { + display_name: { name: "display_name", type: "string" }, + }, + }, + }, + }; + this.fields = { + tags: { + name: "Tags", + type: "many2many", + relation: "tag", + }, + }; + this.values = { + tags: [1, 3], + }; + } + } + Parent.components = { Record, Many2ManyTagsField }; + Parent.template = xml` + + + + `; + + await mount(Parent, target, { + env: await makeTestEnv({ + serverData, + mockRPC(route, args) { + assert.step(route); + }, + }), + }); + assert.verifySteps(["/web/dataset/call_kw/tag/web_read"]); + assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_tag")), ["bug", "ref"]); + }); + QUnit.test( "supports passing dynamic values -- full control to the user of Record", async (assert) => {