19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:29:53 +01:00
parent 6e54c1af6c
commit 3ca647e428
1087 changed files with 132065 additions and 108499 deletions

View file

@ -0,0 +1,48 @@
import { test, expect } from "@odoo/hoot";
import { mockDate } from "@odoo/hoot-mock";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { getFilledOrder, setupPosEnv } from "@point_of_sale/../tests/unit/utils";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { ManageGiftCardPopup } from "@pos_loyalty/app/components/popups/manage_giftcard_popup/manage_giftcard_popup";
definePosModels();
test("addBalance", async () => {
const store = await setupPosEnv();
// Freeze current date so luxon.DateTime.now() is fixed
mockDate("2025-01-01");
let payloadResult = null;
const order = await getFilledOrder(store);
const popup = await mountWithCleanup(ManageGiftCardPopup, {
props: {
line: order.lines[0],
title: "Sell/Manage physical gift card",
getPayload: (code, amount, expDate) => {
payloadResult = { code, amount, expDate };
},
close: () => {},
},
});
popup.state.inputValue = "";
popup.state.amountValue = "";
const valid = popup.validateCode();
expect(valid).toBe(false);
expect(popup.state.error).toBe(true);
popup.state.inputValue = "101";
popup.state.amountValue = "100";
popup.state.error = false;
popup.state.amountError = false;
await popup.addBalance();
expect(payloadResult.code).toBe("101");
expect(payloadResult.amount).toBe(100);
// expiration is +1 year
expect(payloadResult.expDate).toBe("2026-01-01");
});

View file

@ -0,0 +1,52 @@
import { test, expect } from "@odoo/hoot";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { setupPosEnv } from "@point_of_sale/../tests/unit/utils";
import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { addProductLineToOrder } from "@pos_loyalty/../tests/unit/utils";
definePosModels();
test("_applyReward", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
// Get card #1 - belongs to a loyalty-type program
const card = models["loyalty.card"].get(1);
// Get reward #2 - belongs to the same loyalty program, type = discount reward
const reward = models["loyalty.reward"].get(2);
await addProductLineToOrder(store, order);
// Total quantity in the order
const potentialQty = order.getOrderlines().reduce((acc, line) => acc + line.qty, 0);
const component = await mountWithCleanup(ControlButtons, {});
const result = await component._applyReward(reward, card.id, potentialQty);
expect(result).toBe(true);
});
test("getPotentialRewards", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
// Get loyalty program #1 - type = loyalty
const loyaltyProgram = models["loyalty.program"].get(1);
// Get card #1 - linked to loyalty program #1
const card = models["loyalty.card"].get(1);
await addProductLineToOrder(store, order);
order._code_activated_coupon_ids = [card];
const component = await mountWithCleanup(ControlButtons, {});
const rewards = component.getPotentialRewards();
const reward = rewards[0].reward;
expect(reward).toEqual(models["loyalty.reward"].get(1));
expect(reward.program_id).toEqual(loyaltyProgram);
});

View file

@ -0,0 +1,43 @@
import { test, expect } 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 } from "@point_of_sale/../tests/unit/utils";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { addProductLineToOrder } from "@pos_loyalty/../tests/unit/utils";
definePosModels();
test("_updateGiftCardOrderline", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
const product = models["product.product"].get(1);
// Program #3 - loyalty program for gift cards
const program = models["loyalty.program"].get(3);
// Card #3 - gift card which program type is gift_card
const card = models["loyalty.card"].get(3);
await addProductLineToOrder(store, order);
const points = product.lst_price;
order.uiState.couponPointChanges[card.id] = {
coupon_id: card.id,
program_id: program.id,
product_id: product.id,
points: points,
manual: false,
};
const component = await mountWithCleanup(OrderSummary, {});
await component._updateGiftCardOrderline("ABC123", points);
const updatedLine = order.getSelectedOrderline();
expect(updatedLine.gift_code).toBe("ABC123");
expect(updatedLine.product_id.id).toBe(product.id);
expect(updatedLine.getQuantity()).toBe(1);
expect(order.uiState.couponPointChanges[card.id]).toBe(undefined);
});

View file

@ -0,0 +1,49 @@
import { test, expect } from "@odoo/hoot";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { setupPosEnv } from "@point_of_sale/../tests/unit/utils";
import { PartnerLine } from "@point_of_sale/app/screens/partner_list/partner_line/partner_line";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
definePosModels();
test("_getLoyaltyPointsRepr", async () => {
const store = await setupPosEnv();
const models = store.models;
const partner = models["res.partner"].get(1);
// Get first 3 loyalty cards and map them with program
const loyaltyCards = models["loyalty.card"]
.getAll()
.slice(0, 3)
.map((element) => ({
id: element.id,
points: element.points,
partner_id: partner.id,
program_id: models["loyalty.program"].get(element.id),
}));
const component = await mountWithCleanup(PartnerLine, {
props: {
partner,
close: () => {},
isSelected: false,
isBalanceDisplayed: true,
onClickEdit: () => {},
onClickUnselect: () => {},
onClickPartner: () => {},
onClickOrders: () => {},
},
env: {
...store.env,
utils: {
formatCurrency: (val) => `$${val.toFixed(2)}`,
},
},
});
const results = loyaltyCards.map((card) => component._getLoyaltyPointsRepr(card));
expect(results[0]).toBe("10.00 Points");
expect(results[1]).toBe("E-Wallet Program: $25.00");
expect(results[2]).toMatch("15.00 Gift Card Points");
});

View file

