[FIX] account: reconciliation models: get residual amount of bank statement lines properly

[FIX] account: reconciliation models: don't mix currencies when checking the candidates for applicability of a model

Before that, similiar amount of different currencies could be matched together, regardless of the exchange rate. We now entirely forbid this case from the reconciliation models.

closes odoo/odoo#51153

Signed-off-by: Laurent Smet <smetl@users.noreply.github.com>
This commit is contained in:
oco-odoo
2020-05-18 10:53:51 +00:00
parent ea3e39506a
commit c136c42a7d
3 changed files with 56 additions and 15 deletions
@@ -505,6 +505,10 @@ class AccountBankStatementLine(models.Model):
help="The amount expressed in an optional other currency if it is a multi-currency entry.")
foreign_currency_id = fields.Many2one('res.currency', string='Foreign Currency',
help="The optional other currency if it is a multi-currency entry.")
amount_residual = fields.Float(string="Residual Amount",
compute="_compute_is_reconciled",
store=True,
help="The amount left to be reconciled on this statement line (signed according to its move lines' balance), expressed in its currency. This is a technical field use to speedup the application of reconciliation models.")
currency_id = fields.Many2one('res.currency', string='Journal Currency')
partner_id = fields.Many2one(
comodel_name='res.partner',
@@ -810,15 +814,17 @@ class AccountBankStatementLine(models.Model):
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('currency_id', 'amount',
@api.depends('currency_id', 'amount', 'foreign_currency_id', 'amount_currency',
'move_id.line_ids', 'move_id.line_ids.matched_debit_ids', 'move_id.line_ids.matched_credit_ids')
def _compute_is_reconciled(self):
''' Compute the field indicating if the statement lines are already reconciled with something.
This field is used for display purpose (e.g. display the 'cancel' button on the statement lines).
Also computes the residual amount of the statement line.
'''
for st_line in self:
liquidity_lines, suspense_lines, other_lines = st_line._seek_for_lines()
# Compute is_reconciled
if not st_line.id or suspense_lines:
# New record: The journal items are not yet there.
st_line.is_reconciled = False
@@ -828,6 +834,17 @@ class AccountBankStatementLine(models.Model):
# The journal entry seems reconciled.
st_line.is_reconciled = True
# Compute residual amount
st_line_currency = suspense_lines.currency_id or suspense_lines.company_currency_id
balance_field, residual_field = ('amount_currency', 'amount_residual_currency') if suspense_lines.currency_id else ('balance', 'amount_residual')
if st_line.to_check:
st_line.amount_residual = -st_line.amount_currency if st_line.foreign_currency_id else -st_line.amount
elif suspense_lines.account_id.reconcile:
st_line.amount_residual = sum(suspense_lines.mapped(residual_field))
else:
st_line.amount_residual = sum(suspense_lines.mapped(balance_field))
# -------------------------------------------------------------------------
# CONSTRAINT METHODS
# -------------------------------------------------------------------------
@@ -257,7 +257,7 @@ class AccountReconcileModel(models.Model):
if self.rule_type == 'invoice_matching' and (not self.match_total_amount or (self.match_total_amount_param == 100)):
return []
line_currency = st_line.currency_id or st_line.journal_id.currency_id or self.company_id.currency_id
line_currency = st_line.foreign_currency_id or st_line.currency_id
if line_currency.is_zero(residual_balance):
# No writeoff needs to be done for a full reconciliation
return []
@@ -630,21 +630,26 @@ class AccountReconcileModel(models.Model):
return False
# Match total residual amount.
line_currency = statement_line.foreign_currency_id or statement_line.currency_id
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']
else:
total_residual += aml['aml_currency_id'] and aml['aml_amount_residual_currency'] or aml['aml_amount_residual']
line_residual = statement_line.currency_id and statement_line.amount_currency or statement_line.amount
line_currency = statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.company_id.currency_id
# Statement line amount is equal to the total residual.
if float_is_zero(total_residual - line_residual, precision_rounding=line_currency.rounding):
if float_is_zero(total_residual + statement_line.amount_residual, precision_rounding=line_currency.rounding):
return True
line_residual_to_compare = line_residual if line_residual > 0.0 else -line_residual
total_residual_to_compare = total_residual if line_residual > 0.0 else -total_residual
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
@@ -712,8 +717,7 @@ class AccountReconcileModel(models.Model):
# Iterate all and create results.
for line in st_lines:
line_currency = line.currency_id or line.journal_id.currency_id or line.company_id.currency_id
line_residual = line.currency_id and line.amount_currency or line.amount
line_currency = line.foreign_currency_id or line.currency_id
# Search for applicable rule.
# /!\ BREAK are very important here to avoid applying multiple rules on the same line.
@@ -760,12 +764,14 @@ class AccountReconcileModel(models.Model):
or third_batch_candidates + third_batch_candidates_proposed)
# Special case: the amount are the same, submit the line directly.
for c in available_candidates:
residual_amount = c['aml_currency_id'] and c['aml_amount_residual_currency'] or c['aml_amount_residual']
candidate_currencies = set(candidate['aml_currency_id'] or line.company_currency_id.id for candidate in available_candidates)
if candidate_currencies == {line_currency.id}:
for c in available_candidates:
residual_amount = c['aml_currency_id'] and c['aml_amount_residual_currency'] or c['aml_amount_residual']
if float_is_zero(residual_amount - line_residual, precision_rounding=line_currency.rounding):
available_candidates = [c]
break
if float_is_zero(residual_amount + line.amount_residual, precision_rounding=line_currency.rounding):
available_candidates = [c]
break
# Needed to handle check on total residual amounts.
if first_batch_candidates or first_batch_candidates_proposed or model._check_rule_propositions(line, available_candidates):
@@ -96,10 +96,12 @@ class TestReconciliationMatchingRules(AccountTestCommon):
})
@classmethod
def _create_invoice_line(cls, amount, partner, type):
def _create_invoice_line(cls, amount, partner, type, currency=None):
''' Create an invoice on the fly.'''
invoice_form = Form(cls.env['account.move'].with_context(default_move_type=type, default_invoice_date='2019-09-01', default_date='2019-09-01'))
invoice_form.partner_id = partner
if currency:
invoice_form.currency_id = currency
with invoice_form.invoice_line_ids.new() as invoice_line_form:
invoice_line_form.name = 'xxxx'
invoice_line_form.quantity = 1
@@ -413,3 +415,19 @@ class TestReconciliationMatchingRules(AccountTestCommon):
bank_line_1.id: {'aml_ids': [payment_bnk_line.id], 'model': self.rule_0}
}
self._check_statement_matching(self.rule_0, expected_values, statements=bank_st)
def test_match_different_currencies(self):
partner = self.env['res.partner'].create({'name': 'Bernard Gagnant'})
self.rule_1.write({'match_partner_ids': [(6, 0, partner.ids)], 'match_same_currency': False})
currency_inv = self.env.ref('base.EUR')
currency_statement = self.env.ref('base.JPY')
currency_statement.active = True
invoice_line = self._create_invoice_line(100, partner, 'out_invoice', currency=currency_inv)
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)