diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 87ac3b1e02d..8b2cfacfd0d 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -22,6 +22,7 @@ from odoo.tools import ( email_split, float_compare, float_is_zero, + float_repr, format_amount, format_date, formatLang, @@ -34,7 +35,7 @@ from odoo.tools import ( _logger = logging.getLogger(__name__) -MAX_HASH_VERSION = 2 +MAX_HASH_VERSION = 3 TYPE_REVERSE_MAP = { 'entry': 'entry', @@ -2690,7 +2691,7 @@ class AccountMove(models.Model): 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: + elif hash_version in (2, 3): return ['name', 'date', 'journal_id', 'company_id'] raise NotImplementedError(f"hash_version={hash_version} doesn't exist") @@ -2724,9 +2725,12 @@ class AccountMove(models.Model): @api.depends_context('hash_version') def _compute_string_to_hash(self): def _getattrstring(obj, field_str): + hash_version = self._context.get('hash_version', MAX_HASH_VERSION) field_value = obj[field_str] if obj._fields[field_str].type == 'many2one': field_value = field_value.id + if obj._fields[field_str].type == 'monetary' and hash_version >= 3: + return float_repr(field_value, obj.currency_id.decimal_places) return str(field_value) for move in self: diff --git a/addons/account/models/account_move_line.py b/addons/account/models/account_move_line.py index 2a32ce77ea6..644bf7d8989 100644 --- a/addons/account/models/account_move_line.py +++ b/addons/account/models/account_move_line.py @@ -2766,7 +2766,7 @@ class AccountMoveLine(models.Model): 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: + elif hash_version in (2, 3): return ['name', 'debit', 'credit', 'account_id', 'partner_id'] raise NotImplementedError(f"hash_version={hash_version} doesn't exist") diff --git a/addons/account/tests/test_account_inalterable_hash.py b/addons/account/tests/test_account_inalterable_hash.py index 31995f039eb..f79209bdf19 100644 --- a/addons/account/tests/test_account_inalterable_hash.py +++ b/addons/account/tests/test_account_inalterable_hash.py @@ -188,3 +188,58 @@ class TestAccountMoveInalterableHash(AccountTestInvoicingCommon): 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}.') + + def test_account_move_hash_versioning_3(self): + """ + Version 2 does not take into account floating point representation issues. + Test that version 3 covers correctly this case + """ + 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_v3 = ( + self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[30*0.17, 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_v3.action_post() + + # invalidate cache + moves_v3[0].line_ids[0].invalidate_recordset() + + integrity_check_v3 = moves_v3.company_id._check_hash_integrity()['results'][0] + self.assertRegex(integrity_check_v3['msg_cover'], f'Entries are hashed from {moves_v3[0].name}.*') + + def test_account_move_hash_versioning_v2_to_v3(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 with version 2 and version 3. + """ + 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_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() + + moves_v3 = ( + 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_v3.with_context(hash_version=3).action_post() + + moves = moves_v2 | moves_v3 + 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))) + + 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}.')