19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:07 +01:00
parent ba20ce7443
commit 768b70e05e
2357 changed files with 1057103 additions and 712486 deletions

View file

@ -0,0 +1,124 @@
import {
click,
insertText,
openFormView,
start,
startServer,
triggerHotkey
} from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { asyncStep, contains, defineModels, fields, onRpc, models, waitForSteps} from "@web/../tests/web_test_helpers";
import { defineAccountModels } from "./account_test_helpers";
defineAccountModels();
test("When I switch tabs, it saves", async () => {
const pyEnv = await startServer();
const accountMove = pyEnv["account.move"].create({ name: "move0" });
await start();
onRpc("account.move", "web_save", () => {
asyncStep("tab saved");
});
await openFormView("account.move", accountMove, {
arch: `<form js_class='account_move_form'>
<sheet>
<notebook>
<page id="invoice_tab" name="invoice_tab" string="Invoice Lines">
<field name="name"/>
</page>
<page id="aml_tab" string="Journal Items" name="aml_tab"></page>
</notebook>
</sheet>
</form>`,
});
await insertText("[name='name'] input", "somebody save me!");
triggerHotkey("Enter");
await click('a[name="aml_tab"]');
await waitForSteps(["tab saved"]);
});
test("Confirmation dialog on delete contains a warning", async () => {
const pyEnv = await startServer();
const accountMove = pyEnv["account.move"].create({ name: "move0" });
await start();
onRpc("account.move", "check_move_sequence_chain", () => {
return false;
});
await openFormView("account.move", accountMove, {
arch: `<form js_class='account_move_form'>
<sheet>
<notebook>
<page id="invoice_tab" name="invoice_tab" string="Invoice Lines">
<field name="name"/>
</page>
<page id="aml_tab" string="Journal Items" name="aml_tab"></page>
</notebook>
</sheet>
</form>`,
});
await contains(".o_cp_action_menus button").click();
await contains(".o_menu_item:contains(Delete)").click();
expect(".o_dialog div.text-danger").toHaveText("This operation will create a gap in the sequence.", {
message: "warning message has been added in the dialog"
});
});
class AccountMove extends models.Model {
line_ids = fields.One2many({
string: "Invoice Lines",
relation: "account.move.line",
})
_records = [{ id: 1, name: "account.move" }]
}
class AccountMoveLine extends models.Model {
name = fields.Char();
product_id = fields.Many2one({
string:"Product",
relation:"product",
});
move_id = fields.Many2one({
string: "Account Move",
relation: "account.move",
})
}
class Product extends models.Model {
name = fields.Char();
_records = [{ id: 1, name: "testProduct" }];
}
defineModels({ Product, AccountMoveLine, AccountMove });
test("Update description on product line", async() => {
const pyEnv = await startServer();
const productId = pyEnv["product"].browse([1]);
const accountMove = pyEnv["account.move"].browse([1]);
pyEnv["account.move.line"].create({ name: productId[0].name, product_id: productId[0].id, move_id: accountMove[0].id });
await start();
onRpc("account.move", "web_save", () => { asyncStep("save")});
await openFormView("account.move", accountMove[0].id, {
arch: `<form js_class="account_move_form">
<sheet>
<notebook>
<page id="invoice_tab" name="invoice_tab" string="Invoice Lines">
<field name="invoice_line_ids" mode="list" widget="product_label_section_and_note_field_o2m">
<list name="journal_items" editable="bottom" string="Journal Items">
<field name="product_id" widget="product_label_section_and_note_field" readonly="0"/>
<field name="name" widget="section_and_note_text" optional="show"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>`,
});
await click(".o_many2one");
await contains("#labelVisibilityButtonId").click()
await insertText("textarea[placeholder='Enter a description']", "testDescription");
await click(".o_form_button_save");
await waitForSteps(["save"]);
const line = pyEnv["account.move.line"].browse([1])[0];
expect(line.name).toBe("testProduct\ntestDescription");
});

View file

