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 @@ - +