[FIX] account: add account move's name into the fields used by the hash

Currently, it is possible to resequence account moves which are hashed.
This should not be the case. Therefore, we are adding the name of the
move into the list of hashed fields.

However, since we are changing the hashing algorithm by including a new
field in its computation, we must add a versioning system to make sure
we don't break the integrity (data inalterability) report.

In practice, this means that prior to this commit, all hashed moves used
the fields of v1, and moves after this commit will use v2 (which adds
the name into the list of hashed fields). Thus, whenever we generate
the integrity report, we will run the v1 algorithm, and if it a
potential corrupted move is found, we will switch to v2 and check again.
If it also fails, this means the hash is indeed corrupted.

task-id 3102481

closes odoo/odoo#114376

X-original-commit: cb76725b071d89a9f3e81861f19d073138741b58
Signed-off-by: William André (wan) <wan@odoo.com>
Signed-off-by: Ricardo Gomes Rodrigues (rigr) <rigr@odoo.com>
This commit is contained in:
Ricardo Gomes Rodrigues (rigr)
2023-03-05 16:30:57 +01:00
parent 6ef3772847
commit da019b6280
6 changed files with 281 additions and 32 deletions
+1 -2
View File
@@ -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
+55 -23
View File
@@ -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
# -------------------------------------------------------------------------
+27 -6
View File
@@ -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'):
+7 -1
View File
@@ -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
+1
View File
@@ -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
@@ -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}.')