From 07d5735de0e1b3080676ab26e872e55120fa2628 Mon Sep 17 00:00:00 2001 From: Laurent Smet <las@odoo.com> Date: Tue, 23 Feb 2021 08:26:13 +0000 Subject: [PATCH] [FIX] account: Fix reversal of exchange diff entry when unlinking the full reconcile Suppose an invoice of 1200USD = 3600EUR reconciled with a payment 1800USD = 3600EUR. The generated exchange difference journal entry is: 600USD = 0EUR because 1800 - 1200 - 600 = 0 and 3600 - 3600 = 0 => Everything is reconciled and all residual amounts are 0. Remove the reconciliation. The exchange difference entry is reversed in other to cancel it. Because the current exchange difference entry contains a line of 600USD, the reversal is creating a line of -600USD. Before this commit: Because both lines were sharing the same foreign currency (EUR) but have an amount_residual_currency of 0, no partial was created and then, a new exchange difference was generated in order to fix the amount_residual of 600 in USD. After this commit: A partial is created to handle the residual amount in USD even the residual amount in foreign currency is already zero. Note: this issue is also there when the reconciliation is made using the company's currency with different foreign currencies. In that case, amount_residual is zero but not amount_residual_currency. closes odoo/odoo#66653 Opw: 2450699 Signed-off-by: oco-odoo <oco-odoo@users.noreply.github.com> --- addons/account/models/account_move.py | 16 +- .../tests/test_account_move_reconcile.py | 287 ++++++++++++++++++ 2 files changed, 297 insertions(+), 6 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index c4251614fbb9..184da93d9480 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -4101,8 +4101,8 @@ class AccountMoveLine(models.Model): :return: A recordset of account.partial.reconcile. ''' - debit_lines = iter(self.filtered('debit')) - credit_lines = iter(self.filtered('credit')) + debit_lines = iter(self.filtered(lambda line: line.balance > 0.0 or line.amount_currency > 0.0)) + credit_lines = iter(self.filtered(lambda line: line.balance < 0.0 or line.amount_currency < 0.0)) debit_line = None credit_line = None @@ -4146,17 +4146,21 @@ class AccountMoveLine(models.Model): credit_line_currency = credit_line.company_currency_id min_amount_residual = min(debit_amount_residual, -credit_amount_residual) + has_debit_residual_left = not debit_line.company_currency_id.is_zero(debit_amount_residual) and debit_amount_residual > 0.0 + has_credit_residual_left = not credit_line.company_currency_id.is_zero(credit_amount_residual) and credit_amount_residual < 0.0 + has_debit_residual_curr_left = not debit_line_currency.is_zero(debit_amount_residual_currency) and debit_amount_residual_currency > 0.0 + has_credit_residual_curr_left = not credit_line_currency.is_zero(credit_amount_residual_currency) and credit_amount_residual_currency < 0.0 if debit_line_currency == credit_line_currency: # Reconcile on the same currency. # The debit line is now fully reconciled. - if debit_line_currency.is_zero(debit_amount_residual_currency) or debit_amount_residual_currency < 0.0: + if not has_debit_residual_curr_left and (has_credit_residual_curr_left or not has_debit_residual_left): debit_line = None continue # The credit line is now fully reconciled. - if credit_line_currency.is_zero(credit_amount_residual_currency) or credit_amount_residual_currency > 0.0: + if not has_credit_residual_curr_left and (has_debit_residual_curr_left or not has_credit_residual_left): credit_line = None continue @@ -4168,12 +4172,12 @@ class AccountMoveLine(models.Model): # Reconcile on the company's currency. # The debit line is now fully reconciled. - if debit_line.company_currency_id.is_zero(debit_amount_residual) or debit_amount_residual < 0.0: + if not has_debit_residual_left and (has_credit_residual_left or not has_debit_residual_curr_left): debit_line = None continue # The credit line is now fully reconciled. - if credit_line.company_currency_id.is_zero(credit_amount_residual) or credit_amount_residual > 0.0: + if not has_credit_residual_left and (has_debit_residual_left or not has_credit_residual_curr_left): credit_line = None continue diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index 4c17a415c428..1f4a687f886e 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -674,6 +674,293 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): self.assertFullReconcile(res['full_reconcile'], line_1 + line_2 + line_3 + line_4 + line_5) + def test_reverse_exchange_difference_same_foreign_currency(self): + move_2016 = self.env['account.move'].create({ + 'move_type': 'entry', + 'date': '2016-01-01', + 'line_ids': [ + (0, 0, { + 'debit': 1200.0, + 'credit': 0.0, + 'amount_currency': 3600.0, + 'account_id': self.company_data['default_account_receivable'].id, + 'currency_id': self.currency_data['currency'].id, + }), + (0, 0, { + 'debit': 0.0, + 'credit': 1200.0, + 'account_id': self.company_data['default_account_revenue'].id, + }), + ], + }) + move_2017 = self.env['account.move'].create({ + 'move_type': 'entry', + 'date': '2017-01-01', + 'line_ids': [ + (0, 0, { + 'debit': 0.0, + 'credit': 1800.0, + 'amount_currency': -3600.0, + 'account_id': self.company_data['default_account_receivable'].id, + 'currency_id': self.currency_data['currency'].id, + }), + (0, 0, { + 'debit': 1800.0, + 'credit': 0.0, + 'account_id': self.company_data['default_account_revenue'].id, + }), + ], + }) + (move_2016 + move_2017).action_post() + + rec_line_2016 = move_2016.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable') + rec_line_2017 = move_2017.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable') + + self.assertRecordValues(rec_line_2016 + rec_line_2017, [ + {'amount_residual': 1200.0, 'amount_residual_currency': 3600.0, 'reconciled': False}, + {'amount_residual': -1800.0, 'amount_residual_currency': -3600.0, 'reconciled': False}, + ]) + + # Reconcile. + + res = (rec_line_2016 + rec_line_2017).reconcile() + + self.assertRecordValues(rec_line_2016 + rec_line_2017, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + exchange_diff = res['full_reconcile'].exchange_move_id + exchange_diff_lines = exchange_diff.line_ids.sorted('balance') + + self.assertRecordValues(exchange_diff_lines, [ + { + 'debit': 0.0, + 'credit': 600.0, + 'amount_currency': 0.0, + 'currency_id': self.currency_data['currency'].id, + 'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id, + }, + { + 'debit': 600.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': self.currency_data['currency'].id, + 'account_id': self.company_data['default_account_receivable'].id, + }, + ]) + + self.assertRecordValues(exchange_diff_lines, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + # Unreconcile. + # A reversal is created to cancel the exchange difference journal entry. + + (rec_line_2016 + rec_line_2017).remove_move_reconcile() + + reverse_exchange_diff = exchange_diff_lines[1].matched_credit_ids.credit_move_id.move_id + reverse_exchange_diff_lines = reverse_exchange_diff.line_ids.sorted('balance') + + self.assertRecordValues(reverse_exchange_diff_lines, [ + { + 'debit': 0.0, + 'credit': 600.0, + 'amount_currency': 0.0, + 'currency_id': self.currency_data['currency'].id, + 'account_id': self.company_data['default_account_receivable'].id, + }, + { + 'debit': 600.0, + 'credit': 0.0, + 'amount_currency': 0.0, + 'currency_id': self.currency_data['currency'].id, + 'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id, + }, + ]) + + self.assertRecordValues(exchange_diff_lines + reverse_exchange_diff_lines, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False}, + ]) + + partials = reverse_exchange_diff_lines.matched_debit_ids + reverse_exchange_diff_lines.matched_credit_ids + self.assertPartialReconcile(partials, [{ + 'amount': 600.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': exchange_diff_lines[1].id, + 'credit_move_id': reverse_exchange_diff_lines[0].id, + }]) + + def test_reverse_exchange_multiple_foreign_currencies(self): + move_2016 = self.env['account.move'].create({ + 'move_type': 'entry', + 'date': '2016-01-01', + 'line_ids': [ + (0, 0, { + 'debit': 1200.0, + 'credit': 0.0, + 'amount_currency': 7200.0, + 'account_id': self.company_data['default_account_receivable'].id, + 'currency_id': self.currency_data_2['currency'].id, + }), + (0, 0, { + 'debit': 0.0, + 'credit': 1200.0, + 'account_id': self.company_data['default_account_revenue'].id, + }), + ], + }) + move_2017 = self.env['account.move'].create({ + 'move_type': 'entry', + 'date': '2017-01-01', + 'line_ids': [ + (0, 0, { + 'debit': 0.0, + 'credit': 1200.0, + 'amount_currency': -2400.0, + 'account_id': self.company_data['default_account_receivable'].id, + 'currency_id': self.currency_data['currency'].id, + }), + (0, 0, { + 'debit': 1200.0, + 'credit': 0.0, + 'account_id': self.company_data['default_account_revenue'].id, + }), + ], + }) + (move_2016 + move_2017).action_post() + + rec_line_2016 = move_2016.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable') + rec_line_2017 = move_2017.line_ids.filtered(lambda line: line.account_id.internal_type == 'receivable') + + self.assertRecordValues(rec_line_2016 + rec_line_2017, [ + {'amount_residual': 1200.0, 'amount_residual_currency': 7200.0, 'reconciled': False}, + {'amount_residual': -1200.0, 'amount_residual_currency': -2400.0, 'reconciled': False}, + ]) + + # Reconcile. + + res = (rec_line_2016 + rec_line_2017).reconcile() + + self.assertRecordValues(rec_line_2016 + rec_line_2017, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + exchange_diff = res['full_reconcile'].exchange_move_id + exchange_diff_lines = exchange_diff.line_ids.sorted('amount_currency') + + self.assertRecordValues(exchange_diff_lines, [ + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': -2400.0, + 'currency_id': self.currency_data_2['currency'].id, + 'account_id': self.company_data['default_account_receivable'].id, + }, + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': -1200.0, + 'currency_id': self.currency_data['currency'].id, + 'account_id': self.company_data['default_account_receivable'].id, + }, + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': 1200.0, + 'currency_id': self.currency_data['currency'].id, + 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id, + }, + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': 2400.0, + 'currency_id': self.currency_data_2['currency'].id, + 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id, + }, + ]) + + self.assertRecordValues(exchange_diff_lines, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False}, + ]) + + # Unreconcile. + # A reversal is created to cancel the exchange difference journal entry. + + (rec_line_2016 + rec_line_2017).remove_move_reconcile() + + reverse_exchange_diff = exchange_diff_lines[1].matched_debit_ids.debit_move_id.move_id + reverse_exchange_diff_lines = reverse_exchange_diff.line_ids.sorted('amount_currency') + + self.assertRecordValues(reverse_exchange_diff_lines, [ + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': -2400.0, + 'currency_id': self.currency_data_2['currency'].id, + 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id, + }, + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': -1200.0, + 'currency_id': self.currency_data['currency'].id, + 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id, + }, + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': 1200.0, + 'currency_id': self.currency_data['currency'].id, + 'account_id': self.company_data['default_account_receivable'].id, + }, + { + 'debit': 0.0, + 'credit': 0.0, + 'amount_currency': 2400.0, + 'currency_id': self.currency_data_2['currency'].id, + 'account_id': self.company_data['default_account_receivable'].id, + }, + ]) + + self.assertRecordValues(exchange_diff_lines + reverse_exchange_diff_lines, [ + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + + partials = reverse_exchange_diff_lines.matched_debit_ids + reverse_exchange_diff_lines.matched_credit_ids + self.assertPartialReconcile(partials, [ + { + 'amount': 0.0, + 'debit_amount_currency': 1200.0, + 'credit_amount_currency': 1200.0, + 'debit_move_id': reverse_exchange_diff_lines[2].id, + 'credit_move_id': exchange_diff_lines[1].id, + }, + { + 'amount': 0.0, + 'debit_amount_currency': 2400.0, + 'credit_amount_currency': 2400.0, + 'debit_move_id': reverse_exchange_diff_lines[3].id, + 'credit_move_id': exchange_diff_lines[0].id, + }, + ]) + # ------------------------------------------------------------------------- # Test creation of extra journal entries during the reconciliation to # deal with taxes that are exigible on payment (cash basis). -- GitLab