Skip to content
Snippets Groups Projects
Commit 83bb7967 authored by Andrea Grazioso (agr-odoo)'s avatar Andrea Grazioso (agr-odoo) Committed by Laurent Smet
Browse files

[FIX] account: multicurrency of bank statement reconciliation


- Activate multicurrency and use a 1.5 rate on USD to EUR (to ease
calculations)
- Create two customer invoices in EUR (company is in USD) for two
different partners (e.g. partner1 and partner2) with only one line and
an amount of 100 EUR (removed the tax as well so that payment term line
is 150 USD on receivable account)
- On the "Invoices Matching Rule" reconciliation model:
1. Set Amount Matching to 90%
2. Define any account for the counterpart
3. Remove "Same Currency Matching"
4. Ensure "Partner Is Set & Matches" is marked

- Then create a new bank statement with two lines as follows:

1. dummy label, partner1, 140 USD
2. dummy label, partner2, 100 USD

- When clicking on reconcile,

1. the line for partner1 is not matched
2. the line for partner2 is matched

This occur because the amount from the invoice is not correctly
converted in company currency before making the check with the bank
statement amounts

opw-2261134

closes odoo/odoo#52529

Signed-off-by: default avatarNicolas Martinelli (nim) <nim@odoo.com>
Signed-off-by: default avatarLaurent Smet <smetl@users.noreply.github.com>
parent 482ff59f
No related branches found
No related tags found
No related merge requests found
......@@ -611,11 +611,20 @@ class AccountBankStatementLine(models.Model):
and 'move_line' will be used to create the counterpart of an existing account.move.line to which
the newly created journal item will be reconciled.
:param counterpart_vals: A python dictionary containing:
'balance': Optional amount to consider during the reconciliation. If a foreign currency is set on the
counterpart line in the same foreign currency as the statement line, then this amount is
considered as the amount in foreign currency. If not specified, the full balance is took.
This value must be provided if move_line is not.
**kwargs: Additional values that need to land on the account.move.line to create.
'balance': Optional amount to consider during the reconciliation. If a foreign currency is set on the
counterpart line in the same foreign currency as the statement line, then this amount is
considered as the amount in foreign currency. If not specified, the full balance is took.
This value must be provided if move_line is not.
'amount_residual': The residual amount to reconcile expressed in the company's currency.
/!\ This value should be equivalent to move_line.amount_residual except we want
to avoid browsing the record when the only thing we need in an overview of the
reconciliation, for example in the reconciliation widget.
'amount_residual_currency': The residual amount to reconcile expressed in the foreign's currency.
Using this key doesn't make sense without passing 'currency_id' in vals.
/!\ This value should be equivalent to move_line.amount_residual_currency except
we want to avoid browsing the record when the only thing we need in an overview
of the reconciliation, for example in the reconciliation widget.
**kwargs: Additional values that need to land on the account.move.line to create.
:param move_line: An optional account.move.line move_line representing the counterpart line to reconcile.
:return: The values to create a new account.move.line move_line.
'''
......@@ -627,6 +636,12 @@ class AccountBankStatementLine(models.Model):
journal_currency = journal.currency_id if journal.currency_id != company_currency else False
statement_line_rate = self.amount_currency / (self.amount or 1.0)
balance_to_reconcile = counterpart_vals.pop('balance', None)
amount_residual = -counterpart_vals.pop('amount_residual', move_line.amount_residual if move_line else 0.0) \
if balance_to_reconcile is None else balance_to_reconcile
amount_residual_currency = -counterpart_vals.pop('amount_residual_currency', move_line.amount_residual_currency if move_line else 0.0)\
if balance_to_reconcile is None else balance_to_reconcile
if 'currency_id' in counterpart_vals:
currency_id = counterpart_vals['currency_id']
elif move_line:
......@@ -648,7 +663,7 @@ class AccountBankStatementLine(models.Model):
# There is also a foreign currency set on the journal so the journal item to create will
# use the foreign currency set on the statement line.
amount_currency = counterpart_vals.pop('balance', -move_line.amount_residual_currency if move_line else 0.0)
amount_currency = amount_residual_currency
balance = journal_currency._convert(amount_currency / statement_line_rate, company_currency, journal.company_id, self.date)
elif currency_id == journal_currency.id and self.foreign_currency_id == company_currency:
......@@ -657,7 +672,7 @@ class AccountBankStatementLine(models.Model):
# There is also a foreign currency set on the statement line that is the same as the company one.
# Then, the journal item to create will use the company's currency.
amount_currency = counterpart_vals.pop('balance', -move_line.amount_residual_currency if move_line else 0.0)
amount_currency = amount_residual_currency
balance = amount_currency * statement_line_rate
currency_id = False
amount_currency = 0.0
......@@ -668,7 +683,7 @@ class AccountBankStatementLine(models.Model):
# There is also a foreign currency set on the statement line.
# The residual amount will be convert to the foreign currency set on the statement line.
amount_currency = counterpart_vals.pop('balance', -move_line.amount_residual_currency if move_line else 0.0)
amount_currency = amount_residual_currency
balance = journal_currency._convert(amount_currency, company_currency, journal.company_id, self.date)
amount_currency *= statement_line_rate
currency_id = self.foreign_currency_id.id
......@@ -678,7 +693,7 @@ class AccountBankStatementLine(models.Model):
# Whatever the currency set on the journal item passed as parameter, the counterpart line
# will be expressed in the foreign currency set on the statement line.
balance = counterpart_vals.pop('balance', -move_line.amount_residual if move_line else 0.0)
balance = amount_residual
amount_currency = company_currency._convert(balance, journal_currency, journal.company_id, self.date)
amount_currency *= statement_line_rate
currency_id = self.foreign_currency_id.id
......@@ -690,10 +705,10 @@ class AccountBankStatementLine(models.Model):
# and is used as conversion rate between the company's currency and the foreign currency.
if currency_id == self.foreign_currency_id.id:
amount_currency = counterpart_vals.pop('balance', -move_line.amount_residual_currency if move_line else 0.0)
amount_currency = amount_residual_currency
balance = amount_currency / statement_line_rate
else:
balance = counterpart_vals.pop('balance', -move_line.amount_residual if move_line else 0.0)
balance = amount_residual
amount_currency = balance * statement_line_rate
currency_id = self.foreign_currency_id.id
......@@ -703,10 +718,10 @@ class AccountBankStatementLine(models.Model):
# Everything will be expressed in the journal's currency.
if currency_id == journal_currency.id:
amount_currency = counterpart_vals.pop('balance', -move_line.amount_residual_currency if move_line else 0.0)
amount_currency = amount_residual_currency
balance = journal_currency._convert(amount_currency, company_currency, journal.company_id, self.date)
else:
balance = counterpart_vals.pop('balance', -move_line.amount_residual if move_line else 0.0)
balance = amount_residual
amount_currency = company_currency._convert(balance, journal_currency, journal.company_id, self.date)
currency_id = journal_currency.id
......@@ -715,12 +730,12 @@ class AccountBankStatementLine(models.Model):
# Only a foreign currency set on the counterpart line.
# Ignore it and record the line using the company's currency.
balance = counterpart_vals.pop('balance', -move_line.amount_residual if move_line else 0.0)
balance = amount_residual
amount_currency = 0.0
currency_id = False
else:
balance = counterpart_vals.pop('balance', -move_line.amount_residual if move_line else 0.0)
balance = amount_residual
if self.foreign_currency_id and journal_currency:
......
......@@ -796,35 +796,30 @@ class AccountReconcileModel(models.Model):
if not candidates:
return False
reconciliation_overview, open_balance_vals = statement_line._prepare_reconciliation([{
'currency_id': aml['aml_currency_id'],
'amount_residual': aml['aml_amount_residual'],
'amount_residual_currency': aml['aml_amount_residual_currency'],
} for aml in candidates])
# Match total residual amount.
line_currency = statement_line.foreign_currency_id or statement_line.currency_id
line_residual = statement_line.amount_residual
line_residual_after_reconciliation = line_residual
candidate_currencies = set(candidate['aml_currency_id'] or statement_line.company_currency_id.id for candidate in candidates)
if candidate_currencies != {line_currency.id}:
# We don't apply any automatic match based on residual amount if candidates have differenct currencies
return False
total_residual = 0.0
for aml in candidates:
if aml['account_internal_type'] == 'liquidity':
total_residual += aml['aml_currency_id'] and aml['aml_amount_currency'] or aml['aml_balance']
for reconciliation_vals in reconciliation_overview:
line_vals = reconciliation_vals['line_vals']
if line_vals['currency_id']:
line_residual_after_reconciliation -= line_vals['amount_currency']
else:
total_residual += aml['aml_currency_id'] and aml['aml_amount_residual_currency'] or aml['aml_amount_residual']
line_residual_after_reconciliation -= line_vals['debit'] - line_vals['credit']
# Statement line amount is equal to the total residual.
if float_is_zero(total_residual + statement_line.amount_residual, precision_rounding=line_currency.rounding):
if line_currency.is_zero(line_residual_after_reconciliation):
return True
line_residual_to_compare = abs(statement_line.amount_residual)
total_residual_to_compare = abs(total_residual)
if line_residual_to_compare > total_residual_to_compare:
amount_percentage = (total_residual_to_compare / line_residual_to_compare) * 100
elif total_residual:
amount_percentage = (line_residual_to_compare / total_residual_to_compare) * 100 if total_residual_to_compare else 0.0
else:
return False
return amount_percentage >= self.match_total_amount_param
reconciled_percentage = (abs(line_residual) - abs(line_residual_after_reconciliation)) / abs(line_residual) * 100
return reconciled_percentage >= self.match_total_amount_param
def _filter_candidates(self, candidates, aml_ids_to_exclude, reconciled_amls_ids):
""" Sorts reconciliation candidates by priority and filters them so that only
......
......@@ -12,6 +12,13 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.currency_data_2 = cls.setup_multi_currency_data({
'name': 'Dark Chocolate Coin',
'symbol': '🍫',
'currency_unit_label': 'Dark Choco',
'currency_subunit_label': 'Dark Cacao Powder',
}, rate2016=10.0, rate2017=20.0)
cls.company = cls.company_data['company']
cls.account_pay = cls.company_data['default_account_payable']
......@@ -434,7 +441,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self.bank_line_2.unlink()
self.bank_line_1.write({'partner_id': partner.id, 'foreign_currency_id': currency_statement.id, 'amount_currency': 100, 'payment_ref': 'test'})
self.env['account.reconcile.model'].flush()
self._check_statement_matching(self.rule_1, {self.bank_line_1.id: {'aml_ids': []}}, statements=self.bank_st)
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': invoice_line.ids, 'model': self.rule_1},
}, statements=self.bank_st)
def test_invoice_matching_rule_no_partner(self):
""" Tests that a statement line without any partner can be matched to the
......@@ -510,4 +519,98 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1},
self.bank_line_2.id: {'aml_ids': []},
}, self.bank_st)
\ No newline at end of file
}, self.bank_st)
def test_match_multi_currencies(self):
''' Ensure the matching of candidates is made using the right statement line currency.
In this test, the value of the statement line is 100 USD = 300 GOL = 900 DAR and we want to match two journal
items of:
- 100 USD = 200 GOL (= 600 DAR from the statement line point of view)
- 14 USD = 280 DAR
Both journal items should be suggested to the user because they represents 98% of the statement line amount
(DAR).
'''
partner = self.env['res.partner'].create({'name': 'Bernard Perdant'})
journal = self.env['account.journal'].create({
'name': 'test_match_multi_currencies',
'code': 'xxxx',
'type': 'bank',
'currency_id': self.currency_data['currency'].id,
})
matching_rule = self.env['account.reconcile.model'].create({
'name': 'test_match_multi_currencies',
'rule_type': 'invoice_matching',
'match_partner': True,
'match_partner_ids': [(6, 0, partner.ids)],
'match_total_amount': True,
'match_total_amount_param': 95.0,
'match_same_currency': False,
'company_id': self.company_data['company'].id,
})
statement = self.env['account.bank.statement'].create({
'name': 'test_match_multi_currencies',
'journal_id': journal.id,
'line_ids': [
(0, 0, {
'journal_id': journal.id,
'date': '2016-01-01',
'payment_ref': 'line',
'partner_id': partner.id,
'foreign_currency_id': self.currency_data_2['currency'].id,
'amount': 300.0, # Rate is 3 GOL = 1 USD in 2016.
'amount_currency': 900.0, # Rate is 10 DAR = 1 USD in 2016 but the rate used by the bank is 9:1.
}),
],
})
statement_line = statement.line_ids
statement.button_post()
move = self.env['account.move'].create({
'move_type': 'entry',
'date': '2017-01-01',
'journal_id': self.company_data['default_journal_sale'].id,
'line_ids': [
# Rate is 2 GOL = 1 USD in 2017.
# The statement line will consider this line equivalent to 600 DAR.
(0, 0, {
'account_id': self.company_data['default_account_receivable'].id,
'partner_id': partner.id,
'currency_id': self.currency_data['currency'].id,
'debit': 100.0,
'credit': 0.0,
'amount_currency': 200.0,
}),
# Rate is 20 GOL = 1 USD in 2017.
(0, 0, {
'account_id': self.company_data['default_account_receivable'].id,
'partner_id': partner.id,
'currency_id': self.currency_data_2['currency'].id,
'debit': 14.0,
'credit': 0.0,
'amount_currency': 280.0,
}),
# Line to balance the journal entry:
(0, 0, {
'account_id': self.company_data['default_account_revenue'].id,
'debit': 0.0,
'credit': 114.0,
}),
],
})
move.post()
move_line_1 = move.line_ids.filtered(lambda line: line.debit == 100.0)
move_line_2 = move.line_ids.filtered(lambda line: line.debit == 14.0)
self.env['account.reconcile.model'].flush()
with self.mocked_today('2017-01-01'):
self._check_statement_matching(matching_rule, {
statement_line.id: {'aml_ids': (move_line_1 + move_line_2).ids, 'model': matching_rule}
}, statements=statement)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment