feat(fp.certificate): ACL guard + state hooks + age field (Tasks 6-8)
action_issue gated to Manager/QM/Owner via Python AccessError +
view-level groups= on the Issue button (two-layer enforcement).
Manager bypass via fp_skip_cert_authority_gate=True context flag
with chatter audit.
action_issue post-callback calls job._fp_check_advance_after_cert_issue
so the job auto-advances awaiting_cert → awaiting_ship when every
required cert is issued.
write({'state':'voided'}) override calls
job._fp_check_regress_after_cert_void so a previously-issued cert
being voided slides the job back to awaiting_cert and re-notifies
the QM.
x_fc_age_hours non-stored Float drives the Quality Dashboard age
chip + overdue filter.
Version bump 19.0.7.9.3 → 19.0.8.0.0 (spec said 19.0.6.0.0 but
current is already higher; bumped to next major instead).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Certificates',
|
||||
'version': '19.0.7.9.3',
|
||||
'version': '19.0.8.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Certificate registry for CoC, thickness reports, and quality documents.',
|
||||
'description': """
|
||||
|
||||
@@ -224,6 +224,13 @@ class FpCertificate(models.Model):
|
||||
[('draft', 'Draft'), ('issued', 'Issued'), ('voided', 'Voided')],
|
||||
string='Status', default='draft', tracking=True, required=True,
|
||||
)
|
||||
x_fc_age_hours = fields.Float(
|
||||
string='Age (hours)',
|
||||
compute='_compute_x_fc_age_hours',
|
||||
help='Hours since the cert was created. Drives the Quality '
|
||||
'Dashboard age chip and overdue filter. Non-stored — '
|
||||
'always fresh on read. Spec 2026-05-25.',
|
||||
)
|
||||
void_reason = fields.Text(string='Void Reason')
|
||||
notes = fields.Html(string='Notes')
|
||||
|
||||
@@ -285,6 +292,17 @@ class FpCertificate(models.Model):
|
||||
|
||||
@api.depends('thickness_reading_ids', 'thickness_reading_ids.nip_mils',
|
||||
'spec_min_mils', 'spec_max_mils')
|
||||
def _compute_x_fc_age_hours(self):
|
||||
"""Hours since cert creation. Non-stored, recomputed on read."""
|
||||
from datetime import datetime
|
||||
now = datetime.now()
|
||||
for rec in self:
|
||||
if not rec.create_date:
|
||||
rec.x_fc_age_hours = 0.0
|
||||
continue
|
||||
delta = now - rec.create_date
|
||||
rec.x_fc_age_hours = delta.total_seconds() / 3600.0
|
||||
|
||||
def _compute_reading_stats(self):
|
||||
for rec in self:
|
||||
readings = rec.thickness_reading_ids
|
||||
@@ -420,6 +438,38 @@ class FpCertificate(models.Model):
|
||||
|
||||
# ----- State actions ----------------------------------------------------
|
||||
def action_issue(self):
|
||||
# ===== ACL guard (spec 2026-05-25 §ACL changes) ===============
|
||||
# Only QM / Manager / Owner can issue certificates. Two-layer
|
||||
# enforcement; view-level groups= on the button is the other
|
||||
# layer. Manager bypass via context for cron / scripted issuance.
|
||||
if not self.env.context.get('fp_skip_cert_authority_gate'):
|
||||
cert_authority_gids = []
|
||||
for xmlid in (
|
||||
'fusion_plating.group_fp_quality_manager',
|
||||
'fusion_plating.group_fp_manager',
|
||||
'fusion_plating.group_fp_owner',
|
||||
):
|
||||
grp = self.env.ref(xmlid, raise_if_not_found=False)
|
||||
if grp:
|
||||
cert_authority_gids.append(grp.id)
|
||||
if cert_authority_gids and not (
|
||||
set(self.env.user.all_group_ids.ids)
|
||||
& set(cert_authority_gids)
|
||||
):
|
||||
from odoo.exceptions import AccessError
|
||||
raise AccessError(_(
|
||||
'Only Quality Managers, Managers, and Owners can '
|
||||
'issue certificates. Ask your QM to review and '
|
||||
'issue this CoC.'
|
||||
))
|
||||
else:
|
||||
from markupsafe import Markup
|
||||
for rec in self:
|
||||
rec.message_post(body=Markup(_(
|
||||
'Cert authority gate <b>bypassed</b> by '
|
||||
'<b>%(u)s</b> (context flag '
|
||||
'fp_skip_cert_authority_gate).'
|
||||
)) % {'u': self.env.user.name})
|
||||
for rec in self:
|
||||
if rec.state != 'draft':
|
||||
raise UserError(_('Only draft certificates can be issued.'))
|
||||
@@ -607,6 +657,39 @@ class FpCertificate(models.Model):
|
||||
rec.name, e,
|
||||
)
|
||||
rec.message_post(body=_('Certificate issued.'))
|
||||
# Post-issue: ask the job to check whether ALL required
|
||||
# certs are now issued. If so, the helper auto-advances to
|
||||
# awaiting_ship and resolves any open Issue-CoC activity.
|
||||
# Spec 2026-05-25.
|
||||
if ('x_fc_job_id' in rec._fields and rec.x_fc_job_id
|
||||
and hasattr(rec.x_fc_job_id,
|
||||
'_fp_check_advance_after_cert_issue')):
|
||||
rec.x_fc_job_id._fp_check_advance_after_cert_issue()
|
||||
|
||||
def write(self, vals):
|
||||
"""Override to detect cert voiding and trigger the job state
|
||||
regress (awaiting_ship → awaiting_cert). Spec 2026-05-25.
|
||||
|
||||
Captures which certs were `issued` BEFORE the write, so we know
|
||||
post-write whether a void actually downgraded a previously-
|
||||
issued cert. Calls the inverse-direction helper on the job.
|
||||
"""
|
||||
was_issued = {}
|
||||
if 'state' in vals and vals['state'] == 'voided':
|
||||
was_issued = {
|
||||
rec.id: (rec.state == 'issued')
|
||||
for rec in self
|
||||
}
|
||||
result = super().write(vals)
|
||||
if was_issued:
|
||||
for rec in self:
|
||||
if not was_issued.get(rec.id):
|
||||
continue # wasn't issued — no regress
|
||||
if ('x_fc_job_id' in rec._fields and rec.x_fc_job_id
|
||||
and hasattr(rec.x_fc_job_id,
|
||||
'_fp_check_regress_after_cert_void')):
|
||||
rec.x_fc_job_id._fp_check_regress_after_cert_void()
|
||||
return result
|
||||
|
||||
def _fp_sync_coc_to_delivery(self):
|
||||
"""Push this CoC's attachment onto its job's delivery so the
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
rule 13f.) -->
|
||||
<button name="action_issue" string="Issue"
|
||||
type="object" class="btn-primary"
|
||||
invisible="state != 'draft'"/>
|
||||
invisible="state != 'draft'"
|
||||
groups="fusion_plating.group_fp_quality_manager,fusion_plating.group_fp_manager,fusion_plating.group_fp_owner"/>
|
||||
<!-- Print = the same EN report action the gear-menu
|
||||
Print > Certificate of Conformance (English)
|
||||
calls. Routes through fusion_pdf_preview's
|
||||
|
||||
Reference in New Issue
Block a user