Files
odoo_source/addons/account/tests/test_account_bank_statement.py
T
PoMa 43eae4d032 [FIX] account: bank statement not ignoring canceled and draft lines
Before this commit the canceled lines would not filter out from checks for completeness
 of the statements and also the corrections in split wizard consider canceled lines as
 real values. One of the results of this is when the user import statements with some
 duplicate lines, the duplicates are canceled but the statement stays complete regardless
  of the missing lines.

Also fixes the line date is not auto-filled when creating lines in the list view if they don't have a state
Closes PR #105708

closes odoo/odoo#107894

X-original-commit: 2422b2c619209c1ff4171902dd2f90824194366d
Signed-off-by: Laurent Smet <las@odoo.com>
Signed-off-by: Pouya Malekinejad (poma) <poma@odoo.com>
2022-12-14 11:42:15 +01:00

1173 lines
56 KiB
Python

# -*- 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, Command
@tagged('post_install', '-at_install')
class TestAccountBankStatementLine(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']
cls.statement = cls.env['account.bank.statement'].create({
'name': 'test_statement',
'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,
'journal_id': cls.bank_journal_1.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_1.id,
'account_id': cls.statement.journal_id.default_account_id.id,
'debit': 1250.0,
'credit': 0.0,
'amount_currency': 1250.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)
def create_bank_transaction(self, amount, date, amount_currency=None, currency=None, statement=None,
partner=None, journal=None, sequence=0):
values = {
'payment_ref': str(amount),
'amount': amount,
'date': date,
'partner_id': partner and partner.id,
'sequence': sequence,
}
if amount_currency:
values['amount_currency'] = amount_currency
values['foreign_currency_id'] = currency.id
if statement and journal and statement.journal_id != journal:
raise (ValidationError("The statement and the journal are contradictory"))
if statement:
values['journal_id'] = statement.journal_id.id
values['statement_id'] = statement.id
if journal:
values['journal_id'] = journal.id
if not values.get('journal_id'):
values['journal_id'] = (self.company_data_2['default_journal_bank']
if self.env.company == self.company_data_2['company']
else self.company_data['default_journal_bank']
).id
return self.env['account.bank.statement.line'].create(values)
# -------------------------------------------------------------------------
# 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_line = self.env['account.bank.statement.line'].create({
'date': '2019-01-01',
'journal_id': journal.id,
'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,
})
# ==== 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(
# pylint: disable=bad-whitespace
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(
# pylint: disable=bad-whitespace
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(
# pylint: disable=bad-whitespace
80.0, 120.0,
self.currency_1, self.currency_2,
{'debit': 80.0, 'credit': 0.0, 'amount_currency': 80.0, 'currency_id': self.currency_1.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(
# pylint: disable=bad-whitespace
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(
# pylint: disable=bad-whitespace
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_line = self.env['account.bank.statement.line'].create({
'journal_id': self.bank_journal_2.id,
'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.move_id.line_ids, [
# pylint: disable=bad-whitespace
{'debit': 0.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': self.currency_1.id},
{'debit': 0.0, 'credit': 0.0, 'amount_currency': -10.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_line = self.env['account.bank.statement.line'].create({
'journal_id': self.bank_journal_2.id,
'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.move_id.line_ids, [
# pylint: disable=bad-whitespace
{'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_journal_curr_2_statement_curr_3(self):
self.bank_journal_2.currency_id = self.currency_2
statement_line = self.env['account.bank.statement.line'].create({
'journal_id': self.bank_journal_2.id,
'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.move_id.line_ids, [
# pylint: disable=bad-whitespace
{'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_constraints(self):
def assertStatementLineConstraint(statement_line_vals):
with self.assertRaises(Exception), self.cr.savepoint():
self.env['account.bank.statement.line'].create(statement_line_vals)
statement_line_vals = {
'journal_id': self.bank_journal_2.id,
'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_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_line_vals,
'amount_currency': 10.0,
})
# Can't have a foreign currency set without amount in foreign currency.
assertStatementLineConstraint({
**statement_line_vals,
'foreign_currency_id': self.currency_2.id,
})
# ==== Test constraints at edition ====
st_line = self.env['account.bank.statement.line'].create(statement_line_vals)
# 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)
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.
self.statement_line.write({
'amount': -2000.0,
'amount_currency': -4000.0,
'foreign_currency_id': self.currency_3.id,
})
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': -2000.0,
'currency_id': self.currency_1.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.
self.statement_line.write({
'payment_ref': 'line_1 (bis)',
'partner_id': self.partner_b.id,
})
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': -2000.0,
'currency_id': self.currency_1.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,
},
])
def test_prepare_counterpart_amounts_using_st_line_rate(self):
def assertAppliedRate(
journal_currency, foreign_currency, aml_currency,
amount, amount_currency, aml_amount_currency, aml_balance,
expected_amount_currency, expected_balance,
):
journal = self.bank_journal_1.copy()
journal.currency_id = journal_currency
statement_line = self.env['account.bank.statement.line'].create({
'journal_id': journal.id,
'date': '2019-01-01',
'payment_ref': 'test_prepare_counterpart_amounts_using_st_line_rate',
'foreign_currency_id': foreign_currency.id if foreign_currency != journal_currency else None,
'amount': amount,
'amount_currency': amount_currency if foreign_currency != journal_currency else 0.0,
})
res = statement_line._prepare_counterpart_amounts_using_st_line_rate(aml_currency, -aml_balance,
-aml_amount_currency)
self.assertAlmostEqual(res['amount_currency'], expected_amount_currency)
self.assertAlmostEqual(res['balance'], expected_balance)
for params in (
(self.currency_2, self.currency_3, self.currency_3, 80.0, 120.0, 120.0, 20.0, -120.0, -40.0),
(self.currency_2, self.currency_1, self.currency_2, 120.0, 80.0, 120.0, 40.0, -80.0, -80.0),
(self.currency_2, self.currency_3, self.currency_2, 80.0, 120.0, 80.0, 26.67, -120.0, -40.0),
(self.currency_2, self.currency_3, self.currency_4, 80.0, 120.0, 480.0, 40.0, -120.0, -40.0),
(self.currency_1, self.currency_2, self.currency_2, 80.0, 120.0, 120.0, 40.0, -120.0, -80.0),
(self.currency_1, self.currency_2, self.currency_3, 80.0, 120.0, 480.0, 80.0, -120.0, -80.0),
(self.currency_2, self.currency_2, self.currency_2, 80.0, 80.0, 80.0, 26.67, -80.0, -40.0),
(self.currency_2, self.currency_2, self.currency_3, 80.0, 80.0, 240.0, 40.0, -80.0, -40.0),
(self.currency_1, self.currency_1, self.currency_3, 80.0, 80.0, 480.0, 80.0, -80.0, -80.0),
(self.currency_2, self.currency_1, self.currency_1, 120.0, 80.0, 80.0, 80.0, -80.0, -80.0),
(self.currency_2, self.currency_3, self.currency_1, 80.0, 120.0, 40.0, 40.0, -120.0, -40.0),
(self.currency_1, self.currency_2, self.currency_1, 80.0, 120.0, 80.0, 80.0, -120.0, -80.0),
(self.currency_2, self.currency_2, self.currency_1, 80.0, 80.0, 40.0, 40.0, -80.0, -40.0),
(self.currency_1, self.currency_1, self.currency_1, 80.0, 80.0, 80.0, 80.0, -80.0, -80.0),
):
with self.subTest(params=params):
assertAppliedRate(*params)
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',
'line_ids': [
(0, 0, {
'date': '2019-01-01',
'payment_ref': "Happy new year",
'amount': 0.0,
'journal_id': self.bank_journal_2.id,
}),
],
})
statement_line = statement.line_ids
self.assertRecordValues(statement_line, [{'is_reconciled': True, 'amount_residual': 0.0}])
def test_statement_valid_complete_1(self):
self.env.user.company_id = self.company_data_2['company']
# create a valid and complete statement as the first lines (no statement before)
line1 = self.create_bank_transaction(1, '2020-01-10')
line2 = self.create_bank_transaction(2, '2020-01-11')
statement1 = self.env['account.bank.statement'].create({
'line_ids': [Command.set((line1 + line2).ids)],
'balance_start': 0,
'balance_end_real': 3,
})
self.assertRecordValues(statement1, [{
'is_complete': True,
'is_valid': True,
}])
# remove the first line, so not complete but it is still valid because there is no statement before
line1.statement_id = False
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_complete': False,
'is_valid': True,
}])
# create a new line in the statement to make it complete again. Starting value does not match the last line
# but it is still valid because the previous line has no statement
line3 = self.create_bank_transaction(1, '2020-01-12', statement=statement1)
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_complete': True,
'is_valid': True,
}])
# add a statement to the first line, statement1 is still complete but not valid because balance start
# does not match the previous statement
statement2 = self.env['account.bank.statement'].create({
'line_ids': [Command.set(line1.ids)],
'balance_end_real': 1,
})
(statement1 + statement2).invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1 + statement2, [{
'is_complete': True,
'is_valid': False,
}, {
'is_complete': True,
'is_valid': True, # first statement
}])
# Fix the statement balance start, so the statement is not complete (end - start = 2, vs sum lines = 3)
# but it is matched with the previous statements balance end
statement1.balance_start = 1
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_complete': False,
'is_valid': True,
}])
# change the prev statement so the end balance does not match the start balance of statement 1
statement2.balance_end_real = 10
statement2.flush_recordset(['balance_end_real'])
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_complete': False,
'is_valid': False,
}])
# make the statement valid again
statement1.balance_start = 10
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_complete': False,
'is_valid': True,
}])
# and complete again by adding a new transaction to it
line4 = self.create_bank_transaction(-10, '2020-01-13', statement=statement1)
(statement1 + statement2).invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1 + statement2, [{
'is_complete': True,
'is_valid': True,
'date': fields.Date.from_string('2020-01-13'),
}, {
'is_complete': False,
'is_valid': True,
'date': fields.Date.from_string('2020-01-10'),
}])
# check point
self.assertRecordValues(line1 + line2 + line3 + line4, [
{'date': fields.Date.from_string('2020-01-10'), 'statement_id': statement2.id},
{'date': fields.Date.from_string('2020-01-11'), 'statement_id': statement1.id},
{'date': fields.Date.from_string('2020-01-12'), 'statement_id': statement1.id},
{'date': fields.Date.from_string('2020-01-13'), 'statement_id': statement1.id},
])
# changing statement 2 balance makes statement 1 valid,
# but making statement 1 the first statement should make it valid again
statement2.balance_end_real = 100
statement2.flush_recordset(['balance_end_real'])
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_valid': False,
}])
line1.statement_id = False
line1.flush_model()
statement2.flush_model()
statement1.invalidate_model(['is_valid'])
self.assertRecordValues(statement1, [{
'is_valid': True,
}])
# having a gap in the statement shouldn't make it invalid
line3.statement_id = False
statement1.flush_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_valid': True,
}])
# Change the statement on one of the lines of statement 1
statement3 = self.env['account.bank.statement'].create({
'line_ids': [Command.set(line4.ids)],
'balance_start': -5,
})
(statement1 + statement3).flush_recordset(['is_valid'])
self.assertRecordValues(statement1 + statement3, [{
'is_valid': True,
}, {
'is_valid': False, # balance does not match with statement1
}])
# changing statement1 end_balance should change the validity of statement3
statement1.balance_end_real = -5
statement1.flush_recordset(['balance_end_real'])
(statement1 + statement3).invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1 + statement3, [{
'is_valid': True,
}, {
'is_valid': True, # balance start matches previous end, despite the gap
}])
# check point
self.assertRecordValues(line1 + line2 + line3 + line4, [
{'date': fields.Date.from_string('2020-01-10'), 'statement_id': False},
{'date': fields.Date.from_string('2020-01-11'), 'statement_id': statement1.id},
{'date': fields.Date.from_string('2020-01-12'), 'statement_id': False},
{'date': fields.Date.from_string('2020-01-13'), 'statement_id': statement3.id},
])
self.assertRecordValues(statement1 + statement2 + statement3, [
{'is_valid': True, 'balance_start': 10, 'balance_end_real': -5,
'date': fields.Date.from_string('2020-01-11')},
{'is_valid': True, 'balance_start': False, 'balance_end_real': 100, 'date': False},
{'is_valid': True, 'balance_start': -5, 'balance_end_real': False,
'date': fields.Date.from_string('2020-01-13')},
])
# adding a statement to the first line should make statement1 invalid
line1.statement_id = statement2
statement2.flush_model()
(statement1 + statement2).invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1 + statement2, [{'is_valid': False}, {'is_valid': True}])
# moving statement2 the line between statement1 and statement3 should make statement1 valid again
# and statement3 invalid
statement2.line_ids = line3
statement2.flush_model()
(statement1 + statement2 + statement3).invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1 + statement2 + statement3, [
{'is_valid': True}, {'is_valid': False}, {'is_valid': False},
])
def test_statement_line_ordering(self):
self.env.user.company_id = self.company_data_2['company']
# the line numbers are chosen based on the order of the lines in the list view
line7 = self.create_bank_transaction(7, '2020-01-10', sequence=1)
line8 = self.create_bank_transaction(8, '2020-01-10', sequence=2)
line2 = self.create_bank_transaction(2, '2020-01-13')
line6 = self.create_bank_transaction(6, '2020-01-11')
line5 = self.create_bank_transaction(5, '2020-01-12', sequence=3)
line4 = self.create_bank_transaction(4, '2020-01-12', sequence=2)
line1 = self.create_bank_transaction(1, '2020-01-13')
line3 = self.create_bank_transaction(3, '2020-01-12', sequence=1)
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
{'amount': 1, 'running_balance': 36, 'statement_id': False},
{'amount': 2, 'running_balance': 35, 'statement_id': False},
{'amount': 3, 'running_balance': 33, 'statement_id': False},
{'amount': 4, 'running_balance': 30, 'statement_id': False},
{'amount': 5, 'running_balance': 26, 'statement_id': False},
{'amount': 6, 'running_balance': 21, 'statement_id': False},
{'amount': 7, 'running_balance': 15, 'statement_id': False},
{'amount': 8, 'running_balance': 8, 'statement_id': False},
],
)
# Same but with a subset of lines to ensure the balance is not only computed based on selected records.
self.env['account.bank.statement.line'].invalidate_model(fnames=['running_balance'])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([
('company_id', '=', self.env.company.id),
('amount', '>=', 3),
('amount', '<=', 6),
]),
[
{'amount': 3, 'running_balance': 33},
{'amount': 4, 'running_balance': 30},
{'amount': 5, 'running_balance': 26},
{'amount': 6, 'running_balance': 21},
],
)
# Put line2 -> line4 inside a statement with a wrong balance_end_real.
(line2 + line3 + line4).statement_id = statement1 = \
self.env['account.bank.statement'].create({'balance_end_real': 9})
self.assertRecordValues(statement1, [{
'is_complete': True,
}])
statement1.invalidate_recordset(['is_valid'])
statement1.balance_start = 26
self.assertRecordValues(statement1, [{
'is_complete': False,
}])
statement1.invalidate_recordset(['is_valid'])
statement1.balance_end_real = 35
self.assertRecordValues(statement1, [{
'is_complete': True,
}])
# line3, line4 and line5 have the same date. Move line5 at the first place using the sequence.
line5.sequence = -1
statement1.invalidate_recordset(['is_valid'])
self.env['account.bank.statement.line'].invalidate_model(fnames=['running_balance'])
self.assertRecordValues(statement1, [{
'is_complete': True,
}])
statement1.balance_start = 21
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_complete': False,
}])
self.env['account.bank.statement.line'].invalidate_model(fnames=['running_balance'])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 36, 'statement_id': False},
{'amount': 2, 'running_balance': 35, 'statement_id': statement1.id},
{'amount': 5, 'running_balance': 33, 'statement_id': False},
{'amount': 3, 'running_balance': 28, 'statement_id': statement1.id},
{'amount': 4, 'running_balance': 25, 'statement_id': statement1.id},
{'amount': 6, 'running_balance': 21, 'statement_id': False},
{'amount': 7, 'running_balance': 15, 'statement_id': False},
{'amount': 8, 'running_balance': 8, 'statement_id': False},
],
)
line8.amount = 18
self.env['account.bank.statement.line'].invalidate_model(fnames=['running_balance'])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 36, 'statement_id': False},
{'amount': 2, 'running_balance': 35, 'statement_id': statement1.id},
{'amount': 5, 'running_balance': 33, 'statement_id': False},
{'amount': 3, 'running_balance': 28, 'statement_id': statement1.id},
{'amount': 4, 'running_balance': 25, 'statement_id': statement1.id},
{'amount': 6, 'running_balance': 31, 'statement_id': False},
{'amount': 7, 'running_balance': 25, 'statement_id': False},
{'amount': 18, 'running_balance': 18, 'statement_id': False},
],
)
line5.amount = 15
self.env['account.bank.statement.line'].invalidate_model(fnames=['running_balance'])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 46, 'statement_id': False},
{'amount': 2, 'running_balance': 45, 'statement_id': statement1.id},
{'amount': 15, 'running_balance': 43, 'statement_id': False},
{'amount': 3, 'running_balance': 28, 'statement_id': statement1.id},
{'amount': 4, 'running_balance': 25, 'statement_id': statement1.id},
{'amount': 6, 'running_balance': 31, 'statement_id': False},
{'amount': 7, 'running_balance': 25, 'statement_id': False},
{'amount': 18, 'running_balance': 18, 'statement_id': False},
],
)
line7.unlink()
self.env['account.bank.statement.line'].invalidate_model(fnames=['running_balance'])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 46, 'statement_id': False},
{'amount': 2, 'running_balance': 45, 'statement_id': statement1.id},
{'amount': 15, 'running_balance': 43, 'statement_id': False},
{'amount': 3, 'running_balance': 28, 'statement_id': statement1.id},
{'amount': 4, 'running_balance': 25, 'statement_id': statement1.id},
{'amount': 6, 'running_balance': 24, 'statement_id': False},
{'amount': 18, 'running_balance': 18, 'statement_id': False},
],
)
line1.move_id.button_cancel()
line6.move_id.button_cancel()
line6.move_id.button_draft()
self.env['account.bank.statement.line'].invalidate_model(fnames=['running_balance'])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 45, 'statement_id': False},
{'amount': 2, 'running_balance': 45, 'statement_id': statement1.id},
{'amount': 15, 'running_balance': 43, 'statement_id': False},
{'amount': 3, 'running_balance': 28, 'statement_id': statement1.id},
{'amount': 4, 'running_balance': 25, 'statement_id': statement1.id},
{'amount': 6, 'running_balance': 18, 'statement_id': False},
{'amount': 18, 'running_balance': 18, 'statement_id': False},
],
)
# remove the anchor point
statement1.line_ids = False
self.env['account.bank.statement.line'].invalidate_model(fnames=['running_balance'])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 42, 'statement_id': False},
{'amount': 2, 'running_balance': 42, 'statement_id': False},
{'amount': 15, 'running_balance': 40, 'statement_id': False},
{'amount': 3, 'running_balance': 25, 'statement_id': False},
{'amount': 4, 'running_balance': 22, 'statement_id': False},
{'amount': 6, 'running_balance': 18, 'statement_id': False},
{'amount': 18, 'running_balance': 18, 'statement_id': False},
],
)
def test_statement_split(self):
self.env.user.company_id = self.company_data_2['company']
# the line numbers are chosen based on the order of the lines in the list view
line7 = self.create_bank_transaction(7, '2020-01-10', sequence=1)
line8 = self.create_bank_transaction(8, '2020-01-10', sequence=2)
line2 = self.create_bank_transaction(2, '2020-01-13')
line6 = self.create_bank_transaction(6, '2020-01-11')
_line5 = self.create_bank_transaction(5, '2020-01-12', sequence=3)
line4 = self.create_bank_transaction(4, '2020-01-12', sequence=2)
line1 = self.create_bank_transaction(1, '2020-01-13')
line3 = self.create_bank_transaction(3, '2020-01-12', sequence=1)
# Split the last 2 lines by splitting on the line before last.
statement1 = self.env['account.bank.statement'].with_context({'split_line_id': line7.id}).create({})
self.assertRecordValues(statement1, [{
'is_complete': True,
'balance_end_real': 15,
'balance_end': 15,
'balance_start': 0,
}])
self.assertRecordValues(line7 + line8, [
{'amount': 7, 'statement_id': statement1.id},
{'amount': 8, 'statement_id': statement1.id},
])
# Split on a line adjutant to another statement
self.env['account.bank.statement.line'].invalidate_model(['running_balance'])
statement2 = self.env['account.bank.statement'].with_context({'split_line_id': line6.id}).create({})
self.assertRecordValues(statement1, [{
'is_complete': True,
'balance_end_real': 15,
'balance_start': 0,
}])
self.assertRecordValues(statement2, [{
'is_complete': True,
'balance_end_real': 21,
'balance_start': 15,
}])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 36, 'statement_id': False},
{'amount': 2, 'running_balance': 35, 'statement_id': False},
{'amount': 3, 'running_balance': 33, 'statement_id': False},
{'amount': 4, 'running_balance': 30, 'statement_id': False},
{'amount': 5, 'running_balance': 26, 'statement_id': False},
{'amount': 6, 'running_balance': 21, 'statement_id': statement2.id},
{'amount': 7, 'running_balance': 15, 'statement_id': statement1.id},
{'amount': 8, 'running_balance': 8, 'statement_id': statement1.id},
],
)
# Split on a line with a gap to another statement
statement1.unlink()
self.env['account.bank.statement.line'].invalidate_model(['running_balance'])
statement3 = self.env['account.bank.statement'].with_context({'split_line_id': line3.id}).create({})
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 36, 'statement_id': False},
{'amount': 2, 'running_balance': 35, 'statement_id': False},
{'amount': 3, 'running_balance': 33, 'statement_id': statement3.id},
{'amount': 4, 'running_balance': 30, 'statement_id': statement3.id},
{'amount': 5, 'running_balance': 26, 'statement_id': statement3.id},
{'amount': 6, 'running_balance': 21, 'statement_id': statement2.id},
{'amount': 7, 'running_balance': 15, 'statement_id': False},
{'amount': 8, 'running_balance': 8, 'statement_id': False},
],
)
# Split on a line with a single line statement
statement4 = self.env['account.bank.statement'].with_context({'split_line_id': line6.id}).create({})
self.env['account.bank.statement.line'].invalidate_model(['running_balance'])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 36, 'statement_id': False},
{'amount': 2, 'running_balance': 35, 'statement_id': False},
{'amount': 3, 'running_balance': 33, 'statement_id': statement3.id},
{'amount': 4, 'running_balance': 30, 'statement_id': statement3.id},
{'amount': 5, 'running_balance': 26, 'statement_id': statement3.id},
{'amount': 6, 'running_balance': 21, 'statement_id': statement4.id},
{'amount': 7, 'running_balance': 15, 'statement_id': statement4.id},
{'amount': 8, 'running_balance': 8, 'statement_id': statement4.id},
],
)
# check double split on a single line
self.env['account.bank.statement.line'].invalidate_model(['running_balance'])
statement5 = self.env['account.bank.statement'].with_context({'split_line_id': line2.id}).create({})
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 36, 'statement_id': False},
{'amount': 2, 'running_balance': 35, 'statement_id': statement5.id},
{'amount': 3, 'running_balance': 33, 'statement_id': statement3.id},
{'amount': 4, 'running_balance': 30, 'statement_id': statement3.id},
{'amount': 5, 'running_balance': 26, 'statement_id': statement3.id},
{'amount': 6, 'running_balance': 21, 'statement_id': statement4.id},
{'amount': 7, 'running_balance': 15, 'statement_id': statement4.id},
{'amount': 8, 'running_balance': 8, 'statement_id': statement4.id},
],
)
self.env['account.bank.statement.line'].invalidate_model(['running_balance'])
statement6 = self.env['account.bank.statement'].with_context({'split_line_id': line2.id}).create({})
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 36, 'statement_id': False},
{'amount': 2, 'running_balance': 35, 'statement_id': statement6.id},
{'amount': 3, 'running_balance': 33, 'statement_id': statement3.id},
{'amount': 4, 'running_balance': 30, 'statement_id': statement3.id},
{'amount': 5, 'running_balance': 26, 'statement_id': statement3.id},
{'amount': 6, 'running_balance': 21, 'statement_id': statement4.id},
{'amount': 7, 'running_balance': 15, 'statement_id': statement4.id},
{'amount': 8, 'running_balance': 8, 'statement_id': statement4.id},
],
)
# Split in the middle of a statement
self.env['account.bank.statement.line'].invalidate_model(['running_balance'])
statement7 = self.env['account.bank.statement'].with_context({'split_line_id': line4.id}).create({})
self.env['account.bank.statement.line'].invalidate_model(['running_balance'])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'running_balance': 36, 'statement_id': False},
{'amount': 2, 'running_balance': 35, 'statement_id': statement6.id},
{'amount': 3, 'running_balance': 33, 'statement_id': statement3.id},
{'amount': 4, 'running_balance': 30, 'statement_id': statement7.id},
{'amount': 5, 'running_balance': 26, 'statement_id': statement7.id},
{'amount': 6, 'running_balance': 21, 'statement_id': statement4.id},
{'amount': 7, 'running_balance': 15, 'statement_id': statement4.id},
{'amount': 8, 'running_balance': 8, 'statement_id': statement4.id},
],
)
self.assertRecordValues(statement3 + statement7, [
{'balance_end_real': 33, 'balance_start': 30, 'is_complete': True, 'is_valid': True, },
{'balance_end_real': 30, 'balance_start': 21, 'is_complete': True, 'is_valid': True, },
])
# split at start of another statement
self.env['account.bank.statement.line'].invalidate_model(['running_balance'])
statement8 = self.env['account.bank.statement'].with_context({'split_line_id': line6.id}).create({})
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'statement_id': False},
{'amount': 2, 'statement_id': statement6.id},
{'amount': 3, 'statement_id': statement3.id},
{'amount': 4, 'statement_id': statement7.id},
{'amount': 5, 'statement_id': statement7.id},
{'amount': 6, 'statement_id': statement8.id},
{'amount': 7, 'statement_id': statement8.id},
{'amount': 8, 'statement_id': statement8.id},
],
)
# split at end of another statement
self.env['account.bank.statement.line'].invalidate_model(['running_balance'])
statement9 = self.env['account.bank.statement'].with_context({'split_line_id': line8.id}).create({})
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'statement_id': False},
{'amount': 2, 'statement_id': statement6.id},
{'amount': 3, 'statement_id': statement3.id},
{'amount': 4, 'statement_id': statement7.id},
{'amount': 5, 'statement_id': statement7.id},
{'amount': 6, 'statement_id': statement8.id},
{'amount': 7, 'statement_id': statement8.id},
{'amount': 8, 'statement_id': statement9.id},
],
)
self.assertRecordValues(statement8 + statement9, [
{'balance_end_real': 21, 'balance_start': 8, 'is_complete': True, 'is_valid': True, },
{'balance_end_real': 8, 'balance_start': 0, 'is_complete': True, 'is_valid': True, },
])
# split at most recent line
self.env['account.bank.statement.line'].invalidate_model(['running_balance'])
statement10 = self.env['account.bank.statement'].with_context({'split_line_id': line1.id}).create({})
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
# pylint: disable=C0326
{'amount': 1, 'statement_id': statement10.id},
{'amount': 2, 'statement_id': statement6.id},
{'amount': 3, 'statement_id': statement3.id},
{'amount': 4, 'statement_id': statement7.id},
{'amount': 5, 'statement_id': statement7.id},
{'amount': 6, 'statement_id': statement8.id},
{'amount': 7, 'statement_id': statement8.id},
{'amount': 8, 'statement_id': statement9.id},
],
)
all_statements = self.env['account.bank.statement'].search([
('line_ids', '!=', False),
('company_id', '=', self.env.company.id),
])
all_statements.invalidate_recordset(['is_valid'])
self.assertEqual(len(all_statements), 6)
self.assertEqual(all_statements.mapped('is_valid'), [True] * len(all_statements))
self.assertEqual(all_statements.mapped('is_complete'), [True] * len(all_statements))
def test_statement_with_canceled_lines(self):
line1 = self.create_bank_transaction(1, '2020-01-10')
line2 = self.create_bank_transaction(2, '2020-01-11')
statement1 = self.env['account.bank.statement'].create({
'line_ids': [Command.set((line1 + line2).ids)],
'balance_start': 0,
'balance_end_real': 3,
})
self.assertRecordValues(statement1, [{
'is_complete': True,
'date': fields.Date.from_string(line2.date),
}])
# test canceling a line
line2.move_id.button_cancel()
self.assertRecordValues(statement1, [{
'is_complete': False,
'balance_end': 1,
'date': fields.Date.from_string(line1.date),
}])
# test add a line with same amount as a canceled line makes statement complete again
line3 = self.create_bank_transaction(2, '2020-01-12', statement=statement1)
self.assertRecordValues(statement1, [{
'is_complete': True,
'balance_end': 3,
'date': fields.Date.from_string(line3.date),
}])
# test adding a draft line to a statement, nothing should be changed in statement
line4 = self.create_bank_transaction(4, '2020-01-13')
line4.move_id.button_cancel()
line4.move_id.button_draft()
statement1.line_ids |= line4
self.assertRecordValues(statement1, [{
'is_complete': True,
'balance_end': 3,
'date': fields.Date.from_string(line3.date),
}])
# test split with canceled/draft lines
statement2 = self.env['account.bank.statement'].with_context({'split_line_id': line2.id}).create({})
self.assertRecordValues(statement1 + statement2, [{
'is_complete': True,
'balance_end': 3,
'balance_start': 1,
'line_ids': [line4.id, line3.id],
}, {
'is_complete': True,
'balance_start': 0,
'balance_end': 1,
'line_ids': [line1.id, line2.id],
}])
# test cancel/draft all statement lines
# line 4 is draft, and we cancel line 3 so the statement should be empty
line3.move_id.button_cancel()
self.assertRecordValues(statement1, [{
'is_complete': False,
'balance_start': 1,
'balance_end': 1,
}])
# create a statement line with already canceled lines
statement3 = self.env['account.bank.statement'].create({
'line_ids': [Command.set((line3 + line4).ids)],
})
self.assertRecordValues(statement1 + statement3, [{
'is_complete': False,
'balance_start': 1,
'balance_end': 1,
}, {
'is_complete': False,
'balance_start': 0, # no value is given for balance start and it is not a split
'balance_end': 0,
}])