From 27622ecbbf0a3500f81924d7b67ee5b1ab83c1bd Mon Sep 17 00:00:00 2001 From: oco-odoo Date: Wed, 19 Oct 2022 14:10:40 +0000 Subject: [PATCH] [FIX] account: don't cross exchange rates when reconciling an invoice with a refund Before this fix, when reconciling an invoice with a refund (both using cash basis taxes) in a multicurrency setup, the two cash basis entries that got created were made so that the invoice's used the refund's rate, and the refund's used the invoice's one. This caused confustion, and forced Odoo to always add cash basis adjustment journal items in the exchange difference move, in order to balance the accounts properly. By not swapping the rates anymore, we avoid the creation of these journal items. OPW 2888592 X-original-commit: dc27baac2b39abcd77cfe20290393b246f7b2a9c Part-of: odoo/odoo#104564 --- .../models/account_partial_reconcile.py | 14 +- .../tests/test_account_move_reconcile.py | 153 ++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) diff --git a/addons/account/models/account_partial_reconcile.py b/addons/account/models/account_partial_reconcile.py index 6a7ec11c25b..2209fea4d8a 100644 --- a/addons/account/models/account_partial_reconcile.py +++ b/addons/account/models/account_partial_reconcile.py @@ -160,6 +160,7 @@ class AccountPartialReconcile(models.Model): partial_amount_currency = 0.0 rate_amount = 0.0 rate_amount_currency = 0.0 + if partial.debit_move_id.move_id == move: partial_amount += partial.amount partial_amount_currency += partial.debit_amount_currency @@ -167,6 +168,7 @@ class AccountPartialReconcile(models.Model): rate_amount_currency -= partial.credit_move_id.amount_currency source_line = partial.debit_move_id counterpart_line = partial.credit_move_id + if partial.credit_move_id.move_id == move: partial_amount += partial.amount partial_amount_currency += partial.credit_amount_currency @@ -175,6 +177,16 @@ class AccountPartialReconcile(models.Model): source_line = partial.credit_move_id counterpart_line = partial.debit_move_id + if partial.debit_move_id.move_id.is_invoice(include_receipts=True) and partial.credit_move_id.move_id.is_invoice(include_receipts=True): + # Will match when reconciling a refund with an invoice. + # In this case, we want to use the rate of each businness document to compute its cash basis entry, + # not the rate of what it's reconciled with. + rate_amount = source_line.balance + rate_amount_currency = source_line.amount_currency + payment_date = move.date + else: + payment_date = counterpart_line.date + if move_values['currency'] == move.company_id.currency_id: # Ignore the exchange difference. if move.company_currency_id.is_zero(partial_amount): @@ -197,7 +209,7 @@ class AccountPartialReconcile(models.Model): counterpart_line.company_currency_id, source_line.currency_id, counterpart_line.company_id, - counterpart_line.date, + payment_date, ) elif rate_amount: payment_rate = rate_amount_currency / rate_amount diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 6037f4513a5..0450e9cb923 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -3208,6 +3208,159 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): {'debit': 0.0, 'credit': 50.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id}, ]) + def test_reconcile_cash_basis_refund_multicurrency(self): + self.env.company.tax_exigibility = True + rates_data = self.setup_multi_currency_data(default_values={ + 'name': 'Playmock', + 'symbol': '🦌', + 'rounding': 0.01, + 'currency_unit_label': 'Playmock', + 'currency_subunit_label': 'Cent', + }, rate2016=0.5, rate2017=0.33333333333333333) + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'currency_id': rates_data['currency'].id, + 'invoice_date': '2016-01-01', + 'invoice_line_ids': [(0, 0, { + 'name': 'dudu', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 100.0, + 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)], + })], + }) + + refund = self.env['account.move'].create({ + 'move_type': 'out_refund', + 'partner_id': self.partner_a.id, + 'currency_id': rates_data['currency'].id, + 'invoice_date': '2017-01-01', + 'invoice_line_ids': [(0, 0, { + 'name': 'dudu', + 'account_id': self.company_data['default_account_revenue'].id, + 'price_unit': 100.0, + 'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)], + })], + }) + + invoice.action_post() + refund.action_post() + + (refund + invoice).line_ids.filtered(lambda x: x.account_id.account_type == 'asset_receivable').reconcile() + + # Check the cash basis moves + self.assertRecordValues( + self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', invoice.id)]).line_ids, + [ + { + 'debit': 200, + 'credit': 0, + 'amount_currency': 100, + 'currency_id': rates_data['currency'].id, + 'tax_ids': [], + 'tax_repartition_line_id': None, + 'tax_tag_ids': [], + }, + { + 'debit': 0, + 'credit': 200, + 'amount_currency': -100, + 'currency_id': rates_data['currency'].id, + 'tax_ids': self.cash_basis_tax_a_third_amount.ids, + 'tax_repartition_line_id': None, + 'tax_tag_ids': self.tax_tags[0].ids, + }, + { + 'debit': 66.66, + 'credit': 0, + 'amount_currency': 33.33, + 'currency_id': rates_data['currency'].id, + 'tax_ids': [], + 'tax_repartition_line_id': None, + 'tax_tag_ids': [], + }, + { + 'debit': 0, + 'credit': 66.66, + 'amount_currency': -33.33, + 'currency_id': rates_data['currency'].id, + 'tax_ids': [], + 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_tag_ids': self.tax_tags[1].ids, + }, + ] + ) + + self.assertRecordValues( + self.env['account.move'].search([('tax_cash_basis_origin_move_id', '=', refund.id)]).line_ids, + [ + { + 'debit': 0, + 'credit': 300, + 'amount_currency': -100, + 'currency_id': rates_data['currency'].id, + 'tax_ids': [], + 'tax_repartition_line_id': None, + 'tax_tag_ids': [], + }, + { + 'debit': 300, + 'credit': 0, + 'amount_currency': 100, + 'currency_id': rates_data['currency'].id, + 'tax_ids': self.cash_basis_tax_a_third_amount.ids, + 'tax_repartition_line_id': None, + 'tax_tag_ids': self.tax_tags[2].ids, + }, + { + 'debit': 0, + 'credit': 99.99, + 'amount_currency': -33.33, + 'currency_id': rates_data['currency'].id, + 'tax_ids': [], + 'tax_repartition_line_id': None, + 'tax_tag_ids': [], + }, + { + 'debit': 99.99, + 'credit': 0, + 'amount_currency': 33.33, + 'currency_id': rates_data['currency'].id, + 'tax_ids': [], + 'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_tag_ids': self.tax_tags[3].ids, + }, + ] + ) + + # Check the exchange difference move, to be sure no cash basis rounding data was added into it + self.assertRecordValues( + invoice.line_ids.filtered(lambda x: x.account_id.account_type == 'asset_receivable').matched_credit_ids.exchange_move_id.line_ids, + [ + { + 'debit': 133.33, + 'credit': 0, + 'amount_currency': 0, + 'currency_id': rates_data['currency'].id, + 'tax_ids': [], + 'tax_repartition_line_id': None, + 'tax_tag_ids': [], + }, + { + 'debit': 0, + 'credit': 133.33, + 'amount_currency': 0, + 'currency_id': rates_data['currency'].id, + 'tax_ids': [], + 'tax_repartition_line_id': None, + 'tax_tag_ids': [], + }, + ] + ) + + self.assertFalse(invoice.line_ids.full_reconcile_id.exchange_move_id) + def test_reconcile_cash_basis_revert(self): ''' Ensure the cash basis journal entry can be reverted. ''' self.env.company.tax_exigibility = True