diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 7607712215e..31a0c162023 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -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 # ------------------------------------------------------------------------- diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index c67f471c90d..f0228acd0bd 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -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): diff --git a/addons/account/tests/test_reconciliation_matching_rules.py b/addons/account/tests/test_reconciliation_matching_rules.py index 4133d4c6d98..f9ea3ccd260 100644 --- a/addons/account/tests/test_reconciliation_matching_rules.py +++ b/addons/account/tests/test_reconciliation_matching_rules.py @@ -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) \ No newline at end of file