From 3d8be5aa4c3d5a347423329d9f2095bed418f04a Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Tue, 6 Feb 2024 11:09:49 +0000 Subject: [PATCH] [FIX] website_sale: consistent tax computation While fixing a non deterministic test failing on nightly l10n builds, slight incoherences between sale & website_sale tax computation have been noticed in the computation of the contextual price (used in some snippets). This commit fixes the test, making sure it doesn't fail on l10n builds, but also uses the same tax util in website_sale than in sale, to make sure the displayed amounts are coherent (and supposedly correct). runbot error: 52831 (& a bunch of others) closes odoo/odoo#153299 X-original-commit: 91f9057c73810c34a4b0b85a60c84bb1c482efa4 Signed-off-by: Victor Feyens (vfe) --- addons/sale/models/sale_order_line.py | 3 +- addons/website_sale/models/product_product.py | 27 +++- .../tests/test_website_sale_product.py | 150 +++++++++++++++++- 3 files changed, 170 insertions(+), 10 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 5e55897a949..2f56f5a8f94 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -469,7 +469,8 @@ class SaleOrderLine(models.Model): if not line.product_uom or not line.product_id: line.price_unit = 0.0 else: - price = line.with_company(line.company_id)._get_display_price() + line = line.with_company(line.company_id) + price = line._get_display_price() line.price_unit = line.product_id._get_tax_included_unit_price( line.company_id or line.env.company, line.order_id.currency_id, diff --git a/addons/website_sale/models/product_product.py b/addons/website_sale/models/product_product.py index 7eec169b934..a85e7acfda6 100644 --- a/addons/website_sale/models/product_product.py +++ b/addons/website_sale/models/product_product.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models, _ +from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -109,9 +108,23 @@ class Product(models.Model): def _get_contextual_price_tax_selection(self): self.ensure_one() price = self._get_contextual_price() - website = self.env['website'].get_current_website() - line_tax_type = website.show_line_subtotals_tax_selection - company_taxes = self.taxes_id.filtered(lambda tax: tax.company_id in self.env.company.parent_ids) - if line_tax_type == "tax_included" and company_taxes: - price = company_taxes.compute_all(price, product=self, partner=self.env['res.partner'])['total_included'] + product_taxes = self.sudo().taxes_id.filtered(lambda x: x.company_id in self.env.company.parent_ids) + if product_taxes: + website = self.env['website'].get_current_website() + fiscal_position = website.sudo().fiscal_position_id + + price = self._get_tax_included_unit_price( + website.company_id, + website.currency_id, + fields.Date.context_today(self), + 'sale', + fiscal_position=fiscal_position, + product_price_unit=price, + product_currency=website.currency_id, + ) + line_tax_type = website.show_line_subtotals_tax_selection + tax_display = "total_included" if line_tax_type == "tax_included" else "total_excluded" + + taxes = fiscal_position.map_tax(product_taxes) + price = taxes.compute_all(price, product=self, partner=self.env['res.partner'])[tax_display] return price diff --git a/addons/website_sale/tests/test_website_sale_product.py b/addons/website_sale/tests/test_website_sale_product.py index bb4fdc8f29f..4bdc3891d6b 100644 --- a/addons/website_sale/tests/test_website_sale_product.py +++ b/addons/website_sale/tests/test_website_sale_product.py @@ -1,7 +1,16 @@ -# coding: utf-8 +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import itertools + +from odoo.fields import Command from odoo.tests import tagged + +from odoo.addons.sale.tests.test_sale_product_attribute_value_config import ( + TestSaleProductAttributeValueCommon, +) from odoo.addons.website.tools import MockRequest -from odoo.addons.sale.tests.test_sale_product_attribute_value_config import TestSaleProductAttributeValueCommon +from odoo.addons.website_sale.controllers.main import WebsiteSale + @tagged('post_install', '-at_install') class WebsiteSaleProductTests(TestSaleProductAttributeValueCommon): @@ -12,6 +21,39 @@ class WebsiteSaleProductTests(TestSaleProductAttributeValueCommon): cls.website = cls.env.ref('website.default_website') cls.website.company_id = cls.env.company + cls.tax_5 = cls.env['account.tax'].create({ + 'name': '5% Tax', + 'amount_type': 'percent', + 'amount': 5, + 'price_include': False, + 'include_base_amount': False, + 'type_tax_use': 'sale', + }) + cls.tax_10 = cls.env['account.tax'].create({ + 'name': '10% Tax', + 'amount_type': 'percent', + 'amount': 10, + 'price_include': False, + 'include_base_amount': False, + 'type_tax_use': 'sale', + }) + cls.tax_15 = cls.env['account.tax'].create({ + 'name': '15% Tax', + 'amount_type': 'percent', + 'amount': 15, + 'price_include': False, + 'include_base_amount': False, + 'type_tax_use': 'sale', + }) + cls.fiscal_country = cls.env['res.country'].create({ + 'name': "Super Fiscal Position", + 'code': 'SFP', + }) + cls.product = cls.env['product.product'].create({ + 'name': 'Super Product', + 'list_price': 100.0, + }) + def test_website_sale_contextual_price(self): contextual_price = self.computer._get_contextual_price() self.assertEqual( @@ -78,3 +120,107 @@ class WebsiteSaleProductTests(TestSaleProductAttributeValueCommon): pricelist.discount_policy = 'without_discount' res = product_tmpl._get_sales_prices(pricelist, self.env['account.fiscal.position']) self.assertEqual(res[product_tmpl.id]['base_price'], 75) + + def test_get_contextual_price_tax_selection(self): + """ + `_get_contextual_price_tax_selection` is used to display the price on the website (e.g. in the carousel). + We test that the contextual price is correctly computed. That is, it is coherent with the price displayed on the product when in the cart. + """ + param_main_product_tax_included = [True, False] + param_show_line_subtotals_tax_selection = ['tax_included', 'tax_excluded'] + param_extra_tax = [False, 'included', 'excluded'] + param_fpos = [False, 'to_tax_excluded', 'to_tax_included'] + parameters = itertools.product(param_main_product_tax_included, param_show_line_subtotals_tax_selection, param_extra_tax, param_fpos) + + self.product.taxes_id = self.tax_15 + fiscal_position = self.env['account.fiscal.position'].create({ + 'name': 'Super Fiscal Position', + 'auto_apply': True, + 'country_id': self.fiscal_country.id, + 'tax_ids': [ + Command.create({ + 'tax_src_id': self.tax_15.id, + 'tax_dest_id': self.tax_10.id, + }) + ] + }) + self.env.user.partner_id.country_id = self.fiscal_country + + self.WebsiteSaleController = WebsiteSale() + for main_product_tax_included, show_line_subtotals_tax_selection, extra_tax, fpos in parameters: + with self.subTest(main_product_tax_included=main_product_tax_included, show_line_subtotals_tax_selection=show_line_subtotals_tax_selection, extra_tax=extra_tax, fpos=fpos): + # set "show_line_subtotals_tax_selection" parameter + self.website.invalidate_recordset(['fiscal_position_id'], flush=False) + config = self.env['res.config.settings'].create({}) + config.show_line_subtotals_tax_selection = show_line_subtotals_tax_selection + config.execute() + + self.assertEqual(self.website.show_line_subtotals_tax_selection, show_line_subtotals_tax_selection) + + tax_ids = [self.tax_15.id] + # set "main_product_tax_included" parameter + if main_product_tax_included: + self.tax_15.price_include = True + self.tax_15.include_base_amount = True + else: + self.tax_15.price_include = False + self.tax_15.include_base_amount = False + + # set "extra_tax" parameter + if extra_tax: + if extra_tax == 'included': + self.tax_5.price_include = True + self.tax_5.include_base_amount = True + else: + self.tax_5.price_include = False + self.tax_5.include_base_amount = False + + tax_ids.append(self.tax_5.id) + + self.product.taxes_id = tax_ids + + # set "fpos" parameter + if fpos: + if fpos == 'to_tax_included': + self.tax_10.price_include = True + self.tax_10.include_base_amount = True + else: + self.tax_10.price_include = False + self.tax_10.include_base_amount = False + + fiscal_position.action_unarchive() + else: + fiscal_position.action_archive() + + with MockRequest(self.env, website=self.website): + self.assertEqual( + self.website.fiscal_position_id, + fpos and fiscal_position or self.env['account.fiscal.position'] + ) + contextual_price = self.product.with_context( + website_id=self.website.id, + )._get_contextual_price_tax_selection() + self.WebsiteSaleController.cart_update(product_id=self.product.id, add_qty=1) + sale_order = self.website.sale_get_order() + + self.assertEqual(sale_order.website_id, self.website) + self.assertEqual(sale_order.company_id, self.website.company_id) + self.assertEqual(sale_order.currency_id, self.website.currency_id) + self.assertFalse(sale_order.pricelist_id) + if fpos: + self.assertEqual(sale_order.fiscal_position_id, fiscal_position) + else: + self.assertFalse(sale_order.fiscal_position_id) + + sol = sale_order.order_line + if fpos: + self.assertEqual(sol.tax_id, fiscal_position.map_tax(self.product.taxes_id)) + else: + self.assertEqual(sol.tax_id, self.product.taxes_id) + + if show_line_subtotals_tax_selection == 'tax_included': + self.assertAlmostEqual(sol.price_reduce_taxinc, contextual_price) + self.assertAlmostEqual(sale_order.amount_total, contextual_price) + else: + self.assertAlmostEqual(sol.price_reduce_taxexcl, contextual_price) + self.assertAlmostEqual(sale_order.amount_untaxed, contextual_price)