@ -1,78 +0,0 @@
odoo.define('account.reconciliation_field_tests', function (require) {
"use strict";
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
QUnit.module('account', {
beforeEach: function () {
this.data = {
'account.move': {
fields: {
payments_widget: {string: "payments_widget data", type: "char"},
outstanding_credits_debits_widget: {string: "outstanding_credits_debits_widget data", type: "char"},
},
records: [{
id: 1,
payments_widget: {"content": [{"digits": [69, 2], "currency": "$", "amount": 555.0, "name": "Customer Payment: INV/2017/0004", "date": "2017-04-25", "position": "before", "ref": "BNK1/2017/0003 (INV/2017/0004)", "payment_id": 22, "move_id": 10, "partial_id": 38, "journal_name": "Bank"}], "outstanding": false, "title": "Less Payment"},
outstanding_credits_debits_widget: {"content": [{"digits": [69, 2], "currency": "$", "amount": 100.0, "journal_name": "INV/2017/0004", "position": "before", "id": 20}], "move_id": 4, "outstanding": true, "title": "Outstanding credits"},
}]
},
};
}
}, function () {
QUnit.module('Reconciliation');
QUnit.test('Reconciliation form field [REQUIRES FOCUS]', async function (assert) {
assert.expect(5);
var form = await createView({
View: FormView,
model: 'account.move',
data: this.data,
arch: '<form>'+
'<field name="outstanding_credits_debits_widget" widget="payment"/>'+
'<field name="payments_widget" widget="payment"/>'+
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'js_remove_outstanding_partial') {
assert.deepEqual(args.args, [10, 38], "should call js_remove_outstanding_partial {warning: required focus}");
return Promise.resolve();
}
if (args.method === 'js_assign_outstanding_line') {
assert.deepEqual(args.args, [4, 20], "should call js_assign_outstanding_line {warning: required focus}");
return Promise.resolve();
}
if (args.method === 'action_open_business_doc') {
assert.deepEqual(args.args, [10], "should call action_open_business_doc {warning: required focus}");
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.o_field_widget[name="payments_widget"]').text().replace(/[\s\n\r]+/g, ' '),
" Paid on 04/25/2017 $ 555.00 ",
"should display payment information");
await testUtils.dom.click(form.$('.o_field_widget[name="outstanding_credits_debits_widget"] .outstanding_credit_assign'));
assert.strictEqual(form.$('.o_field_widget[name="outstanding_credits_debits_widget"]').text().replace(/[\s\n\r]+/g, ' '),
" Outstanding credits Add INV/2017/0004 $ 100.00 ",
"should display outstanding information");
form.$('.o_field_widget[name="payments_widget"] .js_payment_info').focus();
await testUtils.nextTick();
await testUtils.dom.click(form.$('.popover .js_open_payment'));
form.$('.o_field_widget[name="payments_widget"] .js_payment_info').focus();
await testUtils.nextTick();
await testUtils.dom.click(form.$('.popover .js_unreconcile_payment'));
form.destroy();
});
});
});

View file

@ -0,0 +1,13 @@
import { AccountMove } from "./mock_server/mock_models/account_move";
import { AccountMoveLine } from "./mock_server/mock_models/account_move_line";
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { defineModels } from "@web/../tests/web_test_helpers";
export const accountModels = {
AccountMove,
AccountMoveLine,
};
export function defineAccountModels() {
return defineModels({ ...mailModels, ...accountModels });
}

View file

@ -0,0 +1,188 @@
import { describe, expect, test } from "@odoo/hoot";
import { setInputFiles } from "@odoo/hoot-dom";
import {
contains,
defineModels,
fields,
mockService,
models,
mountView,
onRpc,
} from "@web/../tests/web_test_helpers";
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
class Partner extends models.Model {
name = fields.Char();
type = fields.Char();
_records = [
{
id: 7,
name: "first record",
type: "purchase",
},
];
_views = {
form: `
<form>
<widget name="account_file_uploader"/>
<field name="name" required="1"/>
</form>
`,
list: `
<list>
<field name="id"/>
<field name="name"/>
</list>
`,
search: `<search/>`,
};
}
class AccountPaymentTerm extends models.Model {
_name = "account_payment_term";
line_ids = fields.One2many({
string: "Payment Term Lines",
relation: "account_payment_term_line",
});
_records = [
{
id: 1,
line_ids: [1, 2],
},
];
}
class AccountPaymentTermLine extends models.Model {
_name = "account_payment_term_line";
value_amount = fields.Float({ string: "Due" });
_records = [
{
id: 1,
value_amount: 0,
},
{
id: 2,
value_amount: 50,
},
];
}
defineModels([AccountPaymentTerm, AccountPaymentTermLine, Partner]);
defineMailModels();
describe("AccountFileUploader", () => {
test("widget contains context based on the record despite field not in view", async () => {
onRpc("ir.attachment", "create", () => {
expect.step("create ir.attachment");
return [99];
});
onRpc("account.journal", "create_document_from_attachment", ({ kwargs }) => {
expect.step("create_document_from_attachment");
expect(kwargs.context.default_journal_id).toBe(7, {
message: "create documents in correct journal",
});
expect(kwargs.context.default_move_type).toBe("in_invoice", {
message: "create documents with correct move type",
});
return {
name: "Generated Documents",
domain: [],
res_model: "partner",
type: "ir.actions.act_window",
context: {},
views: [
[false, "list"],
[false, "form"],
],
view_mode: "list, form",
};
});
mockService("action", {
doAction(action) {
expect.step("doAction");
expect(action.type).toBe("ir.actions.act_window", {
message: "do action after documents created",
});
},
});
await mountView({
type: "form",
resModel: "partner",
resId: 7,
});
expect(".o_widget_account_file_uploader").toHaveCount(1);
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
await contains(".o_widget_account_file_uploader a").click();
await setInputFiles([file]);
await expect.waitForSteps([
"create ir.attachment",
"create_document_from_attachment",
"doAction",
]);
});
});
describe("AccountMoveUploadKanbanView", () => {
test.tags("desktop");
test("can render AccountMoveUploadKanbanView", async () => {
Partner._views.kanban = `
<kanban js_class="account_documents_kanban">
<templates>
<t t-name="card">
<field name="name"/>
</t>
</templates>
</kanban>
`;
onRpc("res.company", "search_read", () => [{ id: 1, country_code: "US" }]);
await mountView({
type: "kanban",
resModel: "partner",
});
expect(".o_control_panel .o_button_upload_bill:visible").toHaveCount(1);
expect(".o_kanban_record:not(.o_kanban_ghost)").toHaveCount(1);
});
});
describe("PaymentTermsLineWidget", () => {
test("records don't get abandoned after clicking globally or on an exisiting record", async () => {
await mountView({
type: "form",
resModel: "account_payment_term",
resId: 1,
arch: `
<form>
<field name="line_ids" widget="payment_term_line_ids">
<list string="Payment Terms" editable="top">
<field name="value_amount"/>
</list>
</field>
</form>
`,
});
expect(".o_data_row").toHaveCount(2);
// click the add button
await contains(".o_field_x2many_list_row_add > a").click();
// make sure the new record is added
expect(".o_data_row").toHaveCount(3);
// global click
await contains(".o_form_view").click();
// make sure the new record is still there
expect(".o_data_row").toHaveCount(3);
// click the add button again
await contains(".o_field_x2many_list_row_add > a").click();
// make sure the new record is added
expect(".o_data_row").toHaveCount(4);
// click on an existing record
await contains(".o_data_row .o_data_cell").click();
// make sure the new record is still there
expect(".o_data_row").toHaveCount(4);
});
});

View file

@ -1,87 +0,0 @@
/** @odoo-module **/
import { editInput, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let target;
let serverData;
QUnit.module("Widgets", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
type: { string: "Type", type: "char"}
},
records: [
{
id: 7,
display_name: "first record",
type: "purchase",
},
],
onchanges: {},
},
},
views: {
"partner,false,form": `<form>
<widget name="account_file_uploader"/>
<field name="display_name" required="1"/>
</form>`,
"partner,false,list": `<tree>
<field name="id"/>
<field name="display_name"/>
</tree>`,
"partner,false,search": `<search/>`,
},
};
setupViewRegistries();
});
QUnit.module("AccountFileUploader");
QUnit.test("widget contains context based on the record despite field not in view", async function (assert) {
const form = await makeView({
type: "form",
resModel: "partner",
serverData,
resId: 7,
mockRPC(route, args) {
if (args.method === "create") {
assert.deepEqual(args.model, "ir.attachment", "create ir.attachment")
return 99;
}
if (args.method === "create_document_from_attachment" && args.model === "account.journal") {
assert.equal(args.kwargs.context.default_journal_id, 7, "create documents in correct journal");
assert.equal(args.kwargs.context.default_move_type, "in_invoice", "create documents with correct move type");
return {
'name': 'Generated Documents',
'domain': [],
'res_model': 'partner',
'type': 'ir.actions.act_window',
'context': {},
'views': [[false, "list"], [false, "form"]],
'view_mode': 'list, form',
}
}
},
});
patchWithCleanup(form.env.services.action, {
doAction(action) {
assert.equal(action.type, "ir.actions.act_window", "do action after documents created");
}
});
assert.expect(5);
assert.containsOnce(target, '.o_widget_account_file_uploader');
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
await editInput(target, ".o_input_file", file);
});
});

