diff --git a/addons/account/i18n/account.pot b/addons/account/i18n/account.pot index ce39a9b040c..9f22fa78d67 100644 --- a/addons/account/i18n/account.pot +++ b/addons/account/i18n/account.pot @@ -15494,8 +15494,7 @@ msgstr "" #: code:addons/account/models/account_move_line.py:0 #, python-format msgid "" -"You cannot edit the following fields due to restrict mode being activated on" -" the journal: %s." +"You cannot edit the following fields: %s.\nThe following entries are already hashed:\n%s" msgstr "" #. module: account diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index cd2028f4cbf..4a682a6db1f 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -34,9 +34,7 @@ from odoo.tools import ( _logger = logging.getLogger(__name__) -#forbidden fields -INTEGRITY_HASH_MOVE_FIELDS = ('date', 'journal_id', 'company_id') -INTEGRITY_HASH_LINE_FIELDS = ('debit', 'credit', 'account_id', 'partner_id') +MAX_HASH_VERSION = 2 TYPE_REVERSE_MAP = { 'entry': 'entry', @@ -81,7 +79,7 @@ class AccountMove(models.Model): # === Accounting fields === # name = fields.Char( string='Number', - compute='_compute_name', readonly=False, store=True, + compute='_compute_name', inverse='_inverse_name', readonly=False, store=True, copy=False, tracking=True, index='trigram', @@ -343,6 +341,7 @@ class AccountMove(models.Model): copy=False, help="The payment reference to set on journal items.", tracking=True, + compute='_compute_payment_reference', inverse='_inverse_payment_reference', store=True, readonly=False, ) display_qr_code = fields.Boolean( string="Display QR-code", @@ -587,6 +586,15 @@ class AccountMove(models.Model): # COMPUTE METHODS # ------------------------------------------------------------------------- + def _compute_payment_reference(self): + for move in self.filtered(lambda m: ( + m.state == 'posted' + and m.move_type == 'out_invoice' + and not m.payment_reference + )): + move.payment_reference = move._get_invoice_computed_reference() + self._inverse_payment_reference() + @api.depends('invoice_date', 'company_id') def _compute_date(self): for move in self: @@ -708,6 +716,7 @@ class AccountMove(models.Model): move._set_next_sequence() self.filtered(lambda m: not m.name).name = '/' + self._inverse_name() @api.depends('journal_id', 'date') def _compute_highest_name(self): @@ -998,7 +1007,6 @@ class AccountMove(models.Model): 'discount_date': invoice_payment_terms.get('discount_date'), 'discount_balance': invoice_payment_terms.get('discount_balance') or 0.0, 'discount_amount_currency': invoice_payment_terms.get('discount_amount_currency') or 0.0, - } if multiple_installments: values['name'] = f'{values["name"]} installment #{i + 1}'.lstrip() @@ -1017,7 +1025,6 @@ class AccountMove(models.Model): })] = { 'balance': invoice.amount_total_signed, 'amount_currency': invoice.amount_total_in_currency_signed, - 'name': invoice.payment_reference or '', } def _compute_payments_widget_to_reconcile_info(self): @@ -1547,6 +1554,16 @@ class AccountMove(models.Model): if invoice.journal_id.currency_id and invoice.journal_id.currency_id != invoice.currency_id: self.env.add_to_compute(self._fields['journal_id'], invoice) + def _inverse_payment_reference(self): + self.line_ids._conditional_add_to_compute('name', lambda line: ( + line.display_type == 'payment_term' + )) + + def _inverse_name(self): + self._conditional_add_to_compute('payment_reference', lambda move: ( + move.name and move.name != '/' + )) + # ------------------------------------------------------------------------- # ONCHANGE METHODS # ------------------------------------------------------------------------- @@ -2093,7 +2110,6 @@ class AccountMove(models.Model): def existing(): return { move: { - 'payment_reference': move.payment_reference, 'commercial_partner_id': move.commercial_partner_id, } for move in container['records'].filtered(lambda m: m.is_invoice(True)) @@ -2107,8 +2123,6 @@ class AccountMove(models.Model): after = existing() for move in after: - if changed('payment_reference'): - move.line_ids.filtered(lambda l: l.display_type == 'payment_term').name = after[move]['payment_reference'] if changed('commercial_partner_id'): move.line_ids.partner_id = after[move]['commercial_partner_id'] @@ -2234,8 +2248,8 @@ class AccountMove(models.Model): return True self._sanitize_vals(vals) for move in self: - if (move.restrict_mode_hash_table and move.state == "posted" and set(vals).intersection(INTEGRITY_HASH_MOVE_FIELDS)): - raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_MOVE_FIELDS)) + if (move.restrict_mode_hash_table and move.state == "posted" and set(vals).intersection(move._get_integrity_hash_fields())): + raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(move._get_integrity_hash_fields())) if (move.restrict_mode_hash_table and move.inalterable_hash and 'inalterable_hash' in vals) or (move.secure_sequence_number and 'secure_sequence_number' in vals): raise UserError(_('You cannot overwrite the values ensuring the inalterability of the accounting.')) if (move.posted_before and 'journal_id' in vals and move.journal_id.id != vals['journal_id']): @@ -2284,6 +2298,7 @@ class AccountMove(models.Model): # Hash the move if vals.get('state') == 'posted': + self.flush_recordset() # Ensure that the name is correctly computed before it is used to generate the hash for move in self.filtered(lambda m: m.restrict_mode_hash_table and not(m.secure_sequence_number or m.inalterable_hash)).sorted(lambda m: (m.date, m.ref or '', m.id)): new_number = move.journal_id.secure_sequence_id.next_by_id() res |= super(AccountMove, move).write({ @@ -2669,6 +2684,18 @@ class AccountMove(models.Model): # HASH # ------------------------------------------------------------------------- + def _get_integrity_hash_fields(self): + # Use the latest hash version by default, but keep the old one for backward compatibility when generating the integrity report. + hash_version = self._context.get('hash_version', MAX_HASH_VERSION) + if hash_version == 1: + return ['date', 'journal_id', 'company_id'] + elif hash_version == MAX_HASH_VERSION: + return ['name', 'date', 'journal_id', 'company_id'] + raise NotImplementedError(f"hash_version={hash_version} doesn't exist") + + def _get_integrity_hash_fields_and_subfields(self): + return self._get_integrity_hash_fields() + [f'line_ids.{subfield}' for subfield in self.line_ids._get_integrity_hash_fields()] + def _get_new_hash(self, secure_seq_number): """ Returns the hash to write on journal entries when they get posted""" self.ensure_one() @@ -2692,6 +2719,8 @@ class AccountMove(models.Model): hash_string = sha256((previous_hash + self.string_to_hash).encode('utf-8')) return hash_string.hexdigest() + @api.depends(lambda self: self._get_integrity_hash_fields_and_subfields()) + @api.depends_context('hash_version') def _compute_string_to_hash(self): def _getattrstring(obj, field_str): field_value = obj[field_str] @@ -2701,11 +2730,11 @@ class AccountMove(models.Model): for move in self: values = {} - for field in INTEGRITY_HASH_MOVE_FIELDS: + for field in move._get_integrity_hash_fields(): values[field] = _getattrstring(move, field) for line in move.line_ids: - for field in INTEGRITY_HASH_LINE_FIELDS: + for field in line._get_integrity_hash_fields(): k = 'line_%d_%s' % (line.id, field) values[k] = _getattrstring(line, field) #make the json serialization canonical @@ -3423,16 +3452,6 @@ class AccountMove(models.Model): if p not in invoice.sudo().message_partner_ids ]) - # Compute 'ref' for 'out_invoice'. - if invoice.move_type == 'out_invoice' and not invoice.payment_reference: - to_write = { - 'payment_reference': invoice._get_invoice_computed_reference(), - 'line_ids': [] - } - for line in invoice.line_ids.filtered(lambda line: line.account_id.account_type in ('asset_receivable', 'liability_payable')): - to_write['line_ids'].append((1, line.id, {'name': to_write['payment_reference']})) - invoice.write(to_write) - if ( invoice.is_sale_document() and invoice.journal_id.sale_activity_type_id @@ -4143,6 +4162,19 @@ class AccountMove(models.Model): # else, attachments with 'res_field' get excluded return res | self.env['account.move.send']._get_linked_attachments(self) + # ------------------------------------------------------------------------- + # TOOLING + # ------------------------------------------------------------------------- + + def _conditional_add_to_compute(self, fname, condition): + field = self._fields[fname] + to_reset = self.filtered(lambda move: + condition(move) + and not self.env.is_protected(field, move) + ) + to_reset.invalidate_recordset([fname]) + self.env.add_to_compute(field, to_reset) + # ------------------------------------------------------------------------- # HOOKS # ------------------------------------------------------------------------- diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index 9a6715774cf..e6d5916007e 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -11,7 +11,7 @@ from odoo.tools import frozendict, formatLang, format_date, float_is_zero, Query from odoo.tools.sql import create_index from odoo.addons.web.controllers.utils import clean_action -INTEGRITY_HASH_LINE_FIELDS = ('debit', 'credit', 'account_id', 'partner_id') +from odoo.addons.account.models.account_move import MAX_HASH_VERSION class AccountMoveLine(models.Model): @@ -461,11 +461,14 @@ class AccountMoveLine(models.Model): @api.depends('product_id', 'journal_id') def _compute_name(self): for line in self: + if line.display_type == 'payment_term': + if line.move_id.payment_reference: + line.name = line.move_id.payment_reference + elif not line.name: + line.name = '' + continue if not line.product_id or line.display_type in ('line_section', 'line_note'): continue - # YTI : Coming from conflicting onchange from account.move - # for line in self.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')): - # line.name = self.payment_reference or '' if line.partner_id.lang: product = line.product_id.with_context(lang=line.partner_id.lang) else: @@ -1403,6 +1406,17 @@ class AccountMoveLine(models.Model): if account_to_write and account_to_write.deprecated: raise UserError(_('You cannot use a deprecated account.')) + inalterable_fields = set(self._get_integrity_hash_fields()).union({'inalterable_hash', 'secure_sequence_number'}) + hashed_moves = self.move_id.filtered('inalterable_hash') + violated_fields = set(vals) & inalterable_fields + if hashed_moves and violated_fields: + raise UserError(_( + "You cannot edit the following fields: %s.\n" + "The following entries are already hashed:\n%s", + ', '.join(f['string'] for f in self.fields_get(violated_fields).values()), + '\n'.join(hashed_moves.mapped('name')), + )) + line_to_write = self vals = self._sanitize_vals(vals) for line in self: @@ -1411,8 +1425,6 @@ class AccountMoveLine(models.Model): continue if line.parent_state == 'posted': - if line.move_id.restrict_mode_hash_table and set(vals).intersection(INTEGRITY_HASH_LINE_FIELDS): - raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_LINE_FIELDS)) if any(key in vals for key in ('tax_ids', 'tax_line_id')): raise UserError(_('You cannot modify the taxes related to a posted journal item, you should reset the journal entry to draft to do so.')) @@ -2506,6 +2518,15 @@ class AccountMoveLine(models.Model): # MISC # ------------------------------------------------------------------------- + def _get_integrity_hash_fields(self): + # Use the new hash version by default, but keep the old one for backward compatibility when generating the integrity report. + hash_version = self._context.get('hash_version', MAX_HASH_VERSION) + if hash_version == 1: + return ['debit', 'credit', 'account_id', 'partner_id'] + elif hash_version == MAX_HASH_VERSION: + return ['name', 'debit', 'credit', 'account_id', 'partner_id'] + raise NotImplementedError(f"hash_version={hash_version} doesn't exist") + def _reconciled_lines(self): ids = [] for aml in self.filtered('reconciled'): diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 0ab69981c34..bc307542fa2 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -8,6 +8,7 @@ from odoo.exceptions import ValidationError, UserError, RedirectWarning from odoo.tools.mail import is_html_empty from odoo.tools.misc import format_date from odoo.tools.float_utils import float_round, float_is_zero +from odoo.addons.account.models.account_move import MAX_HASH_VERSION MONTH_SELECTION = [ @@ -639,8 +640,13 @@ class ResCompany(models.Model): previous_hash = u'' start_move_info = [] hash_corrupted = False + current_hash_version = 1 for move in moves: - if move.inalterable_hash != move._compute_hash(previous_hash=previous_hash): + computed_hash = move.with_context(hash_version=current_hash_version)._compute_hash(previous_hash=previous_hash) + while move.inalterable_hash != computed_hash and current_hash_version < MAX_HASH_VERSION: + current_hash_version += 1 + computed_hash = move.with_context(hash_version=current_hash_version)._compute_hash(previous_hash=previous_hash) + if move.inalterable_hash != computed_hash: rslt.update({'msg_cover': _('Corrupted data on journal entry with id %s.', move.id)}) results_by_journal['results'].append(rslt) hash_corrupted = True diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index 7621da3623a..c5e7dfc81eb 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -9,6 +9,7 @@ from . import test_account_move_in_refund from . import test_account_move_entry from . import test_account_move_date_algorithm from . import test_invoice_tax_totals +from . import test_account_inalterable_hash from . import test_account_journal from . import test_account_account from . import test_account_tax diff --git a/addons/account/tests/test_account_inalterable_hash.py b/addons/account/tests/test_account_inalterable_hash.py new file mode 100644 index 00000000000..31995f039eb --- /dev/null +++ b/addons/account/tests/test_account_inalterable_hash.py @@ -0,0 +1,190 @@ +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.models import Model +from odoo.tests import tagged +from odoo import fields +from odoo.exceptions import UserError +from odoo.tools import format_date + + +@tagged('post_install', '-at_install') +class TestAccountMoveInalterableHash(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + def test_account_move_inalterable_hash(self): + """Test that we cannot alter a field used for the computation of the inalterable hash""" + self.company_data['default_journal_sale'].restrict_mode_hash_table = True + move = self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000], post=True) + + with self.assertRaisesRegex(UserError, "You cannot overwrite the values ensuring the inalterability of the accounting."): + move.inalterable_hash = 'fake_hash' + with self.assertRaisesRegex(UserError, "You cannot overwrite the values ensuring the inalterability of the accounting."): + move.secure_sequence_number = 666 + with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"): + move.name = "fake name" + with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"): + move.date = fields.Date.from_string('2023-01-02') + with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"): + move.company_id = 666 + with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"): + move.write({ + 'company_id': 666, + 'date': fields.Date.from_string('2023-01-03') + }) + + with self.assertRaisesRegex(UserError, "You cannot edit the following fields.*Account.*"): + move.line_ids[0].account_id = move.line_ids[1]['account_id'] + with self.assertRaisesRegex(UserError, "You cannot edit the following fields.*Partner.*"): + move.line_ids[0].partner_id = 666 + + # The following fields are not part of the hash so they can be modified + move.ref = "bla" + move.line_ids[0].date_maturity = fields.Date.from_string('2023-01-02') + + def test_account_move_hash_integrity_report(self): + """Test the hash integrity report""" + moves = ( + self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000]) + ) + moves.action_post() + + # No records to be hashed because the restrict mode is not activated yet + integrity_check = moves.company_id._check_hash_integrity()['results'][0] # First journal + self.assertEqual(integrity_check['msg_cover'], 'This journal is not in strict mode.') + + # No records to be hashed even if the restrict mode is activated because the hashing is not retroactive + self.company_data['default_journal_sale'].restrict_mode_hash_table = True + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertEqual(integrity_check['msg_cover'], 'There isn\'t any journal entry flagged for data inalterability yet for this journal.') + + # Everything should be correctly hashed and verified + new_moves = ( + self.init_invoice("out_invoice", self.partner_a, "2023-01-03", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-04", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_a, "2023-01-05", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-06", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_a, "2023-01-07", amounts=[1000, 2000]) + ) + new_moves.action_post() + moves |= new_moves + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[2].name}.*') + self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[2].date))) + self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date))) + + # Let's change one of the fields used by the hash. It should be detected by the integrity report. + # We need to bypass the write method of account.move to do so. + Model.write(moves[4], {'date': fields.Date.from_string('2023-01-07')}) + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[4].id}.') + + # Revert the previous change + Model.write(moves[4], {'date': fields.Date.from_string("2023-01-05")}) + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[2].name}.*') + + # Let's try with the one of the subfields + Model.write(moves[-1].line_ids[0], {'partner_id': self.partner_b.id}) + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[-1].id}.') + + # Let's try with the inalterable_hash field itself + Model.write(moves[-1].line_ids[0], {'partner_id': self.partner_a.id}) # Revert the previous change + Model.write(moves[-1], {'inalterable_hash': 'fake_hash'}) + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[-1].id}.') + + def test_account_move_hash_versioning_1(self): + """We are updating the hash algorithm. We want to make sure that we do not break the integrity report. + This test focuses on the case where the user has only moves with the old hash algorithm.""" + self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed + self.company_data['default_journal_sale'].restrict_mode_hash_table = True + moves = ( + self.init_invoice("out_invoice", self.partner_a, "2023-01-02", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-04", amounts=[1000, 2000]) + ) + moves.with_context(hash_version=1).action_post() + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*') + self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date))) + self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date))) + + # Let's change one of the fields used by the hash. It should be detected by the integrity report + # independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work. + # We need to bypass the write method of account.move to do so. + Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')}) + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.') + + def test_account_move_hash_versioning_2(self): + """We are updating the hash algorithm. We want to make sure that we do not break the integrity report. + This test focuses on the case where the user has only moves with the new hash algorithm.""" + self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed + self.company_data['default_journal_sale'].restrict_mode_hash_table = True + moves = ( + self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000]) + ) + moves.action_post() + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*') + self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date))) + self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date))) + + # Let's change one of the fields used by the hash. It should be detected by the integrity report + # independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work. + # We need to bypass the write method of account.move to do so. + Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')}) + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.') + + def test_account_move_hash_versioning_v1_to_v2(self): + """We are updating the hash algorithm. We want to make sure that we do not break the integrity report. + This test focuses on the case where the user has moves with both hash algorithms.""" + self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed + self.company_data['default_journal_sale'].restrict_mode_hash_table = True + moves_v1 = ( + self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000]) + ) + moves_v1.with_context(hash_version=1).action_post() + fields_v1 = moves_v1.with_context(hash_version=1)._get_integrity_hash_fields() + moves_v2 = ( + self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000]) + ) + moves_v2.with_context(hash_version=2).action_post() + fields_v2 = moves_v2._get_integrity_hash_fields() + self.assertNotEqual(fields_v1, fields_v2) # Make sure two different hash algorithms were used + + moves = moves_v1 | moves_v2 + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*') + self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date))) + self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date))) + + # Let's change one of the fields used by the hash. It should be detected by the integrity report + # independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work. + # We need to bypass the write method of account.move to do so. + Model.write(moves[4], {'date': fields.Date.from_string('2023-01-07')}) + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[4].id}.') + + # Let's revert the change and make sure that we cannot use the v1 after the v2. + # This means we don't simply check whether the move is correctly hashed with either algorithms, + # but that we can only use v2 after v1 and not go back to v1 afterwards. + Model.write(moves[4], {'date': fields.Date.from_string("2023-01-02")}) # Revert the previous change + moves_v1_bis = ( + self.init_invoice("out_invoice", self.partner_a, "2023-01-10", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-11", amounts=[1000, 2000]) + | self.init_invoice("out_invoice", self.partner_b, "2023-01-12", amounts=[1000, 2000]) + ) + moves_v1_bis.with_context(hash_version=1).action_post() + integrity_check = moves.company_id._check_hash_integrity()['results'][0] + self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves_v1_bis[0].id}.')