[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:
@@ -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
|
||||
|
||||
@@ -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/>
|
||||
|
||||
Reference in New Issue
Block a user