Initial commit: Pos packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 95dfb9edb0
1301 changed files with 264148 additions and 0 deletions

View file

@ -0,0 +1,38 @@
/** @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

@ -0,0 +1,28 @@
/** @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

@ -0,0 +1,132 @@
/** @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

@ -0,0 +1,107 @@
/** @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);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,14 @@
/** @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

@ -0,0 +1,28 @@
/** @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

@ -0,0 +1,25 @@
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

@ -0,0 +1,21 @@
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

@ -0,0 +1,172 @@
/** @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

@ -0,0 +1,250 @@
/** @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

@ -0,0 +1,51 @@
/** @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);