diff --git a/addons/at_accounting/tests/test_bank_rec_widget.py b/addons/at_accounting/tests/test_bank_rec_widget.py new file mode 100644 index 0000000..eda1da8 --- /dev/null +++ b/addons/at_accounting/tests/test_bank_rec_widget.py @@ -0,0 +1,3263 @@ +# -*- coding: utf-8 -*- +from odoo.addons.at_accounting.tests.test_bank_rec_widget_common import TestBankRecWidgetCommon +from odoo.tests import tagged +from odoo.tools import html2plaintext +from odoo import fields, Command + +from freezegun import freeze_time +from unittest.mock import patch +import re + + +@tagged('post_install', '-at_install') +class TestBankRecWidget(TestBankRecWidgetCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company_data_2 = cls.setup_other_company() + + cls.early_payment_term = cls.env['account.payment.term'].create({ + 'name': "early_payment_term", + 'company_id': cls.company_data['company'].id, + 'discount_percentage': 10, + 'discount_days': 10, + 'early_discount': True, + 'line_ids': [ + Command.create({ + 'value': 'percent', + 'value_amount': 100, + 'nb_days': 20, + }), + ], + }) + + cls.account_revenue1 = cls.company_data['default_account_revenue'] + cls.account_revenue2 = cls.copy_account(cls.account_revenue1) + + cls.reco_model_bill = cls.env['account.reconcile.model'].create({ + 'name': "test create bill", + 'rule_type': 'writeoff_button', + 'counterpart_type': 'purchase', + 'line_ids': [ + Command.create({'amount_string': '50'}), + Command.create({'amount_string': '50'}), + ], + }) + + def assert_form_extra_text_value(self, wizard, regex): + line = wizard.line_ids.filtered(lambda x: x.index == wizard.form_index) + value = line.suggestion_html + if regex: + cleaned_value = html2plaintext(value).replace('\n', '') + if not re.match(regex, cleaned_value): + self.fail(f"The following 'form_extra_text':\n\n'{cleaned_value}'\n\n...doesn't match the provided regex:\n\n'{regex}'") + else: + self.assertFalse(value) + + def test_retrieve_partner_from_account_number(self): + st_line = self._create_st_line(1000.0, partner_id=None, account_number="014 474 8555") + bank_account = self.env['res.partner.bank'].create({ + 'acc_number': '0144748555', + 'partner_id': self.partner_a.id, + }) + self.assertEqual(st_line._retrieve_partner(), bank_account.partner_id) + + # Can't retrieve the partner since the bank account is used by multiple partners. + self.env['res.partner.bank'].create({ + 'acc_number': '0144748555', + 'partner_id': self.partner_b.id, + }) + self.assertEqual(st_line._retrieve_partner(), self.env['res.partner']) + + # Archive partner_a and see if partner_b is then chosen + self.partner_a.active = False + self.assertEqual(st_line._retrieve_partner(), self.partner_b) + + def test_retrieve_partner_from_account_number_in_other_company(self): + st_line = self._create_st_line(1000.0, partner_id=None, account_number="014 474 8555") + self.env['res.partner.bank'].create({ + 'acc_number': '0144748555', + 'partner_id': self.partner_a.id, + }) + + # Bank account is owned by another company. + new_company = self.env['res.company'].create({'name': "test_retrieve_partner_from_account_number_in_other_company"}) + self.partner_a.company_id = new_company + self.assertEqual(st_line._retrieve_partner(), self.env['res.partner']) + + def test_retrieve_partner_from_partner_name(self): + """ Ensure the partner having a name fitting exactly the 'partner_name' is retrieved first. + This test create two partners that will be ordered in the lexicographic order when performing + a search. So: + row1: "Turlututu tsoin tsoin" + row2: "turlututu" + + Since "turlututu" matches exactly (case insensitive) the partner_name of the statement line, + it should be suggested first. + + However if we have two partners called turlututu, we should not suggest any or we risk selecting + the wrong one. + """ + _partner_a, partner_b = self.env['res.partner'].create([ + {'name': "Turlututu tsoin tsoin"}, + {'name': "turlututu"}, + ]) + + st_line = self._create_st_line(1000.0, partner_id=None, partner_name="Turlututu") + self.assertEqual(st_line._retrieve_partner(), partner_b) + + self.env['res.partner'].create({'name': "turlututu"}) + self.assertFalse(st_line._retrieve_partner()) + + def test_retrieve_partner_suggested_account_from_rank(self): + """ Ensure a retrieved partner is proposing his receivable/payable according his customer/supplier rank. """ + partner = self.env['res.partner'].create({'name': "turlututu"}) + rec_account_id = partner.property_account_receivable_id.id + pay_account_id = partner.property_account_payable_id.id + + st_line = self._create_st_line(1000.0, partner_id=None, partner_name="turlututu") + liq_account_id = st_line.journal_id.default_account_id.id + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'account_id': liq_account_id, 'balance': 1000.0}, + {'flag': 'auto_balance', 'account_id': rec_account_id, 'balance': -1000.0}, + ]) + + partner._increase_rank('supplier_rank', 1) + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'account_id': liq_account_id, 'balance': 1000.0}, + {'flag': 'auto_balance', 'account_id': pay_account_id, 'balance': -1000.0}, + ]) + + def test_res_partner_bank_find_create_when_archived(self): + """ Test we don't get the "The combination Account Number/Partner must be unique." error with archived + bank account. + """ + partner = self.env['res.partner'].create({ + 'name': "Zitycard", + 'bank_ids': [Command.create({ + 'acc_number': "123456789", + 'active': False, + })], + }) + + st_line = self._create_st_line( + 100.0, + partner_name="Zeumat Zitycard", + account_number="123456789", + ) + inv_line = self._create_invoice_line( + 'out_invoice', + partner_id=partner.id, + invoice_line_ids=[{'price_unit': 100.0, 'tax_ids': []}], + ) + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + wizard._action_validate() + + # Should not trigger the error. + self.env['res.partner.bank'].flush_model() + + def test_res_partner_bank_find_create_multi_company(self): + """ Test we don't get the "The combination Account Number/Partner must be unique." error when the bank account + already exists on another company. + """ + partner = self.env['res.partner'].create({ + 'name': "Zitycard", + 'bank_ids': [Command.create({'acc_number': "123456789"})], + }) + partner.bank_ids.company_id = self.company_data_2['company'] + self.env.user.company_ids = self.env.company + + st_line = self._create_st_line( + 100.0, + partner_name="Zeumat Zitycard", + account_number="123456789", + ) + inv_line = self._create_invoice_line( + 'out_invoice', + partner_id=partner.id, + invoice_line_ids=[{'price_unit': 100.0, 'tax_ids': []}], + ) + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + wizard._action_validate() + + # Should not trigger the error. + self.env['res.partner.bank'].flush_model() + + def test_validation_base_case(self): + st_line = self._create_st_line( + 1000.0, + date='2017-01-01', + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance') + wizard._js_action_mount_line_in_edit(line.index) + line.account_id = self.account_revenue1 + wizard._line_value_changed_account_id(line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0}, + {'flag': 'manual', 'amount_currency': -1000.0, 'currency_id': self.company_data['currency'].id, 'balance': -1000.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # The amount is the same, no message under the 'amount' field. + self.assert_form_extra_text_value(wizard, False) + + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0, 'reconciled': False}, + {'account_id': self.account_revenue1.id, 'amount_currency': -1000.0, 'currency_id': self.company_data['currency'].id, 'balance': -1000.0, 'reconciled': False}, + ]) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0}, + {'flag': 'aml', 'account_id': self.account_revenue1.id, 'amount_currency': -1000.0, 'currency_id': self.company_data['currency'].id, 'balance': -1000.0}, + ]) + + def test_validation_exchange_difference(self): + # 240.0 curr2 == 120.0 comp_curr + st_line = self._create_st_line( + 120.0, + date='2017-01-01', + foreign_currency_id=self.other_currency.id, + amount_currency=240.0, + ) + # 240.0 curr2 == 80.0 comp_curr + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 240.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 120.0, 'currency_id': self.company_data['currency'].id, 'balance': 120.0}, + {'flag': 'new_aml', 'amount_currency': -240.0, 'currency_id': self.other_currency.id, 'balance': -80.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -40.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + wizard._action_validate() + + # Check the statement line. + self.assertRecordValues(st_line.line_ids.sorted(), [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 120.0, 'currency_id': self.company_data['currency'].id, 'balance': 120.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -240.0, 'currency_id': self.other_currency.id, 'balance': -120.0, 'reconciled': True}, + ]) + + # Check the partials. + partials = st_line.line_ids.matched_debit_ids + exchange_move = partials.exchange_move_id + _liquidity_line, _suspense_line, other_line = st_line._seek_for_lines() + self.assertRecordValues(partials.sorted(), [ + # pylint: disable=C0326 + { + 'amount': 40.0, + 'debit_amount_currency': 0.0, + 'credit_amount_currency': 0.0, + 'debit_move_id': exchange_move.line_ids.sorted()[0].id, + 'credit_move_id': other_line.id, + 'exchange_move_id': False, + }, + { + 'amount': 80.0, + 'debit_amount_currency': 240.0, + 'credit_amount_currency': 240.0, + 'debit_move_id': inv_line.id, + 'credit_move_id': other_line.id, + 'exchange_move_id': exchange_move.id, + }, + ]) + + # Check the exchange diff journal entry. + self.assertRecordValues(exchange_move.line_ids.sorted(), [ + # pylint: disable=C0326 + {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 40.0, 'reconciled': True}, + {'account_id': self.env.company.income_currency_exchange_account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -40.0, 'reconciled': False}, + ]) + + def test_validation_new_aml_same_foreign_currency(self): + income_exchange_account = self.env.company.income_currency_exchange_account_id + + # 6000.0 curr2 == 1200.0 comp_curr (bank rate 5:1 instead of the odoo rate 4:1) + st_line = self._create_st_line( + 1200.0, + date='2017-01-01', + foreign_currency_id=self.other_currency_2.id, + amount_currency=6000.0, + ) + # 6000.0 curr2 == 1000.0 comp_curr (rate 6:1) + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 6000.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -6000.0, 'currency_id': self.other_currency_2.id, 'balance': -1000.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -200.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # The amount is the same, no message under the 'amount' field. + self.assert_form_extra_text_value(wizard, False) + + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -6000.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) + self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ + # pylint: disable=C0326 + {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 200.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, + {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -200.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, + ]) + + # Reset the wizard. + wizard._js_action_reset() + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'auto_balance', 'amount_currency': -6000.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0}, + ]) + + # Create the same invoice with a higher amount to check the partial flow. + # 9000.0 curr2 == 1500.0 comp_curr (rate 6:1) + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 9000.0}], + ) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'name': "turlututu"}, + {'flag': 'new_aml', 'amount_currency': -6000.0, 'currency_id': self.other_currency_2.id, 'balance': -1000.0, 'name': "INV/2016/00002"}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -200.0, 'name': "Exchange Difference: INV/2016/00002"}, + ]) + + # Check the message under the 'amount' field. + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + self.assert_form_extra_text_value( + wizard, + r".+open amount of 9,000.000.+ reduced by 6,000.000.+ set the invoice as fully paid .", + ) + self.assertRecordValues(line, [{ + 'suggestion_amount_currency': -9000.0, + 'suggestion_balance': -1500.0, + }]) + + # Switch to a full reconciliation. + wizard._js_action_apply_line_suggestion(line.index) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'name': "turlututu"}, + {'flag': 'new_aml', 'amount_currency': -9000.0, 'currency_id': self.other_currency_2.id, 'balance': -1500.0, 'name': "INV/2016/00002"}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -300.0, 'name': "Exchange Difference: INV/2016/00002"}, + {'flag': 'auto_balance', 'amount_currency': 3000.0, 'currency_id': self.other_currency_2.id, 'balance': 600.0, 'name': "Open balance of 6,000.000 $"}, + ]) + + # Check the message under the 'amount' field. + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + self.assert_form_extra_text_value( + wizard, + r".+open amount of 9,000.000.+ paid .+ record a partial payment .", + ) + self.assertRecordValues(line, [{ + 'suggestion_amount_currency': -6000.0, + 'suggestion_balance': -1000.0, + }]) + + # Switch back to a partial reconciliation. + wizard._js_action_apply_line_suggestion(line.index) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # Reconcile + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -6000.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{ + 'payment_state': 'partial', + 'amount_residual': 3000.0, + }]) + self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ + # pylint: disable=C0326 + {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 200.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, + {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -200.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, + ]) + + def test_validation_expense_exchange_difference(self): + expense_exchange_account = self.env.company.expense_currency_exchange_account_id + + # 1200.0 comp_curr = 3600.0 foreign_curr in 2016 (rate 1:3) + st_line = self._create_st_line( + 1200.0, + date='2016-01-01', + ) + # 1800.0 comp_curr = 3600.0 foreign_curr in 2017 (rate 1:2) + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 3600.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1800.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 600.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) + self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ + # pylint: disable=C0326 + {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -600.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, + {'account_id': expense_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 600.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, + ]) + # Checks that the wizard still display the 3 initial lines + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1800.0}, + {'flag': 'aml', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 600.0}, + ]) + + def test_validation_income_exchange_difference(self): + income_exchange_account = self.env.company.income_currency_exchange_account_id + + # 1800.0 comp_curr = 3600.0 foreign_curr in 2017 (rate 1:2) + st_line = self._create_st_line( + 1800.0, + date='2017-01-01', + ) + # 1200.0 comp_curr = 3600.0 foreign_curr in 2016 (rate 1:3) + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 3600.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'new_aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -600.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1800.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) + self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ + # pylint: disable=C0326 + {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 600.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, + {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -600.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, + ]) + # Checks that the wizard still display the 3 initial lines + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0}, + {'flag': 'aml', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -600.0}, + ]) + + def test_validation_income_exchange_difference_with_rounding(self): + # 1000.0 comp_curr = 3000.0 foreign_curr in 2016 (rate 1:3) + # However divided in 3 invoices + rounding we have 333.33333 ≃ 333.33 comp_curr = 1000.0 foreign_curr + # this implies that the full amount has been used in foreign_curr but there is 0.01 in comp_curr + st_line = self._create_st_line( + 1000.0, + date='2016-01-01', + ) + + # 1500 comp_curr = 3000.0 foreign_curr in 2017 (rate 1:2) + inv_line_1 = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 1000.0}], + ) + + inv_line_2 = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 1000.0}], + ) + + inv_line_3 = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 1000.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line_1 + inv_line_2 + inv_line_3) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0}, + {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.67}, + {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.67}, + {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.67}, + {'flag': 'auto_balance', 'amount_currency': -0.01, 'currency_id': self.company_data['currency'].id, 'balance': -0.01}, + ]) + + # Remove 0.01 cent in the balance of first exchange line + first_exchange_line = wizard.line_ids.filtered(lambda x: x.flag == 'exchange_diff')[:1] + wizard._js_action_mount_line_in_edit(first_exchange_line.index) + first_exchange_line.balance = 166.66 + wizard._line_value_changed_balance(first_exchange_line) + + # Every line balance so no 'auto_balance' is generated + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0}, + {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.66}, + {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.67}, + {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -500.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 166.67}, + ]) + + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + wizard._action_validate() + + # Check that the first line with exchange has -0.01 compared to others + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1000.0, 'currency_id': self.company_data['currency'].id, 'balance': 1000.0, 'reconciled': False}, + {'account_id': inv_line_1.account_id.id, 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -333.34, 'reconciled': True}, + {'account_id': inv_line_2.account_id.id, 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -333.33, 'reconciled': True}, + {'account_id': inv_line_3.account_id.id, 'amount_currency': -1000.0, 'currency_id': self.other_currency.id, 'balance': -333.33, 'reconciled': True}, + ]) + + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line_1.move_id, [{'payment_state': 'paid'}]) + self.assertRecordValues(inv_line_2.move_id, [{'payment_state': 'paid'}]) + self.assertRecordValues(inv_line_3.move_id, [{'payment_state': 'paid'}]) + + def test_validation_exchange_diff_multiple(self): + income_exchange_account = self.env.company.income_currency_exchange_account_id + foreign_currency = self.setup_other_currency('AED', rates=[('2016-01-01', 6.0), ('2017-01-01', 5.0)]) + + # 6000.0 curr2 == 1200.0 comp_curr (bank rate 5:1 instead of the odoo rate 6:1) + st_line = self._create_st_line( + 1200.0, + date='2016-01-01', + foreign_currency_id=foreign_currency.id, + amount_currency=6000.0, + ) + # 1000.0 foreign_curr == 166.67 comp_curr (rate 6:1) + inv_line_1 = self._create_invoice_line( + 'out_invoice', + currency_id=foreign_currency.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 1000.0}], + ) + # 2000.00 foreign_curr == 400.0 comp_curr (rate 5:1) + inv_line_2 = self._create_invoice_line( + 'out_invoice', + currency_id=foreign_currency.id, + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 2000.0}], + ) + # 3000.0 foreign_curr == 500.0 comp_curr (rate 6:1) + inv_line_3 = self._create_invoice_line( + 'out_invoice', + currency_id=foreign_currency.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 3000.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line_1 + inv_line_2 + inv_line_3) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -1000.0, 'currency_id': foreign_currency.id, 'balance': -166.67}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': -33.33}, + {'flag': 'new_aml', 'amount_currency': -2000.0, 'currency_id': foreign_currency.id, 'balance': -400.0}, + {'flag': 'new_aml', 'amount_currency': -3000.0, 'currency_id': foreign_currency.id, 'balance': -500.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': -100.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # The amount is the same, no message under the 'amount' field. + self.assert_form_extra_text_value(wizard, False) + + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, + {'account_id': inv_line_1.account_id.id, 'amount_currency': -1000.0, 'currency_id': foreign_currency.id, 'balance': -200.0, 'reconciled': True}, + {'account_id': inv_line_2.account_id.id, 'amount_currency': -2000.0, 'currency_id': foreign_currency.id, 'balance': -400.0, 'reconciled': True}, + {'account_id': inv_line_3.account_id.id, 'amount_currency': -3000.0, 'currency_id': foreign_currency.id, 'balance': -600.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line_1.move_id, [{'payment_state': 'paid'}]) + self.assertRecordValues(inv_line_2.move_id, [{'payment_state': 'paid'}]) + self.assertRecordValues(inv_line_3.move_id, [{'payment_state': 'paid'}]) + self.assertRecordValues((inv_line_1 + inv_line_2 + inv_line_3).matched_credit_ids.exchange_move_id.line_ids, [ + # pylint: disable=C0326 + {'account_id': inv_line_1.account_id.id, 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': 33.33, 'reconciled': True}, + {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': -33.33, 'reconciled': False}, + {'account_id': inv_line_3.account_id.id, 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': 100.0, 'reconciled': True}, + {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': foreign_currency.id, 'balance': -100.0, 'reconciled': False}, + ]) + + def test_validation_foreign_curr_st_line_comp_curr_payment_partial_exchange_difference(self): + comp_curr = self.env.company.currency_id + foreign_curr = self.other_currency + + st_line = self._create_st_line( + 650.0, + date='2017-01-01', + foreign_currency_id=foreign_curr.id, + amount_currency=800, + ) + + payment = self.env['account.payment'].create({ + 'partner_id': self.partner_a.id, + 'payment_type': 'inbound', + 'partner_type': 'customer', + 'date': '2017-01-01', + 'amount': 725.0, + }) + payment.action_post() + pay_line, _counterpart_lines, _writeoff_lines = payment._seek_for_lines() + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(pay_line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 650.0, 'currency_id': comp_curr.id, 'balance': 650.0}, + {'flag': 'new_aml', 'amount_currency': -650.0, 'currency_id': comp_curr.id, 'balance': -650.0}, + ]) + + # Switch to a full reconciliation. + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + wizard._js_action_apply_line_suggestion(line.index) + + # 725 * 800 / 650 = 892.308 + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 650.0, 'currency_id': comp_curr.id, 'balance': 650.0}, + {'flag': 'new_aml', 'amount_currency': -725.0, 'currency_id': comp_curr.id, 'balance': -725.0}, + {'flag': 'auto_balance', 'amount_currency': 92.308, 'currency_id': foreign_curr.id, 'balance': 75.0}, + ]) + + # Switch to a partial reconciliation. + wizard._js_action_apply_line_suggestion(line.index) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 650.0, 'currency_id': comp_curr.id, 'balance': 650.0}, + {'flag': 'new_aml', 'amount_currency': -650.0, 'currency_id': comp_curr.id, 'balance': -650.0}, + ]) + + wizard._action_validate() + self.assertRecordValues(pay_line, [{'amount_residual': 75.0}]) + + def test_validation_remove_exchange_difference(self): + """ Test the case when the foreign currency is missing on the statement line. + In that case, the user can remove the exchange difference in order to fully reconcile both items without additional + write-off/exchange difference. + """ + # 1200.0 comp_curr = 2400.0 foreign_curr in 2017 (rate 1:2) + st_line = self._create_st_line( + 1200.0, + date='2017-01-01', + ) + # 1200.0 comp_curr = 3600.0 foreign_curr in 2016 (rate 1:3) + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 3600.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -2400.0, 'currency_id': self.other_currency.id, 'balance': -800.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -400.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # Remove the partial. + line_index = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml').index + wizard._js_action_mount_line_in_edit(line_index) + wizard._js_action_apply_line_suggestion(line_index) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': -600.0}, + {'flag': 'auto_balance', 'amount_currency': 600.0, 'currency_id': self.company_data['currency'].id, 'balance': 600.0}, + ]) + + exchange_diff_index = wizard.line_ids.filtered(lambda x: x.flag == 'exchange_diff').index + wizard._js_action_remove_line(exchange_diff_index) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0}, + ]) + + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1200.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) + + def test_validation_new_aml_one_foreign_currency_on_st_line(self): + income_exchange_account = self.env.company.income_currency_exchange_account_id + + # 4800.0 curr2 == 1200.0 comp_curr (rate 4:1) + st_line = self._create_st_line( + 1200.0, + date='2017-01-01', + ) + # 4800.0 curr2 in 2016 (rate 6:1) + inv_line = self._create_invoice_line( + 'out_invoice', + invoice_date='2016-01-01', + currency_id=self.other_currency_2.id, + invoice_line_ids=[{'price_unit': 4800.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # The amount is the same, no message under the 'amount' field. + self.assert_form_extra_text_value(wizard, False) + + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) + self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ + # pylint: disable=C0326 + {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 400.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, + {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, + ]) + + # Checks that the wizard still display the 3 initial lines + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'aml', 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, + {'flag': 'aml', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0}, # represents the exchange diff + ]) + + # Reset the wizard. + wizard._js_action_reset() + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'auto_balance', 'amount_currency': -1200.0, 'currency_id': self.company_data['currency'].id, 'balance': -1200.0}, + ]) + + # Create the same invoice with a higher amount to check the partial flow. + # 4800.0 curr2 in 2016 (rate 6:1) + inv_line = self._create_invoice_line( + 'out_invoice', + invoice_date='2016-01-01', + currency_id=self.other_currency_2.id, + invoice_line_ids=[{'price_unit': 9600.0}], + ) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0}, + ]) + + # Check the message under the 'amount' field. + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + self.assert_form_extra_text_value( + wizard, + r".+open amount of 9,600.000.+ reduced by 4,800.000.+ set the invoice as fully paid .", + ) + self.assertRecordValues(line, [{ + 'suggestion_amount_currency': -9600.0, + 'suggestion_balance': -1600.0, + }]) + + # Switch to a full reconciliation. + wizard._js_action_apply_line_suggestion(line.index) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -9600.0, 'currency_id': self.other_currency_2.id, 'balance': -1600.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, + {'flag': 'auto_balance', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + ]) + + # Check the message under the 'amount' field. + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + self.assert_form_extra_text_value( + wizard, + r".+open amount of 9,600.000.+ paid .+ record a partial payment .", + ) + self.assertRecordValues(line, [{ + 'suggestion_amount_currency': -4800.0, + 'suggestion_balance': -800.0, + }]) + + # Switch back to a partial reconciliation. + wizard._js_action_apply_line_suggestion(line.index) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # Reconcile + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{ + 'payment_state': 'partial', + 'amount_residual': 4800.0, + }]) + self.assertRecordValues(inv_line, [{ + 'amount_residual_currency': 4800.0, + 'amount_residual': 800.0, + 'reconciled': False, + }]) + self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ + # pylint: disable=C0326 + {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 400.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, + {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, + ]) + + def test_validation_new_aml_one_foreign_currency_on_inv_line(self): + income_exchange_account = self.env.company.income_currency_exchange_account_id + + # 1200.0 comp_curr is equals to 4800.0 curr2 in 2017 (rate 4:1) + st_line = self._create_st_line( + 1200.0, + date='2017-01-01', + ) + # 4800.0 curr2 == 800.0 comp_curr (rate 6:1) + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 4800.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # The amount is the same, no message under the 'amount' field. + self.assert_form_extra_text_value(wizard, False) + + # Remove the line to see if the exchange difference is well removed. + wizard._action_remove_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'auto_balance', 'amount_currency': -1200.0, 'currency_id': self.company_data['currency'].id, 'balance': -1200.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'invalid'}]) + + # Mount the line again and validate. + wizard._action_add_new_amls(inv_line) + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) + self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ + # pylint: disable=C0326 + {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 400.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, + {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, + ]) + + # Reset the wizard. + wizard._js_action_reset() + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'auto_balance', 'amount_currency': -1200.0, 'currency_id': self.company_data['currency'].id, 'balance': -1200.0}, + ]) + + # Create the same invoice with a higher amount to check the partial flow. + # 7200.0 curr2 == 1200.0 comp_curr (rate 6:1) + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 7200.0}], + ) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -800.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0}, + ]) + + # Check the message under the 'amount' field. + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + self.assert_form_extra_text_value( + wizard, + r".+open amount of 7,200.000.+ reduced by 4,800.000.+ set the invoice as fully paid .", + ) + self.assertRecordValues(line, [{ + 'suggestion_amount_currency': -7200.0, + 'suggestion_balance': -1200.0, + }]) + + # Switch to a full reconciliation. + wizard._js_action_apply_line_suggestion(line.index) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency': -7200.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -600.0}, + {'flag': 'auto_balance', 'amount_currency': 600.0, 'currency_id': self.company_data['currency'].id, 'balance': 600.0}, + ]) + + # Check the message under the 'amount' field. + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + self.assert_form_extra_text_value( + wizard, + r".+open amount of 7,200.000.+ paid .+ record a partial payment .", + ) + self.assertRecordValues(line, [{ + 'suggestion_amount_currency': -4800.0, + 'suggestion_balance': -800.0, + }]) + + # Switch back to a partial reconciliation. + wizard._js_action_apply_line_suggestion(line.index) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # Reconcile + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -4800.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{ + 'payment_state': 'partial', + 'amount_residual': 2400.0, + }]) + self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ + # pylint: disable=C0326 + {'account_id': inv_line.account_id.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': 400.0, 'reconciled': True, 'date': fields.Date.from_string('2017-01-31')}, + {'account_id': income_exchange_account.id, 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -400.0, 'reconciled': False, 'date': fields.Date.from_string('2017-01-31')}, + ]) + + def test_validation_new_aml_multi_currencies(self): + # 6300.0 curr2 == 1800.0 comp_curr (bank rate 3.5:1 instead of the odoo rate 4:1) + st_line = self._create_st_line( + 1800.0, + date='2017-01-01', + foreign_currency_id=self.other_currency_2.id, + amount_currency=6300.0, + ) + # 21600.0 curr3 == 1800.0 comp_curr (rate 12:1) + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_3.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 21600.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'new_aml', 'amount_currency': -21600.0, 'currency_id': self.other_currency_3.id, 'balance': -1800.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # The amount is the same, no message under the 'amount' field. + self.assert_form_extra_text_value(wizard, False) + + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -21600.0, 'currency_id': self.other_currency_3.id, 'balance': -1800.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{'payment_state': 'paid'}]) + + # Reset the wizard. + wizard._js_action_reset() + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'auto_balance', 'amount_currency': -6300.0, 'currency_id': self.other_currency_2.id, 'balance': -1800.0}, + ]) + + # Create the same invoice with a higher amount to check the partial flow. + # 32400.0 curr3 == 2700.0 comp_curr (rate 12:1) + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_3.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 32400.0}], + ) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'new_aml', 'amount_currency': -21600.0, 'currency_id': self.other_currency_3.id, 'balance': -1800.0}, + ]) + + # Check the message under the 'amount' field. + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + self.assert_form_extra_text_value( + wizard, + r".+open amount of 32,400.000.+ reduced by 21,600.000.+ set the invoice as fully paid .", + ) + self.assertRecordValues(line, [{ + 'suggestion_amount_currency': -32400.0, + 'suggestion_balance': -2700.0, + }]) + + # Switch to a full reconciliation. + wizard._js_action_apply_line_suggestion(line.index) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'new_aml', 'amount_currency': -32400.0, 'currency_id': self.other_currency_3.id, 'balance': -2700.0}, + {'flag': 'auto_balance', 'amount_currency': 3150.0, 'currency_id': self.other_currency_2.id, 'balance': 900.0}, + ]) + + # Check the message under the 'amount' field. + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + self.assert_form_extra_text_value( + wizard, + r".+open amount of 32,400.000.+ paid .+ record a partial payment .", + ) + self.assertRecordValues(line, [{ + 'suggestion_amount_currency': -21600.0, + 'suggestion_balance': -1800.0, + }]) + + # Switch back to a partial reconciliation. + wizard._js_action_apply_line_suggestion(line.index) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # Reconcile + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'account_id': st_line.journal_id.default_account_id.id, 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0, 'reconciled': False}, + {'account_id': inv_line.account_id.id, 'amount_currency': -21600.0, 'currency_id': self.other_currency_3.id, 'balance': -1800.0, 'reconciled': True}, + ]) + self.assertRecordValues(st_line, [{'is_reconciled': True}]) + self.assertRecordValues(inv_line.move_id, [{ + 'payment_state': 'partial', + 'amount_residual': 10800.0, + }]) + + def test_validation_new_aml_multi_currencies_exchange_diff_custom_rates(self): + self.company_data['default_journal_bank'].currency_id = self.other_currency + + self.env['res.currency.rate'].create([ + { + 'name': '2017-02-01', + 'rate': 1.0683, + 'currency_id': self.other_currency.id, + 'company_id': self.env.company.id, + }, + { + 'name': '2017-03-01', + 'rate': 1.0812, + 'currency_id': self.other_currency.id, + 'company_id': self.env.company.id, + }, + ]) + + # 960.14 curr1 = 888.03 comp_curr + st_line = self._create_st_line( + -960.14, + date='2017-03-01', + ) + # 112.7 curr1 == 105.49 comp_curr + inv_line1 = self._create_invoice_line( + 'in_invoice', + currency_id=self.other_currency.id, + invoice_date='2017-02-01', + invoice_line_ids=[{'price_unit': 112.7}], + ) + # 847.44 curr1 == 793.26 comp_curr + inv_line2 = self._create_invoice_line( + 'in_invoice', + currency_id=self.other_currency.id, + invoice_date='2017-02-01', + invoice_line_ids=[{'price_unit': 847.44}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line1) + wizard._action_add_new_amls(inv_line2) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': -960.14, 'balance': -888.03}, + {'flag': 'new_aml', 'amount_currency': 112.7, 'balance': 105.49}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -1.25}, + {'flag': 'new_aml', 'amount_currency': 847.44, 'balance': 793.26}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -9.47}, + ]) + wizard._action_remove_new_amls(inv_line1 + inv_line2) + wizard._action_add_new_amls(inv_line2) + wizard._action_add_new_amls(inv_line1) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': -960.14, 'balance': -888.03}, + {'flag': 'new_aml', 'amount_currency': 847.44, 'balance': 793.26}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -9.47}, + {'flag': 'new_aml', 'amount_currency': 112.7, 'balance': 105.49}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -1.25}, + ]) + + def test_validation_with_partner(self): + partner = self.partner_a.copy() + + st_line = self._create_st_line(1000.0, partner_id=self.partner_a.id) + + # The wizard can be validated directly thanks to the receivable account set on the partner. + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # Validate and check the statement line. + wizard._action_validate() + self.assertRecordValues(st_line, [{'partner_id': self.partner_a.id}]) + liquidity_line, _suspense_line, other_line = st_line._seek_for_lines() + account = self.partner_a.property_account_receivable_id + self.assertRecordValues(liquidity_line + other_line, [ + # pylint: disable=C0326 + {'account_id': liquidity_line.account_id.id, 'balance': 1000.0}, + {'account_id': account.id, 'balance': -1000.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'reconciled'}]) + + # Match an invoice with a different partner. + wizard._js_action_reset() + inv_line = self._create_invoice_line( + 'out_invoice', + partner_id=partner.id, + invoice_line_ids=[{'price_unit': 1000.0}], + ) + wizard._action_add_new_amls(inv_line) + wizard._action_validate() + liquidity_line, suspense_line, other_line = st_line._seek_for_lines() + self.assertRecordValues(st_line, [{'partner_id': partner.id}]) + self.assertRecordValues(st_line.move_id, [{'partner_id': partner.id}]) + self.assertRecordValues(liquidity_line + other_line, [ + # pylint: disable=C0326 + {'account_id': liquidity_line.account_id.id, 'partner_id': partner.id, 'balance': 1000.0}, + {'account_id': inv_line.account_id.id, 'partner_id': partner.id, 'balance': -1000.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'reconciled'}]) + + # Reset the wizard and match invoices with different partners. + wizard._js_action_reset() + partner1 = self.partner_a.copy() + inv_line1 = self._create_invoice_line( + 'out_invoice', + partner_id=partner1.id, + invoice_line_ids=[{'price_unit': 300.0}], + ) + partner2 = self.partner_a.copy() + inv_line2 = self._create_invoice_line( + 'out_invoice', + partner_id=partner2.id, + invoice_line_ids=[{'price_unit': 300.0}], + ) + wizard._action_add_new_amls(inv_line1 + inv_line2) + wizard._action_validate() + liquidity_line, _suspense_line, other_line = st_line._seek_for_lines() + self.assertRecordValues(st_line, [{'partner_id': False}]) + self.assertRecordValues(st_line.move_id, [{'partner_id': False}]) + self.assertRecordValues(liquidity_line + other_line, [ + # pylint: disable=C0326 + {'account_id': liquidity_line.account_id.id, 'partner_id': False, 'balance': 1000.0}, + {'account_id': inv_line1.account_id.id, 'partner_id': partner1.id, 'balance': -300.0}, + {'account_id': inv_line2.account_id.id, 'partner_id': partner2.id, 'balance': -300.0}, + {'account_id': account.id, 'partner_id': False, 'balance': -400.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'reconciled'}]) + + # Clear the accounts set on the partner and reset the widget. + # The wizard should be invalid since we are not able to set an open balance. + partner.property_account_receivable_id = None + wizard._js_action_reset() + liquidity_line, suspense_line, other_line = st_line._seek_for_lines() + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'account_id': liquidity_line.account_id.id}, + {'flag': 'auto_balance', 'account_id': suspense_line.account_id.id}, + ]) + self.assertRecordValues(wizard, [{'state': 'invalid'}]) + + def test_partner_receivable_payable_account(self): + self.partner_a.write({'customer_rank': 1, 'supplier_rank': 0}) # always receivable + self.partner_b.write({'customer_rank': 0, 'supplier_rank': 1}) # always payable + partner_c = self.partner_b.copy({'customer_rank': 3, 'supplier_rank': 2}) # no preference + + positive_st_line = self._create_st_line(1000) + journal_account = positive_st_line.journal_id.default_account_id + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=positive_st_line.id).new({}) + suspense_line = wizard.line_ids.filtered(lambda l: l.flag != "liquidity") + wizard._js_action_mount_line_in_edit(suspense_line.index) + + suspense_line.partner_id = self.partner_a + wizard._line_value_changed_partner_id(suspense_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'partner_id': False, 'account_id': journal_account.id}, + {'partner_id': self.partner_a.id, 'account_id': self.partner_a.property_account_receivable_id.id}, + ]) + + suspense_line.partner_id = self.partner_b + wizard._line_value_changed_partner_id(suspense_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'partner_id': False, 'account_id': journal_account.id}, + {'partner_id': self.partner_b.id, 'account_id': self.partner_b.property_account_payable_id.id}, + ]) + + suspense_line.partner_id = partner_c + wizard._line_value_changed_partner_id(suspense_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'partner_id': False, 'account_id': journal_account.id}, + {'partner_id': partner_c.id, 'account_id': partner_c.property_account_receivable_id.id}, + ]) + + negative_st_line = self._create_st_line(-1000) + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=negative_st_line.id).new({}) + suspense_line = wizard.line_ids.filtered(lambda l: l.flag != "liquidity") + wizard._js_action_mount_line_in_edit(suspense_line.index) + + suspense_line.partner_id = self.partner_a + wizard._line_value_changed_partner_id(suspense_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'partner_id': False, 'account_id': journal_account.id}, + {'partner_id': self.partner_a.id, 'account_id': self.partner_a.property_account_receivable_id.id}, + ]) + + suspense_line.partner_id = self.partner_b + wizard._line_value_changed_partner_id(suspense_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'partner_id': False, 'account_id': journal_account.id}, + {'partner_id': self.partner_b.id, 'account_id': self.partner_b.property_account_payable_id.id}, + ]) + + suspense_line.partner_id = partner_c + wizard._line_value_changed_partner_id(suspense_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'partner_id': False, 'account_id': journal_account.id}, + {'partner_id': partner_c.id, 'account_id': partner_c.property_account_payable_id.id}, + ]) + + def test_validation_using_custom_account(self): + st_line = self._create_st_line(1000.0) + + # By default, the wizard can't be validated directly due to the suspense account. + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + self.assertRecordValues(wizard, [{'state': 'invalid'}]) + + # Mount the auto-balance line in edit mode. + line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance') + wizard._js_action_mount_line_in_edit(line.index) + liquidity_line, suspense_line, _other_lines = st_line._seek_for_lines() + self.assertRecordValues(line, [{ + 'account_id': suspense_line.account_id.id, + 'balance': -1000.0, + }]) + + # Switch to a custom account. + account = self.env['account.account'].create({ + 'name': "test_validation_using_custom_account", + 'code': "424242", + 'account_type': "asset_current", + }) + line.account_id = account + wizard._line_value_changed_account_id(line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'account_id': liquidity_line.account_id.id, 'balance': 1000.0}, + {'flag': 'manual', 'account_id': account.id, 'balance': -1000.0}, + ]) + + # The wizard can be validated. + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # Validate and check the statement line. + wizard._action_validate() + liquidity_line, _suspense_line, other_line = st_line._seek_for_lines() + self.assertRecordValues(liquidity_line + other_line, [ + # pylint: disable=C0326 + {'account_id': liquidity_line.account_id.id, 'balance': 1000.0}, + {'account_id': account.id, 'balance': -1000.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'reconciled'}]) + + def test_validation_with_taxes(self): + st_line = self._create_st_line(1000.0) + + tax_tags = self.env['account.account.tag'].create({ + 'name': f'tax_tag_{i}', + 'applicability': 'taxes', + 'country_id': self.env.company.account_fiscal_country_id.id, + } for i in range(4)) + + tax_21 = self.env['account.tax'].create({ + 'name': "tax_21", + 'amount': 21, + 'invoice_repartition_line_ids': [ + Command.create({ + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [Command.set(tax_tags[0].ids)], + }), + Command.create({ + 'factor_percent': 100, + 'repartition_type': 'tax', + 'tag_ids': [Command.set(tax_tags[1].ids)], + }), + ], + 'refund_repartition_line_ids': [ + Command.create({ + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [Command.set(tax_tags[2].ids)], + }), + Command.create({ + 'factor_percent': 100, + 'repartition_type': 'tax', + 'tag_ids': [Command.set(tax_tags[3].ids)], + }), + ], + }) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance') + wizard._js_action_mount_line_in_edit(line.index) + line.tax_ids = [Command.link(tax_21.id)] + wizard._line_value_changed_tax_ids(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0, 'tax_tag_ids': []}, + {'flag': 'manual', 'balance': -826.45, 'tax_tag_ids': tax_tags[0].ids}, + {'flag': 'tax_line', 'balance': -173.55, 'tax_tag_ids': tax_tags[1].ids}, + ]) + + # Remove the tax directly. + line.tax_ids = [Command.clear()] + wizard._line_value_changed_tax_ids(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0, 'tax_tag_ids': []}, + {'flag': 'manual', 'balance': -1000.0, 'tax_tag_ids': []}, + ]) + + # Edit the base line. The tax tags should be the refund ones. + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.tax_ids = [Command.link(tax_21.id)] + wizard._line_value_changed_tax_ids(line) + line.balance = 500.0 + wizard._line_value_changed_balance(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0, 'tax_tag_ids': []}, + {'flag': 'manual', 'balance': 500.0, 'tax_tag_ids': tax_tags[2].ids}, + {'flag': 'tax_line', 'balance': 105.0, 'tax_tag_ids': tax_tags[3].ids}, + {'flag': 'auto_balance', 'balance': -1605.0, 'tax_tag_ids': []}, + ]) + + # Edit the base line. + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.balance = -500.0 + wizard._line_value_changed_balance(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0, 'tax_tag_ids': []}, + {'flag': 'manual', 'balance': -500.0, 'tax_tag_ids': tax_tags[0].ids}, + {'flag': 'tax_line', 'balance': -105.0, 'tax_tag_ids': tax_tags[1].ids}, + {'flag': 'auto_balance', 'balance': -395.0, 'tax_tag_ids': []}, + ]) + + # Edit the tax line. + line = wizard.line_ids.filtered(lambda x: x.flag == 'tax_line') + wizard._js_action_mount_line_in_edit(line.index) + line.balance = -100.0 + wizard._line_value_changed_balance(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0, 'tax_tag_ids': []}, + {'flag': 'manual', 'balance': -500.0, 'tax_tag_ids': tax_tags[0].ids}, + {'flag': 'tax_line', 'balance': -100.0, 'tax_tag_ids': tax_tags[1].ids}, + {'flag': 'auto_balance', 'balance': -400.0, 'tax_tag_ids': []}, + ]) + + # Add a new tax. + tax_10 = self.env['account.tax'].create({ + 'name': "tax_10", + 'amount': 10, + }) + + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.tax_ids = [Command.link(tax_10.id)] + wizard._line_value_changed_tax_ids(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0}, + {'flag': 'manual', 'balance': -500.0}, + {'flag': 'tax_line', 'balance': -105.0}, + {'flag': 'tax_line', 'balance': -50.0}, + {'flag': 'auto_balance', 'balance': -345.0}, + ]) + + # Remove the taxes. + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.tax_ids = [Command.clear()] + wizard._line_value_changed_tax_ids(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0}, + {'flag': 'manual', 'balance': -500.0}, + {'flag': 'auto_balance', 'balance': -500.0}, + ]) + + # Reset the amount. + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.balance = -1000.0 + wizard._line_value_changed_balance(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0}, + {'flag': 'manual', 'balance': -1000.0}, + ]) + + # Add taxes. We should be back into the "price included taxes" mode. + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.tax_ids = [Command.link(tax_21.id)] + wizard._line_value_changed_tax_ids(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0}, + {'flag': 'manual', 'balance': -826.45}, + {'flag': 'tax_line', 'balance': -173.55}, + ]) + + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.tax_ids = [Command.link(tax_10.id)] + wizard._line_value_changed_tax_ids(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0}, + {'flag': 'manual', 'balance': -763.35}, + {'flag': 'tax_line', 'balance': -160.31}, + {'flag': 'tax_line', 'balance': -76.34}, + ]) + + # Changing the account should recompute the taxes but preserve the "price included taxes" mode. + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.account_id = self.account_revenue1 + wizard._line_value_changed_account_id(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0}, + {'flag': 'manual', 'balance': -763.35}, + {'flag': 'tax_line', 'balance': -160.31}, + {'flag': 'tax_line', 'balance': -76.34}, + ]) + + # The wizard can be validated. + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + # Validate and check the statement line. + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'balance': 1000.0}, + {'balance': -763.35}, + {'balance': -160.31}, + {'balance': -76.34}, + ]) + self.assertRecordValues(wizard, [{'state': 'reconciled'}]) + + def test_validation_caba_tax_account(self): + """ Cash basis taxes usually put their tax lines on a transition account, and the cash basis entries then move those amounts + to the regular tax accounts. When using a cash basis tax in the bank reconciliation widget, their won't be any cash basis + entry and the lines will directly be exigible, so we want to use the final tax account directly. + """ + tax_account = self.company_data['default_account_tax_sale'] + + caba_tax = self.env['account.tax'].create({ + 'name': "CABA", + 'amount_type': 'percent', + 'amount': 20.0, + 'tax_exigibility': 'on_payment', + 'cash_basis_transition_account_id': self.safe_copy(tax_account).id, + 'invoice_repartition_line_ids': [ + (0, 0, { + 'repartition_type': 'base', + }), + (0, 0, { + 'repartition_type': 'tax', + 'account_id': tax_account.id, + }), + ], + 'refund_repartition_line_ids': [ + (0, 0, { + 'repartition_type': 'base', + }), + (0, 0, { + 'repartition_type': 'tax', + 'account_id': tax_account.id, + }), + ], + }) + + st_line = self._create_st_line(120.0) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance') + wizard._js_action_mount_line_in_edit(line.index) + line.account_id = self.account_revenue1 + line.tax_ids = [Command.link(caba_tax.id)] + wizard._line_value_changed_tax_ids(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 120.0, 'account_id': st_line.journal_id.default_account_id.id}, + {'flag': 'manual', 'balance': -100.0, 'account_id': self.account_revenue1.id}, + {'flag': 'tax_line', 'balance': -20.0, 'account_id': tax_account.id}, + ]) + + self.assertRecordValues(wizard, [{'state': 'valid'}]) + + wizard._action_validate() + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'balance': 120.0, 'tax_ids': [], 'tax_line_id': False, 'account_id': st_line.journal_id.default_account_id.id}, + {'balance': -100.0, 'tax_ids': caba_tax.ids, 'tax_line_id': False, 'account_id': self.account_revenue1.id}, + {'balance': -20.0, 'tax_ids': [], 'tax_line_id': caba_tax.id, 'account_id': tax_account.id}, + ]) + self.assertRecordValues(wizard, [{'state': 'reconciled'}]) + + def test_validation_changed_default_account(self): + st_line = self._create_st_line(100.0, partner_id=self.partner_a.id) + original_journal_account_id = st_line.journal_id.default_account_id + # Change the default account of the journal (exceptional case) + st_line.journal_id.default_account_id = self.company_data['default_journal_cash'].default_account_id + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + self.assertRecordValues(wizard, [{'state': 'valid'}]) + # Validate and check the statement line. + wizard._action_validate() + liquidity_line, _suspense_line, _other_line = st_line._seek_for_lines() + self.assertRecordValues(liquidity_line, [ + {'account_id': original_journal_account_id.id, 'balance': 100.0}, + ]) + self.assertRecordValues(wizard, [{'state': 'reconciled'}]) + + def test_apply_taxes_with_reco_model(self): + st_line = self._create_st_line(1000.0) + + tax_21 = self.env['account.tax'].create({ + 'name': "tax_21", + 'amount': 21, + }) + + reco_model = self.env['account.reconcile.model'].create({ + 'name': "test_apply_taxes_with_reco_model", + 'rule_type': 'writeoff_button', + 'line_ids': [Command.create({ + 'account_id': self.account_revenue1.id, + 'tax_ids': [Command.set(tax_21.ids)], + })], + }) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_select_reconcile_model(reco_model) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1000.0}, + {'flag': 'manual', 'balance': -826.45}, + {'flag': 'tax_line', 'balance': -173.55}, + ]) + + def test_percentage_st_line_with_reco_model(self): + journal_curr = self.other_currency + foreign_curr = self.other_currency_2 + self.company_data['default_journal_bank'].currency_id = journal_curr + + # Setup triple currency. + st_line = self._create_st_line( + 1000.0, + date='2018-01-01', + foreign_currency_id=foreign_curr.id, + amount_currency=4000.0, + ) + + reco_model = self.env['account.reconcile.model'].create({ + 'name': "test_percentage_st_line_with_reco_model", + 'rule_type': 'writeoff_button', + 'line_ids': [ + Command.create({ + 'amount_type': 'percentage_st_line', + 'amount_string': str(percentage), + 'label': str(i), + 'account_id': self.account_revenue1.id, + }) + for i, percentage in enumerate((74.0, 24.0, 12.0, -10.0)) + ], + }) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_select_reconcile_model(reco_model) + + self.assertRecordValues(wizard.line_ids, [ + {'flag': 'liquidity', 'currency_id': journal_curr.id, 'amount_currency': 1000.0, 'balance': 500.0}, + {'flag': 'manual', 'currency_id': journal_curr.id, 'amount_currency': -740.0, 'balance': -370.0}, + {'flag': 'manual', 'currency_id': journal_curr.id, 'amount_currency': -240.0, 'balance': -120.0}, + {'flag': 'manual', 'currency_id': journal_curr.id, 'amount_currency': -120.0, 'balance': -60.0}, + {'flag': 'manual', 'currency_id': journal_curr.id, 'amount_currency': 100.0, 'balance': 50.0}, + ]) + + def test_manual_edits_not_replaced(self): + """ 2 partial payments should keep the edited balance """ + st_line = self._create_st_line( + 1200.0, + date='2017-02-01', + ) + inv_line_1 = self._create_invoice_line( + 'out_invoice', + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 3000.0}], + ) + inv_line_2 = self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 4000.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line_1) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1200.0}, + {'flag': 'new_aml', 'balance':-1200.0}, + ]) + + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + line.balance = -600.0 + wizard._line_value_changed_balance(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1200.0}, + {'flag': 'new_aml', 'balance': -600.0}, + {'flag': 'auto_balance', 'balance': -600.0}, + ]) + + wizard._action_add_new_amls(inv_line_2) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 1200.0}, + {'flag': 'new_aml', 'balance': -600.0}, + {'flag': 'new_aml', 'balance': -600.0}, + ]) + + def test_manual_edits_not_replaced_multicurrency(self): + """ 2 partial payments should keep the edited amount_currency """ + st_line = self._create_st_line( + 1200.0, + date='2018-01-01', + foreign_currency_id=self.other_currency_2.id, + amount_currency=6000.0, # rate 5:1 + ) + + inv_line_1 = self._create_invoice_line( + 'out_invoice', + invoice_date='2016-01-01', + currency_id=self.other_currency_2.id, + invoice_line_ids=[{'price_unit': 6000.0}], # 1000 company curr (rate 6:1) + ) + inv_line_2 = self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-01', + currency_id=self.other_currency_2.id, + invoice_line_ids=[{'price_unit': 4000.0}], # 1000 company curr (rate 4:1) + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line_1) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency':-6000.0, 'balance':-1000.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -200.0}, + ]) + + line = wizard.line_ids.filtered(lambda x: x.flag == 'new_aml') + wizard._js_action_mount_line_in_edit(line.index) + line.amount_currency = -3000.0 + wizard._line_value_changed_amount_currency(line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency':-3000.0, 'balance': -500.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -100.0}, + {'flag': 'auto_balance', 'amount_currency':-3000.0, 'balance': -600.0}, + ]) + + wizard._action_add_new_amls(inv_line_2) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'balance': 1200.0}, + {'flag': 'new_aml', 'amount_currency':-3000.0, 'balance': -500.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -100.0}, + {'flag': 'new_aml', 'amount_currency':-3000.0, 'balance': -750.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': 150.0}, + ]) + + def test_creating_manual_line_multi_currencies(self): + # 6300.0 curr2 == 1800.0 comp_curr (bank rate 3.5:1 instead of the odoo rate 4:1) + st_line = self._create_st_line( + 1800.0, + date='2017-01-01', + foreign_currency_id=self.other_currency_2.id, + amount_currency=6300.0, + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'auto_balance', 'amount_currency': -6300.0, 'currency_id': self.other_currency_2.id, 'balance': -1800.0}, + ]) + + # Custom balance. + line = wizard.line_ids.filtered(lambda x: x.flag == 'auto_balance') + wizard._js_action_mount_line_in_edit(line.index) + line.balance = -1500.0 + wizard._line_value_changed_balance(line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'manual', 'amount_currency': -6300.0, 'currency_id': self.other_currency_2.id, 'balance': -1500.0}, + {'flag': 'auto_balance', 'amount_currency': 0.0, 'currency_id': self.other_currency_2.id, 'balance': -300.0}, + ]) + + # Custom amount_currency. + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.amount_currency = -4200.0 + wizard._line_value_changed_amount_currency(line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'manual', 'amount_currency': -4200.0, 'currency_id': self.other_currency_2.id, 'balance': -1200.0}, + {'flag': 'auto_balance', 'amount_currency': -2100.0, 'currency_id': self.other_currency_2.id, 'balance': -600.0}, + ]) + + # Custom currency_id. + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.currency_id = self.other_currency + wizard._line_value_changed_currency_id(line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'manual', 'amount_currency': -4200.0, 'currency_id': self.other_currency.id, 'balance': -2100.0}, + {'flag': 'auto_balance', 'amount_currency': 1050.0, 'currency_id': self.other_currency_2.id, 'balance': 300.0}, + ]) + + # Custom balance. + line = wizard.line_ids.filtered(lambda x: x.flag == 'manual') + wizard._js_action_mount_line_in_edit(line.index) + line.balance = -1800.0 + wizard._line_value_changed_balance(line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'currency_id': self.company_data['currency'].id, 'balance': 1800.0}, + {'flag': 'manual', 'amount_currency': -4200.0, 'currency_id': self.other_currency.id, 'balance': -1800.0}, + ]) + + def test_auto_reconcile_cron(self): + self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() + cron = self.env.ref('at_accounting.auto_reconcile_bank_statement_line') + self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)]).unlink() + + st_line = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01') + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 1) + + self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 1234.0}], + ) + + rule = self.env['account.reconcile.model'].create({ + 'name': "test_auto_reconcile_cron", + 'rule_type': 'writeoff_suggestion', + 'auto_reconcile': False, + 'line_ids': [Command.create({'account_id': self.account_revenue1.id})], + }) + + # The CRON is not doing anything since the model is not auto reconcile. + with freeze_time('2017-01-01'): + self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines() + self.assertRecordValues(st_line, [{'is_reconciled': False, 'cron_last_check': False}]) + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 1) + + rule.auto_reconcile = True + + # The CRON don't consider old statement lines. + with freeze_time('2017-06-01'): + self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines() + self.assertRecordValues(st_line, [{'is_reconciled': False, 'cron_last_check': False}]) + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 1) + + # The CRON will auto-reconcile the line. + with freeze_time('2017-01-02'): + self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines() + self.assertRecordValues(st_line, [{'is_reconciled': True, 'cron_last_check': fields.Datetime.from_string('2017-01-02 00:00:00')}]) + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 1) + + st_line1 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2018-01-01') + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 2) + self._create_invoice_line( + 'out_invoice', + invoice_date='2018-01-01', + invoice_line_ids=[{'price_unit': 1234.0}], + ) + st_line2 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2018-01-01') + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 3) + self._create_invoice_line( + 'out_invoice', + invoice_date='2018-01-01', + invoice_line_ids=[{'price_unit': 1234.0}], + ) + + # Simulate the cron already tried to process 'st_line1' before. + with freeze_time('2017-12-31'): + st_line1.cron_last_check = fields.Datetime.now() + + # The statement line with no 'cron_last_check' must be processed before others. + with freeze_time('2018-01-02'): + self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(batch_size=1) + + self.assertRecordValues(st_line1 + st_line2, [ + {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2017-12-31 00:00:00')}, + {'is_reconciled': True, 'cron_last_check': fields.Datetime.from_string('2018-01-02 00:00:00')}, + ]) + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 4) + + with freeze_time('2018-01-03'): + self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(batch_size=1) + + self.assertRecordValues(st_line1, [{'is_reconciled': True, 'cron_last_check': fields.Datetime.from_string('2018-01-03 00:00:00')}]) + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 4) + + st_line3 = self._create_st_line(1234.0, date='2018-01-01') + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 5) + self._create_invoice_line( + 'out_invoice', + invoice_date='2018-01-01', + invoice_line_ids=[{'price_unit': 1234.0}], + ) + st_line4 = self._create_st_line(1234.0, date='2018-01-01') + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 6) + self._create_invoice_line( + 'out_invoice', + invoice_date='2018-01-01', + invoice_line_ids=[{'price_unit': 1234.0}], + ) + + # Make sure the CRON is no longer applicable. + rule.match_partner = True + rule.match_partner_ids = [Command.set(self.partner_a.ids)] + with freeze_time('2018-01-01'): + self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(batch_size=1) + + self.assertRecordValues(st_line3 + st_line4, [ + {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-01 00:00:00')}, + {'is_reconciled': False, 'cron_last_check': False}, + ]) + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 7) + + # Make sure the statement lines are reconciled by the cron in the right order. + self.assertRecordValues(st_line3 + st_line4, [ + {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-01 00:00:00')}, + {'is_reconciled': False, 'cron_last_check': False}, + ]) + + # st_line4 is processed because cron_last_check is null. + with freeze_time('2018-01-02'): + self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(batch_size=1) + + self.assertRecordValues(st_line3 + st_line4, [ + {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-01 00:00:00')}, + {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-02 00:00:00')}, + ]) + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 7) + + # st_line3 is processed because it has the oldest cron_last_check. + with freeze_time('2018-01-03'): + self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(batch_size=1) + + self.assertRecordValues(st_line3 + st_line4, [ + {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-03 00:00:00')}, + {'is_reconciled': False, 'cron_last_check': fields.Datetime.from_string('2018-01-02 00:00:00')}, + ]) + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 7) + + def test_duplicate_amls_constraint(self): + st_line = self._create_st_line(1000.0) + inv_line = self._create_invoice_line( + 'out_invoice', + invoice_line_ids=[{'price_unit': 1000.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + self.assertTrue(len(wizard.line_ids), 2) + + wizard._action_add_new_amls(inv_line) + self.assertTrue(len(wizard.line_ids), 2) + + @freeze_time('2017-01-01') + def test_reconcile_model_with_payment_tolerance(self): + self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() + + invoice_line = self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 1000.0}], + ) + st_line = self._create_st_line(998.0, partner_id=self.partner_a.id, date='2017-01-01', payment_ref=invoice_line.move_id.name) + + rule = self.env['account.reconcile.model'].create({ + 'name': "test_reconcile_model_with_payment_tolerance", + 'rule_type': 'invoice_matching', + 'allow_payment_tolerance': True, + 'payment_tolerance_type': 'percentage', + 'payment_tolerance_param': 2.0, + 'line_ids': [Command.create({'account_id': self.account_revenue1.id})], + }) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_trigger_matching_rules() + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 998.0, 'reconcile_model_id': False}, + {'flag': 'new_aml', 'balance': -1000.0, 'reconcile_model_id': rule.id}, + {'flag': 'manual', 'balance': 2.0, 'reconcile_model_id': rule.id}, + ]) + + @freeze_time('2017-01-01') + def test_auto_reconcile_model_with_archived_partner(self): + self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() + self.env['res.partner.bank'].search([('company_id', '=', self.company_data['company'].id)]).unlink() + + # Needed as partner_a does not have a company and we need a company in order to find matching partner from account_number + self.partner_a.company_id = self.company_data['company'].id + self.env['res.partner.bank'].create({ + 'partner_id': self.partner_a.id, + 'acc_number': '12345', + }) + invoice_line = self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 1000.0}], + ) + st_line = self._create_st_line(1000.0, account_number='12345', date='2017-01-01', payment_ref=invoice_line.move_id.name) + + self.env['account.reconcile.model'].create({ + 'name': "test_reconcile_model_with_archived_partner", + 'rule_type': 'invoice_matching', + 'auto_reconcile': True, + 'match_partner': True, + }) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_trigger_matching_rules() + self.assertEqual(wizard.partner_id, self.partner_a) + self.assertTrue(wizard.matching_rules_allow_auto_reconcile) + + # archive partner and trigger again, partner should still be set (match based on account_number), because it's the only match + self.partner_a.active = False + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_trigger_matching_rules() + self.assertEqual(wizard.partner_id, self.partner_a) # Partner should still be set + self.assertTrue(wizard.matching_rules_allow_auto_reconcile) # no special process because the partner is unactive + + def test_early_payment_included_multi_currency(self): + self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() + self.early_payment_term.early_pay_discount_computation = 'included' + income_exchange_account = self.env.company.income_currency_exchange_account_id + expense_exchange_account = self.env.company.expense_currency_exchange_account_id + + inv_line1_with_epd = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + partner_id=self.partner_a.id, + invoice_payment_term_id=self.early_payment_term.id, + invoice_date='2016-12-01', + invoice_line_ids=[ + { + 'price_unit': 4800.0, + 'account_id': self.account_revenue1.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + { + 'price_unit': 9600.0, + 'account_id': self.account_revenue2.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + ], + ) + inv_line1_with_epd_rec_lines = inv_line1_with_epd.move_id.line_ids\ + .filtered(lambda x: x.account_type == 'asset_receivable')\ + .sorted(lambda x: x.discount_date or x.date_maturity) + self.assertRecordValues( + inv_line1_with_epd_rec_lines, + [ + { + 'amount_currency': 16560.0, + 'balance': 2760.0, + 'discount_amount_currency': 14904.0, + 'discount_balance': 2484.0, + 'discount_date': fields.Date.from_string('2016-12-11'), + 'date_maturity': fields.Date.from_string('2016-12-21'), + }, + ], + ) + + inv_line2_with_epd = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + partner_id=self.partner_a.id, + invoice_payment_term_id=self.early_payment_term.id, + invoice_date='2017-01-20', + invoice_line_ids=[ + { + 'price_unit': 480.0, + 'account_id': self.account_revenue1.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + { + 'price_unit': 960.0, + 'account_id': self.account_revenue2.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + ], + ) + inv_line2_with_epd_rec_lines = inv_line2_with_epd.move_id.line_ids\ + .filtered(lambda x: x.account_type == 'asset_receivable')\ + .sorted(lambda x: x.discount_date or x.date_maturity) + self.assertRecordValues( + inv_line2_with_epd_rec_lines, + [ + { + 'amount_currency': 1656.0, + 'balance': 414.0, + 'discount_amount_currency': 1490.4, + 'discount_balance': 372.6, + 'discount_date': fields.Date.from_string('2017-01-30'), + 'date_maturity': fields.Date.from_string('2017-02-09'), + }, + ], + ) + + # inv1: 16560.0 (no epd) + # inv2: 1490.4 (epd) + st_line = self._create_st_line( + 4512.0, # instead of 4512.6 (rate 1:4) + date='2017-01-04', + foreign_currency_id=self.other_currency_2.id, + amount_currency=18050.4, + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + + # Add all lines from the first invoice plus the first one from the second one. + wizard._action_add_new_amls(inv_line1_with_epd_rec_lines + inv_line2_with_epd_rec_lines) + liquidity_acc = st_line.journal_id.default_account_id + receivable_acc = self.company_data['default_account_receivable'] + early_pay_acc = self.env.company.account_journal_early_pay_discount_loss_account_id + tax_acc = self.company_data['default_tax_sale'].invoice_repartition_line_ids.account_id + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 4512.0, 'balance': 4512.0, 'account_id': liquidity_acc.id}, + {'flag': 'new_aml', 'amount_currency': -16560.0, 'balance': -2760.0, 'account_id': receivable_acc.id}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -1379.45, 'account_id': income_exchange_account.id}, + {'flag': 'new_aml', 'amount_currency': -1656.0, 'balance': -414.0, 'account_id': receivable_acc.id}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': 0.06, 'account_id': expense_exchange_account.id}, + {'flag': 'early_payment', 'amount_currency': 144.0, 'balance': 36.0, 'account_id': early_pay_acc.id}, + {'flag': 'early_payment', 'amount_currency': 21.6, 'balance': 5.4, 'account_id': tax_acc.id}, + {'flag': 'early_payment', 'amount_currency': 0.0, 'balance': -0.01, 'account_id': income_exchange_account.id}, + ]) + + def test_early_payment_excluded_multi_currency(self): + self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() + self.early_payment_term.early_pay_discount_computation = 'excluded' + income_exchange_account = self.env.company.income_currency_exchange_account_id + expense_exchange_account = self.env.company.expense_currency_exchange_account_id + + inv_line1_with_epd = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + partner_id=self.partner_a.id, + invoice_payment_term_id=self.early_payment_term.id, + invoice_date='2016-12-01', + invoice_line_ids=[ + { + 'price_unit': 4800.0, + 'account_id': self.account_revenue1.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + { + 'price_unit': 9600.0, + 'account_id': self.account_revenue2.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + ], + ) + inv_line1_with_epd_rec_lines = inv_line1_with_epd.move_id.line_ids\ + .filtered(lambda x: x.account_type == 'asset_receivable')\ + .sorted(lambda x: x.discount_date or x.date_maturity) + self.assertRecordValues( + inv_line1_with_epd_rec_lines, + [ + { + 'amount_currency': 16560.0, + 'balance': 2760.0, + 'discount_amount_currency': 15120.0, + 'discount_balance': 2520.0, + 'discount_date': fields.Date.from_string('2016-12-11'), + 'date_maturity': fields.Date.from_string('2016-12-21'), + }, + ], + ) + + inv_line2_with_epd = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + partner_id=self.partner_a.id, + invoice_payment_term_id=self.early_payment_term.id, + invoice_date='2017-01-20', + invoice_line_ids=[ + { + 'price_unit': 480.0, + 'account_id': self.account_revenue1.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + { + 'price_unit': 960.0, + 'account_id': self.account_revenue2.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + ], + ) + inv_line2_with_epd_rec_lines = inv_line2_with_epd.move_id.line_ids\ + .filtered(lambda x: x.account_type == 'asset_receivable')\ + .sorted(lambda x: x.discount_date or x.date_maturity) + self.assertRecordValues( + inv_line2_with_epd_rec_lines, + [ + { + 'amount_currency': 1656.0, + 'balance': 414.0, + 'discount_amount_currency': 1512.0, + 'discount_balance': 378.0, + 'discount_date': fields.Date.from_string('2017-01-30'), + 'date_maturity': fields.Date.from_string('2017-02-09'), + }, + ], + ) + + # inv1: 16560.0 (no epd) + # inv2: 1512.0 (epd) + st_line = self._create_st_line( + 4515.0, # instead of 4518.0 (rate 1:4) + date='2017-01-04', + foreign_currency_id=self.other_currency_2.id, + amount_currency=18072.0, + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + + # Add all lines from the first invoice plus the first one from the second one. + wizard._action_add_new_amls(inv_line1_with_epd_rec_lines + inv_line2_with_epd_rec_lines[:2]) + liquidity_acc = st_line.journal_id.default_account_id + receivable_acc = self.company_data['default_account_receivable'] + early_pay_acc = self.env.company.account_journal_early_pay_discount_loss_account_id + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 4515.0, 'balance': 4515.0, 'account_id': liquidity_acc.id}, + {'flag': 'new_aml', 'amount_currency': -16560.0, 'balance': -2760.0, 'account_id': receivable_acc.id}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -1377.25, 'account_id': income_exchange_account.id}, + {'flag': 'new_aml', 'amount_currency': -1656.0, 'balance': -414.0, 'account_id': receivable_acc.id}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': 0.27, 'account_id': expense_exchange_account.id}, + {'flag': 'early_payment', 'amount_currency': 144.0, 'balance': 36.0, 'account_id': early_pay_acc.id}, + {'flag': 'early_payment', 'amount_currency': 0.0, 'balance': -0.02, 'account_id': income_exchange_account.id}, + ]) + + def test_early_payment_mixed_multi_currency(self): + self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() + self.early_payment_term.early_pay_discount_computation = 'mixed' + income_exchange_account = self.env.company.income_currency_exchange_account_id + expense_exchange_account = self.env.company.expense_currency_exchange_account_id + + inv_line1_with_epd = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + partner_id=self.partner_a.id, + invoice_payment_term_id=self.early_payment_term.id, + invoice_date='2016-12-01', + invoice_line_ids=[ + { + 'price_unit': 4800.0, + 'account_id': self.account_revenue1.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + { + 'price_unit': 9600.0, + 'account_id': self.account_revenue2.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + ], + ) + inv_line1_with_epd_rec_lines = inv_line1_with_epd.move_id.line_ids\ + .filtered(lambda x: x.account_type == 'asset_receivable')\ + .sorted(lambda x: x.discount_date or x.date_maturity) + self.assertRecordValues( + inv_line1_with_epd_rec_lines, + [ + { + 'amount_currency': 16344.0, + 'balance': 2724.0, + 'discount_amount_currency': 14904.0, + 'discount_balance': 2484.0, + 'discount_date': fields.Date.from_string('2016-12-11'), + 'date_maturity': fields.Date.from_string('2016-12-21'), + }, + ], + ) + + inv_line2_with_epd = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + partner_id=self.partner_a.id, + invoice_payment_term_id=self.early_payment_term.id, + invoice_date='2017-01-20', + invoice_line_ids=[ + { + 'price_unit': 480.0, + 'account_id': self.account_revenue1.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + { + 'price_unit': 960.0, + 'account_id': self.account_revenue2.id, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + }, + ], + ) + inv_line2_with_epd_rec_lines = inv_line2_with_epd.move_id.line_ids\ + .filtered(lambda x: x.account_type == 'asset_receivable')\ + .sorted(lambda x: x.discount_date or x.date_maturity) + self.assertRecordValues( + inv_line2_with_epd_rec_lines, + [ + { + 'amount_currency': 1634.4, + 'balance': 408.6, + 'discount_amount_currency': 1490.4, + 'discount_balance': 372.6, + 'discount_date': fields.Date.from_string('2017-01-30'), + 'date_maturity': fields.Date.from_string('2017-02-09'), + }, + ], + ) + + # inv1: 16344.0 (no epd) + # inv2: 1490.4 (epd) + st_line = self._create_st_line( + 4458.0, # instead of 4458.6 (rate 1:4) + date='2017-01-04', + foreign_currency_id=self.other_currency_2.id, + amount_currency=17834.4, + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + + # Add all lines from the first invoice plus the first one from the second one. + wizard._action_add_new_amls(inv_line1_with_epd_rec_lines + inv_line2_with_epd_rec_lines[:2]) + liquidity_acc = st_line.journal_id.default_account_id + receivable_acc = self.company_data['default_account_receivable'] + early_pay_acc = self.env.company.account_journal_early_pay_discount_loss_account_id + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 4458.0, 'balance': 4458.0, 'account_id': liquidity_acc.id}, + {'flag': 'new_aml', 'amount_currency': -16344.0, 'balance': -2724.0, 'account_id': receivable_acc.id}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -1361.45, 'account_id': income_exchange_account.id}, + {'flag': 'new_aml', 'amount_currency': -1634.4, 'balance': -408.6, 'account_id': receivable_acc.id}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': 0.05, 'account_id': expense_exchange_account.id}, + {'flag': 'early_payment', 'amount_currency': 144.0, 'balance': 36.0, 'account_id': early_pay_acc.id}, + ]) + + def test_early_payment_included_intracomm_bill(self): + tax_tags = self.env['account.account.tag'].create({ + 'name': f'tax_tag_{i}', + 'applicability': 'taxes', + 'country_id': self.env.company.account_fiscal_country_id.id, + } for i in range(6)) + + intracomm_tax = self.env['account.tax'].create({ + 'name': 'tax20', + 'amount_type': 'percent', + 'amount': 20, + 'type_tax_use': 'purchase', + 'invoice_repartition_line_ids': [ + # pylint: disable=bad-whitespace + Command.create({'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[0].ids)]}), + Command.create({'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[1].ids)]}), + Command.create({'repartition_type': 'tax', 'factor_percent': -100.0, 'tag_ids': [Command.set(tax_tags[2].ids)]}), + ], + 'refund_repartition_line_ids': [ + # pylint: disable=bad-whitespace + Command.create({'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[3].ids)]}), + Command.create({'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [Command.set(tax_tags[4].ids)]}), + Command.create({'repartition_type': 'tax', 'factor_percent': -100.0, 'tag_ids': [Command.set(tax_tags[5].ids)]}), + ], + }) + + early_payment_term = self.env['account.payment.term'].create({ + 'name': "early_payment_term", + 'company_id': self.company_data['company'].id, + 'early_pay_discount_computation': 'included', + 'early_discount': True, + 'discount_percentage': 2, + 'discount_days': 7, + 'line_ids': [ + Command.create({ + 'value': 'percent', + 'value_amount': 100.0, + 'nb_days': 30, + }), + ], + }) + + bill = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'partner_id': self.partner_a.id, + 'invoice_payment_term_id': early_payment_term.id, + 'invoice_date': '2019-01-01', + 'date': '2019-01-01', + 'invoice_line_ids': [ + Command.create({ + 'name': 'line', + 'price_unit': 1000.0, + 'tax_ids': [Command.set(intracomm_tax.ids)], + }), + ], + }) + bill.action_post() + + st_line = self._create_st_line( + -980.0, + date='2017-01-01', + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(bill.line_ids.filtered(lambda x: x.account_type == 'liability_payable')) + wizard._action_validate() + + self.assertRecordValues(st_line.line_ids.sorted('balance'), [ + # pylint: disable=bad-whitespace + {'amount_currency': -980.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_tag_invert': False}, + {'amount_currency': -20.0, 'tax_ids': intracomm_tax.ids, 'tax_tag_ids': tax_tags[3].ids, 'tax_tag_invert': True}, + {'amount_currency': -4.0, 'tax_ids': [], 'tax_tag_ids': tax_tags[4].ids, 'tax_tag_invert': True}, + {'amount_currency': 4.0, 'tax_ids': [], 'tax_tag_ids': tax_tags[5].ids, 'tax_tag_invert': True}, + {'amount_currency': 1000.0, 'tax_ids': [], 'tax_tag_ids': [], 'tax_tag_invert': False}, + ]) + + def test_multi_currencies_with_custom_rate(self): + self.company_data['default_journal_bank'].currency_id = self.other_currency + st_line = self._create_st_line(1200.0) # rate 1:2 + self.assertRecordValues(st_line.move_id.line_ids, [ + # pylint: disable=C0326 + {'amount_currency': 1200.0, 'balance': 600.0}, + {'amount_currency': -1200.0, 'balance': -600.0}, + ]) + + # invoice with other_currency and rate 1:2 + invoice_line1 = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 300.0}], # = 150 USD + ) + + # Remove all rates. + self.other_currency.rate_ids.unlink() + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'balance': 600.0}, + {'flag': 'auto_balance', 'amount_currency': -1200.0, 'balance': -600.0}, + ]) + + # invoice with other_currency_2 and rate 1:6 + invoice_line2 = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 600.0}], # = 100 USD + ) + # invoice with other_currency_2 and rate 1:4 + invoice_line3 = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency_2.id, + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 400.0}], # = 100 USD + ) + + # Remove all rates. + self.other_currency_2.rate_ids.unlink() + + # Ensure no conversion rate has been made. + wizard._action_add_new_amls(invoice_line1 + invoice_line2 + invoice_line3) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'balance': 600.0}, + {'flag': 'new_aml', 'amount_currency': -300.0, 'balance': -150.0}, + {'flag': 'new_aml', 'amount_currency': -600.0, 'balance': -100.0}, + {'flag': 'new_aml', 'amount_currency': -400.0, 'balance': -100.0}, + {'flag': 'auto_balance', 'amount_currency': -500.0, 'balance': -250.0}, + ]) + + def test_partial_reconciliation_suggestion_with_mixed_invoice_and_refund(self): + """ Test the partial reconciliation suggestion is well recomputed when adding another + line. For example, when adding 2 invoices having an higher amount then a refund. In that + case, the partial on the second invoice should be removed since the difference is filled + by the newly added refund. + """ + st_line = self._create_st_line( + 1800.0, + date='2017-01-01', + foreign_currency_id=self.other_currency.id, + amount_currency=3600.0, + ) + + inv1 = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 2400.0}], + ) + inv2 = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 2400.0}], + ) + refund = self._create_invoice_line( + 'out_refund', + currency_id=self.other_currency.id, + invoice_date='2016-01-01', + invoice_line_ids=[{'price_unit': 1200.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv1 + inv2) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'balance': 1800.0}, + {'flag': 'new_aml', 'amount_currency': -2400.0, 'balance': -800.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -400.0}, + {'flag': 'new_aml', 'amount_currency': -1200.0, 'balance': -400.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -200.0}, + ]) + wizard._action_add_new_amls(refund) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1800.0, 'balance': 1800.0}, + {'flag': 'new_aml', 'amount_currency': -2400.0, 'balance': -800.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -400.0}, + {'flag': 'new_aml', 'amount_currency': -2400.0, 'balance': -800.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': -400.0}, + {'flag': 'new_aml', 'amount_currency': 1200.0, 'balance': 400.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'balance': 200.0}, + ]) + + def test_auto_reconcile_cron_with_time_limit(self): + self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() + cron = self.env.ref('at_accounting.auto_reconcile_bank_statement_line') + self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)]).unlink() + + st_line1 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01') + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 1) + st_line2 = self._create_st_line(5678.0, partner_id=self.partner_a.id, date='2017-01-02') + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 2) + + self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 1234.0}], + ) + self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 5678.0}], + ) + self.env['account.reconcile.model'].create({ + 'name': "test_auto_reconcile_cron_with_time_limit", + 'rule_type': 'writeoff_suggestion', + 'auto_reconcile': True, + 'line_ids': [Command.create({'account_id': self.account_revenue1.id})], + }) + + with freeze_time('2017-01-01 00:00:00') as frozen_time: + def datetime_now_override(): + frozen_time.tick() + return frozen_time() + with patch('odoo.fields.Datetime.now', side_effect=datetime_now_override): + # we simulate that the time limit is reached after first loop + self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(limit_time=1) + # after first loop, only one statement should be reconciled + self.assertRecordValues(st_line1, [{'is_reconciled': True, 'cron_last_check': fields.Datetime.from_string('2017-01-01 00:00:01')}]) + # the other one should be in queue for regular cron tigger + self.assertRecordValues(st_line2, [{'is_reconciled': False, 'cron_last_check': False}]) + self.assertEqual(len(self.env['ir.cron.trigger'].search([('cron_id', '=', cron.id)])), 3) + + def test_auto_reconcile_cron_with_provided_statements_lines(self): + self.env['account.reconcile.model'].search([('company_id', '=', self.company_data['company'].id)]).unlink() + + st_line1 = self._create_st_line(1234.0, partner_id=self.partner_a.id, date='2017-01-01') + st_line2 = self._create_st_line(5678.0, partner_id=self.partner_a.id, date='2017-01-02') + self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 1234.0}], + ) + self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 5678.0}], + ) + self.env['account.reconcile.model'].create({ + 'name': "test_auto_reconcile_cron_with_time_limit", + 'rule_type': 'writeoff_suggestion', + 'auto_reconcile': True, + 'line_ids': [Command.create({'account_id': self.account_revenue1.id})], + }) + with freeze_time('2017-01-01 00:00:00'): + # we call auto reconcile on st_lines1 **only** + st_line1._cron_try_auto_reconcile_statement_lines() + self.assertRecordValues(st_line1, [{'is_reconciled': True, 'cron_last_check': fields.Datetime.from_string('2017-01-01 00:00:00')}]) + self.assertRecordValues(st_line2, [{'is_reconciled': False, 'cron_last_check': False}]) + + @freeze_time('2019-01-01') + def test_button_apply_reco_model(self): + inv_line = self._create_invoice_line( + 'in_invoice', + invoice_date='2019-01-01', + invoice_line_ids=[{'price_unit': 980.0}], + ) + st_line = self._create_st_line(-1000.0, partner_id=self.partner_a.id, date=inv_line.date, payment_ref=inv_line.move_name) + + reco_model = self.env['account.reconcile.model'].create({ + 'name': "test_apply_taxes_with_reco_model", + 'rule_type': 'writeoff_button', + 'line_ids': [Command.create({ + 'account_id': self.account_revenue1.copy().id, + 'label': 'Bank Fees' + })], + }) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_trigger_matching_rules() + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'account_id': st_line.journal_id.default_account_id.id, 'balance': -1000.0}, + {'flag': 'new_aml', 'account_id': inv_line.account_id.id, 'balance': 980.0}, + {'flag': 'auto_balance', 'account_id': self.company_data['default_account_payable'].id, 'balance': 20.0}, + ]) + + wizard._action_select_reconcile_model(reco_model) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'account_id': st_line.journal_id.default_account_id.id, 'balance': -1000.0}, + {'flag': 'new_aml', 'account_id': inv_line.account_id.id, 'balance': 980.0}, + {'flag': 'manual', 'account_id': reco_model.line_ids[0].account_id.id, 'balance': 20.0}, + ]) + + def test_exchange_diff_on_partial_aml_multi_currency(self): + self.company_data['default_journal_bank'].currency_id = self.other_currency + st_line = self._create_st_line(-36000.0) # rate 1:2 + inv_line = self._create_invoice_line( + 'in_invoice', + invoice_date='2016-01-01', # rate 1:3 + currency_id=self.other_currency.id, + invoice_line_ids=[{'price_unit': 38000.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': -36000.0, 'currency_id': self.other_currency.id, 'balance': -18000.0}, + {'flag': 'new_aml', 'amount_currency': 36000.0, 'currency_id': self.other_currency.id, 'balance': 12000.0}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 6000.0}, + ]) + + def test_exchange_diff_on_partial_aml_multi_currency_close_amount(self): + self.other_currency.rate_ids.rate = 0.9839 + self.company_data['default_journal_bank'].currency_id = self.other_currency + + st_line = self._create_st_line(-37436.50) + self.assertRecordValues(st_line.line_ids, [ + # pylint: disable=C0326 + {'amount_currency': -37436.50, 'balance': -38049.09}, + {'amount_currency': 37436.50, 'balance': 38049.09}, + ]) + + inv_line = self._create_invoice_line( + 'in_invoice', + invoice_date=st_line.date, + currency_id=self.other_currency.id, + invoice_line_ids=[{'price_unit': 37436.52}], + ) + self.assertRecordValues(inv_line, [{ + 'amount_currency': -37436.52, + 'balance': -38049.11, + }]) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': -37436.50, 'currency_id': self.other_currency.id, 'balance': -38049.09}, + {'flag': 'new_aml', 'amount_currency': 37436.50, 'currency_id': self.other_currency.id, 'balance': 38049.09}, + ]) + + def test_matching_zero_amount_misc_entry(self): + """ Check for division by zero with foreign currencies and some 0 making a broken rate. """ + self.company_data['default_journal_bank'].currency_id = self.other_currency + st_line = self._create_st_line(0.0, amount_currency=10.0, foreign_currency_id=self.company_data['currency'].id) + + entry = self.env['account.move'].create({ + 'date': '2019-01-01', + 'line_ids': [ + Command.create({ + 'account_id': self.company_data['default_account_receivable'].id, + 'currency_id': self.other_currency.id, + 'debit': 1.0, + 'credit': 0.0, + }), + Command.create({ + 'account_id': self.company_data['default_account_revenue'].id, + 'currency_id': self.other_currency.id, + 'debit': 0.0, + 'credit': 1.0, + }), + ] + }) + entry.action_post() + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + aml = entry.line_ids.filtered('debit') + wizard._action_add_new_amls(aml) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'balance': 10.0}, + {'flag': 'new_aml', 'balance': -1.0}, + {'flag': 'exchange_diff', 'balance': 1.0}, + {'flag': 'auto_balance', 'balance': -10.0}, + ]) + + def test_amls_order_with_matching_amount(self): + """ AML's with a matching amount_residual should be displayed first when the order is not specified. """ + + foreign_st_line = self._create_st_line( + 500.0, + date='2016-01-01', + foreign_currency_id=self.other_currency.id, + amount_currency=1500.0, + ) + st_line = self._create_st_line( + 66.66, + date='2016-01-01', + ) + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=foreign_st_line.id).new({}) + + aml1_id = self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-30', + invoice_line_ids=[{'price_unit': 1000.0}], + ).id + aml2_id = self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-29', + currency_id=self.other_currency.id, + invoice_line_ids=[{'price_unit': 1500.0}], # = 100 USD + ).id + aml3_id = self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-28', + invoice_line_ids=[{'price_unit': 500.0}], + ).id + aml4_id = self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-27', + invoice_line_ids=[{'price_unit': 55.55}], # = 55.550000000000004 + ).id + aml5_id = self._create_invoice_line( + 'out_invoice', + invoice_date='2017-01-26', + invoice_line_ids=[{'price_unit': 66.66}], + ).id + + # Check the lines without the context key. + wizard._js_action_mount_st_line(foreign_st_line.id) + domain = wizard.return_todo_command['amls']['domain'] + amls_list = self.env['account.move.line'].search_fetch(domain=domain, field_names=['id']) + self.assertEqual( + [x['id'] for x in amls_list], + [aml1_id, aml2_id, aml3_id, aml4_id, aml5_id], + ) + + # Check the lines with the context key. + suspense_line = wizard.line_ids.filtered(lambda l: l.flag == 'auto_balance') + amls_list = self.env['account.move.line']\ + .with_context(preferred_aml_value=suspense_line.amount_currency * -1, preferred_aml_currency_id=suspense_line.currency_id.id)\ + .search_fetch(domain=domain, field_names=['id']) + self.assertEqual( + [x['id'] for x in amls_list], + [aml2_id, aml1_id, aml3_id, aml4_id, aml5_id], + ) + + # Check the order with limits and offsets + amls_list = self.env['account.move.line']\ + .with_context(preferred_aml_value=suspense_line.amount_currency * -1, preferred_aml_currency_id=suspense_line.currency_id.id)\ + .search_fetch(domain=domain, field_names=['id'], limit=2) + self.assertEqual( + [x['id'] for x in amls_list], + [aml2_id, aml1_id], + ) + amls_list = self.env['account.move.line']\ + .with_context(preferred_aml_value=suspense_line.amount_currency * -1, preferred_aml_currency_id=suspense_line.currency_id.id)\ + .search_fetch(domain=domain, field_names=['id'], offset=2, limit=3) + self.assertEqual( + [x['id'] for x in amls_list], + [aml3_id, aml4_id, aml5_id], + ) + + # Check rounding and new suspense line + wizard._js_action_mount_st_line(st_line.id) + suspense_line = wizard.line_ids.filtered(lambda l: l.flag == 'auto_balance') + amls_list = self.env['account.move.line']\ + .with_context(preferred_aml_value=suspense_line.amount_currency * -1, preferred_aml_currency_id=suspense_line.currency_id.id)\ + .search_fetch(domain=domain, field_names=['id']) + self.assertEqual( + [x['id'] for x in amls_list], + [aml5_id, aml1_id, aml2_id, aml3_id, aml4_id], + ) + wizard._js_action_mount_line_in_edit(suspense_line.index) + suspense_line.balance = -11.11 + wizard._line_value_changed_balance(suspense_line) + suspense_line = wizard.line_ids.filtered(lambda l: l.flag == 'auto_balance') + self.assertEqual(suspense_line.balance, -55.55) + self.env.cr.execute(f""" + UPDATE account_move_line SET amount_residual_currency = 55.550000001 WHERE id = {aml4_id}; + """) + amls_list = self.env['account.move.line']\ + .with_context(preferred_aml_value=55.550003, preferred_aml_currency_id=suspense_line.currency_id.id)\ + .search_fetch(domain=domain, field_names=['id']) + self.assertEqual( + [x['id'] for x in amls_list], + [aml4_id, aml1_id, aml2_id, aml3_id, aml5_id], + ) + + # Check that context keys are not propagated + action = amls_list[0].action_open_business_doc() + self.assertFalse(action['context'].get('preferred_aml_value')) + + @freeze_time('2023-12-25') + def test_analtyic_distribution_model_exchange_diff_line(self): + """Test that the analytic distribution model is present on the exchange diff line.""" + expense_exchange_account = self.env.company.expense_currency_exchange_account_id + analytic_plan = self.env['account.analytic.plan'].create({ + 'name': 'Plan 1', + 'default_applicability': 'unavailable', + }) + analytic_account_1 = self.env['account.analytic.account'].create({'name': 'Account 1', 'plan_id': analytic_plan.id}) + analytic_account_2 = self.env['account.analytic.account'].create({'name': 'Account 1', 'plan_id': analytic_plan.id}) + distribution_model = self.env['account.analytic.distribution.model'].create({ + 'account_prefix': expense_exchange_account.code, + 'partner_id': self.partner_a.id, + 'analytic_distribution': {analytic_account_1.id: 100}, + }) + + # 1200.0 comp_curr = 3600.0 foreign_curr in 2016 (rate 1:3) + st_line = self._create_st_line( + 1200.0, + date='2016-01-01', + ) + # 1800.0 comp_curr = 3600.0 foreign_curr in 2017 (rate 1:2) + inv_line = self._create_invoice_line( + 'out_invoice', + currency_id=self.other_currency.id, + invoice_date='2017-01-01', + invoice_line_ids=[{'price_unit': 3600.0}], + ) + + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'amount_currency': 1200.0, 'currency_id': self.company_data['currency'].id, 'balance': 1200.0, 'analytic_distribution': False}, + {'flag': 'new_aml', 'amount_currency': -3600.0, 'currency_id': self.other_currency.id, 'balance': -1800.0, 'analytic_distribution': False}, + {'flag': 'exchange_diff', 'amount_currency': 0.0, 'currency_id': self.other_currency.id, 'balance': 600.0, 'analytic_distribution': distribution_model.analytic_distribution}, + ]) + + # Test that the analytic distribution is kept on the creation of the exchange diff move + new_distribution = {**distribution_model.analytic_distribution, str(analytic_account_2.id): 100} + + line = wizard.line_ids.filtered(lambda x: x.flag == 'exchange_diff') + line.analytic_distribution = new_distribution + wizard._action_validate() + + self.assertRecordValues(inv_line.matched_credit_ids.exchange_move_id.line_ids, [ + {'analytic_distribution': False}, + {'analytic_distribution': new_distribution}, + ]) + + def test_access_child_bank_with_user_set_on_child(self): + """ + Demo user with a Child Company as default company/allowed companies + should be able to access the Bank set on this same Child Company + """ + child_company = self.env['res.company'].create({ + 'name': 'Childest Company', + 'parent_id': self.env.company.id, + }) + child_bank_journal = self.env['account.journal'].create({ + 'name': 'Child Bank', + 'type': 'bank', + 'company_id': child_company.id, + }) + self.user.write({ + 'company_ids': [Command.set(child_company.ids)], + 'company_id': child_company.id, + 'groups_id': [ + Command.set(self.env.ref('account.group_account_user').ids), + ] + }) + res = self.env['bank.rec.widget'].with_user(self.user).collect_global_info_data(child_bank_journal.id) + self.assertTrue(res, "Journal should be accessible") + + def test_collect_global_info_data_other_company_bank_journal_with_user_on_main_company(self): + """ The aim of this test is checking that a user who having + access to 2 companies will have values even when he's + calling collect_global_info_data function if + it's current company it's not the one on the journal + but is still available. + To do that, we add 2 companies to the user, and try to + call collect_global_info_data on the journal of the second + company, even if the main company it's the first one. + """ + self.user.write({ + 'company_ids': [Command.set((self.company_data['company'] + self.company_data_2['company']).ids)], + 'company_id': self.company_data['company'].id, + }) + + result = self.env['bank.rec.widget'].with_user(self.user).collect_global_info_data(self.company_data_2['default_journal_bank'].id) + self.assertTrue(result['balance_amount'], "Balance amount shouldn't be False value") + + def test_collect_global_info_data_non_existing_bank_journal(self): + """ The aim of this test is checking that we receive an empty + string when we call collect_global_info_data function + with a non-existing journal. This use case could happen + when we try to open the bank rec widget on a journal that + is not actually existing. As this function is callable by + rpc, this usecase could happen. + """ + result = self.env['bank.rec.widget'].with_user(self.user).collect_global_info_data(99999999) + self.assertEqual(result['balance_amount'], "", "If no value, the function should return an empty string") + + def test_res_partner_bank_find_create_multi_account(self): + """ Make sure that we can save multiple bank accounts for a partner. """ + partner = self.env['res.partner'].create({'name': "Zitycard"}) + + for acc_number in ("123456789", "123456780"): + st_line = self._create_st_line(100.0, account_number=acc_number) + inv_line = self._create_invoice_line( + 'out_invoice', + partner_id=partner.id, + invoice_line_ids=[{'price_unit': 100.0, 'tax_ids': []}], + ) + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_add_new_amls(inv_line) + wizard._action_validate() + + bank_accounts = self.env['res.partner.bank'].sudo().with_context(active_test=False).search([ + ('partner_id', '=', partner.id), + ]) + self.assertEqual(len(bank_accounts), 2, "Second bank account was not registered!") + + #################################################### + # RECO MODEL MOVE CREATION + #################################################### + + def create_test_reco_invoice(self, st_line, reco_model): + """ Helper method to create a move given a statement line and reconciliation model """ + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + wizard._action_select_reconcile_model(reco_model) + move = self.env['account.move'].browse(wizard.return_todo_command['res_id']) + return move + + def assert_reco_invoice_values(self, move, st_line, expected_move_type, expected_amount_total=None): + """ Helper method to assert that values in a move match the information in the given statement line """ + if expected_amount_total is None: + expected_amount_total = abs(st_line.amount) + self.assertRecordValues(move, [{ + 'amount_total': expected_amount_total, + 'move_type': expected_move_type, + 'partner_id': st_line.partner_id.id, + 'invoice_date': st_line.date, + }]) + + def test_invoice_creation_from_reco_model(self): + """ Test the created invoice from a sale/purchase reconciliation model. """ + reco_model_invoice = self.env['account.reconcile.model'].create({ + 'name': "test reconcile create invoice", + 'rule_type': 'writeoff_button', + 'counterpart_type': 'sale', + 'line_ids': [ + Command.create({'amount_string': '50'}), + Command.create({'amount_string': '50'}), + ], + }) + + for st_line_amount, move_type, reco_model in ( + (1000.0, 'out_invoice', reco_model_invoice), + (-1000.0, 'out_refund', reco_model_invoice), + (1000.0, 'in_refund', self.reco_model_bill), + (-1000.0, 'in_invoice', self.reco_model_bill), + ): + st_line = self._create_st_line(st_line_amount, partner_id=self.partner_a.id) + with self.subTest(): + move = self.create_test_reco_invoice(st_line, reco_model) + self.assert_reco_invoice_values(move, st_line, move_type) + + def test_invoice_reco_model_round_odd(self): + """ Test if correct move is created when rounding is needed for multiple reco model lines""" + # Odd amount and a reconciliation model with two lines (50% each line) requires rounding to ensure values match. + st_line = self._create_st_line(amount=-111.11, partner_id=self.partner_a.id) + move = self.create_test_reco_invoice(st_line, self.reco_model_bill) + self.assert_reco_invoice_values(move, st_line, 'in_invoice') + + def test_invoice_reco_model_round_single_line(self): + """ Test if correct move is created with single-line reco model when rounding is needed """ + # A st_line of $150 with 15% tax (default) requires rounding, as without it the invoice would total $149.99 + reco_model_single_line = self.env['account.reconcile.model'].create({ + 'name': "single line reco model", + 'rule_type': 'writeoff_button', + 'counterpart_type': 'purchase', + 'line_ids': [Command.create({})], + }) + st_line = self._create_st_line(amount=150, partner_id=self.partner_a.id) + move = self.create_test_reco_invoice(st_line, reco_model_single_line) + self.assert_reco_invoice_values(move, st_line, 'in_refund') + + def test_invoice_reco_model_round_large_percentage(self): + """ Test if total move amount is correctly rounded when reco model lines go above 100% of st_line amount """ + # Reco model with two 100% st_line amount lines + reco_model_two_lines = self.env['account.reconcile.model'].create({ + 'name': "two lines reco model", + 'rule_type': 'writeoff_button', + 'counterpart_type': 'purchase', + 'line_ids': [Command.create({}), Command.create({})], + }) + # Reco model with one 200% st_line amount line + reco_model_single_line = self.env['account.reconcile.model'].create({ + 'name': "single line reco model", + 'rule_type': 'writeoff_button', + 'counterpart_type': 'purchase', + 'line_ids': [Command.create({'amount_string': '200'})], + }) + st_line = self._create_st_line(-150, partner_id=self.partner_a.id) + for reco_model in (reco_model_two_lines, reco_model_single_line): + with self.subTest(): + move = self.create_test_reco_invoice(st_line, reco_model) + # Total move amount should equal 2 * st_line amount = 300 in both cases + self.assert_reco_invoice_values(move, st_line, 'in_invoice', 300.0) + + def test_invoice_reco_model_round_combined(self): + """ Test if total move amount is correctly rounded when reco model lines are a combination of + percentage_st_line and fixed amount types """ + # Reco model with 100% amount + fixed amount, total move amount should equal st_line amount + fixed amount + reco_model_percentage_fixed = self.env['account.reconcile.model'].create({ + 'name': "combined percentage + fixed reco model", + 'rule_type': 'writeoff_button', + 'counterpart_type': 'sale', + 'line_ids': [ + Command.create({}), + Command.create({ + 'amount_type': 'fixed', + 'amount_string': '50', + }), + ], + }) + st_line = self._create_st_line(amount=100, partner_id=self.partner_a.id) + move = self.create_test_reco_invoice(st_line, reco_model_percentage_fixed) + self.assert_reco_invoice_values(move, st_line, 'out_invoice', 150.0) + + def test_invoice_reco_model_multiple_taxes(self): + """ + Test if correct move is created through reco model writeoff button when the + reconciliation model has more than one tax per line. + Note: this only works if taxes are all price_include or all not price_include + """ + tax_21 = self.env['account.tax'].create({ + 'name': "tax_21", + 'amount': 21, + }) + reco_model_bill_mult_taxes = self.env['account.reconcile.model'].create({ + 'name': "test reconcile bill multiple taxes", + 'rule_type': 'writeoff_button', + 'counterpart_type': 'purchase', + 'line_ids': [ + Command.create({ + 'tax_ids': [Command.set((tax_21 + self.company_data['default_tax_purchase']).ids)], + }), + ], + }) + st_line = self._create_st_line(amount=-100, partner_id=self.partner_a.id) + move = self.create_test_reco_invoice(st_line, reco_model_bill_mult_taxes) + self.assert_reco_invoice_values(move, st_line, 'in_invoice') + + def test_invoice_creation_reco_model_percentage(self): + """ Test if correct move is created when rounding is needed """ + # Odd amount and a reconciliation model with two lines (50% each line) requires rounding to ensure values match. + st_line = self._create_st_line(amount=150, partner_id=self.partner_a.id) + reco_model_bill_balance = self.env['account.reconcile.model'].create({ + 'name': "test balance", + 'rule_type': 'writeoff_button', + 'counterpart_type': 'purchase', + 'line_ids': [ + Command.create({ + 'amount_type': 'percentage', + 'amount_string': '50', + }), + Command.create({ + 'amount_type': 'percentage', + 'amount_string': '50', + }), + ], + }) + move = self.create_test_reco_invoice(st_line, reco_model_bill_balance) + self.assert_reco_invoice_values(move, st_line, 'in_refund', 112.5) + + def test_invoice_creation_reco_model_fixed(self): + """ Test if correct move is created when rounding is needed """ + # Odd amount and a reconciliation model with two lines (50% each line) requires rounding to ensure values match. + st_line = self._create_st_line(amount=-150, partner_id=self.partner_a.id) + reco_model_invoice_fixed = self.env['account.reconcile.model'].create({ + 'name': "test fixed", + 'rule_type': 'writeoff_button', + 'counterpart_type': 'sale', + 'line_ids': [ + Command.create({ + 'amount_type': 'fixed', + 'amount_string': '100', + }), + ], + }) + move = self.create_test_reco_invoice(st_line, reco_model_invoice_fixed) + self.assert_reco_invoice_values(move, st_line, 'out_refund', 100.0) + + def test_invoice_creation_reco_model_regex(self): + """ Test if correct move is created when rounding is needed """ + # Odd amount and a reconciliation model with two lines (50% each line) requires rounding to ensure values match. + st_line = self._create_st_line(amount=150, partner_id=self.partner_a.id, payment_ref="150 invoice", statement_name='150 invoice') + reco_model_invoice_regex = self.env['account.reconcile.model'].create({ + 'name': "test label/regex", + 'rule_type': 'writeoff_button', + 'counterpart_type': 'sale', + 'line_ids': [ + Command.create({ + 'amount_type': 'regex', + 'amount_string': r'([\d,.]+)', + }), + ], + }) + move = self.create_test_reco_invoice(st_line, reco_model_invoice_regex) + self.assert_reco_invoice_values(move, st_line, 'out_invoice', 150.0) + + def test_unreconciled_with_other_lines(self): + """Test that other lines are shown in the widget if they exist.""" + st_line = self._create_st_line( + 1000.0, + date='2017-01-01', + ) + + # Edit the associated move to partially reconcile some of the suspense amount; i.e. we add another line + liquidity_line, suspense_line, other_line = st_line._seek_for_lines() + other_account = st_line.journal_id.company_id.default_cash_difference_income_account_id + self.assertFalse(other_line) + move = st_line.move_id + move.button_draft() + move.write({'line_ids': [ + Command.create({'account_id': other_account.id, 'credit': 100.0}), + Command.update(suspense_line.id, {'credit': 900.0}), + ]}) + move.action_post() + liquidity_line, suspense_line, other_line = st_line._seek_for_lines() + self.assertTrue(other_line) + + # Check that the wizard displays the new line + wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({}) + self.assertRecordValues(wizard.line_ids, [ + # pylint: disable=C0326 + {'flag': 'liquidity', 'account_id': liquidity_line.account_id.id, 'amount_currency': 1000.0}, + {'flag': 'aml', 'account_id': other_line.account_id.id, 'amount_currency': -100.0}, + {'flag': 'auto_balance', 'account_id': suspense_line.account_id.id, 'amount_currency': -900.0}, + ])