From df5dde330cc28fff63a9c1c7a2befa60657b5a76 Mon Sep 17 00:00:00 2001 From: fw-bot Date: Tue, 15 Sep 2020 10:39:19 +0000 Subject: [PATCH] [FIX] account: allow full reconciliation for tax lines on entries mixing cash basis and regular taxes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Solves https://github.com/odoo/odoo/issues/56803 Use case: 1) Create accounts ACC TAX A, ACC TAX B, with reconcile = True, internal_type = other 2) Create Tax A, with tax_exigibilty = on_invoice and account_id = ACC TAX A, 20% 3) Create Tax B, with tax_exigibilty = on_payment and account_id = ACC TAX B and cash_basic_account_id = ACC TAX A, 20% 4) Create invoice A, account_id = Tax A, unit_price = 1000, Valide invoice 5) Create invoice B, account_id = Tax B, unit_price = 500, Valide invoice, 6) Register full payment for invoice B. The cash basis tax creates a miscellaneous operation MISC A, containing a line on ACC TAX A. 7) Create a mis operation MISC TVA, like this: line 1 : account_id = ACC TAX A, credit = 300 line 2 : account_id = 401100, debit = 300 8) Reconcile the following move lines: line of MISC A with account ACC TAX A (300) line of invoice A with account ACC TAX A (200) line of MISC TVA with account ACC TAX A (100) ==> When doing that, not full reconcile object was created between the reconciled lines, while it should have been. X-original-commit: ff35a49 [IMP] account: reintroduce a test that should have been forward-ported … This test was made together with a bugfix by https://github.com/odoo/odoo/pull/51529 . The fix did not need to be forward-ported to 13.0, but the test should have been, so that we are sure it does not break again in the future. closes odoo/odoo#58072 X-original-commit: 5d00592f7d803888d8d5befd5a8cd1b096067860 Signed-off-by: oco-odoo Signed-off-by: Laurent Smet --- addons/account/tests/common.py | 28 +++-- .../tests/test_account_move_in_invoice.py | 2 +- .../tests/test_account_move_in_refund.py | 2 +- .../tests/test_account_move_out_invoice.py | 2 +- .../tests/test_account_move_out_refund.py | 2 +- .../tests/test_account_move_reconcile.py | 107 ++++++++++++++++-- addons/account/tests/test_payment_term.py | 2 +- .../tests/test_out_debit_note.py | 4 +- addons/l10n_fi/tests/test_get_reference.py | 2 +- addons/l10n_fi/tests/test_references.py | 2 +- 10 files changed, 126 insertions(+), 27 deletions(-) diff --git a/addons/account/tests/common.py b/addons/account/tests/common.py index 51bcecb9d01..fee6aa5a5f0 100644 --- a/addons/account/tests/common.py +++ b/addons/account/tests/common.py @@ -46,7 +46,7 @@ class AccountTestInvoicingCommon(SavepointCase): cls.cr = cls.env.cr cls.company_data_2 = cls.setup_company_data('company_2_data', chart_template=chart_template) - cls.company_data = cls.setup_company_data('company_1_data', chart_template=chart_template) + cls.company_data = cls.setup_company_data('company_1_data', chart_template=chart_template) user.write({ 'company_ids': [(6, 0, (cls.company_data['company'] + cls.company_data_2['company']).ids)], @@ -359,15 +359,29 @@ class AccountTestInvoicingCommon(SavepointCase): }) @classmethod - def init_invoice(cls, move_type, partner=None, invoice_date=None): + def init_invoice(cls, move_type, partner=None, invoice_date=None, post=False, products=[], amounts=[], taxes=None): move_form = Form(cls.env['account.move'].with_context(default_move_type=move_type)) move_form.invoice_date = invoice_date or fields.Date.from_string('2019-01-01') move_form.partner_id = partner or cls.partner_a - with move_form.invoice_line_ids.new() as line_form: - line_form.product_id = cls.product_a - with move_form.invoice_line_ids.new() as line_form: - line_form.product_id = cls.product_b - return move_form.save() + + for product in products: + with move_form.invoice_line_ids.new() as line_form: + line_form.product_id = product + if taxes: + line_form.tax_ids.add(taxes) + + for amount in amounts: + with move_form.invoice_line_ids.new() as line_form: + line_form.price_unit = amount + if taxes: + line_form.tax_ids.add(taxes) + + rslt = move_form.save() + + if post: + rslt.action_post() + + return rslt def assertInvoiceValues(self, move, expected_lines_values, expected_move_values): def sort_lines(lines): diff --git a/addons/account/tests/test_account_move_in_invoice.py b/addons/account/tests/test_account_move_in_invoice.py index 81a49631df7..278260d40c5 100644 --- a/addons/account/tests/test_account_move_in_invoice.py +++ b/addons/account/tests/test_account_move_in_invoice.py @@ -13,7 +13,7 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon): def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) - cls.invoice = cls.init_invoice('in_invoice') + cls.invoice = cls.init_invoice('in_invoice', products=cls.product_a+cls.product_b) cls.product_line_vals_1 = { 'name': cls.product_a.name, diff --git a/addons/account/tests/test_account_move_in_refund.py b/addons/account/tests/test_account_move_in_refund.py index da76d527547..262a1a82513 100644 --- a/addons/account/tests/test_account_move_in_refund.py +++ b/addons/account/tests/test_account_move_in_refund.py @@ -12,7 +12,7 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon): def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) - cls.invoice = cls.init_invoice('in_refund') + cls.invoice = cls.init_invoice('in_refund', products=cls.product_a+cls.product_b) cls.product_line_vals_1 = { 'name': cls.product_a.name, diff --git a/addons/account/tests/test_account_move_out_invoice.py b/addons/account/tests/test_account_move_out_invoice.py index 9896a833c0e..0d31b231ede 100644 --- a/addons/account/tests/test_account_move_out_invoice.py +++ b/addons/account/tests/test_account_move_out_invoice.py @@ -16,7 +16,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon): def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) - cls.invoice = cls.init_invoice('out_invoice') + cls.invoice = cls.init_invoice('out_invoice', products=cls.product_a+cls.product_b) cls.product_line_vals_1 = { 'name': cls.product_a.name, diff --git a/addons/account/tests/test_account_move_out_refund.py b/addons/account/tests/test_account_move_out_refund.py index 59472be893d..c1720dd2ba8 100644 --- a/addons/account/tests/test_account_move_out_refund.py +++ b/addons/account/tests/test_account_move_out_refund.py @@ -12,7 +12,7 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon): def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) - cls.invoice = cls.init_invoice('out_refund') + cls.invoice = cls.init_invoice('out_refund', products=cls.product_a+cls.product_b) cls.product_line_vals_1 = { 'name': cls.product_a.name, diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 5b8d5b2c617..0c8edd61b97 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -43,14 +43,14 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): 'user_type_id': cls.env.ref('account.data_account_type_revenue').id, 'company_id': cls.company_data['company'].id, }) - + cls.tax_account_1 = cls.env['account.account'].create({ 'code': 'tax_account_1', 'name': 'tax_account_1', 'user_type_id': cls.env.ref('account.data_account_type_revenue').id, 'company_id': cls.company_data['company'].id, }) - + cls.tax_account_2 = cls.env['account.account'].create({ 'code': 'tax_account_2', 'name': 'tax_account_2', @@ -74,6 +74,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): 'amount': 33.3333, 'company_id': cls.company_data['company'].id, 'cash_basis_transition_account_id': cls.cash_basis_transfer_account.id, + 'tax_exigibility': 'on_payment', 'invoice_repartition_line_ids': [ (0, 0, { 'factor_percent': 100, @@ -82,7 +83,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): }), (0, 0, { - 'factor_percent': 10, + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': cls.tax_account_1.id, 'tag_ids': [(6, 0, cls.tax_tags[1].ids)], @@ -96,7 +97,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): }), (0, 0, { - 'factor_percent': 10, + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': cls.tax_account_1.id, 'tag_ids': [(6, 0, cls.tax_tags[3].ids)], @@ -109,6 +110,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): 'amount': 0.0001, 'company_id': cls.company_data['company'].id, 'cash_basis_transition_account_id': cls.cash_basis_transfer_account.id, + 'tax_exigibility': 'on_payment', 'invoice_repartition_line_ids': [ (0, 0, { 'factor_percent': 100, @@ -117,7 +119,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): }), (0, 0, { - 'factor_percent': 10, + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': cls.tax_account_2.id, 'tag_ids': [(6, 0, cls.tax_tags[5].ids)], @@ -131,7 +133,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): }), (0, 0, { - 'factor_percent': 10, + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': cls.tax_account_2.id, 'tag_ids': [(6, 0, cls.tax_tags[7].ids)], @@ -171,10 +173,10 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): def assertAmountsGroupByAccount(self, amount_per_account): expected_values = {account.id: (account, balance, amount_currency) for account, balance, amount_currency in amount_per_account} - + if not expected_values: return - + self.cr.execute(''' SELECT line.account_id, @@ -209,7 +211,7 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): SUM(line.balance) FROM account_account_tag_account_move_line_rel rel JOIN account_move_line line ON line.id = rel.account_move_line_id - WHERE line.tax_exigible IS TRUE + WHERE line.tax_exigible IS TRUE GROUP BY rel.account_account_tag_id ''') @@ -752,9 +754,9 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): }) (cash_basis_move + payment_move).action_post() - + # Initial amounts by accounts: - + self.assertAmountsGroupByAccount([ # Account Balance Amount Currency (self.cash_basis_transfer_account, -33.34, -33.34), @@ -1633,3 +1635,86 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): (self.tax_tags[4], -100.0), (self.tax_tags[5], -0.01), ]) + + def test_caba_mix_reconciliation(self): + """ Test the reconciliation of tax lines (when using a reconcilable tax account) + for cases mixing taxes exigible on payment and on invoices. + """ + + # Make the tax account reconcilable + self.tax_account_1.reconcile = True + + # Create a tax using the same accounts as the CABA one + non_caba_tax = self.env['account.tax'].create({ + 'name': 'tax 20%', + 'type_tax_use': 'purchase', + 'company_id': self.company_data['company'].id, + 'amount': 20, + 'tax_exigibility': 'on_invoice', + 'invoice_repartition_line_ids': [ + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': self.tax_account_1.id, + }), + ], + 'refund_repartition_line_ids': [ + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': self.tax_account_1.id, + }), + ], + }) + + # Create an invoice with a non-CABA tax + non_caba_inv = self.init_invoice('in_invoice', amounts=[1000], post=True, taxes=non_caba_tax) + + # Create an invoice with a CABA tax using the same tax account and pay it + caba_inv = self.init_invoice('in_invoice', amounts=[300], post=True, taxes=self.cash_basis_tax_a_third_amount) + + pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=caba_inv.ids).create({ + 'payment_date': caba_inv.date, + 'journal_id': self.company_data['default_journal_bank'].id, + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + }) + pmt_wizard._create_payments() + + partial_rec = caba_inv.mapped('line_ids.matched_debit_ids') + caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)]) + + # Create a misc operation with a line on the tax account, for full reconcile of those tax lines + misc_move = self.env['account.move'].create({ + 'name': "Misc move", + 'journal_id': self.company_data['default_journal_misc'].id, + 'line_ids': [ + (0, 0, { + 'name': 'line 1', + 'account_id': self.tax_account_1.id, + 'credit': 300, + }), + (0, 0, { + 'name': 'line 2', + 'account_id': self.company_data['default_account_expense'].id, # Whatever the account here + 'debit': 300, + }) + ], + }) + + misc_move.action_post() + + lines_to_reconcile = (misc_move + caba_move + non_caba_inv).mapped('line_ids').filtered(lambda x: x.account_id == self.tax_account_1) + lines_to_reconcile.reconcile() + + # Check full reconciliation + self.assertTrue(all(line.full_reconcile_id for line in lines_to_reconcile), "All tax lines should be fully reconciled") diff --git a/addons/account/tests/test_payment_term.py b/addons/account/tests/test_payment_term.py index 3305e7e86ca..a3133247f6c 100644 --- a/addons/account/tests/test_payment_term.py +++ b/addons/account/tests/test_payment_term.py @@ -67,7 +67,7 @@ class TestAccountInvoiceRounding(AccountTestInvoicingCommon): ], }) - cls.invoice = cls.init_invoice('out_refund') + cls.invoice = cls.init_invoice('out_refund', products=cls.product_a+cls.product_b) def assertPaymentTerm(self, pay_term, invoice_date, dates): with Form(self.invoice) as move_form: diff --git a/addons/account_debit_note/tests/test_out_debit_note.py b/addons/account_debit_note/tests/test_out_debit_note.py index dd024fa23b0..64746236b67 100644 --- a/addons/account_debit_note/tests/test_out_debit_note.py +++ b/addons/account_debit_note/tests/test_out_debit_note.py @@ -9,7 +9,7 @@ class TestAccountDebitNote(AccountTestInvoicingCommon): def test_00_debit_note_out_invoice(self): """ Debit Note of a regular Customer Invoice""" - invoice = self.init_invoice('out_invoice') + invoice = self.init_invoice('out_invoice', products=self.product_a+self.product_b) invoice.action_post() move_debit_note_wiz = self.env['account.debit.note'].with_context(active_model="account.move", active_ids=invoice.ids).create({ @@ -28,7 +28,7 @@ class TestAccountDebitNote(AccountTestInvoicingCommon): def test_10_debit_note_in_refund(self): """ Debit Note of a vendor refund (is a regular vendor bill) """ - invoice = self.init_invoice('in_refund') + invoice = self.init_invoice('in_refund', products=self.product_a+self.product_b) invoice.action_post() move_debit_note_wiz = self.env['account.debit.note'].with_context(active_model="account.move", active_ids=invoice.ids).create({ diff --git a/addons/l10n_fi/tests/test_get_reference.py b/addons/l10n_fi/tests/test_get_reference.py index 7476898097d..ea41528abc5 100644 --- a/addons/l10n_fi/tests/test_get_reference.py +++ b/addons/l10n_fi/tests/test_get_reference.py @@ -10,7 +10,7 @@ class InvoiceGetReferenceTest(AccountTestInvoicingCommon): def setUpClass(cls, chart_template_ref='l10n_fi.fi_chart_template'): super().setUpClass(chart_template_ref=chart_template_ref) - cls.invoice = cls.init_invoice('out_invoice') + cls.invoice = cls.init_invoice('out_invoice', products=cls.product_a+cls.product_b) def test_get_reference_finnish_invoice(self): self.assertFalse(self.invoice.payment_reference) diff --git a/addons/l10n_fi/tests/test_references.py b/addons/l10n_fi/tests/test_references.py index 509d326d5d2..38cf8a7b411 100644 --- a/addons/l10n_fi/tests/test_references.py +++ b/addons/l10n_fi/tests/test_references.py @@ -15,7 +15,7 @@ class PaymentReferenceTest(AccountTestInvoicingCommon): def setUpClass(cls, chart_template_ref='l10n_fi.fi_chart_template'): super().setUpClass(chart_template_ref=chart_template_ref) - cls.invoice = cls.init_invoice('out_invoice') + cls.invoice = cls.init_invoice('out_invoice', products=cls.product_a+cls.product_b) def test_payment_reference_fi(self):