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,55 @@
import { test, expect } from "@odoo/hoot";
import { setupPosEnv } from "@point_of_sale/../tests/unit/utils";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { Orderline } from "@point_of_sale/app/components/orderline/orderline";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
definePosModels();
test("Displays the table with details of the down payment", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const productDownPayment = store.models["product.template"].get(105);
const sol1 = store.models["sale.order.line"].get(1);
const sol2 = store.models["sale.order.line"].get(2);
const line = await store.addLineToOrder(
{
product_tmpl_id: productDownPayment,
sale_order_origin_id: 1,
down_payment_details: [
{
product_name: sol1.display_name,
product_uom_qty: sol1.product_uom_qty,
price_unit: sol1.price_unit,
total: sol1.price_total,
},
{
product_name: sol2.display_name,
product_uom_qty: sol2.product_uom_qty,
price_unit: sol2.price_unit,
total: sol2.price_total,
},
],
qty: 1,
},
order
);
const comp = await mountWithCleanup(Orderline, { props: { line } });
const saleOrderInfo = ".orderline .info-list .sale-order-info";
const cell = (tr, td) => `${saleOrderInfo} tr:nth-child(${tr}) td:nth-child(${td})`;
expect(comp.line).toEqual(line);
expect(saleOrderInfo).toBeVisible();
expect(`${saleOrderInfo} tr`).toHaveCount(2);
expect(cell(1, 1)).toHaveText("5x");
expect(cell(1, 2)).toHaveText("Product 1");
expect(cell(1, 4)).toHaveText(`$ 500.00 (tax incl.)`);
expect(cell(2, 1)).toHaveText("3x");
expect(cell(2, 2)).toHaveText("Product 2");
expect(cell(2, 4)).toHaveText(`$ 150.00 (tax incl.)`);
});

View file

@ -0,0 +1,11 @@
import { PosConfig } from "@point_of_sale/../tests/unit/data/pos_config.data";
PosConfig._records = PosConfig._records.map((config) => {
if (config.id === 1) {
return {
...config,
down_payment_product_id: 105,
};
}
return config;
});

View file

@ -0,0 +1,15 @@
import { patch } from "@web/core/utils/patch";
import { PosOrderLine } from "@point_of_sale/../tests/unit/data/pos_order_line.data";
patch(PosOrderLine.prototype, {
_load_pos_data_fields() {
return [
...super._load_pos_data_fields(),
"sale_order_origin_id",
"sale_order_line_id",
"down_payment_details",
"settled_order_id",
"settled_invoice_id",
];
},
});

View file

@ -0,0 +1,8 @@
import { patch } from "@web/core/utils/patch";
import { PosSession } from "@point_of_sale/../tests/unit/data/pos_session.data";
patch(PosSession.prototype, {
_load_pos_data_models() {
return [...super._load_pos_data_models(), "sale.order", "sale.order.line"];
},
});

View file

@ -0,0 +1,17 @@
import { ProductProduct } from "@point_of_sale/../tests/unit/data/product_product.data";
ProductProduct._records = [
...ProductProduct._records,
{
id: 105,
product_tmpl_id: 105,
lst_price: 0,
standard_price: 0,
display_name: "Down Payment (POS)",
product_tag_ids: [],
barcode: false,
default_code: false,
product_template_attribute_value_ids: [],
product_template_variant_value_ids: [],
},
];

View file

@ -0,0 +1,46 @@
import { patch } from "@web/core/utils/patch";
import { ProductTemplate } from "@point_of_sale/../tests/unit/data/product_template.data";
patch(ProductTemplate.prototype, {
_load_pos_data_fields() {
return [...super._load_pos_data_fields(), "sale_line_warn_msg", "invoice_policy"];
},
});
ProductTemplate._records = [
...ProductTemplate._records,
{
id: 105,
display_name: "Down Payment (POS)",
standard_price: 0,
categ_id: false,
pos_categ_ids: [],
taxes_id: [],
barcode: false,
name: "Down Payment (POS)",
list_price: 0,
is_favorite: false,
default_code: false,
to_weight: false,
uom_id: 1,
description_sale: false,
description: false,
tracking: "none",
type: "service",
service_tracking: "no",
is_storable: false,
write_date: "2025-07-03 17:04:14",
color: 0,
pos_sequence: 5,
available_in_pos: true,
attribute_line_ids: [],
active: true,
image_128: false,
sequence: 1,
combo_ids: [],
product_variant_ids: [7],
public_description: false,
pos_optional_product_ids: [],
product_tag_ids: [],
},
];

View file

@ -0,0 +1,8 @@
import { patch } from "@web/core/utils/patch";
import { ResPartner as MailResPartner } from "@mail/../tests/mock_server/mock_models/res_partner";
patch(MailResPartner.prototype, {
_load_pos_data_fields() {
return [...super._load_pos_data_fields(), "sale_warn_msg"];
},
});

View file

@ -0,0 +1,69 @@
import { patch } from "@web/core/utils/patch";
import { hootPosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { models } from "@web/../tests/web_test_helpers";
export class SaleOrder extends models.ServerModel {
_name = "sale.order";
_load_pos_data_fields() {
return [
"name",
"state",
"user_id",
"order_line",
"partner_id",
"pricelist_id",
"fiscal_position_id",
"amount_total",
"amount_untaxed",
"amount_unpaid",
"picking_ids",
"partner_shipping_id",
"partner_invoice_id",
"date_order",
"write_date",
];
}
_records = [
{
id: 1,
name: "S00001",
state: "sale",
order_line: [1, 2],
partner_id: 3,
pricelist_id: 1,
fiscal_position_id: 1,
amount_total: 650,
amount_untaxed: 500,
amount_unpaid: 650,
partner_shipping_id: 3,
partner_invoice_id: 3,
date_order: "2025-07-03 17:04:14",
write_date: "2025-07-03 17:04:14",
},
];
async load_sale_order_from_pos(id, config_id) {
const order = this.env["sale.order"].find((order) => order.id === id);
const orderLines = this.env["sale.order.line"].filter((line) =>
order.order_line.includes(line.id)
);
const partner = this.env["res.partner"].find((partner) => partner.id === order.partner_id);
const productProducts = this.env["product.product"].filter((product) =>
orderLines.map((line) => line.product_id).includes(product.id)
);
const productTemplates = this.env["product.template"].filter((template) =>
productProducts.map((p) => p.product_tmpl_id).includes(template.id)
);
return {
"sale.order": [order],
"sale.order.line": orderLines,
"res.partner": [partner],
"product.product": productProducts,
"product.template": productTemplates,
};
}
}
patch(hootPosModels, [...hootPosModels, SaleOrder]);

View file

@ -0,0 +1,83 @@
import { patch } from "@web/core/utils/patch";
import { hootPosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { models, MockServer } from "@web/../tests/web_test_helpers";
export class SaleOrderLine extends models.ServerModel {
_name = "sale.order.line";
_load_pos_data_fields() {
return [
"discount",
"display_name",
"price_total",
"price_unit",
"product_id",
"product_uom_qty",
"qty_delivered",
"qty_invoiced",
"qty_to_invoice",
"display_type",
"name",
"tax_ids",
"is_downpayment",
"extra_tax_data",
"write_date",
"is_repair_line",
];
}
_records = [
{
id: 1,
display_name: "Product 1",
product_id: 5,
product_uom_qty: 5,
price_unit: 100,
price_total: 500,
discount: 0,
qty_delivered: 0,
qty_invoiced: 0,
qty_to_invoice: 5,
display_type: false,
name: "Product 1",
tax_ids: [],
is_downpayment: false,
extra_tax_data: {},
write_date: "2025-07-03 17:04:14",
},
{
id: 2,
display_name: "Product 2",
product_id: 6,
product_uom_qty: 3,
price_unit: 50,
price_total: 150,
discount: 0,
qty_delivered: 0,
qty_invoiced: 0,
qty_to_invoice: 3,
display_type: false,
name: "Product 2",
tax_ids: [],
is_downpayment: false,
extra_tax_data: {},
write_date: "2025-07-03 17:04:14",
},
];
async read_converted(ids) {
const model = MockServer.env[this._name];
const posFields = model._load_pos_data_fields();
const records = model.search_read(
[["id", "in", ids]],
posFields,
false,
false,
false,
false
);
return records;
}
}
patch(hootPosModels, [...hootPosModels, SaleOrderLine]);

View file

@ -0,0 +1,118 @@
import { test, expect, describe } from "@odoo/hoot";
import { setupPosEnv, getFilledOrder } from "@point_of_sale/../tests/unit/utils";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
definePosModels();
describe("saleDetails", () => {
test("down payment details as array", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const productDownPayment = store.models["product.template"].get(105);
const line = await store.addLineToOrder(
{
product_tmpl_id: productDownPayment,
down_payment_details: [
{
product_name: "Product 1",
product_uom_qty: 2,
total: 100,
},
{
product_name: "Product 2",
product_uom_qty: 1,
total: 50,
},
],
qty: 1,
},
order
);
const saleDetails = line.saleDetails;
expect(saleDetails).toEqual([
{
product_uom_qty: 2,
product_name: "Product 1",
total: "$\u00a0100.00",
},
{
product_uom_qty: 1,
product_name: "Product 2",
total: "$\u00a050.00",
},
]);
});
test("down payment details as stringified JSON", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const productDownPayment = store.models["product.template"].get(105);
const line = await store.addLineToOrder(
{
product_tmpl_id: productDownPayment,
down_payment_details: JSON.stringify([
{
product_name: "Product 1",
product_uom_qty: 2,
total: 100,
},
{
product_name: "Product 2",
product_uom_qty: 1,
total: 50,
},
]),
qty: 1,
},
order
);
const saleDetails = line.saleDetails;
expect(saleDetails).toEqual([
{
product_uom_qty: 2,
product_name: "Product 1",
total: "$\u00a0100.00",
},
{
product_uom_qty: 1,
product_name: "Product 2",
total: "$\u00a050.00",
},
]);
});
});
describe("setQuantityFromSOL", () => {
test("service product, state != sent/draft → qty_to_invoice", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
const line = order.lines[0];
line.product_id.type = "service";
line.sale_order_origin_id = { state: "sale" }; // not 'sent' or 'draft'
const saleOrderLine = { qty_to_invoice: 2 };
await line.setQuantityFromSOL(saleOrderLine);
expect(line.qty).toBe(2);
});
test("non-service product → qty = uom_qty - max(delivered, invoiced)", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
const line = order.lines[0];
line.product_id.type = "consu";
const saleOrderLine = {
product_uom_qty: 8,
qty_delivered: 1,
qty_invoiced: 2,
};
await line.setQuantityFromSOL(saleOrderLine);
expect(line.qty).toBe(6); // 8 - max(1,2)
});
});

