mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 04:52:04 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -0,0 +1,46 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { expectFormattedPrice, setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { getFilledOrderForPriceCheck } from "./utils";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("Taxes object should contain no discount values", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
order.lines[0].setDiscount(10);
|
||||
order.lines[1].setDiscount(20);
|
||||
|
||||
const details = order.prices.taxDetails;
|
||||
const line1 = order.lines[0].prices;
|
||||
const line2 = order.lines[1].prices;
|
||||
|
||||
// Order prices
|
||||
expect(details.base_amount).toBe(980); // Base amount is 980 = (1000 - 10%) + (100 - 20%)
|
||||
expect(details.tax_amount).toBe(257); // Tax amount is 257 = (250 - 10%) + (15 - 20%) + (25 - 20%)
|
||||
expect(details.total_amount).toBe(1237); // Total amount is 1237 = 980 + 257
|
||||
|
||||
// Formatted prices
|
||||
expectFormattedPrice(order.currencyDisplayPrice, "$ 1,237.00");
|
||||
expectFormattedPrice(order.currencyAmountTaxes, "$ 257.00");
|
||||
expectFormattedPrice(order.lines[0].currencyDisplayPrice, "$ 1,125.00");
|
||||
expectFormattedPrice(order.lines[0].currencyDisplayPriceUnit, "$ 1,125.00");
|
||||
expectFormattedPrice(order.lines[0].currencyDisplayPriceUnitExcl, "$ 900.00");
|
||||
expectFormattedPrice(order.lines[1].currencyDisplayPrice, "$ 112.00");
|
||||
expectFormattedPrice(order.lines[1].currencyDisplayPriceUnit, "$ 112.00");
|
||||
expectFormattedPrice(order.lines[1].currencyDisplayPriceUnitExcl, "$ 80.00");
|
||||
|
||||
// First line (25% on 1000) - no discount
|
||||
expect(line1.no_discount_total_included).toBe(1250);
|
||||
expect(line1.no_discount_total_excluded).toBe(1000);
|
||||
expect(line1.no_discount_taxes_data[0].tax_amount).toBe(250);
|
||||
expect(line1.no_discount_taxes_data[0].tax.amount).toBe(25);
|
||||
|
||||
// Second line (15% + 25% on 100) - no discount
|
||||
expect(line2.no_discount_total_included).toBe(140);
|
||||
expect(line2.no_discount_total_excluded).toBe(100);
|
||||
expect(line2.no_discount_taxes_data[0].tax_amount).toBe(15);
|
||||
expect(line2.no_discount_taxes_data[0].tax.amount).toBe(15);
|
||||
expect(line2.no_discount_taxes_data[1].tax_amount).toBe(25);
|
||||
expect(line2.no_discount_taxes_data[1].tax.amount).toBe(25);
|
||||
});
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
/**
|
||||
* This file contains old tour tests related to accounting that were migrated to Hoot.
|
||||
* These tours were not checking anything on the Python side, so they were simply
|
||||
* converted to Hoot tests without any additional checks.
|
||||
*/
|
||||
|
||||
import { test, expect } from "@odoo/hoot";
|
||||
import { setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { prepareRoundingVals } from "./utils";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("[Old Tour] pos_basic_order_01_multi_payment_and_change", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const product1 = store.models["product.template"].get(15);
|
||||
product1.list_price = 5.1;
|
||||
product1.product_variant_ids[0].lst_price = 5.1;
|
||||
product1.taxes_id = [];
|
||||
|
||||
const cashPm = store.models["pos.payment.method"].find((pm) => pm.is_cash_count);
|
||||
const cardPm = store.models["pos.payment.method"].find((pm) => !pm.is_cash_count);
|
||||
const order = store.addNewOrder();
|
||||
order.pricelist_id = false;
|
||||
|
||||
// Add products
|
||||
await store.addLineToOrder({ product_tmpl_id: product1, qty: 2 }, order);
|
||||
order.addPaymentline(cashPm);
|
||||
order.payment_ids[0].setAmount(5);
|
||||
expect(order.remainingDue).toBe(5.2);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
order.payment_ids[1].setAmount(6);
|
||||
expect(order.change).toBe(-0.8);
|
||||
});
|
||||
|
||||
test("[Old Tour] PaymentScreenRoundingHalfUp", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const product1_2 = store.models["product.template"].get(12);
|
||||
const product1_25 = store.models["product.template"].get(13);
|
||||
const product1_40 = store.models["product.template"].get(14);
|
||||
const { cashPm } = prepareRoundingVals(store, 0.5, "HALF-UP", true);
|
||||
|
||||
product1_2.list_price = 1.2;
|
||||
product1_2.product_variant_ids[0].lst_price = 1.2;
|
||||
product1_2.taxes_id = [];
|
||||
product1_25.list_price = 1.25;
|
||||
product1_25.product_variant_ids[0].lst_price = 1.25;
|
||||
product1_25.taxes_id = [];
|
||||
product1_40.list_price = 1.4;
|
||||
product1_40.product_variant_ids[0].lst_price = 1.4;
|
||||
product1_40.taxes_id = [];
|
||||
|
||||
const order = store.addNewOrder();
|
||||
order.pricelist_id = false;
|
||||
await store.addLineToOrder({ product_tmpl_id: product1_2, qty: 1 }, order);
|
||||
expect(order.totalDue).toBe(1.2);
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.amountPaid).toBe(1.0);
|
||||
expect(order.appliedRounding).toBe(-0.2);
|
||||
expect(order.change).toBe(0.0);
|
||||
|
||||
const order2 = store.addNewOrder();
|
||||
order2.pricelist_id = false;
|
||||
await store.addLineToOrder({ product_tmpl_id: product1_25, qty: 1 }, order2);
|
||||
expect(order2.totalDue).toBe(1.25);
|
||||
order2.addPaymentline(cashPm);
|
||||
expect(order2.amountPaid).toBe(1.5);
|
||||
expect(order2.appliedRounding).toBe(0.25);
|
||||
expect(order2.change).toBe(0.0);
|
||||
|
||||
const order3 = store.addNewOrder();
|
||||
order3.pricelist_id = false;
|
||||
await store.addLineToOrder({ product_tmpl_id: product1_40, qty: 1 }, order3);
|
||||
expect(order3.totalDue).toBe(1.4);
|
||||
order3.addPaymentline(cashPm);
|
||||
expect(order3.amountPaid).toBe(1.5);
|
||||
expect(order3.appliedRounding).toBe(0.1);
|
||||
expect(order3.change).toBe(0.0);
|
||||
|
||||
const order4 = store.addNewOrder();
|
||||
order4.pricelist_id = false;
|
||||
await store.addLineToOrder({ product_tmpl_id: product1_2, qty: 1 }, order4);
|
||||
expect(order4.totalDue).toBe(1.2);
|
||||
order4.addPaymentline(cashPm);
|
||||
order4.payment_ids[0].setAmount(2);
|
||||
expect(order4.amountPaid).toBe(2.0);
|
||||
expect(order4.change).toBe(-1.0);
|
||||
});
|
||||
|
||||
const prepareProduct = (store) => {
|
||||
const product = store.models["product.template"].get(15);
|
||||
const tax15 = store.models["account.tax"].get(1);
|
||||
product.list_price = 13.67;
|
||||
product.product_variant_ids[0].lst_price = 13.67;
|
||||
product.taxes_id = [tax15];
|
||||
return product;
|
||||
};
|
||||
|
||||
test("[Old Tour] test_cash_rounding_halfup_add_invoice_line_not_only_round_cash_method", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
|
||||
const product = prepareProduct(store);
|
||||
const order = store.addNewOrder();
|
||||
order.pricelist_id = false;
|
||||
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
|
||||
expect(order.displayPrice).toBe(15.72);
|
||||
expect(order.priceExcl).toBe(13.67);
|
||||
expect(order.totalDue).toBe(15.7);
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.amountPaid).toBe(15.7);
|
||||
expect(order.appliedRounding).toBe(-0.02);
|
||||
expect(order.change).toBe(0.0);
|
||||
|
||||
const order2 = store.addNewOrder();
|
||||
order2.pricelist_id = false;
|
||||
order.is_refund = true;
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
|
||||
expect(order2.displayPrice).toBe(-15.72);
|
||||
expect(order2.priceExcl).toBe(-13.67);
|
||||
expect(order2.totalDue).toBe(-15.7);
|
||||
order2.addPaymentline(cardPm);
|
||||
expect(order2.amountPaid).toBe(-15.7);
|
||||
expect(order2.appliedRounding).toBe(0.02);
|
||||
expect(order2.change).toBe(0.0);
|
||||
});
|
||||
|
||||
test("[Old Tour] test_cash_rounding_halfup_add_invoice_line_not_only_round_cash_method_pay_by_bank_and_cash", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
|
||||
const product = prepareProduct(store);
|
||||
const order = store.addNewOrder();
|
||||
order.pricelist_id = false;
|
||||
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
|
||||
expect(order.displayPrice).toBe(15.72);
|
||||
expect(order.priceExcl).toBe(13.67);
|
||||
expect(order.totalDue).toBe(15.7);
|
||||
order.addPaymentline(cardPm);
|
||||
order.payment_ids[0].setAmount(0.68);
|
||||
expect(order.amountPaid).toBe(0.68);
|
||||
expect(order.remainingDue).toBe(15.02); // Order is rounded globaly so remaining due is rounded
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[1].amount).toBe(15.02);
|
||||
expect(order.amountPaid).toBe(15.7);
|
||||
expect(order.appliedRounding).toBe(-0.02);
|
||||
expect(order.change).toBe(0.0);
|
||||
|
||||
const order2 = store.addNewOrder();
|
||||
order2.pricelist_id = false;
|
||||
order2.is_refund = true;
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
|
||||
expect(order2.displayPrice).toBe(-15.72);
|
||||
expect(order2.priceExcl).toBe(-13.67);
|
||||
expect(order2.totalDue).toBe(-15.7);
|
||||
order2.addPaymentline(cardPm);
|
||||
order2.payment_ids[0].setAmount(-0.68);
|
||||
expect(order2.amountPaid).toBe(-0.68);
|
||||
expect(order2.remainingDue).toBe(-15.02); // Order is rounded globaly so remaining due is rounded
|
||||
order2.addPaymentline(cashPm);
|
||||
expect(order2.payment_ids[1].amount).toBe(-15.02);
|
||||
expect(order2.amountPaid).toBe(-15.7);
|
||||
expect(order2.appliedRounding).toBe(0.02);
|
||||
expect(order2.change).toBe(0.0);
|
||||
});
|
||||
|
||||
test("[Old Tour] test_cash_rounding_down_add_invoice_line_not_only_round_cash_method_no_rounding_left", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
|
||||
const product = prepareProduct(store);
|
||||
const order = store.addNewOrder();
|
||||
order.pricelist_id = false;
|
||||
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
|
||||
expect(order.displayPrice).toBe(15.72);
|
||||
expect(order.priceExcl).toBe(13.67);
|
||||
expect(order.totalDue).toBe(15.7);
|
||||
order.addPaymentline(cardPm);
|
||||
order.payment_ids[0].setAmount(0.67);
|
||||
expect(order.amountPaid).toBe(0.67);
|
||||
expect(order.remainingDue).toBe(15.03);
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[1].amount).toBe(15.03);
|
||||
expect(order.amountPaid).toBe(15.7);
|
||||
expect(order.appliedRounding).toBe(-0.02);
|
||||
expect(order.change).toBe(0.0);
|
||||
|
||||
const order2 = store.addNewOrder();
|
||||
order2.pricelist_id = false;
|
||||
order2.is_refund = true;
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
|
||||
expect(order2.displayPrice).toBe(-15.72);
|
||||
expect(order2.priceExcl).toBe(-13.67);
|
||||
expect(order2.totalDue).toBe(-15.7);
|
||||
order2.addPaymentline(cardPm);
|
||||
order2.payment_ids[0].setAmount(-0.67);
|
||||
expect(order2.amountPaid).toBe(-0.67);
|
||||
expect(order2.remainingDue).toBe(-15.03);
|
||||
order2.addPaymentline(cardPm);
|
||||
expect(order2.payment_ids[1].amount).toBe(-15.03);
|
||||
expect(order2.amountPaid).toBe(-15.7);
|
||||
expect(order2.appliedRounding).toBe(0.02);
|
||||
expect(order2.change).toBe(0.0);
|
||||
});
|
||||
|
||||
test("[Old Tour] test_cash_rounding_halfup_add_invoice_line_only_round_cash_method", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
|
||||
const product = prepareProduct(store);
|
||||
const order = store.addNewOrder();
|
||||
order.pricelist_id = false;
|
||||
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
|
||||
expect(order.displayPrice).toBe(15.72);
|
||||
expect(order.priceExcl).toBe(13.67);
|
||||
expect(order.totalDue).toBe(15.72);
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.amountPaid).toBe(15.7);
|
||||
expect(order.appliedRounding).toBe(-0.02);
|
||||
expect(order.change).toBe(0.0);
|
||||
|
||||
const order2 = store.addNewOrder();
|
||||
order2.pricelist_id = false;
|
||||
order2.is_refund = true;
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
|
||||
expect(order2.displayPrice).toBe(-15.72);
|
||||
expect(order2.priceExcl).toBe(-13.67);
|
||||
expect(order2.totalDue).toBe(-15.72);
|
||||
order2.addPaymentline(cashPm);
|
||||
expect(order2.amountPaid).toBe(-15.7);
|
||||
expect(order2.appliedRounding).toBe(0.02);
|
||||
expect(order2.change).toBe(0.0);
|
||||
});
|
||||
|
||||
test("[Old Tour] test_cash_rounding_halfup_add_invoice_line_only_round_cash_method_pay_by_bank_and_cash", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
|
||||
const product = prepareProduct(store);
|
||||
const order = store.addNewOrder();
|
||||
order.pricelist_id = false;
|
||||
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
|
||||
expect(order.displayPrice).toBe(15.72);
|
||||
expect(order.priceExcl).toBe(13.67);
|
||||
expect(order.totalDue).toBe(15.72);
|
||||
order.addPaymentline(cardPm);
|
||||
order.payment_ids[0].setAmount(0.68);
|
||||
expect(order.amountPaid).toBe(0.68);
|
||||
expect(order.remainingDue).toBe(15.04);
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[1].amount).toBe(15.05);
|
||||
expect(order.amountPaid).toBe(15.73);
|
||||
expect(order.appliedRounding).toBe(0.01);
|
||||
expect(order.change).toBe(0.0);
|
||||
|
||||
const order2 = store.addNewOrder();
|
||||
order2.pricelist_id = false;
|
||||
order2.is_refund = true;
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
|
||||
expect(order2.displayPrice).toBe(-15.72);
|
||||
expect(order2.priceExcl).toBe(-13.67);
|
||||
expect(order2.totalDue).toBe(-15.72);
|
||||
order2.addPaymentline(cardPm);
|
||||
order2.payment_ids[0].setAmount(-0.68);
|
||||
expect(order2.amountPaid).toBe(-0.68);
|
||||
expect(order2.remainingDue).toBe(-15.04);
|
||||
order2.addPaymentline(cashPm);
|
||||
expect(order2.payment_ids[1].amount).toBe(-15.05);
|
||||
expect(order2.amountPaid).toBe(-15.73);
|
||||
expect(order2.appliedRounding).toBe(-0.01);
|
||||
expect(order2.change).toBe(0.0);
|
||||
});
|
||||
|
||||
test("[Old Tour] test_cash_rounding_with_change", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
|
||||
const product = prepareProduct(store);
|
||||
const order = store.addNewOrder();
|
||||
order.pricelist_id = false;
|
||||
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
|
||||
expect(order.displayPrice).toBe(15.72);
|
||||
expect(order.priceExcl).toBe(13.67);
|
||||
expect(order.totalDue).toBe(15.7);
|
||||
order.addPaymentline(cardPm);
|
||||
order.payment_ids[0].setAmount(20);
|
||||
expect(order.amountPaid).toBe(20);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(-4.3);
|
||||
|
||||
const order2 = store.addNewOrder();
|
||||
order2.pricelist_id = false;
|
||||
order2.is_refund = true;
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
|
||||
expect(order2.displayPrice).toBe(-15.72);
|
||||
expect(order2.priceExcl).toBe(-13.67);
|
||||
expect(order2.totalDue).toBe(-15.7);
|
||||
order2.addPaymentline(cardPm);
|
||||
order2.payment_ids[0].setAmount(-20);
|
||||
expect(order2.amountPaid).toBe(-20);
|
||||
expect(order2.appliedRounding).toBe(0);
|
||||
expect(order2.change).toBe(4.3);
|
||||
});
|
||||
|
||||
test("[Old Tour] test_cash_rounding_only_cash_method_with_change", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
|
||||
const product = prepareProduct(store);
|
||||
const order = store.addNewOrder();
|
||||
order.pricelist_id = false;
|
||||
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: 1 }, order);
|
||||
expect(order.displayPrice).toBe(15.72);
|
||||
expect(order.priceExcl).toBe(13.67);
|
||||
expect(order.totalDue).toBe(15.72);
|
||||
order.addPaymentline(cashPm);
|
||||
order.payment_ids[0].setAmount(20);
|
||||
expect(order.amountPaid).toBe(20);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(-4.3);
|
||||
|
||||
const order2 = store.addNewOrder();
|
||||
order2.pricelist_id = false;
|
||||
order2.is_refund = true;
|
||||
await store.addLineToOrder({ product_tmpl_id: product, qty: -1 }, order2);
|
||||
expect(order2.displayPrice).toBe(-15.72);
|
||||
expect(order2.priceExcl).toBe(-13.67);
|
||||
expect(order2.totalDue).toBe(-15.72);
|
||||
order2.addPaymentline(cashPm);
|
||||
order2.payment_ids[0].setAmount(-20);
|
||||
expect(order2.amountPaid).toBe(-20);
|
||||
expect(order2.appliedRounding).toBe(0);
|
||||
expect(order2.change).toBe(4.3);
|
||||
});
|
||||
|
||||
test(["[Old Tour] test_cash_rounding_up_with_change"], async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm } = prepareRoundingVals(store, 1, "UP", true);
|
||||
const order = store.addNewOrder();
|
||||
order.pricelist_id = false;
|
||||
|
||||
const tax = store.models["account.tax"].get(3);
|
||||
const productA = store.models["product.template"].get(15);
|
||||
const productB = store.models["product.template"].get(16);
|
||||
productA.list_price = 95;
|
||||
productA.product_variant_ids[0].lst_price = 95;
|
||||
productA.taxes_id = [tax];
|
||||
productB.list_price = 42;
|
||||
productB.product_variant_ids[0].lst_price = 42;
|
||||
productB.taxes_id = [tax];
|
||||
|
||||
await store.addLineToOrder({ product_tmpl_id: productA, qty: 1 }, order);
|
||||
await store.addLineToOrder({ product_tmpl_id: productB, qty: 2 }, order);
|
||||
|
||||
expect(order.displayPrice).toBe(179);
|
||||
expect(order.totalDue).toBe(179);
|
||||
order.addPaymentline(cashPm);
|
||||
order.payment_ids[0].setAmount(200);
|
||||
expect(order.amountPaid).toBe(200);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(-21);
|
||||
});
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { expectFormattedPrice, setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { getFilledOrderForPriceCheck } from "./utils";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("Prices includes", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
const details = order.prices.taxDetails;
|
||||
const line1 = order.lines[0].prices;
|
||||
const line2 = order.lines[1].prices;
|
||||
|
||||
// Order prices
|
||||
expect(details.base_amount).toBe(1100);
|
||||
expect(details.tax_amount).toBe(290);
|
||||
expect(details.total_amount).toBe(1390);
|
||||
|
||||
// First line (25% on 1000)
|
||||
expect(line1.total_included).toBe(1250);
|
||||
expect(line1.total_excluded).toBe(1000);
|
||||
expect(line1.taxes_data[0].tax_amount).toBe(250);
|
||||
expect(line1.taxes_data[0].tax.amount).toBe(25);
|
||||
|
||||
// Second line (15% + 25% on 100)
|
||||
expect(line2.total_included).toBe(140);
|
||||
expect(line2.total_excluded).toBe(100);
|
||||
expect(line2.taxes_data[0].tax_amount).toBe(15);
|
||||
expect(line2.taxes_data[0].tax.amount).toBe(15);
|
||||
expect(line2.taxes_data[1].tax_amount).toBe(25);
|
||||
expect(line2.taxes_data[1].tax.amount).toBe(25);
|
||||
|
||||
// Formatted prices
|
||||
expectFormattedPrice(order.currencyDisplayPrice, "$ 1,390.00");
|
||||
expectFormattedPrice(order.currencyAmountTaxes, "$ 290.00");
|
||||
expectFormattedPrice(order.lines[0].currencyDisplayPrice, "$ 1,250.00");
|
||||
expectFormattedPrice(order.lines[0].currencyDisplayPriceUnit, "$ 1,250.00");
|
||||
expectFormattedPrice(order.lines[0].currencyDisplayPriceUnitExcl, "$ 1,000.00");
|
||||
expectFormattedPrice(order.lines[1].currencyDisplayPrice, "$ 140.00");
|
||||
expectFormattedPrice(order.lines[1].currencyDisplayPriceUnit, "$ 140.00");
|
||||
expectFormattedPrice(order.lines[1].currencyDisplayPriceUnitExcl, "$ 100.00");
|
||||
});
|
||||
|
||||
test("Prices excludes", async () => {
|
||||
const store = await setupPosEnv();
|
||||
store.config.iface_tax_included = "subtotal";
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
// Formatted prices
|
||||
expectFormattedPrice(order.currencyDisplayPrice, "$ 1,100.00");
|
||||
expectFormattedPrice(order.lines[0].currencyDisplayPrice, "$ 1,000.00");
|
||||
expectFormattedPrice(order.lines[0].currencyDisplayPriceUnit, "$ 1,000.00");
|
||||
expectFormattedPrice(order.lines[1].currencyDisplayPrice, "$ 100.00");
|
||||
expectFormattedPrice(order.lines[1].currencyDisplayPriceUnit, "$ 100.00");
|
||||
});
|
||||
|
||||
test("Combo prices incl and excl", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
|
||||
const template = store.models["product.template"].get(7);
|
||||
const comboProduct = store.models["product.combo.item"].get(1);
|
||||
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: template,
|
||||
payload: [[{ combo_item_id: comboProduct, qty: 1 }]],
|
||||
qty: 1,
|
||||
},
|
||||
order
|
||||
);
|
||||
order.setOrderPrices();
|
||||
|
||||
const [comboParentLine, comboChildLine] = order.lines;
|
||||
|
||||
expect(comboParentLine.comboTotalPrice).toBe(3.75);
|
||||
expect(comboParentLine.comboTotalPriceWithoutTax).toBe(3);
|
||||
|
||||
expect(comboChildLine.comboTotalPrice).toBe(3.75);
|
||||
expect(comboChildLine.comboTotalPriceWithoutTax).toBe(3);
|
||||
});
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("Pricelist: Precedence Rules (Variant > Template > Category > Global)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const pricelist = store.models["product.pricelist"].create({
|
||||
name: "Test Pricelist",
|
||||
});
|
||||
|
||||
const category = store.models["product.category"].create({ name: "Test Category" });
|
||||
const productTemplate = store.models["product.template"].create({
|
||||
name: "Test Template",
|
||||
list_price: 100,
|
||||
categ_id: category,
|
||||
});
|
||||
const product = store.models["product.product"].create({
|
||||
product_tmpl_id: productTemplate,
|
||||
lst_price: 100,
|
||||
});
|
||||
|
||||
// 1. Global Rule
|
||||
const globalRule = store.models["product.pricelist.item"].create({
|
||||
pricelist_id: pricelist,
|
||||
compute_price: "fixed",
|
||||
fixed_price: 90,
|
||||
});
|
||||
pricelist.update({ item_ids: [globalRule] });
|
||||
pricelist.computeRuleIndexes();
|
||||
expect(product.getPrice(pricelist, 1, 0, false, product)).toBe(90);
|
||||
|
||||
// 2. Category Rule (should win over Global)
|
||||
const categoryRule = store.models["product.pricelist.item"].create({
|
||||
pricelist_id: pricelist,
|
||||
categ_id: category,
|
||||
compute_price: "fixed",
|
||||
fixed_price: 80,
|
||||
});
|
||||
pricelist.update({ item_ids: [globalRule, categoryRule] });
|
||||
pricelist.computeRuleIndexes();
|
||||
expect(product.getPrice(pricelist, 1, 0, false, product)).toBe(80);
|
||||
|
||||
// 3. Template Rule (should win over Category)
|
||||
const templateRule = store.models["product.pricelist.item"].create({
|
||||
pricelist_id: pricelist,
|
||||
product_tmpl_id: productTemplate,
|
||||
compute_price: "fixed",
|
||||
fixed_price: 70,
|
||||
});
|
||||
pricelist.update({ item_ids: [globalRule, categoryRule, templateRule] });
|
||||
pricelist.computeRuleIndexes();
|
||||
expect(product.getPrice(pricelist, 1, 0, false, product)).toBe(70);
|
||||
|
||||
// 4. Variant Rule (should win over Template)
|
||||
const variantRule = store.models["product.pricelist.item"].create({
|
||||
pricelist_id: pricelist,
|
||||
product_id: product,
|
||||
compute_price: "fixed",
|
||||
fixed_price: 60,
|
||||
});
|
||||
pricelist.update({ item_ids: [globalRule, categoryRule, templateRule, variantRule] });
|
||||
pricelist.computeRuleIndexes();
|
||||
expect(product.getPrice(pricelist, 1, 0, false, product)).toBe(60);
|
||||
});
|
||||
|
||||
test("Pricelist: Min Quantity logic", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const pricelist = store.models["product.pricelist"].create({
|
||||
name: "Qty Pricelist",
|
||||
});
|
||||
|
||||
const productTemplate = store.models["product.template"].create({
|
||||
name: "Qty Product",
|
||||
list_price: 100,
|
||||
});
|
||||
const product = store.models["product.product"].create({
|
||||
product_tmpl_id: productTemplate,
|
||||
lst_price: 100,
|
||||
});
|
||||
|
||||
const ruleSmall = store.models["product.pricelist.item"].create({
|
||||
pricelist_id: pricelist,
|
||||
product_id: product,
|
||||
compute_price: "fixed",
|
||||
fixed_price: 50,
|
||||
min_quantity: 0,
|
||||
});
|
||||
const ruleLarge = store.models["product.pricelist.item"].create({
|
||||
pricelist_id: pricelist,
|
||||
product_id: product,
|
||||
compute_price: "fixed",
|
||||
fixed_price: 20,
|
||||
min_quantity: 10,
|
||||
});
|
||||
|
||||
pricelist.update({ item_ids: [ruleSmall, ruleLarge] });
|
||||
pricelist.computeRuleIndexes();
|
||||
|
||||
// Qty 1 -> should use ruleSmall (50)
|
||||
expect(product.getPrice(pricelist, 1, 0, false, product)).toBe(50);
|
||||
// Qty 10 -> should use ruleLarge (20)
|
||||
expect(product.getPrice(pricelist, 10, 0, false, product)).toBe(20);
|
||||
// Qty 15 -> should use ruleLarge (20)
|
||||
expect(product.getPrice(pricelist, 15, 0, false, product)).toBe(20);
|
||||
});
|
||||
|
||||
test("Pricelist: Nested Pricelists (Pricelist of Pricelist)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
|
||||
// Base Pricelist: -10% discount
|
||||
const basePricelist = store.models["product.pricelist"].create({
|
||||
name: "Base Pricelist",
|
||||
});
|
||||
const baseRule = store.models["product.pricelist.item"].create({
|
||||
pricelist_id: basePricelist,
|
||||
compute_price: "percentage",
|
||||
percent_price: 10,
|
||||
base: "list_price",
|
||||
});
|
||||
basePricelist.update({ item_ids: [baseRule] });
|
||||
basePricelist.computeRuleIndexes();
|
||||
|
||||
// Nested Pricelist: Base + another -5$ surcharge
|
||||
const nestedPricelist = store.models["product.pricelist"].create({
|
||||
name: "Nested Pricelist",
|
||||
});
|
||||
const nestedRule = store.models["product.pricelist.item"].create({
|
||||
pricelist_id: nestedPricelist,
|
||||
compute_price: "formula", // formula to use base + surcharge
|
||||
base: "pricelist",
|
||||
base_pricelist_id: basePricelist,
|
||||
price_surcharge: 5,
|
||||
});
|
||||
nestedPricelist.update({ item_ids: [nestedRule] });
|
||||
nestedPricelist.computeRuleIndexes();
|
||||
|
||||
const productTemplate = store.models["product.template"].create({
|
||||
name: "Nested Test Product",
|
||||
list_price: 100,
|
||||
});
|
||||
const product = store.models["product.product"].create({
|
||||
product_tmpl_id: productTemplate,
|
||||
lst_price: 100,
|
||||
});
|
||||
|
||||
// Calculation: (100 - 10%) + 5 = 90 + 5 = 95
|
||||
expect(product.getPrice(nestedPricelist, 1, 0, false, product)).toBe(95);
|
||||
});
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { getFilledOrderForPriceCheck, prepareRoundingVals } from "./utils";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("Rounding sale HALF-UP 0.05 (cash only)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
expect(order.displayPrice).toBe(52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(52.54);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(52.55);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0.01);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
||||
test("Rounding sale HALF-UP 0.05 (all methods)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
expect(order.displayPrice).toBe(52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(52.55);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0.01);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(52.55);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0.01);
|
||||
expect(order.change).toBe(0);
|
||||
|
||||
order.payment_ids[0].delete();
|
||||
order.addPaymentline(cashPm);
|
||||
order.payment_ids[0].setAmount(52.5);
|
||||
expect(order.payment_ids[0].amount).toBe(52.5);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.remainingDue).toBe(0.05);
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
});
|
||||
|
||||
test("Rounding sale UP 10 (cash only)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 10, "UP", true);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
expect(order.displayPrice).toBe(52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(52.54);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(60);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(7.46);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
||||
test("Rounding sale UP 10 (all methods)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 10, "UP", false);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
expect(order.displayPrice).toBe(52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(60);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(7.46);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(60);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(7.46);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(60);
|
||||
order.payment_ids[0].setAmount(70);
|
||||
expect(order.payment_ids[0].amount).toBe(70);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(-10);
|
||||
});
|
||||
|
||||
test("Rounding sale DOWN 10 (all methods)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 10, "DOWN", false);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
expect(order.displayPrice).toBe(52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(50);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-2.54);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(50);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-2.54);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(50);
|
||||
order.payment_ids[0].setAmount(70);
|
||||
expect(order.payment_ids[0].amount).toBe(70);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(-20);
|
||||
});
|
||||
|
||||
test("Rounding sale DOWN 1 (cash only)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 1, "DOWN", true);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
expect(order.displayPrice).toBe(52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(52.54);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(52);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-0.54);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
||||
test("Rounding sale DOWN 1 (all methods)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 1, "DOWN", false);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
expect(order.displayPrice).toBe(52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(52);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-0.54);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(52);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-0.54);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
||||
test("Rounding refund HALF-UP 0.05 (cash only)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
order.is_refund = true;
|
||||
order.lines.map((line) => line.setQuantity(-line.qty));
|
||||
|
||||
expect(order.displayPrice).toBe(-52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-52.54);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-52.55);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-0.01);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
||||
test("Rounding refund HALF-UP 0.05 (all methods)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
order.is_refund = true;
|
||||
order.lines.map((line) => line.setQuantity(-line.qty));
|
||||
|
||||
expect(order.displayPrice).toBe(-52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-52.55);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-0.01);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-52.55);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-0.01);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
||||
test("Rounding refund UP 10 (cash only)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 10, "UP", true);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
order.is_refund = true;
|
||||
order.lines.map((line) => line.setQuantity(-line.qty));
|
||||
|
||||
expect(order.displayPrice).toBe(-52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-52.54);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-60);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-7.46);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
||||
test("Rounding refund UP 10 (all methods)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 10, "UP", false);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
order.is_refund = true;
|
||||
order.lines.map((line) => line.setQuantity(-line.qty));
|
||||
|
||||
expect(order.displayPrice).toBe(-52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-60);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-7.46);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-60);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(-7.46);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
||||
test("Rounding refund DOWN 1 (cash only)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 1, "DOWN", true);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
order.is_refund = true;
|
||||
order.lines.map((line) => line.setQuantity(-line.qty));
|
||||
|
||||
expect(order.displayPrice).toBe(-52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-52.54);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-52);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0.54);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
||||
test("Rounding refund DOWN 1 (all methods)", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 1, "DOWN", false);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
order.is_refund = true;
|
||||
order.lines.map((line) => line.setQuantity(-line.qty));
|
||||
|
||||
expect(order.displayPrice).toBe(-52.54);
|
||||
|
||||
order.addPaymentline(cardPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-52);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0.54);
|
||||
expect(order.change).toBe(0);
|
||||
order.payment_ids[0].delete();
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[0].amount).toBe(-52);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0.54);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
||||
test("Rouding sale HALF-UP 0.05 with two payment method", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const { cashPm, cardPm } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
|
||||
const order = await getFilledOrderForPriceCheck(store);
|
||||
|
||||
expect(order.displayPrice).toBe(52.54);
|
||||
|
||||
// only_round_cash_method is false so the order due is 52.55
|
||||
order.addPaymentline(cardPm);
|
||||
order.payment_ids[0].setAmount(2.54);
|
||||
expect(order.payment_ids[0].amount).toBe(2.54);
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
expect(order.remainingDue).toBe(50.01);
|
||||
order.addPaymentline(cashPm);
|
||||
|
||||
// Cash rounding is not applied on the cash payment line but on the order due
|
||||
expect(order.payment_ids[1].amount).toBe(50.01);
|
||||
expect(order.remainingDue).toBe(0);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0.01);
|
||||
expect(order.change).toBe(0);
|
||||
|
||||
// Set only_round_cash_method to true and check that the order due is now 52.54
|
||||
order.config_id.only_round_cash_method = true;
|
||||
order.payment_ids = [];
|
||||
order.addPaymentline(cardPm);
|
||||
order.payment_ids[0].setAmount(2.54);
|
||||
expect(order.payment_ids[0].amount).toBe(2.54);
|
||||
expect(order.canBeValidated()).toBe(false);
|
||||
expect(order.remainingDue).toBe(50);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(0);
|
||||
order.addPaymentline(cashPm);
|
||||
expect(order.payment_ids[1].amount).toBe(50);
|
||||
expect(order.remainingDue).toBe(0);
|
||||
expect(order.canBeValidated()).toBe(true);
|
||||
expect(order.appliedRounding).toBe(0);
|
||||
expect(order.change).toBe(0);
|
||||
});
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
const { DateTime } = luxon;
|
||||
|
||||
/**
|
||||
* We use a dedicated method for the price check because we don't want to use
|
||||
* getFilledOrder in case of modification of this method breaks the price test.
|
||||
*
|
||||
* This method is a copy of getFilledOrder from utils.js
|
||||
*/
|
||||
export const getFilledOrderForPriceCheck = async (store, data = {}) => {
|
||||
const order = store.addNewOrder(data);
|
||||
// This product1 has a 25% tax with a 100.0 price
|
||||
// This product2 has a 15% + 25% tax with a 1000.0 price
|
||||
const product1 = store.models["product.template"].get(15);
|
||||
const product2 = store.models["product.template"].get(16);
|
||||
|
||||
const date = DateTime.now();
|
||||
order.write_date = date;
|
||||
order.create_date = date;
|
||||
order.pricelist_id = false;
|
||||
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: product1,
|
||||
qty: 1,
|
||||
write_date: date,
|
||||
create_date: date,
|
||||
},
|
||||
order
|
||||
);
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: product2,
|
||||
qty: 1,
|
||||
write_date: date,
|
||||
create_date: date,
|
||||
},
|
||||
order
|
||||
);
|
||||
store.addPendingOrder([order.id]);
|
||||
return order;
|
||||
};
|
||||
|
||||
export const prepareRoundingVals = (store, roundingAmount, roundingMethod, onlyCash = true) => {
|
||||
const config = store.config;
|
||||
const product1 = store.models["product.template"].get(15);
|
||||
const product2 = store.models["product.template"].get(16);
|
||||
const cashPm = store.models["pos.payment.method"].find((pm) => pm.is_cash_count);
|
||||
const cardPm = store.models["pos.payment.method"].find((pm) => !pm.is_cash_count);
|
||||
|
||||
// Changes prices to have a non rounded change
|
||||
product1.list_price = 15.73;
|
||||
product2.list_price = 23.49;
|
||||
product1.product_variant_ids[0].lst_price = 15.73;
|
||||
product2.product_variant_ids[0].lst_price = 23.49;
|
||||
|
||||
config.cash_rounding = true;
|
||||
config.only_round_cash_method = onlyCash;
|
||||
config.rounding_method = store.models["account.cash.rounding"].create({
|
||||
name: "roudning",
|
||||
rounding: roundingAmount,
|
||||
rounding_method: roundingMethod,
|
||||
strategy: "add_invoice_line",
|
||||
});
|
||||
|
||||
return { config, cashPm, cardPm };
|
||||
};
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { test, expect, animationFrame } from "@odoo/hoot";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary";
|
||||
import { setupPosEnv, getFilledOrder } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { queryAll, queryOne } from "@odoo/hoot-dom";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("getNewLine", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const orderSummary = await mountWithCleanup(OrderSummary, {});
|
||||
order.getSelectedOrderline().uiState.savedQuantity = 5;
|
||||
const newLine = orderSummary.getNewLine();
|
||||
expect(newLine.order_id.id).toBe(order.id);
|
||||
expect(newLine.qty).toBe(0);
|
||||
});
|
||||
|
||||
test("Display tax include/exclude subtotal label", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
|
||||
order.config.iface_tax_included = "total";
|
||||
await mountWithCleanup(OrderSummary, {});
|
||||
const total = queryOne(".total");
|
||||
const subtotal = queryAll(".subtotal");
|
||||
expect(subtotal).toHaveLength(0);
|
||||
expect(total.innerHTML).toBe("$ 17.85");
|
||||
|
||||
order.config.iface_tax_included = "subtotal";
|
||||
await animationFrame();
|
||||
const total2 = queryOne(".total");
|
||||
const subtotal2 = queryOne(".subtotal");
|
||||
expect(total2.innerHTML).toBe("$ 17.85");
|
||||
expect(subtotal2.innerHTML).toBe("$ 15.00");
|
||||
});
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { Orderline } from "@point_of_sale/app/components/orderline/orderline";
|
||||
import { expectFormattedPrice, setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("orderline.js", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
const product = store.models["product.template"].get(5);
|
||||
const line = await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: product,
|
||||
qty: 3,
|
||||
note: '[{"text":"Test 1","colorIndex":0},{"text":"Test 2","colorIndex":0}]',
|
||||
},
|
||||
order
|
||||
);
|
||||
|
||||
const comp = await mountWithCleanup(Orderline, {
|
||||
props: { line },
|
||||
});
|
||||
const lineData = comp.lineScreenValues;
|
||||
expect(comp.line.id).toEqual(line.id);
|
||||
expectFormattedPrice(comp.line.currencyDisplayPrice, "$ 10.35");
|
||||
expect(lineData.internalNote).toEqual([
|
||||
{
|
||||
text: "Test 1",
|
||||
colorIndex: 0,
|
||||
},
|
||||
{
|
||||
text: "Test 2",
|
||||
colorIndex: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { setupPosEnv, dialogActions } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { click } from "@odoo/hoot-dom";
|
||||
import { InternalNoteButton } from "@point_of_sale/app/screens/product_screen/control_buttons/orderline_note_button/orderline_note_button";
|
||||
import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("orderline_note_button.js", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
const productTmplCombo = store.models["product.template"].get(7);
|
||||
|
||||
const productComboSteps = [
|
||||
() => click("#article_product_8"), // Wood Chair 1/2
|
||||
() => click("#article_product_8"), // Wood Chair 2/2
|
||||
() => click("#article_product_10"), // Wood desk
|
||||
() => click(".confirm"), // Confirm combo configuration
|
||||
];
|
||||
const lineAction = async () =>
|
||||
await store.addLineToCurrentOrder({
|
||||
product_tmpl_id: productTmplCombo,
|
||||
qty: 1,
|
||||
});
|
||||
const line = await dialogActions(lineAction, productComboSteps);
|
||||
expect(order.lines[0].qty).toBe(1);
|
||||
expect(order.lines[1].qty).toBe(1);
|
||||
expect(order.lines[2].qty).toBe(2);
|
||||
const orderSummary = await mountWithCleanup(OrderSummary, { props: {} });
|
||||
orderSummary._setValue(4);
|
||||
expect(order.lines[0].qty).toBe(4);
|
||||
expect(order.lines[1].qty).toBe(4);
|
||||
expect(order.lines[2].qty).toBe(8);
|
||||
const comp = await mountWithCleanup(InternalNoteButton, { props: { label: "" } });
|
||||
await comp.setChanges(line, '[{"1":"Test","colorIndex":0}]');
|
||||
order.updateLastOrderChange();
|
||||
orderSummary._setValue(9);
|
||||
|
||||
const noteAction = async () => await comp.setChanges(line, '[{"2":"Test","colorIndex":0}]');
|
||||
await dialogActions(noteAction, productComboSteps);
|
||||
// Check quantity
|
||||
expect(order.lines[0].qty).toBe(4);
|
||||
expect(order.lines[1].qty).toBe(4);
|
||||
expect(order.lines[2].qty).toBe(8);
|
||||
expect(order.lines[3].qty).toBe(5);
|
||||
expect(order.lines[4].qty).toBe(5);
|
||||
expect(order.lines[5].qty).toBe(10);
|
||||
|
||||
// Check notes (only on parent lines)
|
||||
expect(order.lines[0].note).toBe('[{"1":"Test","colorIndex":0}]');
|
||||
expect(order.lines[3].note).toBe('[{"2":"Test","colorIndex":0}]');
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { test, animationFrame } from "@odoo/hoot";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { setupPosEnv, getFilledOrder, expectFormattedPrice } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { queryOne } from "@odoo/hoot-dom";
|
||||
import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("Change always incl", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const firstPm = store.models["pos.payment.method"].getFirst();
|
||||
order.config.iface_tax_included = "total";
|
||||
const comp = await mountWithCleanup(PaymentScreen, {
|
||||
props: { orderUuid: order.uuid },
|
||||
});
|
||||
await comp.addNewPaymentLine(firstPm);
|
||||
order.payment_ids[0].setAmount(20);
|
||||
await animationFrame();
|
||||
const total = queryOne(".amount");
|
||||
expectFormattedPrice(total.attributes.amount.value, "$ -2.15");
|
||||
order.config.iface_tax_included = "subtotal";
|
||||
await animationFrame();
|
||||
const subtotal = queryOne(".amount");
|
||||
expectFormattedPrice(subtotal.attributes.amount.value, "$ -2.15");
|
||||
});
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { setupPosEnv } from "../utils";
|
||||
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("_getProductByBarcode", async () => {
|
||||
const store = await setupPosEnv();
|
||||
store.addNewOrder();
|
||||
const order = store.getOrder();
|
||||
const comp = await mountWithCleanup(ProductScreen, { props: { orderUuid: order.uuid } });
|
||||
await comp.addProductToOrder(store.models["product.template"].get(5));
|
||||
|
||||
expect(order.displayPrice).toBe(3.45);
|
||||
expect(comp.total).toBe("$\u00a03.45");
|
||||
expect(comp.items).toBe("1");
|
||||
|
||||
const productByBarcode = await comp._getProductByBarcode({ base_code: "test_test" });
|
||||
expect(productByBarcode.id).toEqual(5);
|
||||
});
|
||||
|
||||
test("fastValidate", async () => {
|
||||
const store = await setupPosEnv();
|
||||
store.addNewOrder();
|
||||
const order = store.getOrder();
|
||||
const fastPaymentMethod = order.config.fast_payment_method_ids[0];
|
||||
const productScreen = await mountWithCleanup(ProductScreen, {
|
||||
props: { orderUuid: order.uuid },
|
||||
});
|
||||
await productScreen.addProductToOrder(store.models["product.template"].get(5));
|
||||
|
||||
expect(order.displayPrice).toBe(3.45);
|
||||
expect(productScreen.total).toBe("$\u00a03.45");
|
||||
expect(productScreen.items).toBe("1");
|
||||
|
||||
await productScreen.fastValidate(fastPaymentMethod);
|
||||
|
||||
expect(order.payment_ids[0].payment_method_id).toEqual(fastPaymentMethod);
|
||||
expect(order.state).toBe("paid");
|
||||
expect(order.amount_paid).toBe(3.45);
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { setupPosEnv, getFilledOrder } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { queryOne } from "@odoo/hoot-dom";
|
||||
import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("Total on receipt always incl", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
order.config.iface_tax_included = "total";
|
||||
await mountWithCleanup(ReceiptScreen, {
|
||||
props: { orderUuid: order.uuid },
|
||||
});
|
||||
const total = queryOne(".pos-receipt-amount .pos-receipt-right-align");
|
||||
expect(total.innerHTML).toBe("$ 17.85");
|
||||
order.config.iface_tax_included = "subtotal";
|
||||
const subtotal = queryOne(".pos-receipt-amount .pos-receipt-right-align");
|
||||
expect(subtotal.innerHTML).toBe("$ 17.85");
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { getFilledOrder, setupPosEnv, expectFormattedPrice } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { CustomerDisplayPosAdapter } from "@point_of_sale/app/customer_display/customer_display_adapter";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("getOrderlineData", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
|
||||
const adapter = new CustomerDisplayPosAdapter();
|
||||
adapter.formatOrderData(order);
|
||||
|
||||
expect(adapter.data.lines).toHaveLength(2);
|
||||
expect(adapter.data.lines[0].isSelected).toBe(false);
|
||||
expect(adapter.data.lines[1].isSelected).toBe(true);
|
||||
});
|
||||
|
||||
test("order amounts summary", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
|
||||
const adapter = new CustomerDisplayPosAdapter();
|
||||
|
||||
adapter.formatOrderData(order);
|
||||
expectFormattedPrice(adapter.data.amount, "$ 17.85");
|
||||
expectFormattedPrice(adapter.data.amountTaxes, "$ 2.85");
|
||||
expect(adapter.data.subtotal).toBe(false);
|
||||
|
||||
store.config.iface_tax_included = "subtotal";
|
||||
adapter.formatOrderData(order);
|
||||
expectFormattedPrice(adapter.data.amount, "$ 17.85");
|
||||
expectFormattedPrice(adapter.data.amountTaxes, "$ 2.85");
|
||||
expectFormattedPrice(adapter.data.subtotal, "$ 15.00");
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class AccountCashRounding extends models.ServerModel {
|
||||
_name = "account.cash.rounding";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class AccountFiscalPosition extends models.ServerModel {
|
||||
_name = "account.fiscal.position";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "display_name", "tax_map", "tax_ids"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Domestic",
|
||||
display_name: "Domestic",
|
||||
tax_ids: [1],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "No tax fp",
|
||||
display_name: "No tax fp",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class AccountJournal extends models.ServerModel {
|
||||
_name = "account.journal";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Point of Sale",
|
||||
type: "sale",
|
||||
code: "POS",
|
||||
company_id: 250,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class AccountMove extends models.ServerModel {
|
||||
_name = "account.move";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class AccountTax extends models.ServerModel {
|
||||
_name = "account.tax";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"id",
|
||||
"name",
|
||||
"price_include",
|
||||
"include_base_amount",
|
||||
"is_base_affected",
|
||||
"has_negative_factor",
|
||||
"amount_type",
|
||||
"children_tax_ids",
|
||||
"amount",
|
||||
"company_id",
|
||||
"id",
|
||||
"sequence",
|
||||
"tax_group_id",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "15%",
|
||||
price_include: false,
|
||||
include_base_amount: false,
|
||||
is_base_affected: true,
|
||||
has_negative_factor: false,
|
||||
amount_type: "percent",
|
||||
children_tax_ids: [],
|
||||
amount: 15.0,
|
||||
company_id: 250,
|
||||
sequence: 1,
|
||||
tax_group_id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "25%",
|
||||
price_include: false,
|
||||
include_base_amount: false,
|
||||
is_base_affected: true,
|
||||
has_negative_factor: false,
|
||||
amount_type: "percent",
|
||||
children_tax_ids: [],
|
||||
amount: 25.0,
|
||||
company_id: 250,
|
||||
sequence: 1,
|
||||
tax_group_id: 3,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "tax incl",
|
||||
type_tax_use: "sale",
|
||||
amount_type: "percent",
|
||||
amount: 7,
|
||||
price_include_override: "tax_included",
|
||||
include_base_amount: true,
|
||||
has_negative_factor: true,
|
||||
company_id: 250,
|
||||
is_base_affected: true,
|
||||
tax_group_id: 4,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "15% incl",
|
||||
type_tax_use: "sale",
|
||||
amount_type: "percent",
|
||||
amount: 15,
|
||||
price_include_override: "tax_included",
|
||||
price_include: true,
|
||||
include_base_amount: true,
|
||||
has_negative_factor: false,
|
||||
company_id: 250,
|
||||
is_base_affected: true,
|
||||
tax_group_id: 5,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class AccountTaxGroup extends models.ServerModel {
|
||||
_name = "account.tax.group";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "pos_receipt_label"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Tax 15%",
|
||||
pos_receipt_label: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Tax 0%",
|
||||
pos_receipt_label: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Tax 25%",
|
||||
pos_receipt_label: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "No group",
|
||||
pos_receipt_label: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "15% incl",
|
||||
pos_receipt_label: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class BarcodeNomenclature extends models.ServerModel {
|
||||
_name = "barcode.nomenclature";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class DecimalPrecision extends models.ServerModel {
|
||||
_name = "decimal.precision";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "digits"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Product Unit",
|
||||
digits: 2,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Percentage Analytic",
|
||||
digits: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Product Price",
|
||||
digits: 2,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Discount",
|
||||
digits: 2,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Stock Weight",
|
||||
digits: 2,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Volume",
|
||||
digits: 2,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Payment Terms",
|
||||
digits: 6,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import { defineModels, onRpc } from "@web/../tests/web_test_helpers";
|
||||
import { mailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { ResourceCalendar } from "./resource_calendar.data";
|
||||
import { AccountMove } from "./account_move.data";
|
||||
import { PosSession } from "./pos_session.data";
|
||||
import { PosConfig } from "./pos_config.data";
|
||||
import { PosPreset } from "./pos_preset.data";
|
||||
import { ResourceCalendarAttendance } from "./resource_calendar_attendance";
|
||||
import { PosOrder } from "./pos_order.data";
|
||||
import { PosOrderLine } from "./pos_order_line.data";
|
||||
import { PosPackOperationLot } from "./pos_pack_operation_lot.data";
|
||||
import { PosPayment } from "./pos_payment.data";
|
||||
import { PosPaymentMethod } from "./pos_payment_method.data";
|
||||
import { PosPrinter } from "./pos_printer.data";
|
||||
import { PosCategory } from "./pos_category.data";
|
||||
import { PosBill } from "./pos_bill.data";
|
||||
import { ResCompany } from "./res_company.data";
|
||||
import { AccountTax } from "./account_tax.data";
|
||||
import { AccountTaxGroup } from "./account_tax_group.data";
|
||||
import { ProductTemplate } from "./product_template.data";
|
||||
import { ProductProduct } from "./product_product.data";
|
||||
import { ProductAttribute } from "./product_attribute.data";
|
||||
import { ProductAttributeCustomValue } from "./product_attribute_custom_value.data";
|
||||
import { ProductTemplateAttributeLine } from "./product_template_attribute_line.data";
|
||||
import { ProductTemplateAttributeValue } from "./product_template_attribute_value.data";
|
||||
import { ProductTemplateAttributeExclusion } from "./product_template_attribute_exclusion.data";
|
||||
import { ProductCombo } from "./product_combo.data";
|
||||
import { ProductComboItem } from "./product_combo_item.data";
|
||||
import { ResUsers } from "./res_users.data";
|
||||
import { ResPartner } from "./res_partner.data";
|
||||
import { ProductUom } from "./product_uom.data";
|
||||
import { DecimalPrecision } from "./decimal_precision.data";
|
||||
import { UomUom } from "./uom_uom.data";
|
||||
import { ResCountry } from "./res_country.data";
|
||||
import { ResCountryState } from "./res_country_state.data";
|
||||
import { ResLang } from "./res_lang.data";
|
||||
import { ProductCategory } from "./product_category.data";
|
||||
import { ProductPricelist } from "./product_pricelist.data";
|
||||
import { ProductPricelistItem } from "./product_pricelist_item.data";
|
||||
import { AccountCashRounding } from "./account_cash_rounding.data";
|
||||
import { AccountFiscalPosition } from "./account_fiscal_position.data";
|
||||
import { StockPickingType } from "./stock_picking_type.data";
|
||||
import { ResCurrency } from "./res_currency.data";
|
||||
import { PosNote } from "./pos_note.data";
|
||||
import { ProductTag } from "./product_tag.data";
|
||||
import { IrModuleModule } from "./ir_module_module.data";
|
||||
import { AccountJournal } from "./account_journal.data";
|
||||
import { IrSequence } from "./ir_sequence.data";
|
||||
import { StockWarehouse } from "./stock_warehouse.data";
|
||||
import { StockRoute } from "./stock_route.data";
|
||||
import { BarcodeNomenclature } from "./barcode_nomenclature.data";
|
||||
import { ProductAttributeValue } from "./product_attribute_value.data";
|
||||
|
||||
export const hootPosModels = [
|
||||
ResCountry,
|
||||
ResCountryState,
|
||||
ResCurrency,
|
||||
ResCompany,
|
||||
ResPartner,
|
||||
ResUsers,
|
||||
ResLang,
|
||||
PosSession,
|
||||
PosConfig,
|
||||
PosPreset,
|
||||
ResourceCalendarAttendance,
|
||||
PosOrder,
|
||||
PosOrderLine,
|
||||
PosPackOperationLot,
|
||||
PosPayment,
|
||||
PosPaymentMethod,
|
||||
PosPrinter,
|
||||
PosCategory,
|
||||
PosBill,
|
||||
AccountTax,
|
||||
AccountTaxGroup,
|
||||
AccountMove,
|
||||
ProductCategory,
|
||||
ProductTemplate,
|
||||
ProductProduct,
|
||||
ProductAttribute,
|
||||
ProductAttributeValue,
|
||||
ProductAttributeCustomValue,
|
||||
ProductTemplateAttributeLine,
|
||||
ProductTemplateAttributeValue,
|
||||
ProductTemplateAttributeExclusion,
|
||||
ProductCombo,
|
||||
ProductComboItem,
|
||||
ProductUom,
|
||||
ProductTag,
|
||||
ProductPricelist,
|
||||
ProductPricelistItem,
|
||||
DecimalPrecision,
|
||||
StockWarehouse,
|
||||
StockRoute,
|
||||
UomUom,
|
||||
AccountCashRounding,
|
||||
AccountFiscalPosition,
|
||||
StockPickingType,
|
||||
IrSequence,
|
||||
PosNote,
|
||||
IrModuleModule,
|
||||
AccountJournal,
|
||||
ResourceCalendar,
|
||||
BarcodeNomenclature,
|
||||
];
|
||||
|
||||
export const definePosModels = () => {
|
||||
const posModelNames = hootPosModels.map((modelClass) => modelClass.prototype.constructor._name);
|
||||
const modelsFromMail = Object.values(mailModels).filter(
|
||||
(modelClass) => !posModelNames.includes(modelClass.prototype.constructor._name)
|
||||
);
|
||||
onRpc("/pos/ping", () => {});
|
||||
defineModels([...modelsFromMail, ...hootPosModels]);
|
||||
};
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { createRelatedModels } from "@point_of_sale/app/models/related_models";
|
||||
import { DataServiceOptions } from "@point_of_sale/app/models/data_service_options";
|
||||
import { MockServer } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export const getModelDefinitions = () => {
|
||||
const session = MockServer.current._models["pos.session"];
|
||||
const params = session.load_data_params();
|
||||
return Object.entries(params).reduce((acc, [modelName, params]) => {
|
||||
acc[modelName] = params.relations;
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
let generatedModels = null;
|
||||
|
||||
export const getRelatedModelsInstance = (useModelClass = true) => {
|
||||
if (generatedModels) {
|
||||
return generatedModels;
|
||||
}
|
||||
|
||||
const options = new DataServiceOptions();
|
||||
const relations = getModelDefinitions();
|
||||
const modelClasses = {};
|
||||
|
||||
if (useModelClass) {
|
||||
for (const posModel of registry.category("pos_available_models").getAll()) {
|
||||
const pythonModel = posModel.pythonModel;
|
||||
const extraFields = posModel.extraFields || {};
|
||||
|
||||
modelClasses[pythonModel] = posModel;
|
||||
relations[pythonModel] = {
|
||||
...relations[pythonModel],
|
||||
...extraFields,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const models = createRelatedModels(relations, useModelClass ? modelClasses : {}, options);
|
||||
generatedModels = models.models;
|
||||
return models.models;
|
||||
};
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class IrModuleModule extends models.ServerModel {
|
||||
_name = "ir.module.module";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "state"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 901,
|
||||
name: "pos_settle_due",
|
||||
state: "installed",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class IrSequence extends models.ServerModel {
|
||||
_name = "ir.sequence";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosBill extends models.ServerModel {
|
||||
_name = "pos.bill";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "value"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "0.05",
|
||||
value: 0.05,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "0.10",
|
||||
value: 0.1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "0.20",
|
||||
value: 0.2,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "0.25",
|
||||
value: 0.25,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "0.50",
|
||||
value: 0.5,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "1.00",
|
||||
value: 1.0,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "2.00",
|
||||
value: 2.0,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "5.00",
|
||||
value: 5.0,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "10.00",
|
||||
value: 10.0,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "20.00",
|
||||
value: 20.0,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "50.00",
|
||||
value: 50.0,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "100.00",
|
||||
value: 100.0,
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: "200.00",
|
||||
value: 200.0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosCategory extends models.ServerModel {
|
||||
_name = "pos.category";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"id",
|
||||
"name",
|
||||
"parent_id",
|
||||
"child_ids",
|
||||
"write_date",
|
||||
"has_image",
|
||||
"color",
|
||||
"sequence",
|
||||
"hour_until",
|
||||
"hour_after",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Category 1",
|
||||
parent_id: false,
|
||||
child_ids: [],
|
||||
has_image: false,
|
||||
color: 0,
|
||||
sequence: 1,
|
||||
hour_until: 0.0,
|
||||
hour_after: 24.0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Category 2",
|
||||
parent_id: false,
|
||||
child_ids: [],
|
||||
has_image: false,
|
||||
color: 0,
|
||||
sequence: 2,
|
||||
hour_until: 0.0,
|
||||
hour_after: 24.0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Food",
|
||||
parent_id: false,
|
||||
child_ids: [4, 5],
|
||||
has_image: false,
|
||||
color: 0,
|
||||
sequence: 4,
|
||||
hour_until: 0.0,
|
||||
hour_after: 24.0,
|
||||
},
|
||||
|
||||
{
|
||||
id: 4,
|
||||
name: "Burger",
|
||||
parent_id: 3,
|
||||
child_ids: [],
|
||||
has_image: false,
|
||||
color: 2,
|
||||
sequence: 3,
|
||||
hour_until: 0.0,
|
||||
hour_after: 24.0,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Pizza",
|
||||
parent_id: 3,
|
||||
child_ids: [],
|
||||
has_image: false,
|
||||
color: 3,
|
||||
sequence: 5,
|
||||
hour_until: 0.0,
|
||||
hour_after: 24.0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosConfig extends models.ServerModel {
|
||||
_name = "pos.config";
|
||||
|
||||
notify_synchronisation(session_id, device_identifier, records = {}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
register_new_device_identifier(self) {
|
||||
return {
|
||||
device_identifier: 1,
|
||||
};
|
||||
}
|
||||
|
||||
read_config_open_orders(configId) {
|
||||
// We can read everything since its only related to the current test.
|
||||
const orderIds = this.env["pos.order"].search_read([], ["id"]).map((order) => order.id);
|
||||
return {
|
||||
deleted_record_ids: {},
|
||||
dynamic_records: {
|
||||
...this.env["pos.order"].read_pos_data(orderIds, [], configId),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get_next_order_refs(device_identifier = 0) {
|
||||
const sequence_num = ++this._orderRef;
|
||||
|
||||
const now = new Date();
|
||||
const YY = now.getFullYear().toString().slice(-2);
|
||||
const LL = String(device_identifier % 100);
|
||||
const SSS = String(this.id);
|
||||
const F = 0;
|
||||
const OOOO = String(sequence_num).padStart(6, "0");
|
||||
const order_ref = `${YY}${LL}-${SSS}-${F}${OOOO}`;
|
||||
|
||||
return [order_ref, sequence_num, String(sequence_num).padStart(3, "0")];
|
||||
}
|
||||
|
||||
_load_pos_data_read(data) {
|
||||
data[0]["_partner_commercial_fields"] = [];
|
||||
data[0]["_server_version"] = "18.3+e";
|
||||
data[0]["_base_url"] = "http://localhost:4444";
|
||||
data[0]["_data_server_date"] = "2025-07-03 12:40:15";
|
||||
data[0]["_has_cash_move_perm"] = true;
|
||||
data[0]["_has_available_products"] = true;
|
||||
data[0]["_pos_special_products_ids"] = [];
|
||||
return data;
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "Hoot",
|
||||
access_token: "test_access_token",
|
||||
name: "Hoot",
|
||||
printer_ids: [1],
|
||||
is_order_printer: true,
|
||||
is_installed_account_accountant: true,
|
||||
picking_type_id: 9,
|
||||
journal_id: 1,
|
||||
invoice_journal_id: 1,
|
||||
currency_id: 1,
|
||||
iface_cashdrawer: false,
|
||||
iface_electronic_scale: false,
|
||||
iface_print_via_proxy: false,
|
||||
iface_scan_via_proxy: false,
|
||||
iface_big_scrollbars: false,
|
||||
iface_print_auto: false,
|
||||
iface_print_skip_screen: true,
|
||||
iface_tax_included: "total",
|
||||
iface_available_categ_ids: [],
|
||||
customer_display_bg_img: false,
|
||||
customer_display_bg_img_name: false,
|
||||
restrict_price_control: false,
|
||||
is_margins_costs_accessible_to_every_user: false,
|
||||
cash_control: true,
|
||||
set_maximum_difference: false,
|
||||
receipt_header: false,
|
||||
receipt_footer: false,
|
||||
basic_receipt: false,
|
||||
proxy_ip: false,
|
||||
active: true,
|
||||
uuid: "6f9034bb-faf8-4875-b216-dafb78982918",
|
||||
session_ids: [1],
|
||||
current_session_id: 1,
|
||||
current_session_state: "opening_control",
|
||||
number_of_rescue_session: 0,
|
||||
last_session_closing_cash: 0.0,
|
||||
last_session_closing_date: false,
|
||||
pos_session_username: "Administrator",
|
||||
pos_session_state: "opening_control",
|
||||
pos_session_duration: "0",
|
||||
pricelist_id: false,
|
||||
available_pricelist_ids: [1],
|
||||
company_id: 250,
|
||||
group_pos_manager_id: false,
|
||||
group_pos_user_id: false,
|
||||
iface_tipproduct: false,
|
||||
tip_product_id: 1,
|
||||
fiscal_position_ids: [],
|
||||
default_fiscal_position_id: false,
|
||||
default_bill_ids: [],
|
||||
use_pricelist: true,
|
||||
use_presets: true,
|
||||
default_preset_id: 1,
|
||||
available_preset_ids: [1, 2],
|
||||
tax_regime_selection: false,
|
||||
limit_categories: false,
|
||||
module_pos_restaurant: false,
|
||||
module_pos_avatax: false,
|
||||
module_pos_discount: false,
|
||||
module_pos_appointment: false,
|
||||
is_posbox: false,
|
||||
is_header_or_footer: false,
|
||||
module_pos_hr: false,
|
||||
amount_authorized_diff: 0.0,
|
||||
payment_method_ids: [2, 1],
|
||||
company_has_template: true,
|
||||
current_user_id: 2,
|
||||
other_devices: false,
|
||||
rounding_method: false,
|
||||
cash_rounding: false,
|
||||
only_round_cash_method: false,
|
||||
has_active_session: true,
|
||||
manual_discount: true,
|
||||
ship_later: false,
|
||||
warehouse_id: false,
|
||||
route_id: false,
|
||||
picking_policy: "direct",
|
||||
auto_validate_terminal_payment: true,
|
||||
trusted_config_ids: [],
|
||||
show_product_images: true,
|
||||
show_category_images: true,
|
||||
note_ids: [],
|
||||
module_pos_sms: false,
|
||||
is_closing_entry_by_product: false,
|
||||
order_edit_tracking: false,
|
||||
last_data_change: "2025-07-03 14:35:55",
|
||||
fallback_nomenclature_id: false,
|
||||
create_date: "2025-07-03 12:40:00",
|
||||
write_date: "2025-07-03 14:35:55",
|
||||
epson_printer_ip: false,
|
||||
use_fast_payment: true,
|
||||
fast_payment_method_ids: [1],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosNote extends models.ServerModel {
|
||||
_name = "pos.note";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["name", "color"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Wait",
|
||||
color: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "To Serve",
|
||||
color: 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Emergency",
|
||||
color: 0,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "No Dressing",
|
||||
color: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import { models, Command } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosOrder extends models.ServerModel {
|
||||
_name = "pos.order";
|
||||
|
||||
get_preparation_change(id) {
|
||||
const read = this.read([id]);
|
||||
const changes = read[0]?.last_order_preparation_change || "{}";
|
||||
return {
|
||||
last_order_preparation_change: changes,
|
||||
};
|
||||
}
|
||||
|
||||
read_pos_orders(domain) {
|
||||
const results = this.search(domain, this._load_pos_data_fields(), false);
|
||||
return this.read_pos_data(results);
|
||||
}
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
action_pos_order_cancel(self) {
|
||||
const records = this.browse(self);
|
||||
const orderIds = [];
|
||||
|
||||
for (const record of records) {
|
||||
this.write([record.id], { state: "cancel" });
|
||||
orderIds.push(record.id);
|
||||
}
|
||||
|
||||
return {
|
||||
"pos.order": this.read(orderIds, this._load_pos_data_fields(), false),
|
||||
};
|
||||
}
|
||||
|
||||
create() {
|
||||
const orderId = super.create(...arguments);
|
||||
this.write([orderId], { pos_reference: "000-0-000000" });
|
||||
return orderId;
|
||||
}
|
||||
|
||||
sync_from_ui(data) {
|
||||
const orderIds = [];
|
||||
for (const record of data) {
|
||||
const record_uuid_mapping = record.relations_uuid_mapping || {};
|
||||
delete record.relations_uuid_mapping;
|
||||
if (record.id) {
|
||||
this.write([record.id], record);
|
||||
orderIds.push(record.id);
|
||||
} else {
|
||||
const id = this.create(record);
|
||||
orderIds.push(id);
|
||||
}
|
||||
for (const [modelName, mapping] of Object.entries(record_uuid_mapping)) {
|
||||
// Search for owner records by UUID
|
||||
const ownerRecords = this.env[modelName].search_read(
|
||||
[["uuid", "in", Object.keys(mapping)]],
|
||||
["id", "uuid"]
|
||||
);
|
||||
for (const [uuid, fields] of Object.entries(mapping)) {
|
||||
for (const [name, uuids] of Object.entries(fields)) {
|
||||
const field = this.env[modelName]._fields[name];
|
||||
if (["one2many", "many2many"].includes(field.type)) {
|
||||
// Get all related records by uuids
|
||||
const relatedRecords = this.env[field.relation].search_read(
|
||||
[["uuid", "in", uuids]],
|
||||
["id", "uuid"]
|
||||
);
|
||||
const ownerRecord = ownerRecords.find((r) => r.uuid === uuid);
|
||||
if (ownerRecord) {
|
||||
this.env[modelName].write([ownerRecord.id], {
|
||||
[name]: relatedRecords.map((r) => Command.link(r.id)),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// single record relation (many2one)
|
||||
const record = this.env[field.relation].search([["uuid", "=", uuids]]);
|
||||
const ownerRecord = ownerRecords.find((r) => r.uuid === uuid);
|
||||
if (ownerRecord && record) {
|
||||
this.env[modelName].write([ownerRecord.id], { [name]: record[0] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.read_pos_data(orderIds, data, this.config_id);
|
||||
}
|
||||
|
||||
read_pos_data(orderIds, data, config_id) {
|
||||
const posOrder = [];
|
||||
const posSession = [];
|
||||
const posPayment = [];
|
||||
const posOrderLine = [];
|
||||
const posPackOperationLot = [];
|
||||
const posCustomAttributeValue = [];
|
||||
const readOrder = this.read(orderIds, this._load_pos_data_fields(config_id), false);
|
||||
|
||||
for (const order of readOrder) {
|
||||
posOrder.push(order);
|
||||
|
||||
const lines = this.env["pos.order.line"].read(
|
||||
order.lines,
|
||||
this.env["pos.order.line"]._load_pos_data_fields(config_id),
|
||||
false
|
||||
);
|
||||
const payments = this.env["pos.payment"].read(
|
||||
order.payment_ids,
|
||||
this.env["pos.payment"]._load_pos_data_fields(config_id),
|
||||
false
|
||||
);
|
||||
const packLotLineIds = lines.flatMap((line) => line.pack_lot_ids);
|
||||
const packLotLines = this.env["pos.pack.operation.lot"].read(
|
||||
packLotLineIds,
|
||||
this.env["pos.pack.operation.lot"]._load_pos_data_fields(config_id),
|
||||
false
|
||||
);
|
||||
const customAttributeValueIds = lines.flatMap(
|
||||
(line) => line.custom_attribute_value_ids
|
||||
);
|
||||
const customAttributeValues = this.env["product.attribute.custom.value"].read(
|
||||
customAttributeValueIds,
|
||||
this.env["product.attribute.custom.value"]._load_pos_data_fields(config_id),
|
||||
false
|
||||
);
|
||||
|
||||
posOrderLine.push(...lines);
|
||||
posPayment.push(...payments);
|
||||
posPackOperationLot.push(...packLotLines);
|
||||
posCustomAttributeValue.push(...customAttributeValues);
|
||||
}
|
||||
|
||||
return {
|
||||
"pos.order": posOrder,
|
||||
"pos.session": posSession,
|
||||
"pos.payment": posPayment,
|
||||
"pos.order.line": posOrderLine,
|
||||
"pos.pack.operation.lot": posPackOperationLot,
|
||||
"product.attribute.custom.value": posCustomAttributeValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosOrderLine extends models.ServerModel {
|
||||
_name = "pos.order.line";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"qty",
|
||||
"attribute_value_ids",
|
||||
"custom_attribute_value_ids",
|
||||
"price_unit",
|
||||
"uuid",
|
||||
"price_subtotal",
|
||||
"price_subtotal_incl",
|
||||
"order_id",
|
||||
"note",
|
||||
"price_type",
|
||||
"product_id",
|
||||
"discount",
|
||||
"tax_ids",
|
||||
"pack_lot_ids",
|
||||
"customer_note",
|
||||
"refunded_qty",
|
||||
"price_extra",
|
||||
"full_product_name",
|
||||
"refunded_orderline_id",
|
||||
"combo_parent_id",
|
||||
"combo_line_ids",
|
||||
"combo_item_id",
|
||||
"refund_orderline_ids",
|
||||
"extra_tax_data",
|
||||
"write_date",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosPackOperationLot extends models.ServerModel {
|
||||
_name = "pos.pack.operation.lot";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["lot_name", "pos_order_line_id", "write_date"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosPayment extends models.ServerModel {
|
||||
_name = "pos.payment";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosPaymentMethod extends models.ServerModel {
|
||||
_name = "pos.payment.method";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"id",
|
||||
"name",
|
||||
"is_cash_count",
|
||||
"use_payment_terminal",
|
||||
"split_transactions",
|
||||
"type",
|
||||
"image",
|
||||
"sequence",
|
||||
"payment_method_type",
|
||||
"default_qr",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 2,
|
||||
name: "Card",
|
||||
is_cash_count: false,
|
||||
use_payment_terminal: false,
|
||||
split_transactions: false,
|
||||
type: "bank",
|
||||
image: false,
|
||||
sequence: 1,
|
||||
payment_method_type: "none",
|
||||
default_qr: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Customer Account",
|
||||
is_cash_count: false,
|
||||
use_payment_terminal: false,
|
||||
split_transactions: true,
|
||||
type: "pay_later",
|
||||
image: false,
|
||||
sequence: 2,
|
||||
payment_method_type: "none",
|
||||
default_qr: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: "Cash",
|
||||
is_cash_count: true,
|
||||
use_payment_terminal: false,
|
||||
split_transactions: false,
|
||||
type: "cash",
|
||||
image: false,
|
||||
sequence: 0,
|
||||
payment_method_type: "none",
|
||||
default_qr: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosPreset extends models.ServerModel {
|
||||
_name = "pos.preset";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"id",
|
||||
"name",
|
||||
"pricelist_id",
|
||||
"fiscal_position_id",
|
||||
"is_return",
|
||||
"color",
|
||||
"has_image",
|
||||
"write_date",
|
||||
"identification",
|
||||
"use_timing",
|
||||
"slots_per_interval",
|
||||
"interval_time",
|
||||
"attendance_ids",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "In",
|
||||
pricelist_id: 1,
|
||||
fiscal_position_id: 1,
|
||||
is_return: false,
|
||||
color: 0,
|
||||
has_image: false,
|
||||
write_date: "2025-07-03 14:34:01",
|
||||
identification: "none",
|
||||
use_timing: false,
|
||||
slots_per_interval: 5,
|
||||
interval_time: 20,
|
||||
attendance_ids: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Out",
|
||||
pricelist_id: false,
|
||||
fiscal_position_id: false,
|
||||
is_return: false,
|
||||
color: 0,
|
||||
has_image: false,
|
||||
write_date: "2025-07-03 14:34:07",
|
||||
identification: "none",
|
||||
use_timing: true,
|
||||
slots_per_interval: 5,
|
||||
interval_time: 20,
|
||||
attendance_ids: [],
|
||||
resource_calendar_id: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Name Required Preset",
|
||||
identification: "name",
|
||||
use_timing: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Address Required Preset",
|
||||
identification: "address",
|
||||
use_timing: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosPrinter extends models.ServerModel {
|
||||
_name = "pos.printer";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "proxy_ip", "product_categories_ids", "printer_type"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Printer",
|
||||
proxy_ip: false,
|
||||
product_categories_ids: [1, 2],
|
||||
printer_type: "epson_epos",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
import { MockServer, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class PosSession extends models.ServerModel {
|
||||
_name = "pos.session";
|
||||
_orderRef = 1;
|
||||
|
||||
_load_pos_data_models(config_id) {
|
||||
return [
|
||||
"pos.session",
|
||||
"pos.config",
|
||||
"pos.preset",
|
||||
"resource.calendar.attendance",
|
||||
"pos.order",
|
||||
"pos.order.line",
|
||||
"pos.pack.operation.lot",
|
||||
"pos.payment",
|
||||
"pos.payment.method",
|
||||
"pos.printer",
|
||||
"pos.category",
|
||||
"pos.bill",
|
||||
"res.company",
|
||||
"account.tax",
|
||||
"account.tax.group",
|
||||
"product.template",
|
||||
"product.product",
|
||||
"product.attribute",
|
||||
"product.attribute.custom.value",
|
||||
"product.template.attribute.line",
|
||||
"product.template.attribute.value",
|
||||
"product.template.attribute.exclusion",
|
||||
"product.combo",
|
||||
"product.combo.item",
|
||||
"res.users",
|
||||
"res.partner",
|
||||
"product.uom",
|
||||
"decimal.precision",
|
||||
"uom.uom",
|
||||
"res.country",
|
||||
"res.country.state",
|
||||
"res.lang",
|
||||
"product.category",
|
||||
"product.pricelist",
|
||||
"product.pricelist.item",
|
||||
"account.cash.rounding",
|
||||
"account.fiscal.position",
|
||||
"stock.picking.type",
|
||||
"res.currency",
|
||||
"pos.note",
|
||||
"product.tag",
|
||||
"ir.module.module",
|
||||
];
|
||||
}
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"id",
|
||||
"name",
|
||||
"user_id",
|
||||
"config_id",
|
||||
"start_at",
|
||||
"stop_at",
|
||||
"payment_method_ids",
|
||||
"state",
|
||||
"update_stock_at_closing",
|
||||
"cash_register_balance_start",
|
||||
"access_token",
|
||||
];
|
||||
}
|
||||
|
||||
// These methods are designed to be overridden to customize the POS data loading behavior using the provided `opts`.
|
||||
getModelsToLoad(opts) {
|
||||
return this._load_pos_data_models();
|
||||
}
|
||||
|
||||
getModelFieldsToLoad(model, opts) {
|
||||
return model._load_pos_data_fields();
|
||||
}
|
||||
|
||||
processPosReadData(model, records, opts) {
|
||||
return (model._load_pos_data_read && model._load_pos_data_read(records)) || records;
|
||||
}
|
||||
|
||||
load_data_params(opts = {}) {
|
||||
const modelToLoad = this.getModelsToLoad(opts);
|
||||
const response = modelToLoad.reduce((acc, modelName) => {
|
||||
acc[modelName] = {
|
||||
fields: {},
|
||||
relations: {},
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (const model of modelToLoad) {
|
||||
const serverModel = MockServer.env[model];
|
||||
const posFields = this.getModelFieldsToLoad(serverModel, opts);
|
||||
const allFields = serverModel.fields_get();
|
||||
const base = posFields.length ? posFields : Object.keys(allFields);
|
||||
|
||||
if (!base.includes("id")) {
|
||||
base.push("id");
|
||||
}
|
||||
|
||||
for (const fieldName of base) {
|
||||
const field = allFields[fieldName];
|
||||
|
||||
if (!field) {
|
||||
console.debug(`Field ${fieldName} not found in model ${model}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
response[model]["relations"][fieldName] = {
|
||||
name: fieldName,
|
||||
model: model,
|
||||
compute: Boolean(field.compute),
|
||||
related: Boolean(field.related),
|
||||
type: field.type,
|
||||
relation: field.relation,
|
||||
inverse_name: field.inverse_fname_by_model_name?.[field.relation] || false,
|
||||
};
|
||||
}
|
||||
|
||||
response[model]["fields"] = posFields;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
load_data(opts = {}) {
|
||||
const modelToLoad = this.getModelsToLoad(opts);
|
||||
const response = modelToLoad.reduce((acc, modelName) => {
|
||||
acc[modelName] = {};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (const modelName of modelToLoad) {
|
||||
const model = MockServer.env[modelName];
|
||||
const posFields = this.getModelFieldsToLoad(model, opts);
|
||||
const records = model.search_read([], posFields, false, false, false, false);
|
||||
response[modelName] = this.processPosReadData(model, records, opts);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
_load_pos_data_read(data) {
|
||||
data[0]["_partner_commercial_fields"] = [];
|
||||
data[0]["_server_version"] = "18.3+e";
|
||||
data[0]["_base_url"] = "http://localhost:4444";
|
||||
data[0]["_data_server_date"] = "2025-07-03 12:40:15";
|
||||
data[0]["_has_cash_move_perm"] = true;
|
||||
data[0]["_has_available_products"] = true;
|
||||
data[0]["_pos_special_products_ids"] = [];
|
||||
return data;
|
||||
}
|
||||
|
||||
filter_local_data() {
|
||||
return {};
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "/",
|
||||
user_id: 2,
|
||||
config_id: 1,
|
||||
start_at: false,
|
||||
stop_at: false,
|
||||
payment_method_ids: [2, 1],
|
||||
state: "opening_control",
|
||||
update_stock_at_closing: false,
|
||||
cash_register_balance_start: 0.0,
|
||||
access_token: "e09c4843-c913-463a-959d-b9e235881201",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductAttribute extends models.ServerModel {
|
||||
_name = "product.attribute";
|
||||
_order = "id";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"name",
|
||||
"display_type",
|
||||
"template_value_ids",
|
||||
"attribute_line_ids",
|
||||
"create_variant",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "color",
|
||||
display_type: "radio",
|
||||
template_value_ids: [],
|
||||
attribute_line_ids: [],
|
||||
create_variant: "always",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "gender",
|
||||
display_type: "radio",
|
||||
template_value_ids: [],
|
||||
attribute_line_ids: [],
|
||||
create_variant: "always",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "material",
|
||||
display_type: "radio",
|
||||
template_value_ids: [],
|
||||
attribute_line_ids: [],
|
||||
create_variant: "always",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "pattern",
|
||||
display_type: "radio",
|
||||
template_value_ids: [],
|
||||
attribute_line_ids: [],
|
||||
create_variant: "always",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "manufacturer",
|
||||
display_type: "radio",
|
||||
template_value_ids: [],
|
||||
attribute_line_ids: [],
|
||||
create_variant: "always",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "brand",
|
||||
display_type: "radio",
|
||||
template_value_ids: [],
|
||||
attribute_line_ids: [],
|
||||
create_variant: "always",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "size",
|
||||
display_type: "radio",
|
||||
template_value_ids: [],
|
||||
attribute_line_ids: [],
|
||||
create_variant: "always",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "age group",
|
||||
display_type: "radio",
|
||||
template_value_ids: [],
|
||||
attribute_line_ids: [],
|
||||
create_variant: "always",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductAttributeCustomValue extends models.ServerModel {
|
||||
_name = "product.attribute.custom.value";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"custom_value",
|
||||
"custom_product_template_attribute_value_id",
|
||||
"pos_order_line_id",
|
||||
"write_date",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductAttributeValue extends models.ServerModel {
|
||||
_name = "product.attribute.value";
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "White",
|
||||
attribute_id: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Black",
|
||||
attribute_id: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Blue",
|
||||
attribute_id: 1,
|
||||
default_extra_price: 5.0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductCategory extends models.ServerModel {
|
||||
_name = "product.category";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "parent_id"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 2,
|
||||
name: "Expenses",
|
||||
parent_id: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Food",
|
||||
parent_id: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: "Goods",
|
||||
parent_id: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Services",
|
||||
parent_id: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductCombo extends models.ServerModel {
|
||||
_name = "product.combo";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "combo_item_ids", "base_price", "qty_free", "qty_max"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Chairs Combo",
|
||||
combo_item_ids: [1, 2],
|
||||
base_price: 100,
|
||||
qty_free: 0,
|
||||
qty_max: 10,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Desks Combo",
|
||||
combo_item_ids: [3, 4],
|
||||
base_price: 200,
|
||||
qty_free: 1,
|
||||
qty_max: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductComboItem extends models.ServerModel {
|
||||
_name = "product.combo.item";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "combo_id", "product_id", "extra_price"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
combo_id: 1,
|
||||
product_id: 8,
|
||||
extra_price: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
combo_id: 1,
|
||||
product_id: 9,
|
||||
extra_price: 35,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
combo_id: 2,
|
||||
product_id: 10,
|
||||
extra_price: 0,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
combo_id: 2,
|
||||
product_id: 11,
|
||||
extra_price: 50,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductPricelist extends models.ServerModel {
|
||||
_name = "product.pricelist";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "display_name", "item_ids"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Test Pricelist A",
|
||||
display_name: "Test Pricelist A (USD)",
|
||||
item_ids: [1],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Test Pricelist B",
|
||||
display_name: "Test Pricelist B (USD)",
|
||||
item_ids: [1],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Test Pricelist 90%",
|
||||
display_name: "Test Pricelist 90% (USD)",
|
||||
item_ids: [2],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductPricelistItem extends models.ServerModel {
|
||||
_name = "product.pricelist.item";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"product_tmpl_id",
|
||||
"product_id",
|
||||
"pricelist_id",
|
||||
"price_surcharge",
|
||||
"price_discount",
|
||||
"price_round",
|
||||
"price_min_margin",
|
||||
"price_max_margin",
|
||||
"company_id",
|
||||
"currency_id",
|
||||
"date_start",
|
||||
"date_end",
|
||||
"compute_price",
|
||||
"fixed_price",
|
||||
"percent_price",
|
||||
"base_pricelist_id",
|
||||
"base",
|
||||
"categ_id",
|
||||
"min_quantity",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
product_tmpl_id: false,
|
||||
product_id: false,
|
||||
pricelist_id: 1,
|
||||
price_surcharge: 0.0,
|
||||
price_discount: 0.0,
|
||||
price_round: 0.0,
|
||||
price_min_margin: 0.0,
|
||||
price_max_margin: 0.0,
|
||||
company_id: 250,
|
||||
currency_id: 1,
|
||||
date_start: false,
|
||||
date_end: false,
|
||||
compute_price: "fixed",
|
||||
fixed_price: 3.0,
|
||||
percent_price: 0.0,
|
||||
base_pricelist_id: false,
|
||||
base: "list_price",
|
||||
categ_id: false,
|
||||
min_quantity: 0.0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
base: "list_price",
|
||||
company_id: 250,
|
||||
compute_price: "percentage",
|
||||
currency_id: 1,
|
||||
pricelist_id: 3,
|
||||
percent_price: 90.0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductProduct extends models.ServerModel {
|
||||
_name = "product.product";
|
||||
|
||||
// NOTE - We don't take into account _eval_taxes_computation_prepare_product_fields
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"id",
|
||||
"lst_price",
|
||||
"display_name",
|
||||
"product_tmpl_id",
|
||||
"product_template_variant_value_ids",
|
||||
"product_template_attribute_value_ids",
|
||||
"barcode",
|
||||
"product_tag_ids",
|
||||
"default_code",
|
||||
"standard_price",
|
||||
"pos_categ_ids",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
product_tmpl_id: 1,
|
||||
lst_price: 1,
|
||||
standard_price: 0,
|
||||
display_name: "TIP",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
pos_categ_ids: [1],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
product_tmpl_id: 5,
|
||||
lst_price: 100,
|
||||
standard_price: 0,
|
||||
display_name: "TEST",
|
||||
product_tag_ids: [],
|
||||
barcode: "test_test",
|
||||
pos_categ_ids: [1],
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
product_tmpl_id: 6,
|
||||
lst_price: 100,
|
||||
standard_price: 0,
|
||||
display_name: "TEST 2",
|
||||
product_tag_ids: [],
|
||||
pos_categ_ids: [2],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
product_tmpl_id: 7,
|
||||
lst_price: 750,
|
||||
standard_price: 0,
|
||||
display_name: "Office combo",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
pos_categ_ids: [],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
product_tmpl_id: 8,
|
||||
lst_price: 300,
|
||||
standard_price: 0,
|
||||
display_name: "Wood chair",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
pos_categ_ids: [],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
product_tmpl_id: 9,
|
||||
lst_price: 450,
|
||||
standard_price: 0,
|
||||
display_name: "Steel chair",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
pos_categ_ids: [],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
product_tmpl_id: 10,
|
||||
lst_price: 600,
|
||||
standard_price: 0,
|
||||
display_name: "Wood chair",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
pos_categ_ids: [],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
product_tmpl_id: 11,
|
||||
lst_price: 700,
|
||||
standard_price: 0,
|
||||
display_name: "Steel desk",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
pos_categ_ids: [],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
product_tmpl_id: 12,
|
||||
lst_price: 14,
|
||||
standard_price: 0,
|
||||
display_name: "Bacon burger",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
pos_categ_ids: [4],
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
product_tmpl_id: 13,
|
||||
lst_price: 12,
|
||||
standard_price: 0,
|
||||
display_name: "Pizza margarita",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
pos_categ_ids: [5],
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
product_tmpl_id: 14,
|
||||
lst_price: 3.4,
|
||||
standard_price: 0,
|
||||
display_name: "Club sandwich",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
pos_categ_ids: [4],
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
product_tmpl_id: 15,
|
||||
lst_price: 1000,
|
||||
standard_price: 0,
|
||||
display_name: "Accounting Test Product 1",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
product_tmpl_id: 16,
|
||||
lst_price: 100,
|
||||
standard_price: 0,
|
||||
display_name: "Accounting Test Product 2",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
product_tmpl_id: 17,
|
||||
lst_price: 100,
|
||||
standard_price: 0,
|
||||
display_name: "Multi Category Product",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
product_tmpl_id: 18,
|
||||
lst_price: 0,
|
||||
standard_price: 0,
|
||||
display_name: "Free Product - Wood chair",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
pos_categ_ids: [],
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
product_tmpl_id: 24,
|
||||
lst_price: 100,
|
||||
standard_price: 0,
|
||||
display_name: "Tax Included Product",
|
||||
product_tag_ids: [],
|
||||
barcode: false,
|
||||
default_code: false,
|
||||
product_template_attribute_value_ids: [],
|
||||
product_template_variant_value_ids: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductTag extends models.ServerModel {
|
||||
_name = "product.tag";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["name", "pos_description", "color", "has_image", "write_date"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,588 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductTemplate extends models.ServerModel {
|
||||
_name = "product.template";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"id",
|
||||
"display_name",
|
||||
"standard_price",
|
||||
"categ_id",
|
||||
"pos_categ_ids",
|
||||
"taxes_id",
|
||||
"barcode",
|
||||
"name",
|
||||
"list_price",
|
||||
"is_favorite",
|
||||
"default_code",
|
||||
"to_weight",
|
||||
"uom_id",
|
||||
"description_sale",
|
||||
"description",
|
||||
"tracking",
|
||||
"type",
|
||||
"service_tracking",
|
||||
"is_storable",
|
||||
"write_date",
|
||||
"color",
|
||||
"pos_sequence",
|
||||
"available_in_pos",
|
||||
"attribute_line_ids",
|
||||
"active",
|
||||
"image_128",
|
||||
"combo_ids",
|
||||
"product_variant_ids",
|
||||
"public_description",
|
||||
"pos_optional_product_ids",
|
||||
"sequence",
|
||||
"product_tag_ids",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "TIP",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [],
|
||||
taxes_id: [],
|
||||
barcode: false,
|
||||
name: "TIP",
|
||||
list_price: 100,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
combo_ids: [],
|
||||
product_variant_ids: [1],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
display_name: "TEST",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [1],
|
||||
taxes_id: [1],
|
||||
barcode: false,
|
||||
name: "TEST",
|
||||
list_price: 100,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
combo_ids: [],
|
||||
product_variant_ids: [5],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
display_name: "TEST 2",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [2],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "TEST 2",
|
||||
list_price: 100,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
combo_ids: [],
|
||||
product_variant_ids: [6],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
display_name: "Product combo",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [2],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Product combo",
|
||||
list_price: 100,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "combo",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
combo_ids: [1, 2],
|
||||
product_variant_ids: [7],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
display_name: "Wood chair",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [2],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Wood chair",
|
||||
list_price: 300,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [8],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
display_name: "Steel chair",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [2],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Steel chair",
|
||||
list_price: 450,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [9],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
display_name: "Wood desk",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [2],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Wood desk",
|
||||
list_price: 600,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [10],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
display_name: "Steel desk",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [2],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Steel desk",
|
||||
list_price: 700,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [11],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
display_name: "Bacon burger",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [4],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Bacon burger",
|
||||
list_price: 14,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [12],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
display_name: "Pizza margarita",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [5],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Pizza margarita",
|
||||
list_price: 12,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [13],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
display_name: "Club sandwich",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [3],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Club sandwich",
|
||||
list_price: 3.4,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [14],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
display_name: "Accounting Test Product 1",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Accounting Test Product 1",
|
||||
list_price: 1000,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [15],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
display_name: "Accounting Test Product 2",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [],
|
||||
taxes_id: [1, 2],
|
||||
barcode: false,
|
||||
name: "Accounting Test Product 2",
|
||||
list_price: 100,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [16],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
display_name: "Multi Category Product",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [1, 2],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Multi Category Product",
|
||||
list_price: 100,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
combo_ids: [],
|
||||
product_variant_ids: [17],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
display_name: "Free Product - Wood chair",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [2],
|
||||
taxes_id: [2],
|
||||
barcode: false,
|
||||
name: "Free Product - Wood chair",
|
||||
list_price: 0,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [18],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
display_name: "Tax Included Product",
|
||||
standard_price: 0,
|
||||
categ_id: false,
|
||||
pos_categ_ids: [2],
|
||||
taxes_id: [4],
|
||||
barcode: false,
|
||||
name: "Tax Included Product",
|
||||
list_price: 300,
|
||||
is_favorite: false,
|
||||
default_code: false,
|
||||
to_weight: false,
|
||||
uom_id: 1,
|
||||
description_sale: false,
|
||||
description: false,
|
||||
tracking: "none",
|
||||
type: "consu",
|
||||
service_tracking: "no",
|
||||
is_storable: false,
|
||||
write_date: "2025-07-03 13:04:14",
|
||||
color: 0,
|
||||
pos_sequence: 5,
|
||||
available_in_pos: true,
|
||||
attribute_line_ids: [],
|
||||
active: true,
|
||||
image_128: false,
|
||||
product_variant_ids: [24],
|
||||
public_description: false,
|
||||
pos_optional_product_ids: [],
|
||||
sequence: 1,
|
||||
product_tag_ids: [],
|
||||
},
|
||||
];
|
||||
get_product_info_pos() {
|
||||
return {
|
||||
all_prices: { tax_details: [] },
|
||||
pricelists: [],
|
||||
warehouses: [],
|
||||
suppliers: [],
|
||||
variants: [],
|
||||
optional_products: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductTemplateAttributeExclusion extends models.ServerModel {
|
||||
_name = "product.template.attribute.exclusion";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["value_ids", "product_template_attribute_value_id"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductTemplateAttributeLine extends models.ServerModel {
|
||||
_name = "product.template.attribute.line";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["display_name", "attribute_id", "product_template_value_ids"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductTemplateAttributeValue extends models.ServerModel {
|
||||
_name = "product.template.attribute.value";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"attribute_id",
|
||||
"attribute_line_id",
|
||||
"product_attribute_value_id",
|
||||
"price_extra",
|
||||
"name",
|
||||
"is_custom",
|
||||
"html_color",
|
||||
"image",
|
||||
"exclude_for",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ProductUom extends models.ServerModel {
|
||||
_name = "product.uom";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "barcode", "product_id", "uom_id"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { webModels } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResCompany extends webModels.ResCompany {
|
||||
_name = "res.company";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"id",
|
||||
"currency_id",
|
||||
"email",
|
||||
"website",
|
||||
"company_registry",
|
||||
"vat",
|
||||
"name",
|
||||
"phone",
|
||||
"partner_id",
|
||||
"country_id",
|
||||
"state_id",
|
||||
"tax_calculation_rounding_method",
|
||||
"nomenclature_id",
|
||||
"point_of_sale_use_ticket_qr_code",
|
||||
"point_of_sale_ticket_unique_code",
|
||||
"point_of_sale_ticket_portal_url_display_mode",
|
||||
"street",
|
||||
"city",
|
||||
"zip",
|
||||
"account_fiscal_country_id",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
...webModels.ResCompany._records,
|
||||
{
|
||||
id: 250,
|
||||
currency_id: 1,
|
||||
email: false,
|
||||
website: false,
|
||||
company_registry: false,
|
||||
vat: false,
|
||||
name: "My Company",
|
||||
phone: "",
|
||||
partner_id: 1,
|
||||
country_id: 233,
|
||||
state_id: false,
|
||||
tax_calculation_rounding_method: "round_per_line",
|
||||
point_of_sale_use_ticket_qr_code: true,
|
||||
point_of_sale_ticket_unique_code: false,
|
||||
point_of_sale_ticket_portal_url_display_mode: "qr_code_and_url",
|
||||
street: "",
|
||||
city: "",
|
||||
zip: "",
|
||||
account_fiscal_country_id: 233,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { webModels } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResCountry extends webModels.ResCountry {
|
||||
_name = "res.country";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "code", "vat_label"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
...webModels.ResCountry._records,
|
||||
{
|
||||
id: 233,
|
||||
name: "United States",
|
||||
code: "US",
|
||||
vat_label: "",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResCountryState extends models.ServerModel {
|
||||
_name = "res.country.state";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "code", "country_id"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 69,
|
||||
name: "Armed Forces Europe",
|
||||
code: "AE",
|
||||
country_id: 233,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { webModels } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResCurrency extends webModels.ResCurrency {
|
||||
_name = "res.currency";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"id",
|
||||
"name",
|
||||
"symbol",
|
||||
"position",
|
||||
"rounding",
|
||||
"rate",
|
||||
"decimal_places",
|
||||
"iso_numeric",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "USD",
|
||||
symbol: "$",
|
||||
position: "before",
|
||||
rounding: 0.01,
|
||||
rate: 1.0,
|
||||
decimal_places: 2,
|
||||
iso_numeric: 840,
|
||||
},
|
||||
{
|
||||
id: 125,
|
||||
name: "EUR",
|
||||
symbol: "€",
|
||||
position: "after",
|
||||
rounding: 0.01,
|
||||
rate: 1.0,
|
||||
decimal_places: 2,
|
||||
iso_numeric: 978,
|
||||
},
|
||||
...webModels.ResCurrency._records.filter((record) => record.id !== 1),
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResLang extends models.ServerModel {
|
||||
_name = "res.lang";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "code", "flag_image_url", "display_name"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "English (US)",
|
||||
code: "en_US",
|
||||
flag_image_url: "/base/static/img/country_flags/us.png",
|
||||
display_name: "English (US)",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { ResPartner as MailResPartner } from "@mail/../tests/mock_server/mock_models/res_partner";
|
||||
|
||||
export class ResPartner extends MailResPartner {
|
||||
_name = "res.partner";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [
|
||||
"id",
|
||||
"name",
|
||||
"street",
|
||||
"street2",
|
||||
"city",
|
||||
"state_id",
|
||||
"country_id",
|
||||
"vat",
|
||||
"lang",
|
||||
"phone",
|
||||
"zip",
|
||||
"email",
|
||||
"barcode",
|
||||
"write_date",
|
||||
"property_product_pricelist",
|
||||
"parent_name",
|
||||
"pos_contact_address",
|
||||
"invoice_emails",
|
||||
"company_type",
|
||||
"fiscal_position_id",
|
||||
];
|
||||
}
|
||||
|
||||
_records = [
|
||||
...MailResPartner.prototype.constructor._records,
|
||||
{
|
||||
id: 3,
|
||||
name: "Administrator",
|
||||
street: false,
|
||||
street2: false,
|
||||
city: false,
|
||||
state_id: false,
|
||||
country_id: false,
|
||||
vat: false,
|
||||
lang: "en_US",
|
||||
phone: false,
|
||||
zip: false,
|
||||
email: false,
|
||||
barcode: false,
|
||||
write_date: "2025-07-03 12:38:12",
|
||||
property_product_pricelist: false,
|
||||
parent_name: false,
|
||||
pos_contact_address: "\n\n \n",
|
||||
invoice_emails: "",
|
||||
company_type: "person",
|
||||
fiscal_position_id: false,
|
||||
credit_limit: 0.0,
|
||||
use_partner_credit_limit: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "User1",
|
||||
street: false,
|
||||
street2: false,
|
||||
city: false,
|
||||
state_id: false,
|
||||
country_id: false,
|
||||
vat: false,
|
||||
lang: "en_US",
|
||||
phone: false,
|
||||
zip: false,
|
||||
email: false,
|
||||
barcode: false,
|
||||
write_date: "2025-08-03 12:12:12",
|
||||
property_product_pricelist: false,
|
||||
parent_name: false,
|
||||
pos_contact_address: "\n\n \n",
|
||||
invoice_emails: "",
|
||||
company_type: "person",
|
||||
fiscal_position_id: false,
|
||||
credit_limit: 0.0,
|
||||
use_partner_credit_limit: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { ResUsers as MailResUsers } from "@mail/../tests/mock_server/mock_models/res_users";
|
||||
|
||||
export class ResUsers extends MailResUsers {
|
||||
_name = "res.users";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "partner_id", "all_group_ids"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
...MailResUsers.prototype.constructor._records,
|
||||
{
|
||||
id: 2,
|
||||
name: "Administrator",
|
||||
partner_id: 3,
|
||||
role: "manager",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "User1",
|
||||
partner_id: 4,
|
||||
role: "cashier",
|
||||
},
|
||||
];
|
||||
|
||||
_post_read_pos_data(records) {
|
||||
records.forEach((user) => {
|
||||
if (user.id === 2) {
|
||||
user._role = "manager";
|
||||
} else {
|
||||
user._role = "cashier";
|
||||
}
|
||||
});
|
||||
return records;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResourceCalendar extends models.ServerModel {
|
||||
_name = "resource.calendar";
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Takeaway",
|
||||
attendance_ids: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class ResourceCalendarAttendance extends models.ServerModel {
|
||||
_name = "resource.calendar.attendance";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "hour_from", "hour_to", "dayofweek", "day_period"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 1,
|
||||
hour_from: 12,
|
||||
hour_to: 15,
|
||||
dayofweek: "1",
|
||||
day_period: "lunch",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
hour_from: 18,
|
||||
hour_to: 22,
|
||||
dayofweek: "1",
|
||||
day_period: "evening",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
hour_from: 12,
|
||||
hour_to: 15,
|
||||
dayofweek: "2",
|
||||
day_period: "lunch",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
hour_from: 18,
|
||||
hour_to: 22,
|
||||
dayofweek: "2",
|
||||
day_period: "evening",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
hour_from: 12,
|
||||
hour_to: 15,
|
||||
dayofweek: "3",
|
||||
day_period: "lunch",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
hour_from: 18,
|
||||
hour_to: 22,
|
||||
dayofweek: "3",
|
||||
day_period: "evening",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
hour_from: 12,
|
||||
hour_to: 15,
|
||||
dayofweek: "4",
|
||||
day_period: "lunch",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
hour_from: 18,
|
||||
hour_to: 22,
|
||||
dayofweek: "4",
|
||||
day_period: "evening",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
hour_from: 12,
|
||||
hour_to: 15,
|
||||
dayofweek: "5",
|
||||
day_period: "lunch",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
hour_from: 18,
|
||||
hour_to: 22,
|
||||
dayofweek: "5",
|
||||
day_period: "evening",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class StockPickingType extends models.ServerModel {
|
||||
_name = "stock.picking.type";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "use_create_lots", "use_existing_lots"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 9,
|
||||
use_create_lots: true,
|
||||
use_existing_lots: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class StockRoute extends models.ServerModel {
|
||||
_name = "stock.route";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class StockWarehouse extends models.ServerModel {
|
||||
_name = "stock.warehouse";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import { models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class UomUom extends models.ServerModel {
|
||||
_name = "uom.uom";
|
||||
|
||||
_load_pos_data_fields() {
|
||||
return ["id", "name", "factor", "is_pos_groupable", "parent_path", "rounding"];
|
||||
}
|
||||
|
||||
_records = [
|
||||
{
|
||||
id: 5,
|
||||
name: "Days",
|
||||
factor: 8.0,
|
||||
is_pos_groupable: false,
|
||||
parent_path: "4/5/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Pack of 6",
|
||||
factor: 6.0,
|
||||
is_pos_groupable: true,
|
||||
parent_path: "1/2/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "m",
|
||||
factor: 1000.0,
|
||||
is_pos_groupable: false,
|
||||
parent_path: "6/7/8/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: "kg",
|
||||
factor: 1000.0,
|
||||
is_pos_groupable: false,
|
||||
parent_path: "14/15/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: "Ton",
|
||||
factor: 1000000.0,
|
||||
is_pos_groupable: false,
|
||||
parent_path: "14/15/16/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "L",
|
||||
factor: 1000.0,
|
||||
is_pos_groupable: false,
|
||||
parent_path: "11/12/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Hours",
|
||||
factor: 1.0,
|
||||
is_pos_groupable: false,
|
||||
parent_path: "4/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: "Units",
|
||||
factor: 1.0,
|
||||
is_pos_groupable: true,
|
||||
parent_path: "1/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: "g",
|
||||
factor: 1.0,
|
||||
is_pos_groupable: false,
|
||||
parent_path: "14/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "ml",
|
||||
factor: 1.0,
|
||||
is_pos_groupable: false,
|
||||
parent_path: "11/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "mm",
|
||||
factor: 1.0,
|
||||
is_pos_groupable: false,
|
||||
parent_path: "6/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "m²",
|
||||
factor: 1.0,
|
||||
is_pos_groupable: false,
|
||||
parent_path: "10/",
|
||||
rounding: 0.01,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
odoo.define('point_of_sale.test_env', async function (require) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Many components in PoS are dependent on the PosGlobalState instance (pos).
|
||||
* Therefore, for unit tests that require pos in the Components' env, we
|
||||
* prepared here a test env maker (makePosTestEnv) based on
|
||||
* makeTestEnvironment of web.
|
||||
*/
|
||||
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
const env = require('point_of_sale.env');
|
||||
const { PosGlobalState } = require('point_of_sale.models');
|
||||
const cleanup = require("@web/../tests/helpers/cleanup");
|
||||
|
||||
// We override this method in the pos unit tests to prevent the unnecessary error in the web tests.
|
||||
cleanup.registerCleanup = () => {}
|
||||
|
||||
await env.session.is_bound;
|
||||
const pos = PosGlobalState.create({ env });
|
||||
await pos.load_server_data();
|
||||
|
||||
/**
|
||||
* @param {Object} env default env
|
||||
* @param {Function} providedRPC mock rpc
|
||||
* @param {Function} providedDoAction mock do_action
|
||||
*/
|
||||
function makePosTestEnv(env = {}, providedRPC = null, providedDoAction = null) {
|
||||
env = Object.assign(env, { pos });
|
||||
return makeTestEnvironment(env, providedRPC);
|
||||
}
|
||||
|
||||
return makePosTestEnv;
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("getAllChildren", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const category = store.models["pos.category"].get(3);
|
||||
const children = category.getAllChildren();
|
||||
expect(children.map((c) => c.id).sort()).toEqual([3, 4, 5]);
|
||||
});
|
||||
|
||||
test("get allParents", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const category = store.models["pos.category"].get(5);
|
||||
const parent = category.allParents;
|
||||
expect(parent[0].id).toEqual(3);
|
||||
});
|
||||
|
||||
test("get associatedProducts", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const category = store.models["pos.category"].get(3);
|
||||
const associatedProducts = category.associatedProducts;
|
||||
expect(associatedProducts.map((p) => p.id).sort()).toEqual([12, 13, 14]);
|
||||
});
|
||||
|
|
@ -0,0 +1,466 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { getFilledOrder, setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("uiState", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
|
||||
expect(order.uiState).toEqual({
|
||||
unmerge: {},
|
||||
lastPrints: [],
|
||||
lineToRefund: {},
|
||||
displayed: true,
|
||||
booked: false,
|
||||
screen_data: {},
|
||||
selected_orderline_uuid: undefined,
|
||||
selected_paymentline_uuid: undefined,
|
||||
TipScreen: {
|
||||
inputTipAmount: "",
|
||||
},
|
||||
requiredPartnerDetails: {},
|
||||
});
|
||||
});
|
||||
|
||||
test("totalQuantity", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
expect(order.totalQuantity).toBe(5);
|
||||
});
|
||||
|
||||
test("setPreset", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
const inPreset = store.models["pos.preset"].get(1);
|
||||
const outPreset = store.models["pos.preset"].get(2);
|
||||
|
||||
expect(order.pricelist_id).toBe(inPreset.pricelist_id);
|
||||
expect(order.fiscal_position_id).toBe(inPreset.fiscal_position_id);
|
||||
|
||||
order.setPreset(outPreset);
|
||||
|
||||
expect(order.pricelist_id).toBe(outPreset.pricelist_id);
|
||||
expect(order.fiscal_position_id).toBe(outPreset.fiscal_position_id);
|
||||
|
||||
order.setPreset(inPreset);
|
||||
|
||||
expect(order.pricelist_id).toBe(inPreset.pricelist_id);
|
||||
expect(order.fiscal_position_id).toBe(inPreset.fiscal_position_id);
|
||||
});
|
||||
|
||||
test("updateLastOrderChange", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
order.setGeneralCustomerNote("Customer note");
|
||||
order.setInternalNote("Internal note");
|
||||
order.updateLastOrderChange();
|
||||
expect(order.last_order_preparation_change.general_customer_note).toBe("Customer note");
|
||||
expect(order.last_order_preparation_change.internal_note).toBe("Internal note");
|
||||
});
|
||||
|
||||
test("removeOrderline", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
order.general_customer_note = "Some note";
|
||||
const line1 = order.lines[0];
|
||||
const line2 = order.lines[1];
|
||||
expect(order.getSelectedOrderline()).toBe(line2);
|
||||
order.removeOrderline(line2);
|
||||
expect(order.general_customer_note).toBe("Some note");
|
||||
expect(order.getSelectedOrderline()).toBe(line1);
|
||||
order.removeOrderline(line1);
|
||||
// General customer note should be removed when removing the last order line
|
||||
expect(order.general_customer_note).toBe("");
|
||||
});
|
||||
|
||||
test("addPaymentline", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const cashPaymentMethod = store.models["pos.payment.method"].get(1);
|
||||
// Test that the payment line is correctly created
|
||||
const result = order.addPaymentline(cashPaymentMethod);
|
||||
const cashPaymentLine = result.data;
|
||||
expect(cashPaymentLine.payment_method_id.id).toBe(cashPaymentMethod.id);
|
||||
expect(cashPaymentLine.amount).toBe(17.85);
|
||||
// Update the cash payment line amount to 10 to confirm that the second created cash payment
|
||||
// line will take the remaining amount to pay (7.85)
|
||||
cashPaymentLine.setAmount(10);
|
||||
const result2 = order.addPaymentline(cashPaymentMethod);
|
||||
expect(result2.data.payment_method_id.id).toBe(cashPaymentMethod.id);
|
||||
expect(result2.data.amount).toBe(7.85);
|
||||
});
|
||||
|
||||
test("getTotalDiscount", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const discount = order.getTotalDiscount();
|
||||
expect(discount).toBe(0);
|
||||
const taxTotals = order.prices.taxDetails;
|
||||
expect(taxTotals.base_amount).toBe(15);
|
||||
expect(taxTotals.total_amount).toBe(17.85);
|
||||
expect(taxTotals.tax_amount_currency).toBe(2.85);
|
||||
|
||||
//Compute total of discount on the order
|
||||
const line1 = order.lines[0];
|
||||
const line2 = order.lines[1];
|
||||
line1.setDiscount(20);
|
||||
line2.setDiscount(50);
|
||||
|
||||
expect(order.getTotalDiscount()).toBe(5.82);
|
||||
const taxTotalsWDiscount = order.prices.taxDetails;
|
||||
expect(taxTotalsWDiscount.base_amount).toBe(10.2);
|
||||
expect(taxTotalsWDiscount.total_amount).toBe(12.03);
|
||||
expect(taxTotalsWDiscount.tax_amount_currency).toBe(1.83);
|
||||
});
|
||||
|
||||
test("preventRoundingErrorsCombo", async () => {
|
||||
const store = await setupPosEnv();
|
||||
store.models["product.product"].get(7).taxes_id = [1];
|
||||
store.models["product.product"].get(7).lst_price = 50;
|
||||
store.models["product.product"].get(8).taxes_id = [1];
|
||||
store.models["product.product"].get(9).taxes_id = [1];
|
||||
store.models["pos.preset"].get(1).pricelist_id = false;
|
||||
store.models["product.combo"].get(1).qty_free = 3;
|
||||
store.models["product.combo"].get(1).base_price = 10;
|
||||
const comboProduct1 = store.models["product.combo.item"].get(1);
|
||||
const comboProduct2 = store.models["product.combo.item"].get(2);
|
||||
comboProduct2.extra_price = 0;
|
||||
const template = store.models["product.template"].get(7);
|
||||
const order = store.addNewOrder();
|
||||
const order2 = store.addNewOrder();
|
||||
const order3 = store.addNewOrder();
|
||||
|
||||
// 3 of the same product
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: template,
|
||||
payload: [[{ combo_item_id: comboProduct1, qty: 3 }]],
|
||||
qty: 1,
|
||||
},
|
||||
order
|
||||
);
|
||||
order.setOrderPrices();
|
||||
expect(order.amount_total).toBe(57.5);
|
||||
expect(order.lines[1].qty).toBe(2);
|
||||
expect(order.lines[1].price_unit).toBe(16.67);
|
||||
expect(order.lines[2].qty).toBe(1);
|
||||
expect(Math.round(order.lines[2].price_unit * 100) / 100).toBe(16.66);
|
||||
|
||||
// 2 different products
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: template,
|
||||
payload: [
|
||||
[
|
||||
{ combo_item_id: comboProduct1, qty: 1 },
|
||||
{ combo_item_id: comboProduct2, qty: 2 },
|
||||
],
|
||||
],
|
||||
qty: 1,
|
||||
},
|
||||
order2
|
||||
);
|
||||
order2.setOrderPrices();
|
||||
expect(order2.amount_total).toBe(57.5);
|
||||
expect(order2.lines[1].price_unit).toBe(16.67);
|
||||
expect(order2.lines[1].qty).toBe(1);
|
||||
expect(order2.lines[2].price_unit).toBe(16.67);
|
||||
expect(order2.lines[2].qty).toBe(1);
|
||||
expect(Math.round(order2.lines[3].price_unit * 100) / 100).toBe(16.66);
|
||||
expect(order2.lines[3].qty).toBe(1);
|
||||
|
||||
// 3 of the same product and 3 of the same extra items
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: template,
|
||||
payload: [
|
||||
[{ combo_item_id: comboProduct1, qty: 3 }],
|
||||
[{ combo_item_id: comboProduct1, qty: 3 }],
|
||||
],
|
||||
qty: 1,
|
||||
},
|
||||
order3
|
||||
);
|
||||
order3.setOrderPrices();
|
||||
expect(order3.amount_total).toBe(92);
|
||||
expect(order3.lines[1].qty).toBe(2);
|
||||
expect(order3.lines[1].price_unit).toBe(16.67);
|
||||
expect(Math.round(order3.lines[2].price_unit * 100) / 100).toBe(16.66);
|
||||
expect(order3.lines[2].qty).toBe(1);
|
||||
expect(order3.lines[3].qty).toBe(3);
|
||||
expect(order3.lines[3].price_unit).toBe(10);
|
||||
});
|
||||
|
||||
test("customer requirements", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const preset = store.models["pos.preset"].get(3); // Address Required Preset
|
||||
const partner = store.models["res.partner"].get(3); // Customer Without Address
|
||||
const order = store.addNewOrder();
|
||||
order.preset_id = preset;
|
||||
|
||||
// No partner
|
||||
expect(order.presetRequirementsFilled).toBe(false);
|
||||
expect(order.uiState.requiredPartnerDetails.field).toBe("Customer");
|
||||
expect(order.uiState.requiredPartnerDetails.message).toBe(
|
||||
"Please add a valid customer to the order."
|
||||
);
|
||||
|
||||
// Partner
|
||||
order.partner_id = partner;
|
||||
expect(order.presetRequirementsFilled).toBe(true);
|
||||
});
|
||||
|
||||
test("Address requirements", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const preset = store.models["pos.preset"].get(4); // Address Required Preset
|
||||
const partner = store.models["res.partner"].get(3); // Customer Without Address
|
||||
const order = store.addNewOrder();
|
||||
order.preset_id = preset;
|
||||
order.partner_id = partner;
|
||||
|
||||
expect(order.presetRequirementsFilled).toBe(false);
|
||||
expect(order.uiState.requiredPartnerDetails.field).toBe("Address");
|
||||
expect(order.uiState.requiredPartnerDetails.message).toBe(
|
||||
"The selected customer needs an address."
|
||||
);
|
||||
|
||||
// Partner with address
|
||||
partner.street = "test abc";
|
||||
expect(order.presetRequirementsFilled).toBe(true);
|
||||
});
|
||||
|
||||
test("slot requirement preset", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const preset = store.models["pos.preset"].get(2); // Time Slot Preset
|
||||
const order = store.addNewOrder();
|
||||
order.preset_id = preset;
|
||||
|
||||
// No slot
|
||||
expect(order.presetRequirementsFilled).toBe(false);
|
||||
expect(order.uiState.requiredPartnerDetails.field).toBe("Slot");
|
||||
expect(order.uiState.requiredPartnerDetails.message).toBe(
|
||||
"Please select a time slot before proceeding."
|
||||
);
|
||||
|
||||
// Slot set
|
||||
order.preset_time = "2025-08-11 14:00:00";
|
||||
expect(order.presetRequirementsFilled).toBe(true);
|
||||
});
|
||||
|
||||
test("isCustomerRequired", async () => {
|
||||
const posStore = await setupPosEnv();
|
||||
const order = await getFilledOrder(posStore);
|
||||
const existingPartner = posStore.models["res.partner"].get(3);
|
||||
|
||||
expect(order.isCustomerRequired).toBe(false);
|
||||
{
|
||||
// preset - name identification
|
||||
const namePreset = posStore.models["pos.preset"].get(3);
|
||||
order.preset_id = namePreset;
|
||||
expect(order.isCustomerRequired).toBe(true);
|
||||
// with floating order name
|
||||
order.floating_order_name = "TEST-P";
|
||||
expect(order.isCustomerRequired).toBe(false);
|
||||
order.floating_order_name = "";
|
||||
// with assigned partner
|
||||
order.partner_id = existingPartner;
|
||||
expect(order.isCustomerRequired).toBe(false);
|
||||
order.partner_id = false;
|
||||
}
|
||||
{
|
||||
// preset - address identification
|
||||
const addressPreset = posStore.models["pos.preset"].get(4);
|
||||
order.preset_id = addressPreset;
|
||||
expect(order.isCustomerRequired).toBe(true);
|
||||
// with assigned partner
|
||||
order.partner_id = existingPartner;
|
||||
expect(order.isCustomerRequired).toBe(false);
|
||||
order.partner_id = false;
|
||||
}
|
||||
{
|
||||
// order invoicing
|
||||
order.preset_id = false;
|
||||
order.to_invoice = true;
|
||||
expect(order.isCustomerRequired).toBe(true);
|
||||
order.to_invoice = false;
|
||||
}
|
||||
{
|
||||
// split payment (customer account)
|
||||
const customerAccountMethod = posStore.models["pos.payment.method"].get(3);
|
||||
order.addPaymentline(customerAccountMethod);
|
||||
expect(order.isCustomerRequired).toBe(true);
|
||||
order.partner_id = existingPartner;
|
||||
expect(order.isCustomerRequired).toBe(false);
|
||||
order.partner_id = false;
|
||||
order.removePaymentline(order.payment_ids[0]);
|
||||
}
|
||||
expect(order.isCustomerRequired).toBe(false);
|
||||
});
|
||||
|
||||
test("setShippingDate and getShippingDate with Luxon", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
|
||||
const testDate = "2019-03-11";
|
||||
order.setShippingDate(testDate);
|
||||
|
||||
expect(order.shipping_date.toISODate()).toBe(testDate);
|
||||
expect(typeof order.getShippingDate()).toBe("string");
|
||||
order.setShippingDate(null);
|
||||
expect(order.getShippingDate()).toBeEmpty();
|
||||
});
|
||||
|
||||
test("[get prices] check prices and taxes", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const data = order.prices;
|
||||
|
||||
// Check taxes on order base_amount is 15 with 15% taxes
|
||||
const orderTaxes = data.taxDetails;
|
||||
expect(orderTaxes.base_amount).toBe(15.0);
|
||||
expect(orderTaxes.total_amount).toBe(17.85);
|
||||
expect(orderTaxes.tax_amount).toBe(2.85);
|
||||
|
||||
// Order prices data also return the prices of all lines
|
||||
// Check first line with a price_unit of 3 and 3 qty
|
||||
const line1Data = data.baseLineByLineUuids[order.lines[0].uuid].tax_details;
|
||||
expect(line1Data.total_excluded).toBe(9.0);
|
||||
expect(line1Data.total_included).toBe(10.35);
|
||||
expect(line1Data.taxes_data[0].tax_amount).toBe(1.35);
|
||||
|
||||
// Check second line with a price_unit of 3 and 2 qty
|
||||
const line2Data = data.baseLineByLineUuids[order.lines[1].uuid].tax_details;
|
||||
expect(line2Data.total_excluded).toBe(6.0);
|
||||
expect(line2Data.total_included).toBe(7.5);
|
||||
expect(line2Data.taxes_data[0].tax_amount).toBe(1.5);
|
||||
|
||||
// Check with a discount on first line of 30%
|
||||
order.lines[0].setDiscount(30);
|
||||
const dataWDiscount = order.prices;
|
||||
const orderTaxesWDiscount = dataWDiscount.taxDetails;
|
||||
expect(orderTaxesWDiscount.base_amount).toBe(12.3);
|
||||
expect(orderTaxesWDiscount.total_amount).toBe(14.75);
|
||||
expect(orderTaxesWDiscount.tax_amount).toBe(2.45);
|
||||
|
||||
// Check first line with a price_unit of 3, 3 qty and 30% discount
|
||||
const line1DataWDiscount = dataWDiscount.baseLineByLineUuids[order.lines[0].uuid].tax_details;
|
||||
expect(line1DataWDiscount.total_excluded).toBe(6.3);
|
||||
expect(line1DataWDiscount.total_included).toBe(7.25);
|
||||
expect(line1DataWDiscount.taxes_data[0].tax_amount).toBe(0.95);
|
||||
expect(line1DataWDiscount.discount_amount).toBe(3.1);
|
||||
|
||||
// No discount values should still represent the line without discount
|
||||
expect(line1DataWDiscount.no_discount_total_excluded).toBe(9.0);
|
||||
expect(line1DataWDiscount.no_discount_total_included).toBe(10.35);
|
||||
expect(line1DataWDiscount.no_discount_taxes_data[0].tax_amount).toBe(1.35);
|
||||
});
|
||||
|
||||
test("showChange remains true when change line name is translated", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const cashPaymentMethod = store.models["pos.payment.method"].get(1);
|
||||
|
||||
const { data: paymentLine } = order.addPaymentline(cashPaymentMethod);
|
||||
const overpaidAmount = order.totalDue + 5;
|
||||
paymentLine.setAmount(overpaidAmount);
|
||||
|
||||
const changeAmount = paymentLine.getAmount() - order.totalDue;
|
||||
const changeLine = order.models["pos.payment"].create({
|
||||
pos_order_id: order,
|
||||
payment_method_id: cashPaymentMethod,
|
||||
});
|
||||
changeLine.setAmount(-changeAmount);
|
||||
changeLine.name = "Retour"; // Simulate translated "return"
|
||||
changeLine.is_change = true;
|
||||
|
||||
order.state = "paid";
|
||||
|
||||
expect(order.change).toBe(-changeAmount);
|
||||
expect(order.showChange).toBe(true);
|
||||
});
|
||||
|
||||
test("priceDoesntChangeWhenChangingPreset", async () => {
|
||||
const store = await setupPosEnv();
|
||||
store.models["pos.preset"].get(1).pricelist_id = false;
|
||||
const otherPreset = store.models["pos.preset"].get(2);
|
||||
store.models["product.combo"].get(1).qty_free = 2;
|
||||
const comboProduct1 = store.models["product.combo.item"].get(1);
|
||||
const comboProductExtra = store.models["product.combo.item"].get(2);
|
||||
const comboProduct2 = store.models["product.combo.item"].get(3);
|
||||
const template = store.models["product.template"].get(7);
|
||||
const order = store.addNewOrder();
|
||||
const order2 = store.addNewOrder();
|
||||
const order3 = store.addNewOrder();
|
||||
const order4 = store.addNewOrder();
|
||||
|
||||
// Normal flow with extras
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: template,
|
||||
payload: [
|
||||
[{ combo_item_id: comboProduct1, qty: 2 }],
|
||||
[{ combo_item_id: comboProduct2, qty: 2 }],
|
||||
],
|
||||
qty: 1,
|
||||
},
|
||||
order
|
||||
);
|
||||
order.setOrderPrices();
|
||||
let total = order.amount_total;
|
||||
order.setPreset(otherPreset);
|
||||
order.setOrderPrices();
|
||||
expect(order.amount_total).toBe(total);
|
||||
|
||||
// Normal flow
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: template,
|
||||
payload: [[{ combo_item_id: comboProduct1, qty: 2 }]],
|
||||
qty: 1,
|
||||
},
|
||||
order2
|
||||
);
|
||||
order2.setOrderPrices();
|
||||
total = order2.amount_total;
|
||||
order2.setPreset(otherPreset);
|
||||
order2.setOrderPrices();
|
||||
expect(order2.amount_total).toBe(total);
|
||||
|
||||
// Flow with products with extra price
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: template,
|
||||
payload: [
|
||||
[{ combo_item_id: comboProduct1, qty: 2 }],
|
||||
[{ combo_item_id: comboProductExtra, qty: 2 }],
|
||||
],
|
||||
qty: 1,
|
||||
},
|
||||
order3
|
||||
);
|
||||
order3.setOrderPrices();
|
||||
total = order3.amount_total;
|
||||
order3.setPreset(otherPreset);
|
||||
order3.setOrderPrices();
|
||||
expect(order3.amount_total).toBe(total);
|
||||
|
||||
// Flow with all the same product
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: template,
|
||||
payload: [
|
||||
[{ combo_item_id: comboProduct1, qty: 2 }],
|
||||
[{ combo_item_id: comboProduct1, qty: 2 }],
|
||||
],
|
||||
qty: 1,
|
||||
},
|
||||
order4
|
||||
);
|
||||
order4.setOrderPrices();
|
||||
total = order4.amount_total;
|
||||
order4.setPreset(otherPreset);
|
||||
order4.setOrderPrices();
|
||||
expect(order4.amount_total).toBe(total);
|
||||
});
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
import { test, expect, describe } from "@odoo/hoot";
|
||||
import { setupPosEnv, getFilledOrder } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
function getAllPricesData(otherData = {}) {
|
||||
return {
|
||||
"pos.order": [
|
||||
{
|
||||
id: 1,
|
||||
name: "Test Order",
|
||||
lines: [1],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Test Combo order",
|
||||
},
|
||||
],
|
||||
"pos.order.line": [
|
||||
{
|
||||
id: 1,
|
||||
order_id: 1,
|
||||
product_id: 5,
|
||||
price_unit: 100.0,
|
||||
qty: 2,
|
||||
tax_ids: [1],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
order_id: 2,
|
||||
product_id: 7,
|
||||
price_unit: 0.0,
|
||||
qty: 1,
|
||||
combo_line_ids: [3, 4],
|
||||
tax_ids: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
order_id: 2,
|
||||
product_id: 8,
|
||||
price_unit: 1,
|
||||
qty: 2,
|
||||
combo_parent_id: 2,
|
||||
tax_ids: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
order_id: 2,
|
||||
product_id: 10,
|
||||
price_unit: 8,
|
||||
qty: 1,
|
||||
combo_parent_id: 2,
|
||||
tax_ids: [],
|
||||
},
|
||||
],
|
||||
...otherData,
|
||||
};
|
||||
}
|
||||
|
||||
test("[get prices()] Base test", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const data = models.loadConnectedData(getAllPricesData());
|
||||
|
||||
const lineTax = data["pos.order.line"][0].prices;
|
||||
expect(lineTax.total_included).toBe(230.0);
|
||||
expect(lineTax.total_excluded).toBe(200.0);
|
||||
expect(lineTax.taxes_data[0].base_amount).toBe(200.0);
|
||||
expect(lineTax.taxes_data[0].tax_amount).toBe(30.0);
|
||||
|
||||
// Test with line qty = 0
|
||||
data["pos.order.line"][0].qty = 0;
|
||||
const zeroQtyLineTax = data["pos.order.line"][0].prices;
|
||||
expect(zeroQtyLineTax.total_included).toBe(0.0);
|
||||
expect(zeroQtyLineTax.total_excluded).toBe(0.0);
|
||||
expect(zeroQtyLineTax.taxes_data[0].base_amount).toBe(0.0);
|
||||
expect(zeroQtyLineTax.taxes_data[0].tax_amount).toBe(0.0);
|
||||
});
|
||||
|
||||
test("[get prices()] with discount applied", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const data = models.loadConnectedData(getAllPricesData());
|
||||
const orderLine = data["pos.order.line"][0];
|
||||
|
||||
// Prices with a discount of 10% applied: 230 * 0.9 = 207.0
|
||||
orderLine.setDiscount(10.0); // 10% discount
|
||||
const lineTax = orderLine.prices;
|
||||
expect(lineTax.total_included).toBe(207.0);
|
||||
expect(lineTax.total_excluded).toBe(180.0);
|
||||
expect(lineTax.taxes_data[0].tax_amount).toBe(27.0);
|
||||
|
||||
// Price with a discount of 100% applied
|
||||
orderLine.setDiscount(100.0);
|
||||
const updatedLineTax = orderLine.prices;
|
||||
expect(updatedLineTax.total_excluded).toBe(0.0);
|
||||
expect(updatedLineTax.total_included).toBe(0.0);
|
||||
expect(updatedLineTax.taxes_data[0].tax_amount).toBe(0.0);
|
||||
});
|
||||
|
||||
test("[get prices()] with multiple taxes settings", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const rawData = getAllPricesData();
|
||||
const product = models["product.product"].get(5);
|
||||
product.taxes_id = models["account.tax"].readMany([1, 2]); // Set two taxes on the product
|
||||
rawData["pos.order.line"][0].qty = 1;
|
||||
rawData["pos.order.line"][0].tax_ids = [1, 2];
|
||||
|
||||
const data = models.loadConnectedData(rawData);
|
||||
const orderLine = data["pos.order.line"][0];
|
||||
|
||||
// Test with two taxes applied (15% and 25%)
|
||||
const lineTax = orderLine.prices;
|
||||
expect(lineTax.total_excluded).toBe(100.0);
|
||||
expect(lineTax.total_included).toBe(140.0);
|
||||
expect(lineTax.taxes_data[0].tax_amount).toBe(15.0);
|
||||
expect(lineTax.taxes_data[1].tax_amount).toBe(25.0);
|
||||
|
||||
// Test with "include_base_amount" and "include_base_amount" to true for both taxes
|
||||
models["account.tax"].get(1).include_base_amount = true;
|
||||
models["account.tax"].get(2).include_base_amount = true;
|
||||
orderLine.order_id.triggerRecomputeAllPrices(); // Force the recompute because updating taxes do not trigger it
|
||||
const updatedLineTax = orderLine.prices;
|
||||
expect(updatedLineTax.total_excluded).toBe(100.0);
|
||||
expect(updatedLineTax.total_included).toBe(143.75);
|
||||
expect(updatedLineTax.taxes_data[0].tax_amount).toBe(15.0);
|
||||
expect(updatedLineTax.taxes_data[1].tax_amount).toBe(28.75);
|
||||
|
||||
// Test without any taxes
|
||||
product.taxes_id = [];
|
||||
orderLine.tax_ids = []; // Do not need to force the recompute here, changing line taxes does it for us
|
||||
const noTaxLine = data["pos.order.line"][0].prices;
|
||||
expect(noTaxLine.total_excluded).toBe(100.0);
|
||||
expect(noTaxLine.total_included).toBe(100.0);
|
||||
expect(noTaxLine.taxes_data).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("[get prices()] with fixed-amount tax", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const data = models.loadConnectedData(getAllPricesData());
|
||||
models["account.tax"].get(1).amount_type = "fixed";
|
||||
const orderLine = data["pos.order.line"][0];
|
||||
orderLine.qty = 3;
|
||||
orderLine.price_unit = 10.0;
|
||||
const lineTax = orderLine.prices;
|
||||
// 3 * 10 = 30, tax = 3 * 15 = 45
|
||||
expect(lineTax.total_excluded).toBe(30.0);
|
||||
expect(lineTax.total_included).toBe(75.0);
|
||||
expect(lineTax.taxes_data[0].tax_amount).toBe(45.0);
|
||||
});
|
||||
|
||||
test("[get prices()] with one price-included and one price-excluded tax", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const product = models["product.product"].get(5);
|
||||
const rawData = getAllPricesData();
|
||||
models["account.tax"].get(1).price_include = true;
|
||||
product.taxes_id = models["account.tax"].readMany([1, 2]);
|
||||
rawData["pos.order.line"][0].tax_ids = [1, 2];
|
||||
|
||||
const data = models.loadConnectedData(rawData);
|
||||
const orderLine = data["pos.order.line"][0];
|
||||
orderLine.qty = 1;
|
||||
orderLine.price_unit = 115.0; // price includes 15% tax
|
||||
const lineTax = orderLine.prices;
|
||||
// priceWithoutTax: 115 / 1.15 = 100, 25% tax = 25, priceWithTax = 115 + 25 = 140
|
||||
expect(lineTax.total_excluded).toBe(100.0);
|
||||
expect(lineTax.total_included).toBe(140.0);
|
||||
expect(lineTax.taxes_data[0].tax_amount).toBe(15.0);
|
||||
expect(lineTax.taxes_data[1].tax_amount).toBe(25.0);
|
||||
});
|
||||
|
||||
test("[get quantityStr] Base test", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const data = models.loadConnectedData(getAllPricesData());
|
||||
const orderLine = data["pos.order.line"][0];
|
||||
const qtyStr = orderLine.quantityStr; // Test with qty = 2
|
||||
expect(qtyStr.qtyStr).toBe("2");
|
||||
expect(qtyStr.unitPart).toBe("2");
|
||||
expect(qtyStr.decimalPoint).toBe(".");
|
||||
expect(qtyStr.decimalPart).toBe("");
|
||||
|
||||
orderLine.qty = 2.5; // Test with qty = 2.5
|
||||
const updatedQtyStr = orderLine.quantityStr;
|
||||
expect(updatedQtyStr.qtyStr).toBe("2.50");
|
||||
expect(updatedQtyStr.unitPart).toBe("2");
|
||||
expect(updatedQtyStr.decimalPoint).toBe(".");
|
||||
expect(updatedQtyStr.decimalPart).toBe("50");
|
||||
});
|
||||
|
||||
test("[setQuantity] Base test", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const data = models.loadConnectedData(getAllPricesData());
|
||||
const orderLine = data["pos.order.line"][0];
|
||||
orderLine.setQuantity(2.77);
|
||||
expect(orderLine.qty).toBe(2.77);
|
||||
orderLine.setQuantity(2.779);
|
||||
expect(orderLine.qty).toBe(2.78);
|
||||
orderLine.setQuantity(2.771);
|
||||
expect(orderLine.qty).toBe(2.77);
|
||||
|
||||
const comboOrderline = data["pos.order.line"][1];
|
||||
const comboChild1 = data["pos.order.line"][2];
|
||||
const comboChild2 = data["pos.order.line"][3];
|
||||
expect(comboOrderline.qty).toBe(1);
|
||||
expect(comboChild1.qty).toBe(2);
|
||||
expect(comboChild2.qty).toBe(1);
|
||||
expect(comboOrderline.price_unit).toBe(0);
|
||||
expect(comboChild1.price_unit).toBe(1);
|
||||
expect(comboChild2.price_unit).toBe(8);
|
||||
comboOrderline.setQuantity(3, true);
|
||||
expect(comboOrderline.qty).toBe(3);
|
||||
expect(comboChild1.qty).toBe(6);
|
||||
expect(comboChild2.qty).toBe(3);
|
||||
expect(comboOrderline.price_unit).toBe(0);
|
||||
expect(comboChild1.price_unit).toBe(1);
|
||||
expect(comboChild2.price_unit).toBe(8);
|
||||
comboOrderline.setQuantity(2, true);
|
||||
expect(comboOrderline.qty).toBe(2);
|
||||
expect(comboChild1.qty).toBe(4);
|
||||
expect(comboChild2.qty).toBe(2);
|
||||
expect(comboOrderline.price_unit).toBe(0);
|
||||
expect(comboChild1.price_unit).toBe(1);
|
||||
expect(comboChild2.price_unit).toBe(8);
|
||||
});
|
||||
|
||||
test("[canBeMergedWith]: Base test", async () => {
|
||||
const store = await setupPosEnv();
|
||||
// Test with different products
|
||||
const order = await getFilledOrder(store);
|
||||
const line1 = order.lines[0];
|
||||
const line2 = order.lines[1];
|
||||
expect(line1.canBeMergedWith(line2)).toBe(false);
|
||||
// Test with same product, same price, same qty, no discount, different name
|
||||
line2.product_id = line1.product_id;
|
||||
expect(line1.canBeMergedWith(line2)).toBe(false);
|
||||
// Test with same product, same price, same qty, no discount, same name
|
||||
line2.setFullProductName("TEST");
|
||||
expect(line1.canBeMergedWith(line2)).toBe(true);
|
||||
// Test with different note
|
||||
line1.setNote("Test note");
|
||||
expect(line1.canBeMergedWith(line2)).toBe(false);
|
||||
// Test with same note
|
||||
line2.setNote("Test note");
|
||||
expect(line1.canBeMergedWith(line2)).toBe(true);
|
||||
// Test with discount applied
|
||||
line1.setDiscount(10.0);
|
||||
expect(line1.canBeMergedWith(line2)).toBe(false);
|
||||
// Test with same discount
|
||||
line2.setDiscount(10.0);
|
||||
expect(line1.canBeMergedWith(line2)).toBe(true);
|
||||
// Test with different price unit
|
||||
line2.price_unit = line1.price_unit + 1;
|
||||
line2.price_type = "manual";
|
||||
expect(line1.canBeMergedWith(line2)).toBe(false);
|
||||
// Test to merge lines
|
||||
line1.merge(line2);
|
||||
expect(line1.qty).toBe(5);
|
||||
});
|
||||
|
||||
describe("Test taxes after fiscal position", () => {
|
||||
test("Orderline containing a taxed product", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const dataDict = getAllPricesData();
|
||||
dataDict["pos.order"][0]["fiscal_position_id"] = 2;
|
||||
const data = models.loadConnectedData(dataDict);
|
||||
const orderLine = data["pos.order.line"][0];
|
||||
const lineValues = orderLine.prepareBaseLineForTaxesComputationExtraValues();
|
||||
expect(lineValues.tax_ids.length).toBe(0);
|
||||
});
|
||||
test("Taxed Orderline orderline after fiscal position", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const dataDict = getAllPricesData();
|
||||
dataDict["pos.order"][0]["fiscal_position_id"] = 1;
|
||||
const data = models.loadConnectedData(dataDict);
|
||||
// Taxed order line
|
||||
const orderLine = data["pos.order.line"][0];
|
||||
// Taxed Product
|
||||
const taxedProductLineValues = orderLine.prepareBaseLineForTaxesComputationExtraValues();
|
||||
expect(taxedProductLineValues.tax_ids.length).toBe(1);
|
||||
// Non Taxed Product
|
||||
orderLine.product_id.taxes_id = [];
|
||||
const nonTaxedProductlineValues = orderLine.prepareBaseLineForTaxesComputationExtraValues();
|
||||
expect(nonTaxedProductlineValues.tax_ids.length).toBe(1);
|
||||
});
|
||||
test("Non-taxed orderline after fiscal position", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const dataDict = getAllPricesData();
|
||||
dataDict["pos.order"][0]["fiscal_position_id"] = 1;
|
||||
const data = models.loadConnectedData(dataDict);
|
||||
const orderLine = data["pos.order.line"][0];
|
||||
// Non taxed order line
|
||||
orderLine.tax_ids = [];
|
||||
// Taxed product
|
||||
const taxedProductLineValues = orderLine.prepareBaseLineForTaxesComputationExtraValues();
|
||||
expect(taxedProductLineValues.tax_ids.length).toBe(0);
|
||||
orderLine.product_id.taxes_id = [];
|
||||
// Non Taxed product
|
||||
const nonTaxedProductlineValues = orderLine.prepareBaseLineForTaxesComputationExtraValues();
|
||||
expect(nonTaxedProductlineValues.tax_ids.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test("Test serial number requirements", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const serial_line = order.lines[0];
|
||||
serial_line.product_id.tracking = "serial";
|
||||
expect(serial_line.hasValidProductLot()).toBe(false); // No SN set
|
||||
serial_line.setPackLotLines({
|
||||
modifiedPackLotLines: {},
|
||||
newPackLotLines: [
|
||||
{
|
||||
lot_name: "SN001",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(serial_line.hasValidProductLot()).toBe(true);
|
||||
serial_line.qty = 2;
|
||||
expect(serial_line.hasValidProductLot()).toBe(false); // Only one SN set
|
||||
serial_line.setPackLotLines({
|
||||
modifiedPackLotLines: {},
|
||||
newPackLotLines: [
|
||||
{
|
||||
lot_name: "SN002",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(serial_line.hasValidProductLot()).toBe(true);
|
||||
|
||||
const lot_line = order.lines[1];
|
||||
lot_line.product_id.tracking = "lot";
|
||||
expect(lot_line.hasValidProductLot()).toBe(false);
|
||||
lot_line.setPackLotLines({
|
||||
modifiedPackLotLines: {},
|
||||
newPackLotLines: [
|
||||
{
|
||||
lot_name: "LOT001",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(lot_line.hasValidProductLot()).toBe(true);
|
||||
lot_line.qty = 2;
|
||||
expect(lot_line.hasValidProductLot()).toBe(true); // One lot is enough
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("generateSlots", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const presetIn = store.models["pos.preset"].get(1);
|
||||
// expect all presetIn.availabilities to be empty arrays
|
||||
for (const key in presetIn.availabilities) {
|
||||
expect(Array.isArray(presetIn.availabilities[key])).toBe(true);
|
||||
expect(presetIn.availabilities[key].length).toBe(0);
|
||||
}
|
||||
// expect days of week of presetOut.availabilities to contains slots
|
||||
const presetOut = store.models["pos.preset"].get(2);
|
||||
let daysWithSlot = 0;
|
||||
for (const key in presetOut.availabilities) {
|
||||
if (Object.keys(presetOut.availabilities[key]).length > 0) {
|
||||
daysWithSlot++;
|
||||
// each day should contains 23 slots of 20 minutes (12:00 to 15:00, and 18:00 to 22:00)
|
||||
expect(Object.keys(presetOut.availabilities[key]).length).toBe(23);
|
||||
}
|
||||
}
|
||||
// expect at least 5 days with slots (Monday to Friday)
|
||||
expect(daysWithSlot).toBe(5);
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { ProductProduct } from "@point_of_sale/app/models/product_product";
|
||||
import { ProductTemplate } from "@point_of_sale/app/models/product_template";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("product template and product product override", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const product = store.models["product.template"].get(18);
|
||||
|
||||
patchWithCleanup(ProductProduct.prototype, {
|
||||
get allBarcodes() {
|
||||
return this.barcode || "";
|
||||
},
|
||||
});
|
||||
patchWithCleanup(ProductTemplate.prototype, {
|
||||
get allBarcodes() {
|
||||
return (
|
||||
(this.barcode || "") + this.product_variant_ids.map((p) => p.allBarcodes).join(",")
|
||||
);
|
||||
},
|
||||
});
|
||||
expect(product.allBarcodes).toBe("");
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { PosData } from "@point_of_sale/app/services/data_service";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
/**
|
||||
* Disable IndexedDB in Hoot tests to avoid creating to much IndexedDB databases
|
||||
* when running the full test suite.
|
||||
*
|
||||
* IndexedDB is still tested in dedicated tours.
|
||||
*/
|
||||
patch(PosData.prototype, {
|
||||
setup() {
|
||||
this.indexedDB = {
|
||||
delete: async () => ({}),
|
||||
create: async () => ({}),
|
||||
reset: async () => ({}),
|
||||
readAll: async () => ({}),
|
||||
};
|
||||
return super.setup(...arguments);
|
||||
},
|
||||
initIndexedDB() {
|
||||
return true;
|
||||
},
|
||||
initListeners() {
|
||||
return true;
|
||||
},
|
||||
synchronizeLocalDataInIndexedDB() {
|
||||
return true;
|
||||
},
|
||||
async getCachedServerDataFromIndexedDB() {
|
||||
return {};
|
||||
},
|
||||
async getLocalDataFromIndexedDB() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { getRelatedModelsInstance } from "../data/get_model_definitions";
|
||||
import { makeMockServer } from "@web/../tests/web_test_helpers";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
describe(`Related models Events`, () => {
|
||||
test("Connecting multiple records must fire update once", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({});
|
||||
const line1 = models["pos.order.line"].create({});
|
||||
const line2 = models["pos.order.line"].create({});
|
||||
const orderUpdates = [];
|
||||
const linesUpdates = [];
|
||||
|
||||
models["pos.order"].addEventListener("update", (data) => {
|
||||
orderUpdates.push(data);
|
||||
});
|
||||
|
||||
models["pos.order.line"].addEventListener("update", (data) => {
|
||||
linesUpdates.push(data);
|
||||
});
|
||||
|
||||
expect(orderUpdates.length).toBe(0);
|
||||
|
||||
order.update({ lines: [line1, line2], amount_total: 10 });
|
||||
expect(orderUpdates.length).toBe(1);
|
||||
expect(orderUpdates[0].id).toEqual(order.id);
|
||||
expect(orderUpdates[0].fields).toEqual(["lines", "amount_total"]);
|
||||
|
||||
expect(linesUpdates.length).toBe(2);
|
||||
expect(linesUpdates[0].fields).toEqual(["order_id"]);
|
||||
});
|
||||
|
||||
test("Loading data", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
let orderCreates = [];
|
||||
let orderUpdates = [];
|
||||
|
||||
models["pos.order"].addEventListener("create", (data) => {
|
||||
orderCreates.push(data);
|
||||
});
|
||||
|
||||
const order1 = models["pos.order"].create({});
|
||||
models["pos.order"].addEventListener("update", (data) => {
|
||||
orderUpdates.push(data);
|
||||
});
|
||||
|
||||
expect(orderUpdates.length).toBe(0);
|
||||
expect(orderCreates.length).toBe(1);
|
||||
expect(orderCreates[0].ids).toEqual([order1.id]);
|
||||
|
||||
orderCreates = [];
|
||||
orderUpdates = [];
|
||||
|
||||
models.connectNewData({
|
||||
"pos.order": [
|
||||
{
|
||||
id: 1,
|
||||
uuid: order1.uuid, //Update
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(orderUpdates.length).toBe(1);
|
||||
expect(orderCreates.length).toBe(1);
|
||||
expect(orderCreates[0].ids).toEqual([2, 3]);
|
||||
});
|
||||
|
||||
test("Connecting new data", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order1 = models["pos.order"].create({ id: 3333 });
|
||||
const orderUpdates = [];
|
||||
const lineUpdates = [];
|
||||
const lineCreates = [];
|
||||
|
||||
models["pos.order"].addEventListener("update", (data) => {
|
||||
orderUpdates.push(data);
|
||||
});
|
||||
|
||||
models["pos.order.line"].addEventListener("create", (data) => {
|
||||
lineCreates.push(data);
|
||||
});
|
||||
|
||||
models["pos.order.line"].addEventListener("update", (data) => {
|
||||
lineUpdates.push(data);
|
||||
});
|
||||
|
||||
models.connectNewData({
|
||||
"pos.order.line": [
|
||||
{
|
||||
id: 1,
|
||||
order_id: order1.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(orderUpdates.length).toBe(1); // The new line is connected to the order and updates it
|
||||
expect(lineUpdates.length).toBe(0);
|
||||
expect(lineCreates.length).toBe(1);
|
||||
});
|
||||
|
||||
test("Delete record", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
let orderUpdates = [];
|
||||
const orderDeletes = [];
|
||||
const order = models["pos.order"].create({});
|
||||
|
||||
models["pos.order"].addEventListener("update", (data) => {
|
||||
orderUpdates.push(data);
|
||||
});
|
||||
|
||||
models["pos.order"].addEventListener("delete", (data) => {
|
||||
orderDeletes.push(data);
|
||||
});
|
||||
|
||||
const linesUpdates = [];
|
||||
models["pos.order.line"].create({ order_id: order.id });
|
||||
models["pos.order.line"].create({ order_id: order.id });
|
||||
models["pos.order.line"].addEventListener("update", (data) => {
|
||||
linesUpdates.push(data);
|
||||
});
|
||||
|
||||
expect(orderUpdates.length).toBe(2); // connecting lines to order
|
||||
expect(orderDeletes.length).toBe(0);
|
||||
expect(linesUpdates.length).toBe(0);
|
||||
|
||||
orderUpdates = [];
|
||||
order.delete();
|
||||
expect(orderDeletes.length).toBe(1);
|
||||
expect(orderUpdates.length).toBe(0);
|
||||
expect(linesUpdates.length).toBe(2);
|
||||
expect(linesUpdates[0].fields).toEqual(["order_id"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { SERIALIZED_UI_STATE_PROP } from "@point_of_sale/app/models/related_models/utils";
|
||||
import { getRelatedModelsInstance } from "../data/get_model_definitions";
|
||||
import { makeMockServer } from "@web/../tests/web_test_helpers";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { getFilledOrder, setupPosEnv } from "../utils";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("newly created record", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({ amount_total: 10 });
|
||||
const line1 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
qty: 1,
|
||||
});
|
||||
const line2 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
qty: 2,
|
||||
});
|
||||
{
|
||||
const result = order.serializeForIndexedDB();
|
||||
expect(result.id).toBe(order.id);
|
||||
expect(result.amount_total).toBe(order.amount_total);
|
||||
expect(Array.isArray(result.lines)).toBe(true);
|
||||
expect(result.lines.length).toBe(2);
|
||||
expect(result.lines[0]).toBe(line1.id);
|
||||
expect(result.lines[1]).toBe(line2.id);
|
||||
expect(result[SERIALIZED_UI_STATE_PROP]).toBeEmpty();
|
||||
}
|
||||
|
||||
{
|
||||
const result = line1.serializeForIndexedDB();
|
||||
expect(result.id).toBe(line1.id);
|
||||
expect(result.qty).toBe(1);
|
||||
expect(result[SERIALIZED_UI_STATE_PROP]).toBeEmpty();
|
||||
}
|
||||
});
|
||||
|
||||
test("UIState serialization", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({ amount_total: 10 });
|
||||
order.uiState = { demoValue: 99 };
|
||||
|
||||
const result = order.serializeForIndexedDB();
|
||||
expect(result.id).toBe(order.id);
|
||||
expect(result.amount_total).toBe(10);
|
||||
expect(result[SERIALIZED_UI_STATE_PROP]).not.toBeEmpty();
|
||||
expect(typeof result[SERIALIZED_UI_STATE_PROP]).toBe("string");
|
||||
});
|
||||
|
||||
test("Restore serialized data", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
order.uiState = { demoValue: 999 };
|
||||
const serialized = order.serializeForIndexedDB();
|
||||
const serializedLines = order.lines.map((line) => line.serializeForIndexedDB());
|
||||
|
||||
store.data.localDeleteCascade(order);
|
||||
const data = store.models.loadConnectedData({
|
||||
"pos.order": [serialized],
|
||||
"pos.order.line": serializedLines,
|
||||
});
|
||||
|
||||
// UI state is restored
|
||||
expect(data["pos.order"][0].uiState.demoValue).toBe(999);
|
||||
// UIState must be excluded from the raw data
|
||||
expect(data["pos.order"][0].raw.uiState).toBeEmpty();
|
||||
});
|
||||
|
|
@ -0,0 +1,437 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { getRelatedModelsInstance } from "../data/get_model_definitions";
|
||||
import { makeMockServer } from "@web/../tests/web_test_helpers";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("basic", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({});
|
||||
const line1 = models["pos.order.line"].create({ order_id: order, qty: 1 });
|
||||
const line2 = models["pos.order.line"].create({ order_id: order, qty: 2 });
|
||||
order.amount_total = 10;
|
||||
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
{
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.uuid).not.toBeEmpty();
|
||||
expect(result.amount_total).toBe(10);
|
||||
expect(result.lines.length).toBe(2);
|
||||
|
||||
expect(result.lines[0][0]).toBe(0);
|
||||
expect(result.lines[0][1]).toBe(0);
|
||||
expect(result.lines[0][2].id).toBe(undefined);
|
||||
expect(result.lines[0][2].uuid).toBe(line1.uuid);
|
||||
expect(result.lines[0][2].qty).toBe(1);
|
||||
|
||||
expect(result.lines[1][0]).toBe(0);
|
||||
expect(result.lines[1][1]).toBe(0);
|
||||
expect(result.lines[1][2].id).toBe(undefined);
|
||||
expect(result.lines[1][2].uuid).toBe(line2.uuid);
|
||||
expect(result.lines[1][2].qty).toBe(2);
|
||||
|
||||
expect(result.relations_uuid_mapping).toBe(undefined);
|
||||
}
|
||||
|
||||
// Server results with ids
|
||||
models.connectNewData({
|
||||
"pos.order": [{ ...order.raw, id: 1, lines: [11, 12] }],
|
||||
"pos.order.line": [
|
||||
{ ...line1.raw, id: 11, order_id: 1 },
|
||||
{ ...line2.raw, id: 12, order_id: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
line1.qty = 99;
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines.length).toBe(1);
|
||||
expect(result.lines[0][0]).toBe(1);
|
||||
expect(result.lines[0][1]).toBe(11);
|
||||
expect(result.lines[0][2].qty).toBe(99);
|
||||
}
|
||||
|
||||
// Delete line
|
||||
line1.delete();
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines.length).toBe(1);
|
||||
expect(result.lines[0][0]).toBe(3);
|
||||
expect(result.lines[0][1]).toBe(11);
|
||||
}
|
||||
});
|
||||
|
||||
test("serialization of non-dynamic model relationships", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({});
|
||||
|
||||
const tax1 = models["account.tax"].create({ id: 99 });
|
||||
const tax2 = models["account.tax"].create({ id: 999 });
|
||||
const line = models["pos.order.line"].create({ order_id: order, qty: 1 });
|
||||
line.tax_ids = [["link", tax1, tax2]];
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines[0][2].tax_ids).toEqual([99, 999]);
|
||||
}
|
||||
|
||||
const tax3 = models["account.tax"].create({ id: 9999 });
|
||||
line.tax_ids = [["link", tax3]];
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines[0][2].tax_ids).toEqual([99, 999, 9999]);
|
||||
}
|
||||
|
||||
line.tax_ids = [["unlink", tax3, tax2]];
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines[0][2].tax_ids).toEqual([99]);
|
||||
}
|
||||
|
||||
line.tax_ids = [];
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines[0][2].tax_ids).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test("serialization of dynamic model without uuid", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
let order = models["pos.order"].create({});
|
||||
const orderUUID = order.uuid;
|
||||
|
||||
let line1 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
});
|
||||
const line1UUID = line1.uuid;
|
||||
|
||||
models["pos.pack.operation.lot"].create({ pos_order_line_id: line1, lot_name: "lot1" });
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines.length).toBe(1);
|
||||
const pack_lot_ids = result.lines[0][2].pack_lot_ids;
|
||||
expect(pack_lot_ids.length).toBe(1);
|
||||
expect(pack_lot_ids[0][0]).toBe(0);
|
||||
expect(pack_lot_ids[0][0]).toBe(0);
|
||||
expect(pack_lot_ids[0][2]).toEqual({ lot_name: "lot1", write_date: false });
|
||||
}
|
||||
|
||||
models.connectNewData({
|
||||
"pos.order": [
|
||||
{
|
||||
id: 1,
|
||||
lines: [11],
|
||||
uuid: order.uuid,
|
||||
},
|
||||
],
|
||||
"pos.order.line": [
|
||||
{
|
||||
id: 11,
|
||||
pack_lot_ids: [99],
|
||||
uuid: line1UUID,
|
||||
},
|
||||
],
|
||||
"pos.pack.operation.lot": [
|
||||
{
|
||||
id: 99,
|
||||
lot_name: "lot1",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
order = models["pos.order"].getBy("uuid", orderUUID);
|
||||
line1 = models["pos.order.line"].getBy("uuid", line1UUID);
|
||||
|
||||
models["pos.pack.operation.lot"].create({ pos_order_line_id: line1, lot_name: "lot2" });
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines.length).toBe(1);
|
||||
const pack_lot_ids = result.lines[0][2].pack_lot_ids;
|
||||
expect(pack_lot_ids.length).toBe(1);
|
||||
expect(pack_lot_ids[0][0]).toBe(0);
|
||||
expect(pack_lot_ids[0][1]).toBe(0);
|
||||
expect(pack_lot_ids[0][2].lot_name).toEqual("lot2");
|
||||
}
|
||||
|
||||
models.connectNewData({
|
||||
"pos.order": [
|
||||
{
|
||||
id: 1,
|
||||
lines: [11],
|
||||
uuid: order.uuid,
|
||||
},
|
||||
],
|
||||
"pos.order.line": [
|
||||
{
|
||||
id: 11,
|
||||
order_id: 1,
|
||||
pack_lot_ids: [99, 999],
|
||||
uuid: line1UUID,
|
||||
},
|
||||
],
|
||||
"pos.pack.operation.lot": [
|
||||
{
|
||||
id: 99,
|
||||
pos_order_line_id: 11,
|
||||
lot_name: "lot1",
|
||||
},
|
||||
{
|
||||
id: 999,
|
||||
pos_order_line_id: 11,
|
||||
lot_name: "lot2",
|
||||
},
|
||||
],
|
||||
});
|
||||
order = models["pos.order"].getBy("uuid", orderUUID);
|
||||
order.lines[0].pack_lot_ids[1].delete();
|
||||
order.lines[0].pack_lot_ids[0].delete();
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines.length).toBe(1);
|
||||
const pack_lot_ids = result.lines[0][2].pack_lot_ids;
|
||||
expect(pack_lot_ids.length).toBe(2);
|
||||
expect(pack_lot_ids).toEqual([
|
||||
[3, 999],
|
||||
[3, 99],
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test("nested lines relationship", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
let order = models["pos.order"].create({});
|
||||
let parentLine = models["pos.order.line"].create({ order_id: order });
|
||||
let line1 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
combo_parent_id: parentLine,
|
||||
});
|
||||
let line2 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
combo_parent_id: parentLine,
|
||||
});
|
||||
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines.length).toBe(3);
|
||||
expect(result.lines[0][2].combo_line_ids).toBeEmpty();
|
||||
expect(result.lines[0][2].combo_parent_id).toBeEmpty();
|
||||
expect(result.lines[1][2].combo_line_ids).toBeEmpty();
|
||||
expect(result.lines[1][2].combo_parent_id).toBeEmpty();
|
||||
expect(result.lines[2][2].combo_line_ids).toBeEmpty();
|
||||
expect(result.lines[2][2].combo_parent_id).toBeEmpty();
|
||||
|
||||
const { relations_uuid_mapping } = result;
|
||||
expect(Object.keys(relations_uuid_mapping["pos.order.line"]).length).toBe(2);
|
||||
expect(relations_uuid_mapping["pos.order.line"][line1.uuid]["combo_parent_id"]).toBe(
|
||||
parentLine.uuid
|
||||
);
|
||||
expect(relations_uuid_mapping["pos.order.line"][line2.uuid]["combo_parent_id"]).toBe(
|
||||
parentLine.uuid
|
||||
);
|
||||
}
|
||||
|
||||
models.connectNewData({
|
||||
"pos.order": [
|
||||
{
|
||||
id: 1,
|
||||
lines: [11, 111],
|
||||
uuid: order.uuid,
|
||||
},
|
||||
],
|
||||
"pos.order.line": [
|
||||
{
|
||||
id: 1,
|
||||
uuid: parentLine.uuid,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
uuid: line1.uuid,
|
||||
combo_parent_id: 1,
|
||||
},
|
||||
{
|
||||
id: 111,
|
||||
uuid: line2.uuid,
|
||||
combo_parent_id: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Update line: the uuid mapping must be present
|
||||
order = models["pos.order"].getBy("uuid", order.uuid);
|
||||
line1 = models["pos.order.line"].getBy("uuid", line1.uuid);
|
||||
line2 = models["pos.order.line"].getBy("uuid", line2.uuid);
|
||||
parentLine = models["pos.order.line"].getBy("uuid", parentLine.uuid);
|
||||
|
||||
line1.qty = 99;
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines.length).toBe(1);
|
||||
expect(result.lines[0][0]).toBe(1);
|
||||
expect(result.lines[0][1]).toBe(11);
|
||||
expect(result.lines[0][2].qty).toBe(99);
|
||||
expect(result.relations_uuid_mapping).toBe(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test("recursive relationship with group of lines", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
let order = models["pos.order"].create({});
|
||||
let line1 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
});
|
||||
let line2 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
combo_parent_id: line1,
|
||||
});
|
||||
let line3 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
combo_parent_id: line1,
|
||||
});
|
||||
models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
combo_parent_id: line1,
|
||||
});
|
||||
|
||||
expect(order.lines.length).toBe(4);
|
||||
expect(order.lines[0].combo_line_ids.length).toBe(3);
|
||||
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines.length).toBe(4);
|
||||
expect(result.lines[0][2].combo_parent_id).toBeEmpty();
|
||||
expect(result.lines[1][2].combo_parent_id).toBeEmpty();
|
||||
|
||||
const { relations_uuid_mapping } = result;
|
||||
expect(Object.keys(relations_uuid_mapping["pos.order.line"]).length).toBe(3);
|
||||
expect(relations_uuid_mapping["pos.order.line"][line2.uuid]["combo_parent_id"]).toBe(
|
||||
line1.uuid
|
||||
);
|
||||
expect(relations_uuid_mapping["pos.order.line"][line3.uuid]["combo_parent_id"]).toBe(
|
||||
line1.uuid
|
||||
);
|
||||
}
|
||||
|
||||
models.connectNewData({
|
||||
"pos.order": [
|
||||
{
|
||||
id: 1,
|
||||
lines: [110, 111],
|
||||
uuid: order.uuid,
|
||||
},
|
||||
],
|
||||
"pos.order.line": [
|
||||
{
|
||||
id: 1,
|
||||
uuid: line1.uuid,
|
||||
},
|
||||
{
|
||||
id: 110,
|
||||
order_id: 1,
|
||||
uuid: line2.uuid,
|
||||
combo_parent_id: 1,
|
||||
},
|
||||
{
|
||||
id: 111,
|
||||
order_id: 1,
|
||||
uuid: line3.uuid,
|
||||
combo_parent_id: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
order = models["pos.order"].getBy("uuid", order.uuid);
|
||||
line1 = models["pos.order.line"].getBy("uuid", line1.uuid);
|
||||
line2 = models["pos.order.line"].getBy("uuid", line2.uuid);
|
||||
line3 = models["pos.order.line"].getBy("uuid", line3.uuid);
|
||||
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.relations_uuid_mapping).toBe(undefined);
|
||||
}
|
||||
|
||||
// Delete line
|
||||
line3.delete();
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
|
||||
expect(result.lines.length).toBe(2);
|
||||
expect(result.lines[0][0]).toBe(3);
|
||||
expect(result.lines[0][1]).toBe(111);
|
||||
expect(result.relations_uuid_mapping).toBe(undefined);
|
||||
}
|
||||
|
||||
{
|
||||
//All update/delete have been cleared
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
expect(result.lines).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
test("grouped lines and nested lines", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({});
|
||||
const parentLine = models["pos.order.line"].create({ order_id: order });
|
||||
const line1 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
combo_parent_id: parentLine,
|
||||
});
|
||||
const line2 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
combo_parent_id: parentLine,
|
||||
});
|
||||
const line3 = models["pos.order.line"].create({
|
||||
order_id: order,
|
||||
});
|
||||
|
||||
{
|
||||
const keepCommandResult = models.serializeForORM(order, { keepCommands: true });
|
||||
const result = models.serializeForORM(order);
|
||||
expect(keepCommandResult).toEqual(result);
|
||||
|
||||
expect(result.lines.length).toBe(4);
|
||||
const { relations_uuid_mapping } = result;
|
||||
expect(Object.keys(relations_uuid_mapping["pos.order.line"]).length).toBe(2);
|
||||
|
||||
const lineMapping = relations_uuid_mapping["pos.order.line"];
|
||||
expect(lineMapping[line1.uuid]["combo_parent_id"]).toBe(parentLine.uuid);
|
||||
expect(lineMapping[line2.uuid]["combo_parent_id"]).toBe(parentLine.uuid);
|
||||
expect(lineMapping[line3.uuid]?.["combo_parent_id"]?.length).toBeEmpty();
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,663 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { getRelatedModelsInstance } from "../data/get_model_definitions";
|
||||
import { makeMockServer } from "@web/../tests/web_test_helpers";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
describe("models with backlinks", () => {
|
||||
describe("many2one and one2many field relations to other models", () => {
|
||||
test("create operation", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const category = models["pos.category"].create({});
|
||||
const product = models["product.template"].create({ pos_categ_ids: [category] });
|
||||
expect(product.pos_categ_ids).toInclude(category);
|
||||
expect(category.backLink("product.template.pos_categ_ids")).toInclude(product);
|
||||
});
|
||||
test("read operation 1", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({});
|
||||
const p1 = models["product.template"].create({ pos_categ_ids: [c1] });
|
||||
const p2 = models["product.template"].create({ pos_categ_ids: [c1] });
|
||||
const p3 = models["product.template"].create({ pos_categ_ids: [c2] });
|
||||
|
||||
// Test reading back the categories directly
|
||||
const readC1 = models["pos.category"].read(c1.id);
|
||||
expect(readC1).toEqual(c1);
|
||||
|
||||
const readP1 = models["product.template"].read(p1.id);
|
||||
expect(readP1).toEqual(p1);
|
||||
|
||||
// Test the many2one relationship from products to category
|
||||
expect(readP1.pos_categ_ids).toEqual([c1]);
|
||||
|
||||
// Additional checks for completeness
|
||||
const readMany = models["product.template"].readMany([p2.id, p3.id]);
|
||||
expect(readMany).toEqual([p2, p3]);
|
||||
|
||||
const readNonExistent = models["product.template"].read(9999);
|
||||
expect(readNonExistent).toBe(undefined);
|
||||
|
||||
const readNonExistentC = models["pos.category"].read(9999);
|
||||
expect(readNonExistentC).toBe(undefined);
|
||||
});
|
||||
|
||||
test("update operation, many2one", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const p1 = models["product.template"].create({});
|
||||
const c1 = models["product.category"].create({});
|
||||
|
||||
p1.update({ categ_id: c1 });
|
||||
expect(p1.categ_id).toBe(c1);
|
||||
});
|
||||
|
||||
test("update operation, one2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const l1 = models["pos.order.line"].create({});
|
||||
const l2 = models["pos.order.line"].create({});
|
||||
const order1 = models["pos.order"].create({});
|
||||
|
||||
order1.update({ lines: [["link", l1, l2]] });
|
||||
expect(order1.lines).toInclude(l1);
|
||||
expect(order1.lines).toInclude(l2);
|
||||
expect(l1.order_id).toBe(order1);
|
||||
});
|
||||
|
||||
test("update operation, unlink many2one", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const o1 = models["pos.order"].create({ lines: [] });
|
||||
const l1 = models["pos.order.line"].create({
|
||||
order_id: o1,
|
||||
});
|
||||
|
||||
expect(l1.order_id).toEqual(o1);
|
||||
|
||||
o1.update({ lines: [] });
|
||||
expect(l1.order_id).toBe(undefined);
|
||||
});
|
||||
|
||||
test("update operation, unlink one2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const o1 = models["pos.order"].create({ lines: [] });
|
||||
const l1 = models["pos.order.line"].create({
|
||||
order_id: o1,
|
||||
});
|
||||
|
||||
o1.update({ lines: [["link", l1]] });
|
||||
expect(o1.lines).toInclude(l1);
|
||||
expect(l1.order_id).toBe(o1);
|
||||
|
||||
o1.update({ lines: [["unlink", l1]] });
|
||||
expect(o1.lines).not.toInclude(l1);
|
||||
expect(l1.order_id).toBe(undefined);
|
||||
});
|
||||
|
||||
test("update operation, Clear one2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const o1 = models["pos.order"].create({});
|
||||
const l1 = models["pos.order.line"].create({
|
||||
order_id: o1,
|
||||
});
|
||||
o1.update({ lines: [["link", l1]] });
|
||||
expect(o1.lines).toInclude(l1);
|
||||
expect(l1.order_id).toBe(o1);
|
||||
|
||||
o1.update({ lines: [["unlink", l1]] });
|
||||
expect(o1.lines).not.toInclude(l1);
|
||||
expect(l1.order_id).toBe(undefined);
|
||||
});
|
||||
|
||||
test("update operation, Clear many2one", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const o1 = models["pos.order"].create({ lines: [] });
|
||||
models["pos.order.line"].create({
|
||||
order_id: o1,
|
||||
});
|
||||
|
||||
models["pos.order"].update(o1, { lines: [] });
|
||||
expect(o1.lines).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("delete operation, one2many item", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const l1 = models["pos.order.line"].create({});
|
||||
const l2 = models["pos.order.line"].create({});
|
||||
const o1 = models["pos.order"].create({});
|
||||
|
||||
o1.update({ lines: [["link", l1, l2]] });
|
||||
expect(o1.lines).toInclude(l1);
|
||||
|
||||
l1.delete();
|
||||
expect(models["pos.order.line"].read(l1.id)).toBe(undefined);
|
||||
expect(o1.lines).not.toInclude(l1);
|
||||
});
|
||||
|
||||
test("delete operation, many2one item", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const o1 = models["pos.order"].create({});
|
||||
const l1 = models["pos.order.line"].create({});
|
||||
|
||||
o1.update({ lines: [["link", l1]] });
|
||||
expect(l1.order_id).toBe(o1);
|
||||
|
||||
l1.delete();
|
||||
expect(models["pos.order.line"].read(l1.id)).toBe(undefined);
|
||||
expect(o1.lines).not.toInclude(l1);
|
||||
});
|
||||
});
|
||||
describe("many2one/one2many field relations to own model", () => {
|
||||
test("create operation", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const o1 = models["pos.order"].create({});
|
||||
const l2 = models["pos.order.line"].create({ order_id: o1 });
|
||||
|
||||
expect(l2.order_id).toBe(o1);
|
||||
expect(o1.lines).toInclude(l2);
|
||||
});
|
||||
|
||||
test("read operation 2", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({ parent_id: c1 });
|
||||
|
||||
const readC1 = models["pos.category"].read(c1.id);
|
||||
expect(readC1.child_ids).toEqual([c2]);
|
||||
|
||||
const readC2 = models["pos.category"].read(c2.id);
|
||||
expect(readC2.parent_id).toEqual(c1);
|
||||
|
||||
const readMany = models["pos.category"].readMany([c1.id, c2.id]);
|
||||
expect(readMany).toEqual([c1, c2]);
|
||||
});
|
||||
|
||||
test("update operation, many2one", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const o1 = models["pos.order"].create({});
|
||||
const l1 = models["pos.order.line"].create({});
|
||||
const l2 = models["pos.order.line"].create({ order_id: o1 });
|
||||
|
||||
expect(l2.order_id).toBe(o1);
|
||||
o1.update({ lines: [l1] });
|
||||
expect(o1.lines).not.toInclude(l2);
|
||||
});
|
||||
|
||||
test("update operation, one2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const o1 = models["pos.order"].create({});
|
||||
const l1 = models["pos.order.line"].create({ order_id: o1 });
|
||||
const l2 = models["pos.order.line"].create({});
|
||||
|
||||
expect(o1.lines).toInclude(l1);
|
||||
l2.update({ order_id: o1 });
|
||||
expect(o1.lines).toInclude(l2);
|
||||
});
|
||||
|
||||
test("update operation, unlink many2one", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({});
|
||||
|
||||
c2.update({ parent_id: c1 });
|
||||
expect(c2.parent_id).toBe(c1);
|
||||
|
||||
c2.update({ parent_id: undefined });
|
||||
expect(c2.parent_id).toBe(undefined);
|
||||
expect(c1.child_ids).not.toInclude(c2);
|
||||
});
|
||||
|
||||
test("update operation, unlink one2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({ parent_id: c1 });
|
||||
|
||||
expect(c1.child_ids).toInclude(c2);
|
||||
|
||||
c1.update({ child_ids: [["unlink", c2]] });
|
||||
expect(c1.child_ids).not.toInclude(c2);
|
||||
expect(c2.parent_id).toBe(undefined);
|
||||
});
|
||||
|
||||
test("update operation, Clear one2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const category = models["pos.category"].create({});
|
||||
models["pos.category"].create({ parent_id: category });
|
||||
models["pos.category"].create({ parent_id: category });
|
||||
|
||||
expect(category.child_ids).toHaveLength(2);
|
||||
models["pos.category"].update(category, { child_ids: [["clear"]] });
|
||||
expect(category.child_ids).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("update operation, Clear many2one", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const category = models["pos.category"].create({});
|
||||
const category1 = models["pos.category"].create({ parent_id: category });
|
||||
|
||||
expect(category.child_ids).toInclude(category1);
|
||||
models["pos.category"].update(category1, { parent_id: undefined });
|
||||
expect(category.child_ids).not.toInclude(category1);
|
||||
});
|
||||
|
||||
test("delete operation, one2many item", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({ parent_id: c1 });
|
||||
|
||||
expect(c1.child_ids).toInclude(c2);
|
||||
|
||||
c2.delete();
|
||||
expect(models["pos.category"].read(c2.id)).toBe(undefined);
|
||||
expect(c1.child_ids).not.toInclude(c2);
|
||||
});
|
||||
|
||||
test("delete operation, many2one item", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({ parent_id: c1 });
|
||||
|
||||
expect(c1.child_ids).toInclude(c2);
|
||||
|
||||
c1.delete();
|
||||
expect(models["pos.category"].read(c1.id)).toBe(undefined);
|
||||
expect(c2.parent_id).toBe(undefined);
|
||||
});
|
||||
});
|
||||
describe("many2many field relations to other models", () => {
|
||||
test("create operation, create", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const line1 = { id: 2 };
|
||||
const line2 = { name: 4 };
|
||||
const product = models["pos.order"].create({
|
||||
name: "Smartphone",
|
||||
lines: [["create", line1, line2]],
|
||||
});
|
||||
expect(product.lines).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("create operation, link", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const line1 = models["pos.order.line"].create({ id: 1 });
|
||||
const line2 = models["pos.order.line"].create({ id: 2 });
|
||||
const product = models["pos.order"].create({
|
||||
name: "Smartphone",
|
||||
lines: [["link", line1, line2]],
|
||||
});
|
||||
expect(product.lines).toInclude(line1);
|
||||
expect(product.lines).toInclude(line2);
|
||||
});
|
||||
|
||||
test("read operation 3", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({});
|
||||
const c3 = models["pos.category"].create({});
|
||||
const t1 = models["product.template"].create({ pos_categ_ids: [["link", c1, c2, c3]] });
|
||||
const t2 = models["product.template"].create({ pos_categ_ids: [["link", c1, c2]] });
|
||||
|
||||
const readT1 = models["product.template"].read(t1.id);
|
||||
expect(readT1).toEqual(t1);
|
||||
const readP1 = models["product.template"].read(t2.id);
|
||||
expect(readP1).toEqual(t2);
|
||||
|
||||
expect(readT1.pos_categ_ids).toInclude(c1);
|
||||
expect(readT1.pos_categ_ids).toInclude(c2);
|
||||
expect(readT1.pos_categ_ids).toInclude(c3);
|
||||
expect(readP1.pos_categ_ids).toInclude(c1);
|
||||
expect(readP1.pos_categ_ids).toInclude(c2);
|
||||
|
||||
const readMany = models["product.template"].readMany([t1.id, t2.id]);
|
||||
expect(readMany[0]).toBe(t1);
|
||||
expect(readMany[1]).toBe(t2);
|
||||
});
|
||||
|
||||
test("update operation, many2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({});
|
||||
const t1 = models["product.template"].create({ pos_categ_ids: [] });
|
||||
expect(t1.pos_categ_ids).not.toInclude(c1);
|
||||
|
||||
t1.update({ pos_categ_ids: [["link", c1]] });
|
||||
expect(t1.pos_categ_ids).toInclude(c1);
|
||||
expect(t1.pos_categ_ids).not.toInclude(c2);
|
||||
|
||||
t1.update({ pos_categ_ids: [["link", c2]] });
|
||||
expect(t1.pos_categ_ids).toInclude(c2);
|
||||
});
|
||||
|
||||
test("update operation, unlink many2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const t1 = models["product.template"].create({ pos_categ_ids: [] });
|
||||
|
||||
t1.update({ pos_categ_ids: [["link", c1]] });
|
||||
expect(t1.pos_categ_ids).toInclude(c1);
|
||||
|
||||
t1.update({ pos_categ_ids: [["unlink", c1]] });
|
||||
expect(t1.pos_categ_ids).not.toInclude(c1);
|
||||
});
|
||||
|
||||
test("update operation, Clear many2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({});
|
||||
const t1 = models["product.template"].create({ pos_categ_ids: [c1, c2] });
|
||||
|
||||
expect(t1.pos_categ_ids).toHaveLength(2);
|
||||
|
||||
t1.update({ pos_categ_ids: [["clear"]] });
|
||||
expect(t1.pos_categ_ids).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("delete operation, many2many item", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({});
|
||||
const t1 = models["product.template"].create({ pos_categ_ids: [] });
|
||||
t1.update({ pos_categ_ids: [["link", c1, c2]] });
|
||||
|
||||
expect(t1.pos_categ_ids).toInclude(c1);
|
||||
|
||||
c1.delete();
|
||||
expect(models["pos.category"].read(c1.id)).toBe(undefined);
|
||||
expect(t1.pos_categ_ids).not.toInclude(c1);
|
||||
expect(t1.pos_categ_ids).toInclude(c2);
|
||||
});
|
||||
|
||||
describe("many2many field relations to own model", () => {
|
||||
test("create operation, link", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const l1 = models["pos.order.line"].create({ id: 1 });
|
||||
const l2 = models["pos.order.line"].create({ id: 2, combo_parent_id: l1 });
|
||||
expect(l1.combo_line_ids).toInclude(l2);
|
||||
expect(l2.combo_parent_id).toBe(l1);
|
||||
});
|
||||
|
||||
test("read operation 4", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const l1 = models["pos.order.line"].create({});
|
||||
const l2 = models["pos.order.line"].create({});
|
||||
const l3 = models["pos.order.line"].create({});
|
||||
const l4 = models["pos.order.line"].create({ combo_line_ids: [l1, l2, l3] });
|
||||
|
||||
const readL1 = models["pos.order.line"].read(l1.id);
|
||||
expect(readL1).toEqual(l1);
|
||||
|
||||
const readL4 = models["pos.order.line"].read(l4.id);
|
||||
expect(readL4).toEqual(l4);
|
||||
|
||||
expect([l1, l2, l3].every((n) => readL4.combo_line_ids.includes(n))).toBe(true);
|
||||
|
||||
const readMany = models["pos.order.line"].readMany([l2.id, l3.id]);
|
||||
expect(readMany[0]).toBe(l2);
|
||||
expect(readMany[1]).toBe(l3);
|
||||
});
|
||||
|
||||
test("update operation, many2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const l1 = models["pos.order.line"].create({});
|
||||
const l2 = models["pos.order.line"].create({});
|
||||
const l3 = models["pos.order.line"].create({});
|
||||
l1.update({ combo_line_ids: [["link", l3]] });
|
||||
expect(l1.combo_line_ids).toInclude(l3);
|
||||
expect(l3.combo_parent_id).toBe(l1);
|
||||
|
||||
l3.update({ combo_line_ids: [["link", l2]] });
|
||||
expect(l3.combo_line_ids).toInclude(l2);
|
||||
|
||||
l3.update({ combo_line_ids: [["unlink", l2]] });
|
||||
expect(l3.combo_line_ids).not.toInclude(l2);
|
||||
expect(l2.combo_line_ids).not.toInclude(l3);
|
||||
});
|
||||
|
||||
test("update operation, unlink many2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const l1 = models["pos.order.line"].create({});
|
||||
const l2 = models["pos.order.line"].create({});
|
||||
|
||||
l2.update({ combo_line_ids: [["link", l1]] });
|
||||
expect(l2.combo_line_ids).toInclude(l1);
|
||||
expect(l1.combo_parent_id).toBe(l2);
|
||||
|
||||
l2.update({ combo_line_ids: [["unlink", l1]] });
|
||||
expect(l2.combo_line_ids).not.toInclude(l1);
|
||||
expect(l1.combo_parent_id).toBeEmpty();
|
||||
});
|
||||
|
||||
test("update operation, Clear many2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const l1 = models["pos.order.line"].create({});
|
||||
const l2 = models["pos.order.line"].create({});
|
||||
const l3 = models["pos.order.line"].create({});
|
||||
const l4 = models["pos.order.line"].create({ combo_line_ids: [l1, l2, l3] });
|
||||
|
||||
expect(l4.combo_line_ids).toHaveLength(3);
|
||||
|
||||
models["pos.order.line"].update(l4, { combo_line_ids: [["clear"]] });
|
||||
|
||||
expect(l4.combo_line_ids).toHaveLength(0);
|
||||
expect(l1.combo_parent_id).toBeEmpty();
|
||||
});
|
||||
|
||||
test("delete operation, many2many item", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const l1 = models["pos.order.line"].create({});
|
||||
const l2 = models["pos.order.line"].create({});
|
||||
const l3 = models["pos.order.line"].create({});
|
||||
l3.update({ combo_line_ids: [["link", l1, l2]] });
|
||||
|
||||
expect([l1, l2].every((n) => l3.combo_line_ids.includes(n))).toBe(true);
|
||||
|
||||
l1.delete();
|
||||
expect(models["pos.order.line"].read(l1.id)).toBe(undefined);
|
||||
expect(l3.combo_line_ids).not.toInclude(l1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("models without backlinks", () => {
|
||||
describe("many2one and one2many field relations to other models", () => {
|
||||
test("create operation", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const category = models["pos.category"].create({ id: 1 });
|
||||
const product = models["product.template"].create({ pos_categ_ids: [category] });
|
||||
expect(product.pos_categ_ids).toEqual([category]);
|
||||
expect(category.backLink("<-product.template.pos_categ_ids")).toEqual([product]);
|
||||
});
|
||||
|
||||
test("read operation 5", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const p1 = models["product.template"].create({ pos_categ_ids: [c1] });
|
||||
const p2 = models["product.template"].create({ pos_categ_ids: [c1] });
|
||||
|
||||
const readC1 = models["pos.category"].read(c1.id);
|
||||
expect(readC1).toEqual(c1);
|
||||
|
||||
const readP1 = models["product.template"].read(p1.id);
|
||||
expect(readP1).toEqual(p1);
|
||||
|
||||
expect(readC1.backLink("product.template.pos_categ_ids")).toEqual([p1, p2]);
|
||||
|
||||
expect(readP1.pos_categ_ids).toEqual([c1]);
|
||||
});
|
||||
|
||||
test("update operation, many2one", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const p1 = models["product.template"].create({});
|
||||
const c1 = models["pos.category"].create({});
|
||||
|
||||
p1.update({ pos_categ_ids: [c1] });
|
||||
expect(p1.pos_categ_ids).toInclude(c1);
|
||||
});
|
||||
|
||||
test("update operation, unlink many2one", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const categ = models["pos.category"].create({});
|
||||
const p1 = models["product.template"].create({ pos_categ_ids: [categ] });
|
||||
|
||||
p1.update({ pos_categ_ids: [] });
|
||||
expect(p1.pos_categ_ids).toHaveLength(0);
|
||||
expect(categ.backLink("product.template.pos_categ_ids")).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("delete operation, many2one item", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const p1 = models["product.template"].create({});
|
||||
const c1 = models["pos.category"].create({});
|
||||
|
||||
p1.update({ pos_categ_ids: [c1] });
|
||||
expect(c1.backLink("<-product.template.pos_categ_ids")).toEqual([p1]);
|
||||
|
||||
c1.delete();
|
||||
expect(models["pos.category"].read(c1.id)).toBe(undefined);
|
||||
expect(p1.pos_categ_ids).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("many2many relations", () => {
|
||||
test("create operation, link", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const l1 = models["pos.order.line"].create({});
|
||||
const l2 = models["pos.order.line"].create({});
|
||||
const l3 = models["pos.order.line"].create({});
|
||||
const o1 = models["pos.order"].create({
|
||||
lines: [["link", l1, l2, l3]],
|
||||
});
|
||||
|
||||
expect(o1.lines).toInclude(l1);
|
||||
});
|
||||
|
||||
test("read operation 6", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const c1 = models["pos.category"].create({});
|
||||
const c2 = models["pos.category"].create({});
|
||||
const p1 = models["product.template"].create({
|
||||
pos_categ_ids: [["link", c1, c2]],
|
||||
});
|
||||
const p2 = models["product.template"].create({
|
||||
pos_categ_ids: [["link", c1, c2]],
|
||||
});
|
||||
const p3 = models["product.template"].create({ pos_categ_ids: [["link", c1]] });
|
||||
|
||||
const readT1 = models["product.template"].read(p1.id);
|
||||
expect(readT1).toEqual(p1);
|
||||
|
||||
const readP1 = models["product.template"].read(p2.id);
|
||||
expect(readP1).toEqual(p2);
|
||||
|
||||
const readMany = models["product.template"].readMany([p2.id, p3.id]);
|
||||
expect(readMany).toEqual([p2, p3]);
|
||||
});
|
||||
|
||||
test("update operation, link", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const p1 = models["product.template"].create({});
|
||||
const p2 = models["product.template"].create({});
|
||||
const t1 = models["pos.category"].create({});
|
||||
|
||||
p1.update({ pos_categ_ids: [["link", t1]] });
|
||||
expect(p1.pos_categ_ids).toInclude(t1);
|
||||
expect(t1.backLink("<-product.template.pos_categ_ids")).toInclude(p1);
|
||||
expect(t1.backLink("<-product.template.pos_categ_ids")).not.toInclude(p2);
|
||||
|
||||
p2.update({ pos_categ_ids: [["link", t1]] });
|
||||
expect(t1.backLink("<-product.template.pos_categ_ids")).toInclude(p2);
|
||||
});
|
||||
|
||||
test("update operation, unlink", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const p1 = models["product.template"].create({});
|
||||
const t1 = models["pos.category"].create({});
|
||||
|
||||
p1.update({ pos_categ_ids: [["link", t1]] });
|
||||
expect(t1.backLink("<-product.template.pos_categ_ids")).toInclude(p1);
|
||||
expect(p1.pos_categ_ids).toInclude(t1);
|
||||
|
||||
p1.update({ pos_categ_ids: [["unlink", t1]] });
|
||||
expect(t1.backLink("<-product.template.pos_categ_ids")).not.toInclude(p1);
|
||||
expect(p1.pos_categ_ids).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("update operation, Clear", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const tag1 = models["pos.category"].create({});
|
||||
const tag2 = models["pos.category"].create({});
|
||||
const product = models["product.template"].create({ pos_categ_ids: [tag1, tag2] });
|
||||
models["product.template"].update(product, { pos_categ_ids: [["clear"]] });
|
||||
const updatedProduct = models["product.template"].read(product.id);
|
||||
expect(updatedProduct.pos_categ_ids).toHaveLength(0);
|
||||
|
||||
models["product.template"].update(product, { pos_categ_ids: [["link", tag1, tag2]] });
|
||||
expect([tag1, tag2].every((t) => product.pos_categ_ids.includes(t))).toBe(true);
|
||||
models["product.template"].update(product, { pos_categ_ids: [["clear"]] });
|
||||
expect(tag1.backLink("<-product.template.pos_categ_ids")).not.toInclude(product);
|
||||
});
|
||||
|
||||
test("delete operation", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const p1 = models["product.template"].create({});
|
||||
const p2 = models["product.template"].create({});
|
||||
const t1 = models["pos.category"].create({});
|
||||
|
||||
p1.update({ pos_categ_ids: [["link", t1]] });
|
||||
p2.update({ pos_categ_ids: [["link", t1]] });
|
||||
|
||||
expect(t1.backLink("<-product.template.pos_categ_ids")).toInclude(p1);
|
||||
|
||||
p1.delete();
|
||||
expect(models["product.template"].read(p1.id)).toBe(undefined);
|
||||
expect(t1.backLink("<-product.template.pos_categ_ids")).not.toInclude(p1);
|
||||
|
||||
t1.delete();
|
||||
expect(models["pos.category"].read(t1.id)).toBe(undefined);
|
||||
expect(p1.pos_categ_ids).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,179 @@
|
|||
import { expect, test, describe } from "@odoo/hoot";
|
||||
import { uuidv4 } from "@point_of_sale/utils";
|
||||
import { getRelatedModelsInstance } from "../data/get_model_definitions";
|
||||
import { makeMockServer } from "@web/../tests/web_test_helpers";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
definePosModels();
|
||||
|
||||
describe("Dirty record", () => {
|
||||
test("field update", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({});
|
||||
expect(order.isDirty()).toBe(true);
|
||||
order.amount_total = 23.5;
|
||||
models.serializeForORM(order, { orm: true });
|
||||
|
||||
// Setting the same value must not mark the record as dirty.
|
||||
expect(order.isDirty()).toBe(false);
|
||||
order.amount_total = 23.5;
|
||||
expect(order.isDirty()).toBe(false);
|
||||
order.amount_total = 25;
|
||||
expect(order.isDirty()).toBe(true);
|
||||
models.serializeForORM(order, { orm: true });
|
||||
expect(order.isDirty()).toBe(false);
|
||||
|
||||
order.update({ amount_total: 26 });
|
||||
expect(order.isDirty()).toBe(true);
|
||||
});
|
||||
|
||||
test("model creation", async () => {
|
||||
// Models created with a numeric ID are not considered dirty by default
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({ id: 12 });
|
||||
expect(order.isDirty()).toBe(false);
|
||||
|
||||
order.amount_total = 23.5;
|
||||
expect(order.isDirty()).toBe(true);
|
||||
});
|
||||
|
||||
test("load data", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const sampleUUID = uuidv4();
|
||||
|
||||
// When loading data, the dirty flag must not be updated.
|
||||
models.loadConnectedData({
|
||||
"pos.order": [
|
||||
{
|
||||
id: 13,
|
||||
amount_total: 30,
|
||||
uuid: sampleUUID,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const order = models["pos.order"].getBy("uuid", sampleUUID);
|
||||
expect(order.id).toBe(13);
|
||||
expect(order.amount_total).toBe(30);
|
||||
expect(order.isDirty()).toBe(false);
|
||||
});
|
||||
|
||||
test("related record update", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({ id: 12 });
|
||||
expect(order.isDirty()).toBe(false);
|
||||
|
||||
function clearOrder() {
|
||||
models.serializeForORM(order, { orm: true });
|
||||
expect(order.isDirty()).toBe(false);
|
||||
}
|
||||
|
||||
// Add new line to the order
|
||||
const line = models["pos.order.line"].create({
|
||||
qty: 1,
|
||||
order_id: order,
|
||||
});
|
||||
expect(line.isDirty()).toBe(true);
|
||||
expect(order.isDirty()).toBe(true);
|
||||
clearOrder();
|
||||
expect(line.isDirty()).toBe(false);
|
||||
|
||||
// Assign a product to the line
|
||||
const sampleProduct = models["product.product"].create({ name: "demo_product", id: 111 });
|
||||
line.product_id = sampleProduct;
|
||||
expect(line.isDirty()).toBe(true);
|
||||
expect(order.isDirty()).toBe(true);
|
||||
clearOrder();
|
||||
expect(line.isDirty()).toBe(false);
|
||||
|
||||
// Update line quantity
|
||||
line.qty = 10;
|
||||
expect(line.isDirty()).toBe(true);
|
||||
expect(order.isDirty()).toBe(true);
|
||||
clearOrder();
|
||||
|
||||
order.lines[0].qty = 1000;
|
||||
expect(line.isDirty()).toBe(true);
|
||||
expect(order.isDirty()).toBe(true);
|
||||
clearOrder();
|
||||
|
||||
// Delete product from line
|
||||
line.product_id = null;
|
||||
expect(order.isDirty()).toBe(true);
|
||||
clearOrder();
|
||||
|
||||
line.delete();
|
||||
expect(order.isDirty()).toBe(true);
|
||||
expect(order.lines.length).toBe(0);
|
||||
});
|
||||
|
||||
test("many2many", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({ id: 12 });
|
||||
function clearOrder() {
|
||||
models.serializeForORM(order, { orm: true });
|
||||
expect(order.isDirty()).toBe(false);
|
||||
}
|
||||
const att1 = models["product.template.attribute.value"].create({ id: 99 });
|
||||
const line = models["pos.order.line"].create({ id: 100, order_id: order, qty: 1 });
|
||||
line.update({ attribute_value_ids: [["link", att1]] });
|
||||
expect(line.isDirty()).toBe(true);
|
||||
expect(order.isDirty()).toBe(true);
|
||||
|
||||
clearOrder();
|
||||
const att2 = models["product.template.attribute.value"].create({ id: 999 });
|
||||
line.update({ attribute_value_ids: [["link", att2]] });
|
||||
expect(line.isDirty()).toBe(true);
|
||||
expect(order.isDirty()).toBe(true);
|
||||
|
||||
clearOrder();
|
||||
line.update({ attribute_value_ids: [["unlink", att1]] });
|
||||
expect(line.isDirty()).toBe(true);
|
||||
expect(order.isDirty()).toBe(true);
|
||||
});
|
||||
|
||||
test("datetime type", async () => {
|
||||
await makeMockServer();
|
||||
const models = getRelatedModelsInstance(false);
|
||||
const order = models["pos.order"].create({ id: 12 });
|
||||
function clearOrder() {
|
||||
models.serializeForORM(order, { orm: true });
|
||||
expect(order.isDirty()).toBe(false);
|
||||
}
|
||||
expect(order.isDirty()).toBe(false);
|
||||
|
||||
order.date_order = undefined;
|
||||
expect(order.isDirty()).toBe(false);
|
||||
|
||||
// Other valid DateTime
|
||||
clearOrder();
|
||||
order.date_order = DateTime.local(2025, 1, 1, 9, 30);
|
||||
expect(order.isDirty()).toBe(true);
|
||||
|
||||
// Same DateTime
|
||||
clearOrder();
|
||||
order.date_order = DateTime.local(2025, 1, 1, 9, 30);
|
||||
expect(order.isDirty()).toBe(false);
|
||||
|
||||
// Different DateTime
|
||||
clearOrder();
|
||||
order.date_order = DateTime.local(2028, 1, 1, 10, 30);
|
||||
expect(order.isDirty()).toBe(true);
|
||||
|
||||
// Set to false / null
|
||||
clearOrder();
|
||||
order.date_order = false;
|
||||
expect(order.isDirty()).toBe(true);
|
||||
|
||||
clearOrder();
|
||||
order.date_order = null;
|
||||
expect(order.isDirty()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { test, describe, expect } from "@odoo/hoot";
|
||||
import { convertRawToDate, convertDateToRaw } from "@point_of_sale/app/models/related_models/utils";
|
||||
const { DateTime } = luxon;
|
||||
|
||||
describe("Date conversion utilities", () => {
|
||||
const mockModel = { model: "test.model" };
|
||||
|
||||
test("convertRawToDate", () => {
|
||||
const rawDate = "2023-12-25";
|
||||
const result = convertRawToDate(mockModel, rawDate, "date_field");
|
||||
expect(result).toBeInstanceOf(DateTime);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(convertRawToDate(mockModel, null, "date_field")).toBe(undefined);
|
||||
expect(convertRawToDate(mockModel, undefined, "date_field")).toBe(undefined);
|
||||
expect(() => convertRawToDate(mockModel, "invalid", "date_field")).toThrow();
|
||||
});
|
||||
|
||||
test("convertDateToRaw", () => {
|
||||
const dateTime = DateTime.fromISO("2023-12-25");
|
||||
const result = convertDateToRaw(dateTime);
|
||||
expect(result).toBe("2023-12-25"); // directly check the string value
|
||||
expect(convertDateToRaw(null)).toBe(undefined);
|
||||
expect(convertDateToRaw(undefined)).toBe(undefined);
|
||||
expect(convertDateToRaw("2023-12-25")).toBe("2023-12-25"); // should return the same string
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { test, expect, describe } from "@odoo/hoot";
|
||||
import { getFilledOrder, setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
describe("data_service", () => {
|
||||
test("localDeleteCascade", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const data = store.data;
|
||||
const order = await getFilledOrder(store);
|
||||
|
||||
expect(store.models["pos.order"].length).toBe(1);
|
||||
expect(store.models["pos.order.line"].length).toBe(2);
|
||||
data.localDeleteCascade(order);
|
||||
expect(store.models["pos.order"].length).toBe(0);
|
||||
expect(store.models["pos.order.line"].length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { test, expect, describe } from "@odoo/hoot";
|
||||
import { setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
describe("printer_service.js", () => {
|
||||
test("print should work whith printer in hardware_proxy", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const printerService = store.env.services.printer;
|
||||
// Mock renderer
|
||||
printerService.renderer = {
|
||||
toHtml: async () => document.createElement("div"),
|
||||
whenMounted: ({ callback, el }) => callback(el),
|
||||
};
|
||||
// Mock printer
|
||||
const mockPrinter = {
|
||||
printReceipt: async () => ({ successful: true }),
|
||||
};
|
||||
store.env.services.hardware_proxy.printer = mockPrinter;
|
||||
// Spy log
|
||||
const originalConsoleLog = console.log;
|
||||
const consoleLogCalls = [];
|
||||
console.log = (...args) => {
|
||||
consoleLogCalls.push(args);
|
||||
};
|
||||
try {
|
||||
const lenBefore = consoleLogCalls.length;
|
||||
await printerService.print(() => {}, {}, {});
|
||||
const lenAfter = consoleLogCalls.length;
|
||||
expect(lenAfter).toBe(lenBefore);
|
||||
} finally {
|
||||
console.log = originalConsoleLog;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,644 @@
|
|||
import { test, expect, describe } from "@odoo/hoot";
|
||||
import { getFilledOrder, setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
import { ConnectionLostError } from "@web/core/network/rpc";
|
||||
import { onRpc } from "@web/../tests/web_test_helpers";
|
||||
import { imageUrl } from "@web/core/utils/urls";
|
||||
import { prepareRoundingVals } from "../accounting/utils";
|
||||
const { DateTime } = luxon;
|
||||
|
||||
definePosModels();
|
||||
|
||||
describe("pos_store.js", () => {
|
||||
test("setTip", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store); // Should have 2 lines
|
||||
expect(order.lines.length).toBe(2);
|
||||
|
||||
await store.setTip(50);
|
||||
expect(order.is_tipped).toBe(true);
|
||||
expect(order.tip_amount).toBe(50);
|
||||
expect(order.lines.length).toBe(3); // 2 original lines + 1 tip line
|
||||
});
|
||||
|
||||
test("orderNoteFormat", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const str = store.getStrNotes("string");
|
||||
expect(str).toBeOfType("string");
|
||||
expect(str).toBe("string");
|
||||
const json2str = store.getStrNotes([{ text: "json", colorIndex: 0 }]);
|
||||
expect(json2str).toBeOfType("string");
|
||||
expect(json2str).toBe("json");
|
||||
});
|
||||
|
||||
test("connectNewData canceled order", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const models = store.models;
|
||||
const order = await getFilledOrder(store);
|
||||
const serializedOrder = { ...order.raw };
|
||||
|
||||
await store.deleteOrders([order]);
|
||||
serializedOrder.state = "cancel";
|
||||
let isListenerCalled = false;
|
||||
const listenerCleanup = models["pos.order"].addEventListener("update", (data) => {
|
||||
if (data.id === order.id) {
|
||||
const orderToRecompute = models["pos.order"].get(data.id);
|
||||
orderToRecompute.triggerRecomputeAllPrices();
|
||||
isListenerCalled = true;
|
||||
}
|
||||
});
|
||||
models.connectNewData({
|
||||
"pos.order": [serializedOrder],
|
||||
});
|
||||
const updatedOrder = models["pos.order"].get(order.id);
|
||||
expect(updatedOrder.state).toBe("cancel");
|
||||
expect(isListenerCalled).toBe(true);
|
||||
listenerCleanup();
|
||||
});
|
||||
|
||||
test("sendOrderInPreparation clears change state when no prep changes", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
const productOutsidePrep = store.models["product.template"].get(14);
|
||||
const date = DateTime.now();
|
||||
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: productOutsidePrep,
|
||||
qty: 1,
|
||||
write_date: date,
|
||||
create_date: date,
|
||||
},
|
||||
order
|
||||
);
|
||||
|
||||
expect(order.hasChange).toBe(true);
|
||||
|
||||
let printCalled = false;
|
||||
store.printChanges = async () => {
|
||||
printCalled = true;
|
||||
return true;
|
||||
};
|
||||
|
||||
await store.sendOrderInPreparation(order);
|
||||
|
||||
expect(printCalled).toBe(false);
|
||||
expect(order.hasChange).toBe(false);
|
||||
});
|
||||
|
||||
describe("syncAllOrders", () => {
|
||||
test("simple sync", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
|
||||
expect(store.getPendingOrder().orderToCreate).toHaveLength(1);
|
||||
expect(order.lines).toHaveLength(2);
|
||||
expect(order.lines[0].id).toBeOfType("string");
|
||||
expect(order.lines[1].id).toBeOfType("string");
|
||||
|
||||
await store.syncAllOrders();
|
||||
// Object should be updated in place
|
||||
expect(store.getPendingOrder().orderToCreate).toHaveLength(0);
|
||||
expect(order.lines).toHaveLength(2);
|
||||
expect(order.lines[0].id).toBeOfType("number");
|
||||
expect(order.lines[1].id).toBeOfType("number");
|
||||
|
||||
const noSync = await store.syncAllOrders();
|
||||
expect(noSync).toBe(undefined);
|
||||
expect(store.models["pos.order"].length).toBe(1);
|
||||
});
|
||||
|
||||
test("sync specific order", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order1 = await getFilledOrder(store);
|
||||
const order2 = await getFilledOrder(store);
|
||||
|
||||
expect(store.getPendingOrder().orderToCreate).toHaveLength(2);
|
||||
expect(order1.lines).toHaveLength(2);
|
||||
expect(order1.lines[0].id).toBeOfType("string");
|
||||
expect(order1.lines[1].id).toBeOfType("string");
|
||||
|
||||
expect(order2.lines).toHaveLength(2);
|
||||
expect(order2.lines[0].id).toBeOfType("string");
|
||||
expect(order2.lines[1].id).toBeOfType("string");
|
||||
|
||||
await store.syncAllOrders({ orders: [order1] });
|
||||
expect(store.getPendingOrder().orderToCreate).toHaveLength(1);
|
||||
expect(order1.lines).toHaveLength(2);
|
||||
expect(order1.lines[0].id).toBeOfType("number");
|
||||
expect(order1.lines[1].id).toBeOfType("number");
|
||||
|
||||
expect(order2.lines).toHaveLength(2);
|
||||
expect(order2.lines[0].id).toBeOfType("string");
|
||||
expect(order2.lines[1].id).toBeOfType("string");
|
||||
|
||||
const data = await store.syncAllOrders();
|
||||
expect(data).toHaveLength(1);
|
||||
expect(store.getPendingOrder().orderToCreate).toHaveLength(0);
|
||||
expect(order2.lines).toHaveLength(2);
|
||||
expect(order2.lines[0].id).toBeOfType("number");
|
||||
expect(order2.lines[1].id).toBeOfType("number");
|
||||
});
|
||||
|
||||
test("sync no network should not raise error", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
|
||||
expect(store.getPendingOrder().orderToCreate).toHaveLength(1);
|
||||
expect(order.lines).toHaveLength(2);
|
||||
expect(order.lines[0].id).toBeOfType("string");
|
||||
expect(order.lines[1].id).toBeOfType("string");
|
||||
|
||||
store.data.network.offline = true;
|
||||
const data = await store.syncAllOrders();
|
||||
expect(data).toBeInstanceOf(ConnectionLostError);
|
||||
expect(store.getPendingOrder().orderToCreate).toHaveLength(1);
|
||||
expect(order.lines).toHaveLength(2);
|
||||
expect(order.lines[0].id).toBeOfType("string");
|
||||
expect(order.lines[1].id).toBeOfType("string");
|
||||
});
|
||||
|
||||
test("insync order should not be re-synced", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
|
||||
expect(store.getPendingOrder().orderToCreate).toHaveLength(1);
|
||||
expect(order.lines).toHaveLength(2);
|
||||
expect(order.lines[0].id).toBeOfType("string");
|
||||
expect(order.lines[1].id).toBeOfType("string");
|
||||
store.syncingOrders.add(order.uuid);
|
||||
|
||||
const data = await store.syncAllOrders();
|
||||
expect(store.getPendingOrder().orderToCreate).toHaveLength(1);
|
||||
expect(data).toBeEmpty();
|
||||
expect(order.lines).toHaveLength(2);
|
||||
expect(order.lines[0].id).toBeOfType("string");
|
||||
expect(order.lines[1].id).toBeOfType("string");
|
||||
});
|
||||
});
|
||||
|
||||
test("addLineToCurrentOrder", async () => {
|
||||
const store = await setupPosEnv();
|
||||
store.setOrder(null);
|
||||
expect(store.getOrder()).toBe(undefined);
|
||||
// Should create order if none exist
|
||||
const product = store.models["product.product"].get(5);
|
||||
await store.addLineToCurrentOrder({ product_tmpl_id: product.product_tmpl_id });
|
||||
expect(store.getOrder()).not.toBe(undefined);
|
||||
expect(store.getOrder().lines.length).toBe(1);
|
||||
expect(store.getOrder().lines[0].product_id.id).toBe(product.id);
|
||||
expect(store.getOrder().lines[0].qty).toBe(1);
|
||||
await store.addLineToCurrentOrder({ product_tmpl_id: product.product_tmpl_id, qty: 3 }, {});
|
||||
expect(store.getOrder().lines[0].qty).toBe(4);
|
||||
});
|
||||
|
||||
test("changesToOrderNoPrepCateg", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const orderChange = store.changesToOrder(order, new Set([]), false);
|
||||
expect(orderChange.new.length).toBe(0);
|
||||
expect(orderChange.cancelled.length).toBe(0);
|
||||
});
|
||||
|
||||
test("orderContainsProduct", async () => {
|
||||
const store = await setupPosEnv();
|
||||
await getFilledOrder(store);
|
||||
const product1 = store.models["product.template"].get(5);
|
||||
const product2 = store.models["product.template"].get(6);
|
||||
const product3 = store.models["product.template"].get(8);
|
||||
expect(store.orderContainsProduct(product1)).toBe(true);
|
||||
expect(store.orderContainsProduct(product2)).toBe(true);
|
||||
expect(store.orderContainsProduct(product3)).toBe(false);
|
||||
const order = await store.addNewOrder();
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: product3,
|
||||
qty: 1,
|
||||
},
|
||||
order
|
||||
);
|
||||
|
||||
expect(store.orderContainsProduct(product1)).toBe(true);
|
||||
expect(store.orderContainsProduct(product2)).toBe(true);
|
||||
expect(store.orderContainsProduct(product3)).toBe(true);
|
||||
});
|
||||
|
||||
test("generateReceiptsDataToPrint", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const pos_categories = store.models["pos.category"].getAll().map((c) => c.id);
|
||||
const order = await getFilledOrder(store);
|
||||
order.lines[1].setNote('[{"text":"Wait","colorIndex":0}]');
|
||||
|
||||
order.lines[0].setCustomerNote("Test Orderline Customer Note");
|
||||
const orderChange = store.changesToOrder(order, new Set([...pos_categories]), false);
|
||||
|
||||
const { orderData, changes } = store.generateOrderChange(
|
||||
order,
|
||||
orderChange,
|
||||
pos_categories,
|
||||
false
|
||||
);
|
||||
|
||||
const receiptsData = await store.generateReceiptsDataToPrint(
|
||||
orderData,
|
||||
changes,
|
||||
orderChange
|
||||
);
|
||||
expect(receiptsData.length).toBe(1);
|
||||
expect(receiptsData[0].changes.title).toBe("NEW");
|
||||
expect(receiptsData[0].changes.data.length).toBe(2);
|
||||
expect(receiptsData[0].changes.data[0]).toEqual({
|
||||
uuid: order.lines[0].uuid,
|
||||
name: "TEST",
|
||||
basic_name: "TEST",
|
||||
combo_parent_uuid: undefined,
|
||||
customer_note: "Test Orderline Customer Note",
|
||||
product_id: 5,
|
||||
attribute_value_names: [],
|
||||
quantity: 3,
|
||||
note: "",
|
||||
pos_categ_id: 1,
|
||||
pos_categ_sequence: 1,
|
||||
display_name: "TEST",
|
||||
group: undefined,
|
||||
isCombo: false,
|
||||
});
|
||||
expect(receiptsData[0].changes.data[1]).toEqual({
|
||||
uuid: order.lines[1].uuid,
|
||||
name: "TEST 2",
|
||||
basic_name: "TEST 2",
|
||||
combo_parent_uuid: undefined,
|
||||
customer_note: "",
|
||||
product_id: 6,
|
||||
attribute_value_names: [],
|
||||
quantity: 2,
|
||||
note: "Wait",
|
||||
pos_categ_id: 2,
|
||||
pos_categ_sequence: 2,
|
||||
display_name: "TEST 2",
|
||||
group: undefined,
|
||||
isCombo: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("filterChangeByCategories", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const allowedCategories = [1];
|
||||
|
||||
const productA = store.models["product.product"].get(5);
|
||||
const productB = store.models["product.product"].get(6);
|
||||
productA.pos_categ_ids = [1];
|
||||
productB.pos_categ_ids = [2];
|
||||
|
||||
const currentOrderChange = {
|
||||
new: [
|
||||
{ uuid: "combo-parent-uuid", isCombo: true },
|
||||
{
|
||||
uuid: "combo-child-a-uuid",
|
||||
combo_parent_uuid: "combo-parent-uuid",
|
||||
product_id: productA.id,
|
||||
isCombo: false,
|
||||
},
|
||||
{
|
||||
uuid: "combo-child-b-uuid",
|
||||
combo_parent_uuid: "combo-parent-uuid",
|
||||
product_id: productB.id,
|
||||
isCombo: false,
|
||||
},
|
||||
{ uuid: "line1", product_id: productA.id, isCombo: false },
|
||||
{ uuid: "line2", product_id: productB.id, isCombo: false },
|
||||
],
|
||||
cancelled: [],
|
||||
noteUpdate: [],
|
||||
};
|
||||
|
||||
const filtered = store.filterChangeByCategories(allowedCategories, currentOrderChange);
|
||||
|
||||
const expectedUuids = ["combo-parent-uuid", "combo-child-a-uuid", "line1"];
|
||||
const actualUuids = filtered.new.map((c) => c.uuid);
|
||||
|
||||
expect(actualUuids.sort()).toEqual(expectedUuids.sort());
|
||||
});
|
||||
|
||||
test("deleteOrders", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order1 = await getFilledOrder(store);
|
||||
await store.syncAllOrders();
|
||||
await store.deleteOrders([order1]);
|
||||
expect(store.models["pos.order"].getBy("uuid", order1.uuid)).toBeEmpty();
|
||||
});
|
||||
|
||||
test("deleteOrders multiple orders", async () => {
|
||||
const store = await setupPosEnv();
|
||||
await getFilledOrder(store);
|
||||
store.addNewOrder();
|
||||
let openOrders = store.getOpenOrders();
|
||||
expect(openOrders.length).toBe(2);
|
||||
const deletedOrders = await store.deleteOrders(openOrders);
|
||||
expect(deletedOrders).toBe(true);
|
||||
openOrders = store.getOpenOrders();
|
||||
expect(openOrders.length).toBe(0);
|
||||
});
|
||||
|
||||
test("getOrderData", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const orderData = store.getOrderData(order);
|
||||
expect(orderData).toEqual({
|
||||
reprint: undefined,
|
||||
pos_reference: "1001",
|
||||
config_name: "Hoot",
|
||||
time: "10:30",
|
||||
tracking_number: "1001",
|
||||
preset_time: false,
|
||||
preset_name: "In",
|
||||
employee_name: "Administrator",
|
||||
internal_note: "",
|
||||
general_customer_note: "",
|
||||
changes: {
|
||||
title: "",
|
||||
data: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("productsToDisplay", async () => {
|
||||
const store = await setupPosEnv();
|
||||
store.selectedCategory = store.models["pos.category"].get(1);
|
||||
let products = store.productsToDisplay;
|
||||
expect(products.length).toBe(2);
|
||||
expect(products[0].id).toBe(17);
|
||||
expect(products[1].id).toBe(5);
|
||||
expect(store.selectedCategory.id).toBe(1);
|
||||
store.selectedCategory = store.models["pos.category"].get(1);
|
||||
store.searchProductWord = "TEST";
|
||||
products = store.productsToDisplay;
|
||||
expect(products.length).toBe(4);
|
||||
expect(products[0].id).toBe(5);
|
||||
expect(products[1].id).toBe(6);
|
||||
expect(store.selectedCategory).toBe(undefined);
|
||||
store.searchProductWord = "TEST 2";
|
||||
products = store.productsToDisplay;
|
||||
expect(products.length).toBe(1);
|
||||
expect(products[0].id).toBe(6);
|
||||
});
|
||||
|
||||
test("productToDisplayByCateg", async () => {
|
||||
const store = await setupPosEnv();
|
||||
|
||||
// Case 1: Grouping disabled
|
||||
store.config.iface_group_by_categ = false;
|
||||
let grouped = store.productToDisplayByCateg;
|
||||
expect(grouped.length).toBe(1); //Only one group
|
||||
expect(grouped[0][0]).toBe("0");
|
||||
expect(grouped[0][1].length).toBe(15);
|
||||
|
||||
// Case 2: Grouping enabled
|
||||
store.config.iface_group_by_categ = true;
|
||||
grouped = store.productToDisplayByCateg;
|
||||
expect(grouped.length).toBe(6);
|
||||
// Confirm grouping structure
|
||||
for (const [catId, prods] of grouped) {
|
||||
expect(Array.isArray(prods)).toBe(true);
|
||||
expect(prods.length).toBeGreaterThan(0);
|
||||
for (const prod of prods) {
|
||||
if (catId === "0") {
|
||||
expect(prod.pos_categ_ids.length).toBe(0);
|
||||
continue;
|
||||
}
|
||||
const categoryIds = prod.pos_categ_ids.map((c) => c.id);
|
||||
expect(categoryIds).toInclude(parseInt(catId));
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Grouping with search filtering
|
||||
store.searchProductWord = "TEST";
|
||||
grouped = store.productToDisplayByCateg;
|
||||
expect(grouped.length).toBe(3);
|
||||
expect(grouped[0][1][0].name).toBe("TEST");
|
||||
expect(grouped[1][1][0].name).toBe("TEST 2");
|
||||
expect(grouped[2][1][0].name).toBe("Accounting Test Product 1");
|
||||
expect(grouped[2][1][1].name).toBe("Accounting Test Product 2");
|
||||
|
||||
// Case 4: Grouping with category filtering
|
||||
store.searchProductWord = "";
|
||||
store.selectedCategory = store.models["pos.category"].get(1);
|
||||
grouped = store.productToDisplayByCateg;
|
||||
expect(grouped.length).toBe(1);
|
||||
expect(grouped[0][0]).toBe(1);
|
||||
expect(grouped[0][1][0].name).toBe("Multi Category Product");
|
||||
expect(grouped[0][1][1].name).toBe("TEST");
|
||||
|
||||
// Case 5: Grouping with category 'Food' selected (parent of 'Burger' & 'Pizza')
|
||||
store.selectedCategory = store.models["pos.category"].get(3);
|
||||
grouped = store.productToDisplayByCateg;
|
||||
expect(grouped.length).toBe(3);
|
||||
expect(grouped[0][0]).toBe(3);
|
||||
expect(grouped[0][1][0].name).toBe("Club sandwich");
|
||||
expect(grouped[1][1][0].name).toBe("Bacon burger");
|
||||
expect(grouped[2][1][0].name).toBe("Pizza margarita");
|
||||
});
|
||||
|
||||
test("productToDisplayByCateg count", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const createProductsAndCateg = (prefix, count) => {
|
||||
const categ = store.models["pos.category"].create({
|
||||
name: `${prefix}_categ`,
|
||||
});
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
store.models["product.template"].create({
|
||||
name: `${prefix}_${i}`,
|
||||
pos_categ_ids: [categ.id],
|
||||
});
|
||||
}
|
||||
|
||||
return categ;
|
||||
};
|
||||
|
||||
store.config.iface_group_by_categ = true;
|
||||
const test1 = createProductsAndCateg("producttest1", 100);
|
||||
const test2 = createProductsAndCateg("producttest2", 150);
|
||||
const grouped = store.productToDisplayByCateg;
|
||||
const byCateg = store.models["product.template"].getAllBy("pos_categ_ids");
|
||||
|
||||
const test1Products = byCateg[test1.id];
|
||||
const test2Products = byCateg[test2.id];
|
||||
const groupedTest1 = grouped.find((data) => data[0] == test1.id)[1];
|
||||
const groupedTest2 = grouped.find((data) => data[0] == test2.id)[1];
|
||||
|
||||
expect(test1Products).toHaveLength(100);
|
||||
expect(test2Products).toHaveLength(150);
|
||||
expect(groupedTest1).toHaveLength(100);
|
||||
expect(groupedTest2).toHaveLength(100);
|
||||
|
||||
store.searchProductWord = "producttest1";
|
||||
const groupedSearchTest1 = store.productToDisplayByCateg;
|
||||
const groupedSearchTest1Prods = groupedSearchTest1.find((data) => data[0] == test1.id)[1];
|
||||
expect(groupedSearchTest1).toHaveLength(1);
|
||||
expect(groupedSearchTest1Prods).toHaveLength(100);
|
||||
|
||||
store.searchProductWord = "producttest";
|
||||
const groupedSearchTest = store.productToDisplayByCateg;
|
||||
const groupedSearchTest1Prods2 = groupedSearchTest.find((data) => data[0] == test1.id)[1];
|
||||
const groupedSearchTest2Prods2 = groupedSearchTest.find((data) => data[0] == test2.id)[1];
|
||||
expect(groupedSearchTest).toHaveLength(2);
|
||||
expect(groupedSearchTest1Prods2).toHaveLength(100);
|
||||
expect(groupedSearchTest2Prods2).toHaveLength(100);
|
||||
|
||||
store.searchProductWord = "";
|
||||
const productWoCategory = store.models["product.template"].readMany([15, 16]);
|
||||
const groupedSearchTest3 = store.productToDisplayByCateg;
|
||||
expect(groupedSearchTest3[groupedSearchTest3.length - 1][0]).toEqual("0");
|
||||
expect(groupedSearchTest3[groupedSearchTest3.length - 1][1]).toHaveLength(2);
|
||||
expect(groupedSearchTest3[groupedSearchTest3.length - 1][1].map((p) => p.id)).toEqual(
|
||||
productWoCategory.map((p) => p.id)
|
||||
);
|
||||
|
||||
store.selectedCategory = test1;
|
||||
const groupedSearchTest4 = store.productToDisplayByCateg;
|
||||
expect(groupedSearchTest4).toHaveLength(1);
|
||||
expect(groupedSearchTest4[0][0]).toEqual(test1.id);
|
||||
expect(groupedSearchTest4[0][1]).toHaveLength(100);
|
||||
});
|
||||
|
||||
test("onDeleteOrder", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
const deletedOrder = await store.onDeleteOrder(order);
|
||||
expect(order.uiState.displayed).toBe(false);
|
||||
expect(deletedOrder).toBe(true);
|
||||
});
|
||||
|
||||
test("setNextOrderRefs", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
await store.setNextOrderRefs(order);
|
||||
expect(order.pos_reference).toBeOfType("string");
|
||||
expect(order.pos_reference.length).toBeGreaterThan(1);
|
||||
expect(order.sequence_number).toBeOfType("integer");
|
||||
expect(order.tracking_number).toBeOfType("string");
|
||||
expect(order.tracking_number.length).toBeGreaterThan(2);
|
||||
});
|
||||
|
||||
test("pending orders", async () => {
|
||||
const store = await setupPosEnv();
|
||||
let { orderToCreate, orderToUpdate, orderToDelete } = store.getPendingOrder();
|
||||
expect(orderToCreate).toHaveLength(0);
|
||||
expect(orderToUpdate).toHaveLength(0);
|
||||
expect(orderToDelete).toHaveLength(0);
|
||||
const order = await getFilledOrder(store);
|
||||
({ orderToCreate, orderToUpdate, orderToDelete } = store.getPendingOrder());
|
||||
expect(order.id).toBe(orderToCreate[0].id);
|
||||
// After sync, order should be in 'orderToUpdate'
|
||||
await store.syncAllOrders({ orders: [order] });
|
||||
store.addPendingOrder([order.id]);
|
||||
({ orderToCreate, orderToUpdate, orderToDelete } = store.getPendingOrder());
|
||||
expect(orderToCreate).toHaveLength(0);
|
||||
expect(orderToUpdate).toHaveLength(1);
|
||||
// Remove pending order
|
||||
store.addPendingOrder([order.id], true);
|
||||
({ orderToCreate, orderToUpdate, orderToDelete } = store.getPendingOrder());
|
||||
expect(orderToUpdate).toHaveLength(0);
|
||||
expect(orderToDelete).toHaveLength(1);
|
||||
// Clear pending orders
|
||||
store.clearPendingOrder();
|
||||
({ orderToCreate, orderToUpdate, orderToDelete } = store.getPendingOrder());
|
||||
expect(orderToDelete).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("getPaymentMethodFmtAmount", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const cashPm = store.models["pos.payment.method"].find((pm) => pm.is_cash_count);
|
||||
|
||||
// Case 1: No rounding enabled
|
||||
expect(store.getPaymentMethodFmtAmount(cashPm, order)).toBeEmpty();
|
||||
|
||||
// Case 2: Rounding enabled, not limited to cash
|
||||
const { cashPm: cash1, cardPm: card1 } = prepareRoundingVals(store, 0.05, "HALF-UP", false);
|
||||
expect(store.getPaymentMethodFmtAmount(cash1, order)).toBe("$ 17.85");
|
||||
expect(store.getPaymentMethodFmtAmount(card1, order)).toBe("$ 17.85");
|
||||
|
||||
// Case 3: Rounding enabled, only for cash
|
||||
const { cashPm: cash2, cardPm: card2 } = prepareRoundingVals(store, 0.05, "HALF-UP", true);
|
||||
expect(store.getPaymentMethodFmtAmount(cash2, order)).toBe("$ 17.85");
|
||||
expect(store.getPaymentMethodFmtAmount(card2, order)).toBeEmpty();
|
||||
});
|
||||
|
||||
test("canEditPayment", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
expect(store.canEditPayment(order)).toBe(true);
|
||||
order.nb_print = 1;
|
||||
expect(store.canEditPayment(order)).toBe(false);
|
||||
});
|
||||
|
||||
describe("cacheReceiptLogo", () => {
|
||||
function getCompanyLogo256Url(companyId) {
|
||||
const fullUrl = imageUrl("res.company", companyId, "logo", {
|
||||
width: 256,
|
||||
height: 256,
|
||||
});
|
||||
const index = fullUrl.indexOf("/web");
|
||||
return fullUrl.substring(index);
|
||||
}
|
||||
|
||||
test("correctly cached", async () => {
|
||||
onRpc(getCompanyLogo256Url("<int:id>"), async (request, { id }) => {
|
||||
expect.step(`Company logo ${id} fetched`);
|
||||
return `Company logo ${id}`;
|
||||
});
|
||||
const store = await setupPosEnv();
|
||||
const companyId = store.company.id;
|
||||
expect.verifySteps([`Company logo ${companyId} fetched`]);
|
||||
const { receiptLogoUrl } = store.config;
|
||||
expect(receiptLogoUrl).toInclude("data:");
|
||||
expect(atob(receiptLogoUrl.split(",")[1])).toInclude(`Company logo ${companyId}`);
|
||||
});
|
||||
|
||||
test("fetch failed", async () => {
|
||||
onRpc(getCompanyLogo256Url("<int:id>"), async (request, { id }) => {
|
||||
expect.step(`Company logo ${id} fetched`);
|
||||
throw new Error("Fetch failed");
|
||||
});
|
||||
const store = await setupPosEnv();
|
||||
const companyId = store.company.id;
|
||||
expect.verifySteps([`Company logo ${companyId} fetched`]);
|
||||
expect(store.config.receiptLogoUrl).toInclude(getCompanyLogo256Url(companyId));
|
||||
});
|
||||
|
||||
test("preSyncAllOrders", async () => {
|
||||
// This test check prices sign on preSyncAllOrders for refunds
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
|
||||
await store.preSyncAllOrders([order]);
|
||||
expect(order.amount_total).toEqual(17.85);
|
||||
expect(order.amount_tax).toEqual(2.85);
|
||||
expect(order.lines[0].qty).toEqual(3);
|
||||
expect(order.lines[0].price_unit).toEqual(3);
|
||||
expect(order.lines[0].price_subtotal).toEqual(9);
|
||||
expect(order.lines[0].price_subtotal_incl).toEqual(10.35);
|
||||
expect(order.lines[1].qty).toEqual(2);
|
||||
expect(order.lines[1].price_unit).toEqual(3);
|
||||
expect(order.lines[1].price_subtotal).toEqual(6);
|
||||
expect(order.lines[1].price_subtotal_incl).toEqual(7.5);
|
||||
|
||||
order.is_refund = true;
|
||||
order.lines.forEach((line) => (line.qty = -line.qty));
|
||||
await store.preSyncAllOrders([order]);
|
||||
|
||||
expect(order.amount_total).toEqual(-17.85);
|
||||
expect(order.amount_tax).toEqual(-2.85);
|
||||
expect(order.lines[0].qty).toEqual(-3);
|
||||
expect(order.lines[0].price_unit).toEqual(3);
|
||||
expect(order.lines[0].price_subtotal).toEqual(9);
|
||||
expect(order.lines[0].price_subtotal_incl).toEqual(10.35);
|
||||
expect(order.lines[1].qty).toEqual(-2);
|
||||
expect(order.lines[1].price_unit).toEqual(3);
|
||||
expect(order.lines[1].price_subtotal).toEqual(6);
|
||||
expect(order.lines[1].price_subtotal_incl).toEqual(7.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { expect, getFixture, test } from "@odoo/hoot";
|
||||
import { mockFetch } from "@odoo/hoot-mock";
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { allowTranslations, mountWithCleanup } from "@web/../tests/web_test_helpers";
|
||||
import { htmlToCanvas } from "@point_of_sale/app/services/render_service";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
odoo.pos_session_id = 1; // Ensure the session ID is set for lazy getters
|
||||
|
||||
test("test the render service", async () => {
|
||||
class ComponentToBeRendered extends Component {
|
||||
static props = ["name"];
|
||||
static template = xml`
|
||||
<div> It's me, <t t-esc="props.name" />! </div>
|
||||
`;
|
||||
}
|
||||
|
||||
allowTranslations(); // this is needed because we are not loading the localization service
|
||||
const comp = await mountWithCleanup("none");
|
||||
const renderedComp = await comp.env.services.renderer.toHtml(ComponentToBeRendered, {
|
||||
name: "Mario",
|
||||
});
|
||||
expect(renderedComp).toHaveOuterHTML("<div> It's me, Mario! </div>");
|
||||
});
|
||||
|
||||
test("htmlToCanvas", async () => {
|
||||
// htmlToCanvas fetches some fonts useless for the test, we mock it to avoid warnings
|
||||
mockFetch(() => "");
|
||||
const target = getFixture();
|
||||
const node = document.createElement("div");
|
||||
node.classList.add("render-container");
|
||||
target.appendChild(node);
|
||||
|
||||
const asciiChars = Array.from({ length: 256 }, (_, i) => String.fromCharCode(i)).join("");
|
||||
node.textContent = asciiChars;
|
||||
|
||||
let canvas = null;
|
||||
try {
|
||||
canvas = await htmlToCanvas(node, { addClass: "pos-receipt-print" });
|
||||
} catch (error) {
|
||||
// htmlToCanvas create an <img> by setting a svg to its src attribute
|
||||
// if this fails, an Event of type "error" is thrown
|
||||
if (error.constructor.name !== "Event") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
expect(canvas).not.toBe(null, {
|
||||
message: "htmlToCanvas should work with all ascii characters",
|
||||
});
|
||||
});
|
||||
|
|
@ -1,414 +0,0 @@
|
|||
odoo.define('point_of_sale.tests.ComponentRegistry', function(require) {
|
||||
'use strict';
|
||||
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
QUnit.module('unit tests for ComponentRegistry', {
|
||||
before() {},
|
||||
});
|
||||
|
||||
QUnit.test('basic extend', async function(assert) {
|
||||
assert.expect(5);
|
||||
|
||||
class A {
|
||||
constructor() {
|
||||
assert.step('A');
|
||||
}
|
||||
}
|
||||
Registries.Component.add(A);
|
||||
|
||||
let A1 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('A1');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(A, A1);
|
||||
|
||||
Registries.Component.freeze();
|
||||
|
||||
const RegA = Registries.Component.get(A);
|
||||
let a = new RegA();
|
||||
assert.verifySteps(['A', 'A1']);
|
||||
assert.ok(a instanceof RegA);
|
||||
assert.ok(RegA.name === 'A');
|
||||
});
|
||||
|
||||
QUnit.test('addByExtending', async function(assert) {
|
||||
assert.expect(8);
|
||||
|
||||
class A {
|
||||
constructor() {
|
||||
assert.step('A');
|
||||
}
|
||||
}
|
||||
Registries.Component.add(A);
|
||||
|
||||
let B = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('B');
|
||||
}
|
||||
};
|
||||
Registries.Component.addByExtending(B, A);
|
||||
|
||||
let A1 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('A1');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(A, A1);
|
||||
|
||||
let A2 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('A2');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(A, A2);
|
||||
|
||||
Registries.Component.freeze();
|
||||
|
||||
const RegA = Registries.Component.get(A);
|
||||
const RegB = Registries.Component.get(B);
|
||||
let b = new RegB();
|
||||
assert.verifySteps(['A', 'A1', 'A2', 'B']);
|
||||
assert.ok(b instanceof RegA);
|
||||
assert.ok(b instanceof RegB);
|
||||
assert.ok(RegB.name === 'B');
|
||||
});
|
||||
|
||||
QUnit.test('extend the one that is added by extending', async function(assert) {
|
||||
assert.expect(6);
|
||||
|
||||
class A {
|
||||
constructor() {
|
||||
assert.step('A');
|
||||
}
|
||||
}
|
||||
Registries.Component.add(A);
|
||||
|
||||
let B = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('B');
|
||||
}
|
||||
};
|
||||
Registries.Component.addByExtending(B, A);
|
||||
|
||||
let B1 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('B1');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(B, B1);
|
||||
|
||||
let B2 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('B2');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(B, B2);
|
||||
|
||||
let A1 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('A1');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(A, A1);
|
||||
|
||||
Registries.Component.freeze();
|
||||
|
||||
const RegB = Registries.Component.get(B);
|
||||
new RegB();
|
||||
assert.verifySteps(['A', 'A1', 'B', 'B1', 'B2']);
|
||||
});
|
||||
|
||||
QUnit.test('addByExtending based on added by extending', async function(assert) {
|
||||
assert.expect(10);
|
||||
|
||||
class A {
|
||||
constructor() {
|
||||
assert.step('A');
|
||||
}
|
||||
}
|
||||
Registries.Component.add(A);
|
||||
|
||||
let B = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('B');
|
||||
}
|
||||
};
|
||||
Registries.Component.addByExtending(B, A);
|
||||
|
||||
let A1 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('A1');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(A, A1);
|
||||
|
||||
let C = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('C');
|
||||
}
|
||||
};
|
||||
Registries.Component.addByExtending(C, B);
|
||||
|
||||
let B7 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('B7');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(B, B7);
|
||||
|
||||
Registries.Component.freeze();
|
||||
|
||||
const RegA = Registries.Component.get(A);
|
||||
const RegB = Registries.Component.get(B);
|
||||
const RegC = Registries.Component.get(C);
|
||||
let c = new RegC();
|
||||
assert.verifySteps(['A', 'A1', 'B', 'B7', 'C']);
|
||||
assert.ok(c instanceof RegA);
|
||||
assert.ok(c instanceof RegB);
|
||||
assert.ok(c instanceof RegC);
|
||||
assert.ok(RegC.name === 'C');
|
||||
});
|
||||
|
||||
QUnit.test('deeper inheritance', async function(assert) {
|
||||
assert.expect(9);
|
||||
|
||||
class A {
|
||||
constructor() {
|
||||
assert.step('A');
|
||||
}
|
||||
}
|
||||
Registries.Component.add(A);
|
||||
|
||||
let B = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('B');
|
||||
}
|
||||
};
|
||||
Registries.Component.addByExtending(B, A);
|
||||
|
||||
let A1 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('A1');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(A, A1);
|
||||
|
||||
let C = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('C');
|
||||
}
|
||||
};
|
||||
Registries.Component.addByExtending(C, B);
|
||||
|
||||
let B2 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('B2');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(B, B2);
|
||||
|
||||
let B3 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('B3');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(B, B3);
|
||||
|
||||
let A9 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('A9');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(A, A9);
|
||||
|
||||
let E = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('E');
|
||||
}
|
||||
};
|
||||
Registries.Component.addByExtending(E, C);
|
||||
|
||||
Registries.Component.freeze();
|
||||
|
||||
// |A| => A9 -> A1 -> A
|
||||
// |B| => B3 -> B2 -> B -> |A|
|
||||
// |C| => C -> |B|
|
||||
// |E| => E -> |C|
|
||||
|
||||
new (Registries.Component.get(E))();
|
||||
assert.verifySteps(['A', 'A1', 'A9', 'B', 'B2', 'B3', 'C', 'E']);
|
||||
});
|
||||
|
||||
QUnit.test('mixins?', async function(assert) {
|
||||
assert.expect(12);
|
||||
|
||||
class A {
|
||||
constructor() {
|
||||
assert.step('A');
|
||||
}
|
||||
}
|
||||
Registries.Component.add(A);
|
||||
|
||||
let Mixin = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('Mixin');
|
||||
}
|
||||
mixinMethod() {
|
||||
return 'mixinMethod';
|
||||
}
|
||||
get mixinGetter() {
|
||||
return 'mixinGetter';
|
||||
}
|
||||
};
|
||||
|
||||
// use the mixin when declaring B.
|
||||
let B = x =>
|
||||
class extends Mixin(x) {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('B');
|
||||
}
|
||||
};
|
||||
Registries.Component.addByExtending(B, A);
|
||||
|
||||
let A1 = x =>
|
||||
class extends x {
|
||||
constructor() {
|
||||
super();
|
||||
assert.step('A1');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(A, A1);
|
||||
|
||||
Registries.Component.freeze();
|
||||
|
||||
B = Registries.Component.get(B);
|
||||
const b = new B();
|
||||
assert.verifySteps(['A', 'A1', 'Mixin', 'B']);
|
||||
// instance of B should have the mixin properties
|
||||
assert.strictEqual(b.mixinMethod(), 'mixinMethod');
|
||||
assert.strictEqual(b.mixinGetter, 'mixinGetter');
|
||||
|
||||
// instance of A should not have the mixin properties
|
||||
A = Registries.Component.get(A);
|
||||
const a = new A();
|
||||
assert.verifySteps(['A', 'A1']);
|
||||
assert.notOk(a.mixinMethod);
|
||||
assert.notOk(a.mixinGetter);
|
||||
});
|
||||
|
||||
QUnit.test('extending methods', async function(assert) {
|
||||
assert.expect(16);
|
||||
|
||||
class A {
|
||||
foo() {
|
||||
assert.step('A foo');
|
||||
}
|
||||
}
|
||||
Registries.Component.add(A);
|
||||
|
||||
let B = x =>
|
||||
class extends x {
|
||||
bar() {
|
||||
assert.step('B bar');
|
||||
}
|
||||
};
|
||||
Registries.Component.addByExtending(B, A);
|
||||
|
||||
let A1 = x =>
|
||||
class extends x {
|
||||
bar() {
|
||||
assert.step('A1 bar');
|
||||
// should only be for A.
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(A, A1);
|
||||
|
||||
let B1 = x =>
|
||||
class extends x {
|
||||
foo() {
|
||||
super.foo();
|
||||
assert.step('B1 foo');
|
||||
}
|
||||
};
|
||||
Registries.Component.extend(B, B1);
|
||||
|
||||
let C = x =>
|
||||
class extends x {
|
||||
foo() {
|
||||
super.foo();
|
||||
assert.step('C foo');
|
||||
}
|
||||
bar() {
|
||||
super.bar();
|
||||
assert.step('C bar');
|
||||
}
|
||||
};
|
||||
Registries.Component.addByExtending(C, B);
|
||||
|
||||
Registries.Component.freeze();
|
||||
|
||||
A = Registries.Component.get(A);
|
||||
B = Registries.Component.get(B);
|
||||
C = Registries.Component.get(C);
|
||||
const a = new A();
|
||||
const b = new B();
|
||||
const c = new C();
|
||||
|
||||
a.foo();
|
||||
assert.verifySteps(['A foo']);
|
||||
b.foo();
|
||||
assert.verifySteps(['A foo', 'B1 foo']);
|
||||
c.foo();
|
||||
assert.verifySteps(['A foo', 'B1 foo', 'C foo']);
|
||||
|
||||
a.bar();
|
||||
assert.verifySteps(['A1 bar']);
|
||||
b.bar();
|
||||
assert.verifySteps(['B bar']);
|
||||
c.bar();
|
||||
assert.verifySteps(['B bar', 'C bar']);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
odoo.define('point_of_sale.tests.NumberBuffer', function(require) {
|
||||
'use strict';
|
||||
|
||||
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
const testUtils = require('web.test_utils');
|
||||
const { mount } = require('@web/../tests/helpers/utils');
|
||||
const { LegacyComponent } = require("@web/legacy/legacy_component");
|
||||
|
||||
const { useState, xml } = owl;
|
||||
|
||||
QUnit.module('unit tests for NumberBuffer', {
|
||||
before() {},
|
||||
});
|
||||
|
||||
QUnit.test('simple fast inputs with capture in between', async function(assert) {
|
||||
assert.expect(3);
|
||||
const target = testUtils.prepareTarget();
|
||||
const env = makeTestEnvironment();
|
||||
|
||||
class Root extends LegacyComponent {
|
||||
setup() {
|
||||
this.state = useState({ buffer: '' });
|
||||
NumberBuffer.activate();
|
||||
NumberBuffer.use({
|
||||
nonKeyboardInputEvent: 'numpad-click-input',
|
||||
state: this.state,
|
||||
});
|
||||
}
|
||||
resetBuffer() {
|
||||
NumberBuffer.capture();
|
||||
NumberBuffer.reset();
|
||||
}
|
||||
onClickOne() {
|
||||
this.trigger('numpad-click-input', { key: '1' });
|
||||
}
|
||||
onClickTwo() {
|
||||
this.trigger('numpad-click-input', { key: '2' });
|
||||
}
|
||||
}
|
||||
Root.template = xml/* html */ `
|
||||
<div>
|
||||
<p><t t-esc="state.buffer" /></p>
|
||||
<button class="one" t-on-click="onClickOne">1</button>
|
||||
<button class="two" t-on-click="onClickTwo">2</button>
|
||||
<button class="reset" t-on-click="resetBuffer">reset</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await mount(Root, target, { env });
|
||||
|
||||
const oneButton = target.querySelector('button.one');
|
||||
const twoButton = target.querySelector('button.two');
|
||||
const resetButton = target.querySelector('button.reset');
|
||||
const bufferEl = target.querySelector('p');
|
||||
|
||||
testUtils.dom.click(oneButton);
|
||||
testUtils.dom.click(twoButton);
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual(bufferEl.textContent, '12');
|
||||
testUtils.dom.click(resetButton);
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual(bufferEl.textContent, '');
|
||||
testUtils.dom.click(twoButton);
|
||||
testUtils.dom.click(oneButton);
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual(bufferEl.textContent, '21');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
odoo.define('point_of_sale.tests.PosPopupController', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosPopupController = require('point_of_sale.PosPopupController');
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const makeTestEnvironment = require('web.test_env');
|
||||
const testUtils = require('web.test_utils');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { mount } = require('@web/../tests/helpers/utils');
|
||||
|
||||
const { EventBus, useSubEnv, xml } = owl;
|
||||
|
||||
QUnit.module('unit tests for PosPopupController', {
|
||||
before() {
|
||||
Registries.Component.freeze();
|
||||
|
||||
// Note that we are creating new popups here to decouple this test from the pos app.
|
||||
class CustomPopup1 extends AbstractAwaitablePopup {}
|
||||
CustomPopup1.template = xml/* html */`
|
||||
<div class="popup custom-popup-1">
|
||||
<footer>
|
||||
<div class="confirm" t-on-click="confirm">
|
||||
Yes
|
||||
</div>
|
||||
<div class="cancel" t-on-click="cancel">
|
||||
No
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
`;
|
||||
|
||||
class CustomPopup2 extends AbstractAwaitablePopup {}
|
||||
CustomPopup2.template = xml/* html */`
|
||||
<div class="popup custom-popup-2">
|
||||
<footer>
|
||||
<div class="confirm" t-on-click="confirm">
|
||||
Okay
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
`;
|
||||
|
||||
PosPopupController.components = { CustomPopup1, CustomPopup2 };
|
||||
},
|
||||
});
|
||||
|
||||
QUnit.test('allow multiple popups at the same time', async function(assert) {
|
||||
assert.expect(12);
|
||||
|
||||
class Root extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useSubEnv({
|
||||
isDebug: () => false,
|
||||
posbus: new EventBus(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Root.env = makeTestEnvironment();
|
||||
Root.template = xml/* html */ `
|
||||
<div>
|
||||
<PosPopupController />
|
||||
</div>
|
||||
`;
|
||||
|
||||
const root = await mount(Root, testUtils.prepareTarget());
|
||||
|
||||
// Check 1 popup
|
||||
let popup1Promise = root.showPopup('CustomPopup1', {});
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual(root.el.querySelectorAll('.popup').length, 1);
|
||||
testUtils.dom.click(root.el.querySelector('.modal-dialog .custom-popup-1 .confirm'));
|
||||
let result1 = await popup1Promise;
|
||||
assert.strictEqual(result1.confirmed, true);
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual(root.el.querySelectorAll('.popup').length, 0);
|
||||
|
||||
// Check multiple popups
|
||||
popup1Promise = root.showPopup('CustomPopup1', {});
|
||||
await testUtils.nextTick();
|
||||
|
||||
// Check if the first popup is shown.
|
||||
assert.strictEqual(root.el.querySelectorAll('.popup').length, 1);
|
||||
|
||||
let popup2Promise = root.showPopup('CustomPopup2', {});
|
||||
await testUtils.nextTick();
|
||||
|
||||
// Check for the second popup.
|
||||
assert.strictEqual(root.el.querySelectorAll('.popup').length, 2);
|
||||
|
||||
// popup 1 should be hidden
|
||||
assert.strictEqual(root.el.querySelectorAll('.modal-dialog.oe_hidden').length, 1);
|
||||
|
||||
// click confirm on popup 2
|
||||
testUtils.dom.click(root.el.querySelector('.modal-dialog .custom-popup-2 .confirm'));
|
||||
await testUtils.nextTick();
|
||||
|
||||
// after confirming on popup 2, only 1 should remain.
|
||||
assert.strictEqual(root.el.querySelectorAll('.popup').length, 1);
|
||||
assert.strictEqual(root.el.querySelectorAll('.modal-dialog .custom-popup-2').length, 0);
|
||||
|
||||
// popup 1 should not be hidden
|
||||
const CustomPopup1 = root.el.querySelector('.modal-dialog')
|
||||
assert.strictEqual(![...CustomPopup1.classList].includes('oe_hidden'), true);
|
||||
testUtils.dom.click(root.el.querySelector('.modal-dialog .custom-popup-1 .cancel'));
|
||||
await testUtils.nextTick();
|
||||
|
||||
// after cancelling popup 1, no popup should remain.
|
||||
assert.strictEqual(root.el.querySelectorAll('.popup').length, 0);
|
||||
|
||||
result1 = await popup1Promise;
|
||||
let result2 = await popup2Promise;
|
||||
assert.strictEqual(result1.confirmed, false); // false because it's cancelled.
|
||||
assert.strictEqual(result2.confirmed, true); // true because it's confirmed.
|
||||
});
|
||||
|
||||
QUnit.test('pressing cancel/confirm key should only close the top popup', async function(assert) {
|
||||
assert.expect(6);
|
||||
|
||||
class Root extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useSubEnv({
|
||||
isDebug: () => false,
|
||||
posbus: new EventBus(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Root.env = makeTestEnvironment();
|
||||
Root.template = xml/* html */ `
|
||||
<div>
|
||||
<PosPopupController />
|
||||
</div>
|
||||
`;
|
||||
|
||||
const root = await mount(Root, testUtils.prepareTarget());
|
||||
|
||||
let popup1Promise = root.showPopup('CustomPopup1', { confirmKey: 'Enter', cancelKey: 'Escape' });
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual(root.el.querySelectorAll('.popup').length, 1);
|
||||
|
||||
let popup2Promise = root.showPopup('CustomPopup2', { confirmKey: 'Enter', cancelKey: 'Escape' });
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual(root.el.querySelectorAll('.popup').length, 2);
|
||||
|
||||
// Pressing 'Escape' should cancel the top popup which is the CustomPopup2.
|
||||
testUtils.dom.triggerEvent(window, 'keyup', { key: 'Escape' });
|
||||
await testUtils.nextTick();
|
||||
|
||||
// Therefore, the popup2Promise has now resolved with `confirmed` value = false.
|
||||
const result2 = await popup2Promise;
|
||||
assert.strictEqual(result2.confirmed, false);
|
||||
|
||||
assert.strictEqual(root.el.querySelectorAll('.popup').length, 1);
|
||||
|
||||
testUtils.dom.triggerEvent(window, 'keyup', { key: 'Enter' });
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.strictEqual(root.el.querySelectorAll('.popup').length, 0);
|
||||
|
||||
const result1 = await popup1Promise;
|
||||
assert.strictEqual(result1.confirmed, true);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { test, expect } from "@odoo/hoot";
|
||||
import { getFilledOrder, setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("Check GAP", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const device = store.device;
|
||||
let orderStack = [];
|
||||
|
||||
// Ensure there is no order at the beginning
|
||||
await store.deleteOrders(store.models["pos.order"].getAll());
|
||||
|
||||
const createNewOrdersAndCheck = async (nbr) => {
|
||||
for (let i = 0; i < nbr; i++) {
|
||||
const order = await getFilledOrder(store);
|
||||
orderStack.push(order);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteOrdersAndCheck = async () => {
|
||||
const numbers = orderStack.map((order) => parseInt(order.pos_reference.split("-")[2]));
|
||||
await store.deleteOrders(orderStack);
|
||||
orderStack = [];
|
||||
expect(device.data.unsynced_number_stack).not.toBeEmpty();
|
||||
expect(device.data.unsynced_number_stack).toMatch(numbers);
|
||||
};
|
||||
|
||||
// Create 15 orders, check that the next number is incremented correctly
|
||||
await createNewOrdersAndCheck(15);
|
||||
expect(device.data.next_number).toBe(16);
|
||||
expect(device.data.unsynced_number_stack).toBeEmpty();
|
||||
|
||||
// Delete all of them, check that the unsynced number stack is filled
|
||||
await deleteOrdersAndCheck();
|
||||
|
||||
// Create 15 more orders, the number should not be incremented, we reuse the unsynced numbers
|
||||
await createNewOrdersAndCheck(15);
|
||||
|
||||
// Stack is empty numbers are used
|
||||
expect(device.data.unsynced_number_stack).toBeEmpty();
|
||||
expect(device.data.next_number).toBe(16);
|
||||
await deleteOrdersAndCheck();
|
||||
expect(device.data.next_number).toBe(16);
|
||||
|
||||
// Create 15 more orders, the number should be incremented
|
||||
await createNewOrdersAndCheck(15);
|
||||
expect(device.data.next_number).toBe(16);
|
||||
|
||||
// Sync orders and cancel them
|
||||
const orders = await store.syncAllOrders();
|
||||
await store.deleteOrders(orders);
|
||||
|
||||
// Create 15 more orders, the number should be incremented again
|
||||
await createNewOrdersAndCheck(15);
|
||||
expect(device.data.next_number).toBe(31);
|
||||
});
|
||||
|
||||
test("Device identifier is set", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const device = store.device;
|
||||
expect(device.identifier).not.toBeEmpty();
|
||||
});
|
||||
|
|
@ -0,0 +1,338 @@
|
|||
import { afterEach, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-dom";
|
||||
import { Component, onWillRender, reactive, useState, xml } from "@odoo/owl";
|
||||
import {
|
||||
mountWithCleanup,
|
||||
allowTranslations,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
|
||||
import {
|
||||
WithLazyGetterTrap,
|
||||
clearGettersCache,
|
||||
createLazyGetter,
|
||||
} from "@point_of_sale/lazy_getter";
|
||||
import { zip } from "@web/core/utils/arrays";
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
function unorderedStep(value) {
|
||||
unorderedSteps.push(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes multiple assertions:
|
||||
* - Are all items in `vals` in steps?
|
||||
* - Are the items in `steps` ordered according to each item in `stepOrders`?
|
||||
* Then it clears the `steps`.
|
||||
* @param {string[]} expectedSteps
|
||||
* @param {Iterable<string[]>} [stepOrders=[]]
|
||||
*/
|
||||
function verifyUnorderedSteps(expectedSteps, stepOrders = []) {
|
||||
expect([...unorderedSteps].sort()).toEqual([...expectedSteps].sort());
|
||||
for (const stepOrder of stepOrders) {
|
||||
expect(
|
||||
zip(stepOrder.slice(0, -1), stepOrder.slice(1)).reduce(
|
||||
(acc, [a, b]) => acc && unorderedSteps.indexOf(a) < unorderedSteps.indexOf(b),
|
||||
true
|
||||
)
|
||||
).toBe(true);
|
||||
}
|
||||
unorderedSteps = [];
|
||||
}
|
||||
|
||||
let unorderedSteps = [];
|
||||
|
||||
allowTranslations();
|
||||
afterEach(clearGettersCache);
|
||||
|
||||
class AppStore extends WithLazyGetterTrap {
|
||||
constructor() {
|
||||
super({ traps: {} });
|
||||
this.a = 0;
|
||||
this.b = 0;
|
||||
this.c = 0;
|
||||
this.d = 0;
|
||||
}
|
||||
get ab() {
|
||||
return this.a + this.b;
|
||||
}
|
||||
get abc() {
|
||||
let result = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
result += this.ab;
|
||||
}
|
||||
return result + this.c;
|
||||
}
|
||||
get bc() {
|
||||
return this.b + this.c;
|
||||
}
|
||||
get cd() {
|
||||
return this.c + this.d;
|
||||
}
|
||||
get x() {
|
||||
return this.abc + this.bc;
|
||||
}
|
||||
get y() {
|
||||
return this.cd + this.x;
|
||||
}
|
||||
}
|
||||
|
||||
class WithStore extends Component {
|
||||
static props = {};
|
||||
static template = xml`
|
||||
<span t-att-class="property">
|
||||
<t t-esc="constructor.name" />: <t t-esc="this.store[property]" />
|
||||
</span>
|
||||
`;
|
||||
|
||||
property = "";
|
||||
|
||||
setup() {
|
||||
this.store = useState(this.env.store);
|
||||
onWillRender(() => this.onWillRender());
|
||||
}
|
||||
|
||||
onWillRender() {}
|
||||
}
|
||||
|
||||
class A extends WithStore {
|
||||
property = "a";
|
||||
}
|
||||
|
||||
class B extends WithStore {
|
||||
property = "b";
|
||||
}
|
||||
|
||||
class C extends WithStore {
|
||||
property = "c";
|
||||
}
|
||||
|
||||
class D extends WithStore {
|
||||
property = "d";
|
||||
}
|
||||
|
||||
class AB extends WithStore {
|
||||
property = "ab";
|
||||
}
|
||||
|
||||
class ABC extends WithStore {
|
||||
property = "abc";
|
||||
}
|
||||
|
||||
class BC extends WithStore {
|
||||
property = "bc";
|
||||
}
|
||||
|
||||
class CD extends WithStore {
|
||||
property = "cd";
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static components = { A, B, C, D, AB, ABC, BC, CD };
|
||||
static props = {};
|
||||
static template = xml`
|
||||
<t t-foreach="constructor.components" t-as="key" t-key="key">
|
||||
<t t-component="constructor.components[key]" />
|
||||
</t>
|
||||
`;
|
||||
}
|
||||
|
||||
test("each getter should only be called once and only when needed", async () => {
|
||||
patchWithCleanup(AppStore.prototype, {
|
||||
get ab() {
|
||||
unorderedStep("ab");
|
||||
return super.ab;
|
||||
},
|
||||
get abc() {
|
||||
unorderedStep("abc");
|
||||
return super.abc;
|
||||
},
|
||||
get bc() {
|
||||
unorderedStep("bc");
|
||||
return super.bc;
|
||||
},
|
||||
get cd() {
|
||||
unorderedStep("cd");
|
||||
return super.cd;
|
||||
},
|
||||
});
|
||||
|
||||
const store = reactive(new AppStore());
|
||||
await mountWithCleanup(Root, {
|
||||
env: { store },
|
||||
noMainContainer: true,
|
||||
});
|
||||
|
||||
verifyUnorderedSteps(["ab", "abc", "bc", "cd"]);
|
||||
|
||||
store.a = 1;
|
||||
|
||||
// Getters should only be called after an interface re-render
|
||||
verifyUnorderedSteps([]);
|
||||
await animationFrame();
|
||||
verifyUnorderedSteps(["ab", "abc"]);
|
||||
|
||||
store.b = 1;
|
||||
|
||||
verifyUnorderedSteps([]);
|
||||
await animationFrame();
|
||||
verifyUnorderedSteps(["bc", "ab", "abc"]);
|
||||
|
||||
store.c = 1;
|
||||
|
||||
verifyUnorderedSteps([]);
|
||||
await animationFrame();
|
||||
verifyUnorderedSteps(["cd", "bc", "abc"]);
|
||||
|
||||
store.d = 1;
|
||||
|
||||
verifyUnorderedSteps([]);
|
||||
await animationFrame();
|
||||
verifyUnorderedSteps(["cd"]);
|
||||
});
|
||||
|
||||
test("only dependent components rerender", async () => {
|
||||
patchWithCleanup(WithStore.prototype, {
|
||||
onWillRender() {
|
||||
unorderedStep(this.property);
|
||||
},
|
||||
});
|
||||
|
||||
const store = reactive(new AppStore());
|
||||
await mountWithCleanup(Root, {
|
||||
env: { store },
|
||||
noMainContainer: true,
|
||||
});
|
||||
|
||||
verifyUnorderedSteps(["a", "b", "c", "d", "ab", "abc", "bc", "cd"]);
|
||||
|
||||
store.a = 1;
|
||||
await animationFrame();
|
||||
|
||||
verifyUnorderedSteps(["a", "ab", "abc"]);
|
||||
|
||||
store.b = 1;
|
||||
await animationFrame();
|
||||
|
||||
verifyUnorderedSteps(["b", "ab", "abc", "bc"]);
|
||||
|
||||
store.c = 1;
|
||||
await animationFrame();
|
||||
|
||||
verifyUnorderedSteps(["c", "abc", "bc", "cd"]);
|
||||
|
||||
store.d = 1;
|
||||
await animationFrame();
|
||||
|
||||
verifyUnorderedSteps(["d", "cd"]);
|
||||
});
|
||||
|
||||
test("only dependent getters are called and in correct order", () => {
|
||||
patchWithCleanup(AppStore.prototype, {
|
||||
get ab() {
|
||||
const result = super.ab;
|
||||
unorderedStep("ab");
|
||||
return result;
|
||||
},
|
||||
get abc() {
|
||||
const result = super.abc;
|
||||
unorderedStep("abc");
|
||||
return result;
|
||||
},
|
||||
get bc() {
|
||||
const result = super.bc;
|
||||
unorderedStep("bc");
|
||||
return result;
|
||||
},
|
||||
get cd() {
|
||||
const result = super.cd;
|
||||
unorderedStep("cd");
|
||||
return result;
|
||||
},
|
||||
get x() {
|
||||
const result = super.x;
|
||||
unorderedStep("x");
|
||||
return result;
|
||||
},
|
||||
get y() {
|
||||
const result = super.y;
|
||||
unorderedStep("y");
|
||||
return result;
|
||||
},
|
||||
});
|
||||
const store = reactive(new AppStore());
|
||||
|
||||
expect(store.y).toBe(0);
|
||||
verifyUnorderedSteps(["ab", "bc", "cd", "abc", "x", "y"], [["ab", "abc", "x", "y"]]);
|
||||
|
||||
store.a = 1;
|
||||
|
||||
expect(store.y).toBe(10);
|
||||
verifyUnorderedSteps(["ab", "abc", "x", "y"], [["ab", "abc", "x", "y"]]);
|
||||
|
||||
store.b = 1;
|
||||
expect(store.y).toBe(21);
|
||||
|
||||
verifyUnorderedSteps(
|
||||
["ab", "bc", "abc", "x", "y"],
|
||||
[
|
||||
["ab", "abc", "x", "y"],
|
||||
["bc", "x", "y"],
|
||||
]
|
||||
);
|
||||
|
||||
store.c = 1;
|
||||
expect(store.y).toBe(24);
|
||||
|
||||
verifyUnorderedSteps(
|
||||
["abc", "bc", "cd", "x", "y"],
|
||||
[
|
||||
["abc", "x", "y"],
|
||||
["bc", "x", "y"],
|
||||
["cd", "y"],
|
||||
]
|
||||
);
|
||||
|
||||
store.d = 1;
|
||||
expect(store.y).toBe(25);
|
||||
|
||||
verifyUnorderedSteps(["cd", "y"], [["cd", "y"]]);
|
||||
});
|
||||
|
||||
test("dynamically creates a lazy getter", () => {
|
||||
class DemoClass extends WithLazyGetterTrap {
|
||||
constructor(params = {}) {
|
||||
super(params);
|
||||
}
|
||||
}
|
||||
|
||||
const reactiveObj = reactive(new DemoClass());
|
||||
reactiveObj.name = "demo";
|
||||
|
||||
let computeCallCount = 0;
|
||||
function computeValue() {
|
||||
computeCallCount++;
|
||||
return "Hello " + this.name;
|
||||
}
|
||||
|
||||
createLazyGetter(reactiveObj, "hello", computeValue);
|
||||
|
||||
expect(reactiveObj.hello).toBe("Hello demo");
|
||||
expect(computeCallCount).toBe(1);
|
||||
|
||||
// On the second call, the computed method is not executed again.
|
||||
expect(reactiveObj.hello).toBe("Hello demo");
|
||||
expect(computeCallCount).toBe(1);
|
||||
|
||||
// Modifying the value will invalidate the computed value
|
||||
expect(computeCallCount).toBe(1);
|
||||
reactiveObj.name = "World";
|
||||
expect(reactiveObj.hello).toBe("Hello World");
|
||||
expect(computeCallCount).toBe(2);
|
||||
|
||||
reactiveObj.notRelatedValue = 1;
|
||||
expect(reactiveObj.hello).toBe("Hello World");
|
||||
expect(computeCallCount).toBe(2);
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { getLNATargetAddressSpace } from "@point_of_sale/app/utils/init_lna";
|
||||
|
||||
test("targetAddressSpace local", () => {
|
||||
expect(getLNATargetAddressSpace("http://192.168.1.1")).toBe("local");
|
||||
expect(getLNATargetAddressSpace("http://192.168.1.1:8008")).toBe("local");
|
||||
expect(getLNATargetAddressSpace("http://192.168.1.1:8080/demo")).toBe("local");
|
||||
|
||||
expect(getLNATargetAddressSpace("invalidurl")).toBe("local");
|
||||
});
|
||||
|
||||
test("targetAddressSpace loopback", () => {
|
||||
expect(getLNATargetAddressSpace("http://localhost")).toBe("loopback");
|
||||
expect(getLNATargetAddressSpace("http://localhost:1234/demo")).toBe("loopback");
|
||||
expect(getLNATargetAddressSpace("http://localhost/demo")).toBe("loopback");
|
||||
|
||||
expect(getLNATargetAddressSpace("http://127.0.0.1")).toBe("loopback");
|
||||
expect(getLNATargetAddressSpace("http://127.0.0.1:1234/demo")).toBe("loopback");
|
||||
expect(getLNATargetAddressSpace("http://127.0.0.1/demo")).toBe("loopback");
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { EQ, AbstractNumbers } from "@point_of_sale/app/utils/numbers";
|
||||
|
||||
class CustomNumbers extends AbstractNumbers {
|
||||
constructor() {
|
||||
super({});
|
||||
}
|
||||
get precision() {
|
||||
return 0.05;
|
||||
}
|
||||
get method() {
|
||||
return "UP";
|
||||
}
|
||||
}
|
||||
|
||||
const numbers = new CustomNumbers();
|
||||
|
||||
test("inputs are rounded before comparison", () => {
|
||||
expect(numbers.comp(1.24, 1.21)).toBe(EQ);
|
||||
expect(numbers.isZero(1.24 - 1.21)).toBe(false);
|
||||
});
|
||||
|
||||
test("rounding", () => {
|
||||
expect(numbers.round(1.28)).toBe(1.3);
|
||||
expect(numbers.round(-1.28)).toBe(-1.3);
|
||||
expect(numbers.asymmetricRound(1.28)).toBe(1.3);
|
||||
expect(numbers.asymmetricRound(-1.28)).toBe(-1.25);
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import OrderPaymentValidation from "@point_of_sale/app/utils/order_payment_validation";
|
||||
import { getFilledOrder, setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("validateOrder", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const fastPaymentMethod = order.config.fast_payment_method_ids[0];
|
||||
const validation = new OrderPaymentValidation({
|
||||
pos: store,
|
||||
orderUuid: store.getOrder().uuid,
|
||||
fastPaymentMethod: fastPaymentMethod,
|
||||
});
|
||||
await validation.validateOrder(false);
|
||||
expect(order.payment_ids[0].payment_method_id).toEqual(fastPaymentMethod);
|
||||
expect(order.state).toBe("paid");
|
||||
expect(order.amount_paid).toBe(17.85);
|
||||
});
|
||||
test("isOrderValid", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = store.addNewOrder();
|
||||
order.setToInvoice(true);
|
||||
const validation = new OrderPaymentValidation({
|
||||
pos: store,
|
||||
orderUuid: store.getOrder().uuid,
|
||||
});
|
||||
const isOrderValid = await validation.isOrderValid(false);
|
||||
expect(order.lines).toHaveLength(0);
|
||||
expect(isOrderValid).toBe(false); // The order cannot be invoiced if the order line count is zero.
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { expect, test } from "@odoo/hoot";
|
||||
import { getFilledOrder, setupPosEnv } from "../utils";
|
||||
import { definePosModels } from "../data/generate_model_definitions";
|
||||
|
||||
definePosModels();
|
||||
|
||||
test("Related models must keep local records", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
const product = store.models["product.template"].get(8);
|
||||
expect(order.isSynced).toBe(false);
|
||||
expect(order.lines.every((l) => l.isSynced === true)).toBe(false);
|
||||
await store.syncAllOrders();
|
||||
expect(order.isSynced).toBe(true);
|
||||
expect(order.lines.every((l) => l.isSynced === true)).toBe(true);
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: product,
|
||||
qty: 1,
|
||||
},
|
||||
order
|
||||
);
|
||||
expect(order.lines.every((l) => l.isSynced === true)).toBe(false);
|
||||
|
||||
// Download the same order from server, the local unsynced line must be kept
|
||||
await store.data.loadServerOrders([["id", "=", order.id]]);
|
||||
expect(order.lines.every((l) => l.isSynced === true)).toBe(false);
|
||||
});
|
||||
|
||||
test("Check behavior when deleting records", async () => {
|
||||
const store = await setupPosEnv();
|
||||
const order = await getFilledOrder(store);
|
||||
expect(order.isSynced).toBe(false);
|
||||
expect(order.lines.every((l) => l.isSynced === true)).toBe(false);
|
||||
await store.syncAllOrders();
|
||||
expect(order.isSynced).toBe(true);
|
||||
expect(order.lines.every((l) => l.isSynced === true)).toBe(true);
|
||||
order.removeOrderline(order.lines[0]);
|
||||
expect(order.lines).toHaveLength(1);
|
||||
|
||||
// At this point if we download the same order from server,
|
||||
// we must not lose the local deletion
|
||||
await store.data.loadServerOrders([["id", "=", order.id]]);
|
||||
expect(order.lines).toHaveLength(2);
|
||||
|
||||
// But if we sync before downloading, the deletion must be kept
|
||||
order.removeOrderline(order.lines[0]);
|
||||
expect(order.lines).toHaveLength(1);
|
||||
await store.syncAllOrders({ orders: [order] });
|
||||
await store.data.loadServerOrders([["id", "=", order.id]]);
|
||||
expect(order.lines).toHaveLength(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import { uuidv4 } from "@point_of_sale/utils";
|
||||
import {
|
||||
getService,
|
||||
makeDialogMockEnv,
|
||||
mountWithCleanup,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/web_test_helpers";
|
||||
import { animationFrame, tick, waitFor, waitUntil } from "@odoo/hoot-dom";
|
||||
import { Deferred } from "@odoo/hoot-mock";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { onMounted } from "@odoo/owl";
|
||||
import { expect } from "@odoo/hoot";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
export const setupPosEnv = async () => {
|
||||
// Do not change these variables, they are in accordance with the demo data
|
||||
odoo.pos_session_id = 1;
|
||||
odoo.pos_config_id = 1;
|
||||
odoo.from_backend = 0;
|
||||
odoo.access_token = uuidv4(); // Avoid indexedDB conflicts
|
||||
odoo.info = {
|
||||
db: `pos-${uuidv4()}`, // Avoid indexedDB conflicts
|
||||
isEnterprise: true,
|
||||
};
|
||||
|
||||
await makeDialogMockEnv();
|
||||
const store = getService("pos");
|
||||
store.setCashier(store.user);
|
||||
patchWithCleanup(user, {
|
||||
// Needed for the allowProductCreation method
|
||||
checkAccessRight: (model, operation) =>
|
||||
operation === "create" && model === "product.product",
|
||||
});
|
||||
return store;
|
||||
};
|
||||
|
||||
export const getFilledOrder = async (store, data = {}) => {
|
||||
const order = store.addNewOrder(data);
|
||||
const product1 = store.models["product.template"].get(5);
|
||||
const product2 = store.models["product.template"].get(6);
|
||||
const date = DateTime.now();
|
||||
order.write_date = date;
|
||||
order.create_date = date;
|
||||
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: product1,
|
||||
qty: 3,
|
||||
write_date: date,
|
||||
create_date: date,
|
||||
},
|
||||
order
|
||||
);
|
||||
await store.addLineToOrder(
|
||||
{
|
||||
product_tmpl_id: product2,
|
||||
qty: 2,
|
||||
write_date: date,
|
||||
create_date: date,
|
||||
},
|
||||
order
|
||||
);
|
||||
store.addPendingOrder([order.id]);
|
||||
return order;
|
||||
};
|
||||
|
||||
export async function waitUntilOrdersSynced(store, options) {
|
||||
await waitUntil(() => !store.syncingOrders.size, options);
|
||||
await tick();
|
||||
}
|
||||
|
||||
export const mountPosDialog = async (component, props) => {
|
||||
patchDialogComponent(component);
|
||||
const dialog = getService("dialog");
|
||||
const root = await mountWithCleanup(MainComponentsContainer);
|
||||
const deferred = new Deferred();
|
||||
|
||||
const getComponentInstance = (root) => {
|
||||
const flattenedChildren = (comp, acc = {}) => {
|
||||
const array = Object.values(comp.children);
|
||||
for (const child of array) {
|
||||
acc[child.name] = child;
|
||||
flattenedChildren(child, acc);
|
||||
}
|
||||
return acc;
|
||||
};
|
||||
const components = flattenedChildren(root);
|
||||
return components[component.name];
|
||||
};
|
||||
|
||||
dialog.add(component, {
|
||||
...props,
|
||||
onMounted() {
|
||||
const dialogComponent = getComponentInstance(root.__owl__);
|
||||
deferred.resolve(dialogComponent.component);
|
||||
},
|
||||
});
|
||||
return await deferred;
|
||||
};
|
||||
|
||||
export const patchDialogComponent = (component) => {
|
||||
component.props = [...component.props, "onMounted?"];
|
||||
patch(component.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
onMounted(() => {
|
||||
this.props.onMounted && this.props.onMounted();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const expectFormattedPrice = (value, expected) => {
|
||||
expect(value).toBe(expected.replaceAll(" ", "\u00a0"));
|
||||
};
|
||||
|
||||
export const dialogActions = async (action, steps = []) => {
|
||||
// Launch the action in a promise to be able to await the end of the steps
|
||||
await mountWithCleanup(MainComponentsContainer);
|
||||
const promise = new Promise((resolve) => {
|
||||
const call = async (fn) => {
|
||||
const result = await fn();
|
||||
resolve(result);
|
||||
};
|
||||
call(action);
|
||||
});
|
||||
|
||||
// Wait for the dialog to be mounted
|
||||
await waitFor(".o_dialog");
|
||||
|
||||
// Execute the steps one by one
|
||||
for (const step of steps) {
|
||||
await step();
|
||||
await animationFrame();
|
||||
}
|
||||
|
||||
// Return the result of the action
|
||||
return await promise;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue