From f4c669d0e412ba594fb77cc907f41da3f52f8f07 Mon Sep 17 00:00:00 2001 From: Brice bib Bartoletti Date: Thu, 3 Mar 2022 13:53:33 +0000 Subject: [PATCH] [FIX] account: price at 0, fixed tax price include The aim of this commit is to correct the compute_all calculation when it is dealing with an article with 0.00 price and having a fixed tax included in it. In this situation, this means the tax is directly taken as an expense by the company that offers it to its client. This also solves the situation in which we have a 100% discount because discount is applied before calling `compute_all` by changing the unit_price. Before this commit: Creating an invoice with a line having a price at 0 or a 100% discount and a fixed tax price_include resulted in a really weird behavior: the company had to reimburse the customer for the amount of the tax. (what a lucky customer :D ) After this commit: Customer is sad, he only gets the product for free. The base is calculated as negative, resulting in a debit amount on the base income line. The tax is then counterbalance by it. The form is displaying a negative untaxed amount, a positive tax and the total is 0. closes odoo/odoo#86223 Ticket: 2660808, 2637512 Community-pr: https://github.com/odoo/odoo/pull/85747 X-original-commit: 727d19cb4e012e9a24d94640987ac5ea6f452a2e Signed-off-by: Laurent Smet Signed-off-by: Brice Bartoletti --- addons/account/models/account_move.py | 3 +- addons/account/models/account_tax.py | 4 +- .../tests/test_account_move_out_invoice.py | 69 +++++++++++++++++++ addons/sale/tests/test_sale_order.py | 40 +++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 3234ccba89e..a612544e302 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -4023,8 +4023,7 @@ class AccountMoveLine(models.Model): # Compute 'price_total'. if taxes: - force_sign = -1 if move_type in ('out_invoice', 'in_refund', 'out_receipt') else 1 - taxes_res = taxes._origin.with_context(force_sign=force_sign).compute_all(line_discount_price_unit, + taxes_res = taxes._origin.with_context(force_sign=1).compute_all(line_discount_price_unit, quantity=quantity, currency=currency, product=product, partner=partner, is_refund=move_type in ('out_refund', 'in_refund')) res['price_subtotal'] = taxes_res['total_excluded'] res['price_total'] = taxes_res['total_included'] diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index 62788dc1142..b89db2fb099 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -582,7 +582,7 @@ class AccountTax(models.Model): sum_repartition_factor = sum(tax_repartition_lines.mapped('factor')) #compute the tax_amount - if not skip_checkpoint and price_include and total_included_checkpoints.get(i) and sum_repartition_factor != 0: + if not skip_checkpoint and price_include and total_included_checkpoints.get(i) is not None and sum_repartition_factor != 0: # We know the total to reach for that tax, so we make a substraction to avoid any rounding issues tax_amount = total_included_checkpoints[i] - (base + cumulated_tax_included_amount) cumulated_tax_included_amount = 0 @@ -594,7 +594,7 @@ class AccountTax(models.Model): tax_amount = round(tax_amount, precision_rounding=prec) factorized_tax_amount = round(tax_amount * sum_repartition_factor, precision_rounding=prec) - if price_include and not total_included_checkpoints.get(i): + if price_include and total_included_checkpoints.get(i) is None: cumulated_tax_included_amount += factorized_tax_amount # If the tax affects the base of subsequent taxes, its tax move lines must diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index 936800ac736..f8d797a91bd 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -1792,6 +1792,75 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): 'amount_total': 260.01, }) + def test_out_invoice_line_tax_fixed_price_include_free_product(self): + ''' Check that fixed tax include are correctly computed while the price_unit is 0 + ''' + fixed_tax_price_include = self.env['account.tax'].create({ + 'name': 'BEBAT 0.05', + 'type_tax_use': 'sale', + 'amount_type': 'fixed', + 'amount': 0.05, + 'price_include': True, + 'include_base_amount': True, + }) + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_date': '2022-03-03', + 'date': '2022-03-03', + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [(0, 0, { + 'name': 'Free product', + 'price_unit': 0.0, + 'account_id': self.company_data['default_account_revenue'].id, + 'tax_ids': [(6, 0, fixed_tax_price_include.ids)], + })], + }) + self.assertRecordValues(invoice, [{ + 'amount_untaxed': -0.05, + 'amount_tax': 0.05, + 'amount_total': 0.0, + }]) + + def test_out_invoice_line_taxes_fixed_price_include_free_product(self): + ''' Check that fixed tax include are correctly computed while the price_unit is 0 + ''' + # please ensure this test remains consistent with + # test_free_product_and_price_include_fixed_tax in the sale module + fixed_tax_price_include_1 = self.env['account.tax'].create({ + 'name': 'BEBAT 0.05', + 'type_tax_use': 'sale', + 'amount_type': 'fixed', + 'amount': 0.05, + 'price_include': True, + 'include_base_amount': True, + }) + fixed_tax_price_include_2 = self.env['account.tax'].create({ + 'name': 'Recupel 0.25', + 'type_tax_use': 'sale', + 'amount_type': 'fixed', + 'amount': 0.25, + 'price_include': True, + 'include_base_amount': True, + }) + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_date': '2022-03-03', + 'date': '2022-03-03', + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [(0, 0, { + 'name': 'Free product', + 'price_unit': 0.0, + 'account_id': self.company_data['default_account_revenue'].id, + 'tax_ids': [(6, 0, (fixed_tax_price_include_1 + fixed_tax_price_include_2).ids)], + })], + }) + + self.assertRecordValues(invoice, [{ + 'amount_untaxed': -0.30, + 'amount_tax': 0.30, + 'amount_total': 0.0, + }]) + def test_out_invoice_create_refund(self): self.invoice.action_post() diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 9cc0b31fc67..08bd2106be5 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -767,6 +767,46 @@ class TestSaleOrder(TestSaleCommon): self.assertEqual(line.price_subtotal, 17527.41) self.assertEqual(line.untaxed_amount_to_invoice, line.price_subtotal) + def test_free_product_and_price_include_fixed_tax(self): + """ Check that fixed tax include are correctly computed while the price_unit is 0 + """ + # please ensure this test remains consistent with + # test_out_invoice_line_onchange_2_taxes_fixed_price_include_free_product in account module + sale_order = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'order_line': [(0, 0, { + 'product_id': self.company_data['product_order_no'].id, + 'product_uom_qty': 1, + 'price_unit': 0.0, + })] + }) + sale_order.action_confirm() + line = sale_order.order_line + line.tax_id = [ + (0, 0, { + 'name': 'BEBAT 0.05', + 'type_tax_use': 'sale', + 'amount_type': 'fixed', + 'amount': 0.05, + 'price_include': True, + 'include_base_amount': True, + }), + (0, 0, { + 'name': 'Recupel 0.25', + 'type_tax_use': 'sale', + 'amount_type': 'fixed', + 'amount': 0.25, + 'price_include': True, + 'include_base_amount': True, + }), + ] + sale_order.action_confirm() + self.assertRecordValues(sale_order, [{ + 'amount_untaxed': -0.30, + 'amount_tax': 0.30, + 'amount_total': 0.0, + }]) + def test_so_tax_mapping(self): uom = self.env['uom.uom'].search([('name', '=', 'Units')], limit=1) pricelist = self.env['product.pricelist'].search([('name', '=', 'Public Pricelist')], limit=1)