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',
|
||||
'version': '19.0.18.15.13',
|
||||
'version': '19.0.18.15.14',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
|
||||
@@ -100,3 +100,59 @@ class FpParentNumberedMixin(models.AbstractModel):
|
||||
'Issued <strong>%s</strong> to %s #%s.'
|
||||
) % (new_name, self._name, self.id))
|
||||
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