# -*- coding: utf-8 -*- from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tests import tagged from odoo.tests.common import Form from odoo.exceptions import ValidationError, UserError from odoo import fields from unittest.mock import patch class TestAccountBankStatementCommon(AccountTestInvoicingCommon): @classmethod def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) # We need a third currency as you could have a company's currency != journal's currency != cls.currency_data_2 = cls.setup_multi_currency_data(default_values={ 'name': 'Dark Chocolate Coin', 'symbol': '🍫', 'currency_unit_label': 'Dark Choco', 'currency_subunit_label': 'Dark Cacao Powder', }, rate2016=6.0, rate2017=4.0) cls.currency_data_3 = cls.setup_multi_currency_data(default_values={ 'name': 'Black Chocolate Coin', 'symbol': '🍫', 'currency_unit_label': 'Black Choco', 'currency_subunit_label': 'Black Cacao Powder', }, rate2016=12.0, rate2017=8.0) cls.bank_journal_1 = cls.company_data['default_journal_bank'] cls.bank_journal_2 = cls.bank_journal_1.copy() cls.bank_journal_3 = cls.bank_journal_2.copy() cls.currency_1 = cls.company_data['currency'] cls.currency_2 = cls.currency_data['currency'] cls.currency_3 = cls.currency_data_2['currency'] cls.currency_4 = cls.currency_data_3['currency'] def assertBankStatementLine(self, statement_line, expected_statement_line_vals, expected_move_line_vals): self.assertRecordValues(statement_line, [expected_statement_line_vals]) self.assertRecordValues(statement_line.line_ids.sorted('balance'), expected_move_line_vals) @tagged('post_install', '-at_install') class TestAccountBankStatement(TestAccountBankStatementCommon): # ------------------------------------------------------------------------- # TESTS about the statement model. # ------------------------------------------------------------------------- def test_starting_ending_balance_chaining(self): # Create first statement on 2019-01-02. bnk1 = self.env['account.bank.statement'].create({ 'name': 'BNK1', 'date': '2019-01-02', 'journal_id': self.company_data['default_journal_bank'].id, 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 100.0})], }) self.assertRecordValues(bnk1, [{ 'balance_start': 0.0, 'balance_end_real': 100.0, 'balance_end': 100.0, 'previous_statement_id': False, }]) # Create a new statement after that one. bnk2 = self.env['account.bank.statement'].create({ 'name': 'BNK2', 'date': '2019-01-10', 'journal_id': self.company_data['default_journal_bank'].id, 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 50.0})], }) self.assertRecordValues(bnk2, [{ 'balance_start': 100.0, 'balance_end_real': 150.0, 'balance_end': 150.0, 'previous_statement_id': bnk1.id, }]) # Create new statement with given ending balance. bnk3 = self.env['account.bank.statement'].create({ 'name': 'BNK3', 'date': '2019-01-15', 'journal_id': self.company_data['default_journal_bank'].id, 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 25.0})], 'balance_end_real': 200.0, }) self.assertRecordValues(bnk3, [{ 'balance_start': 150.0, 'balance_end_real': 200.0, 'balance_end': 175.0, 'previous_statement_id': bnk2.id, }]) # Create new statement with a date right after BNK1. bnk4 = self.env['account.bank.statement'].create({ 'name': 'BNK4', 'date': '2019-01-03', 'journal_id': self.company_data['default_journal_bank'].id, 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 100.0})], }) self.assertRecordValues(bnk4, [{ 'balance_start': 100.0, 'balance_end_real': 200.0, 'balance_end': 200.0, 'previous_statement_id': bnk1.id, }]) # BNK2/BNK3 should have changed their previous statements. self.assertRecordValues(bnk2, [{ 'balance_start': 200.0, 'balance_end_real': 250.0, 'balance_end': 250.0, 'previous_statement_id': bnk4.id, }]) self.assertRecordValues(bnk3, [{ 'balance_start': 250.0, 'balance_end_real': 200.0, 'balance_end': 275.0, 'previous_statement_id': bnk2.id, }]) # Correct the ending balance of BNK3. bnk3.balance_end_real = 275 # Change date of BNK4 to be the last. bnk4.date = '2019-01-20' self.assertRecordValues(bnk1, [{ 'balance_start': 0.0, 'balance_end_real': 100.0, 'balance_end': 100.0, 'previous_statement_id': False, }]) self.assertRecordValues(bnk2, [{ 'balance_start': 100.0, 'balance_end_real': 150.0, 'balance_end': 150.0, 'previous_statement_id': bnk1.id, }]) self.assertRecordValues(bnk3, [{ 'balance_start': 150.0, 'balance_end_real': 175.0, 'balance_end': 175.0, 'previous_statement_id': bnk2.id, }]) self.assertRecordValues(bnk4, [{ 'balance_start': 175.0, 'balance_end_real': 200.0, 'balance_end': 275.0, 'previous_statement_id': bnk3.id, }]) # Correct the ending balance of BNK4. bnk4.balance_end_real = 275 # Move BNK3 to first position. bnk3.date = '2019-01-01' self.assertRecordValues(bnk3, [{ 'balance_start': 0.0, 'balance_end_real': 25.0, 'balance_end': 25.0, 'previous_statement_id': False, }]) self.assertRecordValues(bnk1, [{ 'balance_start': 25.0, 'balance_end_real': 125.0, 'balance_end': 125.0, 'previous_statement_id': bnk3.id, }]) self.assertRecordValues(bnk2, [{ 'balance_start': 125.0, 'balance_end_real': 175.0, 'balance_end': 175.0, 'previous_statement_id': bnk1.id, }]) self.assertRecordValues(bnk4, [{ 'balance_start': 175.0, 'balance_end_real': 275.0, 'balance_end': 275.0, 'previous_statement_id': bnk2.id, }]) # Move BNK1 to the third position. bnk1.date = '2019-01-11' self.assertRecordValues(bnk3, [{ 'balance_start': 0.0, 'balance_end_real': 25.0, 'balance_end': 25.0, 'previous_statement_id': False, }]) self.assertRecordValues(bnk2, [{ 'balance_start': 25.0, 'balance_end_real': 75.0, 'balance_end': 75.0, 'previous_statement_id': bnk3.id, }]) self.assertRecordValues(bnk1, [{ 'balance_start': 75.0, 'balance_end_real': 175.0, 'balance_end': 175.0, 'previous_statement_id': bnk2.id, }]) self.assertRecordValues(bnk4, [{ 'balance_start': 175.0, 'balance_end_real': 275.0, 'balance_end': 275.0, 'previous_statement_id': bnk1.id, }]) # Delete BNK3 and BNK1. (bnk3 + bnk1).unlink() self.assertRecordValues(bnk2, [{ 'balance_start': 0.0, 'balance_end_real': 50.0, 'balance_end': 50.0, 'previous_statement_id': False, }]) self.assertRecordValues(bnk4, [{ 'balance_start': 50.0, 'balance_end_real': 275.0, 'balance_end': 150.0, 'previous_statement_id': bnk2.id, }]) def test_statements_different_journal(self): # Create statements in bank journal. bnk1_1 = self.env['account.bank.statement'].create({ 'name': 'BNK1_1', 'date': '2019-01-01', 'journal_id': self.company_data['default_journal_bank'].id, 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 100.0})], 'balance_end_real': 100.0, }) bnk1_2 = self.env['account.bank.statement'].create({ 'name': 'BNK1_2', 'date': '2019-01-10', 'journal_id': self.company_data['default_journal_bank'].id, 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 50.0})], }) # Create statements in cash journal. bnk2_1 = self.env['account.bank.statement'].create({ 'name': 'BNK2_1', 'date': '2019-01-02', 'journal_id': self.company_data['default_journal_cash'].id, 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 20.0})], 'balance_end_real': 20.0, }) bnk2_2 = self.env['account.bank.statement'].create({ 'name': 'BNK2_2', 'date': '2019-01-12', 'journal_id': self.company_data['default_journal_cash'].id, 'line_ids': [(0, 0, {'payment_ref': '/', 'amount': 10.0})], }) self.assertRecordValues(bnk1_1, [{ 'balance_start': 0.0, 'balance_end_real': 100.0, 'balance_end': 100.0, 'previous_statement_id': False, }]) self.assertRecordValues(bnk1_2, [{ 'balance_start': 100.0, 'balance_end_real': 150.0, 'balance_end': 150.0, 'previous_statement_id': bnk1_1.id, }]) self.assertRecordValues(bnk2_1, [{ 'balance_start': 0.0, 'balance_end_real': 20.0, 'balance_end': 20.0, 'previous_statement_id': False, }]) self.assertRecordValues(bnk2_2, [{ 'balance_start': 20.0, 'balance_end_real': 0.0, 'balance_end': 30.0, 'previous_statement_id': bnk2_1.id, }]) def test_cash_statement_with_difference(self): ''' A cash statement always creates an additional line to store the cash difference towards the ending balance. ''' statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': self.company_data['default_journal_cash'].id, 'balance_end_real': 100.0, }) statement.button_post() self.assertRecordValues(statement.line_ids, [{ 'amount': 100.0, 'is_reconciled': True, }]) @tagged('post_install', '-at_install') class TestAccountBankStatementLine(TestAccountBankStatementCommon): @classmethod def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) cls.statement = cls.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': cls.bank_journal_1.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': cls.partner_a.id, 'foreign_currency_id': cls.currency_2.id, 'amount': 1250.0, 'amount_currency': 2500.0, }), ], }) cls.statement_line = cls.statement.line_ids cls.expected_st_line = { 'date': fields.Date.from_string('2019-01-01'), 'journal_id': cls.statement.journal_id.id, 'payment_ref': 'line_1', 'partner_id': cls.partner_a.id, 'currency_id': cls.currency_1.id, 'foreign_currency_id': cls.currency_2.id, 'amount': 1250.0, 'amount_currency': 2500.0, 'is_reconciled': False, } cls.expected_bank_line = { 'name': cls.statement_line.payment_ref, 'partner_id': cls.statement_line.partner_id.id, 'currency_id': cls.currency_2.id, 'account_id': cls.statement.journal_id.default_account_id.id, 'debit': 1250.0, 'credit': 0.0, 'amount_currency': 2500.0, } cls.expected_counterpart_line = { 'name': cls.statement_line.payment_ref, 'partner_id': cls.statement_line.partner_id.id, 'currency_id': cls.currency_2.id, 'account_id': cls.statement.journal_id.suspense_account_id.id, 'debit': 0.0, 'credit': 1250.0, 'amount_currency': -2500.0, } # ------------------------------------------------------------------------- # TESTS about the statement line model. # ------------------------------------------------------------------------- def _test_statement_line_edition( self, journal, amount, amount_currency, journal_currency, foreign_currency, expected_liquidity_values, expected_counterpart_values): ''' Test the edition of a statement line from itself or from its linked journal entry. :param journal: The account.journal record that will be set on the statement line. :param amount: The amount in journal's currency. :param amount_currency: The amount in the foreign currency. :param journal_currency: The journal's currency as a res.currency record. :param foreign_currency: The foreign currency as a res.currency record. :param expected_liquidity_values: The expected account.move.line values for the liquidity line. :param expected_counterpart_values: The expected account.move.line values for the counterpart line. ''' if journal_currency: journal.currency_id = journal_currency.id statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': journal.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': self.partner_a.id, 'foreign_currency_id': foreign_currency and foreign_currency.id, 'amount': amount, 'amount_currency': amount_currency, }), ], }) statement_line = statement.line_ids # ==== Test the statement line amounts are correct ==== # If there is a bug in the compute/inverse methods, the amount/amount_currency could be # incorrect directly after the creation of the statement line. self.assertRecordValues(statement_line, [{ 'amount': amount, 'amount_currency': amount_currency, }]) self.assertRecordValues(statement_line.move_id, [{ 'partner_id': self.partner_a.id, 'currency_id': (statement_line.foreign_currency_id or statement_line.currency_id).id, }]) # ==== Test the edition of statement line amounts ==== # The statement line must remain consistent with its account.move. # To test the compute/inverse methods are correctly managing all currency setup, # we check the edition of amounts in both directions statement line <-> journal entry. # Check initial state of the statement line. liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines() self.assertRecordValues(liquidity_lines, [expected_liquidity_values]) self.assertRecordValues(suspense_lines, [expected_counterpart_values]) # Check the account.move is still correct after editing the account.bank.statement.line. statement_line.write({ 'amount': statement_line.amount * 2, 'amount_currency': statement_line.amount_currency * 2, }) self.assertRecordValues(statement_line, [{ 'amount': amount * 2, 'amount_currency': amount_currency * 2, }]) self.assertRecordValues(liquidity_lines, [{ **expected_liquidity_values, 'debit': expected_liquidity_values.get('debit', 0.0) * 2, 'credit': expected_liquidity_values.get('credit', 0.0) * 2, 'amount_currency': expected_liquidity_values.get('amount_currency', 0.0) * 2, }]) self.assertRecordValues(suspense_lines, [{ 'debit': expected_counterpart_values.get('debit', 0.0) * 2, 'credit': expected_counterpart_values.get('credit', 0.0) * 2, 'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) * 2, }]) # Check the account.bank.statement.line is still correct after editing the account.move. statement_line.move_id.write({'line_ids': [ (1, liquidity_lines.id, { 'debit': expected_liquidity_values.get('debit', 0.0), 'credit': expected_liquidity_values.get('credit', 0.0), 'amount_currency': expected_liquidity_values.get('amount_currency', 0.0), }), (1, suspense_lines.id, { 'debit': expected_counterpart_values.get('debit', 0.0), 'credit': expected_counterpart_values.get('credit', 0.0), 'amount_currency': expected_counterpart_values.get('amount_currency', 0.0), }), ]}) self.assertRecordValues(statement_line, [{ 'amount': amount, 'amount_currency': amount_currency, }]) def _test_edition_customer_and_supplier_flows( self, amount, amount_currency, journal_currency, foreign_currency, expected_liquidity_values, expected_counterpart_values): ''' Test '_test_statement_line_edition' using the customer (positive amounts) & the supplier flow (negative amounts). :param amount: The amount in journal's currency. :param amount_currency: The amount in the foreign currency. :param journal_currency: The journal's currency as a res.currency record. :param foreign_currency: The foreign currency as a res.currency record. :param expected_liquidity_values: The expected account.move.line values for the liquidity line. :param expected_counterpart_values: The expected account.move.line values for the counterpart line. ''' # Check the full process with positive amount (customer process). self._test_statement_line_edition( self.bank_journal_2, amount, amount_currency, journal_currency, foreign_currency, expected_liquidity_values, expected_counterpart_values, ) # Check the full process with negative amount (supplier process). self._test_statement_line_edition( self.bank_journal_3, -amount, -amount_currency, journal_currency, foreign_currency, { **expected_liquidity_values, 'debit': expected_liquidity_values.get('credit', 0.0), 'credit': expected_liquidity_values.get('debit', 0.0), 'amount_currency': -expected_liquidity_values.get('amount_currency', 0.0), }, { **expected_counterpart_values, 'debit': expected_counterpart_values.get('credit', 0.0), 'credit': expected_counterpart_values.get('debit', 0.0), 'amount_currency': -expected_counterpart_values.get('amount_currency', 0.0), }, ) def test_edition_journal_curr_2_statement_curr_3(self): self._test_edition_customer_and_supplier_flows( 80.0, 120.0, self.currency_2, self.currency_3, {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id}, ) def test_edition_journal_curr_2_statement_curr_1(self): self._test_edition_customer_and_supplier_flows( 120.0, 80.0, self.currency_2, self.currency_1, {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id}, ) def test_edition_journal_curr_1_statement_curr_2(self): self._test_edition_customer_and_supplier_flows( 80.0, 120.0, self.currency_1, self.currency_2, {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id}, ) def test_edition_journal_curr_2_statement_false(self): self._test_edition_customer_and_supplier_flows( 80.0, 0.0, self.currency_2, False, {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id}, ) def test_edition_journal_curr_1_statement_false(self): self._test_edition_customer_and_supplier_flows( 80.0, 0.0, self.currency_1, False, {'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id}, {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id}, ) def test_zero_amount_journal_curr_1_statement_curr_2(self): self.bank_journal_2.currency_id = self.currency_1 statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': self.bank_journal_2.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': self.partner_a.id, 'foreign_currency_id': self.currency_2.id, 'amount': 0.0, 'amount_currency': 10.0, }), ], }) self.assertRecordValues(statement.line_ids.move_id.line_ids, [ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 10.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 0.0, 'amount_currency': -10.0, 'currency_id': self.currency_2.id}, ]) def test_zero_amount_currency_journal_curr_1_statement_curr_2(self): self.bank_journal_2.currency_id = self.currency_1 statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': self.bank_journal_2.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': self.partner_a.id, 'foreign_currency_id': self.currency_2.id, 'amount': 10.0, 'amount_currency': 0.0, }), ], }) self.assertRecordValues(statement.line_ids.move_id.line_ids, [ {'debit': 10.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 10.0, 'amount_currency': 0.0, 'currency_id': self.currency_2.id}, ]) def test_zero_amount_journal_curr_2_statement_curr_1(self): self.bank_journal_2.currency_id = self.currency_2 statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': self.bank_journal_2.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': self.partner_a.id, 'foreign_currency_id': self.currency_1.id, 'amount': 0.0, 'amount_currency': 10.0, }), ], }) self.assertRecordValues(statement.line_ids.move_id.line_ids, [ {'debit': 10.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 10.0, 'amount_currency': -10.0, 'currency_id': self.currency_1.id}, ]) def test_zero_amount_currency_journal_curr_2_statement_curr_1(self): self.bank_journal_2.currency_id = self.currency_2 statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': self.bank_journal_2.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': self.partner_a.id, 'foreign_currency_id': self.currency_1.id, 'amount': 10.0, 'amount_currency': 0.0, }), ], }) self.assertRecordValues(statement.line_ids.move_id.line_ids, [ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 10.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': self.currency_1.id}, ]) def test_zero_amount_journal_curr_2_statement_curr_3(self): self.bank_journal_2.currency_id = self.currency_2 statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': self.bank_journal_2.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': self.partner_a.id, 'foreign_currency_id': self.currency_3.id, 'amount': 0.0, 'amount_currency': 10.0, }), ], }) self.assertRecordValues(statement.line_ids.move_id.line_ids, [ {'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 0.0, 'amount_currency': -10.0, 'currency_id': self.currency_3.id}, ]) def test_zero_amount_currency_journal_curr_2_statement_curr_3(self): self.bank_journal_2.currency_id = self.currency_2 statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': self.bank_journal_2.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': self.partner_a.id, 'foreign_currency_id': self.currency_3.id, 'amount': 10.0, 'amount_currency': 0.0, }), ], }) self.assertRecordValues(statement.line_ids.move_id.line_ids, [ {'debit': 5.0, 'credit': 0.0, 'amount_currency': 10.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 5.0, 'amount_currency': 0.0, 'currency_id': self.currency_3.id}, ]) def test_constraints(self): def assertStatementLineConstraint(statement_vals, statement_line_vals): with self.assertRaises(Exception), self.cr.savepoint(): self.env['account.bank.statement'].create({ **statement_vals, 'line_ids': [(0, 0, statement_line_vals)], }) statement_vals = { 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': self.bank_journal_2.id, } statement_line_vals = { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': self.partner_a.id, 'foreign_currency_id': False, 'amount': 10.0, 'amount_currency': 0.0, } # ==== Test constraints at creation ==== # Foreign currency must not be the same as the journal one. assertStatementLineConstraint(statement_vals, { **statement_line_vals, 'foreign_currency_id': self.currency_1.id, }) # Can't have a stand alone amount in foreign currency without foreign currency set. assertStatementLineConstraint(statement_vals, { **statement_line_vals, 'amount_currency': 10.0, }) # ==== Test constraints at edition ==== statement = self.env['account.bank.statement'].create({ **statement_vals, 'line_ids': [(0, 0, statement_line_vals)], }) st_line = statement.line_ids # You can't messed up the journal entry by adding another liquidity line. addition_lines_to_create = [ { 'debit': 1.0, 'credit': 0, 'account_id': self.bank_journal_2.default_account_id.id, 'move_id': st_line.move_id.id, }, { 'debit': 0, 'credit': 1.0, 'account_id': self.company_data['default_account_revenue'].id, 'move_id': st_line.move_id.id, }, ] with self.assertRaises(UserError), self.cr.savepoint(): st_line.move_id.write({ 'line_ids': [(0, 0, vals) for vals in addition_lines_to_create] }) with self.assertRaises(UserError), self.cr.savepoint(): st_line.line_ids.create(addition_lines_to_create) # You can't set the journal entry in an unconsistent state. with self.assertRaises(UserError), self.cr.savepoint(): st_line.move_id.action_post() def test_statement_line_move_onchange_1(self): ''' Test the consistency between the account.bank.statement.line and the generated account.move.lines using the form view emulator. ''' # Check the initial state of the statement line. self.assertBankStatementLine(self.statement_line, self.expected_st_line, [self.expected_counterpart_line, self.expected_bank_line]) # Inverse the amount + change them. with Form(self.statement) as statement_form: with statement_form.line_ids.edit(0) as st_line_form: st_line_form.amount = -2000.0 st_line_form.amount_currency = -4000.0 st_line_form.foreign_currency_id = self.currency_3 self.assertBankStatementLine(self.statement_line, { **self.expected_st_line, 'amount': -2000.0, 'amount_currency': -4000.0, 'foreign_currency_id': self.currency_3.id, }, [ { **self.expected_bank_line, 'debit': 0.0, 'credit': 2000.0, 'amount_currency': -4000.0, 'currency_id': self.currency_3.id, }, { **self.expected_counterpart_line, 'debit': 2000.0, 'credit': 0.0, 'amount_currency': 4000.0, 'currency_id': self.currency_3.id, }, ]) # Check changing the label and the partner. with Form(self.statement) as statement_form: with statement_form.line_ids.edit(0) as st_line_form: st_line_form.payment_ref = 'line_1 (bis)' st_line_form.partner_id = self.partner_b self.assertBankStatementLine(self.statement_line, { **self.expected_st_line, 'payment_ref': self.statement_line.payment_ref, 'partner_id': self.statement_line.partner_id.id, 'amount': -2000.0, 'amount_currency': -4000.0, 'foreign_currency_id': self.currency_3.id, }, [ { **self.expected_bank_line, 'name': self.statement_line.payment_ref, 'partner_id': self.statement_line.partner_id.id, 'debit': 0.0, 'credit': 2000.0, 'amount_currency': -4000.0, 'currency_id': self.currency_3.id, }, { **self.expected_counterpart_line, 'name': self.statement_line.payment_ref, 'partner_id': self.statement_line.partner_id.id, 'debit': 2000.0, 'credit': 0.0, 'amount_currency': 4000.0, 'currency_id': self.currency_3.id, }, ]) # ------------------------------------------------------------------------- # TESTS about reconciliation: # - Test '_prepare_counterpart_move_line_vals': one test for each case. # - Test 'reconcile': 3 cases: # - Open-balance in debit. # - Open-balance in credit. # - No open-balance. # - Test 'button_undo_reconciliation'. # ------------------------------------------------------------------------- def _test_statement_line_reconciliation( self, journal, amount, amount_currency, counterpart_amount, journal_currency, foreign_currency, counterpart_currency, expected_liquidity_values, expected_counterpart_values): ''' Test the reconciliation of a statement line. :param journal: The account.journal record that will be set on the statement line. :param amount: The amount in journal's currency. :param amount_currency: The amount in the foreign currency. :param counterpart_amount: The amount of the invoice to reconcile. :param journal_currency: The journal's currency as a res.currency record. :param foreign_currency: The foreign currency as a res.currency record. :param counterpart_currency: The invoice currency as a res.currency record. :param expected_liquidity_values: The expected account.move.line values for the liquidity line. :param expected_counterpart_values: The expected account.move.line values for the counterpart line. ''' if journal_currency: journal.currency_id = journal_currency.id statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2019-01-01', 'journal_id': journal.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': self.partner_a.id, 'foreign_currency_id': foreign_currency and foreign_currency.id, 'amount': amount, 'amount_currency': amount_currency, }), ], }) statement_line = statement.line_ids # - There is 3 flows to check: # * The invoice will fully reconcile the statement line. # * The invoice will partially reconcile the statement line and leads to an open balance in debit. # * The invoice will partially reconcile the statement line and leads to an open balance in credit. # - The dates are different to be sure the reconciliation will preserve the conversion rate bank side. move_type = 'out_invoice' if counterpart_amount < 0.0 else 'in_invoice' test_invoices = self.env['account.move'].create([ { 'move_type': move_type, 'invoice_date': fields.Date.from_string('2016-01-01'), 'date': fields.Date.from_string('2016-01-01'), 'partner_id': self.partner_a.id, 'currency_id': counterpart_currency.id, 'invoice_line_ids': [ (0, None, { 'name': 'counterpart line, same amount', 'account_id': self.company_data['default_account_revenue'].id, 'quantity': 1, 'price_unit': abs(counterpart_amount), }), ], }, { 'move_type': move_type, 'invoice_date': fields.Date.from_string('2016-01-01'), 'date': fields.Date.from_string('2016-01-01'), 'partner_id': self.partner_a.id, 'currency_id': counterpart_currency.id, 'invoice_line_ids': [ (0, None, { 'name': 'counterpart line, lower amount', 'account_id': self.company_data['default_account_revenue'].id, 'quantity': 1, 'price_unit': abs(counterpart_amount / 2), }), ], }, { 'move_type': move_type, 'invoice_date': fields.Date.from_string('2016-01-01'), 'date': fields.Date.from_string('2016-01-01'), 'partner_id': self.partner_a.id, 'currency_id': counterpart_currency.id, 'invoice_line_ids': [ (0, None, { 'name': 'counterpart line, bigger amount', 'account_id': self.company_data['default_account_revenue'].id, 'quantity': 1, 'price_unit': abs(counterpart_amount * 2), }), ], }, ]) test_invoices.action_post() statement.button_post() counterpart_lines = test_invoices.mapped('line_ids').filtered(lambda line: line.account_internal_type in ('receivable', 'payable')) # Check the full reconciliation. statement_line.reconcile([{'id': counterpart_lines[0].id}]) liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines() self.assertRecordValues(liquidity_lines, [expected_liquidity_values]) self.assertRecordValues(other_lines, [expected_counterpart_values]) # Check the reconciliation with partial lower amount. statement_line.button_undo_reconciliation() statement_line.reconcile([{'id': counterpart_lines[1].id}]) liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines() self.assertRecordValues(liquidity_lines, [expected_liquidity_values]) self.assertRecordValues(other_lines.sorted('balance', reverse=amount < 0.0), [ { **expected_counterpart_values, 'debit': expected_counterpart_values.get('debit', 0.0) / 2, 'credit': expected_counterpart_values.get('credit', 0.0) / 2, 'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) / 2, }, { 'debit': expected_counterpart_values.get('debit', 0.0) / 2, 'credit': expected_counterpart_values.get('credit', 0.0) / 2, 'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) / 2, 'currency_id': expected_counterpart_values.get('currency_id'), }, ]) # Check the reconciliation with partial higher amount. statement_line.button_undo_reconciliation() statement_line.reconcile([{'id': counterpart_lines[2].id}]) liquidity_lines, suspense_lines, other_lines = statement_line._seek_for_lines() self.assertRecordValues(liquidity_lines, [expected_liquidity_values]) self.assertRecordValues(other_lines.sorted('balance', reverse=amount < 0.0), [ { **expected_counterpart_values, 'debit': expected_counterpart_values.get('debit', 0.0) * 2, 'credit': expected_counterpart_values.get('credit', 0.0) * 2, 'amount_currency': expected_counterpart_values.get('amount_currency', 0.0) * 2, }, { 'debit': expected_counterpart_values.get('credit', 0.0), 'credit': expected_counterpart_values.get('debit', 0.0), 'amount_currency': -expected_counterpart_values.get('amount_currency', 0.0), 'currency_id': expected_counterpart_values.get('currency_id'), }, ]) # Make sure the statement line is still correct. self.assertRecordValues(statement_line, [{ 'amount': amount, 'amount_currency': amount_currency, }]) def _test_reconciliation_customer_and_supplier_flows( self, amount, amount_currency, counterpart_amount, journal_currency, foreign_currency, counterpart_currency, expected_liquidity_values, expected_counterpart_values): ''' Test '_test_statement_line_reconciliation' using the customer (positive amounts) & the supplier flow (negative amounts). :param amount: The amount in journal's currency. :param amount_currency: The amount in the foreign currency. :param counterpart_amount: The amount of the invoice to reconcile. :param journal_currency: The journal's currency as a res.currency record. :param foreign_currency: The foreign currency as a res.currency record. :param counterpart_currency: The invoice currency as a res.currency record. :param expected_liquidity_values: The expected account.move.line values for the liquidity line. :param expected_counterpart_values: The expected account.move.line values for the counterpart line. ''' # Check the full process with positive amount (customer process). self._test_statement_line_reconciliation( self.bank_journal_2, amount, amount_currency, counterpart_amount, journal_currency, foreign_currency, counterpart_currency, expected_liquidity_values, expected_counterpart_values, ) # Check the full process with negative amount (supplier process). self._test_statement_line_reconciliation( self.bank_journal_3, -amount, -amount_currency, -counterpart_amount, journal_currency, foreign_currency, counterpart_currency, { **expected_liquidity_values, 'debit': expected_liquidity_values.get('credit', 0.0), 'credit': expected_liquidity_values.get('debit', 0.0), 'amount_currency': -expected_liquidity_values.get('amount_currency', 0.0), }, { **expected_counterpart_values, 'debit': expected_counterpart_values.get('credit', 0.0), 'credit': expected_counterpart_values.get('debit', 0.0), 'amount_currency': -expected_counterpart_values.get('amount_currency', 0.0), }, ) def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_3(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 120.0, -120.0, self.currency_2, self.currency_3, self.currency_3, {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id}, ) def test_reconciliation_journal_curr_2_statement_curr_1_counterpart_curr_2(self): self._test_reconciliation_customer_and_supplier_flows( 120.0, 80.0, -120.0, self.currency_2, self.currency_1, self.currency_2, {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id}, ) def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_2(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 120.0, -80.0, self.currency_2, self.currency_3, self.currency_2, {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id}, ) def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_4(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 120.0, -480.0, self.currency_2, self.currency_3, self.currency_4, {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id}, ) def test_reconciliation_journal_curr_1_statement_curr_2_counterpart_curr_2(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 120.0, -120.0, self.currency_1, self.currency_2, self.currency_2, {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id}, ) def test_reconciliation_journal_curr_1_statement_curr_2_counterpart_curr_3(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 120.0, -480.0, self.currency_1, self.currency_2, self.currency_3, {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id}, ) def test_reconciliation_journal_curr_2_statement_false_counterpart_curr_2(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 0.0, -80.0, self.currency_2, False, self.currency_2, {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id}, ) def test_reconciliation_journal_curr_2_statement_false_counterpart_curr_3(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 0.0, -240.0, self.currency_2, False, self.currency_3, {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id}, ) def test_reconciliation_journal_curr_1_statement_false_counterpart_curr_3(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 0.0, -480.0, self.currency_1, False, self.currency_3, {'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id}, {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id}, ) def test_reconciliation_journal_curr_2_statement_curr_1_counterpart_curr_1(self): self._test_reconciliation_customer_and_supplier_flows( 120.0, 80.0, -80.0, self.currency_2, self.currency_1, self.currency_1, {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id}, ) def test_reconciliation_journal_curr_2_statement_curr_3_counterpart_curr_1(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 120.0, -40.0, self.currency_2, self.currency_3, self.currency_1, {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 40.0, 'amount_currency': -120.0, 'currency_id': self.currency_3.id}, ) def test_reconciliation_journal_curr_1_statement_curr_2_counterpart_curr_1(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 120.0, -80.0, self.currency_1, self.currency_2, self.currency_1, {'debit': 80.0, 'credit': 0.0, 'amount_currency': 120.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 80.0, 'amount_currency': -120.0, 'currency_id': self.currency_2.id}, ) def test_reconciliation_journal_curr_2_statement_false_counterpart_curr_1(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 0.0, -40.0, self.currency_2, False, self.currency_1, {'debit': 40.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_2.id}, {'debit': 0.0, 'credit': 40.0, 'amount_currency': -80.0, 'currency_id': self.currency_2.id}, ) def test_reconciliation_journal_curr_1_statement_false_counterpart_curr_1(self): self._test_reconciliation_customer_and_supplier_flows( 80.0, 0.0, -80.0, self.currency_1, False, self.currency_1, {'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.id}, {'debit': 0.0, 'credit': 80.0, 'amount_currency': -80.0, 'currency_id': self.currency_1.id}, ) def test_reconciliation_statement_line_state(self): ''' Test the reconciliation on the bank statement line with a foreign currency on the journal: - Ensure the statement line is_reconciled field is well computed. - Ensure the reconciliation is working well when dealing with a foreign currency at different dates. - Ensure the reconciliation can be undo. - Ensure the reconciliation is still possible with to_check. ''' self.statement.button_post() receivable_acc_1 = self.company_data['default_account_receivable'] receivable_acc_2 = self.copy_account(self.company_data['default_account_receivable']) payment_account = self.bank_journal_1.payment_debit_account_id random_acc_1 = self.company_data['default_account_revenue'] random_acc_2 = self.copy_account(self.company_data['default_account_revenue']) test_move = self.env['account.move'].create({ 'move_type': 'entry', 'date': fields.Date.from_string('2016-01-01'), 'line_ids': [ (0, None, { 'name': 'counterpart of the whole move', 'account_id': random_acc_1.id, 'debit': 0.0, 'credit': 1030.0, }), (0, None, { 'name': 'test line 1 - receivable account', 'account_id': receivable_acc_1.id, 'currency_id': self.currency_2.id, 'debit': 500.0, 'credit': 0.0, 'amount_currency': 1500.0, }), (0, None, { 'name': 'test line 2 - another receivable account', 'account_id': receivable_acc_2.id, 'currency_id': self.currency_2.id, 'debit': 500.0, 'credit': 0.0, 'amount_currency': 1500.0, }), (0, None, { 'name': 'test line 3 - payment transfer account', 'account_id': payment_account.id, 'currency_id': self.currency_2.id, 'debit': 30.0, 'credit': 0.0, 'amount_currency': 90.0, }), ] }) test_move.action_post() test_line_1 = test_move.line_ids.filtered(lambda line: line.account_id == receivable_acc_1) test_line_2 = test_move.line_ids.filtered(lambda line: line.account_id == receivable_acc_2) test_line_3 = test_move.line_ids.filtered(lambda line: line.account_id == payment_account) self.statement_line.reconcile([ # test line 1 # Will reconcile 300.0 in balance, 600.0 in amount_currency. {'id': test_line_1.id, 'balance': -600.0}, # test line 2 # Will reconcile 250.0 in balance, 500.0 in amount_currency. {'id': test_line_2.id, 'balance': -500.0}, # test line 3 # Will reconcile 30.0 in balance, 90.0 in amount_currency. {'id': test_line_3.id}, # test line 4 # Will reconcile 50.0 in balance, 100.0 in amount_currency. {'name': 'whatever', 'account_id': random_acc_1.id, 'balance': -100.0}, ]) self.assertBankStatementLine(self.statement_line, { **self.expected_st_line, 'is_reconciled': True, }, [ { 'name': '%s: Open Balance' % self.statement_line.payment_ref, 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': receivable_acc_1.id, # This account is retrieved on the partner. 'debit': 0.0, 'credit': 605.0, 'amount_currency': -1210.0, 'amount_residual': -605.0, 'amount_residual_currency': -1210.0, }, { 'name': test_line_1.name, 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': receivable_acc_1.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': -600.0, 'amount_residual': 0.0, 'amount_residual_currency': 0.0, }, { 'name': test_line_2.name, 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': receivable_acc_2.id, 'debit': 0.0, 'credit': 250.0, 'amount_currency': -500.0, 'amount_residual': 0.0, 'amount_residual_currency': 0.0, }, { 'name': 'whatever', 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': random_acc_1.id, 'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'amount_residual': 0.0, 'amount_residual_currency': 0.0, }, { 'name': test_line_3.name, 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': test_line_3.account_id.id, 'debit': 0.0, 'credit': 45.0, 'amount_currency': -90.0, 'amount_residual': 0.0, 'amount_residual_currency': 0.0, }, { **self.expected_bank_line, 'amount_residual': 1250.0, 'amount_residual_currency': 2500.0, }, ]) # Undo the reconciliation to return to the initial state. self.statement_line.button_undo_reconciliation() self.assertBankStatementLine(self.statement_line, self.expected_st_line, [self.expected_counterpart_line, self.expected_bank_line]) # Modify the counterpart line with to_check enabled. self.statement_line.reconcile([ {'name': 'whatever', 'account_id': random_acc_1.id, 'balance': -100.0}, ], to_check=True) self.assertBankStatementLine(self.statement_line, { **self.expected_st_line, 'is_reconciled': True, }, [ { 'name': '%s: Open Balance' % self.statement_line.payment_ref, 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': receivable_acc_1.id, # This account is retrieved on the partner. 'debit': 0.0, 'credit': 1200.0, 'amount_currency': -2400.0, 'amount_residual': -1200.0, 'amount_residual_currency': -2400.0, }, { 'name': 'whatever', 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': random_acc_1.id, 'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'amount_residual': 0.0, 'amount_residual_currency': 0.0, }, { **self.expected_bank_line, 'amount_residual': 1250.0, 'amount_residual_currency': 2500.0, }, ]) # Modify the counterpart line. Should be allowed by the to_check enabled. self.statement_line.reconcile([ {'name': 'whatever again', 'account_id': random_acc_2.id, 'balance': -500.0}, ]) self.assertBankStatementLine(self.statement_line, { **self.expected_st_line, 'is_reconciled': True, }, [ { 'name': '%s: Open Balance' % self.statement_line.payment_ref, 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': receivable_acc_1.id, # This account is retrieved on the partner. 'debit': 0.0, 'credit': 1000.0, 'amount_currency': -2000.0, 'amount_residual': -1000.0, 'amount_residual_currency': -2000.0, }, { 'name': 'whatever again', 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': random_acc_2.id, 'debit': 0.0, 'credit': 250.0, 'amount_currency': -500.0, 'amount_residual': 0.0, 'amount_residual_currency': 0.0, }, { **self.expected_bank_line, 'amount_residual': 1250.0, 'amount_residual_currency': 2500.0, }, ]) # The statement line is no longer in the 'to_check' mode. # Reconciling again should raise an error. with self.assertRaises(UserError), self.cr.savepoint(): self.statement_line.reconcile([ {'name': 'whatever', 'account_id': random_acc_1.id, 'balance': -100.0}, ]) def test_reconciliation_statement_line_with_generated_payments(self): self.statement.button_post() receivable_account = self.company_data['default_account_receivable'] payment_account = self.bank_journal_1.payment_debit_account_id random_account = self.company_data['default_account_revenue'] test_move = self.env['account.move'].create({ 'move_type': 'entry', 'date': fields.Date.from_string('2016-01-01'), 'line_ids': [ (0, None, { 'name': 'counterpart of the whole move', 'account_id': random_account.id, 'debit': 0.0, 'credit': 1000.0, }), (0, None, { 'name': 'test line 1', 'account_id': receivable_account.id, 'debit': 100.0, 'credit': 0.0, }), (0, None, { 'name': 'test line 2', 'account_id': receivable_account.id, 'currency_id': self.currency_2.id, 'debit': 900.0, 'credit': 0.0, 'amount_currency': 1500.0, }), ] }) test_move.action_post() test_line_1 = test_move.line_ids.filtered(lambda line: line.name == 'test line 1') test_line_2 = test_move.line_ids.filtered(lambda line: line.name == 'test line 2') statement_line = self.statement_line StatementLine_prepare_reconciliation = type(statement_line)._prepare_reconciliation def _prepare_reconciliation(self, lines_vals_list, create_payment_for_invoice=False): if self == statement_line: create_payment_for_invoice = True return StatementLine_prepare_reconciliation(self, lines_vals_list, create_payment_for_invoice) with patch.object(type(statement_line), '_prepare_reconciliation', _prepare_reconciliation): self.statement_line.reconcile([ {'id': test_line_1.id, 'balance': -50.0}, {'id': test_line_2.id}, ]) self.assertBankStatementLine(self.statement_line, { **self.expected_st_line, 'is_reconciled': True, }, [ { 'name': 'test line 2', 'account_id': payment_account.id, 'currency_id': self.currency_2.id, 'debit': 0.0, 'credit': 750.0, 'amount_currency': -1500.0, }, { 'name': 'line_1: Open Balance', 'account_id': receivable_account.id, 'currency_id': self.currency_2.id, 'debit': 0.0, 'credit': 450.0, 'amount_currency': -900.0, }, { 'name': 'test line 1', 'account_id': payment_account.id, 'currency_id': self.currency_2.id, 'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, }, self.expected_bank_line, ]) # Check generated payments. self.assertRecordValues(test_line_1.matched_credit_ids.credit_move_id.payment_id.line_ids.sorted('balance'), [ { 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': test_line_1.account_id.id, 'debit': 0.0, 'credit': 50.0, 'amount_currency': -100.0, 'amount_residual': 0.0, 'amount_residual_currency': 50.0, }, { 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': payment_account.id, 'debit': 50.0, 'credit': 0.0, 'amount_currency': 100.0, 'amount_residual': 0.0, 'amount_residual_currency': 0.0, }, ]) self.assertRecordValues(test_line_2.matched_credit_ids.credit_move_id.payment_id.line_ids.sorted('balance'), [ { 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': test_line_2.account_id.id, 'debit': 0.0, 'credit': 750.0, 'amount_currency': -1500.0, 'amount_residual': 0.0, 'amount_residual_currency': 0.0, }, { 'partner_id': self.statement_line.partner_id.id, 'currency_id': self.currency_2.id, 'account_id': payment_account.id, 'debit': 750.0, 'credit': 0.0, 'amount_currency': 1500.0, 'amount_residual': 0.0, 'amount_residual_currency': 0.0, }, ]) def test_conversion_rate_rounding_issue(self): ''' Ensure the reconciliation is well handling the rounding issue due to multiple currency conversion rates. In this test, the resulting journal entry after reconciliation is: {'amount_currency': 7541.66, 'debit': 6446.97, 'credit': 0.0} {'amount_currency': 226.04, 'debit': 193.22, 'credit': 0.0} {'amount_currency': -7767.70, 'debit': 0.0, 'credit': 6640.19} ... but 226.04 / 1.1698 = 193.23. In this situation, 0.01 has been removed from this write-off line in order to avoid an unecessary open-balance line being an exchange difference issue. ''' self.bank_journal_2.currency_id = self.currency_2 self.currency_data['rates'][-1].rate = 1.1698 statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'date': '2017-01-01', 'journal_id': self.bank_journal_2.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': 'line_1', 'partner_id': self.partner_a.id, 'amount': 7541.66, }), ], }) statement.button_post() statement_line = statement.line_ids payment = self.env['account.payment'].create({ 'amount': 7767.70, 'date': '2019-01-01', 'currency_id': self.currency_2.id, 'payment_type': 'inbound', 'partner_type': 'customer', }) payment.action_post() liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines() self.assertRecordValues(liquidity_lines, [{'amount_currency': 7767.70}]) statement_line.reconcile([ {'id': liquidity_lines.id}, {'balance': 226.04, 'account_id': self.company_data['default_account_revenue'].id, 'name': "write-off"}, ]) self.assertRecordValues(statement_line.line_ids, [ {'amount_currency': 7541.66, 'debit': 6446.97, 'credit': 0.0}, {'amount_currency': 226.04, 'debit': 193.22, 'credit': 0.0}, {'amount_currency': -7767.70, 'debit': 0.0, 'credit': 6640.19}, ]) def test_zero_amount_statement_line(self): ''' Ensure the statement line is directly marked as reconciled when having an amount of zero. ''' self.company_data['company'].account_journal_suspense_account_id.reconcile = False statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({ 'name': 'test_statement', 'date': '2017-01-01', 'journal_id': self.bank_journal_2.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': "Happy new year", 'amount': 0.0, }), ], }) statement_line = statement.line_ids self.assertRecordValues(statement_line, [{'is_reconciled': True, 'amount_residual': 0.0}]) def test_bank_statement_line_analytic(self): ''' Ensure the analytic lines are generated during the reconciliation. ''' analytic_account = self.env['account.analytic.account'].create({'name': 'analytic_account'}) statement = self.env['account.bank.statement'].with_context(skip_check_amounts_currencies=True).create({ 'name': 'test_statement', 'date': '2017-01-01', 'journal_id': self.bank_journal_2.id, 'line_ids': [ (0, 0, { 'date': '2019-01-01', 'payment_ref': "line", 'amount': 100.0, }), ], }) statement_line = statement.line_ids statement_line.reconcile([{ 'balance': -100.0, 'account_id': self.company_data['default_account_revenue'].id, 'name': "write-off", 'analytic_account_id': analytic_account.id, }]) # Check the analytic account is there. self.assertRecordValues(statement_line.line_ids.sorted('balance'), [ {'balance': -100.0, 'analytic_account_id': analytic_account.id}, {'balance': 100.0, 'analytic_account_id': False}, ]) # Check the analytic lines. self.assertRecordValues(statement_line.line_ids.analytic_line_ids, [ {'amount': 100.0, 'account_id': analytic_account.id}, ])