feat(numbering): immutable name/doc_index + unlink block on issued docs
write() override raises UserError if name or x_fc_doc_index is in vals and differs from the stored value (bypass: context flag fp_allow_name_rename=True for the SO-confirm rename + bulk WO creation paths). unlink() override raises UserError for records that have been issued a name; applies to all users including admins — cancellation must go through the state machine. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating',
|
'name': 'Fusion Plating',
|
||||||
'version': '19.0.18.15.13',
|
'version': '19.0.18.15.14',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -100,3 +100,59 @@ class FpParentNumberedMixin(models.AbstractModel):
|
|||||||
'Issued <strong>%s</strong> to %s #%s.'
|
'Issued <strong>%s</strong> to %s #%s.'
|
||||||
) % (new_name, self._name, self.id))
|
) % (new_name, self._name, self.id))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Immutability: name + x_fc_doc_index can't change post-issuance.
|
||||||
|
# Bypass: context flag fp_allow_name_rename=True. Used ONLY by:
|
||||||
|
# 1. sale.order.action_confirm (Q -> SO rename, one-time)
|
||||||
|
# 2. Bulk WO creation mid-create (sets names explicitly)
|
||||||
|
# 3. Legacy-sequence fallback path in child create() overrides
|
||||||
|
# Compliance: once issued, an audit-trail number can never change.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
FP_IMMUTABLE_FIELDS = ('name', 'x_fc_doc_index')
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if not self.env.context.get('fp_allow_name_rename'):
|
||||||
|
for f in self.FP_IMMUTABLE_FIELDS:
|
||||||
|
if f in vals:
|
||||||
|
for rec in self:
|
||||||
|
current = rec[f]
|
||||||
|
if current and current != vals[f]:
|
||||||
|
raise UserError(_(
|
||||||
|
'Field "%(field)s" on %(model)s "%(name)s" '
|
||||||
|
'is immutable. Once issued, it cannot be '
|
||||||
|
'changed - this preserves the compliance '
|
||||||
|
'audit trail. (Attempted: %(old)r -> %(new)r)'
|
||||||
|
) % {
|
||||||
|
'field': f, 'model': self._description,
|
||||||
|
'name': rec.display_name,
|
||||||
|
'old': current, 'new': vals[f],
|
||||||
|
})
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Unlink block: issued documents can't be hard-deleted.
|
||||||
|
# Cancellation must go through the state machine so the audit trail
|
||||||
|
# keeps the issued number tied to its cancellation reason. Hard
|
||||||
|
# delete would leave a phantom gap in the counter. Applies to ALL
|
||||||
|
# users including admins — no group bypass.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def unlink(self):
|
||||||
|
for rec in self:
|
||||||
|
# Records still in their initial 'New' state (no number
|
||||||
|
# ever issued) are fine to delete — they're not yet in
|
||||||
|
# the audit trail. Once x_fc_doc_index is non-zero OR
|
||||||
|
# name is something other than 'New' / '/', the record
|
||||||
|
# has been issued and is permanent.
|
||||||
|
issued = rec.x_fc_doc_index or (
|
||||||
|
rec.name and rec.name not in (False, '', 'New', '/')
|
||||||
|
)
|
||||||
|
if issued:
|
||||||
|
raise UserError(_(
|
||||||
|
'Document "%(name)s" cannot be deleted - it is '
|
||||||
|
'part of the compliance audit trail. Cancel it '
|
||||||
|
'instead (use the state machine\'s Cancel action). '
|
||||||
|
'This rule applies to all users including '
|
||||||
|
'administrators.'
|
||||||
|
) % {'name': rec.display_name})
|
||||||
|
return super().unlink()
|
||||||
|
|||||||
Reference in New Issue
Block a user