Odoo18-Base/addons/web/static/tests/views/fields/many2many_field_tests.js
2025-03-10 11:12:23 +07:00

2189 lines
82 KiB
JavaScript

/** @odoo-module **/
import { Component, xml } from "@odoo/owl";
import {
addRow,
click,
clickOpenedDropdownItem,
clickSave,
editInput,
editSelect,
getFixture,
getNodesTextContent,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { editSearch, validateSearch } from "@web/../tests/search/helpers";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";
import { companyService } from "@web/webclient/company_service";
let target;
let serverData;
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
foo: { string: "Foo", type: "char", default: "My little Foo Value" },
bar: { string: "Bar", type: "boolean", default: true },
int_field: { string: "int_field", type: "integer", sortable: true },
p: {
string: "one2many field",
type: "one2many",
relation: "partner",
relation_field: "trululu",
},
turtles: {
string: "one2many turtle field",
type: "one2many",
relation: "turtle",
relation_field: "turtle_trululu",
},
trululu: { string: "Trululu", type: "many2one", relation: "partner" },
timmy: { string: "pokemon", type: "many2many", relation: "partner_type" },
product_id: { string: "Product", type: "many2one", relation: "product" },
color: {
type: "selection",
selection: [
["red", "Red"],
["black", "Black"],
],
default: "red",
string: "Color",
},
date: { string: "Some Date", type: "date" },
datetime: { string: "Datetime Field", type: "datetime" },
user_id: { string: "User", type: "many2one", relation: "user" },
reference: {
string: "Reference Field",
type: "reference",
selection: [
["product", "Product"],
["partner_type", "Partner Type"],
["partner", "Partner"],
],
},
},
records: [
{
id: 1,
display_name: "first record",
bar: true,
foo: "yop",
int_field: 10,
p: [],
turtles: [2],
timmy: [],
trululu: 4,
user_id: 17,
reference: "product,37",
},
{
id: 2,
display_name: "second record",
bar: true,
foo: "blip",
int_field: 9,
p: [],
timmy: [],
trululu: 1,
product_id: 37,
date: "2017-01-25",
datetime: "2016-12-12 10:55:05",
user_id: 17,
},
{
id: 4,
display_name: "aaa",
bar: false,
},
],
onchanges: {},
},
product: {
fields: {
name: { string: "Product Name", type: "char" },
},
records: [
{
id: 37,
display_name: "xphone",
},
{
id: 41,
display_name: "xpad",
},
],
},
partner_type: {
fields: {
display_name: { string: "Partner Type", type: "char" },
name: { string: "Partner Type", type: "char" },
color: { string: "Color index", type: "integer" },
},
records: [
{ id: 12, display_name: "gold", color: 2 },
{ id: 14, display_name: "silver", color: 5 },
],
},
turtle: {
fields: {
display_name: { string: "Displayed name", type: "char" },
turtle_foo: { string: "Foo", type: "char" },
turtle_bar: { string: "Bar", type: "boolean", default: true },
turtle_int: { string: "int", type: "integer", sortable: true },
turtle_trululu: {
string: "Trululu",
type: "many2one",
relation: "partner",
},
turtle_ref: {
string: "Reference",
type: "reference",
selection: [
["product", "Product"],
["partner", "Partner"],
],
},
product_id: {
string: "Product",
type: "many2one",
relation: "product",
required: true,
},
partner_ids: { string: "Partner", type: "many2many", relation: "partner" },
},
records: [
{
id: 1,
display_name: "leonardo",
turtle_bar: true,
turtle_foo: "yop",
partner_ids: [],
},
{
id: 2,
display_name: "donatello",
turtle_bar: true,
turtle_foo: "blip",
turtle_int: 9,
partner_ids: [2, 4],
},
{
id: 3,
display_name: "raphael",
product_id: 37,
turtle_bar: false,
turtle_foo: "kawa",
turtle_int: 21,
partner_ids: [],
turtle_ref: "product,37",
},
],
onchanges: {},
},
user: {
fields: {
name: { string: "Name", type: "char" },
partner_ids: {
string: "one2many partners field",
type: "one2many",
relation: "partner",
relation_field: "user_id",
},
},
records: [
{
id: 17,
name: "Aline",
partner_ids: [1, 2],
},
{
id: 19,
name: "Christine",
},
],
},
},
};
setupViewRegistries();
});
QUnit.module("Many2ManyField");
QUnit.test("many2many kanban: edition", async function (assert) {
assert.expect(31);
serverData.views = {
"partner_type,false,form": '<form><field name="display_name"/></form>',
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,search": '<search><field name="name" string="Name"/></search>',
};
serverData.models.partner.records[0].timmy = [12, 14];
serverData.models.partner_type.records.push({ id: 15, display_name: "red", color: 6 });
serverData.models.partner_type.records.push({ id: 18, display_name: "yellow", color: 4 });
serverData.models.partner_type.records.push({ id: 21, display_name: "blue", color: 1 });
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<kanban>
<field name="display_name"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<a t-if="!read_only_mode" type="delete" class="fa fa-times float-end delete_icon"/>
<span><t t-esc="record.display_name.value"/></span>
</div>
</t>
</templates>
</kanban>
<form>
<field name="display_name"/>
</form>
</field>
</form>`,
resId: 1,
mockRPC(route, args) {
if (route === "/web/dataset/call_kw/partner_type/write") {
assert.strictEqual(
args.args[1].display_name,
"new name",
"should write 'new_name'"
);
}
if (route === "/web/dataset/call_kw/partner_type/create") {
assert.strictEqual(
args.args[0].display_name,
"A new type",
"should create 'A new type'"
);
}
if (route === "/web/dataset/call_kw/partner/write") {
var commands = args.args[1].timmy;
assert.strictEqual(commands.length, 1, "should have generated one command");
assert.strictEqual(
commands[0][0],
6,
"generated command should be REPLACE WITH"
);
// get the created type's id
var createdType = _.findWhere(serverData.models.partner_type.records, {
display_name: "A new type",
});
var ids = _.sortBy([12, 15, 18].concat(createdType.id), _.identity.bind(_));
assert.ok(
_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), ids),
"new value should be " + ids
);
}
},
});
assert.strictEqual(
$(target).find(".o_kanban_record:not(.o_kanban_ghost)").length,
2,
"should contain 2 records"
);
assert.strictEqual(
$(target).find(".o_kanban_record:first() span").text(),
"gold",
"display_name of subrecord should be the one in DB"
);
assert.ok(
$(target).find(".o_kanban_renderer .delete_icon").length,
"delete icon should be visible in edit"
);
assert.ok(
$(target).find(".o_field_many2many .o-kanban-button-new").length,
'"Add" button should be visible in edit'
);
assert.strictEqual(
$(target).find(".o_field_many2many .o-kanban-button-new").text().trim(),
"Add",
'Create button should have "Add" label'
);
// edit existing subrecord
await click($(target).find(".oe_kanban_global_click:first()")[0]);
await editInput(target, ".modal .o_form_view input", "new name");
await click($(".modal .modal-footer .btn-primary")[0]);
assert.strictEqual(
$(target).find(".o_kanban_record:first() span").text(),
"new name",
"value of subrecord should have been updated"
);
// add subrecords
// -> single select
await click($(target).find(".o_field_many2many .o-kanban-button-new")[0]);
assert.ok($(".modal .o_list_view").length, "should have opened a list view in a modal");
assert.strictEqual(
$(".modal .o_list_view tbody .o_list_record_selector").length,
3,
"list view should contain 3 records"
);
await click($(".modal .o_list_view tbody tr:contains(red) .o_data_cell")[0]);
assert.ok(!$(".modal .o_list_view").length, "should have closed the modal");
assert.strictEqual(
$(target).find(".o_kanban_record:not(.o_kanban_ghost)").length,
3,
"kanban should now contain 3 records"
);
assert.ok(
$(target).find(".o_kanban_record:contains(red)").length,
'record "red" should be in the kanban'
);
// -> multiple select
await click($(target).find(".o_field_many2many .o-kanban-button-new")[0]);
assert.ok(
$(".modal .o_select_button").prop("disabled"),
"select button should be disabled"
);
assert.strictEqual(
$(".modal .o_list_view tbody .o_list_record_selector").length,
2,
"list view should contain 2 records"
);
await click($(".modal .o_list_view thead .o_list_record_selector input")[0]);
await nextTick();
await click($(".modal .o_select_button")[0]);
assert.ok(
!$(".modal .o_select_button").prop("disabled"),
"select button should be enabled"
);
assert.ok(!$(".modal .o_list_view").length, "should have closed the modal");
assert.strictEqual(
$(target).find(".o_kanban_record:not(.o_kanban_ghost)").length,
5,
"kanban should now contain 5 records"
);
// -> created record
await click($(target).find(".o_field_many2many .o-kanban-button-new")[0]);
await click($(".modal .modal-footer .btn-primary:nth(1)")[0]);
assert.ok(
$(".modal .o_form_view .o_form_editable").length,
"should have opened a form view in edit mode, in a modal"
);
await editInput(target, ".modal .o_form_view input", "A new type");
await click($(".modal:not(.o_inactive_modal) footer .btn-primary:first()")[0]);
assert.ok(!$(".modal").length, "should have closed both modals");
assert.strictEqual(
$(target).find(".o_kanban_record:not(.o_kanban_ghost)").length,
6,
"kanban should now contain 6 records"
);
assert.ok(
$(target).find(".o_kanban_record:contains(A new type)").length,
"the newly created type should be in the kanban"
);
// delete subrecords
await click($(target).find(".o_kanban_record:contains(silver)")[0]);
assert.strictEqual(
$(".modal .modal-footer .o_btn_remove").length,
1,
"There should be a modal having Remove Button"
);
await click($(".modal .modal-footer .o_btn_remove")[0]);
assert.containsNone($(".o_modal"), "modal should have been closed");
assert.strictEqual(
$(target).find(".o_kanban_record:not(.o_kanban_ghost)").length,
5,
"should contain 5 records"
);
assert.ok(
!$(target).find(".o_kanban_record:contains(silver)").length,
"the removed record should not be in kanban anymore"
);
await click($(target).find(".o_kanban_record:contains(blue) .delete_icon")[0]);
assert.strictEqual(
$(target).find(".o_kanban_record:not(.o_kanban_ghost)").length,
4,
"should contain 4 records"
);
assert.ok(
!$(target).find(".o_kanban_record:contains(blue)").length,
"the removed record should not be in kanban anymore"
);
// save the record
await clickSave(target);
});
QUnit.test(
"many2many kanban(editable): properly handle add-label node attribute",
async function (assert) {
serverData.models.partner.records[0].timmy = [12];
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" add-label="Add timmy" mode="kanban">
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_details">
<field name="display_name"/>
</div>
</t>
</templates>
</kanban>
</field>
</form>`,
resId: 1,
});
assert.strictEqual(
target
.querySelector(".o_field_many2many[name=timmy] .o-kanban-button-new")
.innerText.trim()
.toUpperCase(), // for community/enterprise compatibility
"ADD TIMMY",
"In M2M Kanban, Add button should have 'Add timmy' label"
);
}
);
QUnit.test("field string is used in the SelectCreateDialog", async function (assert) {
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,search": '<search><field name="display_name"/></search>',
"turtle,false,list": '<tree><field name="display_name"/></tree>',
"turtle,false,search": '<search><field name="display_name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree>
<field name="display_name"/>
</tree>
</field>
<field name="turtles" widget="many2many" string="Abcde">
<tree>
<field name="display_name"/>
</tree>
</field>
</form>`,
});
await click(target.querySelectorAll(".o_field_x2many_list_row_add a")[0]);
assert.containsOnce(target, ".modal");
assert.strictEqual(target.querySelector(".modal .modal-title").innerText, "Add: pokemon");
await click(target.querySelector(".modal .o_form_button_cancel"));
assert.containsNone(target, ".modal");
await click(target.querySelectorAll(".o_field_x2many_list_row_add a")[1]);
assert.containsOnce(target, ".modal");
assert.strictEqual(target.querySelector(".modal .modal-title").innerText, "Add: Abcde");
});
QUnit.test("many2many kanban: create action disabled", async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
serverData.views = {
"partner_type,false,list": '<tree><field name="name"/></tree>',
"partner_type,false,search":
"<search>" + '<field name="display_name" string="Name"/>' + "</search>",
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<kanban create="0">
<field name="display_name"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<a t-if="!read_only_mode" type="delete" class="fa fa-times float-end delete_icon"/>
<span><t t-esc="record.display_name.value"/></span>
</div>
</t>
</templates>
</kanban>
</field>
</form>`,
resId: 1,
});
assert.ok(
$(target).find(".o-kanban-button-new").length,
'"Add" button should be available in edit'
);
assert.ok(
$(target).find(".o_kanban_renderer .delete_icon").length,
"delete icon should be visible in edit"
);
await click($(target).find(".o-kanban-button-new")[0]);
assert.strictEqual(
$(".modal .modal-footer .btn-primary").length,
1, // only button 'Select'
'"Create" button should not be available in the modal'
);
});
QUnit.test("many2many kanban: conditional create/delete actions", async function (assert) {
serverData.views = {
"partner_type,false,form": '<form><field name="name"/></form>',
"partner_type,false,list": '<tree><field name="name"/></tree>',
"partner_type,false,search": "<search/>",
};
serverData.models.partner.records[0].timmy = [12, 14];
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="color"/>
<field name="timmy" options="{'create': [('color', '=', 'red')], 'delete': [('color', '=', 'red')]}">
<kanban>
<field name="display_name"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<span><t t-esc="record.display_name.value"/></span>
</div>
</t>
</templates>
</kanban>
</field>
</form>`,
resId: 1,
});
// color is red
assert.containsOnce(target, ".o-kanban-button-new", '"Add" button should be available');
await click($(target).find(".o_kanban_record:contains(silver)")[0]);
assert.containsOnce(
document.body,
".modal .modal-footer .o_btn_remove",
"remove button should be visible in modal"
);
await click($(".modal .modal-footer .o_form_button_cancel")[0]);
await click($(target).find(".o-kanban-button-new")[0]);
assert.containsN(
document.body,
".modal .modal-footer button",
3,
"there should be 3 buttons available in the modal"
);
await click($(".modal .modal-footer .o_form_button_cancel")[0]);
// set color to black
await editSelect(target, 'div[name="color"] select', '"black"');
assert.containsOnce(
target,
".o-kanban-button-new",
'"Add" button should still be available even after color field changed'
);
await click($(target).find(".o-kanban-button-new")[0]);
// only select and cancel button should be available, create
// button should be removed based on color field condition
assert.containsN(
document.body,
".modal .modal-footer button",
2,
'"Create" button should not be available in the modal after color field changed'
);
await click($(".modal .modal-footer .o_form_button_cancel")[0]);
await click($(target).find(".o_kanban_record:contains(silver)")[0]);
assert.containsNone(
document.body,
".modal .modal-footer .o_btn_remove",
"remove button should not be visible in modal"
);
});
QUnit.test(
"many2many list (non editable): create a new record and click on action button",
async function (assert) {
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,search": '<search><field name="display_name"/></search>',
};
const list = await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree>
<field name="display_name"/>
</tree>
<form>
<header>
<button name="myaction" string="coucou" type="object"/>
</header>
<field name="display_name"/>
</form>
</field>
</form>`,
resId: 1,
mockRPC: async (route, args) => {
assert.step(args.method);
if (args.method === "create") {
assert.deepEqual(args.args[0], { display_name: "Hello" });
}
},
});
patchWithCleanup(list.env.services.action, {
doActionButton: (action, params) => {
assert.step(`action: ${action.name}`);
},
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
let modal = target.querySelector(".modal");
await click(modal, ".o_create_button");
assert.verifySteps(["get_views", "read", "get_views", "web_search_read", "onchange"]);
modal = target.querySelector(".modal");
await editInput(modal, "[name='display_name'] input", "Hello");
assert.strictEqual(modal.querySelector("[name='display_name'] input").value, "Hello");
await click(modal, ".o_statusbar_buttons [name='myaction']");
assert.strictEqual(modal.querySelector("[name='display_name'] input").value, "Hello");
assert.verifySteps(["create", "read", "action: myaction"]);
}
);
QUnit.test(
"many2many list (non editable): create a new record and click on action button",
async function (assert) {
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,search": '<search><field name="display_name"/></search>',
};
const list = await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree>
<field name="display_name"/>
</tree>
<form>
<header>
<button name="myaction" string="coucou" type="object"/>
</header>
<field name="display_name"/>
</form>
</field>
</form>`,
resId: 1,
mockRPC: async (route, args) => {
assert.step(args.method);
if (args.method === "create") {
assert.deepEqual(args.args[0], { display_name: "Hello" });
}
},
});
patchWithCleanup(list.env.services.action, {
doActionButton: (action, params) => {
assert.step(`action: ${action.name}`);
},
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
let modal = target.querySelector(".modal");
await click(modal, ".o_create_button");
assert.verifySteps(["get_views", "read", "get_views", "web_search_read", "onchange"]);
modal = target.querySelector(".modal");
await editInput(modal, "[name='display_name'] input", "Hello");
assert.strictEqual(modal.querySelector("[name='display_name'] input").value, "Hello");
await click(modal, ".o_statusbar_buttons [name='myaction']");
assert.strictEqual(modal.querySelector("[name='display_name'] input").value, "Hello");
assert.deepEqual(
[...modal.querySelectorAll(".modal-footer button")].map(
(button) => button.textContent
),
["Save & Close", "Save & New", "Discard"]
);
await editInput(modal, "[name='display_name'] input", "Hello (edited)");
await click(modal.querySelector(".modal-footer button"));
assert.containsNone(target, ".modal");
assert.deepEqual(
[...target.querySelectorAll("[name='timmy'] .o_data_row")].map(
(row) => row.textContent
),
["Hello (edited)"]
);
assert.verifySteps(["create", "read", "action: myaction", "write", "read", "read"]);
}
);
QUnit.test("add record in a many2many non editable list with context", async function (assert) {
assert.expect(1);
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,search": '<search><field name="display_name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="int_field"/>
<field name="timmy" context="{'abc': int_field}">
<tree>
<field name="display_name"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
if (args.method === "web_search_read") {
// done by the SelectCreateDialog
assert.deepEqual(args.kwargs.context, {
abc: 2,
bin_size: true, // not sure it should be there, but was in legacy
lang: "en",
tz: "taht",
uid: 7,
});
}
},
});
await editInput(target, ".o_field_widget[name=int_field] input", "2");
await click(target.querySelector(".o_field_x2many_list_row_add a"));
});
QUnit.test("many2many list (editable): edition", async function (assert) {
assert.expect(29);
serverData.models.partner.records[0].timmy = [12, 14];
serverData.models.partner_type.records.push({ id: 15, display_name: "bronze", color: 6 });
serverData.models.partner_type.fields.float_field = { string: "Float", type: "float" };
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,search": '<search><field name="display_name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree editable="top">
<field name="display_name"/>
<field name="float_field"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
if (args.method !== "get_views") {
assert.step(_.last(route.split("/")));
}
if (args.method === "write") {
assert.deepEqual(args.args[1].timmy, [
[6, false, [12, 15]],
[1, 12, { display_name: "new name" }],
]);
}
},
resId: 1,
});
assert.containsN(
target,
".o_list_renderer td.o_list_number",
2,
"should contain 2 records"
);
assert.strictEqual(
target.querySelector(".o_list_renderer tbody td").innerText,
"gold",
"display_name of first subrecord should be the one in DB"
);
assert.containsN(
target,
".o_list_record_remove",
2,
"delete icon should be visible in edit"
);
assert.hasClass(
target.querySelector("td.o_list_record_remove button"),
"fa fa-times",
"should have X icons to remove (unlink) records"
);
assert.containsOnce(
target,
".o_field_x2many_list_row_add",
'"Add an item" should not visible in edit'
);
// edit existing subrecord
await click(target.querySelector(".o_list_renderer tbody td"));
assert.containsNone(
target,
".modal",
"in edit, clicking on a subrecord should not open a dialog"
);
assert.hasClass(
target.querySelector(".o_list_renderer tbody tr"),
"o_selected_row",
"first row should be in edition"
);
await editInput(target, ".o_selected_row div[name=display_name] input", "new name");
assert.hasClass(
target.querySelector(".o_list_renderer .o_data_row"),
"o_selected_row",
"first row should still be in edition"
);
assert.strictEqual(
document.activeElement,
target.querySelector(".o_list_renderer div[name=display_name] input"),
"edited field should still have the focus"
);
await click(target.querySelector(".o_form_view"));
assert.doesNotHaveClass(
target.querySelector(".o_list_renderer tbody tr"),
"o_selected_row",
"first row should not be in edition anymore"
);
assert.strictEqual(
target.querySelector(".o_list_renderer tbody td").innerText,
"new name",
"value of subrecord should have been updated"
);
assert.verifySteps(["read", "read"]);
// add new subrecords
await click(target.querySelector(".o_field_x2many_list_row_add a"));
assert.containsOnce(target, ".modal", "a modal should be open");
assert.containsOnce(
target,
".modal .o_list_view .o_data_row",
"the list should contain one row"
);
await click(target.querySelector(".modal .o_list_view .o_data_row .o_data_cell"));
assert.containsNone(target, ".modal .o_list_view", "the modal should be closed");
assert.containsN(
target,
".o_list_renderer td.o_list_number",
3,
"should contain 3 subrecords"
);
// remove subrecords
await click(target.querySelectorAll(".o_list_record_remove")[1]);
assert.containsN(
target,
".o_list_renderer td.o_list_number",
2,
"should contain 2 subrecord"
);
assert.strictEqual(
target.querySelector(".o_list_renderer tbody .o_data_row td").innerText,
"new name",
"the updated row still has the correct values"
);
// save
await clickSave(target);
assert.containsN(
target,
".o_list_renderer td.o_list_number",
2,
"should contain 2 subrecords"
);
assert.strictEqual(
target.querySelector(".o_list_renderer .o_data_row td").innerText,
"new name",
"the updated row still has the correct values"
);
assert.verifySteps([
"web_search_read", // list view in dialog
"read", // relational field (updated)
"write", // save main record
"read", // main record
"read", // relational field
]);
});
QUnit.test("many2many: create & delete attributes (both true)", async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree create="true" delete="true">
<field name="color"/>
</tree>
</field>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
".o_field_x2many_list_row_add",
"should have the 'Add an item' link"
);
assert.containsN(target, ".o_list_record_remove", 2, "should have the 'Add an item' link");
});
QUnit.test("many2many: create & delete attributes (both false)", async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree create="false" delete="false">
<field name="color"/>
</tree>
</field>
</form>`,
resId: 1,
});
assert.containsOnce(
target,
".o_field_x2many_list_row_add",
"should have the 'Add an item' link"
);
assert.containsN(
target,
".o_list_record_remove",
2,
"each record should have the 'Remove Item' link"
);
});
QUnit.test("many2many list: create action disabled", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<tree create="0">
<field name="name"/>
</tree>
</field>
</form>`,
resId: 1,
});
assert.containsOnce(target, ".o_field_x2many_list_row_add");
});
QUnit.test("fieldmany2many list comodel not writable", async function (assert) {
/**
* Many2Many List should behave as the m2m_tags
* that is, the relation can be altered even if the comodel itself is not CRUD-able
* This can happen when someone has read access alone on the comodel
* and full CRUD on the current model
*/
assert.expect(12);
serverData.views = {
"partner_type,false,list": `
<tree create="false" delete="false" edit="false">
<field name="display_name"/>
</tree>`,
"partner_type,false,search": '<search><field name="display_name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="many2many" can_create="false" can_write="false"/>
</form>`,
mockRPC(route, args) {
if (route === "/web/dataset/call_kw/partner/create") {
assert.deepEqual(args.args[0], { timmy: [[6, false, [12]]] });
}
if (route === "/web/dataset/call_kw/partner/write") {
assert.deepEqual(args.args[1], { timmy: [[6, false, []]] });
}
},
});
assert.containsOnce(target, ".o_field_many2many .o_field_x2many_list_row_add");
await click(target.querySelector(".o_field_many2many .o_field_x2many_list_row_add a"));
assert.containsOnce(target, ".modal");
assert.containsN(target.querySelector(".modal-footer"), "button", 2);
assert.containsOnce(target.querySelector(".modal-footer"), "button.o_select_button");
assert.containsOnce(target.querySelector(".modal-footer"), "button.o_form_button_cancel");
await click(target.querySelector(".modal .o_list_view .o_data_cell"));
assert.containsNone(target, ".modal");
assert.containsOnce(target, ".o_field_many2many .o_data_row");
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_field_many2many .o_data_row")),
["gold"]
);
assert.containsOnce(target, ".o_field_many2many .o_field_x2many_list_row_add");
await clickSave(target);
assert.containsOnce(target, ".o_field_many2many .o_data_row .o_list_record_remove");
await click(target.querySelector(".o_field_many2many .o_data_row .o_list_record_remove"));
await clickSave(target);
});
QUnit.test("many2many list: conditional create/delete actions", async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
serverData.views = {
"partner_type,false,list": '<tree><field name="name"/></tree>',
"partner_type,false,search": "<search/>",
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="color"/>
<field name="timmy" options="{'create': [('color', '=', 'red')], 'delete': [('color', '=', 'red')]}">
<tree>
<field name="name"/>
</tree>
</field>
</form>`,
resId: 1,
});
// color is red -> create and delete actions are available
assert.containsOnce(
target,
".o_field_x2many_list_row_add",
"should have the 'Add an item' link"
);
assert.containsN(target, ".o_list_record_remove", 2, "should have two remove icons");
await click($(target).find(".o_field_x2many_list_row_add a")[0]);
assert.containsN(
target,
".modal .modal-footer button",
3,
"there should be 3 buttons available in the modal"
);
await click($(".modal .modal-footer .o_form_button_cancel")[0]);
// set color to black -> create and delete actions are no longer available
await editSelect(target, 'div[name="color"] select', '"black"');
// add a line and remove icon should still be there as they don't create/delete records,
// but rather add/remove links
assert.containsOnce(
target,
".o_field_x2many_list_row_add",
'"Add a line" button should still be available even after color field changed'
);
assert.containsN(
target,
".o_list_record_remove",
2,
"should still have remove icon even after color field changed"
);
await click($(target).find(".o_field_x2many_list_row_add a")[0]);
assert.containsN(
target,
".modal .modal-footer button",
2,
'"Create" button should not be available in the modal after color field changed'
);
});
QUnit.test("many2many field with link/unlink options (list)", async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
serverData.views = {
"partner_type,false,list": '<tree><field name="name"/></tree>',
"partner_type,false,search": "<search/>",
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="color"/>
<field name="timmy" options="{'link': [('color', '=', 'red')], 'unlink': [('color', '=', 'red')]}">
<tree>
<field name="name"/>
</tree>
</field>
</form>`,
resId: 1,
});
// color is red -> link and unlink actions are available
assert.containsOnce(
target,
".o_field_x2many_list_row_add",
"should have the 'Add an item' link"
);
assert.containsN(target, ".o_list_record_remove", 2, "should have two remove icons");
await click($(target).find(".o_field_x2many_list_row_add a")[0]);
assert.containsN(
target,
".modal .modal-footer button",
3,
"there should be 3 buttons available in the modal (Create action is available)"
);
await click($(".modal .modal-footer .o_form_button_cancel")[0]);
// set color to black -> link and unlink actions are no longer available
await editSelect(target, 'div[name="color"] select', '"black"');
assert.containsNone(
target,
".o_field_x2many_list_row_add",
'"Add a line" should no longer be available after color field changed'
);
assert.containsNone(
target,
".o_list_record_remove",
"should no longer have remove icon after color field changed"
);
});
QUnit.test(
'many2many field with link/unlink options (list, create="0")',
async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
serverData.views = {
"partner_type,false,list": '<tree><field name="name"/></tree>',
"partner_type,false,search": "<search/>",
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="color"/>
<field name="timmy" options="{'link': [('color', '=', 'red')], 'unlink': [('color', '=', 'red')]}">
<tree create="0">
<field name="name"/>
</tree>
</field>
</form>`,
resId: 1,
});
// color is red -> link and unlink actions are available
assert.containsOnce(
target,
".o_field_x2many_list_row_add",
"should have the 'Add an item' link"
);
assert.containsN(target, ".o_list_record_remove", 2, "should have two remove icons");
await click($(target).find(".o_field_x2many_list_row_add a")[0]);
assert.containsN(
document.body,
".modal .modal-footer button",
2,
"there should be 2 buttons available in the modal (Create action is not available)"
);
await click($(".modal .modal-footer .o_form_button_cancel")[0]);
// set color to black -> link and unlink actions are no longer available
await editSelect(target, 'div[name="color"] select', '"black"');
assert.containsNone(
target,
".o_field_x2many_list_row_add",
'"Add a line" should no longer be available after color field changed'
);
assert.containsNone(
target,
".o_list_record_remove",
"should no longer have remove icon after color field changed"
);
}
);
QUnit.test("many2many field with link option (kanban)", async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
serverData.views = {
"partner_type,false,list": '<tree><field name="name"/></tree>',
"partner_type,false,search": "<search/>",
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="color"/>
<field name="timmy" options="{'link': [('color', '=', 'red')]}">
<kanban>
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
</t>
</templates>
</kanban>
</field>
</form>`,
resId: 1,
});
// color is red -> link and unlink actions are available
assert.containsOnce(target, ".o-kanban-button-new", "should have the 'Add' button");
await click(target.querySelector(".o-kanban-button-new"));
assert.containsN(
document.body,
".modal .modal-footer button",
3,
"there should be 3 buttons available in the modal (Create action is available"
);
await click($(".modal .modal-footer .o_form_button_cancel")[0]);
// set color to black -> link and unlink actions are no longer available
await editSelect(target, 'div[name="color"] select', '"black"');
assert.containsNone(
target,
".o-kanban-button-new",
'"Add" should no longer be available after color field changed'
);
});
QUnit.test('many2many field with link option (kanban, create="0")', async function (assert) {
serverData.models.partner.records[0].timmy = [12, 14];
serverData.views = {
"partner_type,false,list": '<tree><field name="name"/></tree>',
"partner_type,false,search": "<search/>",
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="color"/>
<field name="timmy" options="{'link': [('color', '=', 'red')]}">
<kanban create="0">
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
</t>
</templates>
</kanban>
</field>
</form>`,
resId: 1,
});
// color is red -> link and unlink actions are available
assert.containsOnce(target, ".o-kanban-button-new", "should have the 'Add' button");
await click(target.querySelector(".o-kanban-button-new"));
assert.containsN(
document.body,
".modal .modal-footer button",
2,
"there should be 2 buttons available in the modal (Create action is not available"
);
await click($(".modal .modal-footer .o_form_button_cancel")[0]);
// set color to black -> link and unlink actions are no longer available
await editSelect(target, 'div[name="color"] select', '"black"');
assert.containsNone(
target,
".o-kanban-button-new",
'"Add" should no longer be available after color field changed'
);
});
QUnit.test("many2many list: list of id as default value", async function (assert) {
serverData.models.partner.fields.turtles.default = [2, 3];
serverData.models.partner.fields.turtles.type = "many2many";
await makeView({
type: "form",
resModel: "partner",
serverData,
arch:
"<form>" +
'<field name="turtles">' +
"<tree>" +
'<field name="turtle_foo"/>' +
"</tree>" +
"</field>" +
"</form>",
});
assert.strictEqual(
$(target).find("td.o_data_cell").text(),
"blipkawa",
"should have loaded default data"
);
});
QUnit.test("many2many list with x2many: add a record", async function (assert) {
serverData.models.partner_type.fields.m2m = {
string: "M2M",
type: "many2many",
relation: "turtle",
};
serverData.models.partner_type.records[0].m2m = [1, 2];
serverData.models.partner_type.records[1].m2m = [2, 3];
serverData.views = {
"partner_type,false,list": `
<tree>
<field name="display_name"/>
<field name="m2m" widget="many2many_tags"/>
</tree>`,
"partner_type,false,search":
'<search><field name="display_name" string="Name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: '<form><field name="timmy"/></form>',
resId: 1,
mockRPC(route, args) {
if (args.method !== "get_views") {
assert.step(_.last(route.split("/")) + " on " + args.model);
}
if (args.model === "turtle") {
assert.step(JSON.stringify(args.args[0])); // the read ids
}
},
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
await click($(target).find(".modal .o_data_row:first .o_data_cell")[0]);
assert.containsOnce(
target,
".o_data_row",
"the record should have been added to the relation"
);
assert.strictEqual(
$(target).find(".o_data_row:first .o_tag_badge_text").text(),
"leonardodonatello",
"inner m2m should have been fetched and correctly displayed"
);
await click(target.querySelector(".o_field_x2many_list_row_add a"));
await click(target.querySelector(".modal .o_data_row:nth-child(1) .o_data_cell"));
assert.containsN(
target,
".o_data_row",
2,
"the second record should have been added to the relation"
);
assert.strictEqual(
$(target).find(".o_data_row:nth(1) .o_tag_badge_text").text(),
"donatelloraphael",
"inner m2m should have been fetched and correctly displayed"
);
assert.verifySteps([
"read on partner",
"web_search_read on partner_type",
"read on turtle",
"[1,2,3]",
"read on partner_type",
"read on turtle",
"[1,2]",
"web_search_read on partner_type",
"read on turtle",
"[2,3]",
"read on partner_type",
"read on turtle",
"[2,3]",
]);
});
QUnit.test("many2many with a domain", async function (assert) {
// The domain specified on the field should not be replaced by the potential
// domain the user writes in the dialog, they should rather be concatenated
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,search":
'<search><field name="display_name" string="Name"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" domain="[['display_name', '=', 'gold']]"/>
</form>`,
resId: 1,
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
assert.strictEqual($(".modal .o_data_row").length, 1, "should contain only one row (gold)");
const modal = document.body.querySelector(".modal");
await editSearch(modal, "s");
await validateSearch(modal);
assert.strictEqual($(".modal .o_data_row").length, 0, "should contain no row");
});
QUnit.test("many2many list with onchange and edition of a record", async function (assert) {
serverData.models.partner.fields.turtles.type = "many2many";
serverData.models.partner.onchanges.turtles = function () {};
serverData.views = {
"turtle,false,form": '<form string="Turtle Power"><field name="turtle_bar"/></form>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="turtles">
<tree>
<field name="turtle_foo"/>
</tree>
</field>
</form>`,
resId: 1,
mockRPC(route, args) {
assert.step(args.method);
},
});
assert.verifySteps(["get_views", "read", "read"]);
await click($(target).find("td.o_data_cell:first")[0]);
assert.verifySteps(["get_views", "read"]);
await click($('.modal-body input[type="checkbox"]')[0]);
await click($(".modal .modal-footer .btn-primary").first()[0]);
assert.verifySteps(["write", "onchange", "read"]);
// there is nothing left to save -> should not do a 'write' RPC
await clickSave(target);
assert.verifySteps([]);
});
QUnit.test(
"many2many widget: creates a new record with a context containing the parentID",
async function (assert) {
serverData.views = {
"turtle,false,list": '<tree><field name="display_name"/></tree>',
"turtle,false,search": '<search><field name="display_name"/></search>',
"turtle,false,form":
'<form string="Turtle Power"><field name="turtle_trululu"/></form>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="turtles" widget="many2many" context="{'default_turtle_trululu': id}" >
<tree>
<field name="turtle_foo"/>
</tree>
</field>
</form>`,
resId: 1,
mockRPC(route, args) {
const { method, kwargs } = args;
assert.step(method);
if (method === "onchange") {
assert.strictEqual(kwargs.context.default_turtle_trululu, 1);
assert.deepEqual(args.args, [
[],
{},
[],
{
turtle_trululu: "",
},
]);
}
},
});
assert.verifySteps(["get_views", "read", "read"]);
await addRow(target);
assert.verifySteps(["get_views", "web_search_read"]);
await click(target, ".o_create_button");
assert.strictEqual(
target.querySelector("[name='turtle_trululu'] input").value,
"first record"
);
assert.verifySteps(["get_views", "onchange"]);
}
);
QUnit.test("onchange with 40+ commands for a many2many", async function (assert) {
// this test ensures that the basic_model correctly handles more LINK_TO
// commands than the limit of the dataPoint (40 for x2many kanban)
assert.expect(25);
// create a lot of partner_types that will be linked by the onchange
var commands = [[5]];
for (var i = 0; i < 45; i++) {
var id = 100 + i;
serverData.models.partner_type.records.push({ id: id, display_name: "type " + id });
commands.push([4, id]);
}
serverData.models.partner.onchanges = {
foo: function (obj) {
obj.timmy = commands;
},
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="foo"/>
<field name="timmy">
<kanban>
<field name="display_name"/>
<templates>
<t t-name="kanban-box">
<div><t t-esc="record.display_name.value"/></div>
</t>
</templates>
</kanban>
</field>
</form>`,
resId: 1,
mockRPC(route, args) {
assert.step(args.method);
if (args.method === "write") {
assert.strictEqual(args.args[1].timmy[0][0], 6, "should send a command 6");
assert.strictEqual(
args.args[1].timmy[0][2].length,
45,
"should replace with 45 ids"
);
}
},
});
assert.verifySteps(["get_views", "read"]);
await editInput(target, ".o_field_widget[name=foo] input", "trigger onchange");
assert.verifySteps(["onchange", "read"]);
assert.strictEqual(
$(target).find(".o_x2m_control_panel .o_pager_counter").text().trim(),
"1-40 / 45",
"pager should be correct"
);
assert.strictEqual(
$(target).find('.o_kanban_record:not(".o_kanban_ghost")').length,
40,
"there should be 40 records displayed on page 1"
);
await click($(target).find(".o_field_widget[name=timmy] .o_pager_next")[0]);
assert.verifySteps(["read"]);
assert.strictEqual(
$(target).find(".o_x2m_control_panel .o_pager_counter").text().trim(),
"41-45 / 45",
"pager should be correct"
);
assert.strictEqual(
$(target).find('.o_kanban_record:not(".o_kanban_ghost")').length,
5,
"there should be 5 records displayed on page 2"
);
await clickSave(target);
assert.strictEqual(
$(target).find(".o_x2m_control_panel .o_pager_counter").text().trim(),
"1-40 / 45",
"pager should be correct"
);
assert.strictEqual(
$(target).find('.o_kanban_record:not(".o_kanban_ghost")').length,
40,
"there should be 40 records displayed on page 1"
);
await click($(target).find(".o_field_widget[name=timmy] .o_pager_next")[0]);
assert.strictEqual(
$(target).find(".o_x2m_control_panel .o_pager_counter").text().trim(),
"41-45 / 45",
"pager should be correct"
);
assert.strictEqual(
$(target).find('.o_kanban_record:not(".o_kanban_ghost")').length,
5,
"there should be 5 records displayed on page 2"
);
await click($(target).find(".o_field_widget[name=timmy] .o_pager_next")[0]);
assert.strictEqual(
$(target).find(".o_x2m_control_panel .o_pager_counter").text().trim(),
"1-40 / 45",
"pager should be correct"
);
assert.strictEqual(
$(target).find('.o_kanban_record:not(".o_kanban_ghost")').length,
40,
"there should be 40 records displayed on page 1"
);
assert.verifySteps(["write", "read", "read", "read"]);
});
QUnit.test("default_get, onchange, onchange on m2m", async function (assert) {
assert.expect(1);
serverData.models.partner.onchanges.int_field = function (obj) {
if (obj.int_field === 2) {
assert.deepEqual(obj.timmy, [
[6, false, [12]],
[1, 12, { display_name: "gold" }],
]);
}
obj.timmy = [[5], [1, 12, { display_name: "gold" }]];
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<sheet>
<field name="timmy">
<tree>
<field name="display_name"/>
</tree>
</field>
<field name="int_field"/>
</sheet>
</form>`,
});
await editInput(target, ".o_field_widget[name=int_field] input", 2);
});
QUnit.test("many2many list add *many* records, remove, re-add", async function (assert) {
assert.expect(5);
serverData.models.partner.fields.timmy.domain = [["color", "=", 2]];
serverData.models.partner.fields.timmy.onChange = true;
serverData.models.partner_type.fields.product_ids = {
string: "Product",
type: "many2many",
relation: "product",
};
for (var i = 0; i < 50; i++) {
var new_record_partner_type = { id: 100 + i, display_name: "batch" + i, color: 2 };
serverData.models.partner_type.records.push(new_record_partner_type);
}
serverData.views = {
"partner_type,false,list": '<tree><field name="display_name"/></tree>',
"partner_type,false,search":
'<search><field name="display_name"/><field name="color"/></search>',
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="many2many">
<tree>
<field name="display_name"/>
<field name="product_ids" widget="many2many_tags"/>
</tree>
</field>
</form>`,
resId: 1,
mockRPC(route, args) {
if (args.method === "get_formview_id") {
assert.deepEqual(
args.args[0],
[1],
"should call get_formview_id with correct id"
);
}
},
});
// First round: add 51 records in batch
await click(target.querySelector(".o_field_x2many_list_row_add a"));
var $modal = $(".modal-lg");
assert.equal($modal.length, 1, "There should be one modal");
await click($modal.find("thead input[type=checkbox]")[0]);
await nextTick();
await click($modal.find(".btn.btn-primary.o_select_button")[0]);
assert.strictEqual(
$(target).find(".o_data_row").length,
51,
"We should have added all the records present in the search view to the m2m field"
); // the 50 in batch + 'gold'
await clickSave(target);
// Secound round: remove one record
var trash_buttons = $(target).find(
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_list_record_remove"
);
await click(trash_buttons.first()[0]);
var pager_limit = $(target).find(
".o_field_many2many.o_field_widget .o_field_x2many.o_field_x2many_list .o_pager_limit"
);
assert.equal(pager_limit.text(), "50", "We should have 50 records in the m2m field");
// Third round: re-add 1 records
await click($(target).find(".o_field_x2many_list_row_add a")[0]);
$modal = $(".modal-lg");
assert.equal($modal.length, 1, "There should be one modal");
await click($modal.find("thead input[type=checkbox]")[0]);
await nextTick();
await click($modal.find(".btn.btn-primary.o_select_button")[0]);
assert.strictEqual(
$(target).find(".o_data_row").length,
51,
"We should have 51 records in the m2m field"
);
});
QUnit.test("many2many kanban: action/type attribute", async function (assert) {
serverData.models.partner.records[0].timmy = [12];
const form = await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy">
<kanban action="a1" type="object">
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="display_name"/>
</div>
</t>
</templates>
</kanban>
</field>
</form>`,
resId: 1,
});
patchWithCleanup(form.env.services.action, {
doActionButton(params) {
assert.step(`doActionButton type ${params.type} name ${params.name}`);
params.onClose();
},
});
await click(target.querySelector(".oe_kanban_global_click"));
assert.verifySteps(["doActionButton type object name a1"]);
});
QUnit.test("select create with _view_ref as text", async (assert) => {
serverData.views = {
"partner_type,my.little.string,list": `<tree><field name="display_name"/></tree>`,
"partner_type,false,search": `<search />`,
};
patchWithCleanup(browser, {
setTimeout: (fn) => Promise.resolve().then(fn),
});
patchWithCleanup(Many2XAutocomplete.defaultProps, {
searchLimit: 1,
});
let checkGetViews = false;
await makeView({
type: "form",
resId: 1,
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_tags" context="{ 'tree_view_ref': 'my.little.string' }"/>
</form>`,
mockRPC(route, args) {
if (args.method === "get_views" && checkGetViews) {
assert.step("get_views");
assert.deepEqual(args.kwargs.views, [
[false, "list"],
[false, "search"],
]);
assert.strictEqual(args.kwargs.context.tree_view_ref, "my.little.string");
}
},
});
await click(target, ".o_field_many2many_selection input");
checkGetViews = true;
await clickOpenedDropdownItem(target, "timmy", "Search More...");
assert.verifySteps([`get_views`]);
assert.containsOnce(target, ".modal");
assert.strictEqual(target.querySelector(".modal-title").textContent, "Search: pokemon");
});
QUnit.test("many2many basic keys in field evalcontext -- in list", async (assert) => {
assert.expect(6);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
patchWithCleanup(session, {
user_companies: {
allowed_companies: {
3: { id: 3, name: "Hermit", sequence: 1 },
2: { id: 2, name: "Herman's", sequence: 2 },
1: { id: 1, name: "Heroes TM", sequence: 3 },
},
current_company: 3,
},
});
registry.category("services").add("company", companyService, { force: true });
patchWithCleanup(browser, {
setTimeout: (fn) => Promise.resolve().then(fn),
});
await makeView({
type: "list",
resModel: "partner",
serverData,
arch: `
<tree editable="top">
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
</tree>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
});
await click(target.querySelector(".o_data_cell"));
await editInput(target, ".o_field_many2many_selection input", "indianapolis");
await nextTick();
await clickOpenedDropdownItem(target, "timmy", "Create and edit...");
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
);
});
QUnit.test("many2many basic keys in field evalcontext -- in form", async (assert) => {
assert.expect(6);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
patchWithCleanup(session, {
user_companies: {
allowed_companies: {
3: { id: 3, name: "Hermit", sequence: 1 },
2: { id: 2, name: "Herman's", sequence: 2 },
1: { id: 1, name: "Heroes TM", sequence: 3 },
},
current_company: 3,
},
});
registry.category("services").add("company", companyService, { force: true });
patchWithCleanup(browser, {
setTimeout: (fn) => Promise.resolve().then(fn),
});
await makeView({
type: "form",
resId: 1,
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
});
await editInput(target, ".o_field_many2many_selection input", "indianapolis");
await nextTick();
await clickOpenedDropdownItem(target, "timmy", "Create and edit...");
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
);
});
QUnit.test(
"many2many basic keys in field evalcontext -- in a x2many in form",
async (assert) => {
assert.expect(6);
serverData.models.partner_type.fields.partner_id = {
string: "Partners",
type: "many2one",
relation: "partner",
};
serverData.views = {
"partner_type,false,form": `<form><field name="partner_id" /></form>`,
};
const rec = serverData.models.partner.records.find(({ id }) => id === 2);
rec.p = [1];
patchWithCleanup(session, {
user_companies: {
allowed_companies: {
3: { id: 3, name: "Hermit", sequence: 1 },
2: { id: 2, name: "Herman's", sequence: 2 },
1: { id: 1, name: "Heroes TM", sequence: 3 },
},
current_company: 3,
},
});
registry.category("services").add("company", companyService, { force: true });
patchWithCleanup(browser, {
setTimeout: (fn) => Promise.resolve().then(fn),
});
await makeView({
type: "form",
resId: 2,
resModel: "partner",
serverData,
arch: `
<form>
<field name="p">
<tree editable="top">
<field name="timmy" widget="many2many_tags" context="{ 'default_partner_id': active_id, 'ids': active_ids, 'model': active_model, 'company_id': current_company_id}"/>
</tree>
</field>
</form>`,
mockRPC(route, args) {
if (args.method === "onchange") {
assert.strictEqual(args.kwargs.context.default_partner_id, 1);
assert.strictEqual(args.kwargs.context.model, "partner");
assert.deepEqual(args.kwargs.context.ids, [1]);
assert.strictEqual(args.kwargs.context.company_id, 3);
}
},
});
await click(target, ".o_data_cell");
await editInput(target, ".o_field_many2many_selection input", "indianapolis");
await clickOpenedDropdownItem(target, "timmy", "Create and edit...");
assert.containsOnce(target, ".modal .o_field_many2one");
assert.strictEqual(
target.querySelector(".modal .o_field_many2one input").value,
"first record"
);
}
);
QUnit.test("many2many field calling replaceWith (add + remove)", async function (assert) {
serverData.models.partner.records[0].p = [1];
class MyX2Many extends Component {
onClick() {
this.props.value.replaceWith([2, 3]);
}
}
MyX2Many.template = xml`
<span class="ids" t-esc="this.props.value.resIds"/>
<button class="my_btn" t-on-click="onClick">To id</button>`;
registry.category("fields").add("my_x2many", MyX2Many);
await makeView({
type: "form",
resModel: "turtle",
serverData,
arch: `
<form>
<field name="partner_ids" widget="my_x2many"/>
</form>`,
resId: 2,
});
assert.strictEqual(target.querySelector(".ids").innerText, "2,4");
await click(target.querySelector(".my_btn"));
assert.strictEqual(target.querySelector(".ids").innerText, "2,3");
});
QUnit.test("`this` inside rendererProps should reference the component", async function (assert) {
class CustomX2manyField extends X2ManyField {
setup() {
super.setup();
this.selectCreate = (params) => {
assert.step("selectCreate");
assert.strictEqual(this.num, 2);
};
this.num = 1;
}
async onAdd({ context, editable } = {}) {
this.num = 2;
assert.step("onAdd");
super.onAdd(...arguments);
}
}
registry.category("fields").add("custom_x2many", CustomX2manyField);
serverData.views = {
"partner_type,false,list": `<tree><field name="display_name"/></tree>`,
"partner_type,false,search": `<search></search>`,
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="timmy" widget="custom_x2many">
<tree editable="top">
<field name="display_name"/>
</tree>
<form>
<field name="display_name" />
</form>
</field>
</form>`,
resId: 1,
});
await click(target.querySelector(".o_field_x2many_list_row_add a"));
assert.verifySteps(["onAdd", "selectCreate"]);
});
});