diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/account/tests/test_reconciliation_matching_rules.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/account/tests/test_reconciliation_matching_rules.py')
| -rw-r--r-- | addons/account/tests/test_reconciliation_matching_rules.py | 1061 |
1 files changed, 1061 insertions, 0 deletions
diff --git a/addons/account/tests/test_reconciliation_matching_rules.py b/addons/account/tests/test_reconciliation_matching_rules.py new file mode 100644 index 00000000..ffe18d63 --- /dev/null +++ b/addons/account/tests/test_reconciliation_matching_rules.py @@ -0,0 +1,1061 @@ +# -*- coding: utf-8 -*- +from freezegun import freeze_time + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.tests.common import Form +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestReconciliationMatchingRules(AccountTestInvoicingCommon): + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + ################# + # Company setup # + ################# + 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'] + cls.current_assets_account = cls.env['account.account'].search([ + ('user_type_id', '=', cls.env.ref('account.data_account_type_current_assets').id), + ('company_id', '=', cls.company.id)], limit=1) + + cls.bank_journal = cls.env['account.journal'].search([('type', '=', 'bank'), ('company_id', '=', cls.company.id)], limit=1) + cls.cash_journal = cls.env['account.journal'].search([('type', '=', 'cash'), ('company_id', '=', cls.company.id)], limit=1) + + cls.tax21 = cls.env['account.tax'].create({ + 'name': '21%', + 'type_tax_use': 'purchase', + 'amount': 21, + }) + + cls.tax12 = cls.env['account.tax'].create({ + 'name': '12%', + 'type_tax_use': 'purchase', + 'amount': 12, + }) + + cls.partner_1 = cls.env['res.partner'].create({'name': 'partner_1', 'company_id': cls.company.id}) + cls.partner_2 = cls.env['res.partner'].create({'name': 'partner_2', 'company_id': cls.company.id}) + cls.partner_3 = cls.env['res.partner'].create({'name': 'partner_3', 'company_id': cls.company.id}) + + ############### + # Rules setup # + ############### + cls.rule_1 = cls.env['account.reconcile.model'].create({ + 'name': 'Invoices Matching Rule', + 'sequence': '1', + 'rule_type': 'invoice_matching', + 'auto_reconcile': False, + 'match_nature': 'both', + 'match_same_currency': True, + 'match_total_amount': True, + 'match_total_amount_param': 100, + 'match_partner': True, + 'match_partner_ids': [(6, 0, (cls.partner_1 + cls.partner_2 + cls.partner_3).ids)], + 'company_id': cls.company.id, + 'line_ids': [(0, 0, {'account_id': cls.current_assets_account.id})], + }) + cls.rule_2 = cls.env['account.reconcile.model'].create({ + 'name': 'write-off model', + 'rule_type': 'writeoff_suggestion', + 'match_partner': True, + 'match_partner_ids': [], + 'line_ids': [(0, 0, {'account_id': cls.current_assets_account.id})], + }) + + ################## + # Invoices setup # + ################## + cls.invoice_line_1 = cls._create_invoice_line(100, cls.partner_1, 'out_invoice') + cls.invoice_line_2 = cls._create_invoice_line(200, cls.partner_1, 'out_invoice') + cls.invoice_line_3 = cls._create_invoice_line(300, cls.partner_1, 'in_refund', name="RBILL/2019/09/0013") + cls.invoice_line_4 = cls._create_invoice_line(1000, cls.partner_2, 'in_invoice') + cls.invoice_line_5 = cls._create_invoice_line(600, cls.partner_3, 'out_invoice') + cls.invoice_line_6 = cls._create_invoice_line(600, cls.partner_3, 'out_invoice', ref="RF12 3456") + cls.invoice_line_7 = cls._create_invoice_line(200, cls.partner_3, 'out_invoice', pay_reference="RF12 3456") + + #################### + # Statements setup # + #################### + # TODO : account_number, partner_name, transaction_type, narration + invoice_number = cls.invoice_line_1.move_id.name + cls.bank_st, cls.bank_st_2, cls.cash_st = cls.env['account.bank.statement'].create([ + { + 'name': 'test bank journal', + 'journal_id': cls.bank_journal.id, + 'line_ids': [ + (0, 0, { + 'date': '2020-01-01', + 'payment_ref': 'invoice %s-%s-%s' % tuple(invoice_number.split('/')[1:]), + 'partner_id': cls.partner_1.id, + 'amount': 100, + 'sequence': 1, + }), + (0, 0, { + 'date': '2020-01-01', + 'payment_ref': 'xxxxx', + 'partner_id': cls.partner_1.id, + 'amount': 600, + 'sequence': 2, + }), + ], + }, { + 'name': 'second test bank journal', + 'journal_id': cls.bank_journal.id, + 'line_ids': [ + (0, 0, { + 'date': '2020-01-01', + 'payment_ref': 'nawak', + 'narration': 'Communication: RF12 3456', + 'partner_id': cls.partner_3.id, + 'amount': 600, + 'sequence': 1, + }), + (0, 0, { + 'date': '2020-01-01', + 'payment_ref': 'RF12 3456', + 'partner_id': cls.partner_3.id, + 'amount': 600, + 'sequence': 2, + }), + (0, 0, { + 'date': '2020-01-01', + 'payment_ref': 'baaaaah', + 'ref': 'RF12 3456', + 'partner_id': cls.partner_3.id, + 'amount': 600, + 'sequence': 2, + }), + ], + }, { + 'name': 'test cash journal', + 'journal_id': cls.cash_journal.id, + 'line_ids': [ + (0, 0, { + 'date': '2020-01-01', + 'payment_ref': 'yyyyy', + 'partner_id': cls.partner_2.id, + 'amount': -1000, + 'sequence': 1, + }), + ], + } + ]) + + cls.bank_line_1, cls.bank_line_2 = cls.bank_st.line_ids + cls.bank_line_3, cls.bank_line_4, cls.bank_line_5 = cls.bank_st_2.line_ids + cls.cash_line_1 = cls.cash_st.line_ids + cls._post_statements(cls) + + @classmethod + def _create_invoice_line(cls, amount, partner, type, currency=None, pay_reference=None, ref=None, name=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 + if pay_reference: + invoice_form.payment_reference = pay_reference + if ref: + invoice_form.ref = ref + if name: + invoice_form.name = name + with invoice_form.invoice_line_ids.new() as invoice_line_form: + invoice_line_form.name = 'xxxx' + invoice_line_form.quantity = 1 + invoice_line_form.price_unit = amount + invoice_line_form.tax_ids.clear() + invoice = invoice_form.save() + invoice.action_post() + lines = invoice.line_ids + return lines.filtered(lambda l: l.account_id.user_type_id.type in ('receivable', 'payable')) + + def _post_statements(self): + self.bank_st.balance_end_real = self.bank_st.balance_end + self.bank_st_2.balance_end_real = self.bank_st_2.balance_end + self.cash_st.balance_end_real = self.cash_st.balance_end + (self.bank_st + self.bank_st_2 + self.cash_st).button_post() + + @freeze_time('2020-01-01') + def _check_statement_matching(self, rules, expected_values, statements=None): + if statements is None: + statements = self.bank_st + self.cash_st + statement_lines = statements.mapped('line_ids').sorted() + matching_values = rules._apply_rules(statement_lines, None) + + for st_line_id, values in matching_values.items(): + values.pop('reconciled_lines', None) + values.pop('write_off_vals', None) + self.assertDictEqual(values, expected_values[st_line_id]) + + def test_matching_fields(self): + # Check without restriction. + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': [ + self.invoice_line_2.id, + self.invoice_line_3.id, + self.invoice_line_1.id, + ], 'model': self.rule_1, + 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + + def test_matching_fields_match_text_location(self): + self.rule_1.match_text_location_label = True + self.rule_1.match_text_location_reference = False + self.rule_1.match_text_location_note = False + self._check_statement_matching(self.rule_1, { + self.bank_line_3.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id}, + self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id}, + self.bank_line_5.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id}, + }, statements=self.bank_st_2) + + self.rule_1.match_text_location_label = True + self.rule_1.match_text_location_reference = False + self.rule_1.match_text_location_note = True + self._check_statement_matching(self.rule_1, { + self.bank_line_3.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id}, + self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id}, + self.bank_line_5.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id}, + }, statements=self.bank_st_2) + + self.rule_1.match_text_location_label = True + self.rule_1.match_text_location_reference = True + self.rule_1.match_text_location_note = False + self._check_statement_matching(self.rule_1, { + self.bank_line_3.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id}, + self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id}, + self.bank_line_5.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id}, + }, statements=self.bank_st_2) + + self.rule_1.match_text_location_label = True + self.rule_1.match_text_location_reference = True + self.rule_1.match_text_location_note = True + self._check_statement_matching(self.rule_1, { + self.bank_line_3.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id}, + self.bank_line_4.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id}, + self.bank_line_5.id: {'aml_ids': [self.invoice_line_7.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id}, + }, statements=self.bank_st_2) + + self.rule_1.match_text_location_label = False + self.rule_1.match_text_location_reference = False + self.rule_1.match_text_location_note = False + self._check_statement_matching(self.rule_1, { + self.bank_line_3.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_3.partner_id}, + self.bank_line_4.id: {'aml_ids': [self.invoice_line_5.id], 'model': self.rule_1, 'partner': self.bank_line_4.partner_id}, + self.bank_line_5.id: {'aml_ids': [self.invoice_line_6.id], 'model': self.rule_1, 'partner': self.bank_line_5.partner_id}, + }, statements=self.bank_st_2) + + def test_matching_fields_match_journal_ids(self): + self.rule_1.match_journal_ids |= self.cash_st.journal_id + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': []}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_journal_ids |= self.bank_st.journal_id + self.cash_st.journal_id + + def test_matching_fields_match_nature(self): + self.rule_1.match_nature = 'amount_received' + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': [ + self.invoice_line_2.id, + self.invoice_line_3.id, + self.invoice_line_1.id, + ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': []}, + }) + self.rule_1.match_nature = 'amount_paid' + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': []}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_nature = 'both' + + def test_matching_fields_match_amount(self): + self.rule_1.match_amount = 'lower' + self.rule_1.match_amount_max = 150 + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': []}, + self.cash_line_1.id: {'aml_ids': []}, + }) + self.rule_1.match_amount = 'greater' + self.rule_1.match_amount_min = 200 + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': [ + self.invoice_line_1.id, + self.invoice_line_2.id, + self.invoice_line_3.id, + ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_amount = 'between' + self.rule_1.match_amount_min = 200 + self.rule_1.match_amount_max = 800 + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': [ + self.invoice_line_1.id, + self.invoice_line_2.id, + self.invoice_line_3.id, + ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': []}, + }) + self.rule_1.match_amount = False + + def test_matching_fields_match_label(self): + self.rule_1.match_label = 'contains' + self.rule_1.match_label_param = 'yyyyy' + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': []}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_label = 'not_contains' + self.rule_1.match_label_param = 'xxxxx' + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': []}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_label = 'match_regex' + self.rule_1.match_label_param = 'xxxxx|yyyyy' + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': [ + self.invoice_line_1.id, + self.invoice_line_2.id, + self.invoice_line_3.id, + ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_label = False + + def test_matching_fields_match_total_amount(self): + # Check match_total_amount: line amount >= total residual amount. + self.rule_1.match_total_amount_param = 90.0 + self.bank_line_1.amount += 10 + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'write_off', 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': [ + self.invoice_line_2.id, + self.invoice_line_3.id, + self.invoice_line_1.id, + ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_total_amount_param = 100.0 + self.bank_line_1.amount -= 10 + + # Check match_total_amount: line amount <= total residual amount. + self.rule_1.match_total_amount_param = 90.0 + self.bank_line_1.amount -= 10 + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'write_off', 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': [ + self.invoice_line_2.id, + self.invoice_line_3.id, + self.invoice_line_1.id, + ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_total_amount_param = 100.0 + self.bank_line_1.amount += 10 + + # Check match_total_amount: line amount >= total residual amount, match_total_amount_param just not matched. + self.rule_1.match_total_amount_param = 90.0 + self.bank_line_1.amount += 10.01 + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': [ + self.invoice_line_1.id, + self.invoice_line_2.id, + self.invoice_line_3.id, + ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_total_amount_param = 100.0 + self.bank_line_1.amount -= 10.01 + + # Check match_total_amount: line amount <= total residual amount, match_total_amount_param just not matched. + self.rule_1.match_total_amount_param = 90.0 + self.bank_line_1.amount -= 10.01 + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': [ + self.invoice_line_1.id, + self.invoice_line_2.id, + self.invoice_line_3.id, + ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_total_amount_param = 100.0 + self.bank_line_1.amount += 10.01 + + def test_matching_fields_match_partner_category_ids(self): + test_category = self.env['res.partner.category'].create({'name': 'Consulting Services'}) + self.partner_2.category_id = test_category + self.rule_1.match_partner_category_ids |= test_category + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': []}, + self.bank_line_2.id: {'aml_ids': []}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + self.rule_1.match_partner_category_ids = False + + def test_mixin_rules(self): + ''' Test usage of rules together.''' + # rule_1 is used before rule_2. + self.rule_1.sequence = 1 + self.rule_2.sequence = 2 + + self._check_statement_matching(self.rule_1 + self.rule_2, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': [ + self.invoice_line_2.id, + self.invoice_line_3.id, + self.invoice_line_1.id, + ], 'model': self.rule_1, 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + + # rule_2 is used before rule_1. + self.rule_1.sequence = 2 + self.rule_2.sequence = 1 + + self._check_statement_matching(self.rule_1 + self.rule_2, { + self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.cash_line_1.partner_id}, + }) + + # rule_2 is used before rule_1 but only on partner_1. + self.rule_2.match_partner_ids |= self.partner_1 + + self._check_statement_matching(self.rule_1 + self.rule_2, { + self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'write_off', 'partner': self.bank_line_2.partner_id}, + self.cash_line_1.id: {'aml_ids': [self.invoice_line_4.id], 'model': self.rule_1, 'partner': self.cash_line_1.partner_id}, + }) + + def test_auto_reconcile(self): + ''' Test auto reconciliation.''' + self.bank_line_1.amount += 5 + + self.rule_1.sequence = 2 + self.rule_1.auto_reconcile = True + self.rule_1.match_total_amount_param = 90 + self.rule_2.sequence = 1 + self.rule_2.match_partner_ids |= self.partner_2 + self.rule_2.auto_reconcile = True + + self._check_statement_matching(self.rule_1 + self.rule_2, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': []}, + self.cash_line_1.id: {'aml_ids': [], 'model': self.rule_2, 'status': 'reconciled', 'partner': self.cash_line_1.partner_id}, + }) + + # Check first line has been well reconciled. + self.assertRecordValues(self.bank_line_1.line_ids, [ + {'partner_id': self.partner_1.id, 'debit': 105.0, 'credit': 0.0}, + {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 5.0}, + {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0}, + ]) + + # Check second line has been well reconciled. + self.assertRecordValues(self.cash_line_1.line_ids, [ + {'partner_id': self.partner_2.id, 'debit': 0.0, 'credit': 1000.0}, + {'partner_id': self.partner_2.id, 'debit': 1000.0, 'credit': 0.0}, + ]) + + def test_larger_invoice_auto_reconcile(self): + ''' Test auto reconciliation with an invoice with larger amount than the + statement line's, for rules without write-offs.''' + self.bank_line_1.amount = 40 + self.invoice_line_1.move_id.payment_reference = self.bank_line_1.payment_ref + + self.rule_1.sequence = 2 + self.rule_1.auto_reconcile = True + self.rule_1.line_ids = [(5, 0, 0)] + + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': []}, + }, statements=self.bank_st) + + # Check first line has been well reconciled. + self.assertRecordValues(self.bank_line_1.line_ids, [ + {'partner_id': self.partner_1.id, 'debit': 40.0, 'credit': 0.0}, + {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 40.0}, + ]) + + self.assertEqual(self.invoice_line_1.amount_residual, 60.0, "The invoice should have been partially reconciled") + + def test_auto_reconcile_with_duplicate_match(self): + """ If multiple bank statement lines match with the same invoice, ensure the + correct line is auto-validated and no crashing happens. + """ + + # Only the invoice defined in this test should have this partner + partner = self.env['res.partner'].create({'name': "The Only One"}) + invoice_line = self._create_invoice_line( + 2000, partner, 'out_invoice', ref="REF 7788") + + # Enable auto-validation and don't restrict the partners that can be matched on + # so our newly created partner can be matched. + self.rule_1.write({ + 'auto_reconcile': True, + 'match_partner_ids': [(5, 0, 0)], + }) + self.rule_1.match_partner_ids = [] + + # This line has a matching payment reference and the exact amount of the + # invoice. As a result it should auto-validate. + self.bank_line_1.amount = 2000 + self.bank_line_1.partner_id = partner + self.bank_line_1.payment_ref = "REF 7788" + + # This line doesn't have a matching amount or reference, but it does have a + # matching partner. + self.bank_line_2.amount = 1800 + self.bank_line_2.partner_id = partner + self.bank_line_2.payment_ref = "something" + + # Verify the auto-validation happens with the first line, and no exceptions. + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: { + 'aml_ids': [invoice_line.id], + 'model': self.rule_1, + 'status': 'reconciled', + 'partner': self.bank_line_1.partner_id + }, + self.bank_line_2.id: { + 'aml_ids': [] + }, + }, statements=self.bank_st) + + def test_auto_reconcile_with_tax(self): + ''' Test auto reconciliation with a tax amount included in the bank statement line''' + self.rule_1.write({ + 'auto_reconcile': True, + 'rule_type': 'writeoff_suggestion', + 'line_ids': [(1, self.rule_1.line_ids.id, { + 'amount': 50, + 'force_tax_included': True, + 'tax_ids': [(6, 0, self.tax21.ids)], + }), (0, 0, { + 'amount': 100, + 'force_tax_included': False, + 'tax_ids': [(6, 0, self.tax12.ids)], + 'account_id': self.current_assets_account.id, + })] + }) + + self.bank_line_1.amount = -121 + + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': [], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_2.partner_id}, + }, statements=self.bank_st) + + # Check first line has been well reconciled. + self.assertRecordValues(self.bank_line_1.line_ids, [ + {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 121.0, 'tax_ids': [], 'tax_line_id': False}, + {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 7.26, 'tax_ids': [], 'tax_line_id': False}, + {'partner_id': self.partner_1.id, 'debit': 50.0, 'credit': 0.0, 'tax_ids': [self.tax21.id], 'tax_line_id': False}, + {'partner_id': self.partner_1.id, 'debit': 10.5, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.tax21.id}, + {'partner_id': self.partner_1.id, 'debit': 60.5, 'credit': 0.0, 'tax_ids': [self.tax12.id], 'tax_line_id': False}, + {'partner_id': self.partner_1.id, 'debit': 7.26, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.tax12.id}, + ]) + + def test_reverted_move_matching(self): + partner = self.partner_1 + AccountMove = self.env['account.move'] + move = AccountMove.create({ + 'journal_id': self.bank_journal.id, + 'line_ids': [ + (0, 0, { + 'account_id': self.account_pay.id, + 'partner_id': partner.id, + 'name': 'One of these days', + 'debit': 10, + }), + (0, 0, { + 'account_id': self.bank_journal.payment_credit_account_id.id, + 'partner_id': partner.id, + 'name': 'I\'m gonna cut you into little pieces', + 'credit': 10, + }) + ], + }) + + payment_bnk_line = move.line_ids.filtered(lambda l: l.account_id == self.bank_journal.payment_credit_account_id) + + move.action_post() + move_reversed = move._reverse_moves() + self.assertTrue(move_reversed.exists()) + + self.bank_line_1.write({ + 'payment_ref': '8', + 'partner_id': partner.id, + 'amount': -10, + }) + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [payment_bnk_line.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': []}, + }, statements=self.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_1.write({'partner_id': partner.id, 'foreign_currency_id': currency_statement.id, 'amount_currency': 100, 'payment_ref': 'test'}) + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': invoice_line.ids, 'model': self.rule_1, 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': []}, + }, 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 + right invoice if they have the same payment reference. + """ + self.invoice_line_1.move_id.write({'payment_reference': 'Tournicoti66'}) + + self.bank_line_1.write({ + 'payment_ref': 'Tournicoti66', + 'partner_id': None, + 'amount': 95, + }) + + self.rule_1.write({ + 'line_ids': [(5, 0, 0)], + 'match_partner': False, + 'match_label': 'contains', + 'match_label_param': 'Tournicoti', # So that we only match what we want to test + }) + + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': []}, + }, self.bank_st) + + def test_inv_matching_rule_auto_rec_no_partner_with_writeoff(self): + self.invoice_line_1.move_id.write({'payment_reference': 'doudlidou355'}) + + self.bank_line_1.write({ + 'payment_ref': 'doudlidou355', + 'partner_id': None, + 'amount': 95, + }) + + self.rule_1.write({ + 'match_partner': False, + 'match_label': 'contains', + 'match_label_param': 'doudlidou', # So that we only match what we want to test + 'match_total_amount_param': 90, + 'auto_reconcile': True, + }) + + # Check bank reconciliation + + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id, 'status': 'reconciled'}, + self.bank_line_2.id: {'aml_ids': []}, + }, self.bank_st) + + # Check invoice line has been fully reconciled, with a write-off. + self.assertRecordValues(self.bank_line_1.line_ids, [ + {'partner_id': self.partner_1.id, 'debit': 95.0, 'credit': 0.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False}, + {'partner_id': self.partner_1.id, 'debit': 5.0, 'credit': 0.0, 'account_id': self.current_assets_account.id, 'reconciled': False}, + {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0, 'account_id': self.invoice_line_1.account_id.id, 'reconciled': True}, + ]) + + self.assertEqual(self.invoice_line_1.amount_residual, 0.0, "The invoice should have been fully reconciled") + + def test_partner_mapping_rule(self): + self.bank_line_1.write({'partner_id': None, 'payment_ref': 'toto42', 'narration': None}) + self.bank_line_2.write({'partner_id': None}) + + # Do the test for both rule 1 and 2, so that we check invoice matching and write-off rules + for rule in (self.rule_1 + self.rule_2): + + # To cope for minor differences in rule results + matching_amls = rule.rule_type == 'invoice_matching' and self.invoice_line_1.ids or [] + result_status = rule.rule_type == 'writeoff_suggestion' and {'status': 'write_off'} or {} + + match_result = {**result_status, 'aml_ids': matching_amls, 'model': rule, 'partner': self.partner_1} + no_match_result = {'aml_ids': []} + + # Without mapping, there should be no match + self._check_statement_matching(rule, { + self.bank_line_1.id: no_match_result, + self.bank_line_2.id: no_match_result, + }, self.bank_st) + + # We add some mapping for payment reference to rule_1 + rule.write({ + 'partner_mapping_line_ids': [(0, 0, { + 'partner_id': self.partner_1.id, + 'payment_ref_regex': 'toto.*', + })] + }) + + # bank_line_1 should now match + self._check_statement_matching(rule, { + self.bank_line_1.id: match_result, + self.bank_line_2.id: no_match_result, + }, self.bank_st) + + # If we now add a narration regex to the same mapping line, nothing should match + rule.partner_mapping_line_ids.write({'narration_regex': ".*coincoin"}) + self.bank_line_1.write({'narration': None}) # Reset from possible previous iteration + + self._check_statement_matching(rule, { + self.bank_line_1.id: no_match_result, + self.bank_line_2.id: no_match_result, + }, self.bank_st) + + # If we set the narration so that it matches the new mapping criterium, line_1 matches + self.bank_line_1.write({'narration': "42coincoin"}) + + self._check_statement_matching(rule, { + self.bank_line_1.id: match_result, + self.bank_line_2.id: no_match_result, + }, self.bank_st) + + def test_partner_name_in_communication(self): + self.invoice_line_1.partner_id.write({'name': "Archibald Haddock"}) + self.bank_line_1.write({'partner_id': None, 'payment_ref': '1234//HADDOCK-Archibald'}) + self.bank_line_2.write({'partner_id': None}) + self.rule_1.write({'match_partner': False}) + + # bank_line_1 should match, as its communication contains the invoice's partner name + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': []}, + }, self.bank_st) + + def test_partner_name_with_regexp_chars(self): + self.invoice_line_1.partner_id.write({'name': "Archibald + Haddock"}) + self.bank_line_1.write({'partner_id': None, 'payment_ref': '1234//HADDOCK+Archibald'}) + self.bank_line_2.write({'partner_id': None}) + self.rule_1.write({'match_partner': False}) + + # The query should still work + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': []}, + }, 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, + 'past_months_limit': False, + }) + + 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_misc'].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.action_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._check_statement_matching(matching_rule, { + statement_line.id: {'aml_ids': (move_line_1 + move_line_2).ids, 'model': matching_rule, 'partner': statement_line.partner_id} + }, statements=statement) + + @freeze_time('2020-01-01') + def test_inv_matching_with_write_off(self): + self.rule_1.match_total_amount_param = 90 + self.bank_st.line_ids[1].unlink() # We don't need this one here + statement_line = self.bank_st.line_ids[0] + statement_line.write({ + 'payment_ref': self.invoice_line_1.move_id.payment_reference, + 'amount': 90, + }) + + # Test the invoice-matching part + self._check_statement_matching(self.rule_1, { + statement_line.id: {'aml_ids': self.invoice_line_1.ids, 'model': self.rule_1, 'partner': self.invoice_line_1.partner_id, 'status': 'write_off'}, + }, self.bank_st) + + # Test the write-off part + expected_write_off = { + 'balance': 10, + 'currency_id': False, + 'reconcile_model_id': self.rule_1.id, + 'account_id': self.current_assets_account.id, + } + + matching_result = self.rule_1._apply_rules(statement_line) + + self.assertEqual(len(matching_result[statement_line.id].get('write_off_vals', [])), 1, "Exactly one write-off line should be proposed.") + + full_write_off_dict = matching_result[statement_line.id]['write_off_vals'][0] + to_compare = { + key: full_write_off_dict[key] + for key in expected_write_off.keys() + } + + self.assertDictEqual(expected_write_off, to_compare) + + def test_inv_matching_with_write_off_autoreconcile(self): + self.bank_line_1.amount = 95 + + self.rule_1.sequence = 2 + self.rule_1.auto_reconcile = True + self.rule_1.match_total_amount_param = 90 + + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'reconciled', 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': []}, + }, statements=self.bank_st) + + # Check first line has been properly reconciled. + self.assertRecordValues(self.bank_line_1.line_ids, [ + {'partner_id': self.partner_1.id, 'debit': 95.0, 'credit': 0.0, 'account_id': self.bank_journal.default_account_id.id, 'reconciled': False}, + {'partner_id': self.partner_1.id, 'debit': 5.0, 'credit': 0.0, 'account_id': self.current_assets_account.id, 'reconciled': False}, + {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 100.0, 'account_id': self.invoice_line_1.account_id.id, 'reconciled': True}, + ]) + + self.assertEqual(self.invoice_line_1.amount_residual, 0.0, "The invoice should have been fully reconciled") + + def test_avoid_amount_matching_bypass(self): + """ By the default, if the label of statement lines exactly matches a payment reference, it bypasses any kind of amount verification. + This is annoying in some setups, so a config parameter was introduced to handle that. + """ + self.env['ir.config_parameter'].set_param('account.disable_rec_models_bypass', '1') + self.rule_1.match_total_amount_param = 90 + second_inv_matching_rule = self.env['account.reconcile.model'].create({ + 'name': 'Invoices Matching Rule', + 'sequence': 2, + 'rule_type': 'invoice_matching', + 'auto_reconcile': False, + 'match_nature': 'both', + 'match_same_currency': False, + 'match_total_amount': False, + 'match_partner': True, + 'company_id': self.company.id, + }) + + self.bank_line_1.write({ + 'payment_ref': self.invoice_line_1.move_id.payment_reference, + 'amount': 99, + }) + self.bank_line_2.write({ + 'payment_ref': self.invoice_line_2.move_id.payment_reference, + 'amount': 1, + }) + + self._check_statement_matching(self.rule_1 + second_inv_matching_rule, { + self.bank_line_1.id: {'aml_ids': [self.invoice_line_1.id], 'model': self.rule_1, 'status': 'write_off', 'partner': self.bank_line_1.partner_id}, + self.bank_line_2.id: {'aml_ids': [self.invoice_line_2.id], 'model': second_inv_matching_rule, 'partner': self.bank_line_2.partner_id} + }, statements=self.bank_st) + + def test_payment_similar_communications(self): + def create_payment_line(amount, memo, partner): + payment = self.env['account.payment'].create({ + 'amount': amount, + 'payment_type': 'inbound', + 'partner_type': 'customer', + 'partner_id': partner.id, + 'ref': memo, + 'destination_account_id': self.company_data['default_account_receivable'].id, + }) + payment.action_post() + + return payment.line_ids.filtered(lambda x: x.account_id.user_type_id.type not in {'receivable', 'payable'}) + + payment_partner = self.env['res.partner'].create({ + 'name': "Bernard Gagnant", + }) + + self.rule_1.match_partner_ids = [(6, 0, payment_partner.ids)] + + pmt_line_1 = create_payment_line(500, 'a1b2c3', payment_partner) + pmt_line_2 = create_payment_line(500, 'a1b2c3', payment_partner) + pmt_line_3 = create_payment_line(500, 'd1e2f3', payment_partner) + + self.bank_line_1.write({ + 'amount': 1000, + 'payment_ref': 'a1b2c3', + 'partner_id': payment_partner.id, + }) + self.bank_line_2.unlink() + self.rule_1.match_total_amount = False + + self._check_statement_matching(self.rule_1, { + self.bank_line_1.id: {'aml_ids': (pmt_line_1 + pmt_line_2).ids, 'model': self.rule_1, 'partner': payment_partner}, + }, statements=self.bank_line_1.statement_id) + + def test_tax_tags_inversion_with_reconciliation_model(self): + country = self.env.ref('base.us') + tax_report = self.env['account.tax.report'].create({ + 'name': "Tax report", + 'country_id': country.id, + }) + tax_report_line = self.env['account.tax.report.line'].create({ + 'name': 'test_tax_report_line', + 'tag_name': 'test_tax_report_line', + 'report_id': tax_report.id, + 'sequence': 10, + }) + tax_tag_pos = tax_report_line.tag_ids.filtered(lambda x: not x.tax_negate) + tax_tag_neg = tax_report_line.tag_ids.filtered(lambda x: x.tax_negate) + tax = self.env['account.tax'].create({ + 'name': 'Test Tax', + 'amount': 10, + 'invoice_repartition_line_ids': [ + (0, 0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [(6, 0, tax_tag_pos.ids)], + }), + (0, 0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'tag_ids': [(6, 0, tax_tag_neg.ids)], + }), + ], + 'refund_repartition_line_ids': [ + (0, 0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [(6, 0, tax_tag_neg.ids)], + }), + (0, 0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'tag_ids': [(6, 0, tax_tag_pos.ids)], + }), + ], + }) + reconciliation_model = self.env['account.reconcile.model'].create({ + 'name': 'Charge with Tax', + 'line_ids': [(0, 0, { + 'account_id': self.company_data['default_account_expense'].id, + 'amount_type': 'percentage', + 'amount_string': '100', + 'tax_ids': [(6, 0, tax.ids)] + })] + }) + bank_stmt = self.env['account.bank.statement'].create({ + 'journal_id': self.bank_journal.id, + 'date': '2020-07-15', + 'name': 'test', + 'line_ids': [(0, 0, { + 'payment_ref': 'testLine', + 'amount': 5, + 'date': '2020-07-15', + })] + }) + res = reconciliation_model._get_write_off_move_lines_dict(bank_stmt.line_ids, 5) + self.assertEqual(len(res), 2) + self.assertEqual( + res[0]['tax_tag_ids'], + [(6, 0, tax.refund_repartition_line_ids[0].tag_ids.ids)], + 'The tags of the first repartition line are not inverted' + ) + self.assertEqual( + res[1]['tax_tag_ids'], + [(6, 0, tax.refund_repartition_line_ids[1].tag_ids.ids)], + 'The tags of the second repartition line are not inverted' + ) |
