19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -0,0 +1,61 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { queryFirst } from "@odoo/hoot-dom";
import { defineModels, mountView } from "@web/../tests/web_test_helpers";
import { defineProjectModels } from "@project/../tests/project_models";
import { ProductProduct, ProjectMilestone, SaleOrderLine } from "./project_task_model"
describe.current.tags("desktop");
defineModels([SaleOrderLine, ProductProduct]);
defineProjectModels();
beforeEach(() => {
ProjectMilestone._records = [
{ id: 1, product_uom_qty: -1, quantity_percentage: -25 },
{ id: 2, product_uom_qty: 5, quantity_percentage: 125 },
{ id: 3, product_uom_qty: 2, quantity_percentage: 0.02 },
];
});
const mountViewParams = {
resModel: "project.milestone",
type: "form",
arch: `
<form>
<field name="product_uom_qty" decoration-danger="quantity_percentage &lt; 0 or 1 &lt; quantity_percentage"/>
<field name="quantity_percentage" decoration-danger="quantity_percentage &lt; 0 or 1 &lt; quantity_percentage"/>
</form>
`,
};
/**
* Helper function to mount the view and test if an element has the `text-danger` class.
* @param {number} resId.
* @param {boolean} shouldHaveClass.
*/
async function _testElementClass(resId, shouldHaveClass) {
mountViewParams.resId = resId;
await mountView(mountViewParams);
const quantityElement = queryFirst('#quantity_percentage_0').parentElement;
const productUomQtyElement = queryFirst('#product_uom_qty_0').parentElement;
if (shouldHaveClass) {
expect(quantityElement).toHaveClass("text-danger");
expect(productUomQtyElement).toHaveClass("text-danger");
} else {
expect(quantityElement).not.toHaveClass("text-danger");
expect(productUomQtyElement).not.toHaveClass("text-danger");
}
}
test("Quantities have text-danger if quantity < 0", async () => {
await _testElementClass(1, true);
});
test("Quantities have text-danger if quantity > 100", async () => {
await _testElementClass(2, true);
});
test("Quantities don't have text-danger if quantity >= 0", async () => {
await _testElementClass(3, false);
});

View file

@ -0,0 +1,60 @@
import { describe, expect, test } from "@odoo/hoot";
import { check, queryAll, queryOne } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { defineModels, mountView } from "@web/../tests/web_test_helpers";
import { mailModels } from "@mail/../tests/mail_test_helpers";
import { defineProjectModels, projectModels } from "@project/../tests/project_models";
import { ProductProduct, SaleOrderLine } from "./project_task_model";
describe.current.tags("desktop");
defineModels([SaleOrderLine, ProductProduct]);
defineProjectModels();
test("cannot edit sale_line_id when partners are different", async () => {
mailModels.ResPartner._records = [
{ id: 101, name: "Acme Corporation" },
{ id: 102, name: "Azure Interior" },
...mailModels.ResPartner._records,
];
projectModels.ProjectTask._records = [
{ id: 1, partner_id: 101, sale_line_id: 1 },
{ id: 2, partner_id: 102, sale_line_id: 2 },
];
SaleOrderLine._records = [
{ id: 1, name: "order1" },
{ id: 2, name: "order2" },
];
await mountView({
resModel: "project.task",
type: "list",
arch: `
<list multi_edit="1" js_class="project_task_list">
<field name="partner_id"/>
<field name="sale_line_id"/>
</list>
`,
});
const [firstRow, secondRow] = queryAll(".o_data_row");
await check(".o_list_record_selector input", { root: firstRow });
await animationFrame();
expect(queryOne("[name=sale_line_id]", { root: firstRow })).not.toHaveClass("o_readonly_modifier", {
message: "None of the fields should be readonly",
});
expect(queryOne("[name=sale_line_id]", { root: secondRow })).not.toHaveClass("o_readonly_modifier", {
message: "None of the fields should be readonly",
});
await check(".o_list_record_selector input", { root: secondRow });
await animationFrame();
expect(queryOne("[name=sale_line_id]", { root: firstRow })).toHaveClass("o_readonly_modifier", {
message: "The sale_ine_id should be readonly",
});
expect(queryOne("[name=sale_line_id]", { root: secondRow })).toHaveClass("o_readonly_modifier", {
message: "The sale_ine_id should be readonly",
});
});

View file

@ -0,0 +1,57 @@
import { fields, models } from "@web/../tests/web_test_helpers";
import { projectModels } from "@project/../tests/project_models";
export class ProjectTask extends projectModels.ProjectTask {
_name = "project.task";
sale_line_id = fields.Many2one({ string: "Sale Order Line", relation: "sale.order.line" });
}
export class ProjectMilestone extends projectModels.ProjectMilestone {
_name = "project.milestone";
product_uom_qty = fields.Float({ string: "Quantity" });
quantity_percentage = fields.Float({ string: "Percentage" });
}
export class SaleOrder extends models.Model {
_name = "sale.order";
name = fields.Char({ string: "name" });
partner_id = fields.Many2one({ string: "Customer", relation: "res.partner" });
project_id = fields.Many2one({ string: "Project", relation: "project.project" });
order_line = fields.One2many({ relation: "sale.order.line" });
_records = [{ id: 1, name: "Sales Order 1" }];
}
export class SaleOrderLine extends models.Model {
_name = "sale.order.line";
name = fields.Char({ related: "product_id.name" });
product_id = fields.Many2one({ string: "Product", relation: "product.product" });
_records = [{ id: 1, product_id: 1 }];
}
export class ProductProduct extends models.Model {
_name = "product.product";
name = fields.Char();
type = fields.Selection({
string: "Type",
selection: [("consu", "Goods"), ("service", "Service"), ("combo", "Combo")],
});
_records = [
{ id: 1, name: "Service Product 1", type: "service" },
{ id: 2, name: "Consumable Product 1", type: "consu" },
{ id: 3, name: "Service Product 2", type: "service" },
];
}
Object.assign(projectModels, {
ProjectMilestone,
ProjectTask,
});

View file

@ -0,0 +1,163 @@
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { click, edit, queryOne, runAllTimers } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { focus, mailModels } from "@mail/../tests/mail_test_helpers";
import { projectModels } from "@project/../tests/project_models";
import { contains, defineModels, mountView, onRpc } from "@web/../tests/web_test_helpers";
import { ProductProduct, ProjectTask, SaleOrder, SaleOrderLine } from "./project_task_model";
describe.current.tags("desktop");
SaleOrder._views = {
form: `
<form>
<group>
<field name="partner_id" required="True"/>
<field name="project_id"/>
</group>
<notebook>
<page string="Order Lines" name="order_lines">
<field name="order_line">
<list editable="bottom">
<field name="product_id"/>
</list>
</field>
</page>
</notebook>
</form>
`,
};
ProjectTask._views = {
form: `
<form>
<group>
<field name="name"/>
<field name="project_id"/>
<field name="partner_id"/>
<field
name="sale_line_id"
context="{'default_project_id': project_id, 'default_partner_id': partner_id}"
widget="so_line_create_button"
/>
</group>
</form>
`,
};
projectModels.ProjectTask = ProjectTask;
defineModels({ ...mailModels, ...projectModels, SaleOrder, SaleOrderLine, ProductProduct });
beforeEach(() => {
ProjectTask._records[0].partner_id = 1;
});
onRpc("get_first_service_line", function ({ args, model }) {
const [solId] = this.env[model].browse(args[0])[0].order_line;
const productId = this.env["sale.order.line"].browse(solId)[0].product_id;
const productType = this.env["product.product"].browse(productId)[0].type;
if (productType === "service") {
expect.step("valid_so");
return [solId];
} else {
expect.step("invalid_so");
return false;
}
});
test("test so_line_create_button widget: valid SO", async () => {
const project_name = projectModels.ProjectProject._records.find(
(project) => project.id === 1
).name;
const partner_name = mailModels.ResPartner._records.find((partner) => partner.id === 1).name;
await mountView({
resId: 1,
resModel: "project.task",
type: "form",
});
await focus("div[name='sale_line_id'] input");
const create_so_button = queryOne(
"div[name='sale_line_id'] a[aria-label='Create Sales Order']"
);
expect(create_so_button).toBeVisible({
message: "The so_line_create_button widget should appear when creating a new record.",
});
await create_so_button.click();
await animationFrame();
expect("div[name='partner_id'] input").toHaveValue(partner_name, {
message:
"The default_partner_id set in the field context should be passed in the SO form view.",
});
expect("div[name='project_id'] input").toHaveValue(project_name, {
message:
"The default_project_id set in the field context should be passed in the SO form view.",
});
await contains(".modal-content .o_field_x2many_list_row_add a").click();
await contains(".modal-content .o_selected_row td[name='product_id'] input").edit(
"Service Product 2"
);
await contains(".modal-content .ui-sortable .o-autocomplete--input").click();
await contains(".dropdown-item:nth-child(1)").click();
await contains(".modal-content button[class*='o_form_button_save']").click();
expect("div[name='sale_line_id'] input").toHaveValue("Service Product 2", {
message: "The sale order line should be created and set in the input field.",
});
// As the SO contains at least one service product, it should be validated and created.
expect.verifySteps(["valid_so"]);
});
test("test so_line_create_button widget: invalid SO", async () => {
await mountView({
resId: 1,
resModel: "project.task",
type: "form",
});
await focus("div[name='sale_line_id'] input");
await contains("a[aria-label='Create Sales Order']").click();
await animationFrame();
await contains(".modal-content .o_field_x2many_list_row_add a").click();
await contains(".modal-content .o_selected_row td[name='product_id'] input").edit(
"Consumable Product 1"
);
await contains(".modal-content .ui-sortable .o-autocomplete--input").click();
await contains(".dropdown-item:nth-child(1)").click();
await contains(".modal-content button[class*='o_form_button_save']").click();
expect("div[name='sale_line_id'] input").toHaveValue("", {
message: "The sale order line should not be created and set in the input field.",
});
// As the SO does not contain at least one service product, it should not be validated and created.
expect.verifySteps(["invalid_so"]);
});
test("test so_line_create_button widget: visibility conditions", async () => {
ProjectTask._records.find((t) => t.id).sale_line_id = SaleOrderLine._records[0].id;
await mountView({
resId: 1,
resModel: "project.task",
type: "form",
});
await click("div[name='sale_line_id'] input");
expect("div[name='sale_line_id'] a[aria-label='Create Sales Order']").toHaveCount(0, {
message:
"The so_line_create_button widget should not appear as there is already a value in sale_line_id field.",
});
await edit("");
await runAllTimers();
await click("div[name='name'] input");
await animationFrame();
await focus("div[name='sale_line_id'] input");
expect("div[name='sale_line_id'] a[aria-label='Create Sales Order']").toBeVisible({
message:
"The so_line_create_button widget should appear as there is no value in sale_line_id field.",
});
});

