From e00c7a4359cc57e472a85798cb1bf13233d149fb Mon Sep 17 00:00:00 2001 From: "Andrea Grazioso (agr-odoo)" Date: Wed, 20 Dec 2023 17:43:59 +0100 Subject: [PATCH] [FIX] sale_coupon: no fixed tax discount Have a product with percent tax and fixed tax Have a promo program applying 50% on the order Create a sale order - Add the product - Add the promo Issue: The fixed tax is discounted We should ignore fixed taxes when applying promotions. opw-3580170 closes odoo/odoo#149165 X-original-commit: 2acce1c84586a0f91ac3a83570d574d0ec96f1f6 Signed-off-by: Andrea Grazioso (agr) --- addons/sale_loyalty/models/sale_order.py | 28 ++++-- .../tests/test_program_numbers.py | 95 +++++++++++++++++++ 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/addons/sale_loyalty/models/sale_order.py b/addons/sale_loyalty/models/sale_order.py index ad2ff6c2747..59fe166be46 100644 --- a/addons/sale_loyalty/models/sale_order.py +++ b/addons/sale_loyalty/models/sale_order.py @@ -171,9 +171,15 @@ class SaleOrder(models.Model): # Ignore lines from this reward if not line.product_uom_qty or not line.price_unit: continue - line_discountable = line.price_unit * line.product_uom_qty * (1 - (line.discount or 0.0) / 100.0) - discountable += line.price_total - discountable_per_tax[line.tax_id] += line_discountable + tax_data = line._convert_to_tax_base_line_dict() + # To compute the discountable amount we get the fixed tax amount and + # subtract it from the order total. This way fixed taxes will not be discounted + tax_data['taxes'] = tax_data['taxes'].filtered(lambda t: t.amount_type == 'fixed') + tax_results = self.env['account.tax']._compute_taxes([tax_data]) + totals = list(tax_results['totals'].values())[0] + discountable += line.price_total - totals['amount_tax'] + taxes = line.tax_id.filtered(lambda t: t.amount_type != 'fixed') + discountable_per_tax[taxes] += totals['amount_untaxed'] return discountable, discountable_per_tax def _cheapest_line(self): @@ -195,7 +201,8 @@ class SaleOrder(models.Model): cheapest_line = self._cheapest_line() discountable = cheapest_line.price_unit * (1 - (cheapest_line.discount or 0) / 100) - return discountable, {cheapest_line.tax_id: discountable} + taxes = cheapest_line.tax_id.filtered(lambda t: t.amount_type != 'fixed') + return discountable, {taxes: discountable} def _get_specific_discountable_lines(self, reward): """ @@ -258,21 +265,21 @@ class SaleOrder(models.Model): else: non_common_lines = discounted_lines - lines_to_discount # Fixed prices are per tax - discounted_amounts = {line.tax_id: abs(line.price_total) for line in lines} + discounted_amounts = {line.tax_id.filtered(lambda t: t.amount_type != 'fixed'): abs(line.price_total) for line in lines} for line in itertools.chain(non_common_lines, common_lines): # For gift card and eWallet programs we have no tax but we can consume the amount completely if lines.reward_id.program_id.is_payment_program: - discounted_amount = discounted_amounts[lines.tax_id] + discounted_amount = discounted_amounts[lines.tax_id.filtered(lambda t: t.amount_type != 'fixed')] else: - discounted_amount = discounted_amounts[line.tax_id] + discounted_amount = discounted_amounts[line.tax_id.filtered(lambda t: t.amount_type != 'fixed')] if discounted_amount == 0: continue remaining = remaining_amount_per_line[line] consumed = min(remaining, discounted_amount) if lines.reward_id.program_id.is_payment_program: - discounted_amounts[lines.tax_id] -= consumed + discounted_amounts[lines.tax_id.filtered(lambda t: t.amount_type != 'fixed')] -= consumed else: - discounted_amounts[line.tax_id] -= consumed + discounted_amounts[line.tax_id.filtered(lambda t: t.amount_type != 'fixed')] -= consumed remaining_amount_per_line[line] -= consumed discountable = 0 @@ -283,7 +290,8 @@ class SaleOrder(models.Model): # line_discountable is the same as in a 'order' discount # but first multiplied by a factor for the taxes to apply # and then multiplied by another factor coming from the discountable - discountable_per_tax[line.tax_id] += line_discountable *\ + taxes = line.tax_id.filtered(lambda t: t.amount_type != 'fixed') + discountable_per_tax[taxes] += line_discountable *\ (remaining_amount_per_line[line] / line.price_total) return discountable, discountable_per_tax diff --git a/addons/sale_loyalty/tests/test_program_numbers.py b/addons/sale_loyalty/tests/test_program_numbers.py index 691794bfaa4..1fbbc7e2bb4 100644 --- a/addons/sale_loyalty/tests/test_program_numbers.py +++ b/addons/sale_loyalty/tests/test_program_numbers.py @@ -1657,3 +1657,98 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon): self.assertEqual(len(order.order_line), 2, 'Promotion should add 1 line') self.assertEqual(order.amount_total, 5, '10$ - 5$(discount) = 5$(total) ') + + def test_fixed_tax_not_affected(self): + program = self.env['loyalty.program'].create({ + 'name': '50% discount', + 'program_type': 'promotion', + 'trigger': 'auto', + 'applies_on': 'current', + 'rule_ids': [(0, 0, { + })], + 'reward_ids': [(0, 0, { + 'reward_type': 'discount', + 'discount_mode': 'percent', + 'discount': 50, + 'discount_applicability': 'order', + 'required_points': 1, + })], + }) + + order = self.empty_order + # Create taxes + self.tax_15pc_excl = self.env['account.tax'].create({ + 'name': "15% Tax excl", + 'amount_type': 'percent', + 'amount': 15, + }) + self.tax_10_fixed = self.env['account.tax'].create({ + 'name': "10$ Fixed tax", + 'amount_type': 'fixed', + 'amount': 10, + }) + + # Set tax and prices on products as neeed for the test + self.product_A.write({'list_price': 100}) + self.product_A.taxes_id = (self.tax_15pc_excl + self.tax_10_fixed) + + # Add products in order + self.env['sale.order.line'].create({ + 'product_id': self.product_A.id, + 'name': 'product A', + 'product_uom_qty': 1.0, + 'order_id': order.id, + }) + + self._auto_rewards(order, program) + + self.assertEqual(len(order.order_line), 2, 'Promotion should add 1 line') + self.assertEqual(order.amount_total, 67.5, '100$ + 15% tax + 10$ tax - 50%(discount) = 67.5$(total) ') + self.assertEqual(order.amount_tax, 17.5, '15% tax + 10$ tax$ - 50%$(discount) = 17.5$(total) ') + + def test_fixed_tax_not_affected_2(self): + program = self.env['loyalty.program'].create({ + 'name': '50$ discount', + 'program_type': 'promotion', + 'trigger': 'auto', + 'applies_on': 'current', + 'rule_ids': [(0, 0, { + })], + 'reward_ids': [(0, 0, { + 'reward_type': 'discount', + 'discount_mode': 'per_order', + 'discount': 50, + 'discount_applicability': 'order', + 'required_points': 1, + })], + }) + + order = self.empty_order + # Create taxes + self.tax_15pc_excl = self.env['account.tax'].create({ + 'name': "15% Tax excl", + 'amount_type': 'percent', + 'amount': 15, + }) + self.tax_10_fixed = self.env['account.tax'].create({ + 'name': "10$ Fixed tax", + 'amount_type': 'fixed', + 'amount': 10, + }) + + # Set tax and prices on products as neeed for the test + self.product_A.write({'list_price': 100}) + self.product_A.taxes_id = (self.tax_15pc_excl + self.tax_10_fixed) + + # Add products in order + self.env['sale.order.line'].create({ + 'product_id': self.product_A.id, + 'name': 'product A', + 'product_uom_qty': 1.0, + 'order_id': order.id, + }) + + self._auto_rewards(order, program) + + self.assertEqual(len(order.order_line), 2, 'Promotion should add 1 line') + self.assertEqual(order.amount_total, 75, '100$ + 15% tax + 10$ tax - 50$(discount) = 75$(total) ')