270 lines
17 KiB
Python
270 lines
17 KiB
Python
|
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||
|
from odoo.models import Model
|
||
|
from odoo.tests import tagged
|
||
|
from odoo.tests.common import Form
|
||
|
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}.')
|
||
|
|
||
|
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}.')
|
||
|
|
||
|
def test_account_move_hash_with_cash_rounding(self):
|
||
|
# Enable inalterable hash
|
||
|
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
|
||
|
# Required for `invoice_cash_rounding_id` to be visible in the view
|
||
|
self.env.user.groups_id += self.env.ref('account.group_cash_rounding')
|
||
|
# Test 'add_invoice_line' rounding
|
||
|
invoice = self.init_invoice('out_invoice', products=self.product_a+self.product_b)
|
||
|
move_form = Form(invoice)
|
||
|
# Add a cash rounding having 'add_invoice_line'.
|
||
|
move_form.invoice_cash_rounding_id = self.cash_rounding_a
|
||
|
with move_form.invoice_line_ids.edit(0) as line_form:
|
||
|
line_form.price_unit = 999.99
|
||
|
move_form.save()
|
||
|
|
||
|
# Should not raise
|
||
|
invoice.action_post()
|
||
|
|
||
|
self.assertEqual(invoice.amount_total, 1410.0)
|
||
|
self.assertEqual(invoice.amount_untaxed, 1200.0)
|
||
|
self.assertEqual(invoice.amount_tax, 210)
|
||
|
self.assertEqual(len(invoice.invoice_line_ids), 2)
|
||
|
self.assertEqual(len(invoice.line_ids), 6)
|