Monday, September 21, 2026
1 change · 18.0
Resolved issues and error corrections
Point-of-sale refunds now deduct loyalty points that were originally earned on the refunded purchase, closing a loophole that let customers keep rewards after getting their money back. The update also keeps refund displays clear for cashiers and customers, while preserving proper eWallet and gift card refund handling.
Original PR description
Description of the issue/feature this PR addresses: When a customer earned loyalty points on a POS order and then refunded that order, the points were never deducted. This allowed a trivial exploit:…
Description of the issue/feature this PR addresses: When a customer earned loyalty points on a POS order and then refunded that order, the points were never deducted. This allowed a trivial exploit: buy a product to earn points, refund it to get the money back, and keep the points. Repeating this cycle let customers accumulate unlimited loyalty points at no cost to themselves. Current behavior before PR: The root cause was that the refund flow in POS only copied product lines with negative quantities into the new refund order. It never touched couponPointChanges, so _postProcessLoyalty had nothing to process and confirm_coupon_programs was never called for the refund. A secondary issue was that _updatePrograms() runs reactively on every order change and calls pointsForPrograms(), which skips lines where totalProductQty < rule.minimum_qty. For refund lines with negative quantities this condition is always true, so the result was always 0 points — silently overwriting any couponPointChanges we tried to set. Desired behavior after PR is merged: The fix works at two levels: - Server-side (pos_order.py): override action_pos_order_paid() to call _reverse_loyalty_points_for_refund() when the paid order is a refund (negative total linked to an original order via refunded_order_id). This reads the original order's loyalty.history, computes a ratio for partial refunds, and directly deducts points from the loyalty card. Points are allowed to go negative intentionally — this prevents the fraud pattern where a customer earns points, spends them on a reward, then refunds the original purchase to reclaim the money while keeping the reward. Expired or inactive cards are skipped since they carry no meaningful balance. An idempotency guard prevents double-processing on retries. - Client-side (pos_store.js): override _updatePrograms() to detect refund orders and route them to _updateProgramsForRefund() instead of the normal pointsForPrograms() path. This reads couponPointChanges from the original order, negates them proportionally, and sets them on the refund order so the loyalty points widget at the bottom of the product screen correctly shows the deduction in real time. After sync, postSyncAllOrders skips confirm_coupon_programs for refund orders (since the server already handled it) and instead re-reads the updated card balance from the server. The order summary widget (order_summary.xml) and receipt template (order_receipt.xml) are updated to display a deducted line alongside the existing won and spent lines, so cashiers and customers can see the point reversal clearly. Fixes #257767 --- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr