fix(certificates): honour recipe thickness suppression at cert issue

The recipe-cert-toggles feature (fb6cccc8) taught
fp.job._resolve_required_cert_types to suppress thickness for recipes
with requires_thickness_report=False (passivation, chemical conversion,
anodize seal-only). But the actual thickness-data ENFORCEMENT never got
the memo: both fp.certificate.action_issue's hard gate AND the
Issue-Certs wizard's readiness hint re-derived 'needs thickness' from
partner flags only and ignored the recipe. Result: a passivation CoC for
a thickness/strict customer could never be issued — the gate demanded
Fischerscope data the process physically cannot produce.

Consolidate the partner-flag + recipe-suppression logic into one
fp.certificate._fp_needs_thickness_data() helper and route both the gate
and the wizard through it, so the cert-type resolver and the issue-time
gate can never drift again. Add regression tests: passivation recipe
suppresses the issue gate even for strict-thickness customers; a normal
recipe still enforces (control, guards aerospace).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 10:43:05 -04:00
parent 6728197570
commit 489312365e
5 changed files with 108 additions and 33 deletions

View File

@@ -436,6 +436,47 @@ class FpCertificate(models.Model):
rec.invalidate_recordset(['name'])
return records
def _fp_needs_thickness_data(self):
"""True when this cert MUST carry thickness data to be issued.
Single source of truth shared by action_issue (hard gate) and the
Issue-Certs wizard (readiness hint) so the two can never drift.
Partner side — the ceiling: a CoC needs thickness when the customer
is strict-thickness (aerospace / Nadcap) or opts into the
thickness-on-CoC bundle; a thickness_report cert always needs it.
Recipe side — suppress-only: a recipe whose requires_thickness_report
is False (passivation, chemical conversion, anodize seal-only — no
plating thickness physically exists) REMOVES the requirement even
when the customer asked. This mirrors Step 2 of
fp.job._resolve_required_cert_types so the cert-type resolver and
this issue-time gate agree. Without it, a passivation CoC for a
thickness customer can never be issued (the gate demands Fischerscope
data the process cannot produce). Field-existence guards keep this
safe when fusion_plating_jobs / fusion_plating are at an older
schema or not installed.
"""
self.ensure_one()
partner = self.partner_id
needs = (
self.certificate_type == 'thickness_report'
or (self.certificate_type == 'coc' and partner and (
('x_fc_strict_thickness_required' in partner._fields
and partner.x_fc_strict_thickness_required)
or ('x_fc_send_thickness_report' in partner._fields
and partner.x_fc_send_thickness_report)
))
)
if not needs:
return False
job = self.x_fc_job_id if 'x_fc_job_id' in self._fields else False
recipe = job.recipe_id if job else False
if (recipe and 'requires_thickness_report' in recipe._fields
and not recipe.requires_thickness_report):
return False
return True
# ----- State actions ----------------------------------------------------
def action_issue(self):
# ===== ACL guard (spec 2026-05-25 §ACL changes) ===============
@@ -580,30 +621,18 @@ class FpCertificate(models.Model):
'type': type_label,
'name': rec.name or rec.display_name,
})
# Thickness data requirement — unified gate covering both
# cert types. A customer needs thickness data on the cert
# when ANY of these is true:
# 1. cert type is thickness_report (the cert IS the data)
# 2. partner.x_fc_strict_thickness_required (aerospace /
# Nadcap — always strict)
# 3. partner.x_fc_send_thickness_report (the bundling
# rule — CoC carries thickness as page 2 by default
# for these customers; see CLAUDE.md "CoC + thickness
# = ONE cert (page 2 merge)")
# Acceptable data: logged readings on the cert OR a
# Fischerscope PDF on the linked QC OR a cert-local
# Fischerscope upload. Any one is enough.
# Thickness data requirement — _fp_needs_thickness_data is the
# single source of truth (shared with the Issue-Certs wizard).
# A customer needs thickness data when the cert is a
# thickness_report, or it's a CoC and the partner is
# strict-thickness / opts into the thickness-on-CoC bundle
# UNLESS the job's recipe suppresses thickness
# (requires_thickness_report=False: passivation, chemical
# conversion, anodize seal-only — no plating thickness exists).
# Acceptable data: logged readings on the cert OR a Fischerscope
# PDF on the linked QC OR a cert-local Fischerscope upload.
partner = rec.partner_id
needs_thickness = (
rec.certificate_type == 'thickness_report'
or (rec.certificate_type == 'coc' and partner and (
('x_fc_strict_thickness_required' in partner._fields
and partner.x_fc_strict_thickness_required)
or ('x_fc_send_thickness_report' in partner._fields
and partner.x_fc_send_thickness_report)
))
)
if needs_thickness:
if rec._fp_needs_thickness_data():
has_readings = bool(rec.thickness_reading_ids)
has_qc_fischer_pdf = bool(
rec.x_fc_thickness_pdf_id