@ -0,0 +1,52 @@
import { patch } from "@web/core/utils/patch";
import { hootPosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { models } from "@web/../tests/web_test_helpers";
const { DateTime } = luxon;
export class LoyaltyCard extends models.ServerModel {
_name = "loyalty.card";
_load_pos_data_fields() {
return ["partner_id", "code", "points", "program_id", "expiration_date", "write_date"];
}
_records = [
{
id: 1,
code: "CARD001",
points: 10,
partner_id: 1,
program_id: 1,
expiration_date: DateTime.now().plus({ days: 1 }).toISODate(),
write_date: DateTime.now().minus({ days: 1 }).toFormat("yyyy-MM-dd HH:mm:ss"),
},
{
id: 2,
code: "CARD002",
points: 25,
partner_id: 1,
program_id: 2,
expiration_date: DateTime.now().minus({ days: 1 }).toISODate(),
write_date: DateTime.now().minus({ days: 2 }).toFormat("yyyy-MM-dd HH:mm:ss"),
},
{
id: 3,
code: "CARD003",
points: 15,
partner_id: 3,
program_id: 3,
write_date: DateTime.now().toFormat("yyyy-MM-dd HH:mm:ss"),
},
{
id: 4,
code: "CARD004",
points: 3,
partner_id: 1,
program_id: 7,
write_date: DateTime.now().minus({ days: 2 }).toFormat("yyyy-MM-dd HH:mm:ss"),
},
];
}
patch(hootPosModels, [...hootPosModels, LoyaltyCard]);

View file

@ -0,0 +1,178 @@
import { patch } from "@web/core/utils/patch";
import { hootPosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { models } from "@web/../tests/web_test_helpers";
const { DateTime } = luxon;
export class LoyaltyProgram extends models.ServerModel {
_name = "loyalty.program";
_load_pos_data_fields() {
return [
"name",
"trigger",
"applies_on",
"program_type",
"pricelist_ids",
"date_from",
"date_to",
"limit_usage",
"max_usage",
"is_nominative",
"portal_visible",
"portal_point_name",
"trigger_product_ids",
"rule_ids",
"reward_ids",
];
}
_records = [
{
id: 1,
name: "Loyalty Program",
trigger: "auto",
applies_on: "both",
program_type: "loyalty",
pricelist_ids: [1],
date_from: false,
date_to: false,
limit_usage: false,
max_usage: 0,
is_nominative: false,
portal_visible: true,
portal_point_name: "Points",
trigger_product_ids: [],
rule_ids: [1],
reward_ids: [],
},
{
id: 2,
name: "E-Wallet Program",
trigger: "auto",
applies_on: "future",
program_type: "ewallet",
pricelist_ids: [],
date_from: false,
date_to: false,
limit_usage: false,
max_usage: 0,
is_nominative: false,
portal_visible: true,
portal_point_name: "E-Wallet Points",
trigger_product_ids: [],
rule_ids: [],
reward_ids: [],
},
{
id: 3,
name: "Gift Card Program",
trigger: "auto",
applies_on: "future",
program_type: "gift_card",
pricelist_ids: [],
date_from: false,
date_to: false,
limit_usage: false,
max_usage: 0,
is_nominative: false,
portal_visible: true,
portal_point_name: "Gift Card Points",
trigger_product_ids: [],
rule_ids: [1],
reward_ids: [],
},
{
id: 4,
name: "E-Wallet Program 2",
trigger: "auto",
applies_on: "future",
program_type: "ewallet",
pricelist_ids: [],
date_from: false,
date_to: false,
limit_usage: false,
max_usage: 0,
is_nominative: false,
portal_visible: true,
portal_point_name: "E-Wallet Points 2",
trigger_product_ids: [],
rule_ids: [],
reward_ids: [],
},
{
id: 5,
name: "Nominative Gift Card Program",
trigger: "auto",
applies_on: "future",
program_type: "gift_card",
pricelist_ids: [],
date_from: false,
date_to: false,
limit_usage: false,
max_usage: 0,
is_nominative: true,
portal_visible: true,
portal_point_name: "Nominative Gift Card Points",
trigger_product_ids: [],
rule_ids: [1],
reward_ids: [],
},
{
id: 6,
name: "E-Wallet Program 2",
trigger: "auto",
applies_on: "future",
program_type: "ewallet",
pricelist_ids: [],
date_from: DateTime.now().minus({ days: 10 }).toISODate(),
date_to: DateTime.now().minus({ days: 1 }).toISODate(),
limit_usage: false,
max_usage: 0,
is_nominative: false,
portal_visible: true,
portal_point_name: "E-Wallet Points",
trigger_product_ids: [],
rule_ids: [],
reward_ids: [],
},
{
id: 7,
name: "Loyalty Program Future",
trigger: "auto",
applies_on: "future",
program_type: "loyalty",
pricelist_ids: [1],
date_from: false,
date_to: false,
limit_usage: false,
max_usage: 0,
is_nominative: true,
portal_visible: true,
portal_point_name: "Points",
trigger_product_ids: [],
rule_ids: [4],
reward_ids: [3],
},
{
id: 8,
name: "100% Cheapest Discount Program",
trigger: "auto",
applies_on: "current",
program_type: "promotion",
pricelist_ids: [1],
date_from: false,
date_to: false,
limit_usage: false,
max_usage: 0,
is_nominative: false,
portal_visible: true,
portal_point_name: "Points",
trigger_product_ids: [],
rule_ids: [5],
reward_ids: [4],
},
];
}
patch(hootPosModels, [...hootPosModels, LoyaltyProgram]);

View file

@ -0,0 +1,124 @@
import { patch } from "@web/core/utils/patch";
import { hootPosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { models } from "@web/../tests/web_test_helpers";
export class LoyaltyReward extends models.ServerModel {
_name = "loyalty.reward";
_load_pos_data_fields() {
return [
"description",
"program_id",
"reward_type",
"required_points",
"clear_wallet",
"currency_id",
"discount",
"discount_mode",
"discount_applicability",
"all_discount_product_ids",
"is_global_discount",
"discount_max_amount",
"discount_line_product_id",
"reward_product_id",
"multi_product",
"reward_product_ids",
"reward_product_qty",
"reward_product_uom_id",
"reward_product_domain",
];
}
_records = [
{
id: 1,
description: "10% Discount",
program_id: 1,
reward_type: "discount",
required_points: 10,
clear_wallet: false,
currency_id: 1,
discount: 10,
discount_mode: "percent",
discount_applicability: "order",
all_discount_product_ids: [],
is_global_discount: true,
discount_max_amount: 0,
discount_line_product_id: false,
reward_product_id: false,
multi_product: false,
reward_product_ids: [5],
reward_product_qty: 1,
reward_product_uom_id: false,
reward_product_domain: "[]",
},
{
id: 2,
description: "20% Discount",
program_id: 2,
reward_type: "product",
required_points: 10,
clear_wallet: false,
currency_id: 1,
discount: 0,
discount_mode: "percent",
discount_applicability: "order",
all_discount_product_ids: [],
is_global_discount: true,
discount_max_amount: 0,
discount_line_product_id: false,
reward_product_id: false,
multi_product: false,
reward_product_ids: [5],
reward_product_qty: 1,
reward_product_uom_id: false,
reward_product_domain: "[]",
},
{
id: 3,
description: "Free Product - Whiteboard Pen",
program_id: 7,
reward_type: "product",
required_points: 1,
clear_wallet: false,
currency_id: 1,
discount: 0,
discount_mode: "percent",
discount_applicability: "order",
all_discount_product_ids: [],
is_global_discount: true,
discount_max_amount: 0,
discount_line_product_id: 18,
reward_product_id: 10,
multi_product: false,
reward_product_ids: [10],
reward_product_qty: 1,
reward_product_uom_id: false,
reward_product_domain: "[]",
},
{
id: 4,
description: "100% Cheapest Discount",
program_id: 8,
reward_type: "discount",
required_points: 1,
clear_wallet: false,
currency_id: 1,
discount: 100,
discount_mode: "percent",
discount_applicability: "cheapest",
all_discount_product_ids: [],
is_global_discount: false,
discount_max_amount: 0,
discount_line_product_id: 5,
reward_product_id: false,
multi_product: false,
reward_product_ids: [5],
reward_product_qty: 1,
reward_product_uom_id: false,
reward_product_domain: "[]",
},
];
}
patch(hootPosModels, [...hootPosModels, LoyaltyReward]);

View file

@ -0,0 +1,100 @@
import { patch } from "@web/core/utils/patch";
import { hootPosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { models } from "@web/../tests/web_test_helpers";
export class LoyaltyRule extends models.ServerModel {
_name = "loyalty.rule";
_load_pos_data_fields() {
return [
"program_id",
"valid_product_ids",
"any_product",
"currency_id",
"reward_point_amount",
"reward_point_split",
"reward_point_mode",
"minimum_qty",
"minimum_amount",
"minimum_amount_tax_mode",
"mode",
"code",
];
}
_records = [
{
id: 1,
program_id: 1,
valid_product_ids: [5],
any_product: true,
currency_id: 1,
reward_point_amount: 1,
reward_point_split: true,
reward_point_mode: "order",
minimum_qty: 0,
minimum_amount: 0,
minimum_amount_tax_mode: "incl",
mode: "auto",
code: false,
},
{
id: 2,
program_id: 2,
valid_product_ids: [5],
any_product: true,
currency_id: 1,
reward_point_amount: 1,
reward_point_split: true,
reward_point_mode: "order",
minimum_qty: 3,
minimum_amount: 40,
minimum_amount_tax_mode: "excl",
mode: "auto",
code: false,
},
{
id: 3,
program_id: 6,
valid_product_ids: [5],
any_product: true,
currency_id: 1,
reward_point_amount: 1,
reward_point_split: true,
reward_point_mode: "order",
minimum_qty: 3,
minimum_amount: 40,
minimum_amount_tax_mode: "excl",
mode: "with_code",
code: "EXPIRED",
},
{
id: 4,
program_id: 7,
any_product: true,
currency_id: 1,
reward_point_amount: 1,
reward_point_split: false,
reward_point_mode: "unit",
minimum_qty: 1,
minimum_amount: 0,
minimum_amount_tax_mode: "incl",
mode: "auto",
},
{
id: 5,
program_id: 8,
any_product: true,
currency_id: 1,
reward_point_amount: 1,
reward_point_split: false,
reward_point_mode: "unit",
minimum_qty: 1,
minimum_amount: 0,
minimum_amount_tax_mode: "incl",
mode: "auto",
},
];
}
patch(hootPosModels, [...hootPosModels, LoyaltyRule]);

View file

@ -0,0 +1,110 @@
import { patch } from "@web/core/utils/patch";
import { PosOrder } from "@point_of_sale/../tests/unit/data/pos_order.data";
import { _t } from "@web/core/l10n/translation";
patch(PosOrder.prototype, {
validate_coupon_programs(self, point_changes) {
const couponIdsFromPos = new Set(Object.keys(point_changes).map((id) => parseInt(id)));
const coupons = this.env["loyalty.card"]
.browse([...couponIdsFromPos])
.filter((c) => c && c.program_id);
const couponDifference = new Set(
[...couponIdsFromPos].filter((id) => !coupons.find((c) => c.id === id))
);
if (couponDifference.size > 0) {
return {
successful: false,
payload: {
message: _t(
"Some coupons are invalid. The applied coupons have been updated. Please check the order."
),
removed_coupons: [...couponDifference],
},
};
}
for (const coupon of coupons) {
const needed = -point_changes[coupon.id];
if (parseFloat(coupon.points.toFixed(2)) < parseFloat(needed.toFixed(2))) {
return {
successful: false,
payload: {
message: _t("There are not enough points for the coupon: %s.", coupon.code),
updated_points: Object.fromEntries(coupons.map((c) => [c.id, c.points])),
},
};
}
}
return {
successful: true,
payload: {},
};
},
confirm_coupon_programs(self, coupon_data) {
const couponNewIdMap = {};
for (const k of Object.keys(coupon_data)) {
const id = parseInt(k);
if (id > 0) {
couponNewIdMap[id] = id;
}
}
const couponsToCreate = Object.fromEntries(
Object.entries(coupon_data).filter(([k]) => parseInt(k) < 0)
);
const couponCreateVals = Object.values(couponsToCreate).map((p) => ({
program_id: p.program_id,
partner_id: p.partner_id || false,
code: p.code || p.barcode || `CODE${Math.floor(Math.random() * 10000)}`,
points: p.points || 0,
}));
const newCouponIds = this.env["loyalty.card"].create(couponCreateVals);
const newCoupons = this.env["loyalty.card"].browse(newCouponIds);
for (let i = 0; i < Object.keys(couponsToCreate).length; i++) {
const oldId = parseInt(Object.keys(couponsToCreate)[i], 10);
const newCoupon = newCouponIds[i];
couponNewIdMap[oldId] = newCoupon.id;
}
const allCoupons = this.env["loyalty.card"].browse(Object.keys(couponNewIdMap).map(Number));
for (const coupon of allCoupons) {
const oldId = couponNewIdMap[coupon.id];
if (oldId && coupon_data[oldId]) {
coupon.points += coupon_data[oldId].points;
}
}
return {
coupon_updates: allCoupons.map((coupon) => ({
old_id: couponNewIdMap[coupon.id],
id: coupon.id,
points: coupon.points,
code: coupon.code,
program_id: coupon.program_id,
partner_id: coupon.partner_id,
})),
program_updates: [...new Set(allCoupons.map((c) => c.program_id))].map((program) => ({
program_id: program,
usages: this.env["loyalty.program"].browse(program)?.[0]?.total_order_count,
})),
new_coupon_info: newCoupons.map((c) => ({
program_name: this.env["loyalty.program"].browse(c.program_id)?.[0]?.name || "",
expiration_date: c.expiration_date || false,
code: c.code,
})),
coupon_report: {},
};
},
add_loyalty_history_lines() {
return true;
},
});

View file

@ -0,0 +1,15 @@
import { patch } from "@web/core/utils/patch";
import { PosOrderLine } from "@point_of_sale/../tests/unit/data/pos_order_line.data";
patch(PosOrderLine.prototype, {
_load_pos_data_fields() {
return [
...super._load_pos_data_fields(),
"is_reward_line",
"reward_id",
"coupon_id",
"reward_identifier_code",
"points_cost",
];
},
});

View file

@ -0,0 +1,14 @@
import { patch } from "@web/core/utils/patch";
import { PosSession } from "@point_of_sale/../tests/unit/data/pos_session.data";
patch(PosSession.prototype, {
_load_pos_data_models() {
return [
...super._load_pos_data_models(),
"loyalty.card",
"loyalty.program",
"loyalty.reward",
"loyalty.rule",
];
},
});

View file

@ -0,0 +1,8 @@
import { patch } from "@web/core/utils/patch";
import { ProductProduct } from "@point_of_sale/../tests/unit/data/product_product.data";
patch(ProductProduct.prototype, {
_load_pos_data_fields() {
return [...super._load_pos_data_fields(), "all_product_tag_ids"];
},
});

View file

@ -0,0 +1,359 @@
import { test, describe, expect } from "@odoo/hoot";
import { tick } from "@odoo/hoot-mock";
import { setupPosEnv, getFilledOrder } from "@point_of_sale/../tests/unit/utils";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { addProductLineToOrder } from "@pos_loyalty/../tests/unit/utils";
definePosModels();
const { DateTime } = luxon;
describe("pos.order - loyalty", () => {
test("_getIgnoredProductIdsTotalDiscount", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const ignoredProductIds = order._getIgnoredProductIdsTotalDiscount();
expect(ignoredProductIds.length).toBeGreaterThan(0);
});
test("getOrderlines, _get_reward_lines and _get_regular_order_lines", async () => {
const store = await setupPosEnv();
const order = await getFilledOrder(store);
const [line1, line2] = order.getOrderlines();
line1.update({ is_reward_line: true });
line2.update({ is_reward_line: false, refunded_orderline_id: 123 });
// Verify getOrderlines method
const orderedLines = order.getOrderlines();
expect(orderedLines[0]).toBe(line2);
expect(orderedLines[1]).toBe(line1);
expect(orderedLines[0].is_reward_line).toBe(false);
expect(orderedLines[1].is_reward_line).toBe(true);
expect(order.getLastOrderline()).toBe(line2);
// Verify _get_reward_lines method
const rewardLines = order._get_reward_lines();
expect(rewardLines).toEqual([line1]);
expect(rewardLines[0].is_reward_line).toBe(true);
// Verify _get_regular_order_lines
const regularLine = await addProductLineToOrder(store, order);
expect(order.getOrderlines().length).toBe(3);
const regularLines = order._get_regular_order_lines();
expect(regularLines.length).toBe(2);
expect(regularLines[1].id).toBe(regularLine.id);
});
test("setPricelist", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
const pricelist2 = models["product.pricelist"].get(2);
order.uiState.couponPointChanges = {
key1: { program_id: 1, points: 100 },
key2: { program_id: 2, points: 50 },
};
order.setPricelist(pricelist2);
const remainingKeys = Object.keys(order.uiState.couponPointChanges);
expect(remainingKeys.length).toBe(1);
expect(order.uiState.couponPointChanges[remainingKeys[0]].program_id).toBe(2);
});
test("_resetPrograms", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
order.uiState.disabledRewards = new Set(["reward1"]);
order.uiState.codeActivatedProgramRules = ["rule1"];
order.uiState.couponPointChanges = { key1: { points: 100 } };
await addProductLineToOrder(store, order, {
is_reward_line: true,
});
order._resetPrograms();
expect(order.uiState.disabledRewards.size).toBeEmpty();
expect(order.uiState.codeActivatedProgramRules.length).toBeEmpty();
expect(order.uiState.couponPointChanges).toMatchObject({});
});
test("_programIsApplicable", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
// Get loyalty program #1 - type = "ewallet"
const program = models["loyalty.program"].get(1);
expect(order._programIsApplicable(program)).toBe(true);
program.partner_id = false;
program.is_nominative = true;
expect(order._programIsApplicable(program)).toBe(false);
});
test("_getRealCouponPoints", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
// Get loyalty card #1 which program_id = 1 (loyalty)
const card = models["loyalty.card"].get(1);
order.uiState.couponPointChanges = {
1: {
coupon_id: 1,
program_id: 1,
points: 25,
},
};
await addProductLineToOrder(store, order, {
is_reward_line: true,
coupon_id: card,
points_cost: 5,
});
expect(order._getRealCouponPoints(card.id)).toBe(30);
});
test("processGiftCard", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
// Get loyalty program #3 - type = "gift_card"
const giftProgram = models["loyalty.program"].get(3);
const line = await addProductLineToOrder(store, order, {
price_unit: 10,
eWalletGiftCardProgram: giftProgram,
});
order.selected_orderline = line;
const expirationDate = DateTime.now().plus({ days: 1 }).toISODate();
order.processGiftCard("GIFT9999", 100, expirationDate);
const couponChanges = Object.values(order.uiState.couponPointChanges);
expect(couponChanges.length).toBe(1);
expect(couponChanges[0].code).toBe("GIFT9999");
expect(couponChanges[0].points).toBe(100);
expect(couponChanges[0].expiration_date).toBe(expirationDate);
expect(couponChanges[0].manual).toBe(true);
});
test("_getDiscountableOnOrder", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
await addProductLineToOrder(store, order, {
qty: 2,
});
await addProductLineToOrder(store, order, {
price_unit: 5,
});
// Get loyalty reward #1 - type = "discount"
const reward = models["loyalty.reward"].get(1);
const result = order._getDiscountableOnOrder(reward);
expect(result.discountable).toBe(25);
});
test("_computeNItems", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = await getFilledOrder(store);
// Get loyalty rule #1 - which program_id = 1 (loyalty)
const rule = models["loyalty.rule"].get(1);
expect(order.getOrderlines().length).toBe(2);
expect(order._computeNItems(rule)).toBe(5);
});
test("_canGenerateRewards", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
await addProductLineToOrder(store, order, {
qty: 5,
});
// Get loyalty program #2 - type = "ewallet"
const program = models["loyalty.program"].get(2);
expect(order._canGenerateRewards(program, 50, 50)).toBe(true);
expect(order._canGenerateRewards(program, 30, 30)).toBe(false);
});
test("isProgramsResettable", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
expect(order.isProgramsResettable()).toBe(false);
order.uiState.disabledRewards = [...new Set(["RULE1"])];
expect(order.isProgramsResettable()).toBe(true);
order.uiState.disabledRewards = new Set();
order.uiState.codeActivatedProgramRules.push("RULE2");
expect(order.isProgramsResettable()).toBe(true);
order.uiState.codeActivatedProgramRules = [];
order.uiState.couponPointChanges = { key1: { points: 10 } };
expect(order.isProgramsResettable()).toBe(true);
});
test("removeOrderline", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
// Get loyalty reward #1 - type = "discount"
const reward = models["loyalty.reward"].get(1);
// Get loyalty card #1 - which program_id = 1 (loyalty)
const coupon = models["loyalty.card"].get(1);
const rewardLine = await addProductLineToOrder(store, order, {
is_reward_line: true,
reward_id: reward,
coupon_id: coupon,
reward_identifier_code: "ABC123",
});
const normalLine = await addProductLineToOrder(store, order, {
price_unit: 20,
is_reward_line: false,
});
expect(order.getOrderlines().length).toBe(2);
const result = order.removeOrderline(rewardLine);
expect(result).toBe(true);
expect(order.getOrderlines().length).toBe(1);
const remainingLines = order.getOrderlines();
expect(remainingLines.length).toBe(1);
expect(remainingLines[0].id).toBe(normalLine.id);
expect(remainingLines[0].is_reward_line).toBe(false);
});
test("isSaleDisallowed", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
// Get loyalty program #3 - type = "gift_card"
const giftProgram = models["loyalty.program"].get(3);
const result = order.isSaleDisallowed({}, { eWalletGiftCardProgram: giftProgram });
expect(result).toBe(false);
});
test("setPartner and getLoyaltyPoints", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
const partner1 = models["res.partner"].get(1);
const partner2 = models["res.partner"].get(3);
order.setPartner(partner1);
order.uiState.couponPointChanges = {
key1: { program_id: 5, points: 100 },
key2: { program_id: 2, points: 50 },
};
order.setPartner(partner2);
const remainingKeys = Object.keys(order.uiState.couponPointChanges);
expect(remainingKeys.length).toBe(1);
expect(order.uiState.couponPointChanges[remainingKeys[0]].program_id).toBe(2);
// Verify getLoyaltyPoints method
order.uiState.couponPointChanges = {
1: {
coupon_id: 1,
program_id: 1,
points: 25,
},
};
const loyaltyStats = order.getLoyaltyPoints();
expect(loyaltyStats.length).toBe(1);
expect(loyaltyStats[0].points.name).toBe("Points");
expect(loyaltyStats[0].points.won).toBe(25);
expect(loyaltyStats[0].points.balance).toBe(10);
});
test("getLoyaltyPoints adapts to qty decreasing", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
const partner1 = models["res.partner"].get(1);
order.setPartner(partner1);
await store.orderUpdateLoyaltyPrograms();
const reward = models["loyalty.reward"].get(3);
const loyalty_card = models["loyalty.card"].get(4);
const line = await addProductLineToOrder(store, order, {
productId: 10,
templateId: 10,
qty: 3,
});
await store.orderUpdateLoyaltyPrograms();
order._applyReward(reward, loyalty_card.id);
const loyaltyStats = order.getLoyaltyPoints();
expect(loyaltyStats[0].points.won).toBe(0);
expect(loyaltyStats[0].points.spent).toBe(3);
expect(loyaltyStats[0].points.total).toBe(0);
expect(loyaltyStats[0].points.balance).toBe(3);
line.setQuantity(2);
await store.updateRewards();
await tick();
const loyaltyStats2 = order.getLoyaltyPoints();
expect(loyaltyStats2[0].points.won).toBe(0);
expect(loyaltyStats2[0].points.spent).toBe(2);
expect(loyaltyStats2[0].points.total).toBe(1);
expect(loyaltyStats2[0].points.balance).toBe(3);
});
test("reward amount tax included cheapest product", async () => {
const store = await setupPosEnv();
const order = store.addNewOrder();
const line = await addProductLineToOrder(store, order, {
productId: 24,
templateId: 24,
qty: 1,
});
expect(line.prices.total_included).toBe(10);
expect(line.prices.total_excluded).toBe(8.7);
await store.updateRewards();
await tick();
expect(order.getOrderlines().length).toBe(2);
const rewardLine = order._get_reward_lines()[0];
expect(rewardLine.prices.total_included).toBe(-10);
});
});

View file

@ -0,0 +1,59 @@
import { test, describe, expect } from "@odoo/hoot";
import { setupPosEnv } from "@point_of_sale/../tests/unit/utils";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { addProductLineToOrder } from "@pos_loyalty/../tests/unit/utils";
definePosModels();
describe("pos.order.line - loyalty", () => {
test("getEWalletGiftCardProgramType", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
// Get loyalty program #2 - type = "ewallet"
const program = models["loyalty.program"].get(2);
const line = await addProductLineToOrder(store, order, {
_e_wallet_program_id: program,
});
expect(line.getEWalletGiftCardProgramType()).toBe(`${program.program_type}`);
});
test("ignoreLoyaltyPoints", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
// Get loyalty program #2 - type = "ewallet"
const programA = models["loyalty.program"].get(2);
// Get loyalty program #4 - type = "ewallet"
const programB = models["loyalty.program"].get(4);
const line = await addProductLineToOrder(store, order);
line.update({ _e_wallet_program_id: programB });
expect(line.ignoreLoyaltyPoints({ program: programA })).toBe(true);
});
test("getGiftCardOrEWalletBalance", async () => {
const store = await setupPosEnv();
const models = store.models;
// Get loyalty card #3 which program_id = 3 (gift_card)
const card = models["loyalty.card"].get(3);
const order = store.addNewOrder();
const line = await addProductLineToOrder(store, order, {
is_reward_line: true,
coupon_id: card,
});
const balance = line.getGiftCardOrEWalletBalance();
expect(balance).toBeOfType("string");
expect(balance).toMatch(new RegExp(`${card.points}`));
});
});

View file

@ -0,0 +1,44 @@
import { test, expect } from "@odoo/hoot";
import { mountWithCleanup } from "@web/../tests/web_test_helpers";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { setupPosEnv } from "@point_of_sale/../tests/unit/utils";
import { addProductLineToOrder } from "@pos_loyalty/../tests/unit/utils";
import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen";
definePosModels();
test("TicketScreen.setOrder keeps reward line and triggers pos.updateRewards", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
const reward = models["loyalty.reward"].get(1);
const coupon = models["loyalty.card"].get(1);
await addProductLineToOrder(store, order, {
is_reward_line: true,
reward_id: reward,
coupon_id: coupon,
reward_identifier_code: "LOAD-ORDER-REWARD",
points_cost: 10,
});
order.uiState.couponPointChanges = {};
store.selectedOrderUuid = null;
let updateRewardsCalled = false;
const originalUpdateRewards = store.updateRewards.bind(store);
store.updateRewards = () => {
updateRewardsCalled = true;
return originalUpdateRewards();
};
const comp = await mountWithCleanup(TicketScreen, {});
await comp.setOrder(order);
expect(updateRewardsCalled).toBe(true);
const currentOrder = store.getOrder();
expect(currentOrder).toBe(order);
const rewardLines = currentOrder.lines.filter((l) => l.is_reward_line);
expect(rewardLines.length).toBe(1);
expect(rewardLines[0].reward_id.id).toBe(reward.id);
expect(rewardLines[0].coupon_id.id).toBe(coupon.id);
});

View file

@ -0,0 +1,68 @@
import { test, describe, expect } from "@odoo/hoot";
import { setupPosEnv } from "@point_of_sale/../tests/unit/utils";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { addProductLineToOrder } from "@pos_loyalty/../tests/unit/utils";
import { onRpc } from "@web/../tests/web_test_helpers";
definePosModels();
describe("PosStore - loyalty essentials", () => {
test("updatePrograms", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
const partner = models["res.partner"].get(1);
// Get loyalty program #5 with program_type = "gift_card" and is_nominative = true
const program = models["loyalty.program"].get(5);
models["loyalty.program"].getAll = () => [program];
order.setPartner(partner);
order._programIsApplicable = () => true;
order._code_activated_coupon_ids = [];
order.uiState.couponPointChanges = {};
order.pricelist_id = { id: 1 };
const line = await addProductLineToOrder(store, order, {
gift_code: "XYZ123",
});
order.pointsForPrograms = () => ({
[program.id]: [{ points: 10 }],
});
await store.updatePrograms();
const changes = order.uiState.couponPointChanges;
const changeList = Object.values(changes);
expect(changeList).toHaveLength(1);
expect(changeList[0].program_id).toBe(program.id);
expect(changeList[0].points).toBe(10);
expect(changeList[0].code).toBe("XYZ123");
expect(changeList[0].product_id).toBe(line.product_id.id);
});
test("activateCode", async () => {
onRpc("loyalty.card", "get_loyalty_card_partner_by_code", () => false);
const store = await setupPosEnv();
store.addNewOrder();
const result = await store.activateCode("EXPIRED");
expect(result).toBe(true);
});
test("fetchLoyaltyCard", async () => {
const store = await setupPosEnv();
const models = store.models;
// Get loyalty program #2 with program_type = "ewallet"
const program = models["loyalty.program"].get(2);
const partner = models["res.partner"].get(1);
const card = await store.fetchLoyaltyCard(program.id, partner.id);
expect(card.id).toBe(2);
});
});

View file

@ -0,0 +1,48 @@
import { test, expect } from "@odoo/hoot";
import { setupPosEnv } from "@point_of_sale/../tests/unit/utils";
import { definePosModels } from "@point_of_sale/../tests/unit/data/generate_model_definitions";
import { addProductLineToOrder } from "@pos_loyalty/../tests/unit/utils";
import OrderPaymentValidation from "@point_of_sale/app/utils/order_payment_validation";
definePosModels();
test("validateOrder", async () => {
const store = await setupPosEnv();
const models = store.models;
const order = store.addNewOrder();
const fastPaymentMethod = order.config.fast_payment_method_ids[0];
// Get loyalty program #1 - type = "loyalty"
const loyaltyProgram = models["loyalty.program"].get(1);
// Get loyalty card #1 - linked to Partner #1
const card = models["loyalty.card"].get(1);
// Get loyalty reward #1 - type = "discount"
const reward = models["loyalty.reward"].get(1);
order.uiState.couponPointChanges = {
[card.id]: { coupon_id: card.id, program_id: loyaltyProgram.id, points: 100 },
"-1": { coupon_id: -1, program_id: loyaltyProgram.id, points: 30, partner_id: 1 },
};
await addProductLineToOrder(store, order, {
coupon_id: card,
is_reward_line: true,
reward_id: reward,
points_cost: 60,
});
const validation = new OrderPaymentValidation({
pos: store,
orderUuid: store.getOrder().uuid,
fastPaymentMethod: fastPaymentMethod,
});
validation.isOrderValid = async () => true;
await validation.validateOrder();
expect(card.points).toBe(50);
expect(loyaltyProgram.total_order_count).toBe(0);
expect(order.new_coupon_info[0].code).toMatch(/^[A-Za-z0-9]+$/);
expect(order.new_coupon_info[0].program_name).toBe(loyaltyProgram.name);
});

View file

@ -0,0 +1,20 @@
export const addProductLineToOrder = async (
store,
order,
{ templateId = 1, productId = 1, qty = 1, price_unit = 10, ...extraFields } = {}
) => {
const template = store.models["product.template"].get(templateId);
const product = store.models["product.product"].get(productId);
const lineData = {
product_tmpl_id: template,
product_id: product,
qty,
price_unit,
...extraFields,
};
const line = await store.addLineToOrder(lineData, order);
return line;
};