From eb33fdf9d8d89bfc355ab81fde0a200cc0249356 Mon Sep 17 00:00:00 2001 From: "Habib (ayh)" Date: Tue, 14 Feb 2023 15:17:35 +0000 Subject: [PATCH] [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 Signed-off-by: Ayob Habib (ayh) Signed-off-by: Christophe Simonis --- addons/account/i18n/account.pot | 32 ++ .../account/models/account_bank_statement.py | 230 +++++++----- .../models/account_bank_statement_line.py | 16 +- .../tests/test_account_bank_statement.py | 350 ++++++++++++++---- .../views/account_bank_statement_views.xml | 5 +- 5 files changed, 453 insertions(+), 180 deletions(-) diff --git a/addons/account/i18n/account.pot b/addons/account/i18n/account.pot index 9f22fa78d67..b643538e177 100644 --- a/addons/account/i18n/account.pot +++ b/addons/account/i18n/account.pot @@ -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 diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 6cfcdfd4783..ab25e37e01d 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -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 diff --git a/addons/account/models/account_bank_statement_line.py b/addons/account/models/account_bank_statement_line.py index aebeee8641d..56a998234d3 100644 --- a/addons/account/models/account_bank_statement_line.py +++ b/addons/account/models/account_bank_statement_line.py @@ -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 diff --git a/addons/account/tests/test_account_bank_statement.py b/addons/account/tests/test_account_bank_statement.py index 157e921e12d..6546de84c8e 100644 --- a/addons/account/tests/test_account_bank_statement.py +++ b/addons/account/tests/test_account_bank_statement.py @@ -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, + }]) diff --git a/addons/account/views/account_bank_statement_views.xml b/addons/account/views/account_bank_statement_views.xml index 23682eb9bc5..42d0e7ad0b8 100644 --- a/addons/account/views/account_bank_statement_views.xml +++ b/addons/account/views/account_bank_statement_views.xml @@ -7,7 +7,8 @@ account.bank.statement.tree account.bank.statement - @@ -31,7 +32,7 @@ - +