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) => {