From b99c147b630ffbb29e613e2c13938094ef5d5f6c Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Wed, 16 Aug 2023 11:15:46 +0000 Subject: [PATCH] [FIX] account: Use payment rate in reconciliation In the specific case in which an invoice in company currency is paid by a payment in foreign currency, in that case, perform the reconciliation in foreign currency but convert the residual amount of the invoice to the foreign currency using the payment rate instead of the invoice one. closes odoo/odoo#138289 X-original-commit: 2123278215befe22d3be4568812bfe7d028e2757 Related: odoo/enterprise#48744 Signed-off-by: John Laterre (jol) Signed-off-by: Laurent Smet (las) --- addons/account/models/account_move_line.py | 15 ++++++-- .../tests/test_account_move_reconcile.py | 36 +++++++++++++++++++ .../tests/test_account_payment_register.py | 4 +-- .../wizard/account_payment_register.py | 14 +++++--- .../tests/test_print_check.py | 6 ++-- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index 33df5cc5103..fd8f50c0b33 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -1663,23 +1663,29 @@ class AccountMoveLine(models.Model): return self[field] @api.model - def _prepare_move_line_residual_amounts(self, aml_values, counterpart_currency, shadowed_aml_values=None): + def _prepare_move_line_residual_amounts(self, aml_values, counterpart_currency, shadowed_aml_values=None, other_aml_values=None): """ Prepare the available residual amounts for each currency. :param aml_values: The values of account.move.line to consider. :param counterpart_currency: The currency of the opposite line this line will be reconciled with. :param shadowed_aml_values: A mapping aml -> dictionary to replace some original aml values to something else. This is usefull if you want to preview the reconciliation before doing some changes on amls like changing a date or an account. + :param other_aml_values: The other aml values to be reconciled with the current one. :return: A mapping currency -> dictionary containing: * residual: The residual amount left for this currency. * rate: The rate applied regarding the company's currency. """ - def get_odoo_rate(aml, currency): + def is_payment(aml): + return aml.move_id.payment_id or aml.move_id.statement_line_id + + def get_odoo_rate(aml, other_aml, currency): if aml.move_id.is_invoice(include_receipts=True): exchange_rate_date = aml.move_id.invoice_date else: exchange_rate_date = aml._get_reconciliation_aml_field_value('date', shadowed_aml_values) + if other_aml and not is_payment(aml) and is_payment(other_aml): + exchange_rate_date = other_aml._get_reconciliation_aml_field_value('date', shadowed_aml_values) return currency._get_conversion_rate(aml.company_currency_id, currency, aml.company_id, exchange_rate_date) def get_accounting_rate(aml, currency): @@ -1689,6 +1695,7 @@ class AccountMoveLine(models.Model): return abs(amount_currency / balance) aml = aml_values['aml'] + other_aml = (other_aml_values or {}).get('aml') remaining_amount_curr = aml_values['amount_residual_currency'] remaining_amount = aml_values['amount_residual'] company_currency = aml.company_currency_id @@ -1715,7 +1722,7 @@ class AccountMoveLine(models.Model): and is_rec_pay_account \ and not has_zero_residual \ and counterpart_currency != company_currency: - rate = get_odoo_rate(aml, counterpart_currency) + rate = get_odoo_rate(aml, other_aml, counterpart_currency) residual_in_foreign_curr = counterpart_currency.round(remaining_amount * rate) if not counterpart_currency.is_zero(residual_in_foreign_curr): available_residual_per_currency[counterpart_currency] = { @@ -1768,11 +1775,13 @@ class AccountMoveLine(models.Model): debit_values, credit_currency, shadowed_aml_values=shadowed_aml_values, + other_aml_values=credit_values, ) credit_available_residual_amounts = self._prepare_move_line_residual_amounts( credit_values, debit_currency, shadowed_aml_values=shadowed_aml_values, + other_aml_values=debit_values, ) if debit_currency != company_currency \ diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py index b7ccc48fe8e..f8a11433493 100644 --- a/addons/account/tests/test_account_move_reconcile.py +++ b/addons/account/tests/test_account_move_reconcile.py @@ -1800,6 +1800,42 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon): {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, ]) + def test_reconcile_invoice_company_curr_payment_foreign_curr(self): + """ Test we always use the payment rate in priority when performing a reconciliation. """ + comp_curr = self.company_data['currency'] + foreign_curr = self.currency_data['currency'] + + invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_date': '2017-01-01', + 'date': '2017-01-01', + 'partner_id': self.partner_a.id, + 'currency_id': comp_curr.id, + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 60.0, + 'tax_ids': [], + })], + }) + invoice.action_post() + + payment = self.env['account.payment.register']\ + .with_context(active_model=invoice._name, active_ids=invoice.ids)\ + .create({ + 'payment_date': '2016-01-01', + 'amount': 90.0, + 'currency_id': foreign_curr.id, + })\ + ._create_payments() + + lines = (invoice + payment.move_id).line_ids\ + .filtered(lambda x: x.account_id.account_type == 'asset_receivable') + self.assertRecordValues(lines, [ + # pylint: disable=bad-whitespace + {'amount_residual': 30.0, 'amount_residual_currency': 30.0, 'reconciled': False}, + {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True}, + ]) + def test_reverse_with_multiple_lines(self): """ Test if all lines from a reversed entry are fully reconciled diff --git a/addons/account/tests/test_account_payment_register.py b/addons/account/tests/test_account_payment_register.py index 819bfe8169f..4b504348046 100644 --- a/addons/account/tests/test_account_payment_register.py +++ b/addons/account/tests/test_account_payment_register.py @@ -1123,7 +1123,7 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): ]) def test_register_payment_invoice_comp_curr_payment_foreign_curr(self): - # Invoice of 400 USD (equivalent to 1200 Gol in 2016). + # Invoice of 600 USD (equivalent to 1200 Gol in 2017). invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', 'date': '2016-01-01', @@ -1132,7 +1132,7 @@ class TestAccountPaymentRegister(AccountTestInvoicingCommon): 'currency_id': self.company_data['currency'].id, 'invoice_line_ids': [Command.create({ 'product_id': self.product_a.id, - 'price_unit': 400.0, + 'price_unit': 600.0, 'tax_ids': [], })], }) diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py index 3ca43914c2b..6d6c4662790 100644 --- a/addons/account/wizard/account_payment_register.py +++ b/addons/account/wizard/account_payment_register.py @@ -532,15 +532,19 @@ class AccountPaymentRegister(models.TransientModel): ), False elif self.source_currency_id == comp_curr and self.currency_id != comp_curr: # Company currency on source line but a foreign currency one on the opposite line. - return abs(sum( - comp_curr._convert( + residual_amount = 0.0 + for aml in batch_result['lines']: + if not aml.move_id.payment_id and not aml.move_id.statement_line_id: + conversion_date = self.payment_date + else: + conversion_date = aml.date + residual_amount += comp_curr._convert( aml.amount_residual, self.currency_id, self.company_id, - aml.date, + conversion_date, ) - for aml in batch_result['lines'] - )), False + return abs(residual_amount), False else: # Foreign currency on payment different than the one set on the journal entries. return comp_curr._convert( diff --git a/addons/account_check_printing/tests/test_print_check.py b/addons/account_check_printing/tests/test_print_check.py index fa100ffc7a0..4f746084500 100644 --- a/addons/account_check_printing/tests/test_print_check.py +++ b/addons/account_check_printing/tests/test_print_check.py @@ -119,7 +119,7 @@ class TestPrintCheck(AccountTestInvoicingCommon): 'invoice_date': '2016-01-01', 'invoice_line_ids': [Command.create({ 'product_id': self.product_a.id, - 'price_unit': 100.0, + 'price_unit': 150.0, 'tax_ids': [] })] }) @@ -138,8 +138,8 @@ class TestPrintCheck(AccountTestInvoicingCommon): self.assertEqual(stub_pages, [[{ 'due_date': '01/01/2016', 'number': invoice.name, - 'amount_total': f'${NON_BREAKING_SPACE}100.00', - 'amount_residual': f'${NON_BREAKING_SPACE}50.00', + 'amount_total': f'${NON_BREAKING_SPACE}150.00', + 'amount_residual': f'${NON_BREAKING_SPACE}75.00', 'amount_paid': f'150.000{NON_BREAKING_SPACE}☺', 'currency': invoice.currency_id, }]])