commit ec65830d334d1e749a0a5dde6df5018431992419 Author: Abdul Aziz Amrullah Date: Mon May 4 09:55:03 2026 +0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..bb74b87 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'POS Loyalty Reward Quantity Limit', + 'version': '1.0', + 'category': 'Sales/Point of Sale', + 'summary': 'Limits the maximum reward product quantity applied in POS to the value configured in the reward rule.', + 'author': 'Abdul Aziz Amrullah', + 'description': """ +POS Loyalty Reward Quantity Limit +=================================== +This module introduces a strict boundary on the total quantity of a reward product that can be applied within a single Point of Sale (POS) order. + +Key Features: +- Replaces Odoo's default multiplier system for loyalty reward products. +- Enforces the 'Quantity Rewarded' (reward_product_qty) field on the Loyalty Reward rule as an absolute maximum limit. +- Ensures that regardless of how many points a customer has accumulated, they cannot claim more than the specified limit of the reward product in a single transaction. +- Automatically adjusts the deducted loyalty points to correctly match the capped quantity, preventing overcharging of points. + +Use Case: +Ideal for retail environments running promotions such as 'Redeem X points for 1 Free Item'. By default, if a user has 3x points, Odoo allows them to claim 3 free items at once. This module strictly restricts the reward so only 1 free item can be claimed per transaction, exactly matching the configuration in the backend. + """, + 'depends': ['point_of_sale', 'pos_loyalty'], + 'data': [], + 'assets': { + 'point_of_sale._assets_pos': [ + 'pos_loyalty_reward_qty_limit/static/src/app/models/pos_order.js', + ], + }, + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0152950 --- /dev/null +++ b/readme.md @@ -0,0 +1,31 @@ +# POS Loyalty Reward Quantity Limit + +## Overview +This custom Odoo 19 module modifies the Point of Sale (POS) loyalty reward mechanism. It enforces a strict upper limit on the quantity of a reward product that can be applied to a single POS transaction. + +By default, Odoo's loyalty engine allows customers with excess loyalty points to multiply a single reward rule, applying it multiple times to their cart in a single click. This module intervenes in this process by ensuring that the **"Quantity Rewarded" (`reward_product_qty`)** defined in the Loyalty Reward configuration acts as an **absolute maximum** per transaction. + +## Features +- **Strict Limit Enforcement:** Prevents the system from giving out more free items than specified in the `reward_product_qty` field. +- **Accurate Point Deduction:** Automatically scales the required loyalty points deducted from the customer's wallet to match the capped quantity, ensuring no points are overcharged. +- **Dynamic UI Validation:** Updates the Point of Sale UI to dynamically hide or disable the reward button once the maximum quantity has been reached in the cart. + +## Use Case Example +**Scenario:** A retail store runs a promotion where customers can redeem 100 points for **1** free coffee. +**Problem in standard Odoo:** If a customer has 300 points, clicking the reward will automatically add 3 free coffees to the cart and deduct 300 points. +**Solution with this module:** The module checks the "Quantity Rewarded" field (which is set to 1). When the customer clicks the reward, only **1** free coffee is added, and only 100 points are deducted. The reward button is then disabled for that transaction. + +## Installation +1. Move the `pos_loyalty_reward_qty_limit` folder into your Odoo `custom/` addons directory. +2. Ensure you have the `point_of_sale` and `pos_loyalty` modules installed. +3. Turn on **Developer Mode**. +4. Go to **Apps** -> **Update Apps List**. +5. Search for `POS Loyalty Reward Quantity Limit` and install it. + +## Technical Details +This module patches the `PosOrder.prototype` located in the base `point_of_sale` frontend assets. +Specifically, it overrides: +- `_computeUnclaimedFreeProductQty` +- `_computePotentialFreeProductQty` + +It dynamically calculates the `maxAllowed` available quantity by subtracting the already claimed amount from the configured `reward_product_qty`. diff --git a/static/src/app/models/pos_order.js b/static/src/app/models/pos_order.js new file mode 100644 index 0000000..d4c6e8c --- /dev/null +++ b/static/src/app/models/pos_order.js @@ -0,0 +1,42 @@ +/** @odoo-module **/ + +import { PosOrder } from "@point_of_sale/app/models/pos_order"; +import { patch } from "@web/core/utils/patch"; + +patch(PosOrder.prototype, { + _computeUnclaimedFreeProductQty(reward, coupon_id, product, remainingPoints) { + const unclaimed = super._computeUnclaimedFreeProductQty(...arguments); + + // Find how many of this reward have already been claimed in the order + let claimed = 0; + for (const line of this.getOrderlines()) { + if (line.reward_id && line.reward_id.id === reward.id) { + claimed += line.getQuantity(); + } + } + + // The maximum total quantity allowed for this reward in an order is reward_product_qty. + // So the maximum we can still claim is reward_product_qty - claimed. + let maxAllowed = reward.reward_product_qty - claimed; + if (maxAllowed < 0) maxAllowed = 0; + + return Math.min(unclaimed, maxAllowed); + }, + + _computePotentialFreeProductQty(reward, product, remainingPoints) { + const potential = super._computePotentialFreeProductQty(...arguments); + + // Apply the same cap here for display purposes + let claimed = 0; + for (const line of this.getOrderlines()) { + if (line.reward_id && line.reward_id.id === reward.id) { + claimed += line.getQuantity(); + } + } + + let maxAllowed = reward.reward_product_qty - claimed; + if (maxAllowed < 0) maxAllowed = 0; + + return Math.min(potential, maxAllowed); + } +});