[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: Nicolas Martinelli (nim) <nim@odoo.com> Signed-off-by: Laurent Smet <smetl@users.noreply.github.com>
This commit is contained in:
committed by
Laurent Smet
parent
482ff59fa3
commit
83bb79677c
@@ -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)
|
||||
}, 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)
|
||||
|
||||
Reference in New Issue
Block a user