[IMP] account: statement automatically set starting and ending balance

To prevent having holes between statements and for better usability using import and synchronization
we try to automatically set the starting and ending balance based on the previous existing
statement.
To do that, we added a new field previous_statement_id so that whenever we change the ending_balance
of the previous statement, we recompute the starting balance and ending balance of the current statement.

Also creating a new statement in between 2 others statements will automatically set the correct value
to the starting and ending balance of that statement and all the statements afterwards.

Exception: creating a statement by hand won't automatically set the balance_end_real, however if you
create one between 2 statements, the balance_end_real of next statements will be recomputed

Was Task #1880409

closes odoo/odoo#38696

Signed-off-by: Quentin De Paoli (qdp) <qdp@openerp.com>
This commit is contained in:
Cedric Snauwaert
2019-11-04 08:50:50 +00:00
parent badb95fbce
commit 0d6332fdbb
4 changed files with 330 additions and 6 deletions
@@ -111,6 +111,34 @@ class AccountBankStmtCloseCheck(models.TransientModel):
class AccountBankStatement(models.Model):
# Note: the reason why we did 2 separate function with the same dependencies (one for balance_start and one for balance_end_real)
# is because if we create a bank statement with a default value for one of the field but not the other, the compute method
# won't be called and therefore the other field will have a value of 0 and we don't want that.
@api.depends('previous_statement_id', 'previous_statement_id.balance_end_real')
def _compute_starting_balance(self):
for statement in self:
if statement.previous_statement_id.balance_end_real != statement.balance_start:
statement.balance_start = statement.previous_statement_id.balance_end_real
else:
# Need default value
statement.balance_start = statement.balance_start or 0.0
@api.depends('previous_statement_id', 'previous_statement_id.balance_end_real')
def _compute_ending_balance(self):
for statement in self:
# recompute balance_end_real in case we are in a bank journal and if we change the
# balance_end_real of previous statement as we don't want
# holes in case we add a statement in between 2 others statements.
# We only do this for the bank journal as we use the balance_end_real in cash
# journal for verification and creating cash difference entries so we don't want
# to recompute the value in that case
if statement.journal_type == 'bank':
total_entry_encoding = sum([line.amount for line in statement.line_ids])
statement.balance_end_real = statement.previous_statement_id.balance_end_real + total_entry_encoding
else:
# Need default value
statement.balance_end_real = statement.balance_end_real or 0.0
@api.depends('line_ids', 'balance_start', 'line_ids.amount', 'balance_end_real')
def _end_balance(self):
for statement in self:
@@ -168,6 +196,63 @@ class AccountBankStatement(models.Model):
return self._get_opening_balance(journal_id)
return 0
@api.depends('balance_start', 'previous_statement_id')
def _compute_is_valid_balance_start(self):
for bnk in self:
bnk.is_valid_balance_start = bnk.balance_start == bnk.previous_statement_id.balance_end_real
@api.depends('date', 'journal_id')
def _get_previous_statement(self):
# We have to sort self by date otherwise we may encounter some errors
# Let's assume the following case
# Self contains 3 record (1,2,3) with the following order on date (2019-01-13, 2019-01-12, 2019-01-15)
# On first iteration, record 1 will correctly set previous_Statement_id = 2
# On second iteration, before record 2 can set its previous_statement_id, it will look for someone
# pointing to him in order to change that to its self.previous_statement_id (because we could change the date of
# record 2 and move it to the end and we don't want the previously statement that was pointing towards him to
# continue doing so). However in the case of a creating in batch like in our example, this will cause the following
# issue: record 3 that was done just before in the first loop iteration will have its previous_statement_id set
# to False and its starting and ending balance won't be computed correctly.
# The solution to avoid that is to sort self by date in ascending order that way we are sure that no record
# set their values too soon.
for st in sorted(self, key=lambda l: l['date']):
# Search for the previous statement
domain = [('date', '<=', st.date), ('journal_id', '=', st.journal_id.id)]
# The reason why we have to perform this test is because we have two use case here:
# First one is in case we are creating a new record, in that case that new record does
# not have any id yet. However if we are updating an existing record, the domain date <= st.date
# will find the record itself, so we have to add a condition in the search to ignore self.id
if not isinstance(st.id, models.NewId):
domain.append(('id', '!=', st.id))
previous_statement = self.search(domain, limit=1)
# Exist early if no change
if previous_statement == st.previous_statement_id:
st.previous_statement_id = previous_statement.id
continue
# Search for statement pointing to myself to change it's previous_statement_id to my previous_statement_id
# This does not need to be compute when we are creating a record (newId)
if not isinstance(st.id, models.NewId):
point_to_me_statement = self.search([('previous_statement_id', '=', st.id)], limit=1)
if point_to_me_statement:
point_to_me_statement.previous_statement_id = st.previous_statement_id.id
# two cases here:
# 1- If we have a previous statement, it is possible that we are creating a statement between 2 others stmt
# so we have to change the link of the stmt that was already pointing to the previous statement to ourself
# We only do this if the record already exists in db, because we can't
# assign a newId to an existing next_statement
# 2- We have found no more previous statement, however it is possible that we have been moved first of the list
# so we have to search for any other existing statement and link that one to us (this again also make sense
# only if we already have an actual id)
st.previous_statement_id = previous_statement.id
if not isinstance(st.id, models.NewId):
next_statement = self.search([('previous_statement_id', '=', previous_statement.id), ('id', '!=', st.id), ('journal_id', '=', st.journal_id.id)], limit=1)
if next_statement:
next_statement.previous_statement_id = st.id
_name = "account.bank.statement"
_description = "Bank Statement"
_order = "date desc, id desc"
@@ -177,8 +262,8 @@ class AccountBankStatement(models.Model):
reference = fields.Char(string='External Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True, help="Used to hold the reference of the external mean that created this statement (name of imported file, reference of online synchronization...)")
date = fields.Date(required=True, states={'confirm': [('readonly', True)]}, index=True, copy=False, default=fields.Date.context_today)
date_done = fields.Datetime(string="Closed On")
balance_start = fields.Monetary(string='Starting Balance', states={'confirm': [('readonly', True)]}, default=_default_opening_balance)
balance_end_real = fields.Monetary('Ending Balance', states={'confirm': [('readonly', True)]})
balance_start = fields.Monetary(string='Starting Balance', states={'confirm': [('readonly', True)]}, compute='_compute_starting_balance', readonly=False, store=True)
balance_end_real = fields.Monetary('Ending Balance', states={'confirm': [('readonly', True)]}, compute='_compute_ending_balance', readonly=False, store=True)
accounting_date = fields.Date(string="Accounting Date", help="If set, the accounting entries created during the bank statement reconciliation process will be created at this date.\n"
"This is useful if the accounting period in which the entries should normally be booked is already closed.")
state = fields.Selection([('open', 'New'), ('confirm', 'Validated')], string='Status', required=True, readonly=True, copy=False, default='open')
@@ -201,10 +286,8 @@ class AccountBankStatement(models.Model):
cashbox_start_id = fields.Many2one('account.bank.statement.cashbox', string="Starting Cashbox")
cashbox_end_id = fields.Many2one('account.bank.statement.cashbox', string="Ending Cashbox")
is_difference_zero = fields.Boolean(compute='_is_difference_zero', string='Is zero', help="Check if difference is zero.")
@api.onchange('journal_id')
def onchange_journal_id(self):
self._set_opening_balance(self.journal_id.id)
previous_statement_id = fields.Many2one('account.bank.statement', help='technical field to compute starting balance correctly', compute='_get_previous_statement', store=True)
is_valid_balance_start = fields.Boolean(string="Is Valid Balance Start", compute="_compute_is_valid_balance_start", help="technical field to display a warning message in case starting balance is different than previous ending balance")
def _balance_check(self):
for stmt in self:
@@ -240,6 +323,11 @@ class AccountBankStatement(models.Model):
raise UserError(_('In order to delete a bank statement, you must first cancel it to delete related journal items.'))
# Explicitly unlink bank statement lines so it will check that the related journal entries have been deleted first
statement.line_ids.unlink()
# Some other bank statements might be link to this one, so in that case we have to switch the previous_statement_id
# from that statement to the one linked to this statement
next_statement = self.search([('previous_statement_id', '=', statement.id)])
if next_statement:
next_statement.previous_statement_id = statement.previous_statement_id
return super(AccountBankStatement, self).unlink()
def open_cashbox_id(self):
+1
View File
@@ -9,6 +9,7 @@ from . import test_account_move_partner_count
from . import test_account_move_rounding
from . import test_account_invoice_report
from . import test_account_journal_dashboard
from . import test_account_bank_statement
from . import test_bank_statement_reconciliation
from . import test_control_journal_account
from . import test_fiscal_position
@@ -0,0 +1,230 @@
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestBankStatementReconciliation(AccountingTestCase):
def setUp(self):
super(TestBankStatementReconciliation, self).setUp()
self.bs_model = self.env['account.bank.statement']
self.bsl_model = self.env['account.bank.statement.line']
self.partner = self.env['res.partner'].create({'name': 'test'})
self.journal = self.env['account.journal'].create({
'name': 'BnkJournal',
'type': 'bank'
})
self.journal2 = self.env['account.journal'].create({
'name': 'BnkJournal2',
'type': 'bank'
})
self.cashjournal = self.env['account.journal'].create({
'name': 'CashJournal',
'type': 'cash'
})
self.number = 1
def create_bank_statement(self, date, line_amount, balance_end_real=False, journal=False):
vals = {
'name': 'BNK' + str(self.number),
'date': date,
'line_ids': [(0, 0, {
'name': '_',
'amount': line_amount,
})],
'journal_id': journal or self.journal.id
}
if balance_end_real:
vals['balance_end_real'] = balance_end_real
self.number += 1
return self.bs_model.create(vals)
def test_compute_balance_end_real_with_lines(self):
bnk1 = self.create_bank_statement('2019-01-02', 100)
self.assertEqual(bnk1.balance_start, 0)
# Balance is automatically computed when creating statement with the lines
self.assertEqual(bnk1.balance_end_real, 100)
self.assertEqual(bnk1.balance_end, 100)
def test_compute_balance_end_real_without_lines(self):
vals = {
'name': 'BNK' + str(self.number),
'date': '2019-01-01',
'journal_id': self.journal.id
}
bnk1 = self.bs_model.create(vals)
self.assertEqual(bnk1.balance_start, 0)
self.assertEqual(bnk1.balance_end_real, 0)
self.assertEqual(bnk1.balance_end, 0)
# Add a line
self.bsl_model.create({
'name': '_',
'amount': 10,
'statement_id': bnk1.id
})
self.assertEqual(bnk1.balance_start, 0)
# balance_end_real should not have changed
self.assertEqual(bnk1.balance_end_real, 0)
# Compute balance should have been computed
self.assertEqual(bnk1.balance_end, 10)
def test_create_new_statement(self):
# Create first statement on 1/1/2019
bnk1 = self.create_bank_statement('2019-01-02', 100)
# import pdb;pdb.set_trace()
self.assertEqual(bnk1.balance_start, 0)
# Balance is automatically computed when creating statement with the lines
self.assertEqual(bnk1.balance_end_real, 100)
self.assertEqual(bnk1.balance_end, 100)
self.assertEqual(bnk1.previous_statement_id.id, False)
# Create a new statement after that one
bnk2 = self.create_bank_statement('2019-01-10', 50)
self.assertEqual(bnk2.balance_start, 100)
self.assertEqual(bnk2.balance_end_real, 150)
self.assertEqual(bnk2.balance_end, 150)
self.assertEqual(bnk2.previous_statement_id.id, bnk1.id)
# Create new statement with given ending balance
bnk3 = self.create_bank_statement('2019-01-15', 25, 200)
self.assertEqual(bnk3.balance_end_real, 200)
self.assertEqual(bnk3.balance_start, 150)
self.assertEqual(bnk3.balance_end, 175)
self.assertEqual(bnk3.previous_statement_id.id, bnk2.id)
bnk4 = self.create_bank_statement('2019-01-03', 100)
self.assertEqual(bnk4.balance_start, 100)
self.assertEqual(bnk4.balance_end_real, 200)
self.assertEqual(bnk4.balance_end, 200)
self.assertEqual(bnk4.previous_statement_id.id, bnk1.id)
# Bnk2 should have changed its previous statement
self.assertEqual(bnk2.previous_statement_id.id, bnk4.id)
# The starting balance and balance_end_real should have been recomputed
self.assertEqual(bnk2.balance_start, 200)
self.assertEqual(bnk2.balance_end_real, 250)
self.assertEqual(bnk2.balance_end, 250)
# The starting balance and balance_end_real of next entries should also have been recomputed
# and since we are propagating an update, the balance_end_real should have been recomputed to
# the correct value
self.assertEqual(bnk3.balance_start, 250)
self.assertEqual(bnk3.balance_end_real, 275)
self.assertEqual(bnk3.balance_end, 275)
# Change date of bank stmt4 to be the last
bnk4.date = '2019-01-20'
self.assertEqual(bnk1.previous_statement_id.id, False)
self.assertEqual(bnk2.previous_statement_id.id, bnk1.id)
self.assertEqual(bnk3.previous_statement_id.id, bnk2.id)
self.assertEqual(bnk4.previous_statement_id.id, bnk3.id)
self.assertEqual(bnk1.balance_start, 0)
self.assertEqual(bnk2.balance_start, 100)
self.assertEqual(bnk3.balance_start, 150)
self.assertEqual(bnk4.balance_start, 175)
self.assertEqual(bnk1.balance_end_real, 100)
self.assertEqual(bnk2.balance_end_real, 150)
self.assertEqual(bnk3.balance_end_real, 175)
self.assertEqual(bnk4.balance_end_real, 275)
# Move bnk3 to first position
bnk3.date = '2019-01-01'
self.assertEqual(bnk3.previous_statement_id.id, False)
self.assertEqual(bnk1.previous_statement_id.id, bnk3.id)
self.assertEqual(bnk2.previous_statement_id.id, bnk1.id)
self.assertEqual(bnk4.previous_statement_id.id, bnk2.id)
self.assertEqual(bnk3.balance_start, 0)
self.assertEqual(bnk1.balance_start, 25)
self.assertEqual(bnk2.balance_start, 125)
self.assertEqual(bnk4.balance_start, 175)
self.assertEqual(bnk3.balance_end_real, 25)
self.assertEqual(bnk1.balance_end_real, 125)
self.assertEqual(bnk2.balance_end_real, 175)
self.assertEqual(bnk4.balance_end_real, 275)
# Change bnk1 and bnk2
bnk1.date = '2019-01-11'
self.assertEqual(bnk3.previous_statement_id.id, False)
self.assertEqual(bnk2.previous_statement_id.id, bnk3.id)
self.assertEqual(bnk1.previous_statement_id.id, bnk2.id)
self.assertEqual(bnk4.previous_statement_id.id, bnk1.id)
self.assertEqual(bnk3.balance_start, 0)
self.assertEqual(bnk2.balance_start, 25)
self.assertEqual(bnk1.balance_start, 75)
self.assertEqual(bnk4.balance_start, 175)
self.assertEqual(bnk3.balance_end_real, 25)
self.assertEqual(bnk2.balance_end_real, 75)
self.assertEqual(bnk1.balance_end_real, 175)
self.assertEqual(bnk4.balance_end_real, 275)
def test_create_statements_in_different_journal(self):
# Bank statement create in two different journal should not link with each other
bnk1 = self.create_bank_statement('2019-01-01', 100, 100)
bnk2 = self.create_bank_statement('2019-01-10', 50)
bnk1other = self.create_bank_statement('2019-01-02', 20, 20, self.journal2.id)
bnk2other = self.create_bank_statement('2019-01-12', 10, False, self.journal2.id)
self.assertEqual(bnk1.previous_statement_id.id, False)
self.assertEqual(bnk2.previous_statement_id.id, bnk1.id)
self.assertEqual(bnk1.balance_start, 0)
self.assertEqual(bnk2.balance_start, 100)
self.assertEqual(bnk2.balance_end_real, 150)
self.assertEqual(bnk1other.previous_statement_id.id, False)
self.assertEqual(bnk2other.previous_statement_id.id, bnk1other.id)
self.assertEqual(bnk1other.balance_start, 0)
self.assertEqual(bnk2other.balance_start, 20)
self.assertEqual(bnk2other.balance_end_real, 30)
def test_statement_cash_journal(self):
# Entry in cash journal should not recompute the balance_end_real
cash1 = self.create_bank_statement('2019-01-01', 100, 100, self.cashjournal.id)
cash2 = self.create_bank_statement('2019-01-03', 100, False, self.cashjournal.id)
self.assertEqual(cash1.balance_start, 0)
self.assertEqual(cash1.balance_end_real, 100)
self.assertEqual(cash2.balance_start, 100)
self.assertEqual(cash2.balance_end_real, 0)
cash2.balance_end_real = 1000
self.assertEqual(cash2.balance_end_real, 1000)
# add cash entry in between, should recompute starting balance of cash2 entry but not ending balance
cash3 = self.create_bank_statement('2019-01-02', 100, 200, self.cashjournal.id)
self.assertEqual(cash3.balance_start, 100)
self.assertEqual(cash3.balance_end_real, 200)
self.assertEqual(cash2.balance_start, 200)
self.assertEqual(cash2.balance_end_real, 1000)
def test_unlink_bank_statement(self):
bnk1 = self.create_bank_statement('2019-01-02', 100)
bnk2 = self.create_bank_statement('2019-01-10', 50)
bnk3 = self.create_bank_statement('2019-01-15', 25)
bnk4 = self.create_bank_statement('2019-01-21', 100)
bnk5 = self.create_bank_statement('2019-01-22', 100)
self.assertEqual(bnk1.previous_statement_id.id, False)
self.assertEqual(bnk2.previous_statement_id.id, bnk1.id)
self.assertEqual(bnk3.previous_statement_id.id, bnk2.id)
self.assertEqual(bnk4.previous_statement_id.id, bnk3.id)
self.assertEqual(bnk5.previous_statement_id.id, bnk4.id)
self.assertEqual(bnk3.balance_start, 150)
self.assertEqual(bnk3.balance_end_real, 175)
self.assertEqual(bnk4.balance_start, 175)
self.assertEqual(bnk4.balance_end_real, 275)
self.assertEqual(bnk5.balance_start, 275)
self.assertEqual(bnk5.balance_end_real, 375)
# Delete bnk2 and check that previous_statement_id and balance are correct
bnk2.unlink()
self.assertEqual(bnk1.previous_statement_id.id, False)
self.assertEqual(bnk3.previous_statement_id.id, bnk1.id)
self.assertEqual(bnk4.previous_statement_id.id, bnk3.id)
self.assertEqual(bnk5.previous_statement_id.id, bnk4.id)
self.assertEqual(bnk3.balance_start, 100)
self.assertEqual(bnk3.balance_end_real, 125)
self.assertEqual(bnk4.balance_start, 125)
self.assertEqual(bnk4.balance_end_real, 225)
self.assertEqual(bnk5.balance_start, 225)
self.assertEqual(bnk5.balance_end_real, 325)
# Delete bnk1 bnk3 and bnk4 at the same time and check that balance are correct
(bnk1 + bnk3 + bnk4).unlink()
self.assertEqual(bnk5.previous_statement_id.id, False)
self.assertEqual(bnk5.balance_start, 0)
self.assertEqual(bnk5.balance_end_real, 100)
+5
View File
@@ -640,6 +640,9 @@ action = model.setting_init_bank_account_action()
<button name="%(action_cash_box_out)d" string="Take Money In/Out" type="action" class="oe_highlight" attrs="{'invisible': [('journal_type','!=','cash')]}"/>
<field name="state" widget="statusbar" statusbar_visible="open,confirm"/>
</header>
<div class="alert alert-warning" role="alert" attrs="{'invisible': ['|', ('previous_statement_id', '=', False), ('is_valid_balance_start', '=', True)]}">
The starting balance <b>doesn't match</b> with the ending balance of the previous bank statement.
</div>
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="button_journal_entries"
@@ -654,6 +657,8 @@ action = model.setting_init_bank_account_action()
</div>
<group>
<group>
<field name="previous_statement_id" invisible="1"/>
<field name="is_valid_balance_start" invisible="1"/>
<field name="journal_type" invisible="1"/>
<field name="cashbox_start_id" invisible="1"/>
<field name="cashbox_end_id" invisible="1"/>