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:
@@ -0,0 +1,14 @@
|
||||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
|
||||
export class SaleOrder extends models.ServerModel {
|
||||
_name = "sale.order";
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "first record",
|
||||
order_line: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
|
||||
export class SaleOrderLine extends models.ServerModel {
|
||||
_name = "sale.order.line";
|
||||
|
||||
// Store the field for testing to be able to set the translation at the record creation.
|
||||
translated_product_name = fields.Char({store: true});
|
||||
}
|
||||
251
frontend/sale/static/tests/sale_order_line_field.test.js
Normal file
251
frontend/sale/static/tests/sale_order_line_field.test.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import { defineMailModels } from '@mail/../tests/mail_test_helpers';
|
||||
import { expect, test } from '@odoo/hoot';
|
||||
import { queryAllTexts } from '@odoo/hoot-dom';
|
||||
import {
|
||||
clickCancel,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
mountView,
|
||||
} from '@web/../tests/web_test_helpers';
|
||||
import { defineComboModels } from '@product/../tests/product_combo_test_helpers';
|
||||
import { saleModels } from './sale_test_helpers';
|
||||
|
||||
class SaleOrderLine extends saleModels.SaleOrderLine {
|
||||
_records = [
|
||||
{ id: 1, name: "Non Combo Line1", product_id: 1, sequence: 1 },
|
||||
{ id: 2, name: "Non Combo Line2", product_id: 2, sequence: 2 },
|
||||
{ id: 3, name: "Test Combo1", product_id: 5, sequence: 3, product_type: 'combo' },
|
||||
{ id: 4, name: "Combo1 Item 1", product_id: 3, combo_item_id: 3, linked_line_id: 3, sequence: 4 },
|
||||
{ id: 5, name: "Combo1 Item 2", product_id: 1, combo_item_id: 1, linked_line_id: 3, sequence: 5 },
|
||||
{ id: 6, name: "Test Combo2", product_id: 5, sequence: 6, product_type: 'combo' },
|
||||
{ id: 7, name: "Combo2 Item 1", product_id: 4, combo_item_id: 4, linked_line_id: 6, sequence: 7 },
|
||||
{ id: 8, name: "Combo2 Item 2", product_id: 2, combo_item_id: 2, linked_line_id: 6, sequence: 8 },
|
||||
{ id: 9, name: "Non Combo Line3", product_id: 1, sequence: 9 },
|
||||
];
|
||||
}
|
||||
|
||||
class SaleOrder extends saleModels.SaleOrder {
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Combo Sale order",
|
||||
order_line: SaleOrderLine._records.map(record => record.id),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
defineComboModels();
|
||||
defineModels({ SaleOrderLine, SaleOrder });
|
||||
defineMailModels();
|
||||
|
||||
test("test combo move up/down", async () => {
|
||||
await mountView({
|
||||
type: 'form',
|
||||
resModel: 'sale.order',
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field
|
||||
name="order_line"
|
||||
widget="sol_o2m"
|
||||
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" invisible="combo_item_id"/>
|
||||
<field name="name"/>
|
||||
<field name="display_type" column_invisible="1"/>
|
||||
<field name="linked_line_id" column_invisible="1"/>
|
||||
<field name="product_type" column_invisible="1"/>
|
||||
<field name="combo_item_id" column_invisible="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts('.o_data_row')).toEqual([
|
||||
"Non Combo Line1",
|
||||
"Non Combo Line2",
|
||||
"Test Combo1",
|
||||
"Combo1 Item 1",
|
||||
"Combo1 Item 2",
|
||||
"Test Combo2",
|
||||
"Combo2 Item 1",
|
||||
"Combo2 Item 2",
|
||||
"Non Combo Line3",
|
||||
]);
|
||||
|
||||
await contains('.o_data_row:contains(Test Combo1) .o_list_section_options button').click();
|
||||
expect('.o-dropdown-item:contains(Move Up)').toHaveCount(1, {
|
||||
message: 'Move up option should be there for Test combo1 line having SO lines before and after'
|
||||
});
|
||||
expect('.o-dropdown-item:contains(Move Down)').toHaveCount(1, {
|
||||
message: 'Move down option should be there for Test combo1 line having SO lines before and after'
|
||||
});
|
||||
|
||||
await contains('.o-dropdown-item:contains(Move Up)').click();
|
||||
await contains('.o_data_row:contains(Test Combo1) .o_list_section_options button').click();
|
||||
await contains('.o-dropdown-item:contains(Move Up)').click();
|
||||
|
||||
await contains('.o_data_row:contains(Test Combo1) .o_list_section_options button').click();
|
||||
expect('.o-dropdown-item:contains(Move Up)').toHaveCount(0, {
|
||||
message: 'Move up option should be invisible for Test combo1 since there aren\'t any non combo SO lines before'
|
||||
});
|
||||
|
||||
await contains('.o_data_row:contains(Test Combo2) .o_list_section_options button').click();
|
||||
expect('.o-dropdown-item:contains(Move Up)').toHaveCount(1, {
|
||||
message: 'Move up option should be there for Test combo2 line having SO lines before and after'
|
||||
});
|
||||
expect('.o-dropdown-item:contains(Move Down)').toHaveCount(1, {
|
||||
message: 'Move down option should be there for Test combo2 line having SO lines before and after'
|
||||
});
|
||||
|
||||
await contains('.o-dropdown-item:contains(Move Down)').click();
|
||||
|
||||
await contains('.o_data_row:contains(Test Combo2) .o_list_section_options button').click();
|
||||
expect('.o-dropdown-item:contains(Move Down)').toHaveCount(0, {
|
||||
message: 'Move down option should be invisible for Test combo2 since there aren\'t any non combo SO lines after'
|
||||
});
|
||||
|
||||
expect(queryAllTexts('.o_data_row')).toEqual([
|
||||
"Test Combo1",
|
||||
"Combo1 Item 1",
|
||||
"Combo1 Item 2",
|
||||
"Non Combo Line1",
|
||||
"Non Combo Line2",
|
||||
"Non Combo Line3",
|
||||
"Test Combo2",
|
||||
"Combo2 Item 1",
|
||||
"Combo2 Item 2",
|
||||
], {
|
||||
message: 'Test combo1 line should be moved up two lines and Test combo2 line should be moved down one line'
|
||||
});
|
||||
|
||||
await clickCancel();
|
||||
|
||||
expect(queryAllTexts('.o_data_row')).toEqual([
|
||||
"Non Combo Line1",
|
||||
"Non Combo Line2",
|
||||
"Test Combo1",
|
||||
"Combo1 Item 1",
|
||||
"Combo1 Item 2",
|
||||
"Test Combo2",
|
||||
"Combo2 Item 1",
|
||||
"Combo2 Item 2",
|
||||
"Non Combo Line3",
|
||||
]);
|
||||
|
||||
await contains('.o_data_row:contains(Test Combo1) .o_list_section_options button').click();
|
||||
await contains('.o-dropdown-item:contains(Move Down)').click();
|
||||
|
||||
expect(queryAllTexts('.o_data_row')).toEqual([
|
||||
"Non Combo Line1",
|
||||
"Non Combo Line2",
|
||||
"Test Combo2",
|
||||
"Combo2 Item 1",
|
||||
"Combo2 Item 2",
|
||||
"Test Combo1",
|
||||
"Combo1 Item 1",
|
||||
"Combo1 Item 2",
|
||||
"Non Combo Line3",
|
||||
], {
|
||||
message: 'Test combo1 and Test combo2 should be swapped when moving Test combo1 down'
|
||||
});
|
||||
|
||||
await clickCancel();
|
||||
|
||||
await contains('.o_data_row:contains(Test Combo2) .o_list_section_options button').click();
|
||||
await contains('.o-dropdown-item:contains(Move Up)').click();
|
||||
|
||||
expect(queryAllTexts('.o_data_row')).toEqual([
|
||||
"Non Combo Line1",
|
||||
"Non Combo Line2",
|
||||
"Test Combo2",
|
||||
"Combo2 Item 1",
|
||||
"Combo2 Item 2",
|
||||
"Test Combo1",
|
||||
"Combo1 Item 1",
|
||||
"Combo1 Item 2",
|
||||
"Non Combo Line3",
|
||||
], {
|
||||
message: 'Test combo1 and Test combo2 should be swapped when moving Test combo2 up'
|
||||
});
|
||||
})
|
||||
|
||||
test("Test combo columns", async () => {
|
||||
// Set different defaults for checking aggregation of columns on combo line
|
||||
SaleOrderLine._fields.price_unit = fields.Float({ default: 3.00 });
|
||||
SaleOrderLine._fields.price_total = fields.Float({ default: 3.00 });
|
||||
SaleOrderLine._fields.product_uom_qty = fields.Float({ default: 3.00 });
|
||||
SaleOrderLine._fields.discount = fields.Integer({ default: 30 });
|
||||
await mountView({
|
||||
type: 'form',
|
||||
resModel: 'sale.order',
|
||||
resId: 1,
|
||||
arch: `
|
||||
<form>
|
||||
<field
|
||||
name="order_line"
|
||||
widget="sol_o2m"
|
||||
options="{'subsections': True}"
|
||||
aggregated_fields="price_total"
|
||||
>
|
||||
<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" invisible="combo_item_id"/>
|
||||
<field name="name"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="discount"/>
|
||||
<field name="price_total"/>
|
||||
<field name="display_type" column_invisible="1"/>
|
||||
<field name="linked_line_id" column_invisible="1"/>
|
||||
<field name="product_type" column_invisible="1"/>
|
||||
<field name="combo_item_id" column_invisible="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
expect(queryAllTexts('.o_data_row .o_list_text')).toEqual([
|
||||
"Non Combo Line1",
|
||||
"Non Combo Line2",
|
||||
"Test Combo1",
|
||||
"Combo1 Item 1",
|
||||
"Combo1 Item 2",
|
||||
"Test Combo2",
|
||||
"Combo2 Item 1",
|
||||
"Combo2 Item 2",
|
||||
"Non Combo Line3",
|
||||
]);
|
||||
|
||||
expect(queryAllTexts('.o_data_row:contains(Test Combo1) > td').filter(Boolean)).toEqual([
|
||||
"Test Combo1", // name
|
||||
"3.00", // product_uom_qty
|
||||
"30", // discount
|
||||
"9.00", // price_total
|
||||
], {
|
||||
message: 'combo line should only have name, product_uom_qty, discount and `aggregated_fields` columns'
|
||||
});
|
||||
|
||||
expect(queryAllTexts('.o_data_row:contains(Non Combo Line1) > td').filter(Boolean)).toEqual([
|
||||
"Non Combo Line1", // name
|
||||
"3.00", // price_unit
|
||||
"3.00", // product_uom_qty
|
||||
"30", // discount
|
||||
"3.00", // price_total
|
||||
], {
|
||||
message: 'Non-combo line should have all columns'
|
||||
});
|
||||
})
|
||||
201
frontend/sale/static/tests/sale_product_field.test.js
Normal file
201
frontend/sale/static/tests/sale_product_field.test.js
Normal file
@@ -0,0 +1,201 @@
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { press, runAllTimers } from "@odoo/hoot-dom";
|
||||
import {
|
||||
clickSave,
|
||||
Command,
|
||||
contains,
|
||||
defineModels,
|
||||
fields,
|
||||
makeMockServer,
|
||||
models,
|
||||
mountView,
|
||||
onRpc,
|
||||
serverState,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { saleModels } from "./sale_test_helpers";
|
||||
|
||||
class SaleOrderLine extends saleModels.SaleOrderLine {
|
||||
product_template_attribute_value_ids = fields.Many2many({
|
||||
string: "Product template attributes values",
|
||||
relation: "product.template.attribute.value",
|
||||
});
|
||||
}
|
||||
|
||||
class ProductTemplateAttributeValue extends models.Model {
|
||||
_name = "product.template.attribute.value";
|
||||
|
||||
name = fields.Char();
|
||||
}
|
||||
|
||||
defineModels({ ...saleModels, SaleOrderLine, ProductTemplateAttributeValue });
|
||||
|
||||
saleModels.SaleOrder._views.form = /* xml */ `
|
||||
<form>
|
||||
<field name="order_line" widget="sol_o2m" mode="list">
|
||||
<list editable="bottom">
|
||||
<field name="product_id" widget="sol_product_many2one"/>
|
||||
<field name="product_template_id" widget="sol_product_many2one"/>
|
||||
<field name="name" widget="sol_text"/>
|
||||
</list>
|
||||
</field>
|
||||
</form>
|
||||
`;
|
||||
|
||||
test.tags("desktop");
|
||||
test("pressing tab with incomplete text will create a product", async () => {
|
||||
onRpc(({ method }) => {
|
||||
expect.step(method);
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order",
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="order_line">
|
||||
<list editable="bottom">
|
||||
<field name="product_template_id" widget="sol_product_many2one"/>
|
||||
<field name="product_id" optional="hide"/>
|
||||
<field name="name" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
// add a line and enter new product name
|
||||
await contains(".o_field_x2many_list .o_field_x2many_list_row_add a").click();
|
||||
await contains("[name='product_template_id'] input").edit("new product");
|
||||
await press("tab");
|
||||
await runAllTimers();
|
||||
expect.verifySteps([
|
||||
"get_views",
|
||||
"onchange",
|
||||
"onchange",
|
||||
"web_name_search",
|
||||
"name_create",
|
||||
"get_single_product_variant",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Hide product name if its not translated", async () => {
|
||||
const { env } = await makeMockServer();
|
||||
const product = env["product.product"][0];
|
||||
const soId = env["sale.order"].create({
|
||||
partner_id: serverState.partnerId,
|
||||
order_line: [
|
||||
Command.create({
|
||||
product_id: product.id,
|
||||
name: [product.name, "A description"].join("\n"),
|
||||
translated_product_name: "Produit de test",
|
||||
}),
|
||||
],
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order",
|
||||
resId: soId,
|
||||
});
|
||||
|
||||
expect(".o_field_product_label_section_and_note_cell .o_input").toHaveText("A description");
|
||||
});
|
||||
|
||||
test("If translated product name already in the SOL name, should not hide the translated product name", async () => {
|
||||
const { env } = await makeMockServer();
|
||||
const translatedProductName = "Produit de test";
|
||||
const product = env["product.product"][0];
|
||||
const soId = env["sale.order"].create({
|
||||
partner_id: serverState.partnerId,
|
||||
order_line: [
|
||||
Command.create({
|
||||
product_id: product.id,
|
||||
name: [product.name, translatedProductName, "A description"].join("\n"),
|
||||
translated_product_name: translatedProductName,
|
||||
}),
|
||||
],
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order",
|
||||
resId: soId,
|
||||
});
|
||||
|
||||
expect(".o_field_product_label_section_and_note_cell .o_input").toHaveText(
|
||||
[translatedProductName, "A description"].join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
test("Editing the description shouldn't show the translated product name", async () => {
|
||||
const { env } = await makeMockServer();
|
||||
const translatedProductName = "Produit de test";
|
||||
const product = env["product.product"][0];
|
||||
const soId = env["sale.order"].create({
|
||||
partner_id: serverState.partnerId,
|
||||
order_line: [
|
||||
Command.create({
|
||||
product_id: product.id,
|
||||
name: [product.name, "something wrong"].join("\n"),
|
||||
translated_product_name: translatedProductName,
|
||||
}),
|
||||
],
|
||||
});
|
||||
const [so] = env["sale.order"].browse(soId);
|
||||
const [sol] = env["sale.order.line"].browse(so.order_line);
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order",
|
||||
resId: soId,
|
||||
});
|
||||
await contains(".o_field_product_label_section_and_note_cell").click();
|
||||
await contains(".o_field_product_label_section_and_note_cell textarea").edit("A description");
|
||||
await clickSave();
|
||||
|
||||
expect(".o_field_product_label_section_and_note_cell .o_input").toHaveText("A description");
|
||||
expect(sol.name).toBe([translatedProductName, "A description"].join("\n"));
|
||||
});
|
||||
|
||||
test("No description should be shown if there does not exist one apart from the product name", async () => {
|
||||
const { env } = await makeMockServer();
|
||||
const translatedProductName = "Produit de test";
|
||||
const product = env["product.product"][0];
|
||||
const soId = env["sale.order"].create({
|
||||
partner_id: serverState.partnerId,
|
||||
order_line: [
|
||||
Command.create({
|
||||
product_id: product.id,
|
||||
name: product.name,
|
||||
translated_product_name: translatedProductName,
|
||||
}),
|
||||
],
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order",
|
||||
resId: soId,
|
||||
});
|
||||
|
||||
expect(".o_field_product_label_section_and_note_cell .o_input").not.toBeVisible();
|
||||
});
|
||||
|
||||
test("No description should be shown if there does not exist one apart from the translated product name", async () => {
|
||||
const { env } = await makeMockServer();
|
||||
const translatedProductName = "Produit de test";
|
||||
const product = env["product.product"][0];
|
||||
const soId = env["sale.order"].create({
|
||||
partner_id: serverState.partnerId,
|
||||
order_line: [
|
||||
Command.create({
|
||||
product_id: product.id,
|
||||
name: translatedProductName,
|
||||
translated_product_name: translatedProductName,
|
||||
}),
|
||||
],
|
||||
});
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "sale.order",
|
||||
resId: soId,
|
||||
});
|
||||
|
||||
expect(".o_field_product_label_section_and_note_cell .o_input").not.toBeVisible();
|
||||
});
|
||||
17
frontend/sale/static/tests/sale_test_helpers.js
Normal file
17
frontend/sale/static/tests/sale_test_helpers.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { mailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { defineModels } from "@web/../tests/web_test_helpers";
|
||||
import { productModels } from "@product/../tests/product_test_helpers";
|
||||
import { SaleOrder } from "./mock_server/mock_models/sale_order";
|
||||
import { SaleOrderLine } from "./mock_server/mock_models/sale_order_line";
|
||||
|
||||
|
||||
export const saleModels = {
|
||||
...mailModels,
|
||||
...productModels,
|
||||
SaleOrder,
|
||||
SaleOrderLine,
|
||||
};
|
||||
|
||||
export function defineSaleModels() {
|
||||
defineModels(saleModels);
|
||||
}
|
||||
72
frontend/sale/static/tests/sales_team_dashboard.test.js
Normal file
72
frontend/sale/static/tests/sales_team_dashboard.test.js
Normal file
@@ -0,0 +1,72 @@
|
||||
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 CRMTeam extends models.Model {
|
||||
_name = "crm.team";
|
||||
|
||||
foo = fields.Char();
|
||||
invoiced = fields.Integer();
|
||||
invoiced_target = fields.Integer();
|
||||
|
||||
_records = [{ id: 1, foo: "yop", invoiced: 0, invoiced_target: 0 }];
|
||||
}
|
||||
|
||||
defineModels([CRMTeam]);
|
||||
defineMailModels();
|
||||
|
||||
test("edit progressbar target", async () => {
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
expect(action).toEqual(
|
||||
{
|
||||
res_model: "crm.team",
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
method: "get_formview_action",
|
||||
},
|
||||
{ message: "should trigger do_action with the correct args" }
|
||||
);
|
||||
expect.step("doAction");
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
onRpc("crm.team", "get_formview_action", ({ method, model }) => ({
|
||||
method,
|
||||
res_model: model,
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
}));
|
||||
|
||||
await mountView({
|
||||
type: "kanban",
|
||||
resModel: "crm.team",
|
||||
arch: /* xml */ `
|
||||
<kanban>
|
||||
<field name="invoiced_target"/>
|
||||
<templates>
|
||||
<div t-name="card">
|
||||
<field name="invoiced" widget="sales_team_progressbar" options="{'current_value': 'invoiced', 'max_value': 'invoiced_target', 'editable': true, 'edit_max_value': true}"/>
|
||||
</div>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
resId: 1,
|
||||
});
|
||||
|
||||
expect(
|
||||
".o_field_sales_team_progressbar:contains(Click to define an invoicing target)"
|
||||
).toHaveCount(1);
|
||||
expect(".o_progressbar input").toHaveCount(0);
|
||||
|
||||
await contains(".sale_progressbar_form_link").click(); // should trigger a do_action
|
||||
expect.verifySteps(["doAction"]);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add("mail_attachment_removal_tour", {
|
||||
steps: () => [
|
||||
|
||||
{
|
||||
content: "click on send by email",
|
||||
trigger: ".o_statusbar_buttons > button[name='action_quotation_send']",
|
||||
run: "click"
|
||||
},
|
||||
{
|
||||
content: "save a new layout",
|
||||
trigger: ".o_technical_modal button[name='document_layout_save']",
|
||||
run: "click"
|
||||
},
|
||||
{
|
||||
content: "delete attachment",
|
||||
trigger: ".o_field_widget[name='attachment_ids'] li > button .fa-times",
|
||||
run: "click"
|
||||
},
|
||||
{
|
||||
content: "send the email",
|
||||
trigger: ".o_mail_send",
|
||||
run: "click"
|
||||
},
|
||||
{
|
||||
content: "confirm quotation",
|
||||
trigger: "button[name='action_confirm']",
|
||||
run: "click"
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_utils";
|
||||
|
||||
const openProductAttribute = (product_attribute) => [
|
||||
...stepUtils.goToAppSteps("sale.sale_menu_root", "Go to the Sales App"),
|
||||
{
|
||||
content: 'Open configuration menu',
|
||||
trigger: '.o-dropdown[data-menu-xmlid="sale.menu_sale_config"]',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: 'Navigate to product attribute list view',
|
||||
trigger: '.o-dropdown-item[data-menu-xmlid="sale.menu_product_attribute_action"]',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: `Navigate to ${product_attribute}`,
|
||||
trigger: `.o_data_cell[data-tooltip=${product_attribute}]`,
|
||||
run: "click",
|
||||
},
|
||||
];
|
||||
const deletePAV = (product_attribute_value, message) => [
|
||||
{
|
||||
content: 'Click delete button',
|
||||
trigger: `.o_data_cell[data-tooltip=${product_attribute_value}] ~ .o_list_record_remove`,
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: 'Check correct message in modal',
|
||||
trigger: message || '.modal-title:contains("Bye-bye, record!")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: 'Close modal',
|
||||
trigger: '.btn-close',
|
||||
run: "click",
|
||||
}
|
||||
]
|
||||
|
||||
// This tour relies on data created on the Python test.
|
||||
registry.category("web_tour.tours").add('delete_product_attribute_value_tour', {
|
||||
url: '/odoo',
|
||||
steps: () => [
|
||||
...openProductAttribute("PA"),
|
||||
// Test error message on a used attribute value
|
||||
...deletePAV("pa_value_1", ".text-prewrap:contains('pa_value_1')"),
|
||||
// Test deletability of a used attribute value on archived product
|
||||
...deletePAV("pa_value_2"),
|
||||
// Test deletability of a removed attribute value on product
|
||||
...deletePAV("pa_value_3"),
|
||||
{
|
||||
content: 'Check test finished',
|
||||
trigger: 'a:contains("Attributes")',
|
||||
}
|
||||
]
|
||||
});
|
||||
98
frontend/sale/static/tests/tours/sale_catalog.js
Normal file
98
frontend/sale/static/tests/tours/sale_catalog.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { addSectionFromProductCatalog } from "@account/js/tours/tour_utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add('sale_catalog', {
|
||||
steps: () => [
|
||||
{
|
||||
content: "Create a new SO",
|
||||
trigger: '.o_list_button_add',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Select the customer field",
|
||||
trigger: ".o_field_res_partner_many2one input.o_input",
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Wait for the field to be active",
|
||||
trigger: ".o_field_res_partner_many2one input[aria-expanded=true]",
|
||||
},
|
||||
{
|
||||
content: "Select a customer from the dropdown",
|
||||
trigger: ".o_field_res_partner_many2one .dropdown-item:not([id$='_loading']):first",
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Open product catalog",
|
||||
trigger: 'button[name="action_add_from_catalog"]',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Type 'Restricted' into the search bar",
|
||||
trigger: 'input.o_searchview_input',
|
||||
run: "edit Restricted",
|
||||
},
|
||||
{
|
||||
content: "Search for the product",
|
||||
trigger: 'input.o_searchview_input',
|
||||
run: "press Enter",
|
||||
},
|
||||
{
|
||||
content: "Wait for catalog rendering",
|
||||
trigger: '.o_kanban_record:contains("Restricted Product")',
|
||||
},
|
||||
{
|
||||
content: "Wait for filtering",
|
||||
trigger: '.o_kanban_renderer:not(:has(.o_kanban_record:contains("AAA Product")))',
|
||||
},
|
||||
{
|
||||
content: "Add the product to the SO",
|
||||
trigger: '.o_kanban_record:contains("Restricted Product") .fa-shopping-cart',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Wait for product to be added",
|
||||
trigger: '.o_kanban_record:contains("Restricted Product"):not(:has(.fa-shopping-cart))',
|
||||
},
|
||||
{
|
||||
content: "Input a custom quantity",
|
||||
trigger: '.o_kanban_record:contains("Restricted Product") .o_input',
|
||||
run: "edit 6",
|
||||
},
|
||||
{
|
||||
content: "Increase the quantity",
|
||||
trigger: '.o_kanban_record:contains("Restricted Product") .fa-plus',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Close the catalog",
|
||||
trigger: '.o-kanban-button-back',
|
||||
run: 'click',
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
registry.category("web_tour.tours").add('test_add_section_from_product_catalog_on_sale_order', {
|
||||
steps: () => [
|
||||
{
|
||||
content: "Create a new SO",
|
||||
trigger: '.o_list_button_add',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Select the customer field",
|
||||
trigger: '.o_field_res_partner_many2one input.o_input',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Wait for the field to be active",
|
||||
trigger: '.o_field_res_partner_many2one input[aria-expanded=true]',
|
||||
},
|
||||
{
|
||||
content: "Select a customer from the dropdown",
|
||||
trigger: '.o_field_res_partner_many2one .dropdown-item:not([id$="_loading"]):first',
|
||||
run: 'click',
|
||||
},
|
||||
...addSectionFromProductCatalog(),
|
||||
]
|
||||
});
|
||||
136
frontend/sale/static/tests/tours/sale_combo_configurator.js
Normal file
136
frontend/sale/static/tests/tours/sale_combo_configurator.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import { registry } from '@web/core/registry';
|
||||
import { stepUtils } from '@web_tour/tour_utils';
|
||||
import comboConfiguratorTourUtils from '@sale/js/tours/combo_configurator_tour_utils';
|
||||
import productConfiguratorTourUtils from '@sale/js/tours/product_configurator_tour_utils';
|
||||
import tourUtils from '@sale/js/tours/tour_utils';
|
||||
|
||||
registry
|
||||
.category('web_tour.tours')
|
||||
.add('sale_combo_configurator', {
|
||||
url: '/odoo',
|
||||
steps: () => [
|
||||
...stepUtils.goToAppSteps('sale.sale_menu_root', "Open the sales app"),
|
||||
...tourUtils.createNewSalesOrder(),
|
||||
...tourUtils.selectCustomer("Test Partner"),
|
||||
...tourUtils.addProduct("Combo product"),
|
||||
// Assert that the combo configurator has the correct data.
|
||||
comboConfiguratorTourUtils.assertComboCount(2),
|
||||
comboConfiguratorTourUtils.assertComboItemCount("Combo A", 2),
|
||||
comboConfiguratorTourUtils.assertComboItemCount("Combo B", 2),
|
||||
// Assert that price changes when the quantity is updated.
|
||||
comboConfiguratorTourUtils.assertQuantity(1),
|
||||
comboConfiguratorTourUtils.assertPrice('25.00'),
|
||||
comboConfiguratorTourUtils.increaseQuantity(),
|
||||
comboConfiguratorTourUtils.assertQuantity(2),
|
||||
comboConfiguratorTourUtils.assertPrice('50.00'),
|
||||
comboConfiguratorTourUtils.decreaseQuantity(),
|
||||
comboConfiguratorTourUtils.assertQuantity(1),
|
||||
comboConfiguratorTourUtils.assertPrice('25.00'),
|
||||
comboConfiguratorTourUtils.setQuantity(3),
|
||||
comboConfiguratorTourUtils.assertQuantity(3),
|
||||
comboConfiguratorTourUtils.assertPrice('75.00'),
|
||||
// Assert that the combo configurator can only be saved after selecting an item for each
|
||||
// combo.
|
||||
comboConfiguratorTourUtils.assertConfirmButtonDisabled(),
|
||||
comboConfiguratorTourUtils.selectComboItem("Product A2"),
|
||||
comboConfiguratorTourUtils.selectComboItem("Product B2"),
|
||||
comboConfiguratorTourUtils.assertConfirmButtonEnabled(),
|
||||
// Assert that the product configurator is opened when a product with configurable
|
||||
// `no_variant` PTALs is selected.
|
||||
comboConfiguratorTourUtils.selectComboItem("Product A1"),
|
||||
productConfiguratorTourUtils.selectAttribute("Product A1", "No variant attribute", "A"),
|
||||
...productConfiguratorTourUtils.saveConfigurator(),
|
||||
// Assert that the extra price of a combo item is applied correctly.
|
||||
comboConfiguratorTourUtils.assertPrice('90.00'),
|
||||
// Assert that the extra price of a `no_variant` PTAV is applied correctly.
|
||||
comboConfiguratorTourUtils.selectComboItem("Product A1"),
|
||||
...productConfiguratorTourUtils.selectAndSetCustomAttribute(
|
||||
"Product A1", "No variant attribute", "B", "Some custom value"
|
||||
),
|
||||
...productConfiguratorTourUtils.saveConfigurator(),
|
||||
comboConfiguratorTourUtils.assertPrice('93.00'),
|
||||
// Assert that the order's content is correct.
|
||||
...comboConfiguratorTourUtils.saveConfigurator(),
|
||||
tourUtils.checkSOLDescriptionContains("Combo product x 3"),
|
||||
tourUtils.checkSOLDescriptionContains(
|
||||
"Product A1", "No variant attribute: B: Some custom value"
|
||||
),
|
||||
tourUtils.checkSOLDescriptionContains("Product B2"),
|
||||
{
|
||||
content: "Verify the combo item quantities",
|
||||
trigger: 'td[name="product_uom_qty"]:contains(3.00)',
|
||||
},
|
||||
{
|
||||
content: "Verify the first combo item's unit price",
|
||||
trigger: 'td[name="price_unit"]:contains(18.50)',
|
||||
},
|
||||
{
|
||||
content: "Verify the second combo item's unit price",
|
||||
trigger: 'td[name="price_unit"]:contains(12.50)',
|
||||
},
|
||||
{
|
||||
content: "Verify the order's total price",
|
||||
trigger: 'div.oe_subtotal_footer:contains(93.00)',
|
||||
},
|
||||
// Assert that the combo configurator is opened with the previous selection when the
|
||||
// combo is edited.
|
||||
tourUtils.editLineMatching("Combo product x 3"),
|
||||
tourUtils.editConfiguration(),
|
||||
comboConfiguratorTourUtils.setQuantity(2),
|
||||
comboConfiguratorTourUtils.assertComboItemSelected("Product A1"),
|
||||
comboConfiguratorTourUtils.assertComboItemSelected("Product B2"),
|
||||
comboConfiguratorTourUtils.selectComboItem("Product A2"),
|
||||
// Assert that the order's content has been updated.
|
||||
...comboConfiguratorTourUtils.saveConfigurator(),
|
||||
tourUtils.checkSOLDescriptionContains("Combo product x 2"),
|
||||
tourUtils.checkSOLDescriptionContains("Product A2"),
|
||||
tourUtils.checkSOLDescriptionContains("Product B2"),
|
||||
{
|
||||
content: "Verify the combo item quantities",
|
||||
trigger: 'td[name="product_uom_qty"]:contains(2.00)',
|
||||
},
|
||||
{
|
||||
content: "Verify the first combo item's unit price",
|
||||
trigger: 'td[name="price_unit"]:contains(12.50)',
|
||||
},
|
||||
{
|
||||
content: "Verify the second combo item's unit price",
|
||||
trigger: 'td[name="price_unit"]:contains(12.50)',
|
||||
},
|
||||
{
|
||||
content: "Verify the order's total price",
|
||||
trigger: 'div.oe_subtotal_footer:contains(50.00)',
|
||||
},
|
||||
// Don't end the tour with a form in edition mode.
|
||||
...stepUtils.saveForm(),
|
||||
],
|
||||
});
|
||||
|
||||
registry
|
||||
.category('web_tour.tours')
|
||||
.add('sale_combo_configurator_with_optional_products', {
|
||||
url: '/odoo',
|
||||
steps: () => [
|
||||
...stepUtils.goToAppSteps('sale.sale_menu_root', "Open the sales app"),
|
||||
...tourUtils.createNewSalesOrder(),
|
||||
...tourUtils.selectCustomer("Test Partner"),
|
||||
...tourUtils.addProduct("Combo product"),
|
||||
comboConfiguratorTourUtils.selectComboItem("Product B2"),
|
||||
...comboConfiguratorTourUtils.saveConfigurator(),
|
||||
productConfiguratorTourUtils.addOptionalProduct("Optional product"),
|
||||
{
|
||||
content: "verify that we cannot reduce main product quantity",
|
||||
trigger: ':not(button[name="sale_quantity_button_minus"])',
|
||||
},
|
||||
{
|
||||
content: "verify that we cannot increase main product quantity",
|
||||
trigger: ':not(button[name="sale_quantity_button_plus"])',
|
||||
},
|
||||
...productConfiguratorTourUtils.saveConfigurator(),
|
||||
tourUtils.checkSOLDescriptionContains("Combo product"),
|
||||
tourUtils.checkSOLDescriptionContains("Product B2"),
|
||||
tourUtils.checkSOLDescriptionContains("Optional product"),
|
||||
// Don't end the tour with a form in edition mode.
|
||||
...stepUtils.saveForm(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { registry } from '@web/core/registry';
|
||||
import { stepUtils } from '@web_tour/tour_utils';
|
||||
import comboConfiguratorTourUtils from '@sale/js/tours/combo_configurator_tour_utils';
|
||||
import productConfiguratorTourUtils from '@sale/js/tours/product_configurator_tour_utils';
|
||||
import tourUtils from '@sale/js/tours/tour_utils';
|
||||
|
||||
registry
|
||||
.category('web_tour.tours')
|
||||
.add('sale_combo_configurator_preconfigure_unconfigurable_ptals', {
|
||||
url: '/odoo',
|
||||
steps: () => [
|
||||
...stepUtils.goToAppSteps('sale.sale_menu_root', "Open the sales app"),
|
||||
...tourUtils.createNewSalesOrder(),
|
||||
...tourUtils.selectCustomer("Test Partner"),
|
||||
...tourUtils.addProduct("Combo product"),
|
||||
{
|
||||
content: "Verify that unconfigurable ptals are preconfigured",
|
||||
trigger: `${comboConfiguratorTourUtils.comboItemSelector("Test product")}:contains("Attribute A: A")`,
|
||||
},
|
||||
{
|
||||
content: "Verify that configurable ptals aren't preconfigured",
|
||||
trigger: `${comboConfiguratorTourUtils.comboItemSelector("Test product")}:not(:contains("Attribute B: B"))`,
|
||||
},
|
||||
comboConfiguratorTourUtils.selectComboItem("Test product"),
|
||||
productConfiguratorTourUtils.selectAttribute(
|
||||
"Test product", "Attribute B", "B", 'multi'
|
||||
),
|
||||
...productConfiguratorTourUtils.saveConfigurator(),
|
||||
{
|
||||
content: "Verify that configurable ptals are now configured",
|
||||
trigger: `${comboConfiguratorTourUtils.comboItemSelector("Test product")}:contains("Attribute B: B")`,
|
||||
},
|
||||
...comboConfiguratorTourUtils.saveConfigurator(),
|
||||
// Don't end the tour with a form in edition mode.
|
||||
...stepUtils.saveForm(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { registry } from '@web/core/registry';
|
||||
import { stepUtils } from '@web_tour/tour_utils';
|
||||
import comboConfiguratorTourUtils from '@sale/js/tours/combo_configurator_tour_utils';
|
||||
import productConfiguratorTourUtils from '@sale/js/tours/product_configurator_tour_utils';
|
||||
import tourUtils from '@sale/js/tours/tour_utils';
|
||||
|
||||
registry
|
||||
.category('web_tour.tours')
|
||||
.add('sale_combo_configurator_preselect_single_unconfigurable_items', {
|
||||
url: '/odoo',
|
||||
steps: () => [
|
||||
...stepUtils.goToAppSteps('sale.sale_menu_root', "Open the sales app"),
|
||||
...tourUtils.createNewSalesOrder(),
|
||||
...tourUtils.selectCustomer("Test Partner"),
|
||||
...tourUtils.addProduct("Combo product"),
|
||||
// Assert that only single unconfigurable items are preselected.
|
||||
comboConfiguratorTourUtils.assertPreselectedComboItemCount(2),
|
||||
comboConfiguratorTourUtils.assertComboItemPreselected("Product A"),
|
||||
comboConfiguratorTourUtils.assertComboItemPreselected("Product C"),
|
||||
comboConfiguratorTourUtils.assertConfirmButtonDisabled(),
|
||||
// Configure the remaining combos.
|
||||
comboConfiguratorTourUtils.selectComboItem("Product B"),
|
||||
productConfiguratorTourUtils.selectAttribute("Product B", "Attribute B", "B", 'multi'),
|
||||
...productConfiguratorTourUtils.saveConfigurator(),
|
||||
comboConfiguratorTourUtils.selectComboItem("Product D"),
|
||||
productConfiguratorTourUtils.setCustomAttribute(
|
||||
"Product D", "Attribute D", "Test D"
|
||||
),
|
||||
...productConfiguratorTourUtils.saveConfigurator(),
|
||||
comboConfiguratorTourUtils.selectComboItem("Product E1"),
|
||||
comboConfiguratorTourUtils.assertConfirmButtonEnabled(),
|
||||
...comboConfiguratorTourUtils.saveConfigurator(),
|
||||
// Don't end the tour with a form in edition mode.
|
||||
...stepUtils.saveForm(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { registry } from '@web/core/registry';
|
||||
import { stepUtils } from '@web_tour/tour_utils';
|
||||
import productConfiguratorTourUtils from '@sale/js/tours/product_configurator_tour_utils';
|
||||
import tourUtils from '@sale/js/tours/tour_utils';
|
||||
|
||||
registry.category('web_tour.tours').add('sale_order_keep_uom_on_variant_wizard_quantity_change', {
|
||||
steps: () => [
|
||||
tourUtils.editLineMatching("Sofa"),
|
||||
tourUtils.editConfiguration(),
|
||||
productConfiguratorTourUtils.increaseProductQuantity("Sofa"),
|
||||
...productConfiguratorTourUtils.saveConfigurator(),
|
||||
...stepUtils.saveForm(),
|
||||
],
|
||||
});
|
||||
99
frontend/sale/static/tests/tours/sale_signature.js
Normal file
99
frontend/sale/static/tests/tours/sale_signature.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { redirect } from "@web/core/utils/urls";
|
||||
|
||||
// This tour relies on data created on the Python test.
|
||||
registry.category("web_tour.tours").add('sale_signature', {
|
||||
url: '/my/quotes',
|
||||
steps: () => [
|
||||
{
|
||||
content: "open the test SO",
|
||||
trigger: 'a:text(test SO)',
|
||||
run: "click",
|
||||
expectUnloadPage: true,
|
||||
},
|
||||
{
|
||||
content: "click sign",
|
||||
trigger: 'a:contains("Sign")',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "clear the signature name",
|
||||
trigger: '.modal .o_web_sign_name_and_signature input',
|
||||
run: "clear",
|
||||
},
|
||||
{
|
||||
content: "check submit is disabled when name is empty",
|
||||
trigger: '.modal .o_portal_sign_submit:disabled',
|
||||
},
|
||||
{
|
||||
content: "reset signature name",
|
||||
trigger: '.modal .o_web_sign_name_and_signature input',
|
||||
run: "fill Joel Willis",
|
||||
},
|
||||
{
|
||||
content: "check submit is enabled",
|
||||
trigger: '.o_portal_sign_submit:enabled',
|
||||
},
|
||||
{
|
||||
trigger: ".modal .o_web_sign_name_and_signature input:value(Joel Willis)"
|
||||
},
|
||||
{
|
||||
trigger: ".modal canvas.o_web_sign_signature",
|
||||
run: "canvasNotEmpty",
|
||||
},
|
||||
{
|
||||
content: "click select style",
|
||||
trigger: '.modal .o_web_sign_auto_select_style button',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "click style 4",
|
||||
trigger: ".o-dropdown-item:eq(3)",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "click submit",
|
||||
trigger: '.modal .o_portal_sign_submit:enabled',
|
||||
run: "click",
|
||||
expectUnloadPage: true,
|
||||
},
|
||||
{
|
||||
content: "check it's confirmed",
|
||||
trigger: '#quote_content:contains("Thank You")',
|
||||
run: "click",
|
||||
}, {
|
||||
trigger: '#quote_content',
|
||||
run: function () {
|
||||
redirect("/odoo");
|
||||
}, // Avoid race condition at the end of the tour by returning to the home page.
|
||||
expectUnloadPage: true,
|
||||
},
|
||||
{
|
||||
trigger: 'nav',
|
||||
}
|
||||
]});
|
||||
|
||||
registry.category("web_tour.tours").add("sale_signature_without_name", {
|
||||
steps: () => [
|
||||
{
|
||||
content: "Wait for interactions to load",
|
||||
trigger: `body[is-ready=true], :iframe body[is-ready=true]`,
|
||||
},
|
||||
{
|
||||
content: "Sign & Pay",
|
||||
trigger:
|
||||
".o_portal_sale_sidebar .btn-primary, :iframe .o_portal_sale_sidebar .btn-primary",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "click submit",
|
||||
trigger: ".o_portal_sign_submit:enabled, :iframe .o_portal_sign_submit:enabled",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
content: "check error because no name",
|
||||
trigger:
|
||||
'.o_portal_sign_error_msg:contains("Signature is missing."), :iframe .o_portal_sign_error_msg:contains("Signature is missing.")',
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user