diff --git a/static/src/app/models/pos_order.js b/static/src/app/models/pos_order.js index 2c1d73d..de13ca3 100644 --- a/static/src/app/models/pos_order.js +++ b/static/src/app/models/pos_order.js @@ -115,5 +115,76 @@ patch(PosOrder.prototype, { return true; }, + + /** + * Override to fix free product rewards for manual_membership (subscription) programs. + * + * Odoo's base _computeUnclaimedFreeProductQty returns Math.min(available, freeQty). + * `available` = qty of the reward product already in the current order. + * For subscription programs (e.g. Makan Pagi Gratis), the free breakfast product is + * NOT in the order yet — so available=0, freeQty=0 and the reward is never shown. + * + * Fix: after the base getClaimableRewards runs, we re-evaluate manual_membership + * programs that were silently skipped and inject their product rewards using the + * raw point-based qty (points / required_points), so the cashier can select them + * from the Rewards popup and the product is added to the order automatically. + */ + getClaimableRewards(coupon_id = false, program_id = false, auto = false) { + const baseResult = super.getClaimableRewards(...arguments); + + // Auto mode is the background reward sweep — don't alter it. + if (auto) { + return baseResult; + } + + // Collect program IDs already present so we don't add duplicates. + const presentProgramIds = new Set( + baseResult.map((r) => r.reward?.program_id?.id) + ); + + const couponPointChanges = this.uiState.couponPointChanges; + const extraRewards = []; + + for (const pe of Object.values(couponPointChanges)) { + const program = this.models['loyalty.program'].get(pe.program_id); + if (!program || !program.manual_membership) { + continue; + } + if (presentProgramIds.has(program.id)) { + continue; + } + if (coupon_id && pe.coupon_id !== coupon_id) { + continue; + } + if (program_id && pe.program_id !== program_id) { + continue; + } + + const points = this._getRealCouponPoints(pe.coupon_id); + for (const reward of program.reward_ids) { + if (points < reward.required_points) { + continue; + } + if (reward.reward_type !== 'product') { + // Non-product rewards are already handled by base logic. + continue; + } + // Compute how many free items can be claimed from remaining points. + const freeQty = Math.floor( + (points / reward.required_points) * (reward.reward_product_qty || 1) + ); + if (freeQty <= 0) { + continue; + } + extraRewards.push({ + coupon_id: pe.coupon_id, + reward: reward, + potentialQty: freeQty, + }); + } + } + + return [...baseResult, ...extraRewards]; + }, });