diff options
Diffstat (limited to 'addons/account/tests/test_account_bank_statement.py')
| -rw-r--r-- | addons/account/tests/test_account_bank_statement.py | 1103 |
1 files changed, 1100 insertions, 3 deletions
diff --git a/addons/account/tests/test_account_bank_statement.py b/addons/account/tests/test_account_bank_statement.py index 6e017e3340e..6cca0200e57 100644 --- a/addons/account/tests/test_account_bank_statement.py +++ b/addons/account/tests/test_account_bank_statement.py @@ -1,5 +1,12 @@ +# -*- coding: utf-8 -*- +from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.addons.account.tests.common import AccountTestCommon 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 @tagged('post_install', '-at_install') @@ -29,7 +36,7 @@ class TestBankStatement(AccountTestCommon): 'name': 'BNK' + str(self.number), 'date': date, 'line_ids': [(0, 0, { - 'name': '_', + 'payment_ref': '_', 'amount': line_amount, })], 'journal_id': journal or self.journal.id @@ -58,7 +65,7 @@ class TestBankStatement(AccountTestCommon): self.assertEqual(bnk1.balance_end, 0) # Add a line self.bsl_model.create({ - 'name': '_', + 'payment_ref': '_', 'amount': 10, 'statement_id': bnk1.id }) @@ -76,7 +83,7 @@ class TestBankStatement(AccountTestCommon): self.assertEqual(bnk1.balance_end_real, 100) self.assertEqual(bnk1.balance_end, 100) self.assertEqual(bnk1.previous_statement_id.id, False) - + # Create a new statement after that one bnk2 = self.create_bank_statement('2019-01-10', 50) self.assertEqual(bnk2.balance_start, 100) @@ -242,3 +249,1093 @@ class TestBankStatement(AccountTestCommon): self.assertEqual(bnk5.previous_statement_id.id, False) self.assertEqual(bnk5.balance_start, 0) self.assertEqual(bnk5.balance_end_real, 375) # Ending balance of last statement should not have changed + + +class TestAccountBankStatementCommon(AccountTestInvoicingCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + # 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'] + + 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_debit_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, + } + + 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 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': 0.0, 'currency_id': False}, + ) + + 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': 0.0, 'currency_id': False}, + {'debit': 0.0, 'credit': 80.0, 'amount_currency': 0.0, 'currency_id': False}, + ) + + 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 ==== + + # Amount can't be 0.0 on a statement line. + assertStatementLineConstraint(statement_vals, { + **statement_line_vals, + 'amount': 0.0, + }) + + # 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 amount_currency = 0.0 with a specified foreign currency. + assertStatementLineConstraint(statement_vals, { + **statement_line_vals, + 'foreign_currency_id': self.currency_2.id, + 'amount_currency': 0.0, + }) + + # 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_debit_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.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.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': 0.0, 'currency_id': False}, + ) + + 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': 0.0, 'currency_id': False}, + {'debit': 0.0, 'credit': 80.0, 'amount_currency': 0.0, 'currency_id': False}, + ) + + 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': 0.0, 'currency_id': False}, + ) + + 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': 0.0, 'currency_id': False}, + {'debit': 0.0, 'credit': 80.0, 'amount_currency': 0.0, 'currency_id': False}, + ) + + 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.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.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') + + def _prepare_reconciliation_with_payments(lines_vals_list): + statement_line_wo_patch = self.env['account.bank.statement.line'].browse(self.statement_line.id) + return statement_line_wo_patch._prepare_reconciliation(lines_vals_list, create_payment_for_invoice=True) + + with patch.object( + self.statement_line, + '_prepare_reconciliation', + lambda *args, **kwargs: _prepare_reconciliation_with_payments(*args) + ): + 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, + }, + ]) |
