summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurent Smet <las@odoo.com>2021-02-23 08:26:13 +0000
committerLaurent Smet <las@odoo.com>2021-02-23 15:11:51 +0000
commit07d5735de0e1b3080676ab26e872e55120fa2628 (patch)
treed1ddaf2622c8047318747cae930e1c14837e89b2
parent280df5ac282ad3f75b74dd3e6d172ee314693c99 (diff)
[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>
-rw-r--r--addons/account/models/account_move.py16
-rw-r--r--addons/account/tests/test_account_move_reconcile.py287
2 files changed, 297 insertions, 6 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index c4251614fbb..184da93d948 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 4c17a415c42..1f4a687f886 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).