[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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}.')
|
||||
Reference in New Issue
Block a user