summaryrefslogtreecommitdiff
path: root/addons/account/tests/test_account_bank_statement.py
diff options
context:
space:
mode:
Diffstat (limited to 'addons/account/tests/test_account_bank_statement.py')
-rw-r--r--addons/account/tests/test_account_bank_statement.py1103
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,
+ },
+ ])