From 0aeeb5374e72f115bbf3e3b1d32d694375625ca9 Mon Sep 17 00:00:00 2001 From: oco-odoo Date: Tue, 30 May 2023 10:13:27 +0000 Subject: [PATCH] [FIX] account: computation of tax_tag_invert Two issues solved in 1 commit; as their solutions depend on one another. ** ISSUE 1 ** To reproduce the issue, on a l10n with a tax report: - Create a misc operation with a tax on it in the same form as a sales invoice ; post it. - Reverse that move, and post the reverse. => Open the tax report: the amounts of the original move (for both tax lines and base lines) are doubled. That's not what we want ; instead, the reverse should have entirely canceled the original move. This is due to the fact the is_refund field of account.move.line is badly computed on the reverse: both tax and base lines are considered refund. Because of that, tax_tag_invert gets inverted, and ends up doubling the amount in the report instead of cancelling it. While made on a reverse move, those lines should not be considered as refunds, since they must both use the original repartition of the reversed move , unlike invoices, which would make use of the refund repartition in this case. ** ISSUE 2 ** To reproduce, on a l10n with a tax report: - Create a cash basis tax, and set tags on its repartition lines so that it should be taken into account by the report. Make sure the refund repartition cancels the invoice repartition. - Create an invoice using that tax, post it - Click the "Add Credit Note" button, select "partial refund", and post the generated refund (which will cover the full amount of the original invoice) - Reconcile the refund with the invoice => Because it's a partial refund, cash basis entries will still be generated. Open the tax report to ensure they sum up to 0... And the tax lines don't ! This happens because the computation of tax_tag_invert field inverts the sign of tag on cash basis entry tax lines when they have a negative tax_base_amount. In our case, when going through the "reverse" button and making a partial refund, we actually do get a negative amount in the credit note's tax_base_amount. This is inconsistent with what a refund generated from scratch does (then, you'll get a positive tax_base_amount in our case), and should only be legit when the document contains a negative line explicitly (so, a line with a negative quantity, hence inverting the meaning of debit/credit). The root of the issue is that the tax_base_amount of the tax line uses the tax_tag_invert of the base line to define its sign. And tax_tag_invert was omitting to set copy=False. When reversing a move, the first step is to copy it. Because of the missing copy=False, tax_tag_invert was copied, and never recomputed (since it's a computed editable field) on the base line, giving it an inconsistent value, and ending up inverting the sign of tax_base_amount on the tax line. In turn, the tax line got a wrong tax_tag_invert because of that, and would propagate that to the cash basis entry when generating it. This commit adds the missing copy=False to tax_tag_invert, but also fixes the computation of tax_tag_invert so that in does not rely on tax_base_amount anymore. This way, existing data will also benefit from the fix without having to recompute the field. Additionnally, there are currently plans to remove tax_base_amount in master, so this would have had to be done eventually anyway. ====== Forward-port note: In 16.2, https://github.com/odoo/odoo/commit/84d7aab94e2984a81923f0546e24caf54cd53033 actually wrongly inverted the sign of the Mexican withholding tag in repartition lines. Those taxes are negative, so a tag needing to get the retention amount in positive must be negative itself. The bugs fixed in this commit shadowed the problem, and this fix reveals it. A fix of the tags's signs was added into this forward-port. OPW 3255511 closes odoo/odoo#123187 X-original-commit: 688ab4d6688ccd2377ea9702c00b1ec16f99527a Related: odoo/enterprise#41748 Signed-off-by: Quentin De Paoli Signed-off-by: Olivier Colson (oco) --- addons/account/models/account_move.py | 2 +- addons/account/models/account_move_line.py | 26 ++++----- .../account/tests/test_account_move_entry.py | 58 +++++++++++++++++++ .../tests/test_account_move_in_invoice.py | 20 +++++++ .../tests/test_account_move_out_invoice.py | 20 +++++++ .../l10n_mx/data/template/account.tax-mx.csv | 20 +++---- 6 files changed, 122 insertions(+), 24 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index edf3af81b72..f2c3320ed0b 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -3378,7 +3378,7 @@ class AccountMove(models.Model): reverse_moves += move.with_context( move_reverse_cancel=cancel, include_business_fields=True, - skip_invoice_sync=bool(move.tax_cash_basis_origin_move_id), + skip_invoice_sync=move.move_type == 'entry', ).copy(default_values) reverse_moves.with_context(skip_invoice_sync=cancel).write({'line_ids': [ diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index 8bc81fdad07..5507e80dc68 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -213,7 +213,7 @@ class AccountMoveLine(models.Model): # inverted when computing its total for each tag (for sales invoices, for # example) tax_tag_invert = fields.Boolean( string="Invert Tags", - compute='_compute_tax_tag_invert', store=True, readonly=False, + compute='_compute_tax_tag_invert', store=True, readonly=False, copy=False, ) # === Reconciliation fields === # @@ -767,12 +767,9 @@ class AccountMoveLine(models.Model): is_refund = record.is_refund tax_type = tax.type_tax_use record.tax_tag_invert = (tax_type == 'purchase' and is_refund) or (tax_type == 'sale' and not is_refund) - else: # For invoices with taxes record.tax_tag_invert = record.move_id.is_inbound() - if record.move_id.tax_cash_basis_origin_move_id and (record.move_id.reversed_entry_id or record.tax_base_amount < 0): - record.tax_tag_invert = not record.tax_tag_invert @api.depends('product_id') def _compute_product_uom_id(self): @@ -1016,21 +1013,24 @@ class AccountMoveLine(models.Model): epd_needed_vals['price_subtotal'] += line.price_subtotal * percentage line.epd_needed = {k: frozendict(v) for k, v in epd_needed.items()} - @api.depends('move_id.move_type', 'balance', 'tax_ids') + @api.depends('move_id.move_type', 'balance', 'tax_repartition_line_id', 'tax_ids') def _compute_is_refund(self): for line in self: is_refund = False if line.move_id.move_type in ('out_refund', 'in_refund'): is_refund = True elif line.move_id.move_type == 'entry': - taxes = line.tax_repartition_line_id.tax_id or line.tax_ids[:1] - tax_type = set(taxes.mapped('type_tax_use')) - if tax_type == {'sale'} and line.credit == 0: - is_refund = True - elif tax_type == {'purchase'} and line.debit == 0: - is_refund = True - if line.tax_repartition_line_id.factor_percent < 0: - is_refund = not is_refund + if line.tax_repartition_line_id: + is_refund = line.tax_repartition_line_id.document_type == 'refund' + else: + tax_type = line.tax_ids[:1].type_tax_use + if tax_type == 'sale' and line.credit == 0: + is_refund = True + elif tax_type == 'purchase' and line.debit == 0: + is_refund = True + + if line.tax_ids and line.move_id.reversed_entry_id: + is_refund = not is_refund line.is_refund = is_refund @api.depends('date_maturity') diff --git a/addons/account/tests/test_account_move_entry.py b/addons/account/tests/test_account_move_entry.py index d90f9c41626..91b33952db9 100644 --- a/addons/account/tests/test_account_move_entry.py +++ b/addons/account/tests/test_account_move_entry.py @@ -964,3 +964,61 @@ class TestAccountMove(AccountTestInvoicingCommon): {'name': 'CABA', 'debit': 0.0, 'credit': 20.0, 'account_id': tax_account.id, 'tax_ids': [], 'tax_line_id': caba_tax.id}, {'name': 'credit', 'debit': 0.0, 'credit': 100.0, 'account_id': income_account.id, 'tax_ids': caba_tax.ids, 'tax_line_id': False}, ]) + + def test_misc_with_taxes_reverse(self): + test_account = self.company_data['default_account_revenue'] + + # With a sale tax + sale_tax = self.company_data['default_tax_sale'] + + move_form = Form(self.env['account.move']) + + with move_form.line_ids.new() as debit_line_form: + debit_line_form.name = 'debit' + debit_line_form.account_id = test_account + debit_line_form.debit = 115 + + with move_form.line_ids.new() as credit_line_form: + credit_line_form.name = 'credit' + credit_line_form.account_id = test_account + credit_line_form.credit = 100 + credit_line_form.tax_ids.clear() + credit_line_form.tax_ids.add(sale_tax) + + sale_move = move_form.save() + + sale_invoice_rep_line = sale_tax.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax') + + self.assertRecordValues(sale_move.line_ids.sorted(lambda x: -x.balance), [ + # pylint: disable=C0326 + {'name': 'debit', 'debit': 115.0, 'credit': 0.0, 'account_id': test_account.id, 'tax_ids': [], 'tax_base_amount': 0, 'tax_tag_invert': False, 'tax_repartition_line_id': False}, + {'name': '15%', 'debit': 0.0, 'credit': 15.0, 'account_id': self.company_data['default_account_tax_sale'].id, 'tax_ids': [], 'tax_base_amount': 100, 'tax_tag_invert': True, 'tax_repartition_line_id': sale_invoice_rep_line.id}, + {'name': 'credit', 'debit': 0.0, 'credit': 100.0, 'account_id': test_account.id, 'tax_ids': sale_tax.ids, 'tax_base_amount': 0, 'tax_tag_invert': True, 'tax_repartition_line_id': False}, + ]) + + # Same with a purchase tax + purchase_tax = self.company_data['default_tax_purchase'] + + move_form = Form(self.env['account.move']) + + with move_form.line_ids.new() as credit_line_form: + credit_line_form.name = 'credit' + credit_line_form.account_id = test_account + credit_line_form.credit = 115 + + with move_form.line_ids.new() as debit_line_form: + debit_line_form.name = 'debit' + debit_line_form.account_id = test_account + debit_line_form.debit = 100 + debit_line_form.tax_ids.clear() + debit_line_form.tax_ids.add(purchase_tax) + + purchase_move = move_form.save() + + purchase_invoice_rep_line = purchase_tax.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax') + self.assertRecordValues(purchase_move.line_ids.sorted(lambda x: x.balance), [ + # pylint: disable=C0326 + {'name': 'credit', 'credit': 115.0, 'debit': 0.0, 'account_id': test_account.id, 'tax_ids': [], 'tax_base_amount': 0, 'tax_tag_invert': False, 'tax_repartition_line_id': False}, + {'name': '15%', 'credit': 0.0, 'debit': 15.0, 'account_id': self.company_data['default_account_tax_purchase'].id, 'tax_ids': [], 'tax_base_amount': 100, 'tax_tag_invert': False, 'tax_repartition_line_id': purchase_invoice_rep_line.id}, + {'name': 'debit', 'credit': 0.0, 'debit': 100.0, 'account_id': test_account.id, 'tax_ids': purchase_tax.ids, 'tax_base_amount': 0, 'tax_tag_invert': False, 'tax_repartition_line_id': False}, + ]) diff --git a/addons/account/tests/test_account_move_in_invoice.py b/addons/account/tests/test_account_move_in_invoice.py index d7ee2c60633..29fb1910a5c 100644 --- a/addons/account/tests/test_account_move_in_invoice.py +++ b/addons/account/tests/test_account_move_in_invoice.py @@ -1128,24 +1128,32 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): 'amount_currency': -800.0, 'debit': 0.0, 'credit': 800.0, + 'tax_tag_invert': True, + 'tax_base_amount': 0.0, }, { **self.product_line_vals_2, 'amount_currency': -160.0, 'debit': 0.0, 'credit': 160.0, + 'tax_tag_invert': True, + 'tax_base_amount': 0.0, }, { **self.tax_line_vals_1, 'amount_currency': -144.0, 'debit': 0.0, 'credit': 144.0, + 'tax_tag_invert': True, + 'tax_base_amount': 960.0, }, { **self.tax_line_vals_2, 'amount_currency': -24.0, 'debit': 0.0, 'credit': 24.0, + 'tax_tag_invert': True, + 'tax_base_amount': 160.0, }, { **self.term_line_vals_1, @@ -1154,6 +1162,8 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): 'debit': 1128.0, 'credit': 0.0, 'date_maturity': move_reversal.date, + 'tax_tag_invert': False, + 'tax_base_amount': 0.0, }, ], { **self.move_vals, @@ -1179,24 +1189,32 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): 'amount_currency': 800.0, 'debit': 800.0, 'credit': 0.0, + 'tax_tag_invert': False, + 'tax_base_amount': 0, }, { **self.product_line_vals_2, 'amount_currency': 160.0, 'debit': 160.0, 'credit': 0.0, + 'tax_tag_invert': False, + 'tax_base_amount': 0, }, { **self.tax_line_vals_1, 'amount_currency': 144.0, 'debit': 144.0, 'credit': 0.0, + 'tax_tag_invert': False, + 'tax_base_amount': 960.0, }, { **self.tax_line_vals_2, 'amount_currency': 24.0, 'debit': 24.0, 'credit': 0.0, + 'tax_tag_invert': False, + 'tax_base_amount': 160.0, }, { **self.term_line_vals_1, @@ -1205,6 +1223,8 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): 'debit': 0.0, 'credit': 1128.0, 'date_maturity': move_reversal.date, + 'tax_tag_invert': False, + 'tax_base_amount': 0, }, ], { **self.move_vals, diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index bb1a02e39f6..3d49b12fb75 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -1744,24 +1744,32 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): 'amount_currency': 1000.0, 'debit': 1000.0, 'credit': 0.0, + 'tax_tag_invert': False, + 'tax_base_amount': 0.0, }, { **self.product_line_vals_2, 'amount_currency': 200.0, 'debit': 200.0, 'credit': 0.0, + 'tax_tag_invert': False, + 'tax_base_amount': 0.0, }, { **self.tax_line_vals_1, 'amount_currency': 180.0, 'debit': 180.0, 'credit': 0.0, + 'tax_tag_invert': False, + 'tax_base_amount': 1200.0, }, { **self.tax_line_vals_2, 'amount_currency': 30.0, 'debit': 30.0, 'credit': 0.0, + 'tax_tag_invert': False, + 'tax_base_amount': 200.0, }, { **self.term_line_vals_1, @@ -1770,6 +1778,8 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): 'debit': 0.0, 'credit': 1410.0, 'date_maturity': move_reversal.date, + 'tax_tag_invert': False, + 'tax_base_amount': 0.0, }, ], { **self.move_vals, @@ -1796,24 +1806,32 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): 'amount_currency': -1000.0, 'debit': 0.0, 'credit': 1000.0, + 'tax_tag_invert': True, + 'tax_base_amount': 0.0, }, { **self.product_line_vals_2, 'amount_currency': -200.0, 'debit': 0.0, 'credit': 200.0, + 'tax_tag_invert': True, + 'tax_base_amount': 0.0, }, { **self.tax_line_vals_1, 'amount_currency': -180.0, 'debit': 0.0, 'credit': 180.0, + 'tax_tag_invert': True, + 'tax_base_amount': 1200.0, }, { **self.tax_line_vals_2, 'amount_currency': -30.0, 'debit': 0.0, 'credit': 30.0, + 'tax_tag_invert': True, + 'tax_base_amount': 200.0, }, { **self.term_line_vals_1, @@ -1822,6 +1840,8 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): 'debit': 1410.0, 'credit': 0.0, 'date_maturity': move_reversal.date, + 'tax_tag_invert': False, + 'tax_base_amount': 0.0, }, ], { **self.move_vals, diff --git a/addons/l10n_mx/data/template/account.tax-mx.csv b/addons/l10n_mx/data/template/account.tax-mx.csv index 2e5fffd87c4..2719d1f54fe 100644 --- a/addons/l10n_mx/data/template/account.tax-mx.csv +++ b/addons/l10n_mx/data/template/account.tax-mx.csv @@ -8,17 +8,17 @@ "","","","","","","","","","","","","","base","refund","","","" "","","","","","","","","","","","","","tax","refund","cuenta208_01","l10n_mx.tag_iva","" "tax1","10","4% WH","VAT withholding","VAT withholding(-4%)","-4.0","percent","purchase","tax_group_iva_ret_4","on_payment","cuenta216_10","Tasa","","base","invoice","","","Retención del IVA" -"","","","","","","","","","","","","","tax","invoice","cuenta216_10_20","+DIOT: Retención","" +"","","","","","","","","","","","","","tax","invoice","cuenta216_10_20","-DIOT: Retención","" "","","","","","","","","","","","","","base","refund","","","" -"","","","","","","","","","","","","","tax","refund","cuenta216_10_20","-DIOT: Retención","" +"","","","","","","","","","","","","","tax","refund","cuenta216_10_20","+DIOT: Retención","" "tax18","20","1.25% WH","VAT witholding RESICO 1.25%","Retención IVA(-1.25%)","-1.25","percent","purchase","tax_group_iva_ret_1_25","on_payment","cuenta216_10","Tasa","0","base","invoice","","","Retención de IVA RESICO 1.25%" -"","","","","","","","","","","","","","tax","invoice","cuenta216_10_20","+DIOT: Retención","" +"","","","","","","","","","","","","","tax","invoice","cuenta216_10_20","-DIOT: Retención","" "","","","","","","","","","","","","","base","refund","","","" -"","","","","","","","","","","","","","tax","refund","cuenta216_10_20","-DIOT: Retención","" +"","","","","","","","","","","","","","tax","refund","cuenta216_10_20","+DIOT: Retención","" "tax2","10","10% WH L","VAT withholding lease 10%","VAT withholding(-10%)","-10.0","percent","purchase","tax_group_iva_ret_10","on_payment","cuenta216_10","Tasa","","base","invoice","","","Retención IVA arrendamiento 10%" -"","","","","","","","","","","","","","tax","invoice","cuenta216_10_20","+DIOT: Retención","" +"","","","","","","","","","","","","","tax","invoice","cuenta216_10_20","-DIOT: Retención","" "","","","","","","","","","","","","","base","refund","","","" -"","","","","","","","","","","","","","tax","refund","cuenta216_10_20","-DIOT: Retención","" +"","","","","","","","","","","","","","tax","refund","cuenta216_10_20","+DIOT: Retención","" "tax3","10","10% WH L I","Withholding of lease income tax 10%","Withholding income(-10%)","-10.0","percent","purchase","tax_group_isr_ret_10","","","Tasa","","base","invoice","","","Retención del impuesto de arrendamiento 10%" "","","","","","","","","","","","","","tax","invoice","cuenta216_03","","" "","","","","","","","","","","","","","base","refund","","","" @@ -28,13 +28,13 @@ "","","","","","","","","","","","","","base","refund","","","" "","","","","","","","","","","","","","tax","refund","cuenta216_04","","" "tax7","10","10.67% WH L","Vat withholding tax on leasing 10.67%","Vat withholding(-10.67%)","-10.67","percent","purchase","tax_group_iva_ret_1067","on_payment","cuenta216_10","Tasa","","base","invoice","","","Retención a cuenta del IVA sobre el arrendamiento financiero 10.67%" -"","","","","","","","","","","","","","tax","invoice","cuenta216_10_20","+DIOT: Retención","" +"","","","","","","","","","","","","","tax","invoice","cuenta216_10_20","-DIOT: Retención","" "","","","","","","","","","","","","","base","refund","","","" -"","","","","","","","","","","","","","tax","refund","cuenta216_10_20","-DIOT: Retención","" +"","","","","","","","","","","","","","tax","refund","cuenta216_10_20","+DIOT: Retención","" "tax8","10","10.67% WH","Vat withholding fees 10.67%","Vat withholding(-10.67%)","-10.67","percent","purchase","tax_group_iva_ret_1067","on_payment","cuenta216_10","Tasa","","base","invoice","","","Retención del IVA 10.67%" -"","","","","","","","","","","","","","tax","invoice","cuenta216_10_20","+DIOT: Retención","" +"","","","","","","","","","","","","","tax","invoice","cuenta216_10_20","-DIOT: Retención","" "","","","","","","","","","","","","","base","refund","","","" -"","","","","","","","","","","","","","tax","refund","cuenta216_10_20","-DIOT: Retención","" +"","","","","","","","","","","","","","tax","refund","cuenta216_10_20","+DIOT: Retención","" "tax13","10","0%","","VAT(0%)","0.0","percent","purchase","tax_group_iva_0","on_payment","cuenta119_01","Tasa","","base","invoice","","+DIOT: 0%","" "","","","","","","","","","","","","","tax","invoice","cuenta118_01","","" "","","","","","","","","","","","","","base","refund","","",""