Skip to content

Commit cfd7189

Browse files
committed
[FIX] account: prevent wrong floating representation of currency
Steps to reproduce: - In Journals/Bills/Advanced activate "Lock post entries with hash" - Create a purchase tax of 17% - Create a bill with a product of a cost of 30 and apply the "17%" tax - Post it - Export the data inalterability check report Issue: "Corrupted data" Cause: In python we have a reprentation issue ``` >>> 30*0.17 5.1000000000000005 ``` We define the hash string during the `_compute_string_to_hash` at the move creation. The issue is, at that time, the move_line tax debit is not rounded. Therefore, when we print the report, we take the `move.line_id. debit` from the db which is rounded and equal to '5.10' and compare it to '5.1000000000000005' which gives a different hash Solution: Implementing a V3 version that uses `repr` for monetary fields. opw-3072693 closes odoo#112016 Signed-off-by: William André (wan) <wan@odoo.com>
1 parent 5ef7bb2 commit cfd7189

File tree

3 files changed

+62
-3
lines changed

3 files changed

+62
-3
lines changed

addons/account/models/account_move.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
email_split,
2121
float_compare,
2222
float_is_zero,
23+
float_repr,
2324
format_amount,
2425
format_date,
2526
formatLang,
@@ -30,7 +31,7 @@
3031
)
3132

3233

33-
MAX_HASH_VERSION = 2
34+
MAX_HASH_VERSION = 3
3435

3536
TYPE_REVERSE_MAP = {
3637
'entry': 'entry',
@@ -2646,7 +2647,7 @@ def _get_integrity_hash_fields(self):
26462647
hash_version = self._context.get('hash_version', MAX_HASH_VERSION)
26472648
if hash_version == 1:
26482649
return ['date', 'journal_id', 'company_id']
2649-
elif hash_version == MAX_HASH_VERSION:
2650+
elif hash_version in (2, 3):
26502651
return ['name', 'date', 'journal_id', 'company_id']
26512652
raise NotImplementedError(f"hash_version={hash_version} doesn't exist")
26522653

@@ -2680,9 +2681,12 @@ def _compute_hash(self, previous_hash):
26802681
@api.depends_context('hash_version')
26812682
def _compute_string_to_hash(self):
26822683
def _getattrstring(obj, field_str):
2684+
hash_version = self._context.get('hash_version', MAX_HASH_VERSION)
26832685
field_value = obj[field_str]
26842686
if obj._fields[field_str].type == 'many2one':
26852687
field_value = field_value.id
2688+
if obj._fields[field_str].type == 'monetary' and hash_version >= 3:
2689+
return float_repr(field_value, obj.currency_id.decimal_places)
26862690
return str(field_value)
26872691

26882692
for move in self:

addons/account/models/account_move_line.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2534,7 +2534,7 @@ def _get_integrity_hash_fields(self):
25342534
hash_version = self._context.get('hash_version', MAX_HASH_VERSION)
25352535
if hash_version == 1:
25362536
return ['debit', 'credit', 'account_id', 'partner_id']
2537-
elif hash_version == MAX_HASH_VERSION:
2537+
elif hash_version in (2, 3):
25382538
return ['name', 'debit', 'credit', 'account_id', 'partner_id']
25392539
raise NotImplementedError(f"hash_version={hash_version} doesn't exist")
25402540

addons/account/tests/test_account_inalterable_hash.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,58 @@ def test_account_move_hash_versioning_v1_to_v2(self):
188188
moves_v1_bis.with_context(hash_version=1).action_post()
189189
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
190190
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves_v1_bis[0].id}.')
191+
192+
def test_account_move_hash_versioning_3(self):
193+
"""
194+
Version 2 does not take into account floating point representation issues.
195+
Test that version 3 covers correctly this case
196+
"""
197+
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000],
198+
post=True) # Not hashed
199+
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
200+
moves_v3 = (
201+
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[30*0.17, 2000])
202+
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
203+
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
204+
)
205+
moves_v3.action_post()
206+
207+
# invalidate cache
208+
moves_v3[0].line_ids[0].invalidate_recordset()
209+
210+
integrity_check_v3 = moves_v3.company_id._check_hash_integrity()['results'][0]
211+
self.assertRegex(integrity_check_v3['msg_cover'], f'Entries are hashed from {moves_v3[0].name}.*')
212+
213+
def test_account_move_hash_versioning_v2_to_v3(self):
214+
"""
215+
We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
216+
This test focuses on the case with version 2 and version 3.
217+
"""
218+
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000],
219+
post=True) # Not hashed
220+
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
221+
moves_v2 = (
222+
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
223+
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
224+
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
225+
)
226+
moves_v2.with_context(hash_version=2).action_post()
227+
228+
moves_v3 = (
229+
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
230+
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
231+
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
232+
)
233+
moves_v3.with_context(hash_version=3).action_post()
234+
235+
moves = moves_v2 | moves_v3
236+
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
237+
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
238+
self.assertEqual(integrity_check['first_move_date'],
239+
format_date(self.env, fields.Date.to_string(moves[0].date)))
240+
self.assertEqual(integrity_check['last_move_date'],
241+
format_date(self.env, fields.Date.to_string(moves[-1].date)))
242+
243+
Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
244+
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
245+
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')

0 commit comments

Comments
 (0)