From a2b6bc833c48377aa8227d8be41d55e5b1eb8678 Mon Sep 17 00:00:00 2001 From: hugues de keyzer Date: Mon, 16 Jun 2025 15:18:38 +0200 Subject: [PATCH] [IMP] improve taxes computation * add support for taxes of amount_type "fixed". * take the include_base_amount field into account. --- .../models/product_product.py | 18 +- .../tests/__init__.py | 1 + .../tests/test_taxes_computation.py | 174 ++++++++++++++++++ 3 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 product_margin_classification/tests/test_taxes_computation.py diff --git a/product_margin_classification/models/product_product.py b/product_margin_classification/models/product_product.py index 680c75615..20fcab609 100644 --- a/product_margin_classification/models/product_product.py +++ b/product_margin_classification/models/product_product.py @@ -76,8 +76,9 @@ def _get_margin_info( ): precision = self.env["decimal.precision"].precision_get("Product Price") if classification: - multi = (100 + classification.markup) / 100 - if sale_taxes.filtered(lambda x: x.amount_type != "percent"): + base_price = standard_price * ((100 + classification.markup) / 100) + extra_included_taxes = 0 + if sale_taxes.filtered(lambda x: x.amount_type not in ("percent", "fixed")): raise ValidationError( _( "Unimplemented Feature\n" @@ -87,10 +88,19 @@ def _get_margin_info( % (product_name) ) for tax in sale_taxes.filtered(lambda x: x.price_include): - multi *= (100 + tax.amount) / 100.0 + if tax.amount_type == "percent": + if tax.include_base_amount: + base_price *= (100 + tax.amount) / 100 + else: + extra_included_taxes += base_price * (tax.amount / 100) + elif tax.amount_type == "fixed": + if tax.include_base_amount: + base_price += tax.amount + else: + extra_included_taxes += tax.amount theoretical_price = ( tools.float_round( - standard_price * multi, + base_price + extra_included_taxes, precision_rounding=classification.price_round, ) + classification.price_surcharge diff --git a/product_margin_classification/tests/__init__.py b/product_margin_classification/tests/__init__.py index 7806aedde..582a3c09c 100644 --- a/product_margin_classification/tests/__init__.py +++ b/product_margin_classification/tests/__init__.py @@ -1,3 +1,4 @@ from . import test_product_margin_classification from . import test_product_product from . import test_product_template +from . import test_taxes_computation diff --git a/product_margin_classification/tests/test_taxes_computation.py b/product_margin_classification/tests/test_taxes_computation.py new file mode 100644 index 000000000..bc19401cf --- /dev/null +++ b/product_margin_classification/tests/test_taxes_computation.py @@ -0,0 +1,174 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestTaxesComputation(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.percent_excluded_tax = cls.env["account.tax"].create( + { + "name": "percent excluded", + "amount_type": "percent", + "amount": 10, + "price_include": False, + } + ) + cls.percent_included_tax = cls.env["account.tax"].create( + { + "name": "percent included", + "amount_type": "percent", + "amount": 10, + "price_include": True, + "include_base_amount": True, + } + ) + cls.fixed_excluded_tax = cls.env["account.tax"].create( + { + "name": "fixed excluded", + "amount_type": "fixed", + "amount": 1, + "price_include": False, + } + ) + cls.fixed_included_tax = cls.env["account.tax"].create( + { + "name": "fixed included", + "amount_type": "fixed", + "amount": 1, + "price_include": True, + } + ) + cls.test_product = cls.env["product.product"].create( + { + "name": "test product", + "standard_price": 10, + "list_price": 15, + "margin_classification_id": cls.env.ref( + "product_margin_classification.classification_normal_margin" + ).id, + } + ) + + def test_percent_excluded_tax(self): + self.test_product.taxes_id = [ + fields.Command.set([self.percent_excluded_tax.id]) + ] + self.assertAlmostEqual(self.test_product.theoretical_price, 15) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(15)["total_excluded"], 15 + ) + + def test_percent_included_tax(self): + self.test_product.taxes_id = [ + fields.Command.set([self.percent_included_tax.id]) + ] + self.assertAlmostEqual(self.test_product.theoretical_price, 16.5) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(16.5)["total_excluded"], 15 + ) + + def test_fixed_excluded_tax(self): + self.test_product.taxes_id = [fields.Command.set([self.fixed_excluded_tax.id])] + self.assertAlmostEqual(self.test_product.theoretical_price, 15) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(15)["total_excluded"], 15 + ) + + def test_fixed_included_tax(self): + self.test_product.taxes_id = [fields.Command.set([self.fixed_included_tax.id])] + self.assertAlmostEqual(self.test_product.theoretical_price, 16) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(16)["total_excluded"], 15 + ) + + def test_percent_excluded_fixed_excluded_tax(self): + self.test_product.taxes_id = [ + fields.Command.set( + [self.percent_excluded_tax.id, self.fixed_excluded_tax.id] + ) + ] + self.assertAlmostEqual(self.test_product.theoretical_price, 15) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(15)["total_excluded"], 15 + ) + + def test_percent_included_fixed_excluded_tax(self): + self.test_product.taxes_id = [ + fields.Command.set( + [self.percent_included_tax.id, self.fixed_excluded_tax.id] + ) + ] + self.assertAlmostEqual(self.test_product.theoretical_price, 16.5) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(16.5)["total_excluded"], 15 + ) + + def test_percent_excluded_fixed_included_tax(self): + self.test_product.taxes_id = [ + fields.Command.set( + [self.percent_excluded_tax.id, self.fixed_included_tax.id] + ) + ] + self.assertAlmostEqual(self.test_product.theoretical_price, 16) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(16)["total_excluded"], 15 + ) + + def test_percent_included_fixed_included_tax(self): + self.test_product.taxes_id = [ + fields.Command.set( + [self.percent_included_tax.id, self.fixed_included_tax.id] + ) + ] + self.assertAlmostEqual(self.test_product.theoretical_price, 17.5) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(17.5)["total_excluded"], 15 + ) + + def test_include_base_amount_1(self): + tax_1 = self.percent_included_tax.copy({"sequence": 1}) + tax_2 = self.percent_included_tax.copy({"sequence": 2}) + self.test_product.taxes_id = [fields.Command.set([tax_1.id, tax_2.id])] + self.assertAlmostEqual(self.test_product.theoretical_price, 18.15) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(18.15)["total_excluded"], 15 + ) + + def test_include_base_amount_2(self): + tax_1 = self.percent_included_tax.copy( + {"sequence": 1, "include_base_amount": False} + ) + tax_2 = self.percent_included_tax.copy({"sequence": 2}) + self.test_product.taxes_id = [fields.Command.set([tax_1.id, tax_2.id])] + self.assertAlmostEqual(self.test_product.theoretical_price, 18) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(18)["total_excluded"], 15 + ) + + def test_include_base_amount_3(self): + tax_1 = self.fixed_included_tax.copy( + {"sequence": 1, "include_base_amount": True} + ) + tax_2 = self.percent_included_tax.copy({"sequence": 2}) + self.test_product.taxes_id = [fields.Command.set([tax_1.id, tax_2.id])] + self.assertAlmostEqual(self.test_product.theoretical_price, 17.6) + self.assertAlmostEqual( + self.test_product.taxes_id.compute_all(17.6)["total_excluded"], 15 + ) + + def test_unsupported_tax_amount_type(self): + division_tax = self.env["account.tax"].create( + { + "name": "division", + "amount_type": "division", + "amount": 10, + } + ) + with self.assertRaises(ValidationError): + self.test_product.taxes_id = [fields.Command.set([division_tax.id])]