# Copyright 2025 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) """Price calculations around included/excluded taxes. Checks how `account.tax.compute_all` and the OCA helper `product.product._get_price` (from `product_get_price_helper`) behave for the shop, including the tax breakdown and fiscal-position mapping. Odoo 18 note: `account.tax.price_include` is a *computed* boolean derived from `price_include_override` (and the company default). Tax fixtures must set `price_include_override` — writing `price_include` directly is discarded. """ from odoo.tests import tagged from odoo.tests.common import TransactionCase @tagged("post_install", "-at_install") class TestPriceWithTaxesIncluded(TransactionCase): """Test that prices displayed include taxes.""" def setUp(self): super().setUp() self.company = self.env["res.company"].create( { "name": "Test Company Tax Included", } ) tax_group = self.env["account.tax.group"].search( [("company_id", "=", self.company.id)], limit=1 ) if not tax_group: tax_group = self.env["account.tax.group"].create( { "name": "IVA", "company_id": self.company.id, } ) country_es = self.env.ref("base.es") self.tax_21 = self._create_tax("IVA 21%", 21.0, tax_group, country_es) self.tax_10 = self._create_tax("IVA 10%", 10.0, tax_group, country_es) self.tax_21_included = self._create_tax( "IVA 21% Incluido", 21.0, tax_group, country_es, included=True ) self.category = self.env["product.category"].create( { "name": "Test Category Tax Included", } ) self.product_21 = self._create_product("Product With 21% Tax", self.tax_21) self.product_10 = self._create_product("Product With 10% Tax", self.tax_10) self.product_no_tax = self._create_product("Product Without Tax", None) self.product_tax_included = self._create_product( # 100 + 21% = 121, already carrying the tax. "Product With Tax Included", self.tax_21_included, list_price=121.0, ) self.pricelist = self.env["product.pricelist"].create( { "name": "Test Pricelist", "company_id": self.company.id, } ) def _create_tax(self, name, amount, tax_group, country, included=False): """Create a sale tax, choosing included/excluded via the override.""" return self.env["account.tax"].create( { "name": name, "amount": amount, "amount_type": "percent", "type_tax_use": "sale", "price_include_override": ( "tax_included" if included else "tax_excluded" ), "company_id": self.company.id, "country_id": country.id, "tax_group_id": tax_group.id, } ) def _create_product(self, name, tax, list_price=100.0): return self.env["product.product"].create( { "name": name, "list_price": list_price, "categ_id": self.category.id, "taxes_id": [(6, 0, [tax.id])] if tax else False, "company_id": self.company.id, } ) def _company_taxes(self, product): return product.taxes_id.filtered(lambda t: t.company_id == self.company) def _get_price(self, product, fposition=False): """Call the OCA helper for the fixture company. `_get_price` defaults `company` to `self.env.company` and filters the product taxes by it, so omitting it here would silently drop every tax of the test company and make `tax_included` always False. """ return product._get_price( qty=1.0, pricelist=self.pricelist, fposition=fposition, company=self.company, ) def _compute_all(self, product, base_price=100.0, taxes=None): taxes = self._company_taxes(product) if taxes is None else taxes return taxes.compute_all( base_price, currency=self.env.company.currency_id, quantity=1.0, product=product, ) def test_price_include_is_driven_by_the_override(self): """Guard: the fixtures really are included/excluded as intended. `price_include` is computed in Odoo 18, so a fixture that sets the wrong field silently produces excluded taxes everywhere. """ self.assertFalse(self.tax_21.price_include) self.assertFalse(self.tax_10.price_include) self.assertTrue(self.tax_21_included.price_include) def test_price_with_21_percent_tax(self): """Test that 21% tax is correctly added to base price.""" result = self._compute_all(self.product_21) self.assertAlmostEqual( result["total_included"], 121.0, places=2, msg="100 + 21% should be 121.0" ) def test_price_with_10_percent_tax(self): """Test that 10% tax is correctly added to base price.""" result = self._compute_all(self.product_10) self.assertAlmostEqual( result["total_included"], 110.0, places=2, msg="100 + 10% should be 110.0" ) def test_price_without_tax(self): """A product with no taxes keeps its base price.""" self.assertFalse( self._company_taxes(self.product_no_tax), "Product should have no taxes" ) price_info = self._get_price(self.product_no_tax) self.assertAlmostEqual(price_info["value"], 100.0, places=2) self.assertFalse(price_info["tax_included"]) def test_oca_get_price_returns_base_without_tax(self): """OCA `_get_price` returns the base price for tax-excluded products.""" price_info = self._get_price(self.product_21) self.assertAlmostEqual( price_info["value"], 100.0, places=2, msg="OCA _get_price should return base price without tax", ) self.assertFalse( price_info["tax_included"], msg="tax_included should be False for a tax-excluded tax", ) def test_oca_get_price_with_included_tax(self): """OCA `_get_price` flags tax_included for a tax-included product.""" price_info = self._get_price(self.product_tax_included) self.assertAlmostEqual( price_info["value"], 121.0, places=2, msg="Price with included tax should stay at 121.0", ) self.assertTrue( price_info["tax_included"], msg="tax_included should be True for a tax-included tax", ) def test_compute_all_with_multiple_taxes(self): """Test tax calculation with multiple taxes.""" product_multi = self.env["product.product"].create( { "name": "Product With Multiple Taxes", "list_price": 100.0, "categ_id": self.category.id, "taxes_id": [(6, 0, [self.tax_21.id, self.tax_10.id])], "company_id": self.company.id, } ) result = self._compute_all(product_multi) # 100 + 21 + 10 = 131.0 self.assertAlmostEqual( result["total_included"], 131.0, places=2, msg="100 + 21% + 10% = 131.0" ) def test_compute_all_with_fiscal_position(self): """A fiscal position remapping 21% to 10% changes the total.""" fiscal_position = self.env["account.fiscal.position"].create( { "name": "Test Fiscal Position", "company_id": self.company.id, } ) self.env["account.fiscal.position.tax"].create( { "position_id": fiscal_position.id, "tax_src_id": self.tax_21.id, "tax_dest_id": self.tax_10.id, } ) mapped_taxes = fiscal_position.map_tax(self._company_taxes(self.product_21)) self.assertEqual(mapped_taxes, self.tax_10) result = self._compute_all(self.product_21, taxes=mapped_taxes) self.assertAlmostEqual( result["total_included"], 110.0, places=2, msg="Fiscal position should map to the 10% tax", ) def test_get_price_applies_fiscal_position(self): """`_get_price` honours the fiscal position it is handed.""" fiscal_position = self.env["account.fiscal.position"].create( { "name": "Map To Included Tax", "company_id": self.company.id, } ) self.env["account.fiscal.position.tax"].create( { "position_id": fiscal_position.id, "tax_src_id": self.tax_21.id, "tax_dest_id": self.tax_21_included.id, } ) price_info = self._get_price(self.product_21, fposition=fiscal_position) # The mapped tax is tax-included, so the helper must say so. self.assertTrue(price_info["tax_included"]) def test_tax_amount_details(self): """Test that compute_all provides a detailed tax breakdown.""" result = self._compute_all(self.product_21) self.assertIn("total_included", result) self.assertIn("total_excluded", result) self.assertIn("taxes", result) self.assertAlmostEqual(result["total_excluded"], 100.0, places=2) self.assertAlmostEqual(result["total_included"], 121.0, places=2) self.assertEqual(len(result["taxes"]), 1) self.assertAlmostEqual(result["taxes"][0]["amount"], 21.0, places=2) def test_included_tax_breakdown_keeps_the_gross_price(self): """For an included tax, the gross price is the one the member sees.""" result = self._compute_all(self.product_tax_included, base_price=121.0) self.assertAlmostEqual(result["total_included"], 121.0, places=2) self.assertAlmostEqual(result["total_excluded"], 100.0, places=2) self.assertAlmostEqual(result["taxes"][0]["amount"], 21.0, places=2) def test_zero_price_with_tax(self): """Test tax calculation on a free product.""" free_product = self._create_product( "Free Product With Tax", self.tax_21, list_price=0.0 ) result = self._compute_all(free_product, base_price=0.0) self.assertAlmostEqual( result["total_included"], 0.0, places=2, msg="Free product with tax should still be free", ) def test_high_precision_price_with_tax(self): """Test tax calculation with high precision prices.""" precise_product = self._create_product( "Precise Price Product", self.tax_21, list_price=99.99 ) result = self._compute_all(precise_product, base_price=99.99) # 99.99 + 21% = 120.9879 -> 120.99 self.assertAlmostEqual(result["total_included"], 99.99 * 1.21, places=2)