[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) <jol@odoo.com>
Signed-off-by: Laurent Smet (las) <las@odoo.com>
This commit is contained in:
Laurent Smet
2023-10-11 15:45:31 +00:00
parent 71eb903d59
commit b99c147b63
5 changed files with 62 additions and 13 deletions
+12 -3
View File
@@ -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 \
@@ -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
@@ -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': [],
})],
})
@@ -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(
@@ -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,
}]])