[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:
@@ -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)
|
||||
Reference in New Issue
Block a user