[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/odoo#114567

X-original-commit: cfd71892aabf1510273b28032ff8f2d4cfd4d1f7
Signed-off-by: William André (wan) <wan@odoo.com>
Signed-off-by: Yolann Sabaux (yosa) <yosa@odoo.com>
This commit is contained in:
Yolann Sabaux
2023-03-07 16:56:45 +01:00
parent 3e0e643212
commit f091c5d701
3 changed files with 62 additions and 3 deletions
+6 -2
View File
@@ -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:
+1 -1
View File
@@ -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")
@@ -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}.')