# POS Loyalty Extend **Version:** 1.0 **Author:** Suherdy Yacob **License:** LGPL-3 **Category:** Sales / Point of Sale ## Overview `pos_loyalty_extend` is a custom Odoo 19 module that extends the built-in `pos_loyalty` module with additional reward applicability options in the Point of Sale interface. The primary feature added is **"Cheapest Product on Order"** applicability for **Buy X Get Y** loyalty rewards — allowing merchants to automatically discount or give away the cheapest item(s) in a customer's cart based on the number of products purchased. --- ## Features ### 🏷️ Cheapest Product Applicability - Adds a new `reward_product_applicability` field (`cheapest`) to `loyalty.reward`. - When set, the reward dynamically targets the cheapest product(s) in the current POS order instead of a pre-configured fixed product. ### 📊 Proportional Free Item Calculation The number of free items scales automatically based on the buy ratio: | Products Bought | Free Items | |---|---| | 2 | 1 | | 3 | 1 | | 4 | 2 | | 5 | 2 | | 6 | 3 | Formula: `floor(points_earned / required_points_per_reward)` ### 🛒 Multi-Product Free Lines When multiple free items are awarded, each one targets a **different cheapest product** in the order (sorted by unit price ascending), rather than duplicating the single cheapest item: - Buy 4 items → 2 free: **Terong Penyet (Rp 15,000)** + **Telor Penyet (Rp 19,000)** ✅ - ~~Buy 4 items → 2 free: 2× Terong Penyet (Rp 15,000)~~ ❌ ### ⚡ Auto-Claim in POS Cheapest product rewards are automatically discovered and claimed in the POS UI without requiring manual product configuration on the reward. --- ## Dependencies | Module | Purpose | |---|---| | `point_of_sale` | Core POS framework | | `pos_loyalty` | Loyalty program UI and logic in POS | | `sale_loyalty` | Backend loyalty program models | --- ## Configuration ### Setting Up a "Buy 2 Get 1 Free (Cheapest)" Program 1. Go to **Point of Sale → Products → Loyalty Programs** 2. Create a new program with type **Loyalty Card** or **Promotion** 3. Under **Conditional Rules**: - **Minimum Quantity:** `2` - **Grant:** `1 Credit per Unit Paid` 4. Under **Rewards** → Add a reward: - **Reward Type:** `Free Product` - **Reward Product Applicability:** `Cheapest Product on Order` *(new field)* - **In Exchange of:** `2 Credits` *(1 credit per product, 2 needed → 1 free)* - **Quantity Rewarded:** `1` > **Important:** Set "In Exchange of" to `2` (not `1`) to get the correct `buy 2 → 1 free` ratio. > The formula is `floor(total_credits / credits_required) = free_items`. --- ## Technical Details ### Modified Files #### Python (Backend) | File | Description | |---|---| | `models/loyalty_reward.py` | Adds `reward_product_applicability` selection field; overrides `_compute_multi_product` to handle cheapest type | | `models/sale_order.py` | Overrides reward value computation for server-side cheapest product identification | #### JavaScript (Frontend) | File | Description | |---|---| | `static/src/app/models/pos_order.js` | Core reward logic patches — cheapest product detection, unclaimed qty computation, multi-product reward line generation, `getClaimableRewards` override | | `static/src/app/services/pos_store.js` | Patches `getPotentialFreeProductRewards` to surface cheapest rewards with no fixed `reward_product_ids` | | `static/src/app/screens/product_screen/control_buttons/control_buttons.js` | Patches `_applyReward` to route cheapest rewards through dynamic product resolution | #### Views | File | Description | |---|---| | `views/loyalty_reward_views.xml` | Adds the `reward_product_applicability` field to the loyalty reward form | ### Key Methods - **`_getCheapestProductInOrder(reward)`** — Returns the single cheapest non-reward product in the current order - **`_getCheapestProductsInOrder(reward, n)`** — Returns the N cheapest individual items (sorted by unit price), expanding multi-qty lines - **`_computeUnclaimedFreeProductQtyForCheapest(...)`** — Calculates how many free items are still unclaimed based on remaining loyalty points - **`getClaimableRewards(...)`** — Extended to include cheapest-type rewards that the core skips (due to null `reward_product_id`) --- ## Installation ```bash # Copy module to your custom addons path cp -r pos_loyalty_extend /path/to/odoo/customaddons/ # Update addons list and install ./odoo-bin -c odoo.conf -u pos_loyalty_extend ``` Or via Odoo UI: 1. Enable **Developer Mode** 2. Go to **Apps → Update Apps List** 3. Search for `POS Loyalty Extend` and install --- ## Changelog ### v1.0 - Initial release - Added `cheapest` reward product applicability - Auto-claim cheapest product rewards in POS - Proportional multi-product free item generation (N cheapest products, not N× same product) - Fixed `getClaimableRewards` and `getPotentialFreeProductRewards` to surface cheapest rewards - Fixed `_updateRewardLines` deduplication for multiple reward lines per reward