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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before After
Before After

View file

@ -0,0 +1,150 @@
import { Component, onMounted, useRef, useState } from "@odoo/owl";
import { Dialog } from "@web/core/dialog/dialog";
import { useService } from "@web/core/utils/hooks";
import { DateTimeInput } from "@web/core/datetime/datetime_input";
import { deserializeDateTime, serializeDate } from "@web/core/l10n/dates";
import { usePos } from "@point_of_sale/app/hooks/pos_hook";
import { _t } from "@web/core/l10n/translation";
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { debounce } from "@bus/workers/bus_worker_utils";
import { logPosMessage } from "@point_of_sale/app/utils/pretty_console_log";
import { roundCurrency } from "@point_of_sale/app/models/utils/currency";
export class ManageGiftCardPopup extends Component {
static template = "pos_loyalty.ManageGiftCardPopup";
static components = { Dialog, DateTimeInput };
static props = {
title: String,
placeholder: { type: String, optional: true },
rows: { type: Number, optional: true },
line: Object,
getPayload: Function,
close: Function,
};
static defaultProps = {
startingValue: "",
placeholder: "",
rows: 1,
};
setup() {
this.ui = useService("ui");
this.dialog = useService("dialog");
this.pos = usePos();
this.state = useState({
lockGiftCardFields: false,
loading: false,
inputValue: this.props.startingValue,
amountValue: this.props.line.prices.total_included.toString(),
error: false,
amountError: false,
expirationDate: luxon.DateTime.now().plus({ year: 1 }),
});
this.inputRef = useRef("input");
this.amountInputRef = useRef("amountInput");
this.batchedGiftcardCodeKeydown = debounce(this.checkGiftCard.bind(this), 500);
onMounted(this.onMounted);
}
onMounted() {
// Removing the main "DateTimeInput" component's class "o_input" and
// adding the CSS classes "form-control" and "form-control-lg" for styling the form input with Bootstrap.
const expirationDateInput = document.querySelector(".o_exp_date_container").children[1];
expirationDateInput.classList.remove("o_input");
expirationDateInput.classList.add("form-control", "form-control-lg");
this.inputRef.el.focus();
}
onKeydownGiftCardCode() {
this.state.loading = true;
this.batchedGiftcardCodeKeydown();
}
async checkGiftCard() {
try {
const code = this.state.inputValue.trim();
const result = await this.pos.data.call("loyalty.card", "get_gift_card_status", [
code,
this.pos.config.id,
]);
if (!result.status) {
this.dialog.add(AlertDialog, {
title: _t("Invalid Gift Card Code"),
body: _t(
"This code seems to be invalid, please check the Gift Card code and try again."
),
});
this.state.error = true;
this.state.lastCheck = false;
this.state.inputValue = "";
return false;
}
if (result.data["loyalty.card"].length > 0) {
const giftCard = result.data["loyalty.card"][0];
this.state.amountValue = roundCurrency(
giftCard.points?.toString() || "0",
this.pos.currency
).toString();
this.state.lockGiftCardFields = true;
if (giftCard.expiration_date) {
this.state.expirationDate = deserializeDateTime(giftCard.expiration_date);
}
} else {
this.state.lockGiftCardFields = false;
}
} catch (error) {
logPosMessage(
"ManageGiftCardPopup",
"checkGiftCard",
"Error fetching gift card data",
false,
[error]
);
this.pos.notification.add({
type: "danger",
body: _t("An error occurred while checking the gift card."),
});
} finally {
this.state.error = false;
this.state.loading = false;
}
return true;
}
async addBalance(ev) {
if (!this.validateCode()) {
return;
}
this.props.getPayload(
this.state.inputValue,
parseFloat(this.state.amountValue),
this.state.expirationDate ? serializeDate(this.state.expirationDate) : false
);
this.props.close();
}
close() {
this.props.close();
}
validateCode() {
const { inputValue, amountValue } = this.state;
if (inputValue.trim() === "") {
this.state.error = true;
return false;
}
if (amountValue.trim() === "") {
this.state.amountError = true;
return false;
}
return true;
}
onExpDateChange(date) {
this.state.expirationDate = date;
}
}

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_loyalty.ManageGiftCardPopup">
<Dialog title="props.title" size="'md'">
<div class="position-relative">
<input id="code"
t-att-rows="props.rows"
class="form-control form-control-lg mx-auto"
type="text"
autocomplete="off"
t-model="state.inputValue"
t-ref="input"
t-on-keydown="onKeydownGiftCardCode"
t-att-placeholder="props.placeholder"
t-att-style="state.error ? 'border-color: red;' : ''" />
<div t-if="state.loading" class="gift-card-loading pe-3 h-100 position-absolute end-0 top-0 d-flex align-items-center">
<i class="fa fa-spinner fa-spin" />
</div>
</div>
<div class="mt-3 d-flex">
<div t-attf-class="col align-items-center d-flex {{!ui.isSmall? 'me-2 w-50': ''}}">
<div class="col-form-label text-center pe-0 me-4 fs-5">Amount</div>
<div t-attf-class="{{ui.isSmall? 'flex-grow-1' : ''}}">
<input id="amount"
class="form-control form-control-lg"
type="number"
t-att-disabled="state.lockGiftCardFields || state.loading"
t-model="state.amountValue"
t-ref="amountInput"
placeholder="Enter amount"
t-att-style="state.amountError ? 'border-color: red;' : ''"/>
</div>
</div>
<div t-if="!ui.isSmall" class="d-flex ms-2 w-50 o_exp_date_container">
<div class="col-form-label text-center pe-0 me-4 fs-5">Expiration</div>
<DateTimeInput
type="'date'"
disabled="state.lockGiftCardFields || state.loading"
value="state.expirationDate"
onChange.bind="onExpDateChange" />
</div>
</div>
<div t-if="ui.isSmall" class="d-flex my-2 o_exp_date_container">
<div class="col-form-label text-center pe-0 me-2 fs-5">Expiration</div>
<DateTimeInput
type="'date'"
value="state.expirationDate"
disabled="state.lockGiftCardFields || state.loading"
onChange.bind="onExpDateChange" />
</div>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="addBalance" t-att-disabled="state.loading">
<t t-if="state.lockGiftCardFields">
Add existing Gift Card
</t>
<t t-else="">
Add Balance
</t>
</button>
</t>
</Dialog>
</t>
</templates>

View file

@ -0,0 +1,30 @@
import { DataServiceOptions } from "@point_of_sale/app/models/data_service_options";
import { patch } from "@web/core/utils/patch";
patch(DataServiceOptions.prototype, {
get databaseTable() {
return {
...super.databaseTable,
"loyalty.card": {
key: "id",
condition: (record) =>
record
.backLink("<-pos.order.line.coupon_id")
.find((l) => !l.order_id?.canBeRemovedFromIndexedDB),
getRecordsBasedOnLines: (orderlines) =>
orderlines.map((line) => line.coupon_id).filter((c) => c),
},
};
},
get pohibitedAutoLoadedModels() {
return [
...super.pohibitedAutoLoadedModels,
"loyalty.program",
"loyalty.rule",
"loyalty.reward",
];
},
get cleanupModels() {
return [...super.cleanupModels, "loyalty.program"];
},
});

View file

@ -0,0 +1,19 @@
import { registry } from "@web/core/registry";
import { Base } from "@point_of_sale/app/models/related_models";
const { DateTime } = luxon;
export class LoyaltyCard extends Base {
static pythonModel = "loyalty.card";
isExpired() {
// If no expiration date is set, the card is not expired
if (!this.expiration_date) {
return false;
}
return DateTime.fromISO(this.expiration_date).toMillis() < DateTime.now().toMillis();
}
}
registry.category("pos_available_models").add(LoyaltyCard.pythonModel, LoyaltyCard);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,85 @@
import { PosOrderline } from "@point_of_sale/app/models/pos_order_line";
import { formatCurrency } from "@point_of_sale/app/models/utils/currency";
import { patch } from "@web/core/utils/patch";
patch(PosOrderline, {
extraFields: {
...(PosOrderline.extraFields || {}),
_e_wallet_program_id: {
model: "pos.order.line",
name: "_e_wallet_program_id",
relation: "loyalty.program",
type: "many2one",
local: true,
},
gift_code: {
model: "pos.order.line",
name: "gift_code",
type: "char",
local: true,
},
_gift_barcode: {
model: "pos.order.line",
name: "_gift_barcode",
type: "char",
local: true,
},
_gift_card_id: {
model: "pos.order.line",
name: "_gift_card_id",
relation: "loyalty.card",
type: "many2one",
local: true,
},
_reward_product_id: {
model: "pos.order.line",
name: "_reward_product_id",
relation: "product.product",
type: "many2one",
local: true,
},
},
});
patch(PosOrderline.prototype, {
setOptions(options) {
if (options.eWalletGiftCardProgram) {
this._e_wallet_program_id = options.eWalletGiftCardProgram;
}
if (options.giftBarcode) {
this._gift_barcode = options.giftBarcode;
}
if (options.giftCardId) {
this._gift_card_id = options.giftCardId;
}
return super.setOptions(...arguments);
},
getEWalletGiftCardProgramType() {
return this._e_wallet_program_id && this._e_wallet_program_id.program_type;
},
ignoreLoyaltyPoints({ program }) {
return (
(["gift_card", "ewallet"].includes(program.program_type) &&
this._e_wallet_program_id?.id !== program.id) ||
this.settled_invoice_id ||
this.settled_order_id
);
},
isGiftCardOrEWalletReward() {
const coupon = this.coupon_id;
if (!coupon || !this.is_reward_line) {
return false;
}
return ["ewallet", "gift_card"].includes(coupon.program_id?.program_type);
},
getGiftCardOrEWalletBalance() {
const coupon = this.coupon_id;
return formatCurrency(coupon?.points || 0, this.currency);
},
getDisplayClasses() {
return {
...super.getDisplayClasses(),
"fst-italic": this.is_reward_line,
};
},
});

View file

@ -0,0 +1,23 @@
import { _t } from "@web/core/l10n/translation";
import { usePos } from "@point_of_sale/app/hooks/pos_hook";
import { PartnerLine } from "@point_of_sale/app/screens/partner_list/partner_line/partner_line";
import { patch } from "@web/core/utils/patch";
import { formatFloat } from "@web/core/utils/numbers";
patch(PartnerLine.prototype, {
setup() {
super.setup(...arguments);
this.pos = usePos();
},
_getLoyaltyPointsRepr(loyaltyCard) {
const program = loyaltyCard.program_id;
if (program.program_type === "ewallet") {
return `${program.name}: ${this.env.utils.formatCurrency(loyaltyCard.points)}`;
}
const balanceRepr = formatFloat(loyaltyCard.points, { digits: [69, 2] });
if (program.portal_visible) {
return `${balanceRepr} ${program.portal_point_name}`;
}
return _t("%s Points", balanceRepr);
},
});

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_loyalty.PartnerLine" t-inherit="point_of_sale.PartnerLine" t-inherit-mode="extension" owl="1">
<t t-name="pos_loyalty.PartnerLine" t-inherit="point_of_sale.PartnerLine" t-inherit-mode="extension">
<xpath expr="//td[hasclass('partner-line-balance')]" position="inside">
<t t-set="_loyaltyCards" t-value="env.pos.getLoyaltyCards(props.partner)" />
<t t-set="_loyaltyCards" t-value="pos.getLoyaltyCards(props.partner)" />
<t t-foreach="_loyaltyCards" t-as="_loyaltyCard" t-key="_loyaltyCard.id">
<div class="pos-right-align">
<t t-esc="_getLoyaltyPointsRepr(_loyaltyCard)"/>

View file

@ -0,0 +1,28 @@
import { PartnerList } from "@point_of_sale/app/screens/partner_list/partner_list";
import { patch } from "@web/core/utils/patch";
patch(PartnerList.prototype, {
/**
* Needs to be set to true to show the loyalty points in the partner list.
* @override
*/
get isBalanceDisplayed() {
return true;
},
async searchPartner() {
const res = await super.searchPartner();
const programIds = this.pos.models["loyalty.program"].getAll().map((p) => p.id);
const coupons = await this.pos.fetchCoupons(
[
["partner_id", "in", res.map((partner) => partner.id)],
["program_id.active", "=", true],
["program_id", "in", programIds],
["points", ">", 0],
],
null
);
this.pos.computePartnerCouponIds(coupons);
return res;
},
});

View file

