diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 31a0c162023b7bb7f42688e6d2c2b7f0605432d9..71d6f336dacbf3e8352d19b9c2d8834e31da30e9 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -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: diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index 3630a0557b2e97481b322b5b3206dbe8b178e93a..524082e1b47f5907461a1e65443f959c78351216 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -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 diff --git a/addons/account/tests/test_reconciliation_matching_rules.py b/addons/account/tests/test_reconciliation_matching_rules.py index 3688d47cfb4da9c823d9321528137675be233941..15c86f2ea2b0aa2db4df4d8e551951afbbcc4fb8 100644 --- a/addons/account/tests/test_reconciliation_matching_rules.py +++ b/addons/account/tests/test_reconciliation_matching_rules.py @@ -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)