View file

@ -0,0 +1,78 @@
import { expect, test } from "@odoo/hoot";
import { queryFirst } from "@odoo/hoot-dom";
import {
contains,
defineModels,
fieldInput,
fields,
models,
mountView,
} from "@web/../tests/web_test_helpers";
import { defineAccountModels } from "./account_test_helpers";
class Account extends models.Model {
_name = "account.account";
_inherit = [];
code = fields.Char({
string: "Code",
trim: true,
});
placeholder_code = fields.Char();
_records = [
{
id: 1,
placeholder_code: "Placeholder Code",
},
];
_views = {
list: /* xml */ `
<list editable="top" create="1" delete="1">
<field name="placeholder_code" column_invisible="1" />
<field name="code" widget="char_with_placeholder_field" options="{'placeholder_field': 'placeholder_code'}" />
</list>
`,
};
}
defineAccountModels();
defineModels([Account]);
test.tags("desktop");
test("List: placeholder_field shows as text/placeholder", async () => {
await mountView({
type: "list",
resModel: "account.account",
});
const firstCellSelector = "tbody td:not(.o_list_record_selector):first";
expect(`${firstCellSelector} span`).toHaveText("Placeholder Code", {
message: "placeholder_field should be the text value",
});
expect(`${firstCellSelector} span`).toHaveClass("text-muted", {
message: "placeholder_field should be greyed out",
});
await contains(firstCellSelector).click();
expect(queryFirst(firstCellSelector).parentElement).toHaveClass("o_selected_row", {
message: "should be set as edit mode",
});
expect(`${firstCellSelector} input`).toHaveValue("", {
message: "once in edit mode, should have no value in input",
});
expect(`${firstCellSelector} input`).toHaveAttribute("placeholder", "Placeholder Code", {
message: "once in edit mode, should have placeholder_field as placeholder",
});
await fieldInput("code").edit("100001", { confirm: false });
await contains(".o_list_button_save").click();
expect(firstCellSelector).toHaveText("100001", {
message: "entered value should be saved",
});
expect(firstCellSelector).not.toHaveClass("text-muted", {
message: "field should not be greyed out",
});
});