@ -0,0 +1,219 @@
import { useState, onWillRender } from "@odoo/owl";
import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons";
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { TextInputPopup } from "@point_of_sale/app/components/popups/text_input_popup/text_input_popup";
import { _t } from "@web/core/l10n/translation";
import { SelectionPopup } from "@point_of_sale/app/components/popups/selection_popup/selection_popup";
import { makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog";
import { patch } from "@web/core/utils/patch";
patch(ControlButtons.prototype, {
setup() {
super.setup(...arguments);
this.state = useState({
nbrRewards: 0,
});
onWillRender(() => {
this.state.nbrRewards = this.getPotentialRewards().length;
});
},
_getEWalletRewards(order) {
const claimableRewards = order.getClaimableRewards();
return claimableRewards.filter((reward_line) => {
const coupon = this.pos.models["loyalty.card"].get(reward_line.coupon_id);
return (
coupon &&
reward_line.reward.program_id.program_type == "ewallet" &&
!coupon.isExpired()
);
});
},
_getEWalletPrograms() {
return this.pos.models["loyalty.program"].filter((p) => p.program_type == "ewallet");
},
async onClickWallet() {
const order = this.pos.getOrder();
const eWalletPrograms = this._getEWalletPrograms();
const orderTotal = order.priceIncl;
const eWalletRewards = this._getEWalletRewards(order);
if (eWalletRewards.length === 0 && orderTotal >= 0) {
this.dialog.add(AlertDialog, {
title: _t("No valid eWallet found"),
body: _t("Please select a customer and a valid eWallet."),
});
return;
}
if (orderTotal < 0 && eWalletPrograms.length >= 1) {
let selectedProgram = null;
if (eWalletPrograms.length == 1) {
selectedProgram = eWalletPrograms[0];
} else {
selectedProgram = await makeAwaitable(this.dialog, SelectionPopup, {
title: _t("Refund with eWallet"),
list: eWalletPrograms.map((program) => ({
id: program.id,
item: program,
label: program.name,
})),
});
}
if (selectedProgram) {
this.pos.addLineToCurrentOrder(
{
product_id: selectedProgram.trigger_product_ids[0],
product_tmpl_id: selectedProgram.trigger_product_ids[0].product_tmpl_id,
_e_wallet_program_id: selectedProgram,
price_unit: -orderTotal,
},
{}
);
}
} else if (eWalletRewards.length >= 1) {
let eWalletReward = null;
if (eWalletRewards.length == 1) {
eWalletReward = eWalletRewards[0];
} else {
eWalletReward = await makeAwaitable(this.dialog, SelectionPopup, {
title: _t("Use eWallet to pay"),
list: eWalletRewards.map(({ reward, coupon_id }) => ({
id: reward.id,
item: { reward, coupon_id },
label: `${reward.description} (${reward.program_id.name})`,
})),
});
}
if (eWalletReward) {
const result = order._applyReward(
eWalletReward.reward,
eWalletReward.coupon_id,
{}
);
if (result !== true) {
// Returned an error
this.dialog.add(AlertDialog, {
title: _t("Error"),
body: result,
});
}
this.pos.updateRewards();
}
}
},
async clickPromoCode() {
this.dialog.add(TextInputPopup, {
title: _t("Enter Code"),
placeholder: _t("Gift card or Discount code"),
getPayload: async (code) => {
code = code.trim();
if (code !== "") {
const res = await this.pos.activateCode(code);
if (res !== true) {
this.notification.add(res, { type: "danger" });
}
}
},
});
},
getPotentialRewards() {
const order = this.pos.getOrder();
// Claimable rewards excluding those from eWallet programs.
// eWallet rewards are handled in the eWalletButton.
let rewards = [];
if (order) {
const claimableRewards = order.getClaimableRewards();
rewards = claimableRewards.filter(
({ reward }) => reward.program_id.program_type !== "ewallet"
);
}
const result = {};
const discountRewards = rewards.filter(({ reward }) => reward.reward_type == "discount");
const freeProductRewards = rewards.filter(({ reward }) => reward.reward_type == "product");
const potentialFreeProductRewards = this.pos.getPotentialFreeProductRewards();
const avaiRewards = [
...potentialFreeProductRewards,
...discountRewards,
...freeProductRewards, // Free product rewards at the end of array to prioritize them
];
for (const reward of avaiRewards) {
result[reward.reward.id] = reward;
}
return Object.values(result);
},
/**
* Applies the reward on the current order, if multiple products can be claimed opens a popup asking for which one.
*
* @param {Object} reward
* @param {Integer} coupon_id
*/
async _applyReward(reward, coupon_id, potentialQty) {
const order = this.pos.getOrder();
order.uiState.disabledRewards.delete(reward.id);
const args = {};
if (reward.reward_type === "product" && reward.multi_product) {
const productsList = reward.reward_product_ids.map((product_id) => ({
id: product_id.id,
label: product_id.display_name,
item: product_id,
}));
const selectedProduct = await makeAwaitable(this.dialog, SelectionPopup, {
title: _t("Please select a product for this reward"),
list: productsList,
});
if (!selectedProduct) {
return false;
}
args["product"] = selectedProduct;
}
if (
(reward.reward_type == "product" && reward.program_id.applies_on !== "both") ||
(reward.program_id.applies_on == "both" && potentialQty)
) {
const product = args["product"] || reward.reward_product_ids[0];
await this.pos.addLineToCurrentOrder(
{
product_id: product,
product_tmpl_id: product.product_tmpl_id,
qty: potentialQty || 1,
},
{}
);
return true;
} else {
const result = order._applyReward(reward, coupon_id, args);
if (result !== true) {
// Returned an error
this.notification.add(result);
}
this.pos.updateRewards();
return result;
}
},
async clickRewards() {
const rewards = this.getPotentialRewards();
if (rewards.length >= 1) {
const rewardsList = rewards.map((reward) => ({
id: reward.reward.id,
label: reward.reward.program_id.name,
description: `Add "${reward.reward.description}"`,
item: reward,
}));
this.dialog.add(SelectionPopup, {
title: _t("Available rewards"),
list: rewardsList,
getPayload: (selectedReward) => {
this._applyReward(
selectedReward.reward,
selectedReward.coupon_id,
selectedReward.potentialQty
);
},
});
}
},
});

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_loyalty.ControlButtons" t-inherit="point_of_sale.ControlButtons" t-inherit-mode="extension">
<xpath
expr="//t[@t-if='props.showRemainingButtons']/div/button[hasclass('o_pricelist_button')]"
position="before">
<t t-if="pos.models['loyalty.program'].some((p) => p.program_type == 'ewallet')">
<t t-set="_orderTotal" t-value="pos.getOrder().priceIncl" />
<t t-set="_eWalletPrograms" t-value="_getEWalletPrograms()" />
<t t-set="_eWalletRewards" t-value="_getEWalletRewards(pos.getOrder())" />
<button t-att-class="buttonClass"
t-on-click="onClickWallet"
t-attf-class="{{(_orderTotal lt 0 and _eWalletPrograms.length gte 1) or _eWalletRewards.length gte 1 ? 'highlight text-action': 'disabled'}}">
<i class="fa fa-credit-card me-1" />
<t t-if="_orderTotal lt 0 and _eWalletPrograms.length">eWallet Refund</t>
<t t-elif="_eWalletRewards.length">eWallet Pay</t>
<t t-else="">eWallet</t>
</button>
</t>
<t t-if="pos.models['loyalty.program'].some((p) => ['coupons', 'promotion', 'gift_card', 'promo_code', 'next_order_coupons'].includes(p.program_type))">
<button t-att-class="buttonClass"
t-on-click="() => this.clickPromoCode()">
<i class="fa fa-barcode me-1"/>Enter Code
</button>
</t>
<t t-if="pos.models['loyalty.program'].length and this.pos.cashier._role !== 'minimal'">
<button class="control-button"
t-att-class="buttonClass"
t-attf-class="{{state.nbrRewards ? 'highlight' : 'disabled'}}"
t-on-click="() => this.clickRewards()">
<i class="fa fa-star me-1 text-favourite"/>Reward
</button>
</t>
</xpath>
<xpath expr="//t[@t-if='props.showRemainingButtons']/div/button[hasclass('o_pricelist_button')]" position="before">
<t t-if="pos.models['loyalty.program'].some((p) => ['coupons', 'promotion'].includes(p.program_type)) and this.pos.cashier._role !== 'minimal'">
<button class="btn btn-secondary btn-lg py-5" t-att-class="{'disabled': !pos.getOrder().isProgramsResettable()}"
t-on-click="() => this.pos.resetPrograms()">
<i class="fa fa-star me-1"/>Reset Programs
</button>
</t>
</xpath>
<xpath expr="//button[hasclass('more-btn')]" position="attributes">
<attribute name="t-attf-class">{{ state.nbrRewards ? 'active text-action' : '' }}</attribute>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,189 @@
import { _t } from "@web/core/l10n/translation";
import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary";
import { patch } from "@web/core/utils/patch";
import { ask } from "@point_of_sale/app/utils/make_awaitable_dialog";
import { useService } from "@web/core/utils/hooks";
import { AlertDialog, ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { ManageGiftCardPopup } from "@pos_loyalty/app/components/popups/manage_giftcard_popup/manage_giftcard_popup";
import { logPosMessage } from "@point_of_sale/app/utils/pretty_console_log";
patch(OrderSummary.prototype, {
setup() {
super.setup(...arguments);
this.notification = useService("notification");
},
async updateSelectedOrderline({ buffer, key }) {
const selectedLine = this.currentOrder.getSelectedOrderline();
if (selectedLine?.gift_code && key !== "Backspace" && key !== "Delete") {
this.dialog.add(AlertDialog, {
title: _t("Gift Card"),
body: _t("You cannot change the quantity or price of a physical gift card."),
});
return;
}
if (key === "-") {
if (selectedLine && selectedLine._e_wallet_program_id) {
// Do not allow negative quantity or price in a gift card or ewallet orderline.
// Refunding gift card or ewallet is not supported.
this.notification.add(
_t("You cannot set negative quantity or price to gift card or ewallet."),
4000
);
return;
}
}
if (
selectedLine &&
selectedLine.is_reward_line &&
!selectedLine.manual_reward &&
(key === "Backspace" || key === "Delete")
) {
const reward = selectedLine.reward_id;
const confirmed = await ask(this.dialog, {
title: _t("Deactivating reward"),
body: _t(
"Are you sure you want to remove %s from this order?\n You will still be able to claim it through the reward button.",
reward.description
),
cancelLabel: _t("No"),
confirmLabel: _t("Yes"),
});
if (confirmed) {
buffer = null;
} else {
// Cancel backspace
return;
}
}
return super.updateSelectedOrderline({ buffer, key });
},
/**
* 1/ Perform the usual set value operation (super._setValue(val)) if the line being modified
* is not a reward line or if it is a reward line, the `val` being set is '' or 'remove' only.
*
* 2/ Update activated programs and coupons when removing a reward line.
*
* 3/ Trigger 'update-rewards' if the line being modified is a regular line or
* if removing a reward line.
*
* @override
*/
_setValue(val) {
const selectedLine = this.currentOrder.getSelectedOrderline();
if (!selectedLine) {
return;
}
if (selectedLine.is_reward_line && val === "remove") {
this.currentOrder.uiState.disabledRewards.add(selectedLine.reward_id.id);
const coupon = selectedLine.coupon_id;
if (
coupon &&
coupon.id > 0 &&
this.currentOrder._code_activated_coupon_ids.find((c) => c.code === coupon.code)
) {
coupon.delete();
}
}
if (
!selectedLine ||
!selectedLine.is_reward_line ||
(selectedLine.is_reward_line && ["", "remove"].includes(val))
) {
super._setValue(val);
}
if (!selectedLine.is_reward_line || (selectedLine.is_reward_line && val === "remove")) {
this.pos.updateRewards();
}
},
async _showDecreaseQuantityPopup() {
const result = await super._showDecreaseQuantityPopup();
if (result) {
this.pos.updateRewards();
}
},
/**
* Updates the order line with the gift card information:
* 1. Reduce the quantity if greater than one, otherwise remove the order line.
* 2. Add a new order line with updated gift card code and points, removing any existing related couponPointChanges.
*/
async _updateGiftCardOrderline(code, points) {
let selectedLine = this.currentOrder.getSelectedOrderline();
const product = selectedLine.product_id;
if (selectedLine.getQuantity() > 1) {
selectedLine.setQuantity(selectedLine.getQuantity() - 1);
} else {
this.currentOrder.removeOrderline(selectedLine);
}
const program = this.pos.models["loyalty.program"].find(
(p) => p.program_type === "gift_card"
);
const existingCouponIds = Object.keys(this.currentOrder.uiState.couponPointChanges).filter(
(key) => {
const change = this.currentOrder.uiState.couponPointChanges[key];
return (
change.points === product.lst_price &&
change.program_id === program.id &&
change.product_id === product.id &&
!change.manual
);
}
);
if (existingCouponIds.length) {
const couponId = existingCouponIds.shift();
delete this.currentOrder.uiState.couponPointChanges[couponId];
}
await this.pos.addLineToCurrentOrder(
{ product_id: product, product_tmpl_id: product.product_tmpl_id },
{ price_unit: points }
);
selectedLine = this.currentOrder.getSelectedOrderline();
selectedLine.gift_code = code;
},
manageGiftCard(line) {
this.dialog.add(ManageGiftCardPopup, {
title: _t("Sell/Manage physical gift card"),
placeholder: _t("Enter Gift Card Number"),
line: line,
getPayload: async (code, points, expirationDate) => {
points = parseFloat(points);
if (isNaN(points)) {
logPosMessage(
"OrderSummary",
"updateOnlinePaymentsDataWithServer",
`Invalid amount value: ${points}`,
false
);
return;
}
code = code.trim();
// check for duplicate code
if (this.currentOrder.duplicateCouponChanges(code)) {
this.dialog.add(ConfirmationDialog, {
title: _t("Validation Error"),
body: _t("A coupon/loyalty card must have a unique code."),
});
return;
}
await this._updateGiftCardOrderline(code, points);
this.currentOrder.processGiftCard(code, points, expirationDate);
},
});
},
clickLine(ev, orderline) {
if (orderline.isSelected() && orderline.getEWalletGiftCardProgramType() === "gift_card") {
return;
} else {
super.clickLine(ev, orderline);
}
},
});

View file

@ -0,0 +1,3 @@
.loyalty-points span.value {
width: 80px;
}

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_loyalty.OrderSummary" t-inherit="point_of_sale.OrderSummary" t-inherit-mode="extension">
<xpath expr="//Orderline" position="inside" >
<li t-if="line.isGiftCardOrEWalletReward()">
Current Balance: <t t-esc="line.getGiftCardOrEWalletBalance()"/>
</li>
<t t-if="!line.isGiftCardOrEWalletReward() and line.getEWalletGiftCardProgramType() === 'gift_card'">
<a t-if="!line.gift_code and line.qty &lt;= 1" class="text-wrap text-action" t-on-click="() => this.manageGiftCard(line)">Sell physical gift card?</a>
<div t-if="line.gift_code" class="text-wrap" t-esc="line.gift_code"/>
</t>
</xpath>
<xpath expr="//OrderDisplay/t[@t-set-slot='details']" position="inside">
<t t-foreach="pos.getOrder()?.getLoyaltyPoints() or []" t-as="_loyaltyStat" t-key="_loyaltyStat.couponId">
<div t-if="_loyaltyStat.points.won || _loyaltyStat.points.spent" class="d-flex justify-content-between px-2 py-2 bg-view">
<div t-esc="_loyaltyStat.points.name" class="loyalty-points-title fs-4 fw-bolder"/>
<div class="d-flex gap-1 fw-bold">
<div t-if='_loyaltyStat.points.balance' class="loyalty-points loyalty-points-balance">
<span class='value'><t t-esc='_loyaltyStat.points.balance'/></span>
</div>
<div t-if='_loyaltyStat.points.won' class="loyalty-points loyalty-points-won">
<span class='value text-success'>+<t t-esc='_loyaltyStat.points.won'/></span>
</div>
<div t-if='_loyaltyStat.points.spent' class="loyalty-points loyalty-points-spent">
<span class='value text-danger'>-<t t-esc='_loyaltyStat.points.spent'/></span>
</div>
=
<div class="loyalty-points loyalty-points-totaltext-end fw-bolder">
<span class='value'><t t-esc='_loyaltyStat.points.total'/></span>
</div>
</div>
</div>
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,33 @@
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { useBarcodeReader } from "@point_of_sale/app/hooks/barcode_reader_hook";
import { patch } from "@web/core/utils/patch";
import { useService } from "@web/core/utils/hooks";
patch(ProductScreen.prototype, {
setup() {
super.setup(...arguments);
this.notification = useService("notification");
useBarcodeReader({
coupon: this._onCouponScan,
});
},
async _onCouponScan(code) {
// IMPROVEMENT: Ability to understand if the scanned code is to be paid or to be redeemed.
const res = await this.pos.activateCode(code.base_code);
if (res !== true) {
this.notification.add(res, { type: "danger" });
}
},
async _barcodeProductAction(code) {
await super._barcodeProductAction(code);
this.pos.updateRewards();
},
async _barcodeGS1Action(code) {
await super._barcodeGS1Action(code);
this.pos.updateRewards();
},
async _barcodePartnerAction(code) {
await super._barcodePartnerAction(code);
this.pos.updateRewards();
},
});

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_coupon.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension">
<xpath expr="//div[hasclass('pos-receipt')]//div[hasclass('before-footer')]" position="inside">
<t t-if="order.models['loyalty.program'].length">
<t t-foreach="order.getLoyaltyPoints() or []" t-as="_loyaltyStat" t-key="_loyaltyStat.couponId">
<div t-if="_loyaltyStat.program.portal_visible and (_loyaltyStat.points.won || _loyaltyStat.points.spent)" class='pt-3 loyalty'>
<t t-if='_loyaltyStat.points.won'>
<div class="d-flex" style="font-size: 75%;">
<span><t t-out="_loyaltyStat.points.name"/> Won:</span>
<span t-out='_loyaltyStat.points.won' class="ms-auto"/>
</div>
</t>
<t t-if='_loyaltyStat.points.spent'>
<div class="d-flex" style="font-size: 75%;">
<span><t t-out="_loyaltyStat.points.name"/> Spent:</span>
<span t-out='_loyaltyStat.points.spent' class="ms-auto"/>
</div>
</t>
<t t-if='_loyaltyStat.points.balance'>
<div class="d-flex" style="font-size: 75%;">
<span>Balance <t t-out="_loyaltyStat.points.name"/>:</span>
<span t-out='_loyaltyStat.points.balance' class="ms-auto"/>
</div>
</t>
</div>
</t>
</t>
<t t-if="order.new_coupon_info and order.new_coupon_info.length !== 0">
<div class="pos-coupon-rewards pt-3">
<t t-foreach="order.new_coupon_info" t-as="coupon_info" t-key="coupon_info.code">
<div class="coupon-container">
<div class="row g-2">
<div class="col-4">
<t t-out="coupon_info['program_name']"/>
</div>
<div class="col-8">
<img style="transform: translateX(13%);" t-att-src="'/report/barcode/Code128/'+coupon_info['code']" class="img-fluid" alt="Barcode"/>
</div>
<div class="col-6">
<span>Until: </span>
<t t-if="coupon_info['expiration_date']">
<t t-out="coupon_info['expiration_date']"/>
</t>
<t t-else="">
no expiration
</t>
</div>
<div class="col-6 text-end">
<t t-out="coupon_info['code']"/>
</div>
</div>
</div>
</t>
</div>
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,60 @@
import { _t } from "@web/core/l10n/translation";
import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen";
import { useService } from "@web/core/utils/hooks";
import { patch } from "@web/core/utils/patch";
/**
* Prevent refunding ewallet/gift card lines.
*/
patch(TicketScreen.prototype, {
setup() {
super.setup(...arguments);
this.notification = useService("notification");
},
async setOrder(order) {
await super.setOrder(...arguments);
if (order && this.pos.models["loyalty.program"]?.length) {
this.pos.updateRewards();
}
},
_onUpdateSelectedOrderline() {
const order = this.getSelectedOrder();
if (!order) {
return this.numberBuffer.reset();
}
const selectedOrderlineId = this.getSelectedOrderlineId();
const orderline = order.lines.find((line) => line.id == selectedOrderlineId);
if (orderline && this._isEWalletGiftCard(orderline)) {
this._showNotAllowedRefundNotification();
return this.numberBuffer.reset();
}
return super._onUpdateSelectedOrderline(...arguments);
},
_prepareAutoRefundOnOrder(order) {
const selectedOrderlineId = this.getSelectedOrderlineId();
const orderline = order.lines.find((line) => line.id == selectedOrderlineId);
if (this._isEWalletGiftCard(orderline)) {
this._showNotAllowedRefundNotification();
return false;
}
return super._prepareAutoRefundOnOrder(...arguments);
},
_showNotAllowedRefundNotification() {
this.notification.add(
_t(
"Refunding a top up or reward product for an eWallet or gift card program is not allowed."
),
5000
);
},
_isEWalletGiftCard(orderline) {
if (orderline.is_reward_line) {
const reward = orderline.reward_id;
const program = reward && reward.program_id;
if (program && ["gift_card", "ewallet"].includes(program.program_type)) {
return true;
}
}
return false;
},
});

View file

@ -0,0 +1,873 @@
import { patch } from "@web/core/utils/patch";
import { PosStore } from "@point_of_sale/app/services/pos_store";
import { _t } from "@web/core/l10n/translation";
import { SelectionPopup } from "@point_of_sale/app/components/popups/selection_popup/selection_popup";
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { Domain, InvalidDomainError } from "@web/core/domain";
import { ask, makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog";
import { Mutex } from "@web/core/utils/concurrency";
import { serializeDate } from "@web/core/l10n/dates";
import { omit } from "@web/core/utils/objects";
let nextId = -1;
const mutex = new Mutex();
const updateRewardsMutex = new Mutex();
const updateProgramsMutex = new Mutex();
const pointsForProgramsCountedRules = {};
const { DateTime } = luxon;
export function loyaltyIdsGenerator() {
return nextId--;
}
function inverted(fn) {
return (arg) => !fn(arg);
}
patch(PosStore.prototype, {
async setup() {
this.couponByLineUuidCache = {};
this.rewardProductByLineUuidCache = {};
await super.setup(...arguments);
await this.updateOrder(this.getOrder());
},
async afterProcessServerData() {
// Remove reward lines that have no reward anymore (could happen if the program got archived)
this.models["pos.order.line"]
.filter((order) => order.is_reward_line && !order.reward_id)
.map((line) => line.delete());
await super.afterProcessServerData(...arguments);
},
async updateOrder(order) {
// Read value to trigger effect
order?.lines?.length;
await this.orderUpdateLoyaltyPrograms();
},
async selectPartner(partner) {
const res = await super.selectPartner(partner);
await this.updateRewards();
return res;
},
async selectPricelist(pricelist) {
await super.selectPricelist(pricelist);
await this.updateRewards();
},
async resetPrograms() {
const order = this.getOrder();
order._resetPrograms();
await this.orderUpdateLoyaltyPrograms();
await this.updateRewards();
},
async orderUpdateLoyaltyPrograms() {
if (!this.getOrder()) {
return;
}
await this.checkMissingCoupons();
await this.updatePrograms();
},
updateRewards() {
// Calls are not expected to take some time besides on the first load + when loyalty programs are made applicable
if (this.models["loyalty.program"].length === 0) {
return;
}
const order = this.getOrder();
if (!order || order.finalized) {
return;
}
updateRewardsMutex.exec(() =>
this.orderUpdateLoyaltyPrograms().then(async () => {
// Try auto claiming rewards
const claimableRewards = order.getClaimableRewards(false, false, true);
let changed = false;
for (const { coupon_id, reward } of claimableRewards) {
if (
reward.program_id.reward_ids.length === 1 &&
!reward.program_id.is_nominative &&
(reward.reward_type !== "product" ||
(reward.reward_type == "product" && !reward.multi_product))
) {
order._applyReward(reward, coupon_id);
changed = true;
}
}
const rewardLinesChanged = order._updateRewardLines();
// Rewards may impact the number of points gained
if (changed || rewardLinesChanged) {
await this.orderUpdateLoyaltyPrograms();
}
})
);
},
async couponForProgram(program) {
const order = this.getOrder();
if (program.is_nominative) {
return await this.fetchLoyaltyCard(program.id, order.getPartner().id);
}
// This type of coupons don't need to really exist up until validating the order, so no need to cache
return this.models["loyalty.card"].create({
id: loyaltyIdsGenerator(),
code: null,
program_id: program,
partner_id: order.partner_id,
points: 0,
});
},
/**
* Update our couponPointChanges, meaning the points/coupons each program give etc.
*/
async updatePrograms() {
return updateProgramsMutex.exec(async () => {
await this._updatePrograms();
});
},
// This method should never be called directly, use updatePrograms() instead
async _updatePrograms() {
const order = this.getOrder();
// 'order.delivery_provider_id' check is used for UrbanPiper orders (as loyalty points and rewards are not allowed for UrbanPiper orders)
if (!order || order.delivery_provider_id) {
return;
}
const changesPerProgram = {};
const programsToCheck = new Set();
// By default include all programs that are considered 'applicable'
for (const program of this.models["loyalty.program"].getAll()) {
if (order._programIsApplicable(program)) {
programsToCheck.add(program.id);
}
}
for (const pe of Object.values(order.uiState.couponPointChanges)) {
if (!changesPerProgram[pe.program_id]) {
changesPerProgram[pe.program_id] = [];
programsToCheck.add(pe.program_id);
}
changesPerProgram[pe.program_id].push(pe);
}
for (const coupon of order._code_activated_coupon_ids) {
programsToCheck.add(coupon.program_id.id);
}
const programs = [...programsToCheck].map((programId) =>
this.models["loyalty.program"].get(programId)
);
const pointsAddedPerProgram = order.pointsForPrograms(programs);
for (const program of this.models["loyalty.program"].getAll()) {
// Future programs may split their points per unit paid (gift cards for example), consider a non applicable program to give no points
const pointsAdded = order._programIsApplicable(program)
? pointsAddedPerProgram[program.id]
: [];
// For programs that apply to both (loyalty) we always add a change of 0 points, if there is none, since it makes it easier to
// track for claimable rewards, and makes sure to load the partner's loyalty card.
if (program.is_nominative && !pointsAdded.length && order.getPartner()) {
pointsAdded.push({ points: 0 });
}
const oldChanges = changesPerProgram[program.id] || [];
// Update point changes for those that exist
for (
let idx = 0;
idx < Math.min(pointsAdded.length, oldChanges.length) && !oldChanges[idx].manual;
idx++
) {
Object.assign(oldChanges[idx], pointsAdded[idx]);
}
if (pointsAdded.length < oldChanges.length || !order._programIsApplicable(program)) {
const removedIds = oldChanges.map((pe) => pe.coupon_id);
order.uiState.couponPointChanges = Object.fromEntries(
Object.entries(order.uiState.couponPointChanges).filter(
([k, pe]) => !removedIds.includes(pe.coupon_id)
)
);
} else if (pointsAdded.length > oldChanges.length) {
const pointsCount = pointsAdded.reduce((acc, pointObj) => {
const { points, barcode = "" } = pointObj;
const key = barcode ? `${points}-${barcode}` : `${points}`;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
oldChanges.forEach((pointObj) => {
const { points, barcode = "" } = pointObj;
const key = barcode ? `${points}-${barcode}` : `${points}`;
if (pointsCount[key] && pointsCount[key] > 0) {
pointsCount[key]--;
}
});
// Get new points added which are not in oldChanges
const newPointsAdded = [];
Object.keys(pointsCount).forEach((key) => {
const [points, barcode = ""] = key.split("-");
while (pointsCount[key] > 0) {
newPointsAdded.push({ points: Number(points), barcode });
pointsCount[key]--;
}
});
for (const pa of newPointsAdded) {
const coupon = await this.couponForProgram(program);
const couponPointChange = {
points: pa.points,
program_id: program.id,
coupon_id: coupon.id,
barcode: pa.barcode,
appliedRules: pointsForProgramsCountedRules[program.id],
};
if (program && program.program_type === "gift_card") {
couponPointChange.product_id = order.getSelectedOrderline()?.product_id.id;
couponPointChange.expiration_date = serializeDate(
luxon.DateTime.now().plus({ year: 1 })
);
couponPointChange.code = order.getSelectedOrderline()?.gift_code;
couponPointChange.partner_id = order.getPartner()?.id;
}
order.uiState.couponPointChanges[coupon.id] = couponPointChange;
}
}
}
// Also remove coupons from _code_activated_coupon_ids if their program applies_on current orders and the program does not give any points
const toUnlink = order._code_activated_coupon_ids.filter(
inverted((coupon) => {
const program = coupon.program_id;
if (
program.applies_on === "current" &&
pointsAddedPerProgram[program.id].length === 0
) {
return false;
}
return true;
})
);
order._code_activated_coupon_ids = [["unlink", ...toUnlink]];
},
async activateCode(code) {
const order = this.getOrder();
const rule = this.models["loyalty.rule"].find(
(rule) =>
rule.mode === "with_code" && (rule.promo_barcode === code || rule.code === code)
);
const partnerId = (
await this.data.call("loyalty.card", "get_loyalty_card_partner_by_code", [code])
)?.[0];
let claimableRewards = null;
let coupon = null;
// If the code belongs to a loyalty card we just set the partner
if (partnerId) {
if (!this.models["res.partner"].get(partnerId)) {
await this.data.read("res.partner", [partnerId]);
}
const partner = this.models["res.partner"].get(partnerId);
order.setPartner(partner);
await this.updateRewards();
} else if (rule) {
const date_order = DateTime.fromSQL(order.date_order);
if (
rule.program_id.date_from &&
date_order < rule.program_id.date_from.startOf("day")
) {
return _t("That promo code program is not yet valid.");
}
if (rule.program_id.date_to && date_order > rule.program_id.date_to.endOf("day")) {
return _t("That promo code program is expired.");
}
const program_pricelists = rule.program_id.pricelist_ids;
if (
program_pricelists.length > 0 &&
(!order.pricelist_id ||
!program_pricelists.some((pr) => pr.id === order.pricelist_id.id))
) {
return _t("That promo code program requires a specific pricelist.");
}
if (order.uiState.codeActivatedProgramRules.includes(rule.id)) {
return _t("That promo code program has already been activated.");
}
order.uiState.codeActivatedProgramRules.push(rule.id);
await this.orderUpdateLoyaltyPrograms();
claimableRewards = order.getClaimableRewards(false, rule.program_id.id);
} else {
if (order._code_activated_coupon_ids.find((coupon) => coupon.code === code)) {
return _t("That coupon code has already been scanned and activated.");
}
const customerId = order.getPartner() ? order.getPartner().id : false;
const { successful, payload } = await this.data.call("pos.config", "use_coupon_code", [
[this.config.id],
code,
order.date_order,
customerId,
order.pricelist_id ? order.pricelist_id.id : false,
]);
if (successful) {
// Allow rejecting a gift card that is not yet paid.
const program = this.models["loyalty.program"].get(payload.program_id);
if (program && program.program_type === "gift_card" && !payload.has_source_order) {
const confirmed = await ask(this.dialog, {
title: _t("Unpaid gift card"),
body: _t(
"This gift card is not linked to any order. Do you really want to apply its reward?"
),
});
if (!confirmed) {
return _t("Unpaid gift card rejected.");
}
}
// TODO JCB: It's possible that the coupon is already loaded. We should check for that.
// - At the moment, creating a new one with existing id creates a duplicate.
coupon = this.models["loyalty.card"].create({
id: payload.coupon_id,
code: code,
program_id: this.models["loyalty.program"].get(payload.program_id),
partner_id: this.models["res.partner"].get(payload.partner_id),
points: payload.points,
points_display: payload.points_display,
// TODO JCB: make the expiration_date work.
// expiration_date: payload.expiration_date,
});
order._code_activated_coupon_ids = [["link", coupon]];
await this.orderUpdateLoyaltyPrograms();
claimableRewards = order.getClaimableRewards(coupon.id);
} else {
return payload.error_message;
}
}
if (claimableRewards && claimableRewards.length === 1) {
if (
claimableRewards[0].reward.reward_type !== "product" ||
!claimableRewards[0].reward.multi_product
) {
order._applyReward(claimableRewards[0].reward, claimableRewards[0].coupon_id);
this.updateRewards();
}
}
if (!rule && order.lines.length === 0 && coupon) {
return _t("%s: %s\nBalance: %s", coupon.program_id.name, code, coupon.points_display);
}
return true;
},
async checkMissingCoupons() {
// This function must stay sequential to avoid potential concurrency errors.
const order = this.getOrder();
await mutex.exec(async () => {
if (!order.invalidCoupons) {
return;
}
order.invalidCoupons = false;
order.uiState.couponPointChanges = Object.fromEntries(
Object.entries(order.uiState.couponPointChanges).filter(([k, pe]) =>
this.models["loyalty.card"].get(pe.coupon_id)
)
);
});
},
async applyDiscount(percent, order = this.getOrder()) {
await super.applyDiscount(...arguments);
await this.updatePrograms();
},
async addLineToCurrentOrder(vals, opt = {}, configure = true) {
if (!vals.product_tmpl_id && vals.product_id) {
vals.product_tmpl_id = vals.product_id.product_tmpl_id;
}
const productTmpl = vals.product_tmpl_id;
const productIds = productTmpl.product_variant_ids.map((v) => v.id);
const order = this.getOrder();
const linkedPrograms = [
...new Set(
productIds
.flatMap(
(id) =>
this.models["loyalty.program"].getBy("trigger_product_ids", id) || []
)
.filter((p) => ["gift_card", "ewallet"].includes(p.program_type))
),
];
let selectedProgram = null;
if (linkedPrograms.length > 1) {
selectedProgram = await makeAwaitable(this.dialog, SelectionPopup, {
title: _t("Select program"),
list: linkedPrograms.map((program) => ({
id: program.id,
item: program,
label: program.name,
})),
});
if (!selectedProgram) {
return;
}
} else if (linkedPrograms.length === 1) {
selectedProgram = linkedPrograms[0];
}
const orderTotal = this.getOrder().priceIncl;
if (
selectedProgram &&
["gift_card", "ewallet"].includes(selectedProgram.program_type) &&
orderTotal < 0
) {
opt.price_unit = -orderTotal;
}
if (selectedProgram && selectedProgram.program_type == "gift_card") {
const shouldProceed = await this._setupGiftCardOptions(selectedProgram, opt);
if (!shouldProceed) {
return;
}
} else if (selectedProgram && selectedProgram.program_type == "ewallet") {
const shouldProceed = await this.setupEWalletOptions(selectedProgram, opt);
if (!shouldProceed) {
return;
}
}
const potentialRewards = this.getPotentialFreeProductRewards();
// move price_unit from opt to vals
if (opt.price_unit !== undefined) {
vals.price_unit = opt.price_unit;
delete opt.price_unit;
}
const result = await super.addLineToCurrentOrder(vals, opt, configure);
if (!result) {
return;
}
const rewardsToApply = [];
for (const reward of potentialRewards) {
for (const reward_product_id of reward.reward.reward_product_ids) {
if (result.product_id.id == reward_product_id.id) {
rewardsToApply.push(reward);
}
}
}
await this.updatePrograms();
if (rewardsToApply.length == 1) {
const reward = rewardsToApply[0];
order._applyReward(reward.reward, reward.coupon_id, {
product: result.product_id,
});
}
this.updateRewards();
return result;
},
/**
* Sets up the options for the gift card product.
* @param {object} program
* @param {object} options
* @returns {Promise<boolean>} whether to proceed with adding the product or not
*/
async _setupGiftCardOptions(program, options) {
options.quantity = 1;
options.merge = false;
options.eWalletGiftCardProgram = program;
return true;
},
async setupEWalletOptions(program, options) {
options.quantity = 1;
options.merge = false;
options.eWalletGiftCardProgram = program;
return true;
},
/**
* Returns the reward such that when its reward product is added
* in the order, it will be added as free. That is, when added,
* it comes with the corresponding reward product line.
*/
async pay() {
const currentOrder = this.getOrder();
const eWalletLine = currentOrder
.getOrderlines()
.find((line) => line.getEWalletGiftCardProgramType() === "ewallet");
if (eWalletLine && !currentOrder.getPartner()) {
const confirmed = await ask(this.dialog, {
title: _t("Customer needed"),
body: _t("eWallet requires a customer to be selected"),
});
if (confirmed) {
await this.selectPartner();
}
} else {
return super.pay(...arguments);
}
},
getPotentialFreeProductRewards() {
const order = this.getOrder();
const result = [];
if (!order) {
return result;
}
const allCouponPrograms = Object.values(order.uiState.couponPointChanges)
.map((pe) => ({
program_id: pe.program_id,
coupon_id: pe.coupon_id,
}))
.concat(
order._code_activated_coupon_ids.map((coupon) => ({
program_id: coupon.program_id.id,
coupon_id: coupon.id,
}))
);
for (const couponProgram of allCouponPrograms) {
const program = this.models["loyalty.program"].get(couponProgram.program_id);
if (
program.pricelist_ids.length > 0 &&
(!order.pricelist_id ||
!program.pricelist_ids.some((pl) => pl.id === order.pricelist_id.id))
) {
continue;
}
const points = order._getRealCouponPoints(couponProgram.coupon_id);
const hasLine = order.lines.filter((line) => !line.is_reward_line).length > 0;
for (const reward of program.reward_ids.filter(
(reward) => reward.reward_type == "product"
)) {
if (points < reward.required_points) {
continue;
}
// Loyalty program (applies_on == 'both') should needs an orderline before it can apply a reward.
const considerTheReward =
program.applies_on !== "both" || (program.applies_on == "both" && hasLine);
if (reward.reward_type === "product" && considerTheReward) {
for (const { id } of reward.reward_product_ids) {
const product = this.models["product.product"].get(id);
const potentialQty = order._computePotentialFreeProductQty(
reward,
product,
points
);
if (potentialQty > 0) {
result.push({
coupon_id: couponProgram.coupon_id,
reward: reward,
potentialQty,
});
break;
}
}
}
}
}
return result;
},
//@override
async processServerData() {
await super.processServerData();
this.partnerId2CouponIds = {};
this.computeDiscountProductIdsForAllRewards();
this.models["product.product"].addEventListener(
"create",
this.computeDiscountProductIdsForAllRewards.bind(this)
);
for (const rule of this.models["loyalty.rule"].getAll()) {
rule.validProductIds = new Set(rule.raw.valid_product_ids);
}
this.models["loyalty.card"].addEventListener("create", (records) => {
records = records.ids.map((record) => this.models["loyalty.card"].get(record));
this.computePartnerCouponIds(records);
});
this.computePartnerCouponIds();
},
computeDiscountProductIdsForAllRewards(data) {
const productModel = this.models["product.product"].toRaw(); // Limit the number of reactivity proxy instances
const products = data ? productModel.readMany(data.ids) : productModel.getAll();
for (const reward of this.models["loyalty.reward"].getAll()) {
this.computeDiscountProductIds(reward, products);
}
},
computePartnerCouponIds(loyaltyCards = null) {
const cards = loyaltyCards || this.models["loyalty.card"].getAll();
for (const card of cards) {
if (!card.partner_id || card.id < 0) {
continue;
}
if (!this.partnerId2CouponIds[card.partner_id.id]) {
this.partnerId2CouponIds[card.partner_id.id] = new Set();
}
this.partnerId2CouponIds[card.partner_id.id].add(card.id);
}
},
computeDiscountProductIds(reward, products) {
const reward_product_domain = JSON.parse(reward.reward_product_domain);
if (!reward_product_domain) {
return;
}
const domain = new Domain(reward_product_domain);
try {
const existingProduct = reward.all_discount_product_ids;
reward.all_discount_product_ids = [
...existingProduct,
...products.filter((p) => domain.contains(p.raw)),
];
} catch (error) {
if (!(error instanceof InvalidDomainError || error instanceof TypeError)) {
throw error;
}
const index = this.models["loyalty.reward"].indexOf(reward);
if (index != -1) {
this.dialog.add(AlertDialog, {
title: _t("A reward could not be loaded"),
body: _t(
'The reward "%s" contain an error in its domain, your domain must be compatible with the PoS client',
this.models["loyalty.reward"].getAll()[index].description
),
showReloadButton: true,
});
reward.delete();
}
}
},
async initServerData() {
await super.initServerData(...arguments);
if (this.selectedOrderUuid) {
this.updateRewards();
}
},
/**
* Fetches `loyalty.card` records from the server and adds/updates them in our cache.
*
* @param {domain} domain For the search
* @param {int} limit Default to 1
*/
async fetchCoupons(domain, limit = 1) {
return await this.data.searchRead(
"loyalty.card",
domain,
this.data.fields["loyalty.card"],
{ limit }
);
},
/**
* Fetches a loyalty card for the given program and partner, put in cache afterwards
* if a matching card is found in the cache, that one is used instead.
* If no card is found a local only card will be created until the order is validated.
*
* @param {int} programId
* @param {int} partnerId
*/
async fetchLoyaltyCard(programId, partnerId) {
const coupon = this.models["loyalty.card"].find(
(c) => c.partner_id?.id === partnerId && c.program_id?.id === programId
);
if (coupon) {
return coupon;
}
const fetchedCoupons = await this.fetchCoupons([
["partner_id", "=", partnerId],
["program_id", "=", programId],
]);
let dbCoupon = fetchedCoupons.length > 0 ? fetchedCoupons[0] : null;
if (!dbCoupon) {
dbCoupon = await this.models["loyalty.card"].create({
id: loyaltyIdsGenerator(),
code: null,
program_id: this.models["loyalty.program"].get(programId),
partner_id: this.models["res.partner"].get(partnerId),
points: 0,
expiration_date: null,
});
}
return dbCoupon;
},
getLoyaltyCards(partner) {
const loyaltyCards = [];
if (this.partnerId2CouponIds[partner.id]) {
this.partnerId2CouponIds[partner.id].forEach((couponId) =>
loyaltyCards.push(this.models["loyalty.card"].get(couponId))
);
}
return loyaltyCards;
},
/**
* IMPROVEMENT: It would be better to update the local order object instead of creating a new one.
* - This way, we don't need to remember the lines linked to negative coupon ids and relink them after pushing the order.
*/
async preSyncAllOrders(orders) {
await super.preSyncAllOrders(orders);
for (const order of orders) {
Object.assign(
this.couponByLineUuidCache,
order.lines.reduce((agg, line) => {
if (line.coupon_id && line.coupon_id.id < 0) {
return { ...agg, [line.uuid]: line.coupon_id.id };
} else {
return agg;
}
}, {})
);
Object.assign(
this.rewardProductByLineUuidCache,
order.lines.reduce((agg, line) => {
if (line._reward_product_id) {
return { ...agg, [line.uuid]: line._reward_product_id.id };
} else {
return agg;
}
}, {})
);
}
},
async postSyncAllOrders(orders) {
super.postSyncAllOrders(orders);
for (const order of orders) {
for (const line of order.lines) {
if (line.uuid in this.couponByLineUuidCache) {
line.coupon_id = this.models["loyalty.card"].get(
this.couponByLineUuidCache[line.uuid]
);
}
}
for (const line of order.lines) {
if (line.uuid in this.rewardProductByLineUuidCache) {
line._reward_product_id = this.models["product.product"].get(
this.rewardProductByLineUuidCache[line.uuid]
);
}
}
if (!["draft", "cancel"].includes(order.state)) {
await this.postProcessLoyalty(order);
}
}
},
async postProcessLoyalty(order) {
// Compile data for our function
const ProgramModel = this.models["loyalty.program"];
const rewardLines = order._get_reward_lines();
const partner = order.getPartner();
let couponData = Object.values(order.uiState.couponPointChanges).reduce((agg, pe) => {
agg[pe.coupon_id] = Object.assign({}, pe, {
points: pe.points - order._getPointsCorrection(ProgramModel.get(pe.program_id)),
});
const program = ProgramModel.get(pe.program_id);
if (
(program.is_nominative || program.program_type == "next_order_coupons") &&
partner
) {
agg[pe.coupon_id].partner_id = partner.id;
}
if (program.program_type != "loyalty") {
agg[pe.coupon_id].expiration_date = program.date_to || pe.expiration_date;
}
return agg;
}, {});
for (const line of rewardLines) {
const reward = line.reward_id;
const couponId = line.coupon_id.id;
if (!couponData[couponId]) {
couponData[couponId] = {
points: 0,
program_id: reward.program_id.id,
coupon_id: couponId,
barcode: false,
};
if (reward.program_type != "loyalty") {
couponData[couponId].expiration_date = reward.program_id.date_to;
}
}
if (!couponData[couponId].line_codes) {
couponData[couponId].line_codes = [];
}
if (!couponData[couponId].line_codes.includes(line.reward_identifier_code)) {
!couponData[couponId].line_codes.push(line.reward_identifier_code);
}
couponData[couponId].points -= line.points_cost;
}
// We actually do not care about coupons for 'current' programs that did not claim any reward, they will be lost if not validated
couponData = Object.fromEntries(
Object.entries(couponData)
.filter(([key, value]) => {
const program = ProgramModel.get(value.program_id);
if (program.applies_on === "current") {
return value.line_codes && value.line_codes.length;
}
return true;
})
.map(([key, value]) => [key, omit(value, "appliedRules")])
);
if (Object.keys(couponData || {}).length > 0) {
const payload = await this.data.call("pos.order", "confirm_coupon_programs", [
order.id,
couponData,
]);
if (payload.coupon_updates) {
for (const couponUpdate of payload.coupon_updates) {
// The following code is a workaround to update the id of an existing record.
// It's so ugly.
// FIXME: Find a better way of updating the id of an existing record.
// It would be better if we can do this:
// const coupon = this.models["loyalty.card"].get(couponUpdate.old_id);
// coupon.update({ id: couponUpdate.id, points: couponUpdate.points })
if (couponUpdate.old_id == couponUpdate.id) {
// just update the points
const coupon = this.models["loyalty.card"].get(couponUpdate.id);
if (!coupon) {
await this.data.read("loyalty.card", [couponUpdate.id]);
} else {
coupon.points = couponUpdate.points;
}
} else {
// create a new coupon and delete the old one
const coupon = this.models["loyalty.card"].create({
id: couponUpdate.id,
code: couponUpdate.code,
program_id: this.models["loyalty.program"].get(couponUpdate.program_id),
partner_id: this.models["res.partner"].get(couponUpdate.partner_id),
points: couponUpdate.points,
});
// Before deleting the old coupon, update the order lines that use it.
for (const line of order.lines) {
if (line.coupon_id?.id == couponUpdate.old_id) {
line.coupon_id = coupon;
}
}
this.models["loyalty.card"].get(couponUpdate.old_id)?.delete();
}
}
}
// Update the usage count since it is checked based on local data
if (payload.program_updates) {
for (const programUpdate of payload.program_updates) {
const program = ProgramModel.get(programUpdate.program_id);
if (program) {
program.total_order_count = programUpdate.usages;
}
}
}
if (payload.coupon_report && Object.keys(payload.coupon_report).length > 0) {
for (const [actionId, active_ids] of Object.entries(payload.coupon_report)) {
await this.env.services.report.doAction(actionId, active_ids);
}
order.has_pdf_gift_card = Object.keys(payload.coupon_report).length > 0;
}
if (payload.new_coupon_info?.length) {
order.new_coupon_info = payload.new_coupon_info;
}
}
},
});

View file

@ -0,0 +1,68 @@
import OrderPaymentValidation from "@point_of_sale/app/utils/order_payment_validation";
import { patch } from "@web/core/utils/patch";
import { _t } from "@web/core/l10n/translation";
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
patch(OrderPaymentValidation.prototype, {
async validateOrder(isForceValidate) {
const pointChanges = {};
const newCodes = [];
for (const pe of Object.values(this.order.uiState.couponPointChanges)) {
if (pe.coupon_id > 0) {
pointChanges[pe.coupon_id] = pe.points;
} else if (pe.barcode && !pe.giftCardId) {
// New coupon with a specific code, validate that it does not exist
newCodes.push(pe.barcode);
}
}
for (const line of this.order._get_reward_lines()) {
if (line.coupon_id.id < 1) {
continue;
}
if (!pointChanges[line.coupon_id.id]) {
pointChanges[line.coupon_id.id] = -line.points_cost;
} else {
pointChanges[line.coupon_id.id] -= line.points_cost;
}
}
if (!(await this.isOrderValid(isForceValidate))) {
return;
}
// No need to do an rpc if no existing coupon is being used.
if (Object.keys(pointChanges || {}).length > 0 || newCodes.length) {
try {
const { successful, payload } = await this.pos.data.call(
"pos.order",
"validate_coupon_programs",
[[], pointChanges, newCodes]
);
// Payload may contain the points of the concerned coupons to be updated in case of error. (So that rewards can be corrected)
if (payload && payload.updated_points) {
for (const pointChange of Object.entries(payload.updated_points)) {
const coupon = this.pos.models["loyalty.card"].get(pointChange[0]);
if (coupon) {
coupon.points = pointChange[1];
}
}
}
if (payload && payload.removed_coupons) {
for (const couponId of payload.removed_coupons) {
const coupon = this.pos.models["loyalty.card"].get(couponId);
coupon && coupon.delete();
}
}
if (!successful) {
this.pos.dialog.add(AlertDialog, {
title: _t("Error validating rewards"),
body: payload.message,
});
return;
}
} catch {
// Do nothing with error, while this validation step is nice for error messages
// it should not be blocking.
}
}
await super.validateOrder(...arguments);
},
});

View file

@ -1,136 +1,3 @@
.pos .popups .gift-card-popup {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
text-align: center;
margin: 1em;
.gift-card-container {
display: flex;
padding: 5px;
font-weight: bold;
text-align: center;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: space-between;
}
.gift-card-footer {
text-align: center;
}
.gift-card-input-amount {
border-bottom: 1px solid;
background-color: #F0EEEE;
box-shadow: none;
text-align: right;
width: 80%;
}
.gift-card-input-code {
border-bottom: 1px solid;
background-color: #F0EEEE;
box-shadow: none;
text-align: center;
width: 80%;
}
.gift-card-button {
width: auto !important;
height: auto !important;
padding: 10px;
}
.gift-card-button-confirm {
flex-grow: 1;
height: auto !important;
padding: 10px;
}
.gift-card-input-container {
width: 80%;
}
.footer .gift-card-footer-button {
margin-left: 5px;
width: 70px;
}
.gift-card-error {
width: 100%;
background-color: rgba(255, 76, 76, 0.5);
}
}
.pos .order {
.active-programs {
padding-top: 0.5em;
font-weight: normal;
font-size: 80%;
text-align: right;
padding: 0 1rem .5rem;
.title {
padding-top: 0.8em;
padding-bottom: 0.2em;
font-weight: bold;
}
}
.orderline.program-reward {
font-style: italic;
}
.summary {
.loyalty-points {
float: left;
padding: 10px;
max-width: 216px;
text-align: left;
color: #6EC89B;
background: rgba(110, 200, 155, 0.17);
border-radius: 3px;
}
.loyalty-points-title {
font-size: small;
}
.loyalty-points-balance {
color: #714B67;
}
.loyalty-points-spent {
color: #C86E6E;
}
.loyalty-points-total {
border-top: solid 2px;
text-align: center;
padding-top: 4px;
margin-top: 4px;
}
}
}
.pos-receipt .pos-coupon-rewards {
text-align: center;
padding: 1em;
}
.pos-coupon-rewards .coupon-container {
padding-top: 1em;
font-size: 75%;
}
// Seems unused
.loyalty .subtitle{
text-align: center;
}
.loyalty .title {
font-size: 20px;
text-align: center;
}

View file

@ -1,38 +0,0 @@
/** @odoo-module **/
import PosComponent from 'point_of_sale.PosComponent';
import ProductScreen from 'point_of_sale.ProductScreen';
import Registries from 'point_of_sale.Registries';
import { useListener } from "@web/core/utils/hooks";
export class PromoCodeButton extends PosComponent {
setup() {
super.setup();
useListener('click', this.onClick);
}
async onClick() {
let { confirmed, payload: code } = await this.showPopup('TextInputPopup', {
title: this.env._t('Enter Code'),
startingValue: '',
placeholder: this.env._t('Gift card or Discount code'),
});
if (confirmed) {
code = code.trim();
if (code !== '') {
this.env.pos.get_order().activateCode(code);
}
}
}
}
PromoCodeButton.template = 'PromoCodeButton';
ProductScreen.addControlButton({
component: PromoCodeButton,
condition: function () {
return this.env.pos.programs.some(p => ['coupons', 'promotion', 'gift_card', 'promo_code', 'next_order_coupons'].includes(p.program_type));
}
});
Registries.Component.add(PromoCodeButton);

View file

@ -1,28 +0,0 @@
/** @odoo-module **/
import PosComponent from 'point_of_sale.PosComponent';
import ProductScreen from 'point_of_sale.ProductScreen';
import Registries from 'point_of_sale.Registries';
import { useListener } from "@web/core/utils/hooks";
export class ResetProgramsButton extends PosComponent {
setup() {
super.setup();
useListener('click', this.onClick);
}
async onClick() {
this.env.pos.get_order()._resetPrograms();
}
}
ResetProgramsButton.template = 'ResetProgramsButton';
ProductScreen.addControlButton({
component: ResetProgramsButton,
condition: function () {
return this.env.pos.programs.some(p => ['coupons', 'promotion'].includes(p.program_type));
}
});
Registries.Component.add(ResetProgramsButton);

View file

@ -1,132 +0,0 @@
/** @odoo-module **/
import { Gui } from 'point_of_sale.Gui';
import PosComponent from 'point_of_sale.PosComponent';
import ProductScreen from 'point_of_sale.ProductScreen';
import Registries from 'point_of_sale.Registries';
import { useListener } from "@web/core/utils/hooks";
export class RewardButton extends PosComponent {
setup() {
super.setup()
useListener('click', this.onClick);
}
/**
* If rewards are the same, prioritize the one from freeProductRewards.
* Make sure that the reward is claimable first.
*/
_mergeFreeProductRewards(freeProductRewards, potentialFreeProductRewards) {
const result = []
for (const reward of potentialFreeProductRewards) {
if (!freeProductRewards.find(item => item.reward.id === reward.reward.id)) {
result.push(reward);
}
}
return freeProductRewards.concat(result);
}
_getPotentialRewards() {
const order = this.env.pos.get_order();
// Claimable rewards excluding those from eWallet programs.
// eWallet rewards are handled in the eWalletButton.
let rewards = [];
if (order) {
const claimableRewards = order.getClaimableRewards();
rewards = claimableRewards.filter(({ reward }) => reward.program_id.program_type !== 'ewallet');
}
const discountRewards = rewards.filter(({ reward }) => reward.reward_type == 'discount');
const freeProductRewards = rewards.filter(({ reward }) => reward.reward_type == 'product');
const potentialFreeProductRewards = order.getPotentialFreeProductRewards();
return discountRewards.concat(this._mergeFreeProductRewards(freeProductRewards, potentialFreeProductRewards));
}
hasClaimableRewards() {
return this._getPotentialRewards().length > 0;
}
/**
* Applies the reward on the current order, if multiple products can be claimed opens a popup asking for which one.
*
* @param {Object} reward
* @param {Integer} coupon_id
*/
async _applyReward(reward, coupon_id, potentialQty) {
const order = this.env.pos.get_order();
order.disabledRewards.delete(reward.id);
const args = {};
if (reward.reward_type === 'product' && reward.multi_product) {
const productsList = reward.reward_product_ids.map((product_id) => ({
id: product_id,
label: this.env.pos.db.get_product_by_id(product_id).display_name,
item: product_id,
}));
const { confirmed, payload: selectedProduct } = await this.showPopup('SelectionPopup', {
title: this.env._t('Please select a product for this reward'),
list: productsList,
});
if (!confirmed) {
return false;
}
args['product'] = selectedProduct;
}
if (
(reward.reward_type == 'product' && reward.program_id.applies_on !== 'both') ||
(reward.program_id.applies_on == 'both' && potentialQty)
) {
const product = this.env.pos.db.get_product_by_id(args['product'] || reward.reward_product_ids[0]);
this.trigger(
'click-product',
{ product, quantity: potentialQty }
);
return true;
} else {
const result = order._applyReward(reward, coupon_id, args);
if (result !== true) {
// Returned an error
Gui.showNotification(result);
}
order._updateRewards();
return result;
}
}
async onClick() {
const rewards = this._getPotentialRewards();
if (rewards.length === 0) {
await this.showPopup('ErrorPopup', {
title: this.env._t('No rewards available.'),
body: this.env._t('There are no rewards claimable for this customer.')
});
return false;
} else if (rewards.length === 1) {
return this._applyReward(rewards[0].reward, rewards[0].coupon_id, rewards[0].potentialQty);
} else {
const rewardsList = rewards.map((reward) => ({
id: reward.reward.id,
label: reward.reward.description,
item: reward,
}));
const { confirmed, payload: selectedReward } = await this.showPopup('SelectionPopup', {
title: this.env._t('Please select a reward'),
list: rewardsList,
});
if (confirmed) {
return this._applyReward(selectedReward.reward, selectedReward.coupon_id, selectedReward.potentialQty);
}
}
return false;
}
}
RewardButton.template = 'RewardButton';
ProductScreen.addControlButton({
component: RewardButton,
condition: function() {
return this.env.pos.programs.length > 0;
}
});
Registries.Component.add(RewardButton);

View file

@ -1,107 +0,0 @@
/** @odoo-module **/
import PosComponent from 'point_of_sale.PosComponent';
import ProductScreen from 'point_of_sale.ProductScreen';
import Registries from 'point_of_sale.Registries';
export class eWalletButton extends PosComponent {
_getEWalletRewards(order) {
const claimableRewards = order.getClaimableRewards();
return claimableRewards.filter((reward_line) => {
const coupon = this.env.pos.couponCache[reward_line.coupon_id];
return coupon && reward_line.reward.program_id.program_type == 'ewallet' && !coupon.isExpired();
});
}
_getEWalletPrograms() {
return this.env.pos.programs.filter((p) => p.program_type == 'ewallet');
}
async _onClickWalletButton() {
const order = this.env.pos.get_order();
const eWalletPrograms = this.env.pos.programs.filter((p) => p.program_type == 'ewallet');
const orderTotal = order.get_total_with_tax();
const eWalletRewards = this._getEWalletRewards(order);
if (eWalletRewards.length === 0 && orderTotal >= 0) {
this.showPopup('ErrorPopup', {
title: this.env._t('No valid eWallet found'),
body: this.env._t('You either have not created an eWallet or all your eWallets have expired.'),
});
return;
}
if (orderTotal < 0 && eWalletPrograms.length >= 1) {
let selectedProgram = null;
if (eWalletPrograms.length == 1) {
selectedProgram = eWalletPrograms[0];
} else {
const { confirmed, payload } = await this.showPopup('SelectionPopup', {
title: this.env._t('Refund with eWallet'),
list: eWalletPrograms.map((program) => ({
id: program.id,
item: program,
label: program.name,
})),
});
if (confirmed) {
selectedProgram = payload;
}
}
if (selectedProgram) {
const eWalletProduct = this.env.pos.db.get_product_by_id(selectedProgram.trigger_product_ids[0]);
order.add_product(eWalletProduct, {
price: -orderTotal,
merge: false,
eWalletGiftCardProgram: selectedProgram,
});
}
} else if (eWalletRewards.length >= 1) {
let eWalletReward = null;
if (eWalletRewards.length == 1) {
eWalletReward = eWalletRewards[0];
} else {
const { confirmed, payload } = await this.showPopup('SelectionPopup', {
title: this.env._t('Use eWallet to pay'),
list: eWalletRewards.map(({ reward, coupon_id }) => ({
id: reward.id,
item: { reward, coupon_id },
label: `${reward.description} (${reward.program_id.name})`,
})),
});
if (confirmed) {
eWalletReward = payload;
}
}
if (eWalletReward) {
const result = order._applyReward(eWalletReward.reward, eWalletReward.coupon_id, {});
if (result !== true) {
// Returned an error
this.showPopup('ErrorPopup', {
title: this.env._t('Error'),
body: result,
});
}
order._updateRewards();
}
}
}
_shouldBeHighlighted(orderTotal, eWalletPrograms, eWalletRewards) {
return (orderTotal < 0 && eWalletPrograms.length >= 1) || eWalletRewards.length >= 1;
}
_getText(orderTotal, eWalletPrograms, eWalletRewards) {
if (orderTotal < 0 && eWalletPrograms.length >= 1) {
return this.env._t('eWallet Refund');
} else if (eWalletRewards.length >= 1) {
return this.env._t('eWallet Pay');
} else {
return this.env._t('eWallet');
}
}
}
eWalletButton.template = 'point_of_sale.eWalletButton';
ProductScreen.addControlButton({
component: eWalletButton,
condition: function () {
return this.env.pos.programs.filter((p) => p.program_type == 'ewallet').length > 0;
},
});
Registries.Component.add(eWalletButton);

View file

@ -1,14 +0,0 @@
/** @odoo-module **/
import OrderSummary from 'point_of_sale.OrderSummary';
import Registries from 'point_of_sale.Registries';
export const PosLoyaltyOrderSummary = (OrderSummary) =>
class PosLoyaltyOrderSummary extends OrderSummary {
getLoyaltyPoints() {
const order = this.env.pos.get_order();
return order.getLoyaltyPoints();
}
};
Registries.Component.extend(OrderSummary, PosLoyaltyOrderSummary)

View file

@ -1,28 +0,0 @@
/** @odoo-module **/
import Orderline from 'point_of_sale.Orderline';
import Registries from 'point_of_sale.Registries';
export const PosLoyaltyOrderline = (Orderline) =>
class extends Orderline{
get addedClasses() {
return Object.assign({'program-reward': this.props.line.is_reward_line}, super.addedClasses);
}
_isGiftCardOrEWalletReward() {
const coupon = this.env.pos.couponCache[this.props.line.coupon_id];
if (coupon) {
const program = this.env.pos.program_by_id[coupon.program_id]
return ['ewallet', 'gift_card'].includes(program.program_type) && this.props.line.is_reward_line;
}
return false;
}
_getGiftCardOrEWalletBalance() {
const coupon = this.env.pos.couponCache[this.props.line.coupon_id];
if (coupon) {
return this.env.pos.format_currency(coupon.balance);
}
return this.env.pos.format_currency(0);
}
};
Registries.Component.extend(Orderline, PosLoyaltyOrderline);

View file

@ -1,25 +0,0 @@
odoo.define('pos_loyalty.PartnerLine', function (require) {
'use strict';
const PartnerLine = require('point_of_sale.PartnerLine');
const Registries = require('point_of_sale.Registries');
const PosLoyaltyPartnerLine = (PartnerLine) =>
class extends PartnerLine {
_getLoyaltyPointsRepr(loyaltyCard) {
const program = this.env.pos.program_by_id[loyaltyCard.program_id];
if (program.program_type === 'ewallet') {
return `${program.name}: ${this.env.pos.format_currency(loyaltyCard.balance)}`;
}
const balanceRepr = this.env.pos.format_pr(loyaltyCard.balance, 0.01);
if (program.portal_visible) {
return `${balanceRepr} ${program.portal_point_name}`;
}
return _.str.sprintf(this.env._t('%s Points'), balanceRepr);
}
};
Registries.Component.extend(PartnerLine, PosLoyaltyPartnerLine);
return PartnerLine;
});

View file

@ -1,21 +0,0 @@
odoo.define('pos_loyalty.PartnerListScreen', function (require) {
'use strict';
const PartnerListScreen = require('point_of_sale.PartnerListScreen');
const Registries = require('point_of_sale.Registries');
const PosLoyaltyPartnerListScreen = (PartnerListScreen) =>
class extends PartnerListScreen {
/**
* Needs to be set to true to show the loyalty points in the partner list.
* @override
*/
get isBalanceDisplayed() {
return true;
}
};
Registries.Component.extend(PartnerListScreen, PosLoyaltyPartnerListScreen);
return PartnerListScreen;
});

View file

@ -1,172 +0,0 @@
/** @odoo-module **/
import PaymentScreen from 'point_of_sale.PaymentScreen';
import Registries from 'point_of_sale.Registries';
import session from 'web.session';
import { PosLoyaltyCard } from '@pos_loyalty/js/Loyalty';
export const PosLoyaltyPaymentScreen = (PaymentScreen) =>
class extends PaymentScreen {
//@override
async validateOrder(isForceValidate) {
const pointChanges = {};
const newCodes = [];
for (const pe of Object.values(this.currentOrder.couponPointChanges)) {
if (pe.coupon_id > 0) {
pointChanges[pe.coupon_id] = pe.points;
} else if (pe.barcode && !pe.giftCardId) {
// New coupon with a specific code, validate that it does not exist
newCodes.push(pe.barcode);
}
}
for (const line of this.currentOrder._get_reward_lines()) {
if (line.coupon_id < 1) {
continue;
}
if (!pointChanges[line.coupon_id]) {
pointChanges[line.coupon_id] = -line.points_cost;
} else {
pointChanges[line.coupon_id] -= line.points_cost;
}
}
if (!await this._isOrderValid(isForceValidate)) {
return;
}
// No need to do an rpc if no existing coupon is being used.
if (!_.isEmpty(pointChanges) || newCodes.length) {
try {
const {successful, payload} = await this.rpc({
model: 'pos.order',
method: 'validate_coupon_programs',
args: [[], pointChanges, newCodes],
kwargs: { context: session.user_context },
});
// Payload may contain the points of the concerned coupons to be updated in case of error. (So that rewards can be corrected)
if (payload && payload.updated_points) {
for (const pointChange of Object.entries(payload.updated_points)) {
if (this.env.pos.couponCache[pointChange[0]]) {
this.env.pos.couponCache[pointChange[0]].balance = pointChange[1];
}
}
}
if (payload && payload.removed_coupons) {
for (const couponId of payload.removed_coupons) {
if (this.env.pos.couponCache[couponId]) {
delete this.env.pos.couponCache[couponId];
}
}
this.currentOrder.codeActivatedCoupons = this.currentOrder.codeActivatedCoupons.filter((coupon) => !payload.removed_coupons.includes(coupon.id));
}
if (!successful) {
this.showPopup('ErrorPopup', {
title: this.env._t('Error validating rewards'),
body: payload.message,
});
return;
}
} catch (_e) {
// Do nothing with error, while this validation step is nice for error messages
// it should not be blocking.
}
}
await super.validateOrder(...arguments);
}
/**
* @override
*/
async _postPushOrderResolve(order, server_ids) {
// Compile data for our function
const rewardLines = order._get_reward_lines();
const partner = order.get_partner();
let couponData = Object.values(order.couponPointChanges).reduce((agg, pe) => {
agg[pe.coupon_id] = Object.assign({}, pe, {
points: pe.points - order._getPointsCorrection(this.env.pos.program_by_id[pe.program_id]),
});
const program = this.env.pos.program_by_id[pe.program_id];
if (program.is_nominative && partner) {
agg[pe.coupon_id].partner_id = partner.id;
}
if (program.program_type != 'loyalty') {
agg[pe.coupon_id].date_to = program.date_to;
}
return agg;
}, {});
for (const line of rewardLines) {
const reward = this.env.pos.reward_by_id[line.reward_id];
if (!couponData[line.coupon_id]) {
couponData[line.coupon_id] = {
points: 0,
program_id: reward.program_id.id,
coupon_id: line.coupon_id,
barcode: false,
}
if (reward.program_type != 'loyalty') {
couponData[line.coupon_id].date_to = reward.program_id.date_to;
}
}
if (!couponData[line.coupon_id].line_codes) {
couponData[line.coupon_id].line_codes = [];
}
if (!couponData[line.coupon_id].line_codes.includes(line.reward_identifier_code)) {
!couponData[line.coupon_id].line_codes.push(line.reward_identifier_code);
}
couponData[line.coupon_id].points -= line.points_cost;
}
// We actually do not care about coupons for 'current' programs that did not claim any reward, they will be lost if not validated
couponData = Object.fromEntries(Object.entries(couponData).filter(([key, value]) => {
const program = this.env.pos.program_by_id[value.program_id];
if (program.applies_on === 'current') {
return value.line_codes && value.line_codes.length;
}
return true;
}));
if (!_.isEmpty(couponData)) {
const payload = await this.rpc({
model: 'pos.order',
method: 'confirm_coupon_programs',
args: [server_ids, couponData],
kwargs: { context: session.user_context },
});
if (payload.coupon_updates) {
for (const couponUpdate of payload.coupon_updates) {
let dbCoupon = this.env.pos.couponCache[couponUpdate.old_id];
if (dbCoupon) {
dbCoupon.id = couponUpdate.id;
dbCoupon.balance = couponUpdate.points;
dbCoupon.code = couponUpdate.code;
} else {
dbCoupon = new PosLoyaltyCard(
couponUpdate.code, couponUpdate.id, couponUpdate.program_id, couponUpdate.partner_id, couponUpdate.points);
this.env.pos.partnerId2CouponIds[partner.id] = this.env.pos.partnerId2CouponIds[partner.id] || new Set();
this.env.pos.partnerId2CouponIds[partner.id].add(couponUpdate.id);
}
delete this.env.pos.couponCache[couponUpdate.old_id];
this.env.pos.couponCache[couponUpdate.id] = dbCoupon;
}
}
// Update the usage count since it is checked based on local data
if (payload.program_updates) {
for (const programUpdate of payload.program_updates) {
const program = this.env.pos.program_by_id[programUpdate.program_id];
if (program) {
program.total_order_count = programUpdate.usages;
}
}
}
if (payload.coupon_report) {
for (const report_entry of Object.entries(payload.coupon_report)) {
await this.env.legacyActionManager.do_action(report_entry[0], {
additional_context: {
active_ids: report_entry[1],
}
});
}
}
order.new_coupon_info = payload.new_coupon_info;
}
return super._postPushOrderResolve(order, server_ids);
}
};
Registries.Component.extend(PaymentScreen, PosLoyaltyPaymentScreen);

View file

@ -1,250 +0,0 @@
/** @odoo-module **/
import ProductScreen from 'point_of_sale.ProductScreen';
import Registries from 'point_of_sale.Registries';
import { useBarcodeReader } from 'point_of_sale.custom_hooks';
export const PosLoyaltyProductScreen = (ProductScreen) =>
class extends ProductScreen {
setup() {
super.setup();
useBarcodeReader({
coupon: this._onCouponScan,
});
}
async _onClickPay() {
const order = this.env.pos.get_order();
const eWalletLine = order.get_orderlines().find(line => line.getEWalletGiftCardProgramType() === 'ewallet');
if (eWalletLine && !order.get_partner()) {
const {confirmed} = await this.showPopup('ConfirmPopup', {
title: this.env._t('Customer needed'),
body: this.env._t('eWallet requires a customer to be selected'),
});
if (confirmed) {
const { confirmed, payload: newPartner } = await this.showTempScreen(
'PartnerListScreen',
{ partner: null }
);
if (confirmed) {
order.set_partner(newPartner);
order.updatePricelist(newPartner);
}
}
} else {
return super._onClickPay(...arguments);
}
}
/**
* Sets up the options for the gift card product.
* @param {object} program
* @param {object} options
* @returns {Promise<boolean>} whether to proceed with adding the product or not
*/
async _setupGiftCardOptions(program, options) {
options.quantity = 1;
options.merge = false;
options.eWalletGiftCardProgram = program;
// If gift card program setting is 'scan_use', ask for the code.
if (this.env.pos.config.gift_card_settings == 'scan_use') {
const { confirmed, payload: code } = await this.showPopup('TextInputPopup', {
title: this.env._t('Generate a Gift Card'),
startingValue: '',
placeholder: this.env._t('Enter the gift card code'),
});
if (!confirmed) {
return false;
}
const trimmedCode = code.trim();
let nomenclatureRules = this.env.barcode_reader.barcode_parser.nomenclature.rules;
if (this.env.barcode_reader.fallbackBarcodeParser) {
nomenclatureRules.push(...this.env.barcode_reader.fallbackBarcodeParser.nomenclature.rules);
}
const couponNomenclatureRules = _.filter(nomenclatureRules, function(rule) {
return rule.type == "coupon";
});
let nomenclatureCodePatterns = [];
_.each(_.pluck(couponNomenclatureRules, "pattern"), function(pattern){
nomenclatureCodePatterns.push(...pattern.split("|"));
});
const trimmedCodeValid = _.find(nomenclatureCodePatterns, function(pattern) {
return trimmedCode.startsWith(pattern);
});
if (trimmedCode && trimmedCodeValid) {
// check if the code exist in the database
// if so, use its balance, otherwise, use the unit price of the gift card product
const fetchedGiftCard = await this.rpc({
model: 'loyalty.card',
method: 'search_read',
args: [
[['code', '=', trimmedCode], ['program_id', '=', program.id]],
['points', 'source_pos_order_id'],
],
});
// There should be maximum one gift card for a given code.
const giftCard = fetchedGiftCard[0];
if (giftCard && giftCard.source_pos_order_id) {
this.showPopup('ErrorPopup', {
title: this.env._t('This gift card has already been sold'),
body: this.env._t('You cannot sell a gift card that has already been sold.'),
});
return false;
}
options.giftBarcode = trimmedCode;
if (giftCard) {
// Use the balance of the gift card as the price of the orderline.
// NOTE: No need to convert the points to price because when opening a session,
// the gift card programs are made sure to have 1 point = 1 currency unit.
options.price = giftCard.points;
options.giftCardId = giftCard.id;
}
} else {
this.showNotification('Please enter a valid gift card code.');
return false;
}
}
return true;
}
async setupEWalletOptions(program, options) {
options.quantity = 1;
options.merge = false;
options.eWalletGiftCardProgram = program;
return true;
}
/**
* If the product is a potential reward, also apply the reward.
* @override
*/
async _addProduct(product, options) {
const linkedProgramIds = this.env.pos.productId2ProgramIds[product.id] || [];
const linkedPrograms = linkedProgramIds.map(id => this.env.pos.program_by_id[id]);
let selectedProgram = null;
if (linkedPrograms.length > 1) {
const { confirmed, payload: program } = await this.showPopup('SelectionPopup', {
title: this.env._t('Select program'),
list: linkedPrograms.map((program) => ({
id: program.id,
item: program,
label: program.name,
})),
});
if (confirmed) {
selectedProgram = program;
} else {
// Do nothing here if the selection is cancelled.
return;
}
} else if (linkedPrograms.length === 1) {
selectedProgram = linkedPrograms[0];
}
if (selectedProgram && selectedProgram.program_type == 'gift_card') {
const shouldProceed = await this._setupGiftCardOptions(selectedProgram, options);
if (!shouldProceed) {
return;
}
} else if (selectedProgram && selectedProgram.program_type == 'ewallet') {
const shouldProceed = await this.setupEWalletOptions(selectedProgram, options);
if (!shouldProceed) {
return;
}
}
const order = this.env.pos.get_order();
const potentialRewards = order.getPotentialFreeProductRewards();
let rewardsToApply = [];
for (const reward of potentialRewards) {
for (const reward_product_id of reward.reward.reward_product_ids) {
if (reward_product_id == product.id) {
rewardsToApply.push(reward);
}
}
}
await super._addProduct(product, options);
await order._updatePrograms();
if (rewardsToApply.length == 1) {
const reward = rewardsToApply[0];
order._applyReward(reward.reward, reward.coupon_id, { product: product.id });
}
}
_onCouponScan(code) {
// IMPROVEMENT: Ability to understand if the scanned code is to be paid or to be redeemed.
this.currentOrder.activateCode(code.base_code);
}
async _updateSelectedOrderline(event) {
const selectedLine = this.currentOrder.get_selected_orderline();
if (event.detail.key === '-') {
if (selectedLine && selectedLine.eWalletGiftCardProgram) {
// Do not allow negative quantity or price in a gift card or ewallet orderline.
// Refunding gift card or ewallet is not supported.
this.showNotification(this.env._t('You cannot set negative quantity or price to gift card or ewallet.'), 4000);
return;
}
}
if (selectedLine && selectedLine.is_reward_line && !selectedLine.manual_reward &&
(event.detail.key === 'Backspace' || event.detail.key === 'Delete')) {
const reward = this.env.pos.reward_by_id[selectedLine.reward_id];
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Deactivating reward'),
body: _.str.sprintf(
this.env._t('Are you sure you want to remove %s from this order?\n You will still be able to claim it through the reward button.'),
reward.description
),
cancelText: this.env._t('No'),
confirmText: this.env._t('Yes'),
});
if (confirmed) {
event.detail.buffer = null;
} else {
// Cancel backspace
return;
}
}
return super._updateSelectedOrderline(...arguments);
}
/**
* 1/ Perform the usual set value operation (super._setValue) if the line being modified
* is not a reward line or if it is a reward line, the `val` being set is '' or 'remove' only.
*
* 2/ Update activated programs and coupons when removing a reward line.
*
* 3/ Trigger 'update-rewards' if the line being modified is a regular line or
* if removing a reward line.
*
* @override
*/
_setValue(val) {
const selectedLine = this.currentOrder.get_selected_orderline();
if (
!selectedLine ||
!selectedLine.is_reward_line ||
(selectedLine.is_reward_line && ['', 'remove'].includes(val))
) {
super._setValue(val);
}
if (!selectedLine) return;
if (selectedLine.is_reward_line && val === 'remove') {
this.currentOrder.disabledRewards.add(selectedLine.reward_id);
const coupon = this.env.pos.couponCache[selectedLine.coupon_id];
if (coupon && coupon.id > 0 && this.currentOrder.codeActivatedCoupons.find((c) => c.code === coupon.code)) {
delete this.env.pos.couponCache[selectedLine.coupon_id];
this.currentOrder.codeActivatedCoupons.splice(this.currentOrder.codeActivatedCoupons.findIndex((coupon) => {
return coupon.id === selectedLine.coupon_id;
}), 1);
}
}
if (!selectedLine.is_reward_line || (selectedLine.is_reward_line && val === 'remove')) {
selectedLine.order._updateRewards();
}
}
async _showDecreaseQuantityPopup() {
const result = await super._showDecreaseQuantityPopup();
if (result){
this.env.pos.get_order()._updateRewards();
}
}
};
Registries.Component.extend(ProductScreen, PosLoyaltyProductScreen);

View file

@ -1,51 +0,0 @@
/** @odoo-module **/
import TicketScreen from 'point_of_sale.TicketScreen';
import Registries from 'point_of_sale.Registries';
import NumberBuffer from 'point_of_sale.NumberBuffer';
/**
* Prevent refunding ewallet/gift card lines.
*/
export const PosLoyaltyTicketScreen = (TicketScreen) =>
class PosLoyaltyTicketScreen extends TicketScreen {
_onUpdateSelectedOrderline() {
const order = this.getSelectedSyncedOrder();
if (!order) return NumberBuffer.reset();
const selectedOrderlineId = this.getSelectedOrderlineId();
const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId);
if (orderline && this._isEWalletGiftCard(orderline)) {
this._showNotAllowedRefundNotification();
return NumberBuffer.reset();
}
return super._onUpdateSelectedOrderline(...arguments);
}
_prepareAutoRefundOnOrder(order) {
const selectedOrderlineId = this.getSelectedOrderlineId();
const orderline = order.orderlines.find((line) => line.id == selectedOrderlineId);
if (this._isEWalletGiftCard(orderline)) {
this._showNotAllowedRefundNotification();
return false;
}
return super._prepareAutoRefundOnOrder(...arguments);
}
_showNotAllowedRefundNotification() {
this.showNotification(this.env._t("Refunding a top up or reward product for an eWallet or gift card program is not allowed."), 5000);
}
_isEWalletGiftCard(orderline) {
const linkedProgramIds = this.env.pos.productId2ProgramIds[orderline.product.id];
if (linkedProgramIds) {
return linkedProgramIds.length > 0;
}
if (orderline.is_reward_line) {
const reward = this.env.pos.reward_by_id[orderline.reward_id];
const program = reward && reward.program_id;
if (program && ['gift_card', 'ewallet'].includes(program.program_type)) {
return true;
}
}
return false;
}
};
Registries.Component.extend(TicketScreen, PosLoyaltyTicketScreen);

View file

@ -0,0 +1,16 @@
<templates>
<t t-inherit="loyalty.portal_loyalty_card_dialog" t-inherit-mode="extension">
<div name="history_lines" position="before">
<div
t-if="props.program.program_type == 'loyalty'"
class="d-flex align-items-center flex-column"
>
<img
t-att-src="`/report/barcode/Code128/${props.card.code}?&amp;width=350&amp;height=100`"
style="width:400px;height:100px"
/>
<span t-out="props.card.code" class="fs-5"/>
</div>
</div>
</t>
</templates>

View file

@ -1,137 +0,0 @@
/** @odoo-module **/
import { ErrorPopup } from 'point_of_sale.tour.ErrorPopupTourMethods';
import { PosLoyalty } from 'pos_loyalty.tour.PosCouponTourMethods';
import { ProductScreen } from 'point_of_sale.tour.ProductScreenTourMethods';
import { TicketScreen } from 'point_of_sale.tour.TicketScreenTourMethods';
import { Chrome } from 'point_of_sale.tour.ChromeTourMethods';
import { PartnerListScreen } from 'point_of_sale.tour.PartnerListScreenTourMethods';
import { getSteps, startSteps } from 'point_of_sale.tour.utils';
import Tour from 'web_tour.tour';
//#region EWalletProgramTour1
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
// Topup 50$ for partner_aaa
ProductScreen.do.clickDisplayedProduct('Top-up eWallet');
PosLoyalty.check.orderTotalIs('50.00');
ProductScreen.do.clickPayButton(false);
// If there's no partner, we asked to redirect to the partner list screen.
Chrome.do.confirmPopup();
PartnerListScreen.check.isShown();
PartnerListScreen.do.clickPartner('AAAAAAA');
PosLoyalty.exec.finalizeOrder('Cash');
// Topup 10$ for partner_bbb
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('BBBBBBB');
ProductScreen.exec.addOrderline('Top-up eWallet', '1', '10');
PosLoyalty.check.orderTotalIs('10.00');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('EWalletProgramTour1', { test: true, url: '/pos/web' }, getSteps());
//#endregion
//#region EWalletProgramTour2
const getEWalletText = (suffix) => 'eWallet' + (suffix !== '' ? ` ${suffix}` : '');
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Whiteboard Pen', '2', '6', '12.00');
PosLoyalty.check.eWalletButtonState({ highlighted: false });
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAAAAAA');
PosLoyalty.check.eWalletButtonState({ highlighted: true, text: getEWalletText('Pay') });
PosLoyalty.do.clickEWalletButton(getEWalletText('Pay'));
PosLoyalty.check.orderTotalIs('0.00');
PosLoyalty.exec.finalizeOrder('Cash');
// Consume partner_bbb's full eWallet.
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('BBBBBBB');
PosLoyalty.check.eWalletButtonState({ highlighted: false });
ProductScreen.exec.addOrderline('Desk Pad', '6', '6', '36.00');
PosLoyalty.check.eWalletButtonState({ highlighted: true, text: getEWalletText('Pay') });
PosLoyalty.do.clickEWalletButton(getEWalletText('Pay'));
PosLoyalty.check.orderTotalIs('26.00');
PosLoyalty.exec.finalizeOrder('Cash');
// Switching partners should work.
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('BBBBBBB');
ProductScreen.exec.addOrderline('Desk Pad', '2', '19', '38.00');
PosLoyalty.check.eWalletButtonState({ highlighted: false });
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAAAAAA');
PosLoyalty.check.eWalletButtonState({ highlighted: true, text: getEWalletText('Pay') });
PosLoyalty.do.clickEWalletButton(getEWalletText('Pay'));
PosLoyalty.check.orderTotalIs('0.00');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('BBBBBBB');
PosLoyalty.check.eWalletButtonState({ highlighted: false });
PosLoyalty.check.orderTotalIs('38.00');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAAAAAA');
PosLoyalty.check.eWalletButtonState({ highlighted: true, text: getEWalletText('Pay') });
PosLoyalty.do.clickEWalletButton(getEWalletText('Pay'));
PosLoyalty.check.orderTotalIs('0.00');
PosLoyalty.exec.finalizeOrder('Cash');
// Refund with eWallet.
// - Make an order to refund.
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('BBBBBBB');
ProductScreen.exec.addOrderline('Whiteboard Pen', '1', '20', '20.00');
PosLoyalty.check.orderTotalIs('20.00');
PosLoyalty.exec.finalizeOrder('Cash');
// - Refund order.
ProductScreen.do.clickRefund();
TicketScreen.check.filterIs('Paid');
TicketScreen.do.selectOrder('-0004');
TicketScreen.check.partnerIs('BBBBBBB');
TicketScreen.do.confirmRefund();
ProductScreen.check.isShown();
PosLoyalty.check.eWalletButtonState({ highlighted: true, text: getEWalletText('Refund') });
PosLoyalty.do.clickEWalletButton(getEWalletText('Refund'));
PosLoyalty.check.orderTotalIs('0.00');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('EWalletProgramTour2', { test: true, url: '/pos/web' }, getSteps());
//#endregion
//#region ExpiredEWalletProgramTour
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAAA');
ProductScreen.exec.addOrderline('Whiteboard Pen', '2', '6', '12.00');
PosLoyalty.check.eWalletButtonState({ highlighted: false });
PosLoyalty.do.clickEWalletButton();
ErrorPopup.check.isShown();
ErrorPopup.do.clickConfirm();
Tour.register('ExpiredEWalletProgramTour', { test: true, url: '/pos/web' }, getSteps());
//#endregion
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer("partner_a");
PosLoyalty.check.eWalletButtonState({ highlighted: false });
ProductScreen.exec.addOrderline("product_a", "1");
PosLoyalty.check.eWalletButtonState({ highlighted: true, text: getEWalletText("Pay") });
PosLoyalty.do.clickEWalletButton(getEWalletText("Pay"));
PosLoyalty.check.pointsAwardedAre("100"),
PosLoyalty.exec.finalizeOrder("Cash", "90");
Tour.register("PosLoyaltyPointsEwallet", { test: true, url: "/pos/web" }, getSteps());

View file

@ -1,97 +0,0 @@
/** @odoo-module **/
import { PosLoyalty } from 'pos_loyalty.tour.PosCouponTourMethods';
import { ProductScreen } from 'point_of_sale.tour.ProductScreenTourMethods';
import { TextInputPopup } from 'point_of_sale.tour.TextInputPopupTourMethods';
import { getSteps, startSteps } from 'point_of_sale.tour.utils';
import Tour from 'web_tour.tour';
//#region GiftCardProgramCreateSetTour1
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Gift Card');
PosLoyalty.check.orderTotalIs('50.00');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('GiftCardProgramCreateSetTour1', { test: true, url: '/pos/web' }, getSteps());
//#endregion
//#region GiftCardProgramCreateSetTour2
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
PosLoyalty.do.enterCode('044123456');
PosLoyalty.check.orderTotalIs('0.00');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('GiftCardProgramCreateSetTour2', { test: true, url: '/pos/web' }, getSteps());
//#endregion
//#region GiftCardProgramScanUseTour
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
// Pay the 5$ gift card.
ProductScreen.do.clickDisplayedProduct('Gift Card');
TextInputPopup.check.isShown();
TextInputPopup.do.inputText('044123456');
TextInputPopup.do.clickConfirm();
PosLoyalty.check.orderTotalIs('5.00');
PosLoyalty.exec.finalizeOrder('Cash');
// Partially use the gift card. (4$)
ProductScreen.exec.addOrderline('Desk Pad', '2', '2', '4.0');
PosLoyalty.do.enterCode('044123456');
PosLoyalty.check.orderTotalIs('0.00');
PosLoyalty.exec.finalizeOrder('Cash');
// Use the remaining of the gift card. (5$ - 4$ = 1$)
ProductScreen.exec.addOrderline('Whiteboard Pen', '6', '6', '36.0');
PosLoyalty.do.enterCode('044123456');
PosLoyalty.check.orderTotalIs('35.00');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('GiftCardProgramScanUseTour', { test: true, url: '/pos/web' }, getSteps());
//#endregion
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Gift Card');
TextInputPopup.check.isShown();
TextInputPopup.do.inputText('044123456');
TextInputPopup.do.clickConfirm();
PosLoyalty.check.orderTotalIs('50.00');
PosLoyalty.exec.finalizeOrder('Cash');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer("partner_a");
ProductScreen.exec.addOrderline("product_a", "1");
PosLoyalty.do.enterCode("044123456");
PosLoyalty.check.orderTotalIs("50.00");
PosLoyalty.check.pointsAwardedAre("100"),
PosLoyalty.exec.finalizeOrder("Cash", "50");
Tour.register("PosLoyaltyPointsGiftcard", { test: true, url: "/pos/web" }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Gift Card');
TextInputPopup.check.isShown();
TextInputPopup.do.inputText('044123456');
TextInputPopup.do.clickConfirm();
PosLoyalty.check.orderTotalIs('50.00');
PosLoyalty.exec.finalizeOrder('Cash');
ProductScreen.do.clickDisplayedProduct("Test Product A");
PosLoyalty.do.enterCode("044123456");
PosLoyalty.check.orderTotalIs("50.00");
ProductScreen.check.checkTaxAmount("-6.52");
Tour.register("PosLoyaltyGiftCardTaxes", { test: true }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Gift Card');
TextInputPopup.check.isShown();
TextInputPopup.do.inputText('044123456');
TextInputPopup.do.clickConfirm();
PosLoyalty.check.orderTotalIs('0.00');
ProductScreen.do.pressNumpad("Price 5");
PosLoyalty.check.orderTotalIs('5.00');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register("PosLoyaltyGiftCardNoPoints", { test: true }, getSteps());

View file

@ -1,70 +0,0 @@
/** @odoo-module **/
import { PosLoyalty } from 'pos_loyalty.tour.PosCouponTourMethods';
import { ProductScreen } from 'point_of_sale.tour.ProductScreenTourMethods';
import { SelectionPopup } from 'point_of_sale.tour.SelectionPopupTourMethods';
import { getSteps, startSteps } from 'point_of_sale.tour.utils';
import Tour from 'web_tour.tour';
const getEWalletText = (suffix) => 'eWallet' + (suffix !== '' ? ` ${suffix}` : '');
startSteps();
// One card for gift_card_1.
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Gift Card');
SelectionPopup.check.hasSelectionItem('gift_card_1');
SelectionPopup.check.hasSelectionItem('gift_card_2');
SelectionPopup.do.clickItem('gift_card_1');
ProductScreen.do.pressNumpad('Price');
ProductScreen.do.pressNumpad('1 0');
PosLoyalty.check.orderTotalIs('10.00');
PosLoyalty.exec.finalizeOrder('Cash');
// One card for gift_card_1.
ProductScreen.do.clickDisplayedProduct('Gift Card');
SelectionPopup.do.clickItem('gift_card_2');
ProductScreen.do.pressNumpad('Price');
ProductScreen.do.pressNumpad('2 0');
PosLoyalty.check.orderTotalIs('20.00');
PosLoyalty.exec.finalizeOrder('Cash');
// Top up ewallet_1 for AAAAAAA.
ProductScreen.do.clickDisplayedProduct('Top-up eWallet');
SelectionPopup.check.hasSelectionItem('ewallet_1');
SelectionPopup.check.hasSelectionItem('ewallet_2');
SelectionPopup.do.clickItem('ewallet_1');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAAAAAA');
ProductScreen.do.pressNumpad('Price');
ProductScreen.do.pressNumpad('3 0');
PosLoyalty.check.orderTotalIs('30.00');
PosLoyalty.exec.finalizeOrder('Cash');
// Top up ewallet_2 for AAAAAAA.
ProductScreen.do.clickDisplayedProduct('Top-up eWallet');
SelectionPopup.do.clickItem('ewallet_2');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAAAAAA');
ProductScreen.do.pressNumpad('Price');
ProductScreen.do.pressNumpad('4 0');
PosLoyalty.check.orderTotalIs('40.00');
PosLoyalty.exec.finalizeOrder('Cash');
// Top up ewallet_1 for BBBBBBB.
ProductScreen.do.clickDisplayedProduct('Top-up eWallet');
SelectionPopup.do.clickItem('ewallet_1');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('BBBBBBB');
PosLoyalty.check.orderTotalIs('50.00');
PosLoyalty.exec.finalizeOrder('Cash');
// Consume 12$ from ewallet_1 of AAAAAAA.
ProductScreen.exec.addOrderline('Whiteboard Pen', '2', '6', '12.00');
PosLoyalty.check.eWalletButtonState({ highlighted: false });
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAAAAAA');
PosLoyalty.check.eWalletButtonState({ highlighted: true, text: getEWalletText('Pay') });
PosLoyalty.do.clickEWalletButton(getEWalletText('Pay'));
SelectionPopup.check.hasSelectionItem('ewallet_1');
SelectionPopup.check.hasSelectionItem('ewallet_2');
SelectionPopup.do.clickItem('ewallet_1');
PosLoyalty.check.orderTotalIs('0.00');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('MultipleGiftWalletProgramsTour', { test: true, url: '/pos/web' }, getSteps());

View file

@ -1,205 +0,0 @@
/** @odoo-module **/
import { PosLoyalty } from 'pos_loyalty.tour.PosCouponTourMethods';
import { ProductScreen } from 'point_of_sale.tour.ProductScreenTourMethods';
import { getSteps, startSteps } from 'point_of_sale.tour.utils';
import Tour from 'web_tour.tour';
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
// Order1: Generates 2 points.
ProductScreen.exec.addOrderline('Whiteboard Pen', '2');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner AAA');
PosLoyalty.check.orderTotalIs('6.40');
PosLoyalty.exec.finalizeOrder('Cash');
// Order2: Consumes points to get free product.
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner AAA');
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.00');
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '2.00');
// At this point, Test Partner AAA has 4 points.
PosLoyalty.check.isRewardButtonHighlighted(true);
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '3.00');
PosLoyalty.check.hasRewardLine('Free Product - Whiteboard Pen', '-3.20', '1.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
PosLoyalty.check.orderTotalIs('6.40');
PosLoyalty.exec.finalizeOrder('Cash');
// Order3: Generate 4 points.
// - Initially gets free product, but was removed automatically by changing the
// number of items to zero.
// - It's impossible to checked here if the free product reward is really removed
// so we check in the backend the number of orders that consumed the reward.
ProductScreen.exec.addOrderline('Whiteboard Pen', '4');
PosLoyalty.check.orderTotalIs('12.80');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner AAA');
PosLoyalty.check.isRewardButtonHighlighted(true);
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
PosLoyalty.check.hasRewardLine('Free Product - Whiteboard Pen', '-3.20', '1.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.do.pressNumpad('Backspace');
// At this point, the reward line should have been automatically removed
// because there is not enough points to purchase it. Unfortunately, we
// can't check that here.
PosLoyalty.check.orderTotalIs('0.00');
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.00');
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '2.00');
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '3.00');
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '4.00');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.check.orderTotalIs('12.80');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyLoyaltyProgram1', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
// Order1: Immediately set the customer to Test Partner AAA which has 4 points.
// - He has enough points to purchase a free product but since there is still
// no product in the order, reward button should not yet be highlighted.
// - Furthermore, clicking the reward product should not add it as reward product.
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner AAA');
// No item in the order, so reward button is off.
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
PosLoyalty.check.isRewardButtonHighlighted(true);
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
PosLoyalty.check.hasRewardLine('Free Product - Whiteboard Pen', '-3.20', '1.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
PosLoyalty.check.orderTotalIs('3.20');
PosLoyalty.exec.finalizeOrder('Cash');
// Order2: Generate 4 points for Test Partner CCC.
// - Reference: Order2_CCC
// - But set Test Partner BBB first as the customer.
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner BBB');
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.00');
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '2.00');
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '3.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '4.00');
PosLoyalty.check.isRewardButtonHighlighted(true);
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner CCC');
PosLoyalty.check.customerIs('Test Partner CCC');
PosLoyalty.check.orderTotalIs('12.80');
PosLoyalty.exec.finalizeOrder('Cash');
// Order3: Generate 3 points for Test Partner BBB.
// - Reference: Order3_BBB
// - But set Test Partner CCC first as the customer.
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner CCC');
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.exec.addOrderline('Whiteboard Pen', '3');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner BBB');
PosLoyalty.check.customerIs('Test Partner BBB');
PosLoyalty.check.orderTotalIs('9.60');
PosLoyalty.exec.finalizeOrder('Cash');
// Order4: Should not have reward because the customer will be removed.
// - Reference: Order4_no_reward
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
ProductScreen.check.selectedOrderlineHas('Whiteboard Pen', '1.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner CCC');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
PosLoyalty.check.hasRewardLine('Free Product - Whiteboard Pen', '-3.20', '1.00');
ProductScreen.do.clickPartnerButton();
// This deselects the customer.
PosLoyalty.do.unselectPartner();
PosLoyalty.check.customerIs('Customer');
PosLoyalty.check.orderTotalIs('6.40');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyLoyaltyProgram2', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
// Generates 10.2 points and use points to get the reward product with zero sale price
ProductScreen.exec.addOrderline('Desk Organizer', '2');
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner AAA');
// At this point, the free_product program is triggered.
// The reward button should be highlighted.
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
PosLoyalty.check.hasRewardLine('Free Product - Whiteboard Pen', '0.0', '1.00');
PosLoyalty.check.orderTotalIs('10.2');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyLoyaltyProgram3', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAA Partner');
ProductScreen.exec.addOrderline('Test Product 1', '1.00', '100');
ProductScreen.check.totalAmountIs('80.00');
Tour.register('PosLoyaltyPromotion', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
// Generates 10.2 points and use points to get the reward product with zero sale price
ProductScreen.exec.addOrderline('Desk Organizer', '3');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyNextOrderCouponExpirationDate', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('Test Partner');
ProductScreen.exec.addOrderline('Desk Organizer', '1');
ProductScreen.exec.addOrderline('Whiteboard Pen', '1');
PosLoyalty.do.clickRewardButton();
PosLoyalty.check.orderTotalIs('5.10');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyDontGrantPointsForRewardOrderLines', { test: true, url: '/pos/web' }, getSteps());

View file

@ -1,252 +0,0 @@
/** @odoo-module **/
import { PosLoyalty } from 'pos_loyalty.tour.PosCouponTourMethods';
import { ProductScreen } from 'point_of_sale.tour.ProductScreenTourMethods';
import { SelectionPopup } from 'point_of_sale.tour.SelectionPopupTourMethods';
import { getSteps, startSteps } from 'point_of_sale.tour.utils';
import Tour from 'web_tour.tour';
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Desk Organizer', '2');
// At this point, the free_product program is triggered.
// The reward button should be highlighted.
PosLoyalty.check.isRewardButtonHighlighted(true);
// Since the reward button is highlighted, clicking the reward product should be added as reward.
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '3.00');
PosLoyalty.check.hasRewardLine('Free Product - Desk Organizer', '-5.10', '1.00');
// In the succeeding 2 clicks on the product, it is considered as a regular product.
// In the third click, the product will be added as reward.
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
PosLoyalty.check.isRewardButtonHighlighted(true);
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '6.00');
PosLoyalty.check.hasRewardLine('Free Product - Desk Organizer', '-10.20', '2.00');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
PosLoyalty.check.isRewardButtonHighlighted(false);
PosLoyalty.check.orderTotalIs('25.50');
// Finalize order that consumed a reward.
PosLoyalty.exec.finalizeOrder('Cash');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '1.00');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '2.00');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
PosLoyalty.check.hasRewardLine('Free Product - Desk Organizer', '-5.10', '1.00');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '0.00');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '1.00');
ProductScreen.do.clickDisplayedProduct('Desk Organizer');
ProductScreen.check.selectedOrderlineHas('Desk Organizer', '2.00');
PosLoyalty.check.isRewardButtonHighlighted(true);
// Finalize order but without the reward.
// This step is important. When syncing the order, no reward should be synced.
PosLoyalty.check.orderTotalIs('10.20');
PosLoyalty.exec.finalizeOrder('Cash');
ProductScreen.exec.addOrderline('Magnetic Board', '2');
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.do.clickDisplayedProduct('Magnetic Board');
PosLoyalty.check.isRewardButtonHighlighted(true);
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
PosLoyalty.check.isRewardButtonHighlighted(false);
PosLoyalty.check.hasRewardLine('Free Product - Whiteboard Pen', '-3.20', '1.00');
ProductScreen.do.clickOrderline('Magnetic Board', '3.00');
ProductScreen.check.selectedOrderlineHas('Magnetic Board', '3.00');
ProductScreen.do.pressNumpad('6');
ProductScreen.check.selectedOrderlineHas('Magnetic Board', '6.00');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
PosLoyalty.check.isRewardButtonHighlighted(false);
PosLoyalty.check.hasRewardLine('Free Product - Whiteboard Pen', '-6.40', '2.00');
// Finalize order that consumed a reward.
PosLoyalty.check.orderTotalIs('11.88');
PosLoyalty.exec.finalizeOrder('Cash');
ProductScreen.exec.addOrderline('Magnetic Board', '6');
ProductScreen.do.clickDisplayedProduct('Whiteboard Pen');
PosLoyalty.check.hasRewardLine('Free Product - Whiteboard Pen', '-3.20', '1.00');
PosLoyalty.check.isRewardButtonHighlighted(true);
ProductScreen.do.clickOrderline('Magnetic Board', '6.00');
ProductScreen.do.pressNumpad('Backspace');
// At this point, the reward should have been removed.
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.check.selectedOrderlineHas('Magnetic Board', '0.00');
ProductScreen.do.clickDisplayedProduct('Magnetic Board');
ProductScreen.check.selectedOrderlineHas('Magnetic Board', '1.00');
ProductScreen.do.clickDisplayedProduct('Magnetic Board');
ProductScreen.check.selectedOrderlineHas('Magnetic Board', '2.00');
ProductScreen.do.clickDisplayedProduct('Magnetic Board');
ProductScreen.check.selectedOrderlineHas('Magnetic Board', '3.00');
PosLoyalty.check.hasRewardLine('Free Product - Whiteboard Pen', '-3.20', '1.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
PosLoyalty.check.orderTotalIs('5.94');
PosLoyalty.exec.finalizeOrder('Cash');
// Promotion: 2 items of shelves, get desk_pad/monitor_stand free
// This is the 5th order.
ProductScreen.do.clickDisplayedProduct('Wall Shelf Unit');
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', '1.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.do.clickDisplayedProduct('Small Shelf');
ProductScreen.check.selectedOrderlineHas('Small Shelf', '1.00');
PosLoyalty.check.isRewardButtonHighlighted(true);
// Click reward product. Should be automatically added as reward.
ProductScreen.do.clickDisplayedProduct('Desk Pad');
PosLoyalty.check.isRewardButtonHighlighted(false);
PosLoyalty.check.hasRewardLine('Free Product', '-1.98', '1.00');
// Remove the reward line. The next steps will check if cashier
// can select from the different reward products.
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.do.pressNumpad('Backspace');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
SelectionPopup.check.hasSelectionItem('Monitor Stand');
SelectionPopup.check.hasSelectionItem('Desk Pad');
SelectionPopup.do.clickItem('Desk Pad');
PosLoyalty.check.isRewardButtonHighlighted(false);
PosLoyalty.check.hasRewardLine('Free Product', '-1.98', '1.00');
ProductScreen.do.pressNumpad('Backspace');
ProductScreen.do.pressNumpad('Backspace');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.claimReward('Monitor Stand');
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.check.selectedOrderlineHas('Monitor Stand', '1.00', '3.19');
PosLoyalty.check.hasRewardLine('Free Product', '-3.19', '1.00');
PosLoyalty.check.orderTotalIs('4.81');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyFreeProductTour', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAA Partner');
ProductScreen.exec.addOrderline('Test Product A', '1');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
PosLoyalty.check.hasRewardLine('Free Product - Test Product A', '-11.50', '1.00');
Tour.register('PosLoyaltyFreeProductTour2', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Test Product A');
ProductScreen.check.selectedOrderlineHas('Test Product A', '1.00', '40.00');
ProductScreen.do.clickDisplayedProduct('Test Product B');
ProductScreen.check.selectedOrderlineHas('Test Product B', '1.00', '40.00');
PosLoyalty.do.clickRewardButton();
SelectionPopup.do.clickItem("$ 10 per order on specific products");
PosLoyalty.check.hasRewardLine('$ 10 per order on specific products', '-10.00', '1.00');
PosLoyalty.check.orderTotalIs('70.00');
PosLoyalty.do.clickRewardButton();
SelectionPopup.do.clickItem("$ 10 per order on specific products");
PosLoyalty.check.orderTotalIs('60.00');
PosLoyalty.do.clickRewardButton();
SelectionPopup.do.clickItem("$ 30 per order on specific products");
PosLoyalty.check.hasRewardLine('$ 30 per order on specific products', '-30.00', '1.00');
PosLoyalty.check.orderTotalIs('30.00');
Tour.register('PosLoyaltySpecificDiscountTour', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Test Product A');
ProductScreen.do.clickDisplayedProduct('Test Product C');
PosLoyalty.check.orderTotalIs('130.00');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
PosLoyalty.check.orderTotalIs('130.00');
Tour.register('PosLoyaltySpecificDiscountWithFreeProductTour', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Product A');
ProductScreen.check.selectedOrderlineHas('Product A', '1.00', '15.00');
PosLoyalty.check.orderTotalIs('15.00');
ProductScreen.do.clickDisplayedProduct('Product B');
ProductScreen.check.selectedOrderlineHas('Product B', '1.00', '50.00');
PosLoyalty.check.orderTotalIs('40.00');
Tour.register('PosLoyaltySpecificDiscountWithRewardProductDomainTour', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Product A');
ProductScreen.check.selectedOrderlineHas('Product A', '1.00', '15.00');
PosLoyalty.check.orderTotalIs('15.00');
ProductScreen.do.clickDisplayedProduct('Product B');
ProductScreen.check.selectedOrderlineHas('Product B', '1.00', '50.00');
PosLoyalty.check.orderTotalIs('40.00');
Tour.register('PosLoyaltySpecificDiscountCategoryTour', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct("Desk Organizer");
ProductScreen.do.clickDisplayedProduct("Desk Organizer");
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
SelectionPopup.do.clickItem("product_a");
PosLoyalty.check.hasRewardLine("Free Product", "-2", "1.00");
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.do.clickDisplayedProduct("Desk Organizer");
ProductScreen.do.clickDisplayedProduct("Desk Organizer");
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
SelectionPopup.do.clickItem("product_b");
PosLoyalty.check.hasRewardLine("Free Product", "-5", "1.00");
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.do.clickDisplayedProduct("Desk Organizer");
ProductScreen.do.clickDisplayedProduct("Desk Organizer");
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
SelectionPopup.do.clickItem("product_b");
PosLoyalty.check.hasRewardLine("Free Product", "-10", "2.00");
PosLoyalty.check.isRewardButtonHighlighted(false);
Tour.register("PosLoyaltyRewardProductTag", { test: true, url: "/pos/web" }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Product A');
PosLoyalty.do.enterCode('563412');
PosLoyalty.check.hasRewardLine('10% on your order', '-1.50');
Tour.register("test_loyalty_on_order_with_fixed_tax", { test: true, url: "/pos/web" }, getSteps());

View file

@ -1,435 +0,0 @@
/** @odoo-module **/
import { PosLoyalty } from 'pos_loyalty.tour.PosCouponTourMethods';
import { ProductScreen } from 'point_of_sale.tour.ProductScreenTourMethods';
import { getSteps, startSteps } from 'point_of_sale.tour.utils';
import Tour from 'web_tour.tour';
// --- PoS Loyalty Tour Basic Part 1 ---
// Generate coupons for PosLoyaltyTour2.
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
// basic order
// just accept the automatically applied promo program
// applied programs:
// - on cheapest product
ProductScreen.exec.addOrderline('Whiteboard Pen', '5');
PosLoyalty.check.hasRewardLine('90% on the cheapest product', '-2.88');
PosLoyalty.do.selectRewardLine('on the cheapest product');
PosLoyalty.check.orderTotalIs('13.12');
PosLoyalty.exec.finalizeOrder('Cash');
// remove the reward from auto promo program
// no applied programs
ProductScreen.exec.addOrderline('Whiteboard Pen', '6');
PosLoyalty.check.hasRewardLine('on the cheapest product', '-2.88');
PosLoyalty.check.orderTotalIs('16.32');
PosLoyalty.exec.removeRewardLine('90% on the cheapest product');
PosLoyalty.check.orderTotalIs('19.2');
PosLoyalty.exec.finalizeOrder('Cash');
// order with coupon code from coupon program
// applied programs:
// - coupon program
ProductScreen.exec.addOrderline('Desk Organizer', '9');
PosLoyalty.check.hasRewardLine('on the cheapest product', '-4.59');
PosLoyalty.exec.removeRewardLine('90% on the cheapest product');
PosLoyalty.check.orderTotalIs('45.90');
PosLoyalty.do.enterCode('invalid_code');
PosLoyalty.do.enterCode('1234');
PosLoyalty.check.hasRewardLine('Free Product - Desk Organizer', '-15.30');
PosLoyalty.exec.finalizeOrder('Cash');
// Use coupon but eventually remove the reward
// applied programs:
// - on cheapest product
ProductScreen.exec.addOrderline('Letter Tray', '4');
ProductScreen.exec.addOrderline('Desk Organizer', '9');
PosLoyalty.check.hasRewardLine('90% on the cheapest product', '-4.75');
PosLoyalty.check.orderTotalIs('62.27');
PosLoyalty.do.enterCode('5678');
PosLoyalty.check.hasRewardLine('Free Product - Desk Organizer', '-15.30');
PosLoyalty.check.orderTotalIs('46.97');
PosLoyalty.exec.removeRewardLine('Free Product');
PosLoyalty.check.orderTotalIs('62.27');
PosLoyalty.exec.finalizeOrder('Cash');
// specific product discount
// applied programs:
// - on cheapest product
// - on specific products
ProductScreen.exec.addOrderline('Magnetic Board', '10') // 1.98
ProductScreen.exec.addOrderline('Desk Organizer', '3') // 5.1
ProductScreen.exec.addOrderline('Letter Tray', '4') // 4.8 tax 10%
PosLoyalty.check.hasRewardLine('90% on the cheapest product', '-1.78')
PosLoyalty.check.orderTotalIs('54.44')
PosLoyalty.do.enterCode('promocode')
PosLoyalty.check.hasRewardLine('50% on specific products', '-16.66') // 17.55 - 1.78*0.5
PosLoyalty.check.orderTotalIs('37.78')
PosLoyalty.exec.finalizeOrder('Cash')
Tour.register('PosLoyaltyTour1', { test: true, url: '/pos/web' }, getSteps());
// --- PoS Loyalty Tour Basic Part 2 ---
// Using the coupons generated from PosLoyaltyTour1.
startSteps();
ProductScreen.do.clickHomeCategory();
// Test that global discount and cheapest product discounts can be accumulated.
// Applied programs:
// - global discount
// - on cheapest discount
ProductScreen.exec.addOrderline('Desk Organizer', '10'); // 5.1
PosLoyalty.check.hasRewardLine('on the cheapest product', '-4.59');
ProductScreen.exec.addOrderline('Letter Tray', '4'); // 4.8 tax 10%
PosLoyalty.check.hasRewardLine('on the cheapest product', '-4.75');
PosLoyalty.do.enterCode('123456');
PosLoyalty.check.hasRewardLine('10% on your order', '-5.10');
PosLoyalty.check.hasRewardLine('10% on your order', '-1.64');
PosLoyalty.check.orderTotalIs('60.63'); //SUBTOTAL
PosLoyalty.exec.finalizeOrder('Cash');
// Scanning coupon twice.
// Also apply global discount on top of free product to check if the
// calculated discount is correct.
// Applied programs:
// - coupon program (free product)
// - global discount
// - on cheapest discount
ProductScreen.exec.addOrderline('Desk Organizer', '11'); // 5.1 per item
PosLoyalty.check.hasRewardLine('90% on the cheapest product', '-4.59');
PosLoyalty.check.orderTotalIs('51.51');
// add global discount and the discount will be replaced
PosLoyalty.do.enterCode('345678');
PosLoyalty.check.hasRewardLine('10% on your order', '-5.15');
// add free product coupon (for qty=11, free=4)
// the discount should change after having free products
// it should go back to cheapest discount as it is higher
PosLoyalty.do.enterCode('5678');
PosLoyalty.check.hasRewardLine('Free Product - Desk Organizer', '-20.40');
PosLoyalty.check.hasRewardLine('90% on the cheapest product', '-4.59');
// set quantity to 18
// free qty stays the same since the amount of points on the card only allows for 4 free products
ProductScreen.do.pressNumpad('Backspace 8')
PosLoyalty.check.hasRewardLine('10% on your order', '-6.68');
PosLoyalty.check.hasRewardLine('Free Product - Desk Organizer', '-20.40');
// scan the code again and check notification
PosLoyalty.do.enterCode('5678');
PosLoyalty.check.orderTotalIs('60.13');
PosLoyalty.exec.finalizeOrder('Cash');
// Specific products discount (with promocode) and free product (1357)
// Applied programs:
// - discount on specific products
// - free product
ProductScreen.exec.addOrderline('Desk Organizer', '6'); // 5.1 per item
PosLoyalty.check.hasRewardLine('on the cheapest product', '-4.59');
PosLoyalty.exec.removeRewardLine('90% on the cheapest product');
PosLoyalty.do.enterCode('promocode');
PosLoyalty.check.hasRewardLine('50% on specific products', '-15.30');
PosLoyalty.do.enterCode('1357');
PosLoyalty.check.hasRewardLine('Free Product - Desk Organizer', '-10.20');
PosLoyalty.check.hasRewardLine('50% on specific products', '-10.20');
PosLoyalty.check.orderTotalIs('10.20');
PosLoyalty.exec.finalizeOrder('Cash');
// Check reset program
// Enter two codes and reset the programs.
// The codes should be checked afterwards. They should return to new.
// Applied programs:
// - cheapest product
ProductScreen.exec.addOrderline('Monitor Stand', '6'); // 3.19 per item
PosLoyalty.do.enterCode('098765');
PosLoyalty.check.hasRewardLine('90% on the cheapest product', '-2.87');
PosLoyalty.check.hasRewardLine('10% on your order', '-1.63');
PosLoyalty.check.orderTotalIs('14.64');
PosLoyalty.exec.removeRewardLine('90% on the cheapest product');
PosLoyalty.check.hasRewardLine('10% on your order', '-1.91');
PosLoyalty.check.orderTotalIs('17.23');
PosLoyalty.do.resetActivePrograms();
PosLoyalty.check.hasRewardLine('90% on the cheapest product', '-2.87');
PosLoyalty.check.orderTotalIs('16.27');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyTour2', { test: true, url: '/pos/web' }, getSteps());
// --- PoS Loyalty Tour Basic Part 3 ---
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Promo Product');
PosLoyalty.check.orderTotalIs('34.50');
ProductScreen.do.clickDisplayedProduct('Product B');
PosLoyalty.check.hasRewardLine('100% on specific products', '25.00');
ProductScreen.do.clickDisplayedProduct('Product A');
PosLoyalty.check.hasRewardLine('100% on specific products', '15.00');
PosLoyalty.check.orderTotalIs('34.50');
ProductScreen.do.clickDisplayedProduct('Product A');
PosLoyalty.check.hasRewardLine('100% on specific products', '21.82');
PosLoyalty.check.hasRewardLine('100% on specific products', '18.18');
PosLoyalty.check.orderTotalIs('49.50');
Tour.register('PosLoyaltyTour3', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Test Product 1', '1');
ProductScreen.exec.addOrderline('Test Product 2', '1');
ProductScreen.do.clickPricelistButton();
ProductScreen.do.selectPriceList('Public Pricelist');
PosLoyalty.do.enterCode('abcda');
PosLoyalty.check.orderTotalIs('0.00');
ProductScreen.do.clickPricelistButton();
ProductScreen.do.selectPriceList('Test multi-currency');
PosLoyalty.check.orderTotalIs('0.00');
Tour.register('PosLoyaltyTour4', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Test Product 1', '1.00', '100');
PosLoyalty.do.clickDiscountButton();
PosLoyalty.do.clickConfirmButton();
ProductScreen.check.totalAmountIs('92.00');
Tour.register('PosLoyaltyTour5', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAA Partner');
ProductScreen.do.clickDisplayedProduct('Test Product A');
PosLoyalty.do.clickRewardButton();
ProductScreen.check.totalAmountIs('139');
Tour.register('PosLoyaltyTour6', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Test Product', '1');
PosLoyalty.check.orderTotalIs('100');
PosLoyalty.do.enterCode('abcda');
PosLoyalty.check.orderTotalIs('90');
Tour.register('PosLoyaltyTour7', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickDisplayedProduct('Product B');
ProductScreen.do.clickDisplayedProduct('Product A');
ProductScreen.check.totalAmountIs('50.00');
Tour.register('PosLoyaltyTour8', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAA Partner');
ProductScreen.do.clickDisplayedProduct('Product B');
ProductScreen.do.clickDisplayedProduct('Product A');
ProductScreen.check.totalAmountIs('210.00');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
ProductScreen.check.totalAmountIs('205.00');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
ProductScreen.check.totalAmountIs('200.00');
Tour.register('PosLoyaltyTour9', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAA Partner');
ProductScreen.do.clickDisplayedProduct('Product Test');
ProductScreen.check.totalAmountIs('1.00');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.claimReward('Free Product B');
PosLoyalty.check.hasRewardLine('Free Product B', '-1.00');
ProductScreen.check.totalAmountIs('1.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
Tour.register('PosLoyaltyTour10', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAA Partner');
PosLoyalty.check.customerIs('AAA Partner');
ProductScreen.exec.addOrderline('Product Test', '3');
ProductScreen.check.totalAmountIs('150.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyTour11.1', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer('AAA Partner');
PosLoyalty.check.customerIs('AAA Partner');
ProductScreen.do.clickDisplayedProduct('Product Test');
ProductScreen.check.totalAmountIs('50.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
PosLoyalty.do.enterCode('123456');
PosLoyalty.check.isRewardButtonHighlighted(true);
PosLoyalty.do.clickRewardButton();
PosLoyalty.check.hasRewardLine('Free Product', '-3.00');
PosLoyalty.check.isRewardButtonHighlighted(false);
ProductScreen.check.totalAmountIs('50.00');
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyTour11.2', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline('Free Product A', '2');
ProductScreen.do.clickDisplayedProduct('Free Product A');
ProductScreen.check.totalAmountIs('2.00');
PosLoyalty.check.hasRewardLine('Free Product', '-1.00');
ProductScreen.exec.addOrderline('Free Product B', '2');
ProductScreen.do.clickDisplayedProduct('Free Product B');
ProductScreen.check.totalAmountIs('4.00');
PosLoyalty.check.hasRewardLine('Free Product', '-2.00');
ProductScreen.exec.addOrderline('Free Product B', '5');
ProductScreen.do.clickDisplayedProduct('Free Product B');
ProductScreen.check.totalAmountIs('6.00');
PosLoyalty.check.hasRewardLine('Free Product', '-3.00');
Tour.register('PosLoyaltyTour12', { test: true, url: '/pos/web' }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickDisplayedProduct('Product A');
ProductScreen.check.selectedOrderlineHas('Product A', '1.00', '20.00');
PosLoyalty.check.orderTotalIs('20.00');
ProductScreen.do.clickDisplayedProduct('Product B');
ProductScreen.check.selectedOrderlineHas('Product B', '1.00', '30.00');
PosLoyalty.check.orderTotalIs('50.00');
ProductScreen.do.clickDisplayedProduct('Product A');
ProductScreen.check.selectedOrderlineHas('Product A', '2.00', '40.00');
PosLoyalty.check.orderTotalIs('66.00');
Tour.register('PosLoyaltyMinAmountAndSpecificProductTour', {test: true, url: '/pos/web'}, getSteps());
function createOrderCoupon(totalAmount, couponName, couponAmount, loyaltyPoints) {
return [
ProductScreen.do.confirmOpeningPopup(),
ProductScreen.do.clickHomeCategory(),
ProductScreen.do.clickPartnerButton(),
ProductScreen.do.clickCustomer("partner_a"),
ProductScreen.exec.addOrderline("product_a", "1"),
ProductScreen.exec.addOrderline("product_b", "1"),
PosLoyalty.do.enterCode("promocode"),
PosLoyalty.check.hasRewardLine(`${couponName}`, `${couponAmount}`),
PosLoyalty.check.orderTotalIs(`${totalAmount}`),
PosLoyalty.check.pointsAwardedAre(`${loyaltyPoints}`),
PosLoyalty.exec.finalizeOrder("Cash"),
];
}
startSteps();
createOrderCoupon("135.00", "10% on your order", "-15.00", "135");
Tour.register("PosLoyaltyPointsDiscountNoDomainProgramNoDomain", { test: true, url: "/pos/web" }, getSteps());
startSteps();
createOrderCoupon("135.00", "10% on your order", "-15.00", "100");
Tour.register("PosLoyaltyPointsDiscountNoDomainProgramDomain", { test: true, url: "/pos/web" }, getSteps());
startSteps();
createOrderCoupon("140.00", "10% on food", "-10.00", "90");
Tour.register("PosLoyaltyPointsDiscountWithDomainProgramDomain", { test: true, url: "/pos/web" }, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup(),
ProductScreen.do.clickHomeCategory(),
ProductScreen.do.clickPartnerButton(),
ProductScreen.do.clickCustomer("partner_a"),
ProductScreen.exec.addOrderline("product_a", "1"),
PosLoyalty.check.hasRewardLine('10% on your order', '-10.00');
PosLoyalty.check.orderTotalIs('90'),
PosLoyalty.check.pointsAwardedAre("90"),
PosLoyalty.exec.finalizeOrder("Cash", "90"),
Tour.register("PosLoyaltyPointsGlobalDiscountProgramNoDomain", { test: true, url: "/pos/web" }, getSteps());
startSteps();
ProductScreen.do.clickHomeCategory();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer("partner_a");
ProductScreen.do.clickDisplayedProduct('Test Product A');
PosLoyalty.check.checkNoClaimableRewards();
ProductScreen.check.selectedOrderlineHas('Test Product A', '1.00', '100.00');
PosLoyalty.exec.finalizeOrder("Cash");
Tour.register('PosLoyaltyArchivedRewardProductsInactive', {test: true, url: '/pos/web'}, getSteps());
startSteps();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer("partner_a");
ProductScreen.do.clickDisplayedProduct('Test Product A');
PosLoyalty.check.isRewardButtonHighlighted(true);
ProductScreen.check.selectedOrderlineHas('Test Product A', '1.00', '100.00');
PosLoyalty.exec.finalizeOrder("Cash");
Tour.register('PosLoyaltyArchivedRewardProductsActive', {test: true, url: '/pos/web'}, getSteps());
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer("partner_a");
ProductScreen.exec.addOrderline("Test Product A", "5"),
ProductScreen.do.clickDisplayedProduct('Test Product B');
PosLoyalty.check.hasRewardLine('10% on your order', '-3.00');
PosLoyalty.check.hasRewardLine('10% on Test Product B', '-0.45');
PosLoyalty.exec.finalizeOrder("Cash");
Tour.register('PosLoyalty2DiscountsSpecificGlobal', {test: true, url: '/pos/web'}, getSteps());

View file

@ -1,218 +0,0 @@
odoo.define('pos_loyalty.tour.PosCouponTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
const { Do: ProductScreenDo } = require('point_of_sale.tour.ProductScreenTourMethods');
const { Do: PaymentScreenDo, Check: PaymentScreenCheck } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { Do: ReceiptScreenDo } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { Do: ChromeDo } = require('point_of_sale.tour.ChromeTourMethods');
const ProductScreen = { do: new ProductScreenDo() };
const PaymentScreen = { do: new PaymentScreenDo(), check: new PaymentScreenCheck() };
const ReceiptScreen = { do: new ReceiptScreenDo() };
const Chrome = { do: new ChromeDo() };
class Do {
selectRewardLine(rewardName) {
return [
{
content: 'select reward line',
trigger: `.orderline.program-reward .product-name:contains("${rewardName}")`,
},
{
content: 'check reward line if selected',
trigger: `.orderline.selected.program-reward .product-name:contains("${rewardName}")`,
run: function () {}, // it's a check
},
];
}
enterCode(code) {
const steps = [
{
content: 'open code input dialog',
trigger: '.control-button:contains("Enter Code")',
},
{
content: `enter code value: ${code}`,
trigger: '.popup-textinput input[type="text"]',
run: `text ${code}`,
},
{
content: 'confirm inputted code',
trigger: '.popup-textinput .button.confirm',
},
{
content: 'verify popup is closed',
trigger: 'body:not(:has(.popup-textinput))',
run: function () {}, // it's a check
},
];
return steps;
}
resetActivePrograms() {
return [
{
content: 'open code input dialog',
trigger: '.control-button:contains("Reset Programs")',
},
];
}
clickRewardButton() {
return [
{
content: 'open reward dialog',
trigger: '.control-button:contains("Reward")',
},
];
}
clickEWalletButton(text = 'eWallet') {
return [{ trigger: `.control-button:contains("${text}")` }];
}
claimReward(rewardName) {
return [
{
content: 'open reward dialog',
trigger: '.control-button:contains("Reward")',
},
{
content: 'select reward',
trigger: `.selection-item:contains("${rewardName}")`,
}
];
}
unselectPartner() {
return [{ trigger: '.unselect-tag' }];
}
clickDiscountButton() {
return [
{
content: 'click discount button',
trigger: '.js_discount',
},
];
}
clickConfirmButton() {
return [
{
content: 'click confirm button',
trigger: '.button.confirm',
},
];
}
}
class Check {
hasRewardLine(rewardName, amount, qty) {
const steps = [
{
content: 'check if reward line is there',
trigger: `.orderline.program-reward span.product-name:contains("${rewardName}")`,
run: function () {},
},
{
content: 'check if the reward price is correct',
trigger: `.orderline.program-reward span.price:contains("${amount}")`,
run: function () {},
},
];
if (qty) {
steps.push({
content: 'check if the reward qty is correct',
trigger: `.order .orderline.program-reward .product-name:contains("${rewardName}") ~ .info-list em:contains("${qty}")`,
run: function () {},
});
}
return steps;
}
orderTotalIs(total_str) {
return [
{
content: 'order total contains ' + total_str,
trigger: '.order .total .value:contains("' + total_str + '")',
run: function () {}, // it's a check
},
];
}
checkNoClaimableRewards() {
return [
{
content: 'check that no reward can be claimed',
trigger: ".control-button:contains('Reward'):not(.highlight)",
run: function () {}, // it's a check
}
]
}
isRewardButtonHighlighted(isHighlighted) {
return [
{
trigger: isHighlighted
? '.control-button.highlight:contains("Reward")'
: '.control-button:contains("Reward"):not(:has(.highlight))',
run: function () {}, // it's a check
},
];
}
eWalletButtonState({ highlighted, text = 'eWallet' }) {
return [
{
trigger: highlighted
? `.control-button.highlight:contains("${text}")`
: `.control-button:contains("${text}"):not(:has(.highlight))`,
run: function () {}, // it's a check
},
];
}
customerIs(name) {
return [
{
trigger: `.actionpad button.set-partner:contains("${name}")`,
run: function () {},
}
]
}
pointsAwardedAre(points_str) {
return [
{
content: 'loyalty points awarded ' + points_str,
trigger: '.loyalty-points-won.value:contains("' + points_str + '")',
run: function () {}, // it's a check
},
];
}
}
class Execute {
constructor() {
this.do = new Do();
this.check = new Check();
}
finalizeOrder(paymentMethod, amount) {
const actions = [
...ProductScreen.do.clickPayButton(),
...PaymentScreen.do.clickPaymentMethod(paymentMethod),
];
if (amount) {
actions.push(...PaymentScreen.do.pressNumpad([...amount].join(' ')));
} else {
actions.push(
...PaymentScreen.check.remainingIs('0.0'),
...PaymentScreen.check.changeIs('0.0'),
)
}
actions.push(
...PaymentScreen.do.clickValidate(),
...ReceiptScreen.do.clickNextOrder(),
);
return actions;
}
removeRewardLine(name) {
return [
...this.do.selectRewardLine(name),
...ProductScreen.do.pressNumpad('Backspace'),
...Chrome.do.confirmPopup(),
];
}
}
return createTourMethods('PosLoyalty', Do, Check, Execute);
});

View file

@ -1,36 +0,0 @@
/** @odoo-module **/
import { PosLoyalty } from 'pos_loyalty.tour.PosCouponTourMethods';
import { ProductScreen } from 'point_of_sale.tour.ProductScreenTourMethods';
import { getSteps, startSteps } from 'point_of_sale.tour.utils';
import Tour from 'web_tour.tour';
// First tour should not get any automatic rewards
startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
// Not valid -> date
ProductScreen.exec.addOrderline('Whiteboard Pen', '5');
PosLoyalty.check.checkNoClaimableRewards();
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyValidity1', { test: true, url: '/pos/web' }, getSteps());
// Second tour
startSteps();
ProductScreen.do.clickHomeCategory();
// Valid
ProductScreen.exec.addOrderline('Whiteboard Pen', '5');
PosLoyalty.check.hasRewardLine('90% on the cheapest product', '-2.88');
PosLoyalty.exec.finalizeOrder('Cash');
// Not valid -> usage
ProductScreen.exec.addOrderline('Whiteboard Pen', '5');
PosLoyalty.check.checkNoClaimableRewards();
PosLoyalty.exec.finalizeOrder('Cash');
Tour.register('PosLoyaltyValidity2', { test: true, url: '/pos/web' }, getSteps());

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="PromoCodeButton" owl="1">
<span class="control-button">
<i class="fa fa-barcode"></i>
<span> </span>
<span>Enter Code</span>
</span>
</t>
</templates>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ResetProgramsButton" owl="1">
<span class="control-button">
<i class="fa fa-star"></i>
<span> </span>
<span>Reset Programs</span>
</span>
</t>
</templates>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="RewardButton" owl="1">
<span class="control-button" t-att-class="hasClaimableRewards() ? 'highlight' : ''">
<i class="fa fa-star"></i>
<span> </span>
<span>Reward</span>
</span>
</t>
</templates>

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.eWalletButton" owl="1">
<t t-set="_order" t-value="env.pos.get_order()" />
<t t-set="_orderTotal" t-value="_order.get_total_with_tax()" />
<t t-set="_eWalletPrograms" t-value="_getEWalletPrograms()" />
<t t-set="_eWalletRewards" t-value="_getEWalletRewards(_order)" />
<span class="control-button" t-att-class="_shouldBeHighlighted(_orderTotal, _eWalletPrograms, _eWalletRewards) ? 'highlight' : ''" t-on-click="_onClickWalletButton">
<i class="fa fa-credit-card"></i>
<span> </span>
<span><t t-esc="_getText(_orderTotal, _eWalletPrograms, _eWalletRewards)" /></span>
</span>
</t>
</templates>

View file

@ -1,68 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_coupon.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('pos-receipt')]//div[hasclass('before-footer')]" position="inside">
<t t-if='receipt.loyaltyStats'>
<t t-foreach="receipt.loyaltyStats" t-as="_loyaltyStat" t-key="_loyaltyStat.couponId">
<!-- Show only if portal_visible. -->
<div t-if="_loyaltyStat.program.portal_visible and (_loyaltyStat.points.won || _loyaltyStat.points.spent)" class='loyalty'>
<span class="pos-receipt-center-align">
<div>--------------------------------</div>
<br/>
<div t-esc='_loyaltyStat.program.name' class="pos-receipt-title" />
<br />
</span>
<t t-if='_loyaltyStat.points.won'>
<div><t t-esc="_loyaltyStat.points.name"/> Won: <span t-esc='_loyaltyStat.points.won' class="pos-receipt-right-align"/></div>
</t>
<t t-if='_loyaltyStat.points.spent'>
<div><t t-esc="_loyaltyStat.points.name"/> Spent: <span t-esc='_loyaltyStat.points.spent' class="pos-receipt-right-align"/></div>
</t>
<!-- Don't use points.total, it's wrong in this context (after the order synced). -->
<!-- Show balance as it's updated during _postPushOrderResolve. -->
<t t-if='_loyaltyStat.points.balance'>
<div>Balance <t t-esc="_loyaltyStat.points.name"/>: <span t-esc='_loyaltyStat.points.balance' class="pos-receipt-right-align"/></div>
</t>
<br />
</div>
</t>
<br/>
<div>Customer <span t-esc='receipt.partner.name' class='pos-receipt-right-align'/></div>
</t>
<t t-if="receipt.new_coupon_info and receipt.new_coupon_info.length !== 0">
<div class="pos-coupon-rewards">
<div>------------------------</div>
<br/>
<div>
Coupon Codes
</div>
<t t-foreach="receipt.new_coupon_info" t-as="coupon_info" t-key="coupon_info.code">
<div class="coupon-container">
<div style="font-size: 110%;">
<t t-esc="coupon_info['program_name']"/>
</div>
<div>
<span>Valid until: </span>
<t t-if="coupon_info['expiration_date']">
<t t-esc="coupon_info['expiration_date']"/>
</t>
<t t-else="">
no expiration
</t>
</div>
<div>
<img t-att-src="'/report/barcode/Code128/'+coupon_info['code']" style="width:200px;height:50px" alt="Barcode"/>
</div>
<div>
<t t-esc="coupon_info['code']"/>
</div>
</div>
</t>
</div>
</t>
</xpath>
</t>
</templates>

View file

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="OrderSummary" t-inherit="point_of_sale.OrderSummary" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('summary')]" position="after">
<div class="summary clearfix">
<t t-set="_loyaltyStats" t-value="getLoyaltyPoints()"/>
<t t-foreach="_loyaltyStats" t-as="_loyaltyStat" t-key="_loyaltyStat.couponId">
<t t-if="_loyaltyStat.points.won || _loyaltyStat.points.spent">
<div class='loyalty-points'>
<div class='loyalty-points-title'>
<t t-esc="_loyaltyStat.points.name"/>
</div>
<t t-if='_loyaltyStat.points.balance'>
<div class="loyalty-points-balance">
<span class='value'><t t-esc='_loyaltyStat.points.balance'/></span>
</div>
</t>
<div>
<t t-if='_loyaltyStat.points.won'>
<span class="value loyalty-points-won">+<t t-esc='_loyaltyStat.points.won'/></span>
</t>
<t t-if='_loyaltyStat.points.spent'>
<span class="value loyalty-points-spent">-<t t-esc='_loyaltyStat.points.spent'/></span>
</t>
</div>
<div class='loyalty-points-total'>
<span class='value'><t t-esc='_loyaltyStat.points.total'/></span>
</div>
</div>
</t>
<t t-else="">
<div></div>
</t>
</t>
</div>
</xpath>
</t>
</templates>

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_loyalty.Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension" owl="1">
<xpath expr="//ul[hasclass('info-list')]" position="attributes">
<attribute name="t-if">!_isGiftCardOrEWalletReward()</attribute>
</xpath>
<xpath expr="//ul[hasclass('info-list')]" position="after">
<ul t-else="" class="info-list">
Current Balance: <span t-esc="_getGiftCardOrEWalletBalance()"/>
</ul>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,219 @@
import * as PosLoyalty from "@pos_loyalty/../tests/tours/utils/pos_loyalty_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as TicketScreen from "@point_of_sale/../tests/pos/tours/utils/ticket_screen_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as PartnerList from "@point_of_sale/../tests/pos/tours/utils/partner_list_util";
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util";
import * as Numpad from "@point_of_sale/../tests/generic_helpers/numpad_util";
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
import { negateStep } from "@point_of_sale/../tests/generic_helpers/utils";
import { delay } from "@web/core/utils/concurrency";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("EWalletProgramTour1", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Topup 50$ for partner_aaa
ProductScreen.clickDisplayedProduct("Top-up eWallet"),
PosLoyalty.orderTotalIs("50.00"),
ProductScreen.clickPayButton(false),
// If there's no partner, we asked to redirect to the partner list screen.
Dialog.confirm(),
PartnerList.clickPartner("AAAAAAA"),
PosLoyalty.finalizeOrder("Cash", "50"),
// Topup 10$ for partner_bbb
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("BBBBBBB"),
ProductScreen.addOrderline("Top-up eWallet", "1", "10"),
PosLoyalty.orderTotalIs("10.00"),
PosLoyalty.finalizeOrder("Cash", "10"),
// Check numpad visibility when clicking on eWallet orderline
ProductScreen.addOrderline("Whiteboard Pen"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAAA"),
PosLoyalty.eWalletButtonState({
highlighted: true,
text: getEWalletText("Pay"),
click: true,
}),
PosLoyalty.orderTotalIs("0.00"),
...ProductScreen.clickLine("eWallet"),
// Added a small wait because the clickLine function uses a 300ms timeout
{
content: "Wait 300ms after clicking orderline",
trigger: "body",
async run() {
await delay(300);
},
},
Numpad.isVisible(),
...Order.hasLine({
withClass: ".selected",
run: "click",
productName: "eWallet",
quantity: "1.0",
}),
{
content: "Wait 300ms after clicking orderline",
trigger: "body",
async run() {
await delay(300);
},
},
negateStep(Numpad.isVisible()),
{
content: "Click Current Balance line in orderline",
trigger: ".orderline li:contains(Current Balance:)",
run: "click",
},
Numpad.isVisible(),
].flat(),
});
const getEWalletText = (suffix) => "eWallet" + (suffix !== "" ? ` ${suffix}` : "");
registry.category("web_tour.tours").add("EWalletProgramTour2", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.addOrderline("Whiteboard Pen", "2", "6", "12.00"),
PosLoyalty.eWalletButtonState({ highlighted: false }),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAAA"),
PosLoyalty.eWalletButtonState({
highlighted: true,
text: getEWalletText("Pay"),
click: true,
}),
PosLoyalty.orderTotalIs("0.00"),
PosLoyalty.finalizeOrder("Cash", "0"),
// Consume partner_bbb's full eWallet.
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("BBBBBBB"),
PosLoyalty.eWalletButtonState({ highlighted: false }),
ProductScreen.addOrderline("Desk Pad", "6", "6", "36.00"),
PosLoyalty.eWalletButtonState({
highlighted: true,
text: getEWalletText("Pay"),
click: true,
}),
PosLoyalty.eWalletButtonState({ highlighted: false }),
PosLoyalty.orderTotalIs("26.00"),
PosLoyalty.finalizeOrder("Cash", "26"),
// Switching partners should work.
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("BBBBBBB"),
ProductScreen.addOrderline("Desk Pad", "2", "19", "38.00"),
PosLoyalty.eWalletButtonState({ highlighted: false }),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAAA"),
PosLoyalty.eWalletButtonState({
highlighted: true,
text: getEWalletText("Pay"),
click: true,
}),
PosLoyalty.orderTotalIs("0.00"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("BBBBBBB"),
PosLoyalty.eWalletButtonState({ highlighted: false }),
PosLoyalty.orderTotalIs("38.00"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAAA"),
PosLoyalty.eWalletButtonState({
highlighted: true,
text: getEWalletText("Pay"),
click: true,
}),
PosLoyalty.orderTotalIs("0.00"),
PosLoyalty.finalizeOrder("Cash", "0"),
// Refund with eWallet.
// - Make an order to refund.
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("BBBBBBB"),
ProductScreen.addOrderline("Whiteboard Pen", "1", "20", "20.00"),
PosLoyalty.orderTotalIs("20.00"),
PosLoyalty.finalizeOrder("Cash", "20"),
// - Refund order.
...ProductScreen.clickRefund(),
TicketScreen.filterIs("Paid"),
TicketScreen.selectOrder("2004"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.isShown(),
PosLoyalty.eWalletButtonState({
highlighted: true,
text: getEWalletText("Refund"),
click: true,
}),
PosLoyalty.orderTotalIs("0.00"),
PosLoyalty.finalizeOrder("Cash", "0"),
].flat(),
});
registry.category("web_tour.tours").add("ExpiredEWalletProgramTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA"),
ProductScreen.addOrderline("Whiteboard Pen", "2", "6", "12.00"),
PosLoyalty.eWalletButtonState({ highlighted: false, click: true }),
Dialog.is({ title: "No valid eWallet found" }),
Dialog.confirm(),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyPointsEwallet", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA"),
PosLoyalty.eWalletButtonState({ highlighted: false }),
ProductScreen.addOrderline("product_a", "1"),
PosLoyalty.eWalletButtonState({
highlighted: true,
text: getEWalletText("Pay"),
click: true,
}),
PosLoyalty.pointsAwardedAre("100"),
PosLoyalty.finalizeOrder("Cash", "90.00"),
].flat(),
});
registry.category("web_tour.tours").add("EWalletLoyaltyHistory", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Top-up eWallet"),
PosLoyalty.orderTotalIs("50.00"),
ProductScreen.clickPartnerButton(),
PartnerList.clickPartner("AAAAAAA"),
PosLoyalty.finalizeOrder("Cash", "50"),
ProductScreen.addOrderline("Whiteboard Pen", "2", "6", "12.00"),
PosLoyalty.eWalletButtonState({ highlighted: false }),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAAA"),
PosLoyalty.eWalletButtonState({
highlighted: true,
text: getEWalletText("Pay"),
click: true,
}),
PosLoyalty.orderTotalIs("0.00"),
PosLoyalty.finalizeOrder("Cash", "0"),
].flat(),
});

View file

@ -0,0 +1,311 @@
import * as PosLoyalty from "@pos_loyalty/../tests/tours/utils/pos_loyalty_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import { registry } from "@web/core/registry";
import * as TicketScreen from "@point_of_sale/../tests/pos/tours/utils/ticket_screen_util";
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
import * as ReceiptScreen from "@point_of_sale/../tests/pos/tours/utils/receipt_screen_util";
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util";
registry.category("web_tour.tours").add("GiftCardProgramTour1", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Gift Card"),
PosLoyalty.orderTotalIs("50.00"),
PosLoyalty.finalizeOrder("Cash", "50"),
].flat(),
});
registry.category("web_tour.tours").add("GiftCardProgramTour2", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.enterCode("044123456"),
PosLoyalty.orderTotalIs("0.00"),
PosLoyalty.finalizeOrder("Cash", "0"),
].flat(),
});
registry.category("web_tour.tours").add("GiftCardWithRefundtTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Magnetic Board", "1"), // 1.98
PosLoyalty.orderTotalIs("1.98"),
PosLoyalty.finalizeOrder("Cash", "20"),
...ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
Order.hasLine({
withClass: ".selected",
productName: "Magnetic Board",
}),
ProductScreen.clickNumpad("1"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
PaymentScreen.clickBack(),
ProductScreen.isShown(),
ProductScreen.clickLine("Magnetic Board", "-1"),
ProductScreen.selectedOrderlineHas("Magnetic Board", "-1"),
ProductScreen.addOrderline("Gift Card", "1"),
ProductScreen.selectedOrderlineHas("Gift Card", "1"),
PosLoyalty.orderTotalIs("0.0"),
].flat(),
});
registry.category("web_tour.tours").add("GiftCardProgramPriceNoTaxTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Use gift card
ProductScreen.addOrderline("Magnetic Board", "1", "1.98", "1.98"),
PosLoyalty.enterCode("043123456"),
Dialog.confirm(),
ProductScreen.clickOrderline("Gift Card"),
ProductScreen.selectedOrderlineHas("Gift Card", "1", "-1.00"),
PosLoyalty.orderTotalIs("0.98"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyPointsGiftcard", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Gift Card", "1", "50", "50"),
PosLoyalty.createManualGiftCard("044123456", 50),
PosLoyalty.orderTotalIs("50.00"),
PosLoyalty.finalizeOrder("Cash", "50"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA"),
ProductScreen.addOrderline("product_a", "1"),
PosLoyalty.enterCode("044123456"),
PosLoyalty.orderTotalIs("50.00"),
PosLoyalty.pointsAwardedAre("100"),
PosLoyalty.finalizeOrder("Cash", "50"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyGiftCardTaxes", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Gift Card", "1", "50", "50"),
PosLoyalty.createManualGiftCard("044123456", 50),
PosLoyalty.orderTotalIs("50.00"),
PosLoyalty.finalizeOrder("Cash", "50"),
ProductScreen.clickDisplayedProduct("Test Product A"),
PosLoyalty.enterCode("044123456"),
PosLoyalty.orderTotalIs("50.00"),
ProductScreen.checkTaxAmount("-6.52"),
].flat(),
});
registry.category("web_tour.tours").add("PhysicalGiftCardProgramSaleTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Gift Card", "1", "50", "50"),
PosLoyalty.createManualGiftCard("test-card-0000", 125),
ProductScreen.selectedOrderlineHas("Gift Card", "1", "125"),
PosLoyalty.orderTotalIs("125"),
PosLoyalty.finalizeOrder("Cash", "125"),
ProductScreen.addOrderline("Gift Card", "1", "50", "50"),
PosLoyalty.createManualGiftCard("test-card-0001", 100),
PosLoyalty.clickPhysicalGiftCard("test-card-0001"),
ProductScreen.selectedOrderlineHas("Gift Card", "1", "100"),
ProductScreen.addOrderline("Gift Card", "1", "50", "50"),
PosLoyalty.createManualGiftCard("new-card-0001", 250),
PosLoyalty.clickPhysicalGiftCard("new-card-0001"),
ProductScreen.selectedOrderlineHas("Gift Card", "1", "250"),
PosLoyalty.orderTotalIs("350"),
PosLoyalty.finalizeOrder("Cash", "350"),
].flat(),
});
registry.category("web_tour.tours").add("MultiplePhysicalGiftCardProgramSaleTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Gift Card"),
PosLoyalty.clickGiftCardProgram("Gift Cards1"),
PosLoyalty.createManualGiftCard("test-card-0000", 125),
PosLoyalty.clickGiftCardProgram("Gift Cards"),
ProductScreen.selectedOrderlineHas("Gift Card", "1.00", "125"),
PosLoyalty.orderTotalIs("125"),
PosLoyalty.finalizeOrder("Cash", "125"),
ProductScreen.clickDisplayedProduct("Gift Card"),
PosLoyalty.clickGiftCardProgram("Gift Cards2"),
PosLoyalty.createManualGiftCard("test-card-0001", 125),
PosLoyalty.clickGiftCardProgram("Gift Cards2"),
ProductScreen.selectedOrderlineHas("Gift Card", "1.00", "125"),
PosLoyalty.orderTotalIs("125"),
PosLoyalty.finalizeOrder("Cash", "125"),
ProductScreen.clickDisplayedProduct("Gift Card"),
PosLoyalty.clickGiftCardProgram("Gift Cards3"),
PosLoyalty.createManualGiftCard("test-card-0002", 125),
PosLoyalty.clickGiftCardProgram("Gift Cards3"),
ProductScreen.selectedOrderlineHas("Gift Card", "1.00", "125"),
PosLoyalty.orderTotalIs("125"),
PosLoyalty.finalizeOrder("Cash", "125"),
].flat(),
});
registry.category("web_tour.tours").add("GiftCardProgramInvoice", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Gift Card"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("A Test Partner"),
PosLoyalty.orderTotalIs("50.00"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("test_gift_card_no_date", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Gift Card"),
PosLoyalty.createManualGiftCard("test", "42", ""),
PosLoyalty.finalizeOrder("Cash", "42"),
].flat(),
});
registry.category("web_tour.tours").add("test_physical_gift_card_invoiced", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AABBCC Test Partner"),
ProductScreen.clickDisplayedProduct("Gift Card"),
PosLoyalty.createManualGiftCard("test-card-1234", 125),
ProductScreen.selectedOrderlineHas("Gift Card", "1.00", "125"),
PosLoyalty.orderTotalIs("125"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickInvoiceButton(),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("EmptyProductScreenTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.isEmpty(),
ProductScreen.loadSampleButtonIsThere(),
].flat(),
});
registry.category("web_tour.tours").add("test_physical_gift_card", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Gift Card"),
// Gift card cannot be used as it's already linked to a partner
PosLoyalty.useExistingLoyaltyCard("gift_card_partner", false),
// Gift card cannot be used as it's expired
PosLoyalty.useExistingLoyaltyCard("gift_card_expired", false),
// Gift card is already sold
PosLoyalty.useExistingLoyaltyCard("gift_card_sold", false),
// Use gift_card_generated_but_not_sold - Warning is triggered
PosLoyalty.enterCode("gift_card_generated_but_not_sold"),
Dialog.cancel(),
// Sell the unsold gift card
PosLoyalty.useExistingLoyaltyCard("gift_card_generated_but_not_sold", true),
ProductScreen.selectedOrderlineHas("Gift Card", "1", "60.00"),
ProductScreen.clickNumpad("2"), // Cannot edit a physical gift card
Dialog.confirm(), // Warning is triggered
PosLoyalty.orderTotalIs("60.00"),
PosLoyalty.finalizeOrder("Cash", "60"),
// Use gift_card_valid - No warning should be triggered
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.enterCode("gift_card_valid"),
ProductScreen.clickLine("Gift Card"),
ProductScreen.selectedOrderlineHas("Gift Card", "1", "-3.20"),
PosLoyalty.orderTotalIs("0.00"),
PosLoyalty.finalizeOrder("Cash", "0"),
// Use gift_card_partner - No warning should be triggered
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.enterCode("gift_card_partner"),
ProductScreen.clickLine("Gift Card"),
ProductScreen.selectedOrderlineHas("Gift Card", "1", "-3.20"),
PosLoyalty.orderTotalIs("0.00"),
PosLoyalty.finalizeOrder("Cash", "0"),
// Use gift_card_sold - Warning should be triggered
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.enterCode("gift_card_sold"),
ProductScreen.clickLine("Gift Card"),
ProductScreen.selectedOrderlineHas("Gift Card", "1", "-6.40"),
PosLoyalty.orderTotalIs("0.00"),
PosLoyalty.finalizeOrder("Cash", "0"),
// Use gift_card_generated_but_not_sold - No warning should be triggered
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.enterCode("gift_card_generated_but_not_sold"),
ProductScreen.clickLine("Gift Card"),
ProductScreen.selectedOrderlineHas("Gift Card", "1", "-12.80"),
PosLoyalty.orderTotalIs("0.00"),
PosLoyalty.finalizeOrder("Cash", "0"),
// Try to use expired gift card
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.enterCode("gift_card_expired"),
PosLoyalty.orderTotalIs("9.60"),
PosLoyalty.finalizeOrder("Cash", "9.60"),
// Sell a new gift card with a partner
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("A powerful PoS man!"),
ProductScreen.clickDisplayedProduct("Gift Card"),
ProductScreen.clickNumpad("Price", "9", "9", "9"),
PosLoyalty.orderTotalIs("999.00"),
PosLoyalty.finalizeOrder("Cash", "999"),
].flat(),
});
registry.category("web_tour.tours").add("test_multiple_physical_gift_card_sale", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Gift Card"),
PosLoyalty.createManualGiftCard("test-card-0000", 125),
ProductScreen.selectedOrderlineHas("Gift Card", "1.00", "125"),
ProductScreen.addOrderline("Gift Card", "1", "0", "0"),
PosLoyalty.createManualGiftCard("test-card-0001", 100),
PosLoyalty.finalizeOrder("Cash", "225"),
].flat(),
});

