From 658bb6c72a2aeec0047d650269c7515219d62efb Mon Sep 17 00:00:00 2001 From: "Suherdy SYC. Yacob" Date: Tue, 2 Sep 2025 15:25:52 +0700 Subject: [PATCH] first commit --- README.md | 48 +++++ __init__.py | 3 + __manifest__.py | 23 ++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 202 bytes models/__init__.py | 4 + models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 246 bytes models/__pycache__/pos_config.cpython-312.pyc | Bin 0 -> 1000 bytes models/__pycache__/pos_order.cpython-312.pyc | Bin 0 -> 6601 bytes models/pos_config.py | 18 ++ models/pos_order.py | 151 +++++++++++++ static/src/models.js | 199 ++++++++++++++++++ views/pos_config_views.xml | 29 +++ 12 files changed, 475 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/pos_config.cpython-312.pyc create mode 100644 models/__pycache__/pos_order.cpython-312.pyc create mode 100644 models/pos_config.py create mode 100644 models/pos_order.py create mode 100644 static/src/models.js create mode 100644 views/pos_config_views.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5851a2 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# POS Order Line Discount + +This module converts order-level discounts (like global discount and loyalty program reward discount) to order line discounts in the Point of Sale. + +## Features + +- Convert order-level discounts to line-level discounts +- Support for loyalty program rewards +- Configurable discount distribution methods +- Maintain proper tax calculations + +## Configuration + +1. Go to Point of Sale > Configuration > Point of Sale +2. Select your POS configuration +3. In the Pricing section, you'll find: + - **Line Discount Type**: Choose between percentage or fixed amount distribution + - **Apply Line Discount on Rewards**: Enable to convert loyalty rewards to line discounts + +## How it works + +The module works by intercepting the reward application process and distributing the discount amount across all order lines instead of creating a separate reward line. This ensures that: + +1. All discounts are visible at the line level +2. Tax calculations are properly maintained +3. Reporting shows discounts at the line level + +## Technical Details + +The module patches the following components: + +1. **PosOrder model**: Processes rewards as line discounts during order creation +2. **PosConfig model**: Adds configuration options for discount distribution +3. **Order model (JS)**: Overrides reward application to distribute discounts to lines +4. **Orderline model (JS)**: Enhances line discount handling + +## Compatibility + +This module is compatible with Odoo 17 and requires: +- point_of_sale +- pos_loyalty + +## Installation + +1. Place the module in your Odoo addons directory +2. Update the apps list +3. Install the "POS Order Line Discount" module +4. Configure your POS settings as needed diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..cde864b --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..329c273 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +{ + "name": "POS Order Line Discount", + "version": "1.0", + "category": "Point of Sale", + "summary": "Convert order-level discounts to line-level discounts in POS", + "description": """ + This module converts order-level discounts (like global discount and loyalty program reward discount) + to order line discounts in the Point of Sale. + """, + "depends": ["point_of_sale", "pos_loyalty"], + "data": [ + "views/pos_config_views.xml", + ], + "assets": { + "point_of_sale._assets_pos": [ + 'pos_order_line_discount/static/src/**/*', + ], + }, + "installable": True, + "auto_install": False, + "license": "LGPL-3" +} diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dcdcfb52e287fcaff46bd2178bfe6b9371452287 GIT binary patch literal 202 zcmX@j%ge<81e>zAW*7qL#~=<2FhLog1%Qm{3@HpLj5!Rsj8Tk?43$ip%r6;%!kUb? z*mCnzQge#^G?{KO6fpzERx*4B>HOuIVii+RS(1^Tmkd-9lb@2GZ)hHqTv}X`pPQJH zlAl)`Q;=UApI?-cS`?p?nU@-$l3ARbUz%4E6CaT)XTEqtA0ssLwG|T`1 literal 0 HcmV?d00001 diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e27bb3d --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import pos_order +from . import pos_config diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0d392b268e145829db7d5f0ed76bbdd759670e71 GIT binary patch literal 246 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 RIl1TN{u}%MiJ(bj`Zv=UQhxvd literal 0 HcmV?d00001 diff --git a/models/pos_config.py b/models/pos_config.py new file mode 100644 index 0000000..17f895c --- /dev/null +++ b/models/pos_config.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields + + +class PosConfig(models.Model): + _inherit = 'pos.config' + + line_discount_type = fields.Selection([ + ('percentage', 'Percentage'), + ('fixed', 'Fixed Amount') + ], string='Line Discount Type', default='percentage', + help="Determines how order-level discounts are distributed to order lines") + + apply_line_discount_on_rewards = fields.Boolean( + string="Apply Line Discount on Rewards", + default=True, + help="If checked, loyalty rewards will be applied as line discounts instead of order-level discounts" + ) diff --git a/models/pos_order.py b/models/pos_order.py new file mode 100644 index 0000000..823502a --- /dev/null +++ b/models/pos_order.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +from odoo import models, api, fields +from odoo.tools import float_compare, float_round +import logging + +_logger = logging.getLogger(__name__) + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + def _distribute_discount_to_lines(self, order, discount_amount, is_percentage=False): + """ + Distribute an order-level discount amount to individual order lines + :param order: pos.order record + :param discount_amount: The discount amount to distribute + :param is_percentage: If True, discount_amount is a percentage; otherwise it's a fixed amount + :return: Dictionary with line_id as key and discount value as value + """ + if not order.lines: + return {} + + # Filter out reward lines and lines with zero price + regular_lines = order.lines.filtered(lambda l: not l.is_reward_line and l.price_subtotal > 0) + if not regular_lines: + return {} + + # Calculate total tax-exclusive amount for proportional distribution + total_amount = sum(line.price_subtotal for line in regular_lines) + if total_amount <= 0: + return {} + + line_discounts = {} + + if is_percentage: + # Apply the same percentage discount to all regular lines + for line in regular_lines: + # For percentage discounts, we simply add the percentages (simplified approach) + # In a real scenario, you might want to compound them properly + new_discount = min(100, line.discount + discount_amount) + line_discounts[line.id] = new_discount + else: + # Distribute fixed amount proportionally based on line tax-exclusive subtotal + remaining_discount = discount_amount + lines_count = len(regular_lines) + + for i, line in enumerate(regular_lines): + if i == lines_count - 1: + # Last line gets the remaining discount to avoid rounding issues + # Calculate the additional discount percentage for this line based on tax-exclusive price + if line.price_subtotal > 0: + additional_discount_percentage = (remaining_discount / line.price_subtotal) * 100 + new_discount = min(100, line.discount + additional_discount_percentage) + else: + new_discount = line.discount + line_discounts[line.id] = new_discount + else: + # Calculate proportional discount for this line based on tax-exclusive price + line_ratio = line.price_subtotal / total_amount if total_amount > 0 else 0 + line_discount_amount = discount_amount * line_ratio + # Calculate the additional discount percentage for this line based on tax-exclusive price + if line.price_subtotal > 0: + additional_discount_percentage = (line_discount_amount / line.price_subtotal) * 100 + new_discount = min(100, line.discount + additional_discount_percentage) + else: + new_discount = line.discount + line_discounts[line.id] = new_discount + remaining_discount -= line_discount_amount + + return line_discounts + + def _apply_line_discounts(self, order, line_discounts): + """ + Apply calculated discounts to order lines + :param order: pos.order record + :param line_discounts: Dictionary with line_id as key and discount value as value + """ + for line in order.lines: + if line.id in line_discounts: + # Apply the calculated discount + line.discount = line_discounts[line.id] + # Trigger the onchange to recalculate the line amounts + line._onchange_amount_line_all() + + @api.model + def _process_order_rewards_as_line_discounts(self, order): + """ + Process loyalty rewards as line discounts instead of order-level discounts + :param order: pos.order record + """ + if not order.config_id.apply_line_discount_on_rewards: + return + + # Find reward lines + reward_lines = order.lines.filtered(lambda l: l.is_reward_line) + + for reward_line in reward_lines: + reward = reward_line.reward_id + if reward and reward.reward_type == 'discount': + # Calculate the discount amount that should be applied to regular lines + reward_amount = abs(reward_line.price_subtotal) + + # Remove the reward line + reward_line.unlink() + + # Distribute the discount to regular lines + is_percentage = (reward.discount_mode == 'percent') + discount_value = reward_amount if not is_percentage else reward.discount + + line_discounts = self._distribute_discount_to_lines( + order, + discount_value, + is_percentage=is_percentage + ) + + # Apply the discounts to lines + self._apply_line_discounts(order, line_discounts) + + @api.model + def _process_order(self, order, draft, existing_order): + """ + Override to process rewards as line discounts + """ + pos_order_id = super(PosOrder, self)._process_order(order, draft, existing_order) + + # Convert order-level rewards to line discounts + if isinstance(pos_order_id, int): + pos_order = self.browse(pos_order_id) + self._process_order_rewards_as_line_discounts(pos_order) + + return pos_order_id + + +class PosOrderLine(models.Model): + _inherit = 'pos.order.line' + + def _compute_amount_line_all(self): + """ + Override to handle line-level discounts properly + """ + res = super(PosOrderLine, self)._compute_amount_line_all() + # Additional logic for line discounts can be added here if needed + return res + + @api.onchange('discount') + def _onchange_discount(self): + """ + Override to handle line discount changes + """ + super(PosOrderLine, self)._onchange_discount() + # Additional logic for handling line discount changes can be added here diff --git a/static/src/models.js b/static/src/models.js new file mode 100644 index 0000000..41bb16c --- /dev/null +++ b/static/src/models.js @@ -0,0 +1,199 @@ +/** @odoo-module **/ + +import { Order, Orderline } from "@point_of_sale/app/store/models"; +import { patch } from "@web/core/utils/patch"; + +// Patch Order methods to handle line discounts +patch(Order.prototype, { + /** + * Override to calculate reward values as line discounts instead of order-level discounts + */ + _getRewardLineValues(args) { + const reward = args["reward"]; + const coupon_id = args["coupon_id"]; + + // If config is not set to apply rewards as line discounts, use the original method + // Note: In JavaScript, we access the config through this.pos.config + if (!this.pos.config.apply_line_discount_on_rewards) { + return super._getRewardLineValues(...arguments); + } + + // For discount rewards, we'll distribute the discount to order lines instead of creating reward lines + if (reward.reward_type === "discount") { + // Calculate the reward using the original method first to get the discount amount + const originalRewardLines = super._getRewardLineValues(...arguments); + + // Get the total discount amount from the reward lines (tax-excluded) + let totalDiscountAmount = 0; + originalRewardLines.forEach(line => { + // Calculate tax-excluded amount from the reward line + // The line.price is tax-included, so we need to calculate the tax-excluded amount + // Reward lines are plain objects, not Orderline instances, so we calculate manually + let taxExcludedAmount = Math.abs(line.price * line.quantity); + + // If the line has tax information, we need to calculate the tax-excluded amount + if (line.tax_ids && line.tax_ids.length > 0) { + // Calculate total tax rate (simplified approach for multiple taxes) + let totalTaxRate = 0; + for (const taxId of line.tax_ids) { + const tax = this.pos.taxes_by_id[taxId]; + if (tax && tax.amount_type === 'percent') { + totalTaxRate += tax.amount / 100; + } + } + + if (totalTaxRate > 0) { + taxExcludedAmount = Math.abs(line.price * line.quantity) / (1 + totalTaxRate); + } + } + + totalDiscountAmount += taxExcludedAmount; + }); + + // Distribute the discount to order lines instead of creating reward lines + // Filter out reward lines by checking if they have a reward_id property + const orderLines = this.get_orderlines().filter(line => !line.is_reward_line); + const totalOrderAmount = orderLines.reduce((sum, line) => sum + line.get_price_without_tax(), 0); + + if (totalOrderAmount > 0 && totalDiscountAmount > 0) { + // Apply the discount to each line proportionally based on its contribution to the total + orderLines.forEach(line => { + // Calculate the line's share of the total discount + const lineRatio = line.get_price_without_tax() / totalOrderAmount; + const lineDiscountAmount = totalDiscountAmount * lineRatio; + + // Calculate the discount percentage for this line + // We want to set a discount that results in the lineDiscountAmount reduction + if (line.get_price_without_tax() > 0) { + const lineDiscountPercentage = (lineDiscountAmount / line.get_price_without_tax()) * 100; + // Set the discount percentage for this line (same as %Disc button) + // Ensure the discount percentage is within valid range [0, 100] + const validDiscountPercentage = Math.min(100, Math.max(0, lineDiscountPercentage)); + // Mark this line as having a reward discount applied + if (line.originalDiscount === undefined) { + line.originalDiscount = line.discount; + } + // Apply the new discount on top of the original + line.discount = line.originalDiscount + validDiscountPercentage; + } + }); + } + + // Return empty array since we're not creating reward lines + return []; + } + + // For non-discount rewards, use the original method + return super._getRewardLineValues(...arguments); + }, + + /** + * Distribute an order-level discount to individual lines + */ + _distributeDiscountToLines(discountAmount, isPercentage = false) { + const lines = this.get_orderlines().filter(line => !line.reward_id); + if (lines.length === 0) return; + + // Calculate total amount for proportional distribution + const totalAmount = lines.reduce((sum, line) => sum + line.get_price_without_tax(), 0); + if (totalAmount <= 0) return; + + if (isPercentage) { + // Apply the same percentage discount to all lines + lines.forEach(line => { + const currentDiscount = line.get_discount(); + // Combine discounts (this is a simplification) + // Store the original discount if not already stored + if (line.originalDiscount === undefined) { + line.originalDiscount = line.discount; + } + const newDiscount = Math.min(100, line.originalDiscount + discountAmount); + line.discount = newDiscount; + }); + } else { + // Distribute fixed amount proportionally based on line subtotal + lines.forEach(line => { + const lineRatio = line.get_price_without_tax() / totalAmount; + const lineDiscountAmount = discountAmount * lineRatio; + // Calculate the additional discount percentage for this line + if (line.get_price_without_tax() > 0) { + const additionalDiscountPercentage = (lineDiscountAmount / line.get_price_without_tax()) * 100; + // Store the original discount if not already stored + if (line.originalDiscount === undefined) { + line.originalDiscount = line.discount; + } + const newDiscountPercentage = Math.min(100, line.originalDiscount + additionalDiscountPercentage); + line.discount = newDiscountPercentage; + } + }); + } + }, + + /** + * Apply a global discount to all order lines + */ + setGlobalDiscount(discountPercentage) { + this.get_orderlines().forEach(line => { + if (!line.is_reward_line) { + // Store the original discount if not already stored + if (line.originalDiscount === undefined) { + line.originalDiscount = line.discount; + } + line.discount = discountPercentage; + } + }); + }, + + /** + * Override to also reset line discounts applied by this module + */ + _resetPrograms() { + // First call the original reset method + super._resetPrograms(...arguments); + + // Reset line discounts that were applied by this module + this.get_orderlines().forEach(line => { + if (!line.is_reward_line && line.originalDiscount !== undefined) { + // Reset to the original discount value + line.discount = line.originalDiscount; + delete line.originalDiscount; + } + }); + }, + + /** + * Clear all line discounts applied by this module + */ + clearLineDiscounts() { + this.get_orderlines().forEach(line => { + if (!line.is_reward_line && line.originalDiscount !== undefined) { + // Reset to the original discount value + line.discount = line.originalDiscount; + delete line.originalDiscount; + } + }); + } +}); + +// Patch Orderline methods to handle line discounts +patch(Orderline.prototype, { + /** + * Override to handle line discount changes + */ + set_discount(discount) { + // Call the original method + super.set_discount(...arguments); + + // Store the original discount if not already stored and this is not a reward line + if (this.originalDiscount === undefined && !this.is_reward_line) { + this.originalDiscount = this.discount; + } + }, + + /** + * Get the discount amount for this line + */ + get_discount_amount() { + return this.get_price_without_tax() * (this.get_discount() / 100); + } +}); diff --git a/views/pos_config_views.xml b/views/pos_config_views.xml new file mode 100644 index 0000000..2d1a31e --- /dev/null +++ b/views/pos_config_views.xml @@ -0,0 +1,29 @@ + + + + + pos.config.form.line.discount + pos.config + + + +
+
+

Line Discount Options

+
+
+
+
+
+
+
+
+
+
+
+
+