From 1fc6f4261b909066d180db3eb99f03526469cc79 Mon Sep 17 00:00:00 2001 From: "Andrea Grazioso (agr-odoo)" Date: Mon, 31 Jan 2022 18:43:49 +0000 Subject: [PATCH] [FIX] account,sale: fiscal position map tax incl. to incl. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create a 21% tax included in price Create a 6% tax included in price Create a fiscal position that will replace the 21% tax included by the 6% tax included Create a customer and assign this fiscal position Create a product : Price = 121€ Tax = 21% tax included Create a SO with this customer and this product Create an invoice (from scratch) with this customer and this product SO : the unit price is 100,00€ Invoice : the unit price is 106,00€ Fix by creating a common method to calculate the price_unit opw-2699793 closes odoo/odoo#86514 X-original-commit: 8ab56af6827a640a3f41a87905778432b3786c00 Signed-off-by: Laurent Smet Signed-off-by: Grazioso Andrea (agr) --- addons/account/models/account_move.py | 68 ++++------------------ addons/account/models/product.py | 73 ++++++++++++++++++++++++ addons/sale/models/sale_order_line.py | 12 +++- addons/sale/tests/test_sale_pricelist.py | 58 ++++++++++++++++++- 4 files changed, 151 insertions(+), 60 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 696c41a8ea0..d21f307f8ec 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -3821,67 +3821,21 @@ class AccountMoveLine(models.Model): if not self.product_id: return 0.0 - - company = self.move_id.company_id - currency = self.move_id.currency_id - company_currency = company.currency_id - product_uom = self.product_id.uom_id - fiscal_position = self.move_id.fiscal_position_id - is_refund_document = self.move_id.move_type in ('out_refund', 'in_refund') - move_date = self.move_id.date or fields.Date.context_today(self) - if self.move_id.is_sale_document(include_receipts=True): - product_price_unit = self.product_id.lst_price - product_taxes = self.product_id.taxes_id + document_type = 'sale' elif self.move_id.is_purchase_document(include_receipts=True): - product_price_unit = self.product_id.standard_price - product_taxes = self.product_id.supplier_taxes_id + document_type = 'purchase' else: - return 0.0 - product_taxes = product_taxes.filtered(lambda tax: tax.company_id == company) + document_type = 'other' - # Apply unit of measure. - if self.product_uom_id and self.product_uom_id != product_uom: - product_price_unit = product_uom._compute_price(product_price_unit, self.product_uom_id) - - # Apply fiscal position. - if product_taxes and fiscal_position: - product_taxes_after_fp = fiscal_position.map_tax(product_taxes) - - if set(product_taxes.ids) != set(product_taxes_after_fp.ids): - flattened_taxes_before_fp = product_taxes._origin.flatten_taxes_hierarchy() - if any(tax.price_include for tax in flattened_taxes_before_fp): - taxes_res = flattened_taxes_before_fp.compute_all( - product_price_unit, - quantity=1.0, - currency=company_currency, - product=self.product_id, - partner=self.partner_id, - is_refund=is_refund_document, - ) - product_price_unit = company_currency.round(taxes_res['total_excluded']) - - flattened_taxes_after_fp = product_taxes_after_fp._origin.flatten_taxes_hierarchy() - if any(tax.price_include for tax in flattened_taxes_after_fp): - taxes_res = flattened_taxes_after_fp.compute_all( - product_price_unit, - quantity=1.0, - currency=company_currency, - product=self.product_id, - partner=self.partner_id, - is_refund=is_refund_document, - handle_price_include=False, - ) - for tax_res in taxes_res['taxes']: - tax = self.env['account.tax'].browse(tax_res['id']) - if tax.price_include: - product_price_unit += tax_res['amount'] - - # Apply currency rate. - if currency and currency != company_currency: - product_price_unit = company_currency._convert(product_price_unit, currency, company, move_date) - - return product_price_unit + return self.product_id._get_tax_included_unit_price( + self.move_id.company_id, + self.move_id.currency_id, + self.move_id.date, + document_type, + fiscal_position=self.move_id.fiscal_position_id, + product_uom=self.product_uom_id + ) def _get_computed_account(self): self.ensure_one() diff --git a/addons/account/models/product.py b/addons/account/models/product.py index 7fa7b8cb4b6..e868d989023 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -83,3 +83,76 @@ class ProductProduct(models.Model): def _get_product_accounts(self): return self.product_tmpl_id._get_product_accounts() + + @api.model + def _get_tax_included_unit_price(self, company, currency, document_date, document_type, + is_refund_document=False, product_uom=None, product_currency=None, + product_price_unit=None, product_taxes=None, fiscal_position=None + ): + """ Helper to get the price unit from different models. + This is needed to compute the same unit price in different models (sale order, account move, etc.) with same parameters. + """ + + product = self + + assert document_type + + if product_uom is None: + product_uom = product.uom_id + if not product_currency: + if document_type == 'sale': + product_currency = product.currency_id + elif document_type == 'purchase': + product_currency = company.currency_id + if product_price_unit is None: + if document_type == 'sale': + product_price_unit = product.with_company(company).lst_price + elif document_type == 'purchase': + product_price_unit = product.with_company(company).standard_price + else: + return 0.0 + if product_taxes is None: + if document_type == 'sale': + product_taxes = product.taxes_id.filtered(lambda x: x.company_id == company) + elif document_type == 'purchase': + product_taxes = product.supplier_taxes_id.filtered(lambda x: x.company_id == company) + # Apply unit of measure. + if product_uom and product.uom_id != product_uom: + product_price_unit = product.uom_id._compute_price(product_price_unit, product_uom) + + # Apply fiscal position. + if product_taxes and fiscal_position: + product_taxes_after_fp = fiscal_position.map_tax(product_taxes) + + if set(product_taxes.ids) != set(product_taxes_after_fp.ids): + flattened_taxes_before_fp = product_taxes._origin.flatten_taxes_hierarchy() + if any(tax.price_include for tax in flattened_taxes_before_fp): + taxes_res = flattened_taxes_before_fp.compute_all( + product_price_unit, + quantity=1.0, + currency=currency, + product=product, + is_refund=is_refund_document, + ) + product_price_unit = taxes_res['total_excluded'] + + flattened_taxes_after_fp = product_taxes_after_fp._origin.flatten_taxes_hierarchy() + if any(tax.price_include for tax in flattened_taxes_after_fp): + taxes_res = flattened_taxes_after_fp.compute_all( + product_price_unit, + quantity=1.0, + currency=currency, + product=product, + is_refund=is_refund_document, + handle_price_include=False, + ) + for tax_res in taxes_res['taxes']: + tax = self.env['account.tax'].browse(tax_res['id']) + if tax.price_include: + product_price_unit += tax_res['amount'] + + # Apply currency rate. + if currency != product_currency: + product_price_unit = product_currency._convert(product_price_unit, currency, company, document_date) + + return product_price_unit diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 7258b81f147..e7abd356139 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -410,8 +410,16 @@ class SaleOrderLine(models.Model): line.price_unit = 0.0 else: price = line._get_display_price() - line.price_unit = self.env['account.tax']._fix_tax_included_price_company( - price, line.product_id.taxes_id, line.tax_id, line.company_id) + line.price_unit = line.product_id._get_tax_included_unit_price( + line.company_id, + line.order_id.currency_id, + line.order_id.date_order, + 'sale', + fiscal_position=line.order_id.fiscal_position_id, + product_price_unit=price, + product_currency=line.currency_id + ) + def _get_display_price(self): self.ensure_one() diff --git a/addons/sale/tests/test_sale_pricelist.py b/addons/sale/tests/test_sale_pricelist.py index eb7950083c2..e749356c5a7 100644 --- a/addons/sale/tests/test_sale_pricelist.py +++ b/addons/sale/tests/test_sale_pricelist.py @@ -3,7 +3,7 @@ from .common import TestSaleCommon from odoo.fields import Command -from odoo.tests import tagged +from odoo.tests import tagged, Form @tagged('post_install', '-at_install') @@ -148,6 +148,62 @@ class TestSaleOrder(TestSaleCommon): self.assertEqual(line.discount, 0.0, 'Pricelist of SO should not be applied on an order line') self.assertEqual(line.price_unit, line.product_id.list_price, 'Unit price of order line should be a sale price as the pricelist not applied on the other category\'s product') + def test_fiscalposition_application(self): + """Test application of a fiscal position mapping + price included to price included tax + """ + + uom = self.env['uom.uom'].search([('name', '=', 'Units')]) + pricelist = self.env['product.pricelist'].search([('name', '=', 'Public Pricelist')]) + + partner = self.env['res.partner'].create({ + 'name': "George" + }) + tax_include_src = self.env['account.tax'].create({ + 'name': "Include tax", + 'amount': 21.00, + 'price_include': True, + }) + tax_include_dst = self.env['account.tax'].create({ + 'name': "Exclude tax", + 'amount': 6.00, + 'price_include': True, + }) + + product_tmpl = self.env['product.template'].create({ + 'name': "Voiture", + 'list_price': 121, + 'taxes_id': [(6, 0, [tax_include_src.id])] + }) + + product_product = product_tmpl.product_variant_id + + fpos = self.env['account.fiscal.position'].create({ + 'name': "fiscal position", + 'sequence': 1 + }) + + self.env['account.fiscal.position.tax'].create({ + 'position_id' :fpos.id, + 'tax_src_id': tax_include_src.id, + 'tax_dest_id': tax_include_dst.id + }) + + # Create the SO with one SO line and apply a pricelist and fiscal position on it + order_form = Form(self.env['sale.order'].with_context(tracking_disable=True)) + order_form.partner_id = partner + order_form.pricelist_id = pricelist + order_form.fiscal_position_id = fpos + with order_form.order_line.new() as line: + line.name = product_product.name + line.product_id = product_product + line.product_uom_qty = 1.0 + line.product_uom = uom + sale_order = order_form.save() + + # Check the unit price of SO line + self.assertRecordValues(sale_order.order_line, [{'price_unit': 106}]) + def test_pricelist_application(self): """ Test different prices are correctly applied based on dates """ support_product = self.env['product.product'].create({