[IMP] web: add relatedFields in x2m sub-views

The goal of this commit is to allow relatedFields to be added to a one2many
field using a sub-view.

Before this commit, if an x2many custom contains a sub-view and relatedFields,
we ignore the sub-view and use only the relatedFields.

Now we're going to add the relatedFields to the fields/activeFields
defined in the sub-view.

Use case:
Created a custom x2many field dependent on the 'display_name' of
a many2one field that should not be present in the sub-view. For example,
to simulate a groupBy on this many2one field.

closes odoo/odoo#132351

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
FrancoisGe
2023-09-05 19:58:01 +00:00
parent f581d4ad17
commit d8f03151fa
4 changed files with 173 additions and 30 deletions
@@ -225,6 +225,22 @@ export function extractFieldsFromArchInfo({ fieldNodes, widgetNodes }, fields) {
);
Object.assign(activeField.related.fields, formArchInfo.fields);
}
if (fieldNode.viewMode !== "default" && fieldNode.views.default) {
const defaultArchInfo = extractFieldsFromArchInfo(
fieldNode.views.default,
fieldNode.views.default.fields
);
activeField.related.activeFields = {
...defaultArchInfo.activeFields,
...activeField.related.activeFields,
};
activeField.related.fields = Object.assign(
{},
defaultArchInfo.fields,
activeField.related.fields
);
}
}
}
if (fieldNode.field?.useSubView) {
+35 -27
View File
@@ -312,39 +312,47 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
if (relatedFields instanceof Function) {
relatedFields = relatedFields(fieldInfo);
}
for (const relatedField of relatedFields) {
if (!("readonly" in relatedField)) {
relatedField.readonly = true;
}
}
relatedFields = Object.fromEntries(relatedFields.map((f) => [f.name, f]));
views.default = { fieldNodes: relatedFields, fields: relatedFields };
fieldInfo.viewMode = "default";
} else {
for (const child of node.children) {
const viewType = child.tagName === "tree" ? "list" : child.tagName;
const { ArchParser } = viewRegistry.get(viewType);
const xmlSerializer = new XMLSerializer();
const subArch = xmlSerializer.serializeToString(child);
const archInfo = new ArchParser().parse(subArch, models, fields[name].relation);
views[viewType] = {
...archInfo,
limit: archInfo.limit || 40,
fields: models[fields[name].relation],
};
if (!fieldInfo.field.useSubView) {
fieldInfo.viewMode = "default";
}
}
for (const child of node.children) {
const viewType = child.tagName === "tree" ? "list" : child.tagName;
const { ArchParser } = viewRegistry.get(viewType);
const xmlSerializer = new XMLSerializer();
const subArch = xmlSerializer.serializeToString(child);
const archInfo = new ArchParser().parse(subArch, models, fields[name].relation);
views[viewType] = {
...archInfo,
limit: archInfo.limit || 40,
fields: models[fields[name].relation],
};
}
let viewMode = node.getAttribute("mode");
if (!viewMode) {
if (views.list && !views.kanban) {
viewMode = "list";
} else if (!views.list && views.kanban) {
viewMode = "kanban";
} else if (views.list && views.kanban) {
viewMode = isSmall() ? "kanban" : "list";
}
let viewMode = node.getAttribute("mode");
if (viewMode) {
if (viewMode.split(",").length !== 1) {
viewMode = isSmall() ? "kanban" : "list";
} else {
if (viewMode.split(",").length !== 1) {
viewMode = isSmall() ? "kanban" : "list";
} else {
viewMode = viewMode === "tree" ? "list" : viewMode;
}
viewMode = viewMode === "tree" ? "list" : viewMode;
}
} else {
if (views.list && !views.kanban) {
viewMode = "list";
} else if (!views.list && views.kanban) {
viewMode = "kanban";
} else if (views.list && views.kanban) {
viewMode = isSmall() ? "kanban" : "list";
}
}
if (viewMode) {
fieldInfo.viewMode = viewMode;
}
if (Object.keys(views).length) {
@@ -274,7 +274,7 @@ export const many2ManyTagsField = {
relatedFields: ({ options }) => {
const relatedFields = [{ name: "display_name", type: "char" }];
if (options.color_field) {
relatedFields.push({ name: options.color_field, type: "integer" });
relatedFields.push({ name: options.color_field, type: "integer", readonly: false });
}
return relatedFields;
},
@@ -26,8 +26,8 @@ import {
} from "@web/../tests/helpers/utils";
import {
toggleActionMenu,
toggleSearchBarMenu,
toggleMenuItem,
toggleSearchBarMenu,
} from "@web/../tests/search/helpers";
import { makeView, makeViewInDialog, setupViewRegistries } from "@web/../tests/views/helpers";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
@@ -41,10 +41,11 @@ import { tooltipService } from "@web/core/tooltip/tooltip_service";
import { SIZES } from "@web/core/ui/ui_service";
import { useService } from "@web/core/utils/hooks";
import { session } from "@web/session";
import { Field } from "@web/views/fields/field";
import { CharField } from "@web/views/fields/char/char_field";
import { DateTimeField } from "@web/views/fields/datetime/datetime_field";
import { Field } from "@web/views/fields/field";
import { IntegerField } from "@web/views/fields/integer/integer_field";
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
import { FormController } from "@web/views/form/form_controller";
import { companyService } from "@web/webclient/company_service";
@@ -13985,4 +13986,122 @@ QUnit.module("Views", (hooks) => {
assert.equal(target.querySelectorAll(".o_data_cell")[0].innerText, "updated");
}
);
QUnit.test("custom x2many with relatedFields and list view inline", async function (assert) {
class MyField extends X2ManyField {}
fieldRegistry.add("my_widget", {
...x2ManyField,
component: MyField,
relatedFields: [
{ name: "trululu", type: "many2one", relation: "partner" },
{ name: "int_field", type: "integer" },
],
});
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="p" widget='my_widget'>
<tree editable="bottom" >
<field name="foo"/>
<field name="int_field" />
</tree>
</field>
</form>`,
resId: 2,
mockRPC(route, args) {
if (args.method === "web_read") {
assert.step("web_read");
assert.deepEqual(args.kwargs.specification.p.fields, {
trululu: { fields: { display_name: {} } },
foo: {},
int_field: {},
});
} else if (args.method === "write") {
assert.step("write");
assert.deepEqual(args.args[1].p[0][2], {
foo: "new record",
int_field: 0,
});
} else if (args.method === "web_save") {
assert.step("web_save");
assert.deepEqual(args.kwargs.specification.p.fields, {
trululu: { fields: { display_name: {} } },
foo: {},
int_field: {},
});
}
},
});
await addRow(target);
await editInput(target, ".o_data_row [name='foo'] input", "new record");
await clickSave(target);
assert.verifySteps(["web_read", "web_save"]);
});
QUnit.test(
"custom x2many with relatedFields and list view not inline",
async function (assert) {
class MyField extends X2ManyField {}
fieldRegistry.add("my_widget", {
...x2ManyField,
component: MyField,
relatedFields: [
{ name: "trululu", type: "many2one", relation: "partner" },
{ name: "int_field", type: "integer" },
],
});
serverData.views = {
"partner,false,list": `
<tree editable="bottom" >
<field name="foo"/>
<field name="int_field" />
</tree>`,
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="p" widget='my_widget'/>
</form>`,
resId: 2,
mockRPC(route, args) {
if (args.method === "web_read") {
assert.step("web_read");
assert.deepEqual(args.kwargs.specification.p.fields, {
trululu: { fields: { display_name: {} } },
foo: {},
int_field: {},
});
} else if (args.method === "write") {
assert.step("write");
assert.deepEqual(args.args[1].p[0][2], {
foo: "new record",
int_field: 0,
});
} else if (args.method === "web_save") {
assert.step("web_save");
assert.deepEqual(args.kwargs.specification.p.fields, {
trululu: { fields: { display_name: {} } },
foo: {},
int_field: {},
});
}
},
});
await addRow(target);
await editInput(target, ".o_data_row [name='foo'] input", "new record");
await clickSave(target);
assert.verifySteps(["web_read", "web_save"]);
}
);
});