View file

@ -0,0 +1,180 @@
import { test, expect, describe } from "@odoo/hoot";
import { setupPosEnv, getFilledOrder } from "@point_of_sale/../tests/unit/utils";
import { click, waitFor } from "@odoo/hoot-dom";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { Orderline } from "@point_of_sale/app/components/orderline/orderline";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
definePosModels();
describe("onClickSaleOrder", () => {
test("no selection → abort", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
await mountWithCleanup(ProductScreen, { props: { orderUuid: order.uuid } });
const promiseResult = store.onClickSaleOrder(1);
const button =
".modal-header:has(.modal-title:contains('What do you want to do?')) button[aria-label='Close']";
await waitFor(button);
await click(button);
await promiseResult;
const currentOrder = store.getOrder();
expect(currentOrder.id).toBe(order.id);
expect(currentOrder.lines.length).toBe(2);
expect(currentOrder.lines[0].product_id.id).toBe(5);
expect(currentOrder.lines[0].qty).toBe(3);
expect(currentOrder.lines[1].product_id.id).toBe(6);
expect(currentOrder.lines[1].qty).toBe(2);
});
test("settle → calls settleSO", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
await mountWithCleanup(ProductScreen, { props: { orderUuid: order.uuid } });
const promiseResult = store.onClickSaleOrder(1);
const button = ".modal-body button:contains('Settle the order')";
await waitFor(button);
await click(button);
await promiseResult;
const currentOrder = store.getOrder();
expect(currentOrder.id).toBe(order.id);
expect(currentOrder.lines.length).toBe(4);
expect(currentOrder.lines[0].product_id.id).toBe(5);
expect(currentOrder.lines[0].qty).toBe(3);
expect(currentOrder.lines[0].price_unit).toBe(3);
expect(currentOrder.lines[0].prices.total_excluded).toBe(9);
expect(currentOrder.lines[1].product_id.id).toBe(6);
expect(currentOrder.lines[1].qty).toBe(2);
expect(currentOrder.lines[1].price_unit).toBe(3);
expect(currentOrder.lines[1].prices.total_excluded).toBe(6);
expect(currentOrder.lines[2].product_id.id).toBe(5);
expect(currentOrder.lines[2].qty).toBe(5);
expect(currentOrder.lines[2].price_unit).toBe(100);
expect(currentOrder.lines[2].prices.total_excluded).toBe(500);
expect(currentOrder.lines[3].product_id.id).toBe(6);
expect(currentOrder.lines[3].qty).toBe(3);
expect(currentOrder.lines[3].price_unit).toBe(50);
expect(currentOrder.lines[3].prices.total_excluded).toBe(150);
});
test("dpPercentage → calls downPaymentSO", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
await mountWithCleanup(ProductScreen, { props: { orderUuid: order.uuid } });
const promiseResult = store.onClickSaleOrder(1);
const buttonDownPaymentPercentage =
".modal-body button:contains('Apply a down payment (percentage)')";
await waitFor(buttonDownPaymentPercentage);
await click(buttonDownPaymentPercentage);
await waitFor(".modal-title:contains('Down Payment')");
await click(".modal-body .numpad .numpad-button[value='+50']");
await new Promise((resolve) => setTimeout(resolve, 50));
await click(".modal-footer .btn:contains('Apply')");
await promiseResult;
const currentOrder = store.getOrder();
expect(currentOrder.id).toBe(order.id);
expect(currentOrder.lines.length).toBe(3);
expect(currentOrder.lines[0].product_id.id).toBe(5);
expect(currentOrder.lines[0].qty).toBe(3);
expect(currentOrder.lines[0].price_unit).toBe(3);
expect(currentOrder.lines[0].prices.total_excluded).toBe(9);
expect(currentOrder.lines[1].product_id.id).toBe(6);
expect(currentOrder.lines[1].qty).toBe(2);
expect(currentOrder.lines[1].price_unit).toBe(3);
expect(currentOrder.lines[1].prices.total_excluded).toBe(6);
expect(currentOrder.lines[2].product_id.id).toBe(105);
expect(currentOrder.lines[2].qty).toBe(1);
expect(currentOrder.lines[2].price_unit).toBe(325);
expect(currentOrder.lines[2].prices.total_excluded).toBe(325);
const comp = await mountWithCleanup(Orderline, {
props: { line: currentOrder.lines[2] },
});
const saleOrderInfo = ".orderline .info-list .sale-order-info";
const cell = (tr, td) => `${saleOrderInfo} tr:nth-child(${tr}) td:nth-child(${td})`;
expect(comp.line).toEqual(currentOrder.lines[2]);
expect(`${saleOrderInfo} tr`).toHaveCount(4);
expect(cell(1, 1)).toHaveText("5x");
expect(cell(1, 2)).toHaveText("TEST");
expect(cell(1, 4)).toHaveText(`$ 500.00 (tax incl.)`);
expect(cell(2, 1)).toHaveText("3x");
expect(cell(2, 2)).toHaveText("TEST 2");
expect(cell(2, 4)).toHaveText(`$ 150.00 (tax incl.)`);
});
test("dpAmount → calls downPaymentSO", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
await mountWithCleanup(ProductScreen, { props: { orderUuid: order.uuid } });
const promiseResult = store.onClickSaleOrder(1);
const buttonDownPaymentPercentage =
".modal-body button:contains('Apply a down payment (fixed amount)')";
await waitFor(buttonDownPaymentPercentage);
await click(buttonDownPaymentPercentage);
await waitFor(".modal-title:contains('Down Payment')");
await click(".modal-body .numpad .numpad-button[value='+50']");
await new Promise((resolve) => setTimeout(resolve, 50));
await click(".modal-footer .btn:contains('Apply')");
await promiseResult;
const currentOrder = store.getOrder();
expect(currentOrder.id).toBe(order.id);
expect(currentOrder.lines.length).toBe(3);
expect(currentOrder.lines[0].product_id.id).toBe(5);
expect(currentOrder.lines[0].qty).toBe(3);
expect(currentOrder.lines[0].price_unit).toBe(3);
expect(currentOrder.lines[0].prices.total_excluded).toBe(9);
expect(currentOrder.lines[1].product_id.id).toBe(6);
expect(currentOrder.lines[1].qty).toBe(2);
expect(currentOrder.lines[1].price_unit).toBe(3);
expect(currentOrder.lines[1].prices.total_excluded).toBe(6);
expect(currentOrder.lines[2].product_id.id).toBe(105);
expect(currentOrder.lines[2].qty).toBe(1);
expect(currentOrder.lines[2].price_unit).toBe(50);
expect(currentOrder.lines[2].prices.total_excluded).toBe(50);
const comp = await mountWithCleanup(Orderline, {
props: { line: currentOrder.lines[2] },
});
const saleOrderInfo = ".orderline .info-list .sale-order-info";
const cell = (tr, td) => `${saleOrderInfo} tr:nth-child(${tr}) td:nth-child(${td})`;
expect(comp.line).toEqual(currentOrder.lines[2]);
expect(`${saleOrderInfo} tr`).toHaveCount(4);
expect(cell(1, 1)).toHaveText("5x");
expect(cell(1, 2)).toHaveText("TEST");
expect(cell(1, 4)).toHaveText(`$ 500.00 (tax incl.)`);
expect(cell(2, 1)).toHaveText("3x");
expect(cell(2, 2)).toHaveText("TEST 2");
expect(cell(2, 4)).toHaveText(`$ 150.00 (tax incl.)`);
});
});