View file

@ -0,0 +1,19 @@
import * as PosLoyalty from "@pos_loyalty/../tests/tours/utils/pos_loyalty_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("LoyaltyHistoryTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Whiteboard Pen", "1"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Test Partner"),
PosLoyalty.orderTotalIs("10"),
PosLoyalty.finalizeOrder("Cash", "10"),
].flat(),
});

View file

@ -0,0 +1,79 @@
import * as PosLoyalty from "@pos_loyalty/../tests/tours/utils/pos_loyalty_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as SelectionPopup from "@point_of_sale/../tests/generic_helpers/selection_popup_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import { registry } from "@web/core/registry";
const getEWalletText = (suffix) => "eWallet" + (suffix !== "" ? ` ${suffix}` : "");
registry.category("web_tour.tours").add("MultipleGiftWalletProgramsTour", {
steps: () =>
[
// One card for gift_card_1.
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Gift Card"),
SelectionPopup.has("gift_card_1"),
SelectionPopup.has("gift_card_2"),
SelectionPopup.has("gift_card_1", { run: "click" }),
ProductScreen.selectedOrderlineHas("Gift Card"),
ProductScreen.clickNumpad("Price"),
ProductScreen.modeIsActive("Price"),
ProductScreen.clickNumpad("1", "0"),
PosLoyalty.orderTotalIs("10.00"),
PosLoyalty.finalizeOrder("Cash", "10"),
// One card for gift_card_1.
ProductScreen.clickDisplayedProduct("Gift Card"),
SelectionPopup.has("gift_card_2", { run: "click" }),
ProductScreen.selectedOrderlineHas("Gift Card"),
ProductScreen.clickNumpad("Price"),
ProductScreen.modeIsActive("Price"),
ProductScreen.clickNumpad("2", "0"),
PosLoyalty.orderTotalIs("20.00"),
PosLoyalty.finalizeOrder("Cash", "20"),
// Top up ewallet_1 for AAAAAAA.
ProductScreen.clickDisplayedProduct("Top-up eWallet"),
SelectionPopup.has("ewallet_1"),
SelectionPopup.has("ewallet_2"),
SelectionPopup.has("ewallet_1", { run: "click" }),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAAA"),
ProductScreen.clickNumpad("Price"),
ProductScreen.modeIsActive("Price"),
ProductScreen.clickNumpad("3", "0"),
PosLoyalty.orderTotalIs("30.00"),
PosLoyalty.finalizeOrder("Cash", "30"),
// Top up ewallet_2 for AAAAAAA.
ProductScreen.clickDisplayedProduct("Top-up eWallet"),
SelectionPopup.has("ewallet_2", { run: "click" }),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAAA"),
ProductScreen.clickNumpad("Price"),
ProductScreen.modeIsActive("Price"),
ProductScreen.clickNumpad("4", "0"),
PosLoyalty.orderTotalIs("40.00"),
PosLoyalty.finalizeOrder("Cash", "40"),
// Top up ewallet_1 for BBBBBBB.
ProductScreen.clickDisplayedProduct("Top-up eWallet"),
SelectionPopup.has("ewallet_1", { run: "click" }),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("BBBBBBB"),
PosLoyalty.orderTotalIs("50.00"),
PosLoyalty.finalizeOrder("Cash", "50"),
// Consume 12$ from ewallet_1 of AAAAAAA.
ProductScreen.addOrderline("Whiteboard Pen", "2", "6", "12.00"),
PosLoyalty.eWalletButtonState({ highlighted: false }),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAAAAA"),
PosLoyalty.eWalletButtonState({
highlighted: true,
text: getEWalletText("Pay"),
click: true,
}),
SelectionPopup.has("ewallet_1"),
SelectionPopup.has("ewallet_2"),
SelectionPopup.has("ewallet_1", { run: "click" }),
PosLoyalty.orderTotalIs("0.00"),
PosLoyalty.finalizeOrder("Cash", "0"),
].flat(),
});

View file

@ -0,0 +1,403 @@
import * as PosLoyalty from "@pos_loyalty/../tests/tours/utils/pos_loyalty_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as combo from "@point_of_sale/../tests/pos/tours/utils/combo_popup_util";
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
import { inLeftSide } from "@point_of_sale/../tests/pos/tours/utils/common";
import { registry } from "@web/core/registry";
import { negate } from "@point_of_sale/../tests/generic_helpers/utils";
registry.category("web_tour.tours").add("PosLoyaltyLoyaltyProgram1", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Order1: Generates 2 points.
ProductScreen.addOrderline("Whiteboard Pen", "2"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Test Partner"),
PosLoyalty.orderTotalIs("6.40"),
PosLoyalty.finalizeOrder("Cash", "10"),
// Order2: Consumes points to get free product.
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Test Partner"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "1"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "2"),
// At this point, AAA Test Partner has 4 points.
PosLoyalty.isMoreControlButtonActive(true),
PosLoyalty.isRewardButtonHighlighted(true),
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "3"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1"),
PosLoyalty.isMoreControlButtonActive(false),
PosLoyalty.isRewardButtonHighlighted(false),
PosLoyalty.orderTotalIs("6.40"),
PosLoyalty.finalizeOrder("Cash", "10"),
// Order3: Generate 4 points.
// - Initially gets free product, but was removed automatically by changing the
// number of items to zero.
// - It's impossible to checked here if the free product reward is really removed
// so we check in the backend the number of orders that consumed the reward.
ProductScreen.addOrderline("Whiteboard Pen", "4"),
PosLoyalty.orderTotalIs("12.80"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Test Partner"),
PosLoyalty.isRewardButtonHighlighted(true),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1"),
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.clickNumpad("⌫"),
// At this point, the reward line should have been automatically removed
// because there is not enough points to purchase it. Unfortunately, we
// can't check that here.
PosLoyalty.orderTotalIs("0.00"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "1.00"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "2.00"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "3.00"),
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
ProductScreen.selectedOrderlineHas("Whiteboard Pen", "4.00"),
PosLoyalty.isRewardButtonHighlighted(true),
PosLoyalty.orderTotalIs("12.80"),
PosLoyalty.finalizeOrder("Cash", "20"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyLoyaltyProgram2", {
steps: () =>
[
// Order1: Immediately set the customer to Test Partner AAA which has 4 points.
// - He has enough points to purchase a free product but since there is still
// no product in the order, reward button should not yet be highlighted.
// - Furthermore, clicking the reward product should not add it as reward product.
Chrome.startPoS(),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Test Partner"),
// No item in the order, so reward button is off.
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.isRewardButtonHighlighted(true),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1"),
PosLoyalty.isRewardButtonHighlighted(false),
PosLoyalty.orderTotalIs("3.20"),
PosLoyalty.finalizeOrder("Cash", "10"),
// Order2: Generate 4 points for CCC Test Partner.
// - Reference: Order2_CCC
// - But set BBB Test Partner first as the customer.
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("BBB Test Partner"),
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "1"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "2"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "3"),
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "4"),
PosLoyalty.isRewardButtonHighlighted(true),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("CCC Test Partner"),
PosLoyalty.customerIs("CCC Test Partner"),
PosLoyalty.orderTotalIs("12.80"),
PosLoyalty.finalizeOrder("Cash", "20"),
// Order3: Generate 3 points for BBB Test Partner.
// - Reference: Order3_BBB
// - But set CCC Test Partner first as the customer.
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("CCC Test Partner"),
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.addOrderline("Whiteboard Pen", "3"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("BBB Test Partner"),
PosLoyalty.customerIs("BBB Test Partner"),
PosLoyalty.orderTotalIs("9.60"),
PosLoyalty.finalizeOrder("Cash", "10"),
// Order4: Should not have reward because the customer will be removed.
// - Reference: Order4_no_reward
ProductScreen.clickDisplayedProduct("Whiteboard Pen", true, "1"),
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("CCC Test Partner"),
PosLoyalty.isRewardButtonHighlighted(true),
PosLoyalty.claimReward("Free Product - Whiteboard Pen"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1"),
ProductScreen.clickPartnerButton(),
// This deselects the customer.
PosLoyalty.unselectPartner(),
PosLoyalty.customerIs("Customer"),
PosLoyalty.orderTotalIs("6.40"),
PosLoyalty.finalizeOrder("Cash", "10"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyChangeRewardQty", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("DDD Test Partner"),
ProductScreen.addOrderline("Desk Organizer", "1"),
PosLoyalty.isRewardButtonHighlighted(true),
PosLoyalty.claimReward("Free Product - Whiteboard Pen"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1"),
ProductScreen.clickNumpad("Qty"),
ProductScreen.clickNumpad("1"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyLoyaltyProgram3", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Generates 10.2 points and use points to get the reward product with zero sale price
ProductScreen.addOrderline("Desk Organizer", "2"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Test Partner"),
// At this point, the free_product program is triggered.
// The reward button should be highlighted.
PosLoyalty.isRewardButtonHighlighted(true, true),
PosLoyalty.claimReward("Free Product - Whiteboard Pen"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-1.00", "1"),
PosLoyalty.orderTotalIs("10.2"),
PosLoyalty.finalizeOrder("Cash", "10.2"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyPromotion", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Partner"),
ProductScreen.addOrderline("Test Product 1", "1", "100"),
ProductScreen.totalAmountIs("90.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyDontGrantPointsForRewardOrderLines", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("A Test Partner"),
ProductScreen.addOrderline("Desk Organizer", "1"),
ProductScreen.addOrderline("Whiteboard Pen", "1"),
PosLoyalty.isRewardButtonHighlighted(true, true),
PosLoyalty.claimReward("100% on the cheapest product"),
PosLoyalty.orderTotalIs("5.10"),
PosLoyalty.finalizeOrder("Cash", "5.10"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyNextOrderCouponExpirationDate", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Desk Organizer", "3"),
PosLoyalty.finalizeOrder("Cash", "15.3"),
].flat(),
});
registry.category("web_tour.tours").add("PosComboCheapestRewardProgram", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Expensive product"),
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.select("Combo Product 1"),
combo.select("Combo Product 4"),
combo.select("Combo Product 6"),
Dialog.confirm(),
inLeftSide(Order.hasLine({ productName: "10% on the cheapest product" })),
PosLoyalty.orderTotalIs("1,204.25"),
PosLoyalty.finalizeOrder("Cash", "1204.25"),
ProductScreen.clickDisplayedProduct("Cheap product"),
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.select("Combo Product 1"),
combo.select("Combo Product 4"),
combo.select("Combo Product 6"),
Dialog.confirm(),
Order.hasLine({ productName: "10% on the cheapest product" }),
PosLoyalty.orderTotalIs("61.03"),
PosLoyalty.finalizeOrder("Cash", "61.03"),
].flat(),
});
registry.category("web_tour.tours").add("PosComboSpecificProductProgram", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.select("Combo Product 1"),
combo.select("Combo Product 4"),
combo.select("Combo Product 6"),
Dialog.confirm(),
Order.hasLine({ productName: "10% on Office Combo" }),
PosLoyalty.orderTotalIs("216.00"),
PosLoyalty.finalizeOrder("Cash", "216.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosCheapestProductTaxInclude", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product"),
ProductScreen.addOrderline("Desk Organizer", "1"),
Order.hasLine({ productName: "10% on the cheapest product" }),
PosLoyalty.orderTotalIs("6.00"), // taxe of 9 cents (≈ 10% HT)
].flat(),
});
registry.category("web_tour.tours").add("test_not_create_loyalty_card_expired_program", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Test Partner", true),
ProductScreen.addOrderline("Desk Organizer", "3"),
PosLoyalty.finalizeOrder("Cash", "15.3"),
].flat(),
});
registry.category("web_tour.tours").add("PosOrderClaimReward", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Test Partner"),
ProductScreen.addOrderline("Desk Organizer", "3"),
PosLoyalty.isPointsDisplayed(true),
PosLoyalty.claimReward("Free Product - Whiteboard Pen"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1.00"),
PosLoyalty.finalizeOrder("Cash", "15.3"),
].flat(),
});
registry.category("web_tour.tours").add("PosOrderNoPoints", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Test Partner 2"),
ProductScreen.addOrderline("Desk Organizer", "3"),
PosLoyalty.isPointsDisplayed(false),
PosLoyalty.finalizeOrder("Cash", "15.3"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyMultipleOrders", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Order1: Add a product and leave the order in draft.
ProductScreen.addOrderline("Whiteboard Pen", "2"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test 1"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
// Order2: Finalize a different order.
Chrome.createFloatingOrder(),
ProductScreen.addOrderline("Desk Organizer", "1"),
PosLoyalty.finalizeOrder("Cash", "10"),
].flat(),
});
registry.category("web_tour.tours").add("test_combo_product_dont_grant_point", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.select("Combo Product 1"),
combo.select("Combo Product 4"),
combo.select("Combo Product 6"),
Dialog.confirm(),
ProductScreen.clickDisplayedProduct("Office Combo"),
combo.select("Combo Product 1"),
combo.select("Combo Product 4"),
combo.select("Combo Product 6"),
Dialog.confirm(),
Order.hasLine({ productName: "100% on the cheapest product" }),
ProductScreen.totalAmountIs("50.00"),
].flat(),
});
registry.category("web_tour.tours").add("test_buy_x_get_y_reward_qty", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Whiteboard Pen", "10"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1.00"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-6.40", "2.00"),
PosLoyalty.finalizeOrder("Cash", "32"),
ProductScreen.addOrderline("Whiteboard Pen", "10"),
PosLoyalty.claimReward("Free Product - Whiteboard Pen"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-9.60", "3.00"),
PosLoyalty.finalizeOrder("Cash", "32"),
].flat(),
});
registry.category("web_tour.tours").add("test_multiple_loyalty_products", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
{
content: "Check that selection popup is not opened",
trigger: negate(`.selection-item`),
},
Order.hasLine({ productName: "Whiteboard Pen", quantity: "1" }),
Order.hasLine({ productName: "10% on your order", quantity: "1" }),
].flat(),
});
registry.category("web_tour.tours").add("test_max_usage_partner_with_point", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Desk Organizer", "3"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Partner 2"),
PosLoyalty.isRewardButtonHighlighted(true),
PosLoyalty.claimReward("100% on your order"),
PosLoyalty.finalizeOrder("Cash", "0"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Partner"),
ProductScreen.addOrderline("Desk Organizer", "3"),
PosLoyalty.isRewardButtonHighlighted(false),
].flat(),
});