View file

@ -0,0 +1,59 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add('project_create_sol_tour', {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(), {
trigger: ".o_app[data-menu-xmlid='project.menu_main_pm']",
content: 'Select Project main menu.',
run: "click",
}, {
trigger: ".o_kanban_record:contains(Test History Project):first",
content: "Open the project dropdown of project name 'Test History Project'.",
run: "hover && click .o_kanban_record:contains(Test History Project):first .o_dropdown_kanban .dropdown-toggle",
}, {
trigger: ".o_kanban_card_manage_settings a:contains('Settings')",
content: 'Start editing the project.',
run: "click",
}, {
trigger: ".o_field_widget[name='partner_id'] input",
content: "Add the customer for this project",
run: "click",
}, {
isActive: ["auto"],
trigger: ".ui-autocomplete > li > a:not(:has(i.fa))",
content: "Select the customer in the autocomplete dropdown",
run: "click",
},
{
trigger: 'div.o_notebook_headers',
},
{
trigger: 'a.nav-link[name="settings"]',
content: 'Click on Settings tab to configure this project.',
run: "click",
}, {
id: "project_sale_timesheet_start",
trigger: "div[name='sale_line_id'] input",
content: 'Add the Sales Order Item',
run: "fill New Sale order line",
}, {
trigger: ".o_field_widget[name=sale_line_id] .o-autocomplete--dropdown-menu .o_m2o_dropdown_option_create a",
content: "Create an Sales Order Item in the autocomplete dropdown.",
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
trigger: ".o_form_button_save:enabled",
content: "Save project",
run: "click",
},
// Those steps are currently needed in order to prevent the following issue:
// "Form views in edition mode are automatically saved when the page is closed, which leads to stray network requests and inconsistencies."
...stepUtils.toggleHomeMenu(),
...stepUtils.goToAppSteps("project.menu_main_pm", 'Go to the Project app.'),
]});

View file

@ -0,0 +1,22 @@
/**
* Add custom step to check allow_billable during project creation
* to be able to set a partner on project/tasks.
*/
import { registry } from "@web/core/registry";
import "@project/../tests/tours/project_tour";
import { patch } from "@web/core/utils/patch";
patch(registry.category("web_tour.tours").get("project_test_tour"), {
steps() {
const originalSteps = super.steps();
const projectCreationStepIndex = originalSteps.findIndex(
(step) => step.id === "project_creation"
);
originalSteps.splice(projectCreationStepIndex, 0, {
trigger: "div[name='allow_billable'] input",
run: "edit Test",
});
return originalSteps;
},
});