View file

@ -0,0 +1,10 @@
import { models } from "@web/../tests/web_test_helpers";
export class AccountMove extends models.ServerModel {
_name = "account.move";
get_extra_print_items() {
return [];
}
}

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class AccountMoveLine extends models.ServerModel {
_name = "account.move.line";
}

File diff suppressed because it is too large Load diff

View file

@ -1,122 +0,0 @@
/** @odoo-module **/
import {
click,
dragAndDrop,
getNodesTextContent,
getFixture,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
QUnit.module('section_and_note', (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
invoice: {
fields: {
invoice_line_ids: {
string: "Lines",
type: 'one2many',
relation: 'invoice_line',
relation_field: 'invoice_id'
},
},
records: [
{id: 1, invoice_line_ids: [1, 2, 3]},
],
},
invoice_line: {
fields: {
sequence: { string: "sequence", type: "integer", sortable: true },
display_type: {
string: 'Type',
type: 'selection',
selection: [['line_section', "Section"], ['line_note', "Note"]]
},
invoice_id: {
string: "Invoice",
type: 'many2one',
relation: 'invoice'
},
name: {
string: "Name",
type: 'text'
},
price: {
string: "Price",
type: 'monetary',
}
},
records: [
{id: 1, display_type: false, invoice_id: 1, name: 'product\n2 lines', price: 123.45},
{id: 2, display_type: 'line_section', invoice_id: 1, name: 'section'},
{id: 3, display_type: 'line_note', invoice_id: 1, name: 'note'},
]
},
},
};
setupViewRegistries();
});
QUnit.test('correct display of section and note fields', async (assert) => {
assert.expect(9);
await makeView({
type: 'form',
resModel: 'invoice',
serverData,
arch: `
<form>
<field name="invoice_line_ids" widget="section_and_note_one2many">
<tree editable="bottom">
<field name="sequence" widget="handle"/>
<field name="display_type" invisible="1"/>
<field name="name" widget="section_and_note_text"/>
<field name="price"/>
</tree>
</field>
</form>`,
resId: 1,
});
assert.hasClass(target.querySelector('[name="invoice_line_ids"] table'), 'o_section_and_note_list_view');
// product should be displayed correctly
assert.doesNotHaveClass(target.querySelector('tr.o_data_row:nth-child(1'), 'o_is_line_section',
"should not have a section class");
// section should be displayed correctly
const section_line = target.querySelector('tr.o_data_row:nth-child(2)');
const section_cell = section_line.querySelector('td.o_section_and_note_text_cell');
assert.hasClass(section_line, 'o_is_line_section',
"should have a section class");
assert.hasAttrValue(section_cell, 'colspan', '2')
// note should be displayed correctly
const note_line = target.querySelector('tr.o_data_row:nth-child(3)');
const note_cell = note_line.querySelector('td.o_section_and_note_text_cell');
assert.hasClass(note_line, 'o_is_line_note',
"should have a note class");
assert.hasAttrValue(note_cell, 'colspan', '2')
// editing note line should be textarea
await click(note_cell);
assert.containsOnce(note_line, 'td.o_section_and_note_text_cell div[name="name"] textarea',
"note line should be textarea");
// editing section line should be input
await click(section_cell);
assert.containsOnce(section_line, 'td.o_section_and_note_text_cell div[name="name"] input',
"section line should be input");
// Drag and drop the second line in first position
await dragAndDrop("tbody tr:nth-child(2) .o_row_handle", "tbody tr:nth-child(1)");
assert.deepEqual(
getNodesTextContent(target.querySelectorAll(".o_data_cell.o_list_text")),
["section", "product\n2 lines", "note"]
);
});
});

