[FIX] account: bank statement balance default

Using the bank recon widget (enterprise), it is possible to create bank statements from the list view.
The default start and end balances were not computed.
This PR fixes that (computes the balances) and has a few additional improvements:
- A warning is added to the statement form view to indicate the problem with the statement
- Statement validity can be computed before the statement is saved, to guide the user when manually creating statements
- balance_start, balance_end_real are now computed fields
- default_get method is simplified and moved
- the action "Create Statement" is removed in favour of multi-edit
- tests and translation templates

related enterprise PR contains the necessary view changes.

closes odoo/odoo#115174

X-original-commit: dfae9e204195abc1b776b9797043ceb38318de08
Related: odoo/enterprise#38131
Signed-off-by: Laurent Smet <las@odoo.com>
Signed-off-by: Ayob Habib (ayh) <ayh@odoo.com>
Signed-off-by: Christophe Simonis <chs@odoo.com>
This commit is contained in:
Habib (ayh)
2023-03-14 18:09:04 +01:00
parent 36c85200e9
commit eb33fdf9d8
5 changed files with 453 additions and 180 deletions
+32
View File
@@ -1104,6 +1104,13 @@ msgid ""
"A second payment will be created automatically in the destination journal."
msgstr ""
#. module: account
#. odoo-python
#: code:addons/account/models/account_bank_statement.py:0
#, python-format
msgid "A statement should only contain lines from the same journal."
msgstr ""
#. module: account
#: model:ir.model.constraint,message:account.constraint_account_fiscal_position_tax_tax_src_dest_uniq
msgid "A tax fiscal position could be defined only one time on same taxes."
@@ -13726,6 +13733,22 @@ msgid ""
"but you selected some of both types."
msgstr ""
#. module: account
#. odoo-python
#: code:addons/account/models/account_bank_statement.py:0
#, python-format
msgid "The running balance (%s) doesn't match the specified ending balance."
msgstr ""
#. module: account
#. odoo-python
#: code:addons/account/models/account_bank_statement.py:0
#, python-format
msgid ""
"The starting balance doesn't match the ending balance of the previous "
"statement, or an earlier statement is missing."
msgstr ""
#. module: account
#: model:ir.model.fields,help:account.field_account_move_line__statement_line_id
msgid "The statement line that created this entry"
@@ -14466,6 +14489,15 @@ msgstr ""
msgid "UP"
msgstr ""
#. module: account
#. odoo-python
#: code:addons/account/models/account_bank_statement.py:0
#, python-format
msgid ""
"Unable to create a statement due to missing transactions. You may want to "
"reorder the transactions before proceeding."
msgstr ""
#. module: account
#. odoo-python
#: code:addons/account/models/account_journal.py:0
+138 -92
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError
from odoo.tools.misc import formatLang
class AccountBankStatement(models.Model):
_name = "account.bank.statement"
@@ -9,65 +9,6 @@ class AccountBankStatement(models.Model):
_order = "first_line_index desc"
_check_company_auto = True
@api.model
def default_get(self, fields_list):
# EXTENDS base
defaults = super().default_get(fields_list)
# create statement on a saved statement line in the tree view
if self._context.get('st_line_id'):
st_line = self.env['account.bank.statement.line'].browse(self._context['st_line_id'])
defaults['balance_start'] = st_line.running_balance - (st_line.amount if st_line.state == 'posted' else 0)
return defaults
# create statement from a new line in the tree view, not stored in the db yet
if self._context.get('st_line_date'):
defaults['balance_start'] = self.env['account.bank.statement.line'].search(
domain=[
('date', '<=', self._context['st_line_date']),
('journal_id', '=', self._context.get('st_line_journal_id')),
],
order='internal_index desc',
limit=1
).running_balance
return defaults
lines = None
# creating statements with split button
if self._context.get('split_line_id'):
current_st_line = self.env['account.bank.statement.line'].browse(self.env.context.get('split_line_id'))
line_before = self.env['account.bank.statement.line'].search(
domain=[
('internal_index', '<', current_st_line.internal_index),
('journal_id', '=', current_st_line.journal_id.id),
('statement_id', '!=', current_st_line.statement_id.id),
('statement_id', '!=', False),
],
order='internal_index desc',
limit=1,
)
lines = self.env['account.bank.statement.line'].search(
domain=[
('internal_index', '<=', current_st_line.internal_index),
('internal_index', '>', line_before.internal_index or ''),
('journal_id', '=', current_st_line.journal_id.id),
],
order='internal_index desc',
)
# if it is called from the action menu, we can have both start and end balances and we filter out
# completed statements, because it is probably due to a mistake from the user
elif self._context.get('active_model') == 'account.bank.statement.line' and self._context.get('active_ids'):
lines = self.env['account.bank.statement.line'].browse(self._context.get('active_ids')) \
.filtered(lambda line: not line.statement_complete) \
.sorted()
if not lines:
raise UserError(_('One or more selected lines already belong to a complete statement.'))
if lines:
defaults['line_ids'] = [Command.set(lines.ids)]
defaults['balance_start'] = lines[-1:].running_balance - (
lines[-1:].amount if lines[-1:].state == 'posted' else 0
)
defaults['balance_end_real'] = lines[:1].running_balance
return defaults
name = fields.Char(
string='Reference',
copy=False,
@@ -94,7 +35,7 @@ class AccountBankStatement(models.Model):
balance_start = fields.Monetary(
string='Starting Balance',
default=0.0,
compute='_compute_balance_start', store=True, readonly=False,
)
# Balance end is calculated based on the statement line amounts and real starting balance.
@@ -105,7 +46,7 @@ class AccountBankStatement(models.Model):
balance_end_real = fields.Monetary(
string='Ending Balance',
default=0.0,
compute='_compute_balance_end_real', store=True, readonly=False,
)
company_id = fields.Many2one(
@@ -131,9 +72,8 @@ class AccountBankStatement(models.Model):
required=True,
)
# A statement assumed to be complete when the sum of encoded lines is equal to the difference between initial and
# ending balances. In other words, a statement is complete when there are enough lines to fill the value between
# initial and final balances. When the user reaches this point, the statement is not autofilled on the new lines.
# A statement assumed to be complete when the sum of encoded lines is equal to the difference between start and
# end balances.
is_complete = fields.Boolean(
compute='_compute_is_complete', store=True,
)
@@ -149,6 +89,10 @@ class AccountBankStatement(models.Model):
search='_search_is_valid',
)
problem_description = fields.Text(
compute='_compute_problem_description',
)
attachment_ids = fields.Many2many(
comodel_name='ir.attachment',
string="Attachments",
@@ -164,12 +108,45 @@ class AccountBankStatement(models.Model):
stmt.first_line_index = sorted_lines[:1].internal_index
stmt.date = sorted_lines.filtered(lambda l: l.state == 'posted')[-1:].date
@api.depends('create_date')
def _compute_balance_start(self):
for stmt in self.sorted(lambda x: x.first_line_index or '0'):
journal_id = stmt.journal_id.id or stmt.line_ids.journal_id.id
previous_line_with_statement = self.env['account.bank.statement.line'].search([
('internal_index', '<', stmt.first_line_index),
('journal_id', '=', journal_id),
('state', '=', 'posted'),
('statement_id', '!=', False),
], limit=1)
balance_start = previous_line_with_statement.statement_id.balance_end_real
lines_in_between_domain = [
('internal_index', '<', stmt.first_line_index),
('journal_id', '=', journal_id),
('state', '=', 'posted'),
]
if previous_line_with_statement:
lines_in_between_domain.append(('internal_index', '>', previous_line_with_statement.internal_index))
# remove lines from previous statement (when multi-editing a line already in another statement)
previous_st_lines = previous_line_with_statement.statement_id.line_ids
lines_in_common = previous_st_lines.filtered(lambda l: l.id in stmt.line_ids._origin.ids)
balance_start -= sum(lines_in_common.mapped('amount'))
lines_in_between = self.env['account.bank.statement.line'].search(lines_in_between_domain)
balance_start += sum(lines_in_between.mapped('amount'))
stmt.balance_start = balance_start
@api.depends('balance_start', 'line_ids.amount', 'line_ids.state')
def _compute_balance_end(self):
for statement in self:
statement.balance_end = statement.balance_start + sum(
statement.line_ids.filtered(lambda l: l.state == 'posted').mapped('amount')
)
for stmt in self:
lines = stmt.line_ids.filtered(lambda x: x.state == 'posted')
stmt.balance_end = stmt.balance_start + sum(lines.mapped('amount'))
@api.depends('balance_start')
def _compute_balance_end_real(self):
for stmt in self:
stmt.balance_end_real = stmt.balance_end
@api.depends('journal_id')
def _compute_currency_id(self):
@@ -181,19 +158,34 @@ class AccountBankStatement(models.Model):
for statement in self:
statement.journal_id = statement.line_ids.journal_id
@api.depends('balance_end_real', 'balance_end')
@api.depends('balance_end', 'balance_end_real', 'line_ids.amount', 'line_ids.state')
def _compute_is_complete(self):
for stmt in self:
stmt.is_complete = stmt.line_ids.filtered(lambda l: l.state == 'posted') and stmt.currency_id.compare_amounts(
stmt.balance_end, stmt.balance_end_real) == 0
@api.depends('balance_end', 'balance_end_real')
def _compute_is_valid(self):
# we extract the invalid statements, the statements with no lines and the first statement are not in the query
# because they don't have a previous statement, so they are excluded from the join, and we consider them valid.
# if we have extracted the valid ones, we would have to mark above-mentioned statements valid manually
invalids = self.filtered(lambda s: s.id in self._get_invalid_statement_ids())
invalids.is_valid = False
(self - invalids).is_valid = True
# For new statements, a sql query can't be used
if len(self) == 1:
self.is_valid = self._get_statement_validity()
else:
invalids = self.filtered(lambda s: s.id in self._get_invalid_statement_ids())
invalids.is_valid = False
(self - invalids).is_valid = True
@api.depends('is_valid', 'is_complete')
def _compute_problem_description(self):
for stmt in self:
description = None
if not stmt.is_valid:
description = _("The starting balance doesn't match the ending balance of the previous statement, or an earlier statement is missing.")
elif not stmt.is_complete:
description = _("The running balance (%s) doesn't match the specified ending balance.", formatLang(self.env, stmt.balance_end, currency_obj=stmt.currency_id))
stmt.problem_description = description
def _search_is_valid(self, operator, value):
if operator not in ('=', '!=', '<>'):
@@ -203,28 +195,22 @@ class AccountBankStatement(models.Model):
return [('id', 'in', invalid_ids)]
return [('id', 'not in', invalid_ids)]
# -------------------------------------------------------------------------
# CRUD
# -------------------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
# EXTENDS base
# If we are doing a split, we have to correct the split statement's balance to keep both original and new
# statements valid.
if self._context.get('split_line_id'):
old_statement = self.env['account.bank.statement.line'].browse(self._context.get('split_line_id')).statement_id
old_lines = old_statement.line_ids
statements = super().create(vals_list)
if self._context.get('split_line_id'):
statements.ensure_one()
if old_statement:
net_change = sum((statements.line_ids & old_lines).filtered(lambda l: l.state == 'posted').mapped('amount'))
old_statement.balance_start += net_change
return statements
# -------------------------------------------------------------------------
# BUSINESS METHODS
# -------------------------------------------------------------------------
def _get_statement_validity(self):
""" Compares the balance_start to the previous statements balance_end_real """
self.ensure_one()
previous = self.env['account.bank.statement'].search(
[
('first_line_index', '<', self.first_line_index),
('journal_id', '=', self.journal_id.id),
],
limit=1,
order='first_line_index DESC',
)
return not previous or self.currency_id.compare_amounts(self.balance_start, previous.balance_end_real) == 0
def _get_invalid_statement_ids(self, all_statements=None):
""" Returns the statements that are invalid for _compute and _search methods."""
@@ -252,3 +238,63 @@ class AccountBankStatement(models.Model):
})
res = self.env.cr.fetchall()
return [r[0] for r in res]
# -------------------------------------------------------------------------
# LOW-LEVEL METHODS
# -------------------------------------------------------------------------
@api.model
def default_get(self, fields_list):
# EXTENDS base
defaults = super().default_get(fields_list)
if 'line_ids' not in fields_list:
return defaults
active_ids = self._context.get('active_ids')
context_split_line_id = self._context.get('split_line_id')
context_st_line_id = self._context.get('st_line_id')
lines = None
# creating statements with split button
if context_split_line_id:
current_st_line = self.env['account.bank.statement.line'].browse(context_split_line_id)
line_before = self.env['account.bank.statement.line'].search(
domain=[
('internal_index', '<', current_st_line.internal_index),
('journal_id', '=', current_st_line.journal_id.id),
('statement_id', '!=', current_st_line.statement_id.id),
('statement_id', '!=', False),
],
order='internal_index desc',
limit=1,
)
lines = self.env['account.bank.statement.line'].search(
domain=[
('internal_index', '<=', current_st_line.internal_index),
('internal_index', '>', line_before.internal_index or ''),
('journal_id', '=', current_st_line.journal_id.id),
],
order='internal_index desc',
)
# single line edit
elif context_st_line_id and len(active_ids) <= 1:
lines = self.env['account.bank.statement.line'].browse(context_st_line_id)
# multi edit
elif context_st_line_id and len(active_ids) > 1:
lines = self.env['account.bank.statement.line'].browse(active_ids).sorted()
if len(lines.journal_id) > 1:
raise UserError(_("A statement should only contain lines from the same journal."))
# Check that the selected lines are contiguous
indexes = lines.mapped('internal_index')
count_lines_between = self.env['account.bank.statement.line'].search_count([
('internal_index', '>=', min(indexes)),
('internal_index', '<=', max(indexes)),
('journal_id', '=', lines.journal_id.id),
])
if len(lines) != count_lines_between:
raise UserError(_("Unable to create a statement due to missing transactions. You may want to reorder the transactions before proceeding."))
if lines:
defaults['line_ids'] = [Command.set(lines.ids)]
return defaults
@@ -27,16 +27,15 @@ class AccountBankStatementLine(models.Model):
# to enter the next transaction, they do not have to enter the date and the statement every time until the
# statement is completed. It is only possible if we know the journal that is used, so it can only be done
# in a view in which the journal is already set and so is single journal view.
if'journal_id' in defaults:
if 'journal_id' in defaults and 'date' in fields_list:
last_line = self.search([
('journal_id', '=', defaults.get('journal_id')),
('state', '=', 'posted'),
], limit=1)
statement = last_line.statement_id
if statement and not statement.is_complete:
defaults.setdefault('statement_id', statement.id)
if statement:
defaults.setdefault('date', statement.date)
elif last_line and not statement:
elif last_line:
defaults.setdefault('date', last_line.date)
return defaults
@@ -276,15 +275,6 @@ class AccountBankStatementLine(models.Model):
# The journal entry seems reconciled.
st_line.is_reconciled = True
@api.onchange('journal_id')
def _onchange_journal_id(self):
"""
Reset the statement line when the journal is changed. In some rare cases that journal is not in the context
the journal_id field might be accessible to the user. In this cse we need to reset the statement_id field when
the journal_id is changed.
:return:
"""
self.statement_id = self._get_default_statement(self.journal_id.id, self.date)
# -------------------------------------------------------------------------
# CONSTRAINT METHODS
@@ -548,24 +548,25 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
# 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,
})
statement1 = self.env['account.bank.statement'].with_context(st_line_id=line1.id, active_ids=[line1.id, line2.id]).create({})
self.assertRecordValues(statement1, [{
'is_complete': True,
'is_valid': True,
'balance_start': 0,
'balance_end': 3,
'balance_end_real': 3,
}])
# 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,
'balance_start': 0,
'balance_end': 2,
'balance_end_real': 3,
}])
# 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
# but it is still valid because it is the first statement
line3 = self.create_bank_transaction(1, '2020-01-12', statement=statement1)
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
@@ -586,12 +587,11 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
'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
# Fix the statement balance start, the balance_end_real is computed, making it complete
statement1.balance_start = 1
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_complete': False,
'is_complete': True,
'is_valid': True,
}])
# change the prev statement so the end balance does not match the start balance of statement 1
@@ -599,11 +599,11 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
statement2.flush_recordset(['balance_end_real'])
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_complete': False,
'is_complete': True,
'is_valid': False,
}])
# make the statement valid again
statement1.balance_start = 10
# make the statement valid again, but keep it incomplete
statement1.write({'balance_start': 10, 'balance_end_real': 3})
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_complete': False,
@@ -629,7 +629,7 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
{'date': fields.Date.from_string('2020-01-13'), 'statement_id': statement1.id},
])
# changing statement 2 balance makes statement 1 valid,
# changing statement 2 balance makes statement 1 invalid,
# but making statement 1 the first statement should make it valid again
statement2.balance_end_real = 100
statement2.flush_recordset(['balance_end_real'])
@@ -684,7 +684,7 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
{'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,
{'is_valid': True, 'balance_start': -5, 'balance_end_real': -15,
'date': fields.Date.from_string('2020-01-13')},
])
@@ -748,7 +748,8 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
# 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.env['account.bank.statement'].create({'balance_start': 20, 'balance_end_real': 29})
self.assertRecordValues(statement1, [{
'is_complete': True,
}])
@@ -756,14 +757,10 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
statement1.invalidate_recordset(['is_valid'])
statement1.balance_start = 26
self.assertRecordValues(statement1, [{
'is_complete': False,
'is_complete': True,
'balance_end_real': 35, # autocorrect
}])
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'])
@@ -775,7 +772,8 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
statement1.balance_start = 21
statement1.invalidate_recordset(['is_valid'])
self.assertRecordValues(statement1, [{
'is_complete': False,
'is_complete': True,
'balance_end_real': 30,
}])
self.env['account.bank.statement.line'].invalidate_model(fnames=['running_balance'])
@@ -896,28 +894,26 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
# 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, [{
'balance_start': 0.0,
'balance_end': 15.0,
'balance_end_real': 15.0,
'is_complete': True,
'balance_end_real': 15,
'balance_end': 15,
'balance_start': 0,
'is_valid': True,
}])
self.assertRecordValues(line7 + line8, [
self.assertRecordValues(line7 + line8 + line6, [
{'amount': 7, 'statement_id': statement1.id},
{'amount': 8, 'statement_id': statement1.id},
{'amount': 6, 'statement_id': False},
])
# 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, [{
'balance_start': 15.0,
'balance_end': 21.0,
'balance_end_real': 21.0,
'is_complete': True,
'balance_end_real': 21,
'balance_start': 15,
'is_valid': True,
}])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
@@ -936,8 +932,14 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
# 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(statement3, [{
'balance_start': 21.0,
'balance_end': 33.0,
'balance_end_real': 33.0,
'is_complete': True,
'is_valid': True,
}])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
@@ -954,7 +956,22 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
)
# 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(statement3 + statement4, [
{
'balance_start': 21.0,
'balance_end': 33.0,
'balance_end_real': 33.0,
'is_complete': True,
'is_valid': True,
},
{
'balance_start': 0.0,
'balance_end': 21.0,
'balance_end_real': 21.0,
'is_complete': True,
'is_valid': True,
},
])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
@@ -970,8 +987,14 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
],
)
# 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(statement5, [{
'balance_start': 33.0,
'balance_end': 35.0,
'balance_end_real': 35.0,
'is_complete': True,
'is_valid': True,
}])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
@@ -986,8 +1009,14 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
{'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({})
statement6 = self.env['account.bank.statement'].with_context({'split_line_id': line2.id}).create({'reference': '6'})
self.assertRecordValues(statement6, [{
'balance_start': 33.0,
'balance_end': 35.0,
'balance_end_real': 35.0,
'is_complete': True,
'is_valid': True,
}])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
@@ -1004,9 +1033,34 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
)
# 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(statement3 + statement7, [
{
'balance_start': 21.0,
'balance_end': 24.0,
'balance_end_real': 33.0,
'is_complete': False,
'is_valid': False,
},
{
'balance_start': 21.0,
'balance_end': 30.0,
'balance_end_real': 30.0,
'is_complete': True,
'is_valid': True,
},
])
# Fix statement3
statement3._compute_balance_start()
self.assertRecordValues(statement3, [{
'balance_start': 30.0,
'balance_end': 33.0,
'balance_end_real': 33.0,
'is_complete': True,
'is_valid': True,
}])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
@@ -1021,14 +1075,25 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
{'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(statement7 + statement8, [
{
'balance_end_real': 30.0,
'balance_end': 30.0,
'balance_start': 21.0,
'is_complete': True,
'is_valid': True,
},
{
'balance_end_real': 21.0,
'balance_end': 21.0,
'balance_start': 0.0,
'is_complete': True,
'is_valid': True,
},
])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
@@ -1045,8 +1110,34 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
)
# 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(statement8 + statement9, [
{
'balance_end_real': 21.0,
'balance_end': 13.0,
'balance_start': 0.0,
'is_complete': False,
'is_valid': False,
},
{
'balance_end_real': 8.0,
'balance_end': 8.0,
'balance_start': 0.0,
'is_complete': True,
'is_valid': True,
},
])
# Fix statement8
statement8._compute_balance_start() # TODO: add_to_compute not working, why?
self.assertRecordValues(statement8, [{
'balance_end_real': 21.0,
'balance_end': 21.0,
'balance_start': 8.0,
'is_complete': True,
'is_valid': True,
}])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
@@ -1061,14 +1152,16 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
{'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(statement10, [{
'balance_start': 35.0,
'balance_end': 36.0,
'balance_end_real': 36.0,
'is_complete': True,
'is_valid': True,
}])
self.assertRecordValues(
self.env['account.bank.statement.line'].search([('company_id', '=', self.env.company.id)]),
[
@@ -1089,21 +1182,20 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
('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))
self.assertRecordValues(all_statements, [{'is_valid': True, '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')
line1 = self.create_bank_transaction(1, '2020-01-10', journal=self.bank_journal_2)
line2 = self.create_bank_transaction(2, '2020-01-11', journal=self.bank_journal_2)
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,
'date': fields.Date.from_string(line2.date),
'balance_start': 0,
'balance_end_real': 3,
}])
# test canceling a line
line2.move_id.button_cancel()
@@ -1112,15 +1204,15 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
'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)
# add a line with same amount as the canceled line makes statement1 complete again
line3 = self.create_bank_transaction(2, '2020-01-12', journal=self.bank_journal_2, 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 = self.create_bank_transaction(4, '2020-01-13', journal=self.bank_journal_2)
line4.move_id.button_cancel()
line4.move_id.button_draft()
statement1.line_ids |= line4
@@ -1132,9 +1224,9 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
# 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,
'is_complete': False,
'balance_end': 2,
'balance_start': 0,
'line_ids': [line4.id, line3.id],
}, {
'is_complete': True,
@@ -1148,21 +1240,22 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
line3.move_id.button_cancel()
self.assertRecordValues(statement1, [{
'is_complete': False,
'balance_start': 1,
'balance_end': 1,
'balance_start': 0,
'balance_end': 0,
}])
# 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_start': 0,
'balance_end': 0,
}, {
'is_complete': False, # no posted transactions
'balance_start': 1, # from statement2's balance_end_real
'balance_end': 1, # no posted transactions
}])
def test_create_statement_line_with_inconsistent_currencies(self):
@@ -1181,3 +1274,114 @@ class TestAccountBankStatementLine(AccountTestInvoicingCommon):
'amount': 200.0,
'amount_currency': 0.0,
}])
def test_statement_balance_warnings(self):
''' Ensure that new statements have the correct opening/closing balances or warnings '''
lines = [
self.create_bank_transaction(amount, date, journal=self.bank_journal_2)
for amount, date in [
(10.0, '2019-01-01'),
(15.0, '2019-01-02'),
(20.0, '2019-01-03'),
(30.0, '2019-01-03'),
(40.0, '2019-01-04'),
(50.0, '2019-01-05'),
]
]
# new statement from single line
contexts = [{
'active_ids': [line.id],
'st_line_id': line.id,
} for line in lines[:3]]
self.assertRecordValues(self.env['account.bank.statement'].with_context(contexts[1]).new({}), [{
'balance_start': 10.0,
'balance_end_real': 25.0,
'is_valid': True,
'is_complete': True,
}])
st1 = self.env['account.bank.statement'].with_context(contexts[0]).create({'name': 'Statement 1'})
self.assertRecordValues(st1, [{
'balance_start': 0.0,
'balance_end_real': 10.0,
'is_valid': True,
'is_complete': True,
}])
self.assertEqual(lines[0].statement_id, st1)
self.assertRecordValues(self.env['account.bank.statement'].with_context(contexts[2]).new(), [{
'balance_start': 25.0,
'balance_end_real': 45.0,
'is_valid': False,
'is_complete': True,
}])
self.assertRecordValues(self.env['account.bank.statement'].with_context(contexts[1]).new(), [{
'balance_start': 10.0,
'balance_end_real': 25.0,
'is_valid': True,
'is_complete': True,
}])
# multi line edit, one line with statement
context = {
'active_ids': [line.id for line in lines[:3]],
'st_line_id': lines[2].id,
}
self.assertRecordValues(self.env['account.bank.statement'].with_context(context).new({}), [{
'balance_start': 0.0,
'balance_end_real': 45.0,
'is_valid': True,
'is_complete': True,
'line_ids': [lines[0].id, lines[1].id, lines[2].id],
}])
# multi line edit, skip lines
context = {
'active_ids': [line.id for line in lines[2:4]],
'st_line_id': lines[3].id,
}
self.assertRecordValues(self.env['account.bank.statement'].with_context(context).new({}), [{
'balance_start': 25.0,
'balance_end_real': 75.0,
'is_valid': False,
'is_complete': True,
'line_ids': [lines[2].id, lines[3].id],
}])
# multi line edit
expected_st_vals = [{
'balance_start': 10.0,
'balance_end_real': 75.0,
'is_valid': True,
'is_complete': True,
'line_ids': [lines[1].id, lines[2].id, lines[3].id],
}]
context = {
'active_ids': [line.id for line in lines[1:4]],
'st_line_id': lines[3].id,
}
self.assertRecordValues(self.env['account.bank.statement'].with_context(context).new({}), expected_st_vals)
# split button
self.assertRecordValues(self.env['account.bank.statement'].with_context({'split_line_id': lines[3].id}).new({}),
expected_st_vals)
# raise error if lines skipped during multi-edit
context = {
'active_ids': [lines[1].id, lines[3].id],
'st_line_id': lines[3].id,
}
with self.assertRaises(UserError):
self.env['account.bank.statement'].with_context(context).create({})
# create the second statement using split button
st2 = self.env['account.bank.statement'].with_context({'split_line_id': lines[-1].id}).create({'name': 'Statement 2'})
self.assertRecordValues(st2, [{
'balance_start': 10.0,
'balance_end_real': 165.0,
'is_valid': True,
'is_complete': True,
}])
@@ -7,7 +7,8 @@
<field name="name">account.bank.statement.tree</field>
<field name="model">account.bank.statement</field>
<field name="arch" type="xml">
<tree decoration-danger="not is_complete or not is_valid"
<tree decoration-danger="journal_id and not is_complete or not is_valid"
decoration-muted="not journal_id"
create="false"
string="Statements">
<field name="name"/>
@@ -31,7 +32,7 @@
<search string="Search Bank Statements">
<field name="name" string="Bank Statement"/>
<field name="date"/>
<filter string="Empty" name="empty" domain="[('line_ids','=',[])]"/>
<filter string="Empty" name="empty" domain="[('line_ids','=',False)]"/>
<filter name="invalid" string="Invalid"
domain="['|', ('is_valid', '=', False),('is_complete', '=', False)]"/>
<separator/>