1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123 |
- # -*- coding: utf-8 -*-
- from freezegun import freeze_time
- from contextlib import contextmanager
- from odoo.addons.account.tests.common import AccountTestInvoicingCommon
- from odoo.tests.common import Form
- from odoo.tests import tagged
- from odoo import Command
- @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([
- ('account_type', '=', 'asset_current'),
- ('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,
- 'allow_payment_tolerance': True,
- 'payment_tolerance_type': 'percentage',
- 'payment_tolerance_param': 0.0,
- '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_line_1, cls.bank_line_2,\
- cls.bank_line_3, cls.bank_line_4,\
- cls.bank_line_5, cls.cash_line_1 = cls.env['account.bank.statement.line'].create([
- {
- 'journal_id': cls.bank_journal.id,
- 'date': '2020-01-01',
- 'payment_ref': 'invoice %s-%s' % tuple(invoice_number.split('/')[1:]),
- 'partner_id': cls.partner_1.id,
- 'amount': 100,
- 'sequence': 1,
- },
- {
- 'journal_id': cls.bank_journal.id,
- 'date': '2020-01-01',
- 'payment_ref': 'xxxxx',
- 'partner_id': cls.partner_1.id,
- 'amount': 600,
- 'sequence': 2,
- },
- {
- 'journal_id': cls.bank_journal.id,
- 'date': '2020-01-01',
- 'payment_ref': 'nawak',
- 'narration': 'Communication: RF12 3456',
- 'partner_id': cls.partner_3.id,
- 'amount': 600,
- 'sequence': 1,
- },
- {
- 'journal_id': cls.bank_journal.id,
- 'date': '2020-01-01',
- 'payment_ref': 'RF12 3456',
- 'partner_id': cls.partner_3.id,
- 'amount': 600,
- 'sequence': 2,
- },
- {
- 'journal_id': cls.bank_journal.id,
- 'date': '2020-01-01',
- 'payment_ref': 'baaaaah',
- 'ref': 'RF12 3456',
- 'partner_id': cls.partner_3.id,
- 'amount': 600,
- 'sequence': 2,
- },
- {
- 'journal_id': cls.cash_journal.id,
- 'date': '2020-01-01',
- 'payment_ref': 'yyyyy',
- 'partner_id': cls.partner_2.id,
- 'amount': -1000,
- 'sequence': 1,
- },
- ])
- @classmethod
- def _create_invoice_line(cls, amount, partner, move_type, currency=None, pay_reference=None, ref=None, name=None, inv_date='2019-09-01'):
- ''' Create an invoice on the fly.'''
- invoice_form = Form(cls.env['account.move'].with_context(default_move_type=move_type, default_invoice_date=inv_date, default_date=inv_date))
- 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.account_type in ('asset_receivable', 'liability_payable'))
- @classmethod
- def _create_st_line(cls, amount=1000.0, date='2019-01-01', payment_ref='turlututu', **kwargs):
- st_line = cls.env['account.bank.statement.line'].create({
- 'journal_id': kwargs.get('journal_id', cls.bank_journal.id),
- 'amount': amount,
- 'date': date,
- 'payment_ref': payment_ref,
- 'partner_id': cls.partner_a.id,
- **kwargs,
- })
- return st_line
- @classmethod
- def _create_reconcile_model(cls, **kwargs):
- return cls.env['account.reconcile.model'].create({
- 'name': "test",
- 'rule_type': 'invoice_matching',
- 'allow_payment_tolerance': True,
- 'payment_tolerance_type': 'percentage',
- 'payment_tolerance_param': 0.0,
- **kwargs,
- 'line_ids': [
- Command.create({
- 'account_id': cls.company_data['default_account_revenue'].id,
- 'amount_type': 'percentage',
- 'label': f"test {i}",
- **line_vals,
- })
- for i, line_vals in enumerate(kwargs.get('line_ids', []))
- ],
- 'partner_mapping_line_ids': [
- Command.create(line_vals)
- for i, line_vals in enumerate(kwargs.get('partner_mapping_line_ids', []))
- ],
- })
- @freeze_time('2020-01-01')
- def _check_statement_matching(self, rules, expected_values_list):
- for statement_line, expected_values in expected_values_list.items():
- res = rules._apply_rules(statement_line, statement_line._retrieve_partner())
- self.assertDictEqual(res, expected_values)
- def test_matching_fields(self):
- # Check without restriction.
- self._check_statement_matching(self.rule_1, {
- self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
- self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1},
- self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
- })
- def test_matching_fields_match_journal_ids(self):
- self.rule_1.match_journal_ids |= self.cash_line_1.journal_id
- self._check_statement_matching(self.rule_1, {
- self.bank_line_1: {},
- self.bank_line_2: {},
- self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
- })
- 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: {'amls': self.invoice_line_1, 'model': self.rule_1},
- self.bank_line_2: {
- 'amls': self.invoice_line_2 + self.invoice_line_3 + self.invoice_line_1,
- 'model': self.rule_1,
- },
- self.cash_line_1: {},
- })
- self.rule_1.match_nature = 'amount_paid'
- self._check_statement_matching(self.rule_1, {
- self.bank_line_1: {},
- self.bank_line_2: {},
- self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
- })
- 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: {'amls': self.invoice_line_1, 'model': self.rule_1},
- self.bank_line_2: {},
- self.cash_line_1: {},
- })
- self.rule_1.match_amount = 'greater'
- self.rule_1.match_amount_min = 200
- self._check_statement_matching(self.rule_1, {
- self.bank_line_1: {},
- self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1},
- self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
- })
- 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: {},
- self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1},
- self.cash_line_1: {},
- })
- 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: {},
- self.bank_line_2: {},
- self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
- })
- 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: {'amls': self.invoice_line_1, 'model': self.rule_1},
- self.bank_line_2: {},
- self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
- })
- 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: {},
- self.bank_line_2: {'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3, 'model': self.rule_1},
- self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
- })
- @freeze_time('2019-01-01')
- def test_zero_payment_tolerance(self):
- rule = self._create_reconcile_model(line_ids=[{}])
- for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)):
- invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, inv_date='2019-01-01')
- # Exact matching.
- st_line = self._create_st_line(amount=bsl_sign * 1000.0)
- self._check_statement_matching(
- rule,
- {st_line: {'amls': invl, 'model': rule}},
- )
- # No matching because there is no tolerance.
- st_line = self._create_st_line(amount=bsl_sign * 990.0)
- self._check_statement_matching(
- rule,
- {st_line: {}},
- )
- # The payment amount is higher than the invoice one.
- st_line = self._create_st_line(amount=bsl_sign * 1010.0)
- self._check_statement_matching(
- rule,
- {st_line: {'amls': invl, 'model': rule}},
- )
- @freeze_time('2019-01-01')
- def test_zero_payment_tolerance_auto_reconcile(self):
- rule = self._create_reconcile_model(
- auto_reconcile=True,
- line_ids=[{}],
- )
- for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)):
- invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, pay_reference='123456', inv_date='2019-01-01')
- # No matching because there is no tolerance.
- st_line = self._create_st_line(amount=bsl_sign * 990.0)
- self._check_statement_matching(
- rule,
- {st_line: {}},
- )
- # The payment amount is higher than the invoice one.
- st_line = self._create_st_line(amount=bsl_sign * 1010.0, payment_ref='123456')
- self._check_statement_matching(
- rule,
- {st_line: {'amls': invl, 'model': rule, 'auto_reconcile': True}},
- )
- @freeze_time('2019-01-01')
- def test_not_enough_payment_tolerance(self):
- rule = self._create_reconcile_model(
- payment_tolerance_param=0.5,
- line_ids=[{}],
- )
- for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)):
- with self.subTest(inv_type=inv_type, bsl_sign=bsl_sign):
- invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, inv_date='2019-01-01')
- # No matching because there is no enough tolerance.
- st_line = self._create_st_line(amount=bsl_sign * 990.0)
- self._check_statement_matching(
- rule,
- {st_line: {}},
- )
- # The payment amount is higher than the invoice one.
- # However, since the invoice amount is lower than the payment amount,
- # the tolerance is not checked and the invoice line is matched.
- st_line = self._create_st_line(amount=bsl_sign * 1010.0)
- self._check_statement_matching(
- rule,
- {st_line: {'amls': invl, 'model': rule}},
- )
- @freeze_time('2019-01-01')
- def test_enough_payment_tolerance(self):
- rule = self._create_reconcile_model(
- payment_tolerance_param=1.0,
- line_ids=[{}],
- )
- for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)):
- invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, inv_date='2019-01-01')
- # Enough tolerance to match the invoice line.
- st_line = self._create_st_line(amount=bsl_sign * 990.0)
- self._check_statement_matching(
- rule,
- {st_line: {'amls': invl, 'model': rule, 'status': 'write_off'}},
- )
- # The payment amount is higher than the invoice one.
- # However, since the invoice amount is lower than the payment amount,
- # the tolerance is not checked and the invoice line is matched.
- st_line = self._create_st_line(amount=bsl_sign * 1010.0)
- self._check_statement_matching(
- rule,
- {st_line: {'amls': invl, 'model': rule}},
- )
- @freeze_time('2019-01-01')
- def test_enough_payment_tolerance_auto_reconcile_not_full(self):
- rule = self._create_reconcile_model(
- payment_tolerance_param=1.0,
- auto_reconcile=True,
- line_ids=[{'amount_type': 'percentage_st_line', 'amount_string': '200.0'}],
- )
- for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)):
- invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, pay_reference='123456', inv_date='2019-01-01')
- # Enough tolerance to match the invoice line.
- st_line = self._create_st_line(amount=bsl_sign * 990.0, payment_ref='123456')
- self._check_statement_matching(
- rule,
- {st_line: {'amls': invl, 'model': rule, 'status': 'write_off', 'auto_reconcile': True}},
- )
- @freeze_time('2019-01-01')
- def test_allow_payment_tolerance_lower_amount(self):
- rule = self._create_reconcile_model(line_ids=[{'amount_type': 'percentage_st_line'}])
- for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)):
- invl = self._create_invoice_line(990.0, self.partner_a, inv_type, inv_date='2019-01-01')
- st_line = self._create_st_line(amount=bsl_sign * 1000)
- # Partial reconciliation.
- self._check_statement_matching(
- rule,
- {st_line: {'amls': invl, 'model': rule}},
- )
- @freeze_time('2019-01-01')
- def test_enough_payment_tolerance_auto_reconcile(self):
- rule = self._create_reconcile_model(
- payment_tolerance_param=1.0,
- auto_reconcile=True,
- line_ids=[{}],
- )
- for inv_type, bsl_sign in (('out_invoice', 1), ('in_invoice', -1)):
- invl = self._create_invoice_line(1000.0, self.partner_a, inv_type, pay_reference='123456', inv_date='2019-01-01')
- # Enough tolerance to match the invoice line.
- st_line = self._create_st_line(amount=bsl_sign * 990.0, payment_ref='123456')
- self._check_statement_matching(
- rule,
- {st_line: {
- 'amls': invl,
- 'model': rule,
- 'status': 'write_off',
- 'auto_reconcile': True,
- }},
- )
- @freeze_time('2019-01-01')
- def test_percentage_st_line_auto_reconcile(self):
- rule = self._create_reconcile_model(
- payment_tolerance_param=1.0,
- rule_type='writeoff_suggestion',
- auto_reconcile=True,
- line_ids=[
- {'amount_type': 'percentage_st_line', 'amount_string': '100.0', 'label': 'A'},
- {'amount_type': 'percentage_st_line', 'amount_string': '-100.0', 'label': 'B'},
- {'amount_type': 'percentage_st_line', 'amount_string': '100.0', 'label': 'C'},
- ],
- )
- for bsl_sign in (1, -1):
- st_line = self._create_st_line(amount=bsl_sign * 1000.0)
- self._check_statement_matching(
- rule,
- {st_line: {
- 'model': rule,
- 'status': 'write_off',
- 'auto_reconcile': True,
- }},
- )
- def test_matching_fields_match_partner_category_ids(self):
- test_category = self.env['res.partner.category'].create({'name': 'Consulting Services'})
- test_category2 = self.env['res.partner.category'].create({'name': 'Consulting Services2'})
- self.partner_2.category_id = test_category + test_category2
- self.rule_1.match_partner_category_ids |= test_category
- self._check_statement_matching(self.rule_1, {
- self.bank_line_1: {},
- self.bank_line_2: {},
- self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
- })
- 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: {
- 'amls': self.invoice_line_1,
- 'model': self.rule_1,
- },
- self.bank_line_2: {
- 'amls': self.invoice_line_2 + self.invoice_line_3 + self.invoice_line_1,
- 'model': self.rule_1,
- },
- self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
- })
- # 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: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'},
- self.bank_line_2: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'},
- self.cash_line_1: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'},
- })
- # 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: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'},
- self.bank_line_2: {'model': self.rule_2, 'auto_reconcile': False, 'status': 'write_off'},
- self.cash_line_1: {'amls': self.invoice_line_4, 'model': self.rule_1},
- })
- 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.payment_tolerance_param = 10.0
- 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: {
- 'amls': self.invoice_line_1,
- 'model': self.rule_1,
- 'auto_reconcile': True,
- },
- self.bank_line_2: {
- 'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3,
- 'model': self.rule_1,
- },
- self.cash_line_1: {
- 'model': self.rule_2,
- 'status': 'write_off',
- 'auto_reconcile': True,
- },
- })
- 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.allow_payment_tolerance = False
- 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: {
- 'amls': self.invoice_line_1,
- 'model': self.rule_1,
- 'auto_reconcile': True,
- },
- self.bank_line_2: {
- 'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3,
- 'model': self.rule_1,
- },
- })
- 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: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True},
- self.bank_line_2: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True},
- })
- def test_auto_reconcile_with_tax_fpos(self):
- """ Test the fiscal positions are applied by reconcile models when using taxes.
- """
- self.rule_1.write({
- 'auto_reconcile': True,
- 'rule_type': 'writeoff_suggestion',
- 'line_ids': [(1, self.rule_1.line_ids.id, {
- 'amount': 100,
- 'force_tax_included': True,
- 'tax_ids': [(6, 0, self.tax21.ids)],
- })]
- })
- self.partner_1.country_id = self.env.ref('base.lu')
- belgium = self.env.ref('base.be')
- self.partner_2.country_id = belgium
- self.bank_line_2.partner_id = self.partner_2
- self.bank_line_1.amount = -121
- self.bank_line_2.amount = -112
- self.env['account.fiscal.position'].create({
- 'name': "Test",
- 'country_id': belgium.id,
- 'auto_apply': True,
- 'tax_ids': [
- Command.create({
- 'tax_src_id': self.tax21.id,
- 'tax_dest_id': self.tax12.id,
- }),
- ]
- })
- self._check_statement_matching(self.rule_1, {
- self.bank_line_1: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True},
- self.bank_line_2: {'model': self.rule_1, 'status': 'write_off', 'auto_reconcile': True},
- })
- 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.company_id.account_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.company_id.account_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: {'amls': payment_bnk_line, 'model': self.rule_1},
- self.bank_line_2: {
- 'amls': self.invoice_line_1 + self.invoice_line_2 + self.invoice_line_3,
- 'model': self.rule_1,
- },
- })
- 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_inv.active = True
- 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: {'amls': invoice_line, 'model': self.rule_1},
- self.bank_line_2: {},
- })
- 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.rule_1.allow_payment_tolerance = False
- 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
- })
- # TODO: 'invoice_line_1' has no reason to match 'bank_line_1' here... to check
- # self._check_statement_matching(self.rule_1, {
- # self.bank_line_1: {'amls': self.invoice_line_1, 'model': self.rule_1},
- # self.bank_line_2: {'amls': []},
- # }, self.bank_st)
- def test_inv_matching_rule_auto_rec_no_partner_with_writeoff(self):
- self.invoice_line_1.move_id.ref = "doudlidou3555"
- self.bank_line_1.write({
- 'payment_ref': 'doudlidou3555',
- '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
- 'payment_tolerance_param': 10.0,
- 'auto_reconcile': True,
- })
- # Check bank reconciliation
- self._check_statement_matching(self.rule_1, {
- self.bank_line_1: {
- 'amls': self.invoice_line_1,
- 'model': self.rule_1,
- 'status': 'write_off',
- 'auto_reconcile': True,
- },
- self.bank_line_2: {},
- })
- def test_partner_mapping_rule(self):
- st_line = self._create_st_line(partner_id=None, payment_ref=None)
- rule = self._create_reconcile_model(
- partner_mapping_line_ids=[{
- 'partner_id': self.partner_1.id,
- 'payment_ref_regex': 'toto.*',
- }],
- )
- # No match because the reference is not matching the regex.
- self.assertEqual(st_line._retrieve_partner(), self.env['res.partner'])
- st_line.payment_ref = "toto42"
- # Matching using the regex on payment_ref.
- self.assertEqual(st_line._retrieve_partner(), self.partner_1)
- rule.partner_mapping_line_ids.narration_regex = ".*coincoin"
- # No match because the narration is not matching the regex.
- self.assertEqual(st_line._retrieve_partner(), self.env['res.partner'])
- st_line.narration = "42coincoin"
- # Matching is back thanks to "coincoin".
- self.assertEqual(st_line._retrieve_partner(), self.partner_1)
- # More complex matching to match something from bank sync data.
- # Note: the indentation is done with multiple \n to mimic the bank sync behavior. Keep them for this test!
- rule.partner_mapping_line_ids.narration_regex = ".*coincoin.*"
- st_line.narration = """
- {
- "informations": "coincoin turlututu tsoin tsoin",
- }
- """
- # Same check with json data into the narration field.
- self.assertEqual(st_line._retrieve_partner(), self.partner_1)
- 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)],
- 'allow_payment_tolerance': True,
- 'payment_tolerance_type': 'percentage',
- 'payment_tolerance_param': 5.0,
- 'match_same_currency': False,
- 'company_id': self.company_data['company'].id,
- 'past_months_limit': False,
- })
- statement_line = self.env['account.bank.statement.line'].create({
- '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.
- })
- 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: {'amls': move_line_1 + move_line_2, 'model': matching_rule}
- })
- @freeze_time('2020-01-01')
- def test_matching_with_write_off_foreign_currency(self):
- journal_foreign_curr = self.company_data['default_journal_bank'].copy()
- journal_foreign_curr.currency_id = self.currency_data['currency']
- reco_model = self._create_reconcile_model(
- auto_reconcile=True,
- rule_type='writeoff_suggestion',
- line_ids=[{
- 'amount_type': 'percentage',
- 'amount': 100.0,
- 'account_id': self.company_data['default_account_revenue'].id,
- }],
- )
- st_line = self._create_st_line(amount=100.0, payment_ref='123456', journal_id=journal_foreign_curr.id)
- self._check_statement_matching(reco_model, {
- st_line: {
- 'model': reco_model,
- 'status': 'write_off',
- 'auto_reconcile': True,
- },
- })
- 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.account_type not in {'asset_receivable', 'liability_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)
- 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.allow_payment_tolerance = False
- self._check_statement_matching(self.rule_1, {
- self.bank_line_1: {'amls': pmt_line_1 + pmt_line_2, 'model': self.rule_1, 'status': 'write_off'},
- })
- def test_no_amount_check_keep_first(self):
- """ In case the reconciliation model doesn't check the total amount of the candidates,
- we still don't want to suggest more than are necessary to match the statement.
- For example, if a statement line amounts to 250 and is to be matched with three invoices
- of 100, 200 and 300 (retrieved in this order), only 100 and 200 should be proposed.
- """
- self.rule_1.allow_payment_tolerance = False
- self.bank_line_2.amount = 250
- self.bank_line_1.partner_id = None
- self._check_statement_matching(self.rule_1, {
- self.bank_line_1: {},
- self.bank_line_2: {
- 'amls': self.invoice_line_1 + self.invoice_line_2,
- 'model': self.rule_1,
- 'status': 'write_off',
- },
- })
- def test_no_amount_check_exact_match(self):
- """ If a reconciliation model finds enough candidates for a full reconciliation,
- it should still check the following candidates, in case one of them exactly
- matches the amount of the statement line. If such a candidate exist, all the
- other ones are disregarded.
- """
- self.rule_1.allow_payment_tolerance = False
- self.bank_line_2.amount = 300
- self.bank_line_1.partner_id = None
- self._check_statement_matching(self.rule_1, {
- self.bank_line_1: {},
- self.bank_line_2: {
- 'amls': self.invoice_line_3,
- 'model': self.rule_1,
- 'status': 'write_off',
- },
- })
- @freeze_time('2019-01-01')
- def test_invoice_matching_using_match_text_location(self):
- @contextmanager
- def rollback():
- savepoint = self.cr.savepoint()
- yield
- savepoint.rollback()
- rule = self._create_reconcile_model(
- match_partner=False,
- allow_payment_tolerance=False,
- match_text_location_label=False,
- match_text_location_reference=False,
- match_text_location_note=False,
- )
- st_line = self._create_st_line(amount=1000, partner_id=False)
- invoice = self.env['account.move'].create({
- 'move_type': 'out_invoice',
- 'partner_id': self.partner_a.id,
- 'invoice_date': '2019-01-01',
- 'invoice_line_ids': [Command.create({
- 'product_id': self.product_a.id,
- 'price_unit': 100,
- })],
- })
- invoice.action_post()
- term_line = invoice.line_ids.filtered(lambda x: x.display_type == 'payment_term')
- # No match at all.
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {},
- )
- with rollback():
- term_line.name = "1234"
- st_line.payment_ref = "1234"
- # Matching if no checkbox checked.
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {'amls': term_line, 'model': rule},
- )
- # No matching if other checkbox is checked.
- rule.match_text_location_note = True
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {},
- )
- with self.subTest(rule_field='match_text_location_label', st_line_field='payment_ref'):
- with rollback():
- term_line.name = ''
- st_line.payment_ref = '/?'
- # No exact matching when the term line name is an empty string
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {},
- )
- for rule_field, st_line_field in (
- ('match_text_location_label', 'payment_ref'),
- ('match_text_location_reference', 'ref'),
- ('match_text_location_note', 'narration'),
- ):
- with self.subTest(rule_field=rule_field, st_line_field=st_line_field):
- with rollback():
- rule[rule_field] = True
- st_line[st_line_field] = "123456"
- term_line.name = "123456"
- # Matching if the corresponding flag is enabled.
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {'amls': term_line, 'model': rule},
- )
- # It works also if the statement line contains the word.
- st_line[st_line_field] = "payment for 123456 urgent!"
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {'amls': term_line, 'model': rule},
- )
- # Not if the invoice has nothing in common even if numerical.
- term_line.name = "78910"
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {},
- )
- # Exact matching on a single word.
- st_line[st_line_field] = "TURLUTUTU21"
- term_line.name = "TURLUTUTU21"
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {'amls': term_line, 'model': rule},
- )
- # No matching if not enough numerical values.
- st_line[st_line_field] = "12"
- term_line.name = "selling 3 apples, 2 tomatoes and 12kg of potatoes"
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {},
- )
- invoice2 = self.env['account.move'].create({
- 'move_type': 'out_invoice',
- 'partner_id': self.partner_a.id,
- 'invoice_date': '2019-01-01',
- 'invoice_line_ids': [Command.create({
- 'product_id': self.product_a.id,
- 'price_unit': 100,
- })],
- })
- invoice2.action_post()
- term_lines = (invoice + invoice2).line_ids.filtered(lambda x: x.display_type == 'payment_term')
- # Matching multiple invoices.
- rule.match_text_location_label = True
- st_line.payment_ref = "paying invoices 1234 & 5678"
- term_lines[0].name = "INV/1234"
- term_lines[1].name = "INV/5678"
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {'amls': term_lines, 'model': rule},
- )
- # Matching multiple invoices sharing the same reference.
- term_lines[1].name = "INV/1234"
- self.assertDictEqual(
- rule._apply_rules(st_line, None),
- {'amls': term_lines, 'model': rule},
- )
|