Files
goodie/frontend/account/static/tests/section_and_note.test.js
Marc 8741282322 Eliminate Python dependency: embed frontend assets in odoo-go
- Copy all OWL frontend assets (JS/CSS/XML/fonts/images) into frontend/
  directory (2925 files, 43MB) — no more runtime reads from Python Odoo
- Replace OdooAddonsPath config with FrontendDir pointing to local frontend/
- Rewire bundle.go, static.go, templates.go, webclient.go to read from
  frontend/ instead of external Python Odoo addons directory
- Auto-detect frontend/ and build/ dirs relative to binary in main.go
- Delete obsolete Python helper scripts (tools/*.py)

The Go server is now fully self-contained: single binary + frontend/ folder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 23:09:12 +02:00

1516 lines
48 KiB
JavaScript

import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, getFixture, test } from "@odoo/hoot";
import { animationFrame, edit, press, queryAllTexts } from "@odoo/hoot-dom";
import {
clickSave,
contains,
defineModels,
fields,
models,
mountView,
onRpc,
} from "@web/../tests/web_test_helpers";
class InvoiceLine extends models.Model {
_name = "invoice_line";
_records = [
{ id: 1, name: "r1", display_type: false, sequence: 1, m2m: [1] },
{ id: 2, name: "r2", display_type: false, sequence: 2, m2m: [2] },
{ id: 3, name: "A", display_type: "line_section", sequence: 3, m2m: [1] },
{ id: 4, name: "A1", display_type: false, sequence: 4, m2m: [1, 3] },
{ id: 5, name: "A2", display_type: false, sequence: 5, m2m: [1, 2] },
{ id: 6, name: "B", display_type: "line_section", sequence: 6, m2m: [2] },
{ id: 7, name: "B1", display_type: false, sequence: 7, m2m: [1, 3] },
{ id: 8, name: "B2", display_type: false, sequence: 8, m2m: [] },
{ id: 9, name: "Ba", display_type: "line_subsection", sequence: 9, m2m: [3] },
{ id: 10, name: "Ba1", display_type: false, sequence: 10, m2m: [2, 3] },
{ id: 11, name: "Ba2", display_type: false, sequence: 11, m2m: [3] },
{ id: 12, name: "C", display_type: "line_section", sequence: 12, m2m: [1] },
{ id: 13, name: "C1", display_type: false, sequence: 13, m2m: [] },
];
name = fields.Char();
display_type = fields.Selection({
default: false,
selection: [
["line_section", "Section"],
["line_subsection", "Subsection"],
["line_note", "Note"],
],
});
invoice_id = fields.Many2one({
string: "Invoice",
relation: "invoice",
});
sequence = fields.Integer();
m2m = fields.Many2many({ relation: "bar" });
collapse_composition = fields.Boolean({ default: false });
collapse_prices = fields.Boolean({ default: false });
}
class Invoice extends models.Model {
_records = [
{
id: 1,
invoice_line_ids: Array.from({ length: InvoiceLine._records.length }, (_, i) => i + 1),
},
];
invoice_line_ids = fields.One2many({ relation: "invoice_line" });
}
class Bar extends models.Model {
name = fields.Char();
_records = [
{ id: 1, name: "Value 1" },
{ id: 2, name: "Value 2" },
{ id: 3, name: "Value 3" },
];
}
defineModels([Invoice, InvoiceLine, Bar]);
defineMailModels();
const LINE_COLLAPSE_ARCH = `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True, 'hide_composition': True, 'hide_prices': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
<field name="collapse_composition" column_invisible="1"/>
<field name="collapse_prices" column_invisible="1"/>
</list>
</field>
</form>
`;
onRpc("has_group", () => true);
test("can add a line in a section", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_list_section_options:eq(0) button").click();
await contains(".o-dropdown-item:contains(Add a line)").click();
await edit("A3");
await contains(getFixture()).click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"A3",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
});
test("can add a line in a subsection", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_list_section_options:last button").click();
await contains(".o-dropdown-item:contains(Add a line)").click();
await edit("Ca3");
await contains(getFixture()).click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
"Ca3",
]);
});
test("can add a subsection in a section", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_list_section_options:eq(0) button").click();
await contains(".o-dropdown-item:contains(Add a subsection)").click();
await edit("Aa");
await contains(getFixture()).click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"Aa",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
expect(".o_is_line_subsection:contains(Aa)").toHaveCount(1);
});
test("can't add a subsection if value not in options", async () => {
InvoiceLine._records[10].display_type = "line_section";
InvoiceLine._fields.display_type = fields.Selection({
default: false,
selection: [
["line_section", "Section"],
["line_note", "Note"],
],
});
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
await contains(".o_list_section_options:last button").click();
expect(".o-dropdown-item:contains(Add a subsection)").toHaveCount(0);
});
test("can delete sections", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_list_section_options:eq(1) button").click();
await contains(".o-dropdown-item:contains(Delete)").click();
expect(queryAllTexts(".o_data_row")).toEqual(["r1", "r2", "A", "A1", "A2", "C", "C1"]);
});
test("can delete subsections", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_is_line_subsection .o_list_section_options button").click();
await contains(".o-dropdown-item:contains(Delete)").click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"C",
"C1",
]);
});
test("can duplicate sections", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_list_section_options:eq(1) button").click();
await contains(".o-dropdown-item:contains(Duplicate)").click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
});
test("can duplicate subsections", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_is_line_subsection .o_list_section_options button").click();
await contains(".o-dropdown-item:contains(Duplicate)").click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
});
test("can resequence records inside sections", async () => {
onRpc("web_save", ({ args }) => {
expect.step("web_save");
expect(args[1]).toEqual({
invoice_line_ids: [
[1, 4, { sequence: 1 }],
[1, 1, { sequence: 2 }],
[1, 2, { sequence: 3 }],
[1, 3, { sequence: 4 }],
[1, 6, { sequence: 6 }],
[1, 7, { sequence: 7 }],
[1, 8, { sequence: 8 }],
[1, 9, { sequence: 9 }],
[1, 10, { sequence: 10 }],
[1, 11, { sequence: 11 }],
[1, 5, { sequence: 12 }],
[1, 13, { sequence: 5 }],
[1, 12, { sequence: 13 }],
],
});
});
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_data_row:eq(3) .o_row_handle").dragAndDrop(".o_data_row:eq(0)");
expect(queryAllTexts(".o_data_row")).toEqual([
"A1",
"r1",
"r2",
"A",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_data_row:eq(4) .o_row_handle").dragAndDrop(".o_data_row:eq(10)");
expect(queryAllTexts(".o_data_row")).toEqual([
"A1",
"r1",
"r2",
"A",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"A2",
"C",
"C1",
]);
await contains(".o_data_row:last .o_row_handle").dragAndDrop(".o_data_row:eq(4)");
expect(queryAllTexts(".o_data_row")).toEqual([
"A1",
"r1",
"r2",
"A",
"C1",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"A2",
"C",
]);
await clickSave();
expect.verifySteps(["web_save"]);
});
test("resequence can be discarded", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_data_row:eq(3) .o_row_handle").dragAndDrop(".o_data_row:eq(0)");
expect(queryAllTexts(".o_data_row")).toEqual([
"A1",
"r1",
"r2",
"A",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_form_button_cancel").click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
});
test("can resequence sections", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
await contains(".o_data_row:eq(11) .o_row_handle", { visible: false }).dragAndDrop(".o_data_row:eq(0)");
expect(queryAllTexts(".o_data_row")).toEqual(
["C", "r1", "r2", "A", "A1", "A2", "B", "B1", "B2", "Ba", "Ba1", "Ba2", "C1"],
{
message: "With C on top, B becomes the top section for all records starting from B1",
}
);
await contains(".o_list_section_options:eq(2) button").click();
await contains(".o-dropdown-item:contains(Delete)").click();
expect(queryAllTexts(".o_data_row")).toEqual(["C", "r1", "r2", "A", "A1", "A2"], {
message: "Deleting B will then remove all records starting from B1",
});
});
test("add a section", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
expect(`.o_note_row`).toHaveCount(0);
await contains(".o_field_x2many_list_row_add a:eq(1)").click();
await edit("D");
await contains(getFixture()).click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
"D",
]);
expect(`.o_is_line_section`).toHaveCount(4);
});
test("add note", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
expect(`.o_note_row`).toHaveCount(0);
await contains(".o_field_x2many_list_row_add a:last").click();
await edit("this is a note");
await contains(getFixture()).click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
"this is a note",
]);
expect(`.o_is_line_note`).toHaveCount(1);
});
test("section_and_note_text widget", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name" widget="section_and_note_text"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
expect(`.o_note_row`).toHaveCount(0);
await contains(".o_field_x2many_list_row_add a:last").click();
expect(`.o_is_line_note textarea`).toHaveCount(1);
await edit("this is a note\non 2 lines");
await contains(getFixture()).click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
"this is a note\non 2 lines",
]);
});
test("sections with required content field", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name" required="1"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(".o_data_row").toHaveCount(13);
await contains(".o_list_section_options:eq(0) button").click();
await contains(".o-dropdown-item:contains(Add a subsection)").click();
expect(".o_data_row").toHaveCount(14);
await contains(".o_list_section_options:eq(1) button").click();
await contains(".o-dropdown-item:contains(Delete)").click();
expect(".o_data_row").toHaveCount(13);
await contains(".o_list_section_options:eq(0) button").click();
await contains(".o-dropdown-item:contains(Add a subsection)").click();
expect(".o_invalid_cell").toHaveCount(0);
await press("Enter");
await animationFrame();
expect(".o_invalid_cell").toHaveCount(1);
expect(".o_data_row").toHaveCount(14);
await contains(".o_form_button_cancel").click();
expect(".o_data_row").toHaveCount(13);
await contains(".o_field_x2many_list_row_add a:eq(1)").click();
expect(".o_data_row").toHaveCount(14);
expect(".o_invalid_cell").toHaveCount(0);
await press("Enter");
await animationFrame();
expect(".o_invalid_cell").toHaveCount(1);
await edit("D");
await press("Enter");
await animationFrame();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
"D",
"",
]);
});
test("sections duplicate with many2many", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="m2m" widget="many2many_tags"/>
<field name="sequence"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual(
[
"r1 \nValue 1\n 1",
"r2 \nValue 2\n 2",
"A",
"A1 \nValue 1\nValue 3\n 4",
"A2 \nValue 1\nValue 2\n 5",
"B",
"B1 \nValue 1\nValue 3\n 7",
"B2 \n 8",
"Ba",
"Ba1 \nValue 2\nValue 3\n 10",
"Ba2 \nValue 3\n 11",
"C",
"C1 \n 13",
],
{ message: "m2m values are not shown inside (sub-)section rows" }
);
await contains(".o_list_section_options:eq(1) button").click();
await contains(".o-dropdown-item:contains(Duplicate)").click();
expect(queryAllTexts(".o_data_row")).toEqual(
[
"r1 \nValue 1\n 1",
"r2 \nValue 2\n 2",
"A",
"A1 \nValue 1\nValue 3\n 4",
"A2 \nValue 1\nValue 2\n 5",
"B",
"B1 \nValue 1\nValue 3\n 7",
"B2 \n 8",
"Ba",
"Ba1 \nValue 2\nValue 3\n 10",
"Ba2 \nValue 3\n 11",
"B",
"B1 \nValue 1\nValue 3\n 13",
"B2 \n 14",
"Ba",
"Ba1 \nValue 2\nValue 3\n 16",
"Ba2 \nValue 3\n 17",
"C",
"C1 \n 19",
],
{ message: "m2m values are copied as well" }
);
await contains(".o_list_section_options:eq(2) button").click();
await contains(".o-dropdown-item:contains(Duplicate)").click();
expect(queryAllTexts(".o_data_row")).toEqual(
[
"r1 \nValue 1\n 1",
"r2 \nValue 2\n 2",
"A",
"A1 \nValue 1\nValue 3\n 4",
"A2 \nValue 1\nValue 2\n 5",
"B",
"B1 \nValue 1\nValue 3\n 7",
"B2 \n 8",
"Ba",
"Ba1 \nValue 2\nValue 3\n 10",
"Ba2 \nValue 3\n 11",
"Ba",
"Ba1 \nValue 2\nValue 3\n 13",
"Ba2 \nValue 3\n 14",
"B",
"B1 \nValue 1\nValue 3\n 16",
"B2 \n 17",
"Ba",
"Ba1 \nValue 2\nValue 3\n 19",
"Ba2 \nValue 3\n 20",
"C",
"C1 \n 22",
],
{ message: "m2m values are copied as well" }
);
});
test("swap sections and subsections", async () => {
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True}"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_list_section_options:eq(1) button").click();
await contains(".o-dropdown-item:contains(Add a subsection)").click();
await edit("Bb");
await contains(getFixture()).click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"Bb",
"C",
"C1",
]);
await contains(".o_list_section_options:eq(1) button").click();
expect(".o-dropdown-item:contains(Move Down)").toHaveCount(1);
expect(".o-dropdown-item:contains(Move Up)").toHaveCount(1);
await contains(".o_list_section_options:eq(0) button").click();
expect(".o-dropdown-item:contains(Move Down)").toHaveCount(1);
expect(".o-dropdown-item:contains(Move Up)").toHaveCount(0);
await contains(".o_list_section_options:eq(4) button").click();
expect(".o-dropdown-item:contains(Move Down)").toHaveCount(0);
expect(".o-dropdown-item:contains(Move Up)").toHaveCount(1);
await contains(".o_list_section_options:eq(1) button").click();
await contains(".o-dropdown-item:contains(Move Up)").click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"Bb",
"A",
"A1",
"A2",
"C",
"C1",
]);
await contains(".o_list_section_options:eq(1) button").click();
expect(".o-dropdown-item:contains(Move Down)").toHaveCount(1);
expect(".o-dropdown-item:contains(Move Up)").toHaveCount(0);
await contains(".o_list_section_options:eq(2) button").click();
expect(".o-dropdown-item:contains(Move Down)").toHaveCount(0);
expect(".o-dropdown-item:contains(Move Up)").toHaveCount(1);
await contains(".o_list_section_options:eq(1) button").click();
await contains(".o-dropdown-item:contains(Move Down)").click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"B",
"B1",
"B2",
"Bb",
"Ba",
"Ba1",
"Ba2",
"A",
"A1",
"A2",
"C",
"C1",
]);
});
test("check collapse_ fields' muting logic for widget", async () => {
InvoiceLine._fields.aggregated_field = fields.Float({ default: 3.00 });
InvoiceLine._records = [
{ id: 1, name: "sec1", display_type: "line_section", sequence: 1, m2m: [3], collapse_composition: true },
{ id: 2, name: "sec1-r1", display_type: false, sequence: 2, m2m: [2, 3] },
{ id: 3, name: "sec1-sub1", display_type: "line_subsection", sequence: 3, m2m: [1] },
{ id: 4, name: "sec1-sub1-r1", display_type: false, sequence: 4, m2m: [3] },
{ id: 5, name: "sec1-sub2", display_type: "line_subsection", sequence: 5, m2m: [1] },
{ id: 6, name: "sec1-sub2-r1", display_type: false, sequence: 6, m2m: [] },
{ id: 7, name: "sec2", display_type: "line_section", sequence: 7, m2m: [1], collapse_prices: true },
{ id: 8, name: "sec2-r1", display_type: false, sequence: 8, m2m: [2, 3] },
{ id: 9, name: "sec2-r2", display_type: false, sequence: 9, m2m: [3] },
{ id: 10, name: "sec2-sub1", display_type: "line_subsection", sequence: 10, m2m: [1] },
{ id: 11, name: "sec2-sub1-r1", display_type: false, sequence: 11, m2m: [3] },
{ id: 12, name: "sec2-sub2", display_type: "line_subsection", sequence: 12, m2m: [1] },
{ id: 13, name: "sec2-sub2-r1", display_type: false, sequence: 13, m2m: [] },
{ id: 14, name: "sec3", display_type: "line_section", sequence: 14, m2m: [1] },
{ id: 15, name: "sec3-r1", display_type: false, sequence: 15, m2m: [2, 3] },
{ id: 16, name: "sec3-sub1", display_type: "line_subsection", sequence: 16, m2m: [1], collapse_prices: true },
{ id: 17, name: "sec3-sub1-r1", display_type: false, sequence: 17, m2m: [3] },
{ id: 18, name: "sec3-sub2", display_type: "line_subsection", sequence: 18, m2m: [1], collapse_composition: true },
{ id: 19, name: "sec3-sub2-r1", display_type: false, sequence: 19, m2m: [] },
]
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: `
<form>
<field
name="invoice_line_ids"
widget="section_and_note_one2many"
options="{'subsections': True, 'hide_composition': True, 'hide_prices': True}"
aggregated_fields="aggregated_field"
>
<list editable="bottom">
<control>
<create name="add_line_control" string="Add a line"/>
<create name="add_section_control" string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="aggregated_field"/>
<field name="display_type" column_invisible="1"/>
<field name="collapse_composition" column_invisible="1"/>
<field name="collapse_prices" column_invisible="1"/>
</list>
</field>
</form>
`,
});
expect(queryAllTexts(".o_data_row .o_list_char")).toEqual([
"sec1",
"sec1-r1",
"sec1-sub1",
"sec1-sub1-r1",
"sec1-sub2",
"sec1-sub2-r1",
"sec2",
"sec2-r1",
"sec2-r2",
"sec2-sub1",
"sec2-sub1-r1",
"sec2-sub2",
"sec2-sub2-r1",
"sec3",
"sec3-r1",
"sec3-sub1",
"sec3-sub1-r1",
"sec3-sub2",
"sec3-sub2-r1",
]);
await contains(".o_list_section_options:first button").click();
expect(".o-dropdown-item:contains(Show Composition)").toHaveCount(1, {
message: "Sections should always show hide composition button"
});
expect(".o-dropdown-item:contains(Hide Prices)").toHaveCount(1, {
message: "Sections should always show hide prices button"
});
await contains(".o_data_row:contains(sec1-sub1) .o_list_section_options button").click();
expect(".o-dropdown-item:contains(Hide Composition)").toHaveClass("disabled", {
message: "Subsection under hidden section should have disabled hide composition button"
});
expect(".o-dropdown-item:contains(Hide Prices)").toHaveClass("disabled", {
message: "Subsection under hidden section should have disabled Hide Prices button"
});
expect(".o_data_row:contains(sec1-r1)").toHaveClass("text-muted", {
message: "Line under hidden section should be muted"
});
expect(".o_data_row:contains(sec1-sub1)").toHaveClass("text-muted", {
message: "Subsection under hidden section should be muted"
});
expect(".o_data_row:contains(sec1-sub1-r1)").toHaveClass("text-muted", {
message: "Line under subsection(which is under hidden section) should be muted"
});
expect(".o_data_row:contains(sec2-r1) > td[name='aggregated_field']").toHaveClass("text-muted", {
message: "Aggregated field column of Line under hidden prices section should be muted"
});
expect(".o_data_row:contains(sec2-sub1) > td[name='aggregated_field']").toHaveClass("text-muted", {
message: "Aggregated field column of Subsection under hidden prices section should be muted"
});
expect(".o_data_row:contains(sec2-sub1-r1) > td[name='aggregated_field']").toHaveClass("text-muted", {
message: "Aggregated field column of Line under subsection(which is under hidden prices section) should be muted"
});
expect(".o_data_row:contains(sec3-sub1-r1) > td[name='aggregated_field']").toHaveClass("text-muted", {
message: "Aggregated field column of Line under hidden prices subsection should be muted"
});
expect(".o_data_row:contains(sec3-sub2-r1)").toHaveClass("text-muted", {
message: "Line under hidden subsection should be muted"
});
})
test("check collapse_ fields' duplicating logic", async () => {
[2, 5, 8].forEach((i) => {
InvoiceLine._records[i].collapse_composition = true;
InvoiceLine._records[i].collapse_prices = true;
});
onRpc("web_save", ({ args }) => {
expect.step("web_save");
const [, { invoice_line_ids }] = args;
// Filter section/subsection records created via (0, 0, values)
const createdSectionRecords = invoice_line_ids.filter(([cmd, , values]) =>
cmd === 0 && ['line_section', 'line_subsection'].includes(values.display_type)
);
for (const [, , values] of createdSectionRecords) {
expect(values.collapse_composition).toBe(true, {
message: `collapse_composition should be true for (sub)section ${values.name}`,
});
expect(values.collapse_prices).toBe(true, {
message: `collapse_prices should be true for (sub)section ${values.name}`,
});
}
});
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: LINE_COLLAPSE_ARCH,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_data_row:contains(A):first .o_list_section_options button").click();
await contains(".o-dropdown-item:contains(Duplicate)").click();
await contains(".o_data_row:contains(B):first .o_list_section_options button").click();
await contains(".o-dropdown-item:contains(Duplicate)").click();
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await clickSave();
expect.verifySteps(["web_save"]);
})
test("check subsections' collapse_ fields' drag and drop logic", async () => {
InvoiceLine._records[2].collapse_composition = true;
InvoiceLine._records[8].collapse_composition = true;
InvoiceLine._records[8].collapse_prices = true;
onRpc("web_save", ({ args }) => {
expect.step("web_save");
expect(args[1].invoice_line_ids[0][2].collapse_composition).toBe(false, {
message: `collapse_composition should be reset to false for section 'Ba'`,
});
expect(args[1].invoice_line_ids[0][2].collapse_prices).toBe(false, {
message: `collapse_prices should be reset to false for section 'Ba'`,
});
});
await mountView({
type: "form",
resModel: "invoice",
resId: 1,
arch: LINE_COLLAPSE_ARCH,
});
expect(queryAllTexts(".o_data_row")).toEqual([
"r1",
"r2",
"A",
"A1",
"A2",
"B",
"B1",
"B2",
"Ba",
"Ba1",
"Ba2",
"C",
"C1",
]);
await contains(".o_data_row:contains(Ba):first .o_row_handle").dragAndDrop(".o_data_row:contains(A1):first");
await clickSave();
expect.verifySteps(["web_save"]);
})