From c71acbf7cba848ec114f025e8de9d65f1b8f173c Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Fri, 22 May 2026 22:27:10 +0700 Subject: [PATCH] feat: patch PosOrder to exclude temporary loyalty cards from applicability and rewards calculations --- README.md | 6 ++- __manifest__.py | 1 + static/src/app/models/pos_order_patch.js | 51 ++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 static/src/app/models/pos_order_patch.js diff --git a/README.md b/README.md index 8bc8f16..8a73f96 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ This custom Odoo module enhances the POS Loyalty interface and partner creation - Solves a core Odoo 19 multi-company operational bug where cashiers in branch companies encounter "Access Errors" when validating orders for customers whose loyalty cards belong to the parent company. - Bypasses company-specific record rules during POS loyalty audit logging using standard and safe `sudo` environment execution. +5. **Loyalty Reward and Point Access Control:** + - Restricts loyalty program reward eligibility, point balance displays, and program applicability in the POS UI strictly to customers who have a persisted, active card in the database for that specific program. + - Prevents unauthorized claim/access to zero-point rewards (e.g., 100% discount rewards in executive/exclusive programs) by non-enrolled members. + ## Technical Details -- **JS OWL Patching:** Overrides POS frontend partner management safely using clean, standard owl-level class overrides. +- **JS OWL Patching:** Overrides POS frontend partner management and order processing (`PosOrder`) safely using clean, standard owl-level class overrides to filter program eligibility, claimable rewards, and loyalty point lists based on card status. - **Python Inheritance:** Customizes backend models (`res.partner`, `pos.order`) to clean loading domains and securely provision cross-company data. diff --git a/__manifest__.py b/__manifest__.py index ceda515..a6d69d3 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -24,6 +24,7 @@ Custom POS Loyalty and Membership management features: 'assets': { 'point_of_sale._assets_pos': [ 'pos_loyalty_member_custom/static/src/app/screens/partner_list_patch.js', + 'pos_loyalty_member_custom/static/src/app/models/pos_order_patch.js', ] }, 'installable': True, diff --git a/static/src/app/models/pos_order_patch.js b/static/src/app/models/pos_order_patch.js new file mode 100644 index 0000000..c8960a8 --- /dev/null +++ b/static/src/app/models/pos_order_patch.js @@ -0,0 +1,51 @@ +/** @odoo-module **/ + +import { PosOrder } from "@point_of_sale/app/models/pos_order"; +import { patch } from "@web/core/utils/patch"; + +patch(PosOrder.prototype, { + _programIsApplicable(program) { + const isApplicable = super._programIsApplicable(...arguments); + if (!isApplicable) { + return false; + } + + if (program.program_type === 'loyalty') { + const partner = this.getPartner(); + if (partner) { + // Check if a card exists in cache for this program and partner + const card = this.models['loyalty.card'].find( + (c) => c.partner_id?.id === partner.id && c.program_id?.id === program.id + ); + // If a card exists and is temporary (ID < 0), the program is not applicable + if (card && card.id < 0) { + return false; + } + } + } + + return true; + }, + + getClaimableRewards(coupon_id = false, program_id = false, auto = false) { + const claimable = super.getClaimableRewards(...arguments); + return claimable.filter((item) => { + const card = this.models['loyalty.card'].get(item.coupon_id); + if (card && card.program_id?.program_type === 'loyalty' && card.id < 0) { + return false; + } + return true; + }); + }, + + getLoyaltyPoints() { + const points = super.getLoyaltyPoints(...arguments); + return points.filter((item) => { + const couponId = parseInt(item.couponId); + if (couponId < 0 && item.program?.program_type === 'loyalty') { + return false; + } + return true; + }); + } +});