View file

@ -1,42 +0,0 @@
odoo.define('account.dashboard.setup.tour', function (require) {
"use strict";
var core = require('web.core');
var tour = require('web_tour.tour');
var _t = core._t;
tour.register('account_render_report', {
test: true,
url: '/web',
}, [tour.stepUtils.showAppsMenuItem(),
{
id: 'account_menu_click',
trigger: '.o_app[data-menu-xmlid="account.menu_finance"]',
position: 'bottom',
}, {
trigger: '.o_data_row:first .o_data_cell',
extra_trigger: '.breadcrumb',
}, {
trigger: '.o_control_panel button:contains("' + _t('Print') + '")',
}, {
trigger: '.o_control_panel .o-dropdown--menu span:contains("' + _t('Invoices without Payment') + '")',
}, {
trigger: 'iframe .o_report_layout_standard h2',
content: 'Primary color is correct',
run: function () {
if (this.$anchor.css('color') !== "rgb(18, 52, 86)") {
console.error('The primary color should be the one set on the company.');
}
},
}, {
trigger: 'iframe .o_report_layout_standard #informations div strong',
content: 'Secondary color is correct',
run: function () {
if (this.$anchor.css('color') !== "rgb(120, 145, 1)") {
console.error('The secondary color should be the one set on the company.');
}
},
}
]);
});

View file

@ -0,0 +1,35 @@
import { addSectionFromProductCatalog, showProductColumn } from "@account/js/tours/tour_utils";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("test_use_product_catalog_on_invoice", {
steps: () => [
{
content: "Click Catalog Button",
trigger: "button[name=action_add_from_catalog]",
run: "click",
},
{
content: "Add a Product",
trigger: ".o_kanban_record:contains(Test Product)",
run: "click",
},
{
content: "Wait for it",
trigger: ".o_product_added",
},
{
content: "Back to Invoice",
trigger: ".o-kanban-button-back",
run: "click",
},
...showProductColumn(),
{
content: "Ensure product is added",
trigger: ".o_field_product_label_section_and_note_cell:contains(Test Product)",
},
],
});
registry.category("web_tour.tours").add('test_add_section_from_product_catalog_on_invoice', {
steps: () => addSectionFromProductCatalog()
});

View file

@ -0,0 +1,28 @@
import { registry } from '@web/core/registry';
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add("deductible_amount_column", {
url: "/odoo/vendor-bills/new",
steps: () => [
{
content: "Add item",
trigger: "div[name='invoice_line_ids'] .o_field_x2many_list_row_add a:contains('Add a line')",
run: "click",
},
{
content: "Edit name",
trigger: ".o_field_widget[name='name'] .o_input",
run: "edit Laptop"
},
{
content: "Edit deductible amount",
trigger: ".o_field_widget[name='deductible_amount'] > .o_input",
run: "edit 80"
},
{
content: "Set Bill Date",
trigger: "input[data-field=invoice_date]",
run: "edit 2025-12-01",
},
...stepUtils.saveForm(),
]})

View file

@ -1,128 +1,147 @@
/** @odoo-module alias=account.tax.group.tour.tests */
"use strict";
import { accountTourSteps } from "@account/js/tours/account";
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import tour from 'web_tour.tour';
tour.register('account_tax_group', {
test: true,
url: "/web",
}, [tour.stepUtils.showAppsMenuItem(),
{
id: 'account_menu_click',
content: "Go to Invoicing",
trigger: '.o_app[data-menu-xmlid="account.menu_finance"]',
},
registry.category("web_tour.tours").add('account_tax_group', {
url: "/odoo",
steps: () => [
...accountTourSteps.goToAccountMenu("Go to Invoicing"),
{
content: "Go to Vendors",
trigger: 'span:contains("Vendors")',
run: "click",
},
{
content: "Go to Bills",
trigger: 'a:contains("Bills")',
run: "click",
},
{
trigger: ".o_breadcrumb .text-truncate:contains(Bills)",
},
{
extra_trigger: '.breadcrumb:contains("Bills")',
content: "Create new bill",
trigger: '.o_list_button_add',
trigger: '.o_control_panel_main_buttons .o_list_button_add',
run: "click",
},
// Set a vendor
{
content: "Add vendor",
trigger: 'div.o_field_widget.o_field_res_partner_many2one[name="partner_id"] div input',
run: 'text Account Tax Group Partner',
run: "edit Account Tax Group Partner",
},
{
content: "Valid vendor",
trigger: '.ui-menu-item a:contains("Account Tax Group Partner")',
run: "click",
},
// Show product column
{
content: "Open line fields list",
trigger: ".o_optional_columns_dropdown_toggle",
run: "click"
},
{
content: "Show product column",
trigger: '.o-dropdown-item input[name="product_id"]',
run: "click"
},
{
content: "Close line fields list",
trigger: ".o_optional_columns_dropdown_toggle",
run: "click"
},
// Add First product
{
content: "Add items",
trigger: 'div[name="invoice_line_ids"] .o_field_x2many_list_row_add a:contains("Add a line")',
run: "click",
},
{
content: "Select input",
trigger: 'div[name="invoice_line_ids"] .o_selected_row .o_list_many2one[name="product_id"] input',
},
{
content: "Type item",
trigger: 'div[name="invoice_line_ids"] .o_selected_row .o_list_many2one[name="product_id"] input',
run: "text Account Tax Group Product",
run: "edit Account Tax Group Product",
},
{
content: "Valid item",
trigger: '.ui-menu-item-wrapper:contains("Account Tax Group Product")',
run: "click",
},
{
content: "Set Bill Date",
trigger: "input[data-field=invoice_date]",
run: "edit 2025-12-01",
},
// Save account.move
{
content: "Save the account move",
trigger: '.o_form_button_save',
},
...tour.stepUtils.saveForm(),
...stepUtils.saveForm(),
// Edit tax group amount
{
content: "Edit tax group amount",
trigger: '.o_tax_group_edit',
run: "click",
},
{
content: "Modify the input value",
trigger: '.o_tax_group_edit_input input',
run: function (actions) {
$('.o_tax_group_edit_input input').val(200);
$('.o_tax_group_edit_input input').select();
$('.o_tax_group_edit_input input').blur();
run() {
this.anchor.value = 200;
this.anchor.select();
this.anchor.blur();
},
},
// Check new value for total (with modified tax_group_amount).
{
content: "Valid total amount",
trigger: 'span[name="amount_total"]:contains("800")',
run: "click",
},
// Modify the quantity of the object
{
content: "Select item quantity",
trigger: 'div[name="invoice_line_ids"] tbody tr.o_data_row .o_list_number[name="quantity"]',
run: "click",
},
{
content: "Change item quantity",
trigger: 'div[name="invoice_line_ids"] tbody tr.o_data_row .o_list_number[name="quantity"] input',
run: 'text 2',
run: "edit 2",
},
{
content: "Valid the new value",
trigger: 'div[name="invoice_line_ids"] tbody tr.o_data_row .o_list_number[name="quantity"] input',
run: function (actions) {
let keydownEvent = jQuery.Event('keydown');
keydownEvent.which = 13;
this.$anchor.trigger(keydownEvent);
},
run: "press Enter",
},
// Save form
{
content: "Save the account move",
trigger: '.o_form_button_save',
},
...tour.stepUtils.saveForm(),
// Check new tax group value
{
content: "Check new value of tax group",
trigger: '.o_tax_group_amount_value:contains("120")',
run: "click",
},
// Save form
...stepUtils.saveForm(),
// Check new tax group value
{
content: "Check new value of tax group",
trigger: '.o_tax_group_amount_value:contains("120")',
run: "click",
},
{
content: "Edit tax value",
trigger: '.o_tax_group_edit_input input',
run: 'text 2'
run: "edit 2 && click body",
},
{
content: "Check new value of total",
trigger: '.oe_subtotal_footer_separator:contains("1,202")',
run: "click",
},
{
content: "Discard changes",
trigger: '.o_form_button_cancel',
run: "click",
},
{
content: "Check tax value is reset",
trigger: '.o_tax_group_amount_value:contains("120")',
},
]);
]});

View file

@ -0,0 +1,16 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add('tests_shared_js_python', {
url: "/account/init_tests_shared_js_python",
steps: () => [
{
content: "Click",
trigger: 'button',
run: "click",
},
{
content: "Wait",
trigger: 'button.text-success',
timeout: 3000,
},
]});

View file

@ -0,0 +1,138 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import {
contains,
defineModels,
fields,
mockService,
models,
mountView,
onRpc,
} from "@web/../tests/web_test_helpers";
class AccountMove extends models.Model {
_name = "account.move";
name = fields.Char();
duplicated_ref_ids = fields.Many2many({
string: "Duplicated Bills",
relation: "account.move",
});
ref = fields.Char({ string: "Bill Reference" });
_records = [
// for the sake of mocking data, we don't care about the consistency of duplicated refs across records
{ id: 1, display_name: "Bill 1", duplicated_ref_ids: [2, 3], ref: "b1" },
{ id: 2, display_name: "Bill 2", duplicated_ref_ids: [1], ref: "b2" },
{ id: 3, display_name: "Bill 3", duplicated_ref_ids: [1], ref: "b3" },
{ id: 4, display_name: "Bill 4", duplicated_ref_ids: [1, 2, 3], ref: "b4" },
{ id: 5, display_name: "Bill 5", duplicated_ref_ids: [], ref: "b5" },
{ id: 6, display_name: "Bill 6", duplicated_ref_ids: [1, 2, 3, 4, 5], ref: "b6" },
];
_views = {
form: `
<form>
<field name="display_name"/>
<field name="ref"/>
<field name="duplicated_ref_ids" widget="x2many_buttons"/>
</form>
`,
};
}
defineModels([AccountMove]);
defineMailModels();
test("component rendering: less than 3 records on field", async () => {
expect.assertions(2);
await mountView({
resModel: "account.move",
resId: 1,
type: "form",
});
expect(".o_field_x2many_buttons").toHaveCount(1);
expect(".o_field_x2many_buttons button").toHaveCount(2);
});
test("component rendering: exactly 3 records on field", async () => {
expect.assertions(2);
await mountView({
resModel: "account.move",
resId: 4,
type: "form",
});
expect(".o_field_x2many_buttons").toHaveCount(1);
expect(".o_field_x2many_buttons button").toHaveCount(3);
});
test("component rendering: more than 3 records on field", async () => {
expect.assertions(3);
await mountView({
resModel: "account.move",
resId: 6,
type: "form",
});
expect(".o_field_x2many_buttons").toHaveCount(1);
expect(".o_field_x2many_buttons button").toHaveCount(4);
expect(".o_field_x2many_buttons button:eq(3)").toHaveText("... (View all)");
});
test("edit record and check if edits get discarded when click on one of the buttons and redirects to proper record", async () => {
onRpc("account.move", "action_open_business_doc", ({ args }) => {
expect.step("action_open_business_doc");
expect(args.length).toBe(1);
expect(args[0]).toBe(2);
return {
res_model: "account.move",
res_id: 2,
type: "ir.actions.act_window",
views: [[false, "form"]],
};
});
await mountView({
resModel: "account.move",
resId: 1,
type: "form",
});
await contains("[name='ref'] input").edit("new ref");
expect("[name='ref'] input").toHaveValue("new ref");
await contains(".o_field_x2many_buttons button").click();
expect("[name='ref'] input").toHaveValue("b1");
expect.verifySteps(["action_open_business_doc"]);
});
// test if clicking on last button redirects to records in list view
test("redirect to list view and discards edits when clicking on last button with more than 3 records on field", async () => {
expect.assertions(3);
mockService("action", {
doAction(action) {
expect(action).toEqual({
domain: [["id", "in", [1, 2, 3, 4, 5]]],
name: "Duplicated Bills",
res_model: "account.move",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
context: {
list_view_ref: "account.view_duplicated_moves_tree_js",
},
});
},
});
await mountView({
resModel: "account.move",
resId: 6,
type: "form",
});
await contains("[name='ref'] input").edit("new ref");
expect("[name='ref'] input").toHaveValue("new ref");
await contains(".o_field_x2many_buttons button:eq(3)").click();
expect("[name='ref'] input").toHaveValue("b6");
});