View file

@ -0,0 +1,325 @@
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as PosLoyalty from "@pos_loyalty/../tests/tours/utils/pos_loyalty_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as SelectionPopup from "@point_of_sale/../tests/generic_helpers/selection_popup_util";
import { registry } from "@web/core/registry";
import * as ProductConfiguratorPopup from "@point_of_sale/../tests/pos/tours/utils/product_configurator_util";
import { negateStep } from "@point_of_sale/../tests/generic_helpers/utils";
import * as PartnerList from "@point_of_sale/../tests/pos/tours/utils/partner_list_util";
registry.category("web_tour.tours").add("PosLoyaltyFreeProductTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Desk Organizer", "2"),
// At this point, the free_product program is triggered.
// The reward button should be highlighted.
PosLoyalty.isRewardButtonHighlighted(true),
// Since the reward button is highlighted, clicking the reward product should be added as reward.
ProductScreen.clickDisplayedProduct("Desk Organizer", "3"),
PosLoyalty.hasRewardLine("Free Product - Desk Organizer", "-5.10", "1"),
// In the succeeding 2 clicks on the product, it is considered as a regular product.
// In the third click, the product will be added as reward.
ProductScreen.clickDisplayedProduct("Desk Organizer"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
PosLoyalty.isRewardButtonHighlighted(true),
ProductScreen.clickDisplayedProduct("Desk Organizer", true, "6"),
PosLoyalty.hasRewardLine("Free Product - Desk Organizer", "-10.20", "2"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
PosLoyalty.isRewardButtonHighlighted(false),
PosLoyalty.orderTotalIs("25.50"),
// Finalize order that consumed a reward.
PosLoyalty.finalizeOrder("Cash", "30"),
ProductScreen.clickDisplayedProduct("Desk Organizer", true, "1"),
ProductScreen.clickDisplayedProduct("Desk Organizer", true, "2"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
PosLoyalty.hasRewardLine("Free Product - Desk Organizer", "-5.10", "1"),
ProductScreen.clickNumpad("9"),
ProductScreen.selectedOrderlineHas("Desk Organizer", "9"),
PosLoyalty.hasRewardLine("Free Product - Desk Organizer", "-15.30", "3"),
ProductScreen.clickNumpad("⌫"),
ProductScreen.selectedOrderlineHas("Desk Organizer", "0"),
ProductScreen.clickDisplayedProduct("Desk Organizer", true, "1"),
ProductScreen.clickDisplayedProduct("Desk Organizer", true, "2"),
PosLoyalty.isRewardButtonHighlighted(true),
// Finalize order but without the reward.
// This step is important. When syncing the order, no reward should be synced.
PosLoyalty.orderTotalIs("10.20"),
PosLoyalty.finalizeOrder("Cash", "20"),
ProductScreen.addOrderline("Magnetic Board", "2"),
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.clickDisplayedProduct("Magnetic Board"),
PosLoyalty.isRewardButtonHighlighted(true),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.isRewardButtonHighlighted(false),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1"),
ProductScreen.clickOrderline("Magnetic Board", "3"),
ProductScreen.selectedOrderlineHas("Magnetic Board", "3"),
ProductScreen.clickNumpad("6"),
ProductScreen.selectedOrderlineHas("Magnetic Board", "6"),
PosLoyalty.isRewardButtonHighlighted(true),
PosLoyalty.claimReward("Free Product - Whiteboard Pen"),
PosLoyalty.isRewardButtonHighlighted(false),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-6.40", "2"),
// Finalize order that consumed a reward.
PosLoyalty.orderTotalIs("11.88"),
PosLoyalty.finalizeOrder("Cash", "20"),
ProductScreen.addOrderline("Magnetic Board", "6"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1"),
PosLoyalty.isRewardButtonHighlighted(true),
ProductScreen.clickOrderline("Magnetic Board", "6"),
ProductScreen.clickNumpad("⌫"),
// At this point, the reward should have been removed.
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.selectedOrderlineHas("Magnetic Board", "0"),
ProductScreen.clickDisplayedProduct("Magnetic Board"),
ProductScreen.selectedOrderlineHas("Magnetic Board", "1"),
ProductScreen.clickDisplayedProduct("Magnetic Board"),
ProductScreen.selectedOrderlineHas("Magnetic Board", "2"),
ProductScreen.clickDisplayedProduct("Magnetic Board"),
ProductScreen.selectedOrderlineHas("Magnetic Board", "3"),
PosLoyalty.hasRewardLine("Free Product - Whiteboard Pen", "-3.20", "1"),
PosLoyalty.isRewardButtonHighlighted(false),
PosLoyalty.orderTotalIs("5.94"),
PosLoyalty.finalizeOrder("Cash", "10"),
// Promotion: 2 items of shelves, get desk_pad/monitor_stand free
// This is the 5th order.
ProductScreen.clickDisplayedProduct("Wall Shelf Unit"),
ProductScreen.selectedOrderlineHas("Wall Shelf Unit", "1"),
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.clickDisplayedProduct("Small Shelf"),
ProductScreen.selectedOrderlineHas("Small Shelf", "1"),
PosLoyalty.isRewardButtonHighlighted(true),
// Click reward product. Should be automatically added as reward.
ProductScreen.clickDisplayedProduct("Desk Pad"),
PosLoyalty.isRewardButtonHighlighted(false),
PosLoyalty.hasRewardLine("Free Product", "-1.98", "1"),
// Remove the reward line. The next steps will check if cashier
// can select from the different reward products.
ProductScreen.clickNumpad("⌫"),
ProductScreen.clickNumpad("⌫"),
PosLoyalty.isRewardButtonHighlighted(true),
PosLoyalty.claimReward("Free Product - [Desk Pad, Monitor Stand]"),
SelectionPopup.has("Monitor Stand"),
SelectionPopup.has("Desk Pad"),
SelectionPopup.has("Desk Pad", { run: "click" }),
PosLoyalty.isRewardButtonHighlighted(false),
PosLoyalty.hasRewardLine("Free Product", "-1.98", "1"),
ProductScreen.clickNumpad("⌫"),
ProductScreen.clickNumpad("⌫"),
PosLoyalty.isRewardButtonHighlighted(true),
PosLoyalty.claimReward("Free Product - [Desk Pad, Monitor Stand]"),
SelectionPopup.has("Monitor Stand"),
SelectionPopup.has("Desk Pad"),
SelectionPopup.has("Monitor Stand", { run: "click" }),
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.selectedOrderlineHas("Monitor Stand", "1", "3.19"),
PosLoyalty.hasRewardLine("Free Product", "-3.19", "1"),
PosLoyalty.orderTotalIs("4.81"),
PosLoyalty.finalizeOrder("Cash", "10"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyFreeProductTour2", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Partner"),
ProductScreen.addOrderline("Test Product A", "1"),
PosLoyalty.isRewardButtonHighlighted(true, true),
ProductScreen.clickControlButton("Reward"),
SelectionPopup.has("Free Product - Test Product A", { run: "click" }),
PosLoyalty.hasRewardLine("Free Product - Test Product A", "-11.50", "1"),
PosLoyalty.isRewardButtonHighlighted(false),
].flat(),
});
registry.category("web_tour.tours").add("test_loyalty_free_product_rewards_2", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
PosLoyalty.hasRewardLine("Free Product - Desk Organizer", "-5.10", "1.00"),
PosLoyalty.orderTotalIs("10.20"),
PosLoyalty.finalizeOrder("Cash", "10.20"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltySpecificDiscountTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Test Product A"),
ProductScreen.selectedOrderlineHas("Test Product A", "1", "40.00"),
ProductScreen.clickDisplayedProduct("Test Product B"),
ProductScreen.selectedOrderlineHas("Test Product B", "1", "40.00"),
ProductScreen.clickControlButton("Reward"),
SelectionPopup.has("$ 10 on specific products", { run: "click" }),
PosLoyalty.hasRewardLine("$ 10 on specific products", "-10.00", "1"),
PosLoyalty.orderTotalIs("70.00"),
ProductScreen.clickControlButton("Reward"),
SelectionPopup.has("$ 10 on specific products", { run: "click" }),
PosLoyalty.orderTotalIs("60.00"),
ProductScreen.clickControlButton("Reward"),
SelectionPopup.has("$ 30 on specific products", { run: "click" }),
PosLoyalty.hasRewardLine("$ 30 on specific products", "-30.00", "1"),
PosLoyalty.orderTotalIs("30.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltySpecificDiscountWithFreeProductTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Test Product A"),
ProductScreen.clickDisplayedProduct("Test Product C"),
PosLoyalty.orderTotalIs("130.00"),
PosLoyalty.isRewardButtonHighlighted(true, false),
{
content: `click Reward button`,
trigger: ProductScreen.controlButtonTrigger("Reward"),
run: "click",
},
Dialog.cancel(),
PosLoyalty.orderTotalIs("130.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltySpecificDiscountWithRewardProductDomainTour", {
steps: () =>
[
// Steps to check if the alert dialog for invalid domain loyalty program is present, only then will the pos screen load correctly
Dialog.is("A reward could not be loaded"),
Dialog.confirm("Ok"),
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.selectedOrderlineHas("Product A", "1", "15.00"),
PosLoyalty.orderTotalIs("15.00"),
ProductScreen.clickDisplayedProduct("Product B"),
ProductScreen.selectedOrderlineHas("Product B", "1", "50.00"),
PosLoyalty.orderTotalIs("40.00"),
ProductScreen.clickControlButton("Reward"),
SelectionPopup.has("10$ on your order - Product B - Saleable", { run: "click" }),
ProductScreen.clickControlButton("Reward"),
SelectionPopup.has("10$ on your order - Product B - Not Saleable", { run: "click" }),
PosLoyalty.orderTotalIs("30.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyRewardProductTag", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
PosLoyalty.isRewardButtonHighlighted(true, true),
PosLoyalty.claimReward("Free Product - [Product A, Product B]"),
SelectionPopup.has("Product A", { run: "click" }),
PosLoyalty.hasRewardLine("Free Product", "-2", "1"),
PosLoyalty.isRewardButtonHighlighted(false, true),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
PosLoyalty.isRewardButtonHighlighted(true, true),
PosLoyalty.claimReward("Free Product - [Product A, Product B]"),
SelectionPopup.has("Product B", { run: "click" }),
PosLoyalty.hasRewardLine("Free Product", "-5", "1"),
PosLoyalty.isRewardButtonHighlighted(false, true),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
PosLoyalty.isRewardButtonHighlighted(true, true),
PosLoyalty.claimReward("Free Product - [Product A, Product B]"),
SelectionPopup.has("Product B", { run: "click" }),
PosLoyalty.hasRewardLine("Free Product", "-10", "2"),
PosLoyalty.isRewardButtonHighlighted(false, true),
].flat(),
});
registry.category("web_tour.tours").add("test_loyalty_on_order_with_fixed_tax", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product A"),
PosLoyalty.enterCode("563412"),
PosLoyalty.hasRewardLine("10% on your order", "-1.50"),
].flat(),
});
registry.category("web_tour.tours").add("test_loyalty_reward_with_variant", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
PartnerList.searchCustomerValue("Test Partner", true),
ProductScreen.clickCustomer("Test Partner"),
ProductScreen.clickDisplayedProduct("Test Product"),
Dialog.discard(),
ProductScreen.clickDisplayedProduct("Test Product"),
ProductConfiguratorPopup.pickRadio("Value 1"),
Dialog.confirm(),
ProductScreen.clickDisplayedProduct("Test Product"),
ProductConfiguratorPopup.pickRadio("Value 1"),
Dialog.confirm(),
ProductScreen.clickDisplayedProduct("Test Product"),
ProductConfiguratorPopup.pickRadio("Value 1"),
Dialog.confirm(),
PosLoyalty.hasRewardLine("Free Product", "-10", "1.00"),
].flat(),
});
registry.category("web_tour.tours").add("test_multiple_reward_line_free_product", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.clickDisplayedProduct("Product A"),
PosLoyalty.hasRewardLine("Free Product - Product A", "-10", "1.00"),
ProductScreen.clickDisplayedProduct("Product B"),
ProductScreen.clickDisplayedProduct("Product B"),
PosLoyalty.hasRewardLine("Free Product - Product B").map(negateStep),
ProductScreen.clickDisplayedProduct("Product B"),
PosLoyalty.hasRewardLine("Free Product - Product B", "-5", "1.00"),
PosLoyalty.hasRewardLine("Free Product - Product A", "-10", "1.00"),
ProductScreen.clickDisplayedProduct("Product B"),
ProductScreen.clickDisplayedProduct("Product B"),
PosLoyalty.hasRewardLine("Free Product - Product B", "-5", "1.00"),
PosLoyalty.hasRewardLine("Free Product - Product A", "-10", "1.00"),
ProductScreen.clickDisplayedProduct("Product A"),
PosLoyalty.hasRewardLine("Free Product - Product B", "-5", "1.00"),
PosLoyalty.hasRewardLine("Free Product - Product A", "-20", "2.00"),
].flat(),
});

