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>
This commit is contained in:
Marc
2026-03-31 23:09:12 +02:00
parent 0ed29fe2fd
commit 8741282322
2933 changed files with 280644 additions and 264 deletions

View File

@@ -0,0 +1,68 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { contains, defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
class Quant extends models.Model {
quantity = fields.Float();
inventory_quantity = fields.Float({
string: "Counted quantity",
onChange: (quant) => {
quant.inventory_diff_quantity = quant.inventory_quantity - quant.quantity;
},
});
inventory_quantity_set = fields.Boolean({
string: "Inventory quantity set",
});
inventory_diff_quantity = fields.Float({ string: "Difference" });
_records = [{ id: 1, quantity: 50 }];
}
defineModels([Quant]);
defineMailModels();
test("Test changing the inventory quantity with the widget", async function () {
await mountView({
type: "list",
resModel: "quant",
arch: `<list editable="bottom">
<field name="quantity"/>
<field name="inventory_quantity" widget="counted_quantity_widget"/>
<field name="inventory_quantity_set"/>
<field name="inventory_diff_quantity"/>
</list>
`,
});
await contains("td.o_counted_quantity_widget_cell").click();
await contains("td.o_counted_quantity_widget_cell input").edit("23");
await contains("td[name=inventory_diff_quantity]").click();
expect("td[name=inventory_diff_quantity] div input").toHaveValue(-27);
expect("td[name=inventory_quantity_set] div input").toBeChecked();
await contains("td.o_counted_quantity_widget_cell").click();
await contains("td.o_counted_quantity_widget_cell input").edit("40.5");
await contains("td[name=inventory_diff_quantity]").click();
expect("td[name=inventory_diff_quantity] div input").toHaveValue(-9.5);
});
test("Test setting the inventory quantity to its default value of 0", async function () {
await mountView({
type: "list",
resModel: "quant",
arch: `<list editable="bottom">
<field name="quantity"/>
<field name="inventory_quantity" widget="counted_quantity_widget"/>
<field name="inventory_quantity_set"/>
<field name="inventory_diff_quantity"/>
</list>
`,
});
await contains("td.o_counted_quantity_widget_cell").click();
await contains("td.o_counted_quantity_widget_cell input").edit("0");
await contains("td[name=inventory_diff_quantity]").click();
expect("td[name=inventory_diff_quantity] div input").toHaveValue(-50);
});

View File

@@ -0,0 +1,187 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { animationFrame, queryOne } from "@odoo/hoot-dom";
import {
contains,
defineModels,
fields,
MockServer,
models,
mountView,
onRpc,
} from "@web/../tests/web_test_helpers";
const { DateTime } = luxon;
const arch = `
<list editable="top" js_class="inventory_report_list">
<field name="name"/>
<field name="age"/>
<field name="job"/>
<field name="create_date" invisible="1"/>
<field name="write_date" invisible="1"/>
</list>
`;
const setup_date = "2022-01-03 08:03:44";
onRpc("person", "web_save", ({ args }) => {
// simulate 'stock.quant' create function which can return existing record
const values = args[1];
const existingRecord = MockServer.env.person.find((p) => p.name === values.name);
if (existingRecord) {
values.create_date = existingRecord.create_date;
values.write_date = DateTime.now().toSQL();
return [Object.assign(existingRecord, values)];
}
});
class Person extends models.Model {
name = fields.Char();
age = fields.Integer();
job = fields.Char({ string: "Profession" });
create_date = fields.Datetime({ string: "Created on" });
write_date = fields.Datetime({ string: "Last Updated on" });
_records = [
{
id: 1,
name: "Daniel Fortesque",
age: 32,
job: "Soldier",
create_date: setup_date,
write_date: setup_date,
},
{
id: 2,
name: "Samuel Oak",
age: 64,
job: "Professor",
create_date: setup_date,
write_date: setup_date,
},
{
id: 3,
name: "Leto II Atreides",
age: 128,
job: "Emperor",
create_date: setup_date,
write_date: setup_date,
},
];
}
defineModels([Person]);
defineMailModels();
test("Create new record correctly", async function () {
await mountView({
type: "list",
resModel: "person",
arch,
context: {
inventory_mode: true,
},
});
// Check we have initially 3 records
expect(".o_data_row").toHaveCount(3);
// Create a new line...
await contains(".o_control_panel_main_buttons .o_list_button_add").click();
await contains("[name=name] input").edit("Bilou", { confirm: false });
await contains("[name=age] input").edit("24", { confirm: false });
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
// Check new record is in the list
expect(".o_data_row").toHaveCount(4);
});
test("Don't duplicate record", async function () {
await mountView({
type: "list",
resModel: "person",
arch,
context: {
inventory_mode: true,
},
});
// Check we have initially 3 records
expect(".o_data_row").toHaveCount(3);
// Create a new line for an existing record...
await contains(".o_control_panel_main_buttons .o_list_button_add").click();
await contains("[name=name] input").edit("Leto II Atreides", { confirm: false });
await contains("[name=age] input").edit("72", { confirm: false });
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
expect(".o_data_row").toHaveCount(3, { message: "should still have 3 records" });
expect(".o_data_row:eq(2) .o_list_number").toHaveText("72", {
message: "The age field must be updated",
});
await animationFrame();
expect(".o_notification").toHaveCount(1);
expect(".o_notification .o_notification_body").toHaveText(
"This record already exists. You tried to create a record that already exists. The existing record was modified instead."
);
});
test("Work in grouped list", async function () {
await mountView({
type: "list",
resModel: "person",
arch,
context: {
inventory_mode: true,
},
groupBy: ["job"], // Groups are Emperor, Professor, Soldier
});
// Open 'Professor' group
await contains(".o_group_header:eq(1)").click();
// Check we have only 1 record...
expect(".o_data_row").toHaveCount(1);
// Create a new record...
await contains(".o_group_field_row_add a").click();
await contains("[name=name] input").edit("Del Tutorial", { confirm: false });
await contains("[name=age] input").edit("32", { confirm: false });
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
// Check we have 2 records...
expect(".o_data_row").toHaveCount(2);
// Create an existing record...
await contains(".o_group_field_row_add a").click();
await contains("[name=name] input").edit("Samuel Oak", { confirm: false });
await contains("[name=age] input").edit("55", { confirm: false });
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
// Check we still have 2 records...
expect(".o_data_row").toHaveCount(2);
// Create an existing but not displayed record...
await contains(".o_group_field_row_add a").click();
await contains("[name=name] input").edit("Daniel Fortesque", { confirm: false });
await contains("[name=age] input").edit("55", { confirm: false });
await contains("[name=job] input").edit("Soldier", { confirm: false }); // let it in its original group
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
// Check we have 3 records...
expect(".o_data_row").toHaveCount(3);
// Opens 'Soldier' group
await contains(".o_group_header:eq(2)").click();
// Check 'original' record has been updated...
// : Daniel Fortesque is in record 0 for group Soldier and in record 3 for group Professor
expect('.o_data_row:eq(0) [name="age"]').toHaveText("55");
// Edit the freshly created record...
await contains(".o_data_row:eq(3) .o_field_cell").click();
await contains("[name=age] input").edit("66");
// Check both records have been updated...
expect(queryOne('.o_data_row:eq(0) [name="age"]').textContent).toBe(
queryOne('.o_data_row:eq(3) [name="age"]').textContent
);
});

View File

@@ -0,0 +1,35 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { contains, defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
class Partner extends models.Model {
json_data = fields.Char();
_records = [
{
id: 1,
json_data:
'{"color": "text-danger", "msg": "var that = self // why not?", "title": "JS Master"}',
},
];
}
defineModels([Partner]);
defineMailModels();
test("Test creation/usage form popover widget", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="json_data" widget="popover_widget"/>
</form>`,
resId: 1,
});
expect(".popover").toHaveCount(0);
expect(".fa-info-circle.text-danger").toHaveCount(1);
await contains(".fa-info-circle.text-danger").click();
expect(".popover").toHaveCount(1);
expect(".popover").toHaveText("JS Master\nvar that = self // why not?");
});

View File

@@ -0,0 +1,24 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { defineActions, getService, mountWithCleanup, onRpc } from "@web/../tests/web_test_helpers";
import { WebClient } from "@web/webclient/webclient";
defineActions([
{
id: 42,
name: "Stock report",
tag: "stock_report_generic",
type: "ir.actions.client",
context: {},
params: {},
},
]);
defineMailModels();
test("Rendering with no lines", async function () {
onRpc("get_main_lines", () => []);
await mountWithCleanup(WebClient);
await getService("action").doAction(42);
expect(".o_stock_reports_page").toHaveText("No operation made on this lot.");
});

View File

@@ -0,0 +1,120 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("test_basic_stock_flow_with_minimal_access_rights", {
steps: () => [
{
trigger: ".o_menuitem[href='/odoo/inventory']",
run: "click",
},
{
trigger: "button[data-menu-xmlid='stock.menu_stock_warehouse_mgmt']",
run: "click",
},
{
trigger: ".o-dropdown-item[data-menu-xmlid='stock.in_picking']",
run: "click",
},
{
content: "check that at least one picking is present in the view",
trigger: ".o_stock_list_view_view .o_data_row",
},
{
trigger: ".o_list_button_add",
run: "click",
},
{
trigger: ".o_input[id=partner_id_0]",
run: "edit Test Partner",
},
{
trigger: ".dropdown-item:contains('Test Partner')",
run: "click",
},
{
trigger: ".o_field_x2many_list_row_add > a",
run: "click",
},
{
trigger: ".o_data_row .o_input",
run: "edit Test Product",
},
{
trigger: ".dropdown-item:contains('Test Product')",
run: "click",
},
{
trigger: ".o_data_cell[name=product_uom_qty]",
run: "click",
},
{
trigger: ".o_data_cell[name=product_uom_qty] .o_input",
run: "edit 1",
},
{
trigger: "button[name=action_confirm]",
run: "click",
},
{
trigger: "button[name=button_validate]",
run: "click",
},
{
trigger: ".o_arrow_button_current:contains(Done)",
},
{
trigger: "button[data-menu-xmlid='stock.menu_stock_warehouse_mgmt']",
run: "click",
},
{
trigger: ".o-dropdown-item[data-menu-xmlid='stock.out_picking']",
run: "click",
},
{
content: "check that at least one picking is present in the view",
trigger: ".o_stock_list_view_view .o_data_row",
},
{
trigger: "button:contains(New)",
run: "click",
},
{
trigger: ".o_input[id=partner_id_0]",
run: "edit Test Partner",
},
{
trigger: ".dropdown-item:contains('Test Partner')",
run: "click",
},
{
trigger: ".o_field_x2many_list_row_add > a",
run: "click",
},
{
trigger: ".o_data_row .o_input",
run: "edit Test Product",
},
{
trigger: ".dropdown-item:contains('Test Product')",
run: "click",
},
{
trigger: ".o_data_cell[name=product_uom_qty]",
run: "click",
},
{
trigger: ".o_data_cell[name=product_uom_qty] .o_input",
run: "edit 1",
},
{
trigger: "button[name=action_confirm]",
run: "click",
},
{
trigger: "button[name=button_validate]",
run: "click",
},
{
trigger: ".o_arrow_button_current:contains(Done)",
},
],
});

View File

@@ -0,0 +1,526 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add('test_generate_serial_1', { steps: () => [
{
trigger: '.o_field_x2many_list_row_add > a',
run: "click",
},
{
trigger: ".o_field_widget[name=product_id] input",
run: "edit Serial",
},
{
trigger: ".ui-menu-item > a:contains('Product Serial')",
run: "click",
},
{
trigger: ".btn-primary[name=action_confirm]",
run: "click",
},
{
trigger: "button:contains('Details')",
run: "click",
},
{
trigger: "h4:contains('Detailed Operations')",
run: "click",
},
{
trigger: '.o_widget_generate_serials > button',
run: "click",
},
{
trigger: ".modal h4:contains('Generate Serial numbers')",
run: "click",
},
{
trigger: ".modal div[name=next_serial] input",
run: "edit serial_n_1",
},
{
trigger: ".modal div[name=next_serial_count] input",
run: "edit 5 && click body",
},
{
trigger: ".modal .btn-primary:contains('Generate')",
run: "click",
},
{
trigger: "span[data-tooltip=Quantity]:contains('5')",
run: () => {
const nbLines = document.querySelectorAll(".o_field_cell[name=lot_name]").length;
if (nbLines !== 5){
console.error("wrong number of move lines generated. " + nbLines + " instead of 5");
}
},
},
{
trigger: ".modal button:contains(save)",
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
trigger: ".o_optional_columns_dropdown_toggle",
run: "click",
},
{
trigger: 'input[name="picked"]',
content: 'Check the picked field to display the column on the list view.',
run: function (actions) {
if (!this.anchor.checked) {
actions.click();
}
},
},
{
trigger: ".o_data_cell[name=picked]",
run: "click",
},
{
trigger: ".o_field_widget[name=picked] input",
run: function (actions) {
if (!this.anchor.checked) {
actions.click();
}
}
},
{
trigger: ".btn-primary[name=button_validate]",
run: "click",
},
{
trigger: ".o_control_panel_actions button:contains('Traceability')",
},
]});
registry.category("web_tour.tours").add('test_generate_serial_2', { steps: () => [
{
trigger: '.o_field_x2many_list_row_add > a',
run: "click",
},
{
trigger: ".o_field_widget[name=product_id] input",
run: "edit Lot",
},
{
trigger: ".ui-menu-item > a:contains('Product Lot 1')",
run: "click",
},
{
trigger: ".o_field_widget[name=product_uom_qty] input",
run: "edit 100",
},
{
trigger: ".btn-primary[name=action_confirm]",
run: "click",
},
{
trigger: "button:contains('Details')",
run: "click",
},
{
trigger: ".modal h4:contains('Detailed Operations')",
run: "click",
},
// We generate lots for a first batch of 50 products
{
trigger: ".modal .o_widget_generate_serials > button",
run: "click",
},
{
trigger: ".modal h4:contains('Generate Lot numbers')",
run: "click",
},
{
trigger: ".modal div[name=next_serial] input",
run: "edit lot_n_1_1",
},
{
trigger: ".modal div[name=next_serial_count] input",
run() {
//input type number not supported by tour helpers.
this.anchor.value = "7.5";
}
},
{
trigger: ".modal div[name=total_received] input",
run: "edit 50",
},
{
trigger: ".modal .modal-footer button.btn-primary:contains(Generate)",
run: "click",
},
{
trigger: ".modal span[data-tooltip=Quantity]:contains(50)",
run: () => {
const nbLines = document.querySelectorAll(".o_field_cell[name=lot_name]").length;
if (nbLines !== 7){
console.error("wrong number of move lines generated. " + nbLines + " instead of 7");
}
},
},
// We generate lots for the last 50 products
{
trigger: ".modal .o_widget_generate_serials > button",
run: "click",
},
{
trigger: ".modal h4:contains('Generate Lot numbers')",
},
{
trigger: ".modal div[name=next_serial] input",
run: "edit lot_n_2_1",
},
{
trigger: ".modal div[name=next_serial_count] input",
run: "edit 13",
},
{
trigger: ".modal div[name=total_received] input",
run: "edit 50",
},
{
trigger: ".modal div[name=keep_lines] input",
run: "check",
},
{
trigger: ".modal .modal-footer button.btn-primary:contains(Generate)",
run: "click",
},
{
trigger: ".modal span[data-tooltip=Quantity]:contains(100)",
run: () => {
const nbLines = document.querySelectorAll(".o_field_cell[name=lot_name]").length;
if (nbLines !== 11){
console.error("wrong number of move lines generated. " + nbLines + " instead of 11");
}
},
},
{
trigger: ".modal .o_form_button_save",
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
trigger: ".o_optional_columns_dropdown_toggle",
run: "click",
},
{
trigger: "input[name='picked']",
content: "Check the picked field to display the column on the list view.",
run: function (actions) {
if (!this.anchor.checked) {
actions.click();
}
},
},
{
trigger: ".o_data_cell[name=picked]",
run: "click",
},
{
trigger: ".o_field_widget[name=picked] input",
run: function (actions) {
if (!this.anchor.checked) {
actions.click();
}
}
},
{
trigger: ".btn-primary[name=button_validate]",
run: "click",
},
{
trigger: ".o_control_panel_actions button:contains('Traceability')",
},
]});
registry.category('web_tour.tours').add('test_inventory_adjustment_apply_all', { steps: () => [
{
trigger: '.o_list_button_add',
run: "click",
},
{
trigger: 'div[name=product_id] input',
run: "edit Product 1",
},
{
trigger: '.ui-menu-item > a:contains("Product 1")',
run: "click",
},
{
trigger: 'div[name=inventory_quantity] input',
run: "edit 123",
},
// Unfocus to show the "New" button again
{
trigger: '.o_searchview_input_container',
run: "click",
},
{
trigger: '.o_list_button_add',
run: "click",
},
{
trigger: 'div[name=product_id] input',
run: "edit Product 2",
},
{
trigger: '.ui-menu-item > a:contains("Product 2")',
run: "click",
},
{
trigger: 'div[name=inventory_quantity] input',
run: "edit 456",
},
{
trigger: 'button[name=action_apply_all]',
run: "click",
},
{
trigger: '.modal-content button[name=action_apply]',
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
content: "Check that all quants were applied.",
trigger: "body:not(:has(button[name=action_apply_inventory]))",
},
],
});
registry.category("web_tour.tours").add("test_add_new_line_in_detailled_op", {
steps: () => [
{
trigger: ".o_list_view.o_field_x2many .o_data_row button[name='action_show_details']",
run: "click",
},
{
trigger: ".modal-content",
},
{
trigger: ".modal-content .o_field_x2many_list_row_add > a",
run: "click",
},
{
content: "Pick LOT001 to create a move line with a quantity of 0.00",
trigger: ".o_data_row .o_data_cell[name=lot_id]:contains(LOT001)",
run: "click",
},
{
content: "check that the move contains three lines",
trigger:
".modal-content:has(.modal-header .modal-title:contains(Detailed Operations)) .o_data_row:nth-child(3)",
},
{
content: "Check that the first line is associated with LOT001 for a quantity of 0.00",
trigger:
".modal-content .o_data_row:has(.o_field_pick_from input:value(WH/Stock - LOT001)):has(.o_field_float[name=quantity] input:value(0.00))",
},
{
trigger: ".modal-content .o_field_x2many_list_row_add > a",
run: "click",
},
{
content: "LOT001 should not appear as it is not available",
trigger: ".modal-header .modal-title:contains(Add line: Product Lot)",
run: () => {
const lines = document.querySelectorAll(".o_data_row .o_data_cell[name=lot_id]");
if (lines.length !== 2) {
console.error(
"Wrong number of available quants: " + lines.length + " instead of 2."
);
}
const lineLOT001 = Array.from(lines).filter((line) =>
line.textContent.includes("LOT001")
);
if (lineLOT001.length) {
console.error("LOT001 shoudld not be displayed as unavailable.");
}
},
},
{
content: "Cancel the move line creation",
trigger: ".modal-header:has(.modal-title:contains(Add line: Product Lot)) .btn-close",
run: "click",
},
{
content: "Remove the newly created line",
trigger:
".modal-content .o_data_row:has(.o_field_pick_from input:value(WH/Stock - LOT001)):has(.o_field_float[name=quantity] input:value(0.00)) .o_list_record_remove",
run: "click",
},
{
content: "check that the move contains two lines",
trigger:
".modal-content:has(.modal-header .modal-title:contains(Detailed Operations)):not(:has(.o_data_row:nth-child(3)))",
},
{
content: "Check that the first line is associated with LOT001",
trigger:
".modal-content .o_data_row:nth-child(1):has(.o_field_pick_from:contains(WH/Stock - LOT001))",
},
{
content: "Check that the second line is associated with LOT002",
trigger:
".modal-content .o_data_row:nth-child(2):has(.o_field_pick_from:contains(WH/Stock - LOT002))",
},
{
content: "Modify the quant associated to the second line to fully use LOT003",
trigger: ".modal-content .o_data_row:nth-child(2) .o_field_pick_from",
run: "click",
},
{
trigger: ".modal-content .o_data_row:nth-child(2) .o_field_pick_from input",
run: "edit LOT003",
},
{
trigger: ".dropdown-item:contains(LOT003)",
run: "click",
},
{
content: "Modify the quantity of the first line from 10 to 8",
trigger: ".modal-content .o_data_row:nth-child(1) .o_data_cell[name=quantity]",
run: "click",
},
{
trigger: ".modal-content .o_data_row:nth-child(1) .o_field_widget[name=quantity] input",
run: "edit 8",
},
{
content: "Click on the header to update the total amount",
trigger: ".modal-header .modal-title:contains(Detailed Operations)",
run: "click",
},
{
trigger: ".modal-content .o_list_number:contains(18.00)",
},
{
trigger: ".modal-content .o_field_x2many_list_row_add > a",
run: "click",
},
{
content: "LOT003 should not appear as it is not available",
trigger: ".modal-header .modal-title:contains(Add line: Product Lot)",
run: () => {
const lines = document.querySelectorAll(".o_data_row .o_data_cell[name=lot_id]");
if (lines.length !== 2) {
console.error(
"Wrong number of available quants: " + lines.length + " instead of 2."
);
}
const lineLOT003 = Array.from(lines).filter((line) =>
line.textContent.includes("LOT003")
);
if (lineLOT003.length) {
console.error("LOT003 shoudld not be displayed as unavailable.");
}
},
},
{
content: "Pick LOT001 to create a move line with a quantity of 2.00",
trigger: ".o_data_row .o_data_cell[name=lot_id]:contains(LOT001)",
run: "click",
},
{
trigger: ".modal-content .o_list_number:contains(20.00)",
},
{
content: "Check that 2 units of LOT001 were added",
trigger:
".o_data_row:has(.o_field_pick_from input:value(WH/Stock - LOT001)) .o_field_widget[name=quantity] input:value(2.00)",
},
{
content: "Check that the third line is associated with LOT003",
trigger:
".modal-content .o_data_row:nth-child(3) .o_field_pick_from:contains(WH/Stock - LOT003)",
},
{
content: "Modify the quant associated to the third line to use LOT002",
trigger: ".modal-content .o_data_row:nth-child(3) .o_field_pick_from",
run: "click",
},
{
trigger: ".modal-content .o_data_row:nth-child(3) .o_field_pick_from input",
run: "edit LOT002",
},
{
trigger: ".dropdown-item:contains(LOT002)",
run: "click",
},
{
trigger: ".modal-header .modal-title:contains(Detailed Operations)",
run: "click",
},
{
trigger: ".modal-content .o_data_row:nth-child(3) .o_field_pick_from:contains(LOT002)",
},
{
content: "Modify the quantity of the first line from 10 to 15 to change the demand",
trigger: ".modal-content .o_data_row:nth-child(3) .o_data_cell[name=quantity]",
run: "click",
},
{
trigger: ".modal-content .o_data_row:nth-child(3) .o_field_widget[name=quantity] input",
run: "edit 15",
},
{
content: "Remove the LOT001 line with a quantity of 8.00",
trigger:
".o_data_row:has(.o_data_cell[name=quantity]:contains(8.00)) .o_list_record_remove",
run: "click",
},
{
trigger: ".modal-content .o_list_number:contains(17.00)",
},
{
trigger: ".modal-content .o_field_x2many_list_row_add > a",
run: "click",
},
{
content: "LOT002 should not appear as it is not available",
trigger: ".modal-header .modal-title:contains(Add line: Product Lot)",
run: () => {
const lines = document.querySelectorAll(".o_data_row .o_data_cell[name=lot_id]");
if (lines.length !== 2) {
console.error(
"Wrong number of available quants: " + lines.length + " instead of 2."
);
}
const lineLOT002 = Array.from(lines).filter((line) =>
line.textContent.includes("LOT002")
);
if (lineLOT002.length) {
console.error("LOT002 shoudld not be displayed as unavailable.");
}
},
},
{
content: "Pick LOT001 to create move line to fullfill the demand of 3",
trigger: ".o_data_row .o_data_cell[name=lot_id]:contains(LOT001)",
run: "click",
},
{
trigger: ".modal-content .o_list_number:contains(20.00)",
},
{
content: "Check that 3 units of LOT001 were added",
trigger:
".modal-content .o_data_row:has(.o_field_pick_from input:value(WH/Stock - LOT001)):has(.o_field_float[name=quantity] input:value(3.00))",
},
{
trigger: ".modal-content .o_form_button_save",
run: "click",
},
{
trigger: ".o_list_view.o_field_x2many .o_data_row button[name='action_show_details']",
run: "click",
},
],
});

View File

@@ -0,0 +1,137 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("test_stock_route_diagram_report", {
steps: () => [
{
trigger: ".o_breadcrumb",
},
{
trigger: '.o_kanban_record',
run: "click",
},
{
trigger: '.nav-item > a:contains("Inventory")',
run: "click",
},
{
trigger: '.btn[id="stock.view_diagram_button"]',
run: "click",
},
{
trigger: ':iframe .o_report_stock_rule',
},
],
});
registry.category("web_tour.tours").add("test_context_from_warehouse_filter", {
steps: () => [
// Add "foo" to the warehouse context key
{
trigger: ".o_searchview_input",
run: "click",
},
{
trigger: ".o_searchview_input",
run: "edit foo",
},
{
trigger: ".o-dropdown-item:contains(Warehouse):contains(foo)",
run: "click",
},
// Add warehouse A's id to the warehouse context key
{
trigger: ".o_searchview_input",
run: "click",
},
{
trigger: ".o_searchview_input",
run: "edit warehouse",
},
{
trigger: ".o-dropdown-item:contains(Search Warehouse for:) a.o_expand > i",
run: "click",
},
{
trigger: ".o-dropdown-item.o_indent:contains(Warehouse A) a",
run: "click",
},
// Add warehouse B's id to the warehouse context key
{
trigger: ".o_searchview_input",
run: "edit warehouse",
},
{
trigger: ".o-dropdown-item:contains(Search Warehouse for:) a.o_expand > i",
run: "click",
},
{
trigger: ".o-dropdown-item.o_indent:contains(Warehouse B) a",
run: "click",
},
{
content: "Go to product page",
trigger: ".o_kanban_record:has(span:contains(Lovely Product))",
run: "click",
},
{
trigger: ".o_form_view",
run: () => {
if (!document.querySelector("button[name=action_product_tmpl_forecast_report]")) {
const panelButtons = document.querySelectorAll(
".o_control_panel_actions button"
);
const moreButton = Array.from(panelButtons).find(
(button) => button.textContent.trim() == "More"
);
moreButton.click();
}
},
},
{
trigger: "button[name=action_product_tmpl_forecast_report]",
run: "click",
},
{
trigger: ".o_graph_view",
},
],
});
registry.category("web_tour.tours").add("test_forecast_replenishment", {
steps: () => [
{
trigger: ".o_kanban_record:contains(Lovely product)",
run: "click",
},
{
trigger: "button[name=action_product_tmpl_forecast_report]",
run: "click",
},
{
trigger: "button.o_forecasted_replenish_btn",
run: "click",
},
{
trigger: ".modal-dialog .btn-close",
run: "click",
},
{
trigger: ".o_web_client:not(:has(.modal-dialog))",
},
{
trigger: "button.o_forecasted_replenish_btn",
run: "click",
},
{
trigger: "button[name=launch_replenishment]",
run: "click",
},
{
trigger: ".o_web_client:not(:has(.modal-dialog))",
},
{
trigger:
".o_notification:contains(The following replenishment order have been generated)",
},
],
});

View File

@@ -0,0 +1,9 @@
export function fail(errorMessage) {
throw new Error(errorMessage);
}
export function assert(current, expected, info) {
if (current !== expected) {
fail(`${info}: "${current}" instead of "${expected}".`);
}
}