mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch - 19022023
This commit is contained in:
@@ -17,9 +17,7 @@ import json
|
||||
import re
|
||||
import warnings
|
||||
|
||||
#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
|
||||
|
||||
|
||||
def calc_check_digits(number):
|
||||
@@ -203,6 +201,7 @@ class AccountMove(models.Model):
|
||||
help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Vendor Credit Note, otherwise a Partner bank account number.',
|
||||
check_company=True)
|
||||
payment_reference = fields.Char(string='Payment Reference', index=True, copy=False,
|
||||
compute='_compute_payment_reference', store=True, readonly=False,
|
||||
help="The payment reference to set on journal items.")
|
||||
payment_id = fields.Many2one(
|
||||
index=True,
|
||||
@@ -1300,6 +1299,14 @@ class AccountMove(models.Model):
|
||||
'message': "%s\n\n%s" % (changed, detected)
|
||||
}}
|
||||
|
||||
@api.depends('state')
|
||||
def _compute_payment_reference(self):
|
||||
for move in self.filtered(lambda m: (
|
||||
m.state == 'posted'
|
||||
and m._auto_compute_invoice_reference()
|
||||
)):
|
||||
move.payment_reference = move._get_invoice_computed_reference()
|
||||
|
||||
def _get_last_sequence_domain(self, relaxed=False):
|
||||
self.ensure_one()
|
||||
if not self.date or not self.journal_id:
|
||||
@@ -2057,8 +2064,8 @@ class AccountMove(models.Model):
|
||||
|
||||
def write(self, 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']):
|
||||
@@ -2742,15 +2749,6 @@ class AccountMove(models.Model):
|
||||
for move in to_post:
|
||||
move.message_subscribe([p.id for p in [move.partner_id] if p not in move.sudo().message_partner_ids])
|
||||
|
||||
# Compute 'ref' for 'out_invoice'.
|
||||
if move._auto_compute_invoice_reference():
|
||||
to_write = {
|
||||
'payment_reference': move._get_invoice_computed_reference(),
|
||||
'line_ids': []
|
||||
}
|
||||
for line in move.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')):
|
||||
to_write['line_ids'].append((1, line.id, {'name': to_write['payment_reference']}))
|
||||
move.write(to_write)
|
||||
|
||||
for move in to_post:
|
||||
if move.is_sale_document() \
|
||||
@@ -2904,6 +2902,18 @@ class AccountMove(models.Model):
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -2927,6 +2937,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]
|
||||
@@ -2936,11 +2948,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
|
||||
@@ -3246,7 +3258,7 @@ class AccountMoveLine(models.Model):
|
||||
account_internal_group = fields.Selection(related='account_id.user_type_id.internal_group', string="Internal Group", readonly=True)
|
||||
account_root_id = fields.Many2one(related='account_id.root_id', string="Account Root", store=True, readonly=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
name = fields.Char(string='Label', tracking=True)
|
||||
name = fields.Char(string='Label', tracking=True, compute='_compute_name', store=True, readonly=False)
|
||||
quantity = fields.Float(string='Quantity',
|
||||
default=lambda self: 0 if self._context.get('default_display_type') else 1.0, digits='Product Unit of Measure',
|
||||
help="The optional quantity expressed by this line, eg: number of product sold. "
|
||||
@@ -3425,6 +3437,15 @@ class AccountMoveLine(models.Model):
|
||||
account = repartition_line.account_id
|
||||
return account
|
||||
|
||||
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 _get_computed_name(self):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -3577,6 +3598,11 @@ class AccountMoveLine(models.Model):
|
||||
if rec:
|
||||
record.analytic_tag_ids = rec.analytic_tag_ids
|
||||
|
||||
@api.depends('move_id.payment_reference')
|
||||
def _compute_name(self):
|
||||
for line in self.filtered(lambda l: not l.name and l.account_id.user_type_id.type in ('receivable', 'payable')):
|
||||
line.name = line.move_id.payment_reference
|
||||
|
||||
def _get_price_total_and_subtotal(self, price_unit=None, quantity=None, discount=None, currency=None, product=None, partner=None, taxes=None, move_type=None):
|
||||
self.ensure_one()
|
||||
return self._get_price_total_and_subtotal_model(
|
||||
@@ -4220,10 +4246,18 @@ 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')),
|
||||
))
|
||||
for line in self:
|
||||
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.'))
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import timedelta, datetime, date
|
||||
import calendar
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from flectra.addons.account.models.account_move import MAX_HASH_VERSION
|
||||
from flectra import fields, models, api, _
|
||||
from flectra.exceptions import ValidationError, UserError, RedirectWarning
|
||||
from flectra.tools.misc import format_date
|
||||
@@ -515,8 +516,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
|
||||
|
||||
@@ -8,6 +8,7 @@ from . import test_account_move_in_invoice
|
||||
from . import test_account_move_in_refund
|
||||
from . import test_account_move_entry
|
||||
from . import test_invoice_tax_amount_by_group
|
||||
from . import test_account_inalterable_hash
|
||||
from . import test_account_journal
|
||||
from . import test_account_account
|
||||
from . import test_account_tax
|
||||
|
||||
186
addons/account/tests/test_account_inalterable_hash.py
Normal file
186
addons/account/tests/test_account_inalterable_hash.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from flectra.models import Model
|
||||
from flectra.tests import tagged
|
||||
from flectra import fields
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.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.invoice_date_due = fields.Date.from_string('2023-01-02')
|
||||
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}.')
|
||||
|
||||
# Let's try with the one of the subfields
|
||||
Model.write(moves[4], {'date': fields.Date.from_string("2023-01-05")}) # Revert the previous change
|
||||
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}.')
|
||||
@@ -40,6 +40,7 @@ class Partner(models.Model):
|
||||
replacement_xml = """
|
||||
<div>
|
||||
<field name="country_enforce_cities" invisible="1"/>
|
||||
<field name="type" invisible="1"/>
|
||||
<field name="parent_id" invisible="1"/>
|
||||
<field name='city' placeholder="%(placeholder)s" class="o_address_city"
|
||||
attrs="{
|
||||
|
||||
@@ -249,12 +249,11 @@ class StockPicking(models.Model):
|
||||
sale_order = self.sale_id
|
||||
if sale_order and self.carrier_id.invoice_policy == 'real' and self.carrier_price:
|
||||
delivery_lines = sale_order.order_line.filtered(lambda l: l.is_delivery and l.currency_id.is_zero(l.price_unit) and l.product_id == self.carrier_id.product_id)
|
||||
carrier_price = self.carrier_price * (1.0 + (float(self.carrier_id.margin) / 100.0))
|
||||
if not delivery_lines:
|
||||
delivery_lines = [sale_order._create_delivery_line(self.carrier_id, carrier_price)]
|
||||
delivery_lines = [sale_order._create_delivery_line(self.carrier_id, self.carrier_price)]
|
||||
delivery_line = delivery_lines[0]
|
||||
delivery_line[0].write({
|
||||
'price_unit': carrier_price,
|
||||
'price_unit': self.carrier_price,
|
||||
# remove the estimated price from the description
|
||||
'name': self.carrier_id.with_context(lang=self.partner_id.lang).name,
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ class GoogleService(models.AbstractModel):
|
||||
}
|
||||
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
base_url = get_param('web.base.url', default='http://www.flectrahq.com?NoBaseUrl')
|
||||
base_url = self._context.get('base_url') or self.env.user.get_base_url()
|
||||
client_id = get_param('google_%s_client_id' % (service,), default=False)
|
||||
|
||||
encoded_params = urls.url_encode({
|
||||
|
||||
@@ -18,7 +18,8 @@ class GoogleCalendarController(http.Controller):
|
||||
this URL for authorization for example
|
||||
"""
|
||||
if model == 'calendar.event':
|
||||
GoogleCal = GoogleCalendarService(request.env['google.service'])
|
||||
base_url = request.httprequest.url_root.strip('/')
|
||||
GoogleCal = GoogleCalendarService(request.env['google.service'].with_context(base_url=base_url))
|
||||
|
||||
# Checking that admin have already configured Google API for google synchronization !
|
||||
client_id = request.env['ir.config_parameter'].sudo().get_param('google_calendar_client_id')
|
||||
|
||||
@@ -10,6 +10,7 @@ const {
|
||||
afterNextRender,
|
||||
beforeEach,
|
||||
createRootComponent,
|
||||
nextTick,
|
||||
start,
|
||||
} = require('mail/static/src/utils/test_utils.js');
|
||||
|
||||
@@ -151,6 +152,45 @@ QUnit.test("suggested recipient without partner are unchecked by default", async
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("suggested recipient without partner are unchecked when closing the dialog without creating partner", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data['res.fake'].records.push({
|
||||
id: 10,
|
||||
email_cc: "john@test.be",
|
||||
});
|
||||
|
||||
const params = {
|
||||
archs: {},
|
||||
|
||||
};
|
||||
params.archs["res.partner,false,form"] = `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
</form>
|
||||
`;
|
||||
|
||||
await this.start(params);
|
||||
const chatter = this.env.models['mail.chatter'].create({
|
||||
threadId: 10,
|
||||
threadModel: 'res.fake',
|
||||
});
|
||||
await this.createChatterComponent({ chatter });
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).click()
|
||||
);
|
||||
// click on checkbox to open dialog
|
||||
document.querySelector('.o_ComposerSuggestedRecipient:not([data-partner-id]) input[type=checkbox]').click();
|
||||
await nextTick();
|
||||
// close dialog without changing anything
|
||||
document.querySelector('.modal-dialog .close').click();
|
||||
|
||||
assert.notOk(
|
||||
document.querySelector('.o_ComposerSuggestedRecipient:not([data-partner-id]) input[type=checkbox]').checked,
|
||||
"suggested recipient without partner must be unchecked",
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("suggested recipient with partner are checked by default", async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -129,6 +129,7 @@ class ComposerSuggestedRecipient extends Component {
|
||||
this._isDialogOpen = true;
|
||||
widget.on('closed', this, () => {
|
||||
this._isDialogOpen = false;
|
||||
this._checkboxRef.el.checked = !!this.suggestedRecipientInfo.partner;
|
||||
});
|
||||
widget.context = Object.assign({}, widget.context, session.user_context)
|
||||
widget.open();
|
||||
|
||||
@@ -64,6 +64,8 @@ class StockPicking(models.Model):
|
||||
|
||||
for picking in self:
|
||||
productions_to_done = picking._get_subcontracted_productions()._subcontracting_filter_to_done()
|
||||
if not productions_to_done:
|
||||
continue
|
||||
production_ids_backorder = []
|
||||
if not self.env.context.get('cancel_backorder'):
|
||||
production_ids_backorder = productions_to_done.filtered(lambda mo: mo.state == "progress").ids
|
||||
|
||||
@@ -54,14 +54,14 @@ class StockRule(models.Model):
|
||||
group_id = fields.Many2one('procurement.group', 'Fixed Procurement Group')
|
||||
action = fields.Selection(
|
||||
selection=[('pull', 'Pull From'), ('push', 'Push To'), ('pull_push', 'Pull & Push')], string='Action',
|
||||
required=True)
|
||||
required=True, index=True)
|
||||
sequence = fields.Integer('Sequence', default=20)
|
||||
company_id = fields.Many2one('res.company', 'Company',
|
||||
default=lambda self: self.env.company,
|
||||
domain="[('id', '=?', route_company_id)]")
|
||||
location_id = fields.Many2one('stock.location', 'Destination Location', required=True, check_company=True)
|
||||
domain="[('id', '=?', route_company_id)]", index=True)
|
||||
location_id = fields.Many2one('stock.location', 'Destination Location', required=True, check_company=True, index=True)
|
||||
location_src_id = fields.Many2one('stock.location', 'Source Location', check_company=True)
|
||||
route_id = fields.Many2one('stock.location.route', 'Route', required=True, ondelete='cascade')
|
||||
route_id = fields.Many2one('stock.location.route', 'Route', required=True, ondelete='cascade', index=True)
|
||||
route_company_id = fields.Many2one(related='route_id.company_id', string='Route Company')
|
||||
procure_method = fields.Selection([
|
||||
('make_to_stock', 'Take From Stock'),
|
||||
@@ -85,7 +85,7 @@ class StockRule(models.Model):
|
||||
propagate_cancel = fields.Boolean(
|
||||
'Cancel Next Move', default=False,
|
||||
help="When ticked, if the move created by this rule is cancelled, the next move will be cancelled too.")
|
||||
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', check_company=True)
|
||||
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', check_company=True, index=True)
|
||||
propagate_warehouse_id = fields.Many2one(
|
||||
'stock.warehouse', 'Warehouse to Propagate',
|
||||
help="The warehouse to propagate on the created move/procurement, which can be different of the warehouse this rule is for (e.g for resupplying rules from another warehouse)")
|
||||
|
||||
@@ -369,6 +369,11 @@ class TestBatchPicking02(TransactionCase):
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
self.productB = self.env['product.product'].create({
|
||||
'name': 'Product B',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
def test_same_package_several_pickings(self):
|
||||
"""
|
||||
@@ -416,3 +421,53 @@ class TestBatchPicking02(TransactionCase):
|
||||
{'state': 'done', 'quantity_done': 7},
|
||||
])
|
||||
self.assertEqual(pickings.move_line_ids.result_package_id, package)
|
||||
|
||||
|
||||
def test_batch_validation_without_backorder(self):
|
||||
loc1, loc2 = self.stock_location.child_ids
|
||||
self.env['stock.quant']._update_available_quantity(self.productA, loc1, 10)
|
||||
self.env['stock.quant']._update_available_quantity(self.productB, loc1, 10)
|
||||
picking_1 = self.env['stock.picking'].create({
|
||||
'location_id': loc1.id,
|
||||
'location_dest_id': loc2.id,
|
||||
'picking_type_id': self.picking_type_internal.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': self.productA.name,
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': picking_1.id,
|
||||
'location_id': loc1.id,
|
||||
'location_dest_id': loc2.id,
|
||||
})
|
||||
|
||||
picking_2 = self.env['stock.picking'].create({
|
||||
'location_id': loc1.id,
|
||||
'location_dest_id': loc2.id,
|
||||
'picking_type_id': self.picking_type_internal.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': self.productB.name,
|
||||
'product_id': self.productB.id,
|
||||
'product_uom_qty': 5,
|
||||
'product_uom': self.productB.uom_id.id,
|
||||
'picking_id': picking_2.id,
|
||||
'location_id': loc1.id,
|
||||
'location_dest_id': loc2.id,
|
||||
})
|
||||
(picking_1 | picking_2).action_confirm()
|
||||
(picking_1 | picking_2).action_assign()
|
||||
picking_2.move_lines.move_line_ids.write({'qty_done': 1})
|
||||
|
||||
batch = self.env['stock.picking.batch'].create({
|
||||
'name': 'Batch 1',
|
||||
'company_id': self.env.company.id,
|
||||
'picking_ids': [(4, picking_1.id), (4, picking_2.id)]
|
||||
})
|
||||
batch.action_confirm()
|
||||
action = batch.action_done()
|
||||
Form(self.env[action['res_model']].with_context(action['context'])).save().process_cancel_backorder()
|
||||
self.assertEqual(batch.state, 'done')
|
||||
|
||||
@@ -20,6 +20,14 @@ options.registry.TableOfContent = options.Class.extend({
|
||||
this.observer.observe(targetNode, config);
|
||||
return this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy: function () {
|
||||
// The observer needs to be disconnected first.
|
||||
this.observer.disconnect();
|
||||
this._super(...arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user