View file

@ -0,0 +1,806 @@
/* global posmodel */
import * as PosLoyalty from "@pos_loyalty/../tests/tours/utils/pos_loyalty_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as TicketScreen from "@point_of_sale/../tests/pos/tours/utils/ticket_screen_util";
import * as ReceiptScreen from "@point_of_sale/../tests/pos/tours/utils/receipt_screen_util";
import * as SelectionPopup from "@point_of_sale/../tests/generic_helpers/selection_popup_util";
import * as PartnerList from "@point_of_sale/../tests/pos/tours/utils/partner_list_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util";
import * as Notification from "@point_of_sale/../tests/generic_helpers/notification_util";
import * as Utils from "@point_of_sale/../tests/generic_helpers/utils";
import { registry } from "@web/core/registry";
import { scan_barcode } from "@point_of_sale/../tests/generic_helpers/utils";
registry.category("web_tour.tours").add("PosLoyaltyTour1", {
steps: () =>
[
// --- PoS Loyalty Tour Basic Part 1 ---
// Generate coupons for PosLoyaltyTour2.
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// basic order
// just accept the automatically applied promo program
// applied programs:
// - on cheapest product
ProductScreen.addOrderline("Whiteboard Pen", "5"),
PosLoyalty.hasRewardLine("90% on the cheapest product", "-2.88"),
PosLoyalty.selectRewardLine("on the cheapest product"),
PosLoyalty.orderTotalIs("13.12"),
PosLoyalty.finalizeOrder("Cash", "20"),
// remove the reward from auto promo program
// no applied programs
ProductScreen.addOrderline("Whiteboard Pen", "6"),
PosLoyalty.hasRewardLine("on the cheapest product", "-2.88"),
PosLoyalty.orderTotalIs("16.32"),
PosLoyalty.removeRewardLine("90% on the cheapest product"),
PosLoyalty.orderTotalIs("19.2"),
PosLoyalty.finalizeOrder("Cash", "20"),
// order with coupon code from coupon program
// applied programs:
// - coupon program
ProductScreen.addOrderline("Desk Organizer", "9"),
PosLoyalty.hasRewardLine("on the cheapest product", "-4.59"),
PosLoyalty.removeRewardLine("90% on the cheapest product"),
PosLoyalty.orderTotalIs("45.90"),
PosLoyalty.enterCode("invalid_code"),
Notification.has("invalid_code"),
PosLoyalty.enterCode("1234"),
PosLoyalty.hasRewardLine("Free Product - Desk Organizer", "-15.30"),
PosLoyalty.finalizeOrder("Cash", "50"),
// Use coupon but eventually remove the reward
// applied programs:
// - on cheapest product
ProductScreen.addOrderline("Letter Tray", "4"),
ProductScreen.addOrderline("Desk Organizer", "9"),
PosLoyalty.hasRewardLine("90% on the cheapest product", "-4.59"),
PosLoyalty.orderTotalIs("62.43"),
PosLoyalty.enterCode("5678"),
PosLoyalty.hasRewardLine("Free Product - Desk Organizer", "-15.30"),
PosLoyalty.orderTotalIs("47.13"),
PosLoyalty.removeRewardLine("Free Product"),
PosLoyalty.orderTotalIs("62.43"),
PosLoyalty.finalizeOrder("Cash", "90"),
// specific product discount
// applied programs:
// - on cheapest product
// - on specific products
ProductScreen.addOrderline("Magnetic Board", "10"), // 1.98
ProductScreen.addOrderline("Desk Organizer", "3"), // 5.1
ProductScreen.addOrderline("Letter Tray", "4"), // 4.8 tax 10%
PosLoyalty.hasRewardLine("90% on the cheapest product", "-1.78"),
PosLoyalty.orderTotalIs("54.44"),
PosLoyalty.enterCode("promocode"),
PosLoyalty.hasRewardLine("50% on specific products", "-16.66"), // 17.55 - 1.78*0.5
PosLoyalty.orderTotalIs("37.78"),
PosLoyalty.finalizeOrder("Cash", "50"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyTour2", {
steps: () =>
[
// --- PoS Loyalty Tour Basic Part 2 ---
// Using the coupons generated from PosLoyaltyTour1.
// Test that global discount and cheapest product discounts can be accumulated.
// Applied programs:
// - global discount
// - on cheapest discount
Chrome.startPoS(),
ProductScreen.addOrderline("Desk Organizer", "10"), // 5.1
PosLoyalty.hasRewardLine("on the cheapest product", "-4.59"),
ProductScreen.addOrderline("Letter Tray", "4"), // 4.8 tax 10%
PosLoyalty.hasRewardLine("on the cheapest product", "-4.59"),
PosLoyalty.enterCode("123456"),
PosLoyalty.hasRewardLine("10% on your order", "-4.64"),
PosLoyalty.hasRewardLine("10% on your order", "-2.11"),
PosLoyalty.orderTotalIs("60.78"), //SUBTOTAL
PosLoyalty.finalizeOrder("Cash", "70"),
// Scanning coupon twice.
// Also apply global discount on top of free product to check if the
// calculated discount is correct.
// Applied programs:
// - coupon program (free product)
// - global discount
// - on cheapest discount
ProductScreen.addOrderline("Desk Organizer", "11"), // 5.1 per item
PosLoyalty.hasRewardLine("90% on the cheapest product", "-4.59"),
PosLoyalty.orderTotalIs("51.51"),
// add global discount and the discount will be replaced
PosLoyalty.enterCode("345678"),
PosLoyalty.hasRewardLine("10% on your order", "-5.15"),
// add free product coupon (for qty=11, free=4)
// the discount should change after having free products
// it should go back to cheapest discount as it is higher
PosLoyalty.enterCode("5678"),
PosLoyalty.hasRewardLine("Free Product - Desk Organizer", "-20.40"),
PosLoyalty.hasRewardLine("90% on the cheapest product", "-4.59"),
// set quantity to 18
// free qty stays the same since the amount of points on the card only allows for 4 free products
//TODO: The following step should works with ProductScreen.clickNumpad("⌫", "8"),
ProductScreen.clickNumpad("⌫", "⌫", "1", "8"),
PosLoyalty.hasRewardLine("10% on your order", "-6.68"),
PosLoyalty.hasRewardLine("Free Product - Desk Organizer", "-20.40"),
// scan the code again and check notification
PosLoyalty.enterCode("5678"),
PosLoyalty.orderTotalIs("60.13"),
PosLoyalty.finalizeOrder("Cash", "65"),
// Specific products discount (with promocode) and free product (1357)
// Applied programs:
// - discount on specific products
// - free product
ProductScreen.addOrderline("Desk Organizer", "6"), // 5.1 per item
PosLoyalty.hasRewardLine("on the cheapest product", "-4.59"),
PosLoyalty.removeRewardLine("90% on the cheapest product"),
PosLoyalty.enterCode("promocode"),
PosLoyalty.hasRewardLine("50% on specific products", "-15.30"),
PosLoyalty.enterCode("1357"),
PosLoyalty.hasRewardLine("Free Product - Desk Organizer", "-10.20"),
PosLoyalty.hasRewardLine("50% on specific products", "-10.20"),
PosLoyalty.orderTotalIs("10.20"),
PosLoyalty.finalizeOrder("Cash", "20"),
// Check reset program
// Enter two codes and reset the programs.
// The codes should be checked afterwards. They should return to new.
// Applied programs:
// - cheapest product
ProductScreen.addOrderline("Monitor Stand", "6"), // 3.19 per item
PosLoyalty.enterCode("098765"),
PosLoyalty.hasRewardLine("90% on the cheapest product", "-2.87"),
PosLoyalty.hasRewardLine("10% on your order", "-1.63"),
PosLoyalty.orderTotalIs("14.64"),
PosLoyalty.removeRewardLine("90% on the cheapest product"),
PosLoyalty.hasRewardLine("10% on your order", "-1.91"),
PosLoyalty.orderTotalIs("17.23"),
ProductScreen.clickControlButton("Reset Programs"),
PosLoyalty.hasRewardLine("90% on the cheapest product", "-2.87"),
PosLoyalty.orderTotalIs("16.27"),
PosLoyalty.finalizeOrder("Cash", "20"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyTour3", {
steps: () =>
[
// --- PoS Loyalty Tour Basic Part 3 ---
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Promo Product"),
PosLoyalty.orderTotalIs("34.50"),
ProductScreen.clickDisplayedProduct("Product B"),
PosLoyalty.hasRewardLine("100% on specific products", "25.00"),
ProductScreen.clickDisplayedProduct("Product A"),
PosLoyalty.hasRewardLine("100% on specific products", "15.00"),
PosLoyalty.orderTotalIs("34.50"),
ProductScreen.clickDisplayedProduct("Product A"),
PosLoyalty.hasRewardLine("100% on specific products", "21.82"),
PosLoyalty.hasRewardLine("100% on specific products", "18.18"),
PosLoyalty.orderTotalIs("49.50"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyTour4", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Test Product 1", "1"),
ProductScreen.addOrderline("Test Product 2", "1"),
ProductScreen.clickPriceList("Public Pricelist"),
PosLoyalty.enterCode("abcda"),
PosLoyalty.orderTotalIs("0.00"),
ProductScreen.clickPriceList("Test multi-currency"),
PosLoyalty.orderTotalIs("0.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosCouponTour5", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA"),
ProductScreen.addOrderline("Test Product 1", "1", "100"),
PosLoyalty.pointsAwardedAre("115"),
PosLoyalty.clickDiscountButton(),
Dialog.confirm(),
ProductScreen.totalAmountIs("92.00"),
PosLoyalty.pointsAwardedAre("92"),
Chrome.endTour(),
].flat(),
});
//transform the last tour to match the new format
registry.category("web_tour.tours").add("PosLoyaltyTour6", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Partner"),
ProductScreen.clickDisplayedProduct("Test Product A"),
PosLoyalty.checkAddedLoyaltyPoints("26.5"),
ProductScreen.clickControlButton("Reward"),
SelectionPopup.has("$ 1 per point on your order", { run: "click" }),
ProductScreen.totalAmountIs("165.00"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
PosLoyalty.isLoyaltyPointsAvailable(),
Utils.refresh(),
ReceiptScreen.isShown(),
PosLoyalty.isLoyaltyPointsAvailable(),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyTour7", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Test Product", "1"),
PosLoyalty.orderTotalIs("100"),
PosLoyalty.enterCode("abcda"),
PosLoyalty.orderTotalIs("90"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyTour8", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product B"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.totalAmountIs("50.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltySpecificDiscountCategoryTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product A", true, "1", "15.00"),
PosLoyalty.orderTotalIs("15.00"),
ProductScreen.clickDisplayedProduct("Product B", true, "1", "50.00"),
PosLoyalty.orderTotalIs("40.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyTour9", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Partner"),
ProductScreen.clickDisplayedProduct("Product B"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.totalAmountIs("210.00"),
PosLoyalty.isRewardButtonHighlighted(true),
PosLoyalty.claimReward("$ 5"),
ProductScreen.totalAmountIs("205.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyTour10", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Partner"),
PosLoyalty.customerIs("AAA Partner"),
ProductScreen.clickDisplayedProduct("Product Test"),
ProductScreen.totalAmountIs("1.00"),
ProductScreen.selectedOrderlineHas("Product Test", "1"),
PosLoyalty.isRewardButtonHighlighted(true),
PosLoyalty.claimReward("Free Product B"),
{
content: `click on reward item`,
trigger: `.selection-item:contains("Free Product B")`,
run: "click",
},
PosLoyalty.hasRewardLine("Free Product B", "-1.00"),
ProductScreen.totalAmountIs("1.00"),
PosLoyalty.isRewardButtonHighlighted(false),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyTour11.1", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Partner"),
PosLoyalty.customerIs("AAA Partner"),
ProductScreen.addOrderline("Product Test", "3"),
ProductScreen.totalAmountIs("150.00"),
PosLoyalty.isRewardButtonHighlighted(false),
PosLoyalty.finalizeOrder("Cash", "150"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyTour11.2", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAA Partner"),
PosLoyalty.customerIs("AAA Partner"),
ProductScreen.clickDisplayedProduct("Product Test"),
ProductScreen.totalAmountIs("50.00"),
PosLoyalty.isRewardButtonHighlighted(false),
PosLoyalty.enterCode("123456"),
PosLoyalty.isRewardButtonHighlighted(true),
PosLoyalty.claimReward("Free Product"),
PosLoyalty.hasRewardLine("Free Product", "-3.00"),
PosLoyalty.isRewardButtonHighlighted(false),
ProductScreen.totalAmountIs("50.00"),
PosLoyalty.finalizeOrder("Cash", "50"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyMinAmountAndSpecificProductTour", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.selectedOrderlineHas("Product A", "1", "20.00"),
PosLoyalty.orderTotalIs("20.00"),
ProductScreen.clickDisplayedProduct("Product B"),
ProductScreen.selectedOrderlineHas("Product B", "1", "30.00"),
PosLoyalty.orderTotalIs("50.00"),
ProductScreen.clickDisplayedProduct("Product A"),
ProductScreen.selectedOrderlineHas("Product A", "2", "40.00"),
PosLoyalty.orderTotalIs("66.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyTour12", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Free Product A", "2"),
ProductScreen.clickDisplayedProduct("Free Product A"),
ProductScreen.totalAmountIs("2.00"),
PosLoyalty.hasRewardLine("Free Product", "-1.00"),
ProductScreen.addOrderline("Free Product B", "2"),
ProductScreen.clickDisplayedProduct("Free Product B"),
ProductScreen.totalAmountIs("12.00"),
PosLoyalty.hasRewardLine("Free Product", "-5.00"),
ProductScreen.clickDisplayedProduct("Free Product B"),
ProductScreen.clickDisplayedProduct("Free Product B"),
ProductScreen.clickDisplayedProduct("Free Product B"),
ProductScreen.selectedOrderlineHas("Free Product B", "6"),
ProductScreen.totalAmountIs("22.00"),
PosLoyalty.hasRewardLine("Free Product", "-10.00"),
].flat(),
});
function createOrderCoupon(totalAmount, couponName, couponAmount, loyaltyPoints) {
return [
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA"),
ProductScreen.addOrderline("product_a", "1"),
ProductScreen.addOrderline("product_b", "1"),
PosLoyalty.enterCode("promocode"),
PosLoyalty.hasRewardLine(`${couponName}`, `${couponAmount}`),
PosLoyalty.orderTotalIs(`${totalAmount}`),
PosLoyalty.pointsAwardedAre(`${loyaltyPoints}`),
PosLoyalty.finalizeOrder("Cash", `${totalAmount}`),
].flat();
}
registry.category("web_tour.tours").add("PosLoyaltyPointsDiscountNoDomainProgramNoDomain", {
steps: () => [createOrderCoupon("135.00", "10% on your order", "-15.00", "135")].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyPointsDiscountNoDomainProgramDomain", {
steps: () => [createOrderCoupon("135.00", "10% on your order", "-15.00", "100")].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyPointsDiscountWithDomainProgramDomain", {
steps: () => [createOrderCoupon("140.00", "10% on food", "-10.00", "90")].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyPointsGlobalDiscountProgramNoDomain", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("product_a", "1"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA"),
PosLoyalty.hasRewardLine("10% on your order", "-10.00"),
PosLoyalty.orderTotalIs("90"),
PosLoyalty.pointsAwardedAre("90"),
PosLoyalty.finalizeOrder("Cash", "90"),
].flat(),
});
registry.category("web_tour.tours").add("ChangeRewardValueWithLanguage", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Desk Organizer"),
ProductScreen.selectedOrderlineHas("Desk Organizer", "1", "5.10"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Partner Test 1"),
PosLoyalty.isRewardButtonHighlighted(true, true),
PosLoyalty.claimReward("$ 2 on your order"),
PosLoyalty.hasRewardLine("$ 2 on your order", "-2.00"),
PosLoyalty.orderTotalIs("3.10"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyArchivedRewardProductsInactive", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Test Product A"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA"),
PosLoyalty.isRewardButtonHighlighted(false, true),
ProductScreen.selectedOrderlineHas("Test Product A", "1", "100.00"),
PosLoyalty.finalizeOrder("Cash", "100"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyArchivedRewardProductsActive", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.clickDisplayedProduct("Test Product A"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA"),
PosLoyalty.isRewardButtonHighlighted(true),
ProductScreen.selectedOrderlineHas("Test Product A", "1", "100.00"),
PosLoyalty.finalizeOrder("Cash", "100"),
].flat(),
});
registry.category("web_tour.tours").add("CustomerLoyaltyPointsDisplayed", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("product_a"),
ProductScreen.selectedOrderlineHas("product_a", "1", "100.00"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("John Doe"),
PosLoyalty.orderTotalIs("100.00"),
PosLoyalty.pointsAwardedAre("100"),
PosLoyalty.finalizeOrder("Cash", "100.00"),
PosLoyalty.checkPartnerPoints("John Doe", "100.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyalty2DiscountsSpecificGlobal", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA"),
ProductScreen.addOrderline("Test Product A", "5"),
ProductScreen.clickDisplayedProduct("Test Product B"),
PosLoyalty.hasRewardLine("10% on your order", "-3.00"),
PosLoyalty.hasRewardLine("10% on Test Product B", "-0.45"),
PosLoyalty.finalizeOrder("Cash", "100"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltySpecificProductDiscountWithGlobalDiscount", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Product A", "1"),
PosLoyalty.hasRewardLine("$ 40 on Product A", "-40.00"),
PosLoyalty.clickDiscountButton(),
Dialog.confirm(),
PosLoyalty.hasRewardLine("$ 40 on Product A", "-40.00"),
PosLoyalty.orderTotalIs("20.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosRewardProductScan", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
scan_barcode("95412427100283"),
ProductScreen.selectedOrderlineHas("product_a", "1", "1,150.00"),
PosLoyalty.hasRewardLine("50% on your order", "-575.00"),
PosLoyalty.orderTotalIs("575.00"),
PosLoyalty.finalizeOrder("Cash", "575.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosRewardProductScanGS1", {
steps: () =>
[
Chrome.startPoS(),
scan_barcode("0195412427100283"),
ProductScreen.selectedOrderlineHas("product_a", "1", "1,150.00"),
PosLoyalty.hasRewardLine("50% on your order", "-575.00"),
PosLoyalty.orderTotalIs("575.00"),
PosLoyalty.finalizeOrder("Cash", "575.00"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyPromocodePricelist", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Test Product 1", "1"),
PosLoyalty.enterCode("hellopromo"),
PosLoyalty.orderTotalIs("25.87"),
].flat(),
});
registry.category("web_tour.tours").add("RefundRulesProduct", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("product_a"),
PosLoyalty.finalizeOrder("Cash", "1000"),
ProductScreen.isShown(),
...ProductScreen.clickRefund(),
TicketScreen.filterIs("Paid"),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("1"),
TicketScreen.confirmRefund(),
PaymentScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("test_two_variant_same_discount", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Sofa"),
Chrome.clickBtn("Add"),
].flat(),
});
registry.category("web_tour.tours").add("test_settle_dont_give_points_again", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
PartnerList.settleCustomerAccount("AAA Partner", "10.00", "TSJ/"),
].flat(),
});
registry.category("web_tour.tours").add("test_refund_does_not_decrease_points", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("Refunding Guy", true),
ProductScreen.clickDisplayedProduct("Refund Product"),
ProductScreen.clickControlButton("Reward"),
SelectionPopup.has("$ 1 per point on your order", { run: "click" }),
PosLoyalty.finalizeOrder("Cash", "200"),
ProductScreen.clickRefund(),
TicketScreen.selectOrder("001"),
ProductScreen.clickNumpad("1"),
ProductScreen.clickLine("$ 1 per point on your order"),
ProductScreen.clickNumpad("1"),
TicketScreen.confirmRefund(),
PaymentScreen.totalIs("-200.00"),
PaymentScreen.clickPaymentMethod("Cash"),
PaymentScreen.clickValidate(),
].flat(),
});
registry.category("web_tour.tours").add("test_discount_after_unknown_scan", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Test Product A", "1"),
scan_barcode("00998877665544332211"), //should be unknown
PosLoyalty.hasRewardLine("10% on Test Product A", "-0.50"),
ProductScreen.totalAmountIs("4.50"),
].flat(),
});
registry.category("web_tour.tours").add("test_scan_loyalty_card_select_customer", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
scan_barcode("0444-e050-4548"),
ProductScreen.customerIsSelected("AAA Test Partner"),
].flat(),
});
registry.category("web_tour.tours").add("test_min_qty_points_awarded", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AA Partner"),
ProductScreen.clickDisplayedProduct("Whiteboard Pen"),
PosLoyalty.claimReward("Free Product"),
PosLoyalty.pointsTotalIs("90"),
PosLoyalty.orderTotalIs("0.0"),
PosLoyalty.finalizeOrder("Cash", "0.0"),
].flat(),
});
registry.category("web_tour.tours").add("test_confirm_coupon_programs_one_by_one", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
{
trigger: "body",
content: "Create fake orders",
run: async () => {
// Create 5 orders that will be synced one by one
for (let i = 0; i < 5; i++) {
const order = posmodel.createNewOrder();
const product = posmodel.models["product.template"].find(
(p) => p.name === "Desk Pad"
);
const pm = posmodel.models["pos.payment.method"].getFirst();
const program = posmodel.models["loyalty.program"].find(
(p) => p.program_type === "gift_card"
);
await posmodel.addLineToOrder({ product_tmpl_id: product }, order);
posmodel.addPendingOrder([order.id]);
order.addPaymentline(pm);
order.state = "paid";
// Create fake coupon point changes to simulate coupons to be confirmed
order.uiState.couponPointChanges = [
{
points: 124.2,
program_id: program.id,
coupon_id: -(i + 1),
barcode: "",
},
];
}
},
},
// Create one more order to be able to trigger the sync from the UI
ProductScreen.clickDisplayedProduct("Desk Pad"),
ProductScreen.clickPayButton(),
PaymentScreen.clickPaymentMethod("Bank"),
PaymentScreen.clickValidate(),
ReceiptScreen.isShown(),
].flat(),
});
registry.category("web_tour.tours").add("test_order_reward_product_tax_included_included", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Product Include", "1"),
PosLoyalty.enterCode("hellopromo"),
PosLoyalty.hasRewardLine("$ 10 on your order", "-10.00"),
].flat(),
});
registry.category("web_tour.tours").add("test_order_reward_product_tax_included_excluded", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.addOrderline("Product Include", "1"),
PosLoyalty.enterCode("hellopromo"),
PosLoyalty.hasRewardLine("$ 10 on your order", "-10.00"),
].flat(),
});
registry.category("web_tour.tours").add("test_specific_reward_product_tax_included_included", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.addOrderline("Product Include", "1"),
PosLoyalty.enterCode("hellopromo"),
PosLoyalty.hasRewardLine("$ 10 on Product Include", "-10.00"),
].flat(),
});
registry.category("web_tour.tours").add("test_specific_reward_product_tax_included_excluded", {
steps: () =>
[
Chrome.startPoS(),
ProductScreen.addOrderline("Product Include", "1"),
PosLoyalty.enterCode("hellopromo"),
PosLoyalty.hasRewardLine("$ 10 on Product Include", "-10.00"),
].flat(),
});
registry.category("web_tour.tours").add("test_loyalty_is_not_processed_for_draft_order", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickPartnerButton(),
ProductScreen.clickCustomer("AAAA"),
ProductScreen.addOrderline("Whiteboard Pen", "1", "100"),
PosLoyalty.pointsAwardedAre("100"),
PosLoyalty.pointsTotalIs("150"),
ProductScreen.saveOrder(),
ProductScreen.selectFloatingOrder(0),
PosLoyalty.pointsAwardedAre("100"),
PosLoyalty.pointsTotalIs("150"),
].flat(),
});
registry.category("web_tour.tours").add("test_race_conditions_update_program", {
steps: () =>
[
Chrome.startPoS(),
Dialog.confirm("Open Register"),
ProductScreen.clickDisplayedProduct("Test Product"),
PosLoyalty.orderTotalIs("34.87"),
{
trigger: "body",
run: async () => {
// Check the number of lines in the order
const line_count = document.querySelectorAll(".orderline").length;
if (line_count !== 11) {
throw new Error(`Expected 11 orderlines, found ${line_count}`);
}
},
},
].flat(),
});

View file

@ -0,0 +1,39 @@
import * as PosLoyalty from "@pos_loyalty/../tests/tours/utils/pos_loyalty_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("PosLoyaltyValidity1", {
steps: () =>
[
// First tour should not get any automatic rewards
Chrome.startPoS(),
Dialog.confirm("Open Register"),
// Not valid -> date
ProductScreen.addOrderline("Whiteboard Pen", "5"),
PosLoyalty.isRewardButtonHighlighted(false, true),
PosLoyalty.orderTotalIs("16.00"),
PosLoyalty.finalizeOrder("Cash", "16"),
].flat(),
});
registry.category("web_tour.tours").add("PosLoyaltyValidity2", {
steps: () =>
[
// Second tour
// Valid
Chrome.startPoS(),
ProductScreen.addOrderline("Whiteboard Pen", "5"),
PosLoyalty.hasRewardLine("90% on the cheapest product", "-2.88"),
PosLoyalty.finalizeOrder("Cash", "20"),
// Not valid -> usage
ProductScreen.addOrderline("Whiteboard Pen", "5"),
PosLoyalty.isRewardButtonHighlighted(false, true),
PosLoyalty.orderTotalIs("16.00"),
PosLoyalty.finalizeOrder("Cash", "16.00"),
].flat(),
});

View file

@ -0,0 +1,267 @@
import * as Order from "@point_of_sale/../tests/generic_helpers/order_widget_util";
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
import * as TextInputPopup from "@point_of_sale/../tests/generic_helpers/text_input_popup_util";
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util";
import * as ReceiptScreen from "@point_of_sale/../tests/pos/tours/utils/receipt_screen_util";
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
import { negate } from "@point_of_sale/../tests/generic_helpers/utils";
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
export function selectRewardLine(rewardName) {
return [
...Order.hasLine({
withClass: ".fst-italic",
withoutClass: ".selected",
run: "click",
productName: rewardName,
}),
...Order.hasLine({
withClass: ".selected.fst-italic",
productName: rewardName,
}),
];
}
export function enterCode(code) {
return [
...ProductScreen.clickControlButton("Enter Code"),
TextInputPopup.inputText(code),
Dialog.confirm(),
];
}
export function clickEWalletButton(text = "eWallet") {
return [{ trigger: ProductScreen.controlButtonTrigger(text), run: "click" }];
}
export function claimReward(rewardName) {
return [
...ProductScreen.clickControlButton("Reward"),
{
// There should be description because a program always has a name.
trigger: ".selection-item span:nth-child(2)",
},
{
content: "select reward",
trigger: `.selection-item:contains("${rewardName}")`,
run: "click",
},
];
}
export function unselectPartner() {
return [{ trigger: ".unselect-tag", run: "click" }];
}
export function clickDiscountButton() {
return [
...ProductScreen.clickControlButtonMore(),
{
content: "click discount button",
trigger: ".js_discount",
run: "click",
},
];
}
export function hasRewardLine(rewardName, amount, qty) {
return Order.hasLine({
withClass: ".fst-italic",
productName: rewardName,
price: amount,
quantity: qty,
});
}
export function orderTotalIs(total_str) {
return [...Order.hasTotal(total_str)];
}
export function isRewardButtonHighlighted(isHighlighted, closeModal = true) {
const steps = [
...ProductScreen.clickControlButtonMore(),
{
trigger: isHighlighted
? '.control-buttons button.highlight:contains("Reward")'
: '.control-buttons button.disabled:contains("Reward")',
},
];
if (closeModal) {
steps.push({
content: "Close modal after checked if reward button is highlighted",
trigger: ".modal header .btn-close",
run: "click",
});
}
return steps;
}
export function eWalletButtonState({ highlighted, text = "eWallet", click = false }) {
const step = {
trigger: highlighted
? `.control-buttons button.highlight:contains("${text}")`
: `.control-buttons button.disabled:contains("${text}")`,
};
if (click) {
step.run = "click";
}
const steps = [...ProductScreen.clickControlButtonMore(), step];
if (!click) {
steps.push({
//Previous step is just a check. No need to keep modal openened
trigger: ".modal header .btn-close",
run: "click",
});
}
return steps;
}
export function customerIs(name) {
return [
{
trigger: `.product-screen .set-partner:contains("${name}")`,
},
];
}
export function isPointsDisplayed(isDisplayed) {
return [
{
trigger: isDisplayed
? ".loyalty-points-title"
: "body:not(:has(.loyalty-points-title))",
},
];
}
export function pointsAwardedAre(points_str) {
return [
{
content: "loyalty points awarded " + points_str,
trigger: '.loyalty-points-won:contains("' + points_str + '")',
},
];
}
export function pointsTotalIs(points_str) {
return [
{
content: "loyalty points awarded " + points_str,
trigger: '.loyalty-points-totaltext-end:contains("' + points_str + '")',
},
];
}
export function finalizeOrder(paymentMethod, amount) {
return [
...ProductScreen.clickPayButton(),
...PaymentScreen.clickPaymentMethod(paymentMethod),
...PaymentScreen.clickNumpad([...amount].join(" ")),
...PaymentScreen.clickValidate(),
...ReceiptScreen.clickNextOrder(),
];
}
export function removeRewardLine(name) {
return [selectRewardLine(name), ProductScreen.clickNumpad("⌫"), Dialog.confirm()].flat();
}
export function checkAddedLoyaltyPoints(points) {
return [
{
trigger: `.loyalty-points-won:contains("${points}")`,
},
];
}
export function useExistingLoyaltyCard(code, valid = true) {
const steps = [
{
trigger: `a:contains("Sell physical gift card?")`,
run: "click",
},
{
content: `Input code '${code}'`,
trigger: `input[id="code"]`,
run: `edit ${code}`,
},
{
content: "Not loading",
trigger: negate(".gift-card-loading"),
},
];
if (!valid) {
steps.push(Dialog.confirm("Ok"));
steps.push(Dialog.cancel());
steps.push({
trigger: `a:contains("Sell physical gift card?")`,
run: () => {},
});
} else {
steps.push(...Chrome.waitRequest());
steps.push(Dialog.confirm());
}
return steps;
}
export function createManualGiftCard(code, amount, date = false) {
const steps = [
{
trigger: `a:contains("Sell physical gift card?")`,
run: "click",
},
{
content: `Input code '${code}'`,
trigger: `input[id="code"]`,
run: `edit ${code}`,
},
{
content: `Input amount '${amount}'`,
trigger: `input[id="amount"]`,
run: `edit ${amount}`,
},
];
if (date !== false) {
steps.push({
content: `Input date '${date}'`,
trigger: `.modal input.o_datetime_input.cursor-pointer.form-control.form-control-lg`,
run: `edit ${date}`,
});
}
steps.push({
trigger: `.btn-primary:contains("Add Balance")`,
run: "click",
});
return steps;
}
export function clickGiftCardProgram(name) {
return [
{
content: `Click gift card program '${name}'`,
trigger: `button.selection-item:has(span:contains("${name}"))`,
run: "click",
},
];
}
export function clickPhysicalGiftCard(code = "Sell physical gift card?") {
return [
{
trigger: `ul.info-list .text-wrap:contains("${code}")`,
run: "click",
},
];
}
export function checkPartnerPoints(name, points) {
return [
...ProductScreen.clickPartnerButton(),
{
content: `Check '${name}' has ${points} Loyalty Points`,
trigger: `.partner-list .partner-line:contains(${name}) .partner-line-balance:contains(${points} Loyalty Point(s))`,
},
];
}
export function isMoreControlButtonActive(active) {
return {
content: "More control button is " + (active ? "active" : "not active"),
trigger: active
? ".control-buttons .more-btn.active"
: ".control-buttons:not(:has(.more-btn.active))",
};
}
export function isLoyaltyPointsAvailable() {
return {
content: "Loyalty Points are visible on the receipt",
trigger: ".pos-receipt .loyalty",
};
}

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;
};