Adds recipe-level Boolean toggles (requires_coc / requires_thickness_report / requires_nadcap_cert / requires_mill_test / requires_customer_specific, default True) so a recipe can suppress certs the customer requested when the recipe physically never produces them (passivation = no thickness, commodity ENP = no nadcap). Closes gaps on three orphan fp.certificate.certificate_type values (Nadcap, Mill Test, Customer Specific) — adds partner toggles (x_fc_send_nadcap_cert / x_fc_send_mill_test / x_fc_send_customer_specific, default False), wires them through _resolve_required_cert_types, and sets up manual-attach Issue flow (no QWeb auto-render for orphan types). Brainstorming Q&A locked: recipe SUPPRESSES only, partner+recipe scope (part-level unchanged), 5 booleans default True, manual PDF attach for orphans. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Recipe-Level Cert Suppression + Aerospace Cert-Type Parity — Design Spec
Date: 2026-05-27
Status: Design approved, ready for implementation plan
Modules touched: fusion_plating, fusion_plating_certificates, fusion_plating_jobs
Goal
Two related changes:
- Let a recipe author flip OFF cert types the recipe physically can never produce (e.g. passivation = no thickness report, commodity ENP = no Nadcap), so a job with that recipe stops auto-spawning those certs even when the customer profile asks for them.
- Close gaps on three orphan
fp.certificate.certificate_typeSelection values (Nadcap, Mill Test Report, Customer Specific) — none of them have partner toggles, none auto-spawn today. Wire them end-to-end via the same partner / recipe / resolver / auto-spawn path that CoC and Thickness use.
Locked decisions
| Q | Decision |
|---|---|
| Q1 — Precedence | Recipe SUPPRESSES only. Customer/part is the ceiling; recipe can remove from the set, never add. |
| Q2 — Audit scope | Close gaps for the 3 existing orphan cert types (Nadcap, Mill Test, Customer Specific). No new cert types added. |
| Q3 — Recipe field shape | Five requires_* Booleans on fusion.plating.process.node, default True. Matches the existing requires_signoff / requires_rack_assignment naming pattern. |
| Q4 — Part-level expansion | Leave fp.part.catalog.certificate_requirement exactly as-is. Partner + recipe is enough; per-part Nadcap override deferred until a real need shows up. |
| Q5 — PDF rendering for orphans | Manual attachment. Auto-spawn creates draft fp.certificate row, operator uploads the supplier-/regulator-issued PDF, clicks Issue. No new QWeb templates. |
Section 1 — Data model changes
On res.partner (fusion_plating_certificates/models/res_partner.py)
Three new Booleans, default False (opt-in for aerospace/defence customers):
| Field | Label | Default |
|---|---|---|
x_fc_send_nadcap_cert |
Send Nadcap Certificate | False |
x_fc_send_mill_test |
Send Mill Test Report (MTR) | False |
x_fc_send_customer_specific |
Send Customer-Specific Cert | False |
Grouped under an "Aerospace / Defence" sub-heading inside the existing "Cert + Document Routing" block so commercial-customer profiles stay scannable.
On fusion.plating.process.node (fusion_plating/models/fp_process_node.py)
Five new Booleans, default True:
| Field | Label | Default |
|---|---|---|
requires_coc |
Requires CoC | True |
requires_thickness_report |
Requires Thickness Report | True |
requires_nadcap_cert |
Requires Nadcap Certificate | True |
requires_mill_test |
Requires Mill Test Report | True |
requires_customer_specific |
Requires Customer-Specific Cert | True |
Default True = existing recipes produce the same cert set they produce today. Recipe author flips OFF only when a recipe physically never produces that cert (passivation, commodity ENP, etc.).
What does NOT change
fp.part.catalog.certificate_requirementSelection — unchanged.fp.certificate.certificate_typeSelection — unchanged (the 3 orphan values are already there).- No new cert types, no new tables, no new ACL rules.
Section 2 — Resolver logic update
Single update point: fp.job._resolve_required_cert_types at line 585.
New algorithm
def _resolve_required_cert_types(self):
self.ensure_one()
# Step 1 — Start from partner + part (today's logic, extended for 3 new types)
req = (self.part_catalog_id
and self.part_catalog_id.certificate_requirement) or 'inherit'
if req == 'inherit':
wanted = set()
p = self.partner_id
if p.x_fc_send_coc: wanted.add('coc')
if p.x_fc_send_thickness_report: wanted.add('thickness_report')
if p.x_fc_send_nadcap_cert: wanted.add('nadcap_cert')
if p.x_fc_send_mill_test: wanted.add('mill_test')
if p.x_fc_send_customer_specific: wanted.add('customer_specific')
else:
wanted = {
'none': set(),
'coc': {'coc'},
'coc_thickness': {'coc', 'thickness_report'},
}.get(req, {'coc'})
# Step 2 — Apply recipe suppression (recipe can only remove)
recipe = self.recipe_id
if recipe:
if not recipe.requires_coc: wanted.discard('coc')
if not recipe.requires_thickness_report: wanted.discard('thickness_report')
if not recipe.requires_nadcap_cert: wanted.discard('nadcap_cert')
if not recipe.requires_mill_test: wanted.discard('mill_test')
if not recipe.requires_customer_specific: wanted.discard('customer_specific')
# Step 3 — Bundling rule preserved: thickness merges into CoC PDF
if 'coc' in wanted and 'thickness_report' in wanted:
wanted.discard('thickness_report')
return wanted
Behavioural invariants
- Existing customers see zero behaviour change at deploy time. All 3 new partner Booleans default False; all 5 new recipe Booleans default True; resolver result is bit-identical to today's output for every existing job until an author flips a toggle.
- Bundling rule for CoC + Thickness is preserved end to end. The Fischerscope-as-page-2 merge keeps working.
- Recipe can suppress part-level overrides too — if
part.certificate_requirement='coc'andrecipe.requires_coc=False, result is empty set. Same rule everywhere: recipe wins on suppression.
Section 3 — UI changes
Partner form (fusion_plating_certificates/views/res_partner_views.xml)
Three new toggles in the existing "Cert + Document Routing" group, rendered inside a child <group> with string="Aerospace / Defence" directly below the existing CoC / Thickness fields. All three use widget="boolean_toggle" for consistency with the surrounding fields.
Recipe form (fusion_plating/views/fp_process_node_views.xml)
New "Certificate Output" group, visible only when node_type == 'recipe'. Five toggles + an info banner explaining the precedence:
ℹ A recipe can only SUPPRESS certs the customer requested. Turn a toggle OFF for recipes that physically never produce that cert (e.g. passivation = thickness off, commodity ENP = nadcap off).
All five use widget="boolean_toggle".
Cert form (fusion_plating_certificates/views/fp_certificate_views.xml)
No structural changes. One UX nudge added: a <div class="alert alert-warning"> block immediately above the attachment_id field, with invisible="certificate_type not in ('nadcap_cert', 'mill_test', 'customer_specific') or attachment_id". Banner text: "This certificate type expects a PDF you upload from disk (supplier doc / regulator-issued cert). Auto-rendering is not provided."
No menu / action / smart-button changes.
Section 4 — Auto-spawn behaviour for orphan types
What works out of the box
fp.job._fp_create_certificates iterates the set returned by _resolve_required_cert_types. Adding nadcap_cert / mill_test / customer_specific to the set means the existing code path creates one draft fp.certificate row per type — no per-type branching needed. Idempotency check (('certificate_type', '=', t) filter on existing certs) prevents dupes on re-run.
Three small adjustments
-
_fp_render_and_attach_pdfguard. Currently always renders the CoC QWeb template. Add at the top:if self.certificate_type != 'coc': return # orphan types are manual-attach onlyMakes intent explicit and prevents attempting to render a CoC template for a Nadcap cert.
-
action_issueprecondition for orphan types. RaiseUserError("Attach the supplier's PDF before issuing this certificate.")whencertificate_type in ('nadcap_cert', 'mill_test', 'customer_specific')ANDattachment_idis empty. Better than a silent half-issued state. -
Email-on-issue path unchanged. The existing
Send to Customerbutton readsattachment_idregardless of cert_type; mail template labels read from the Selection field naturally.
Manager dashboard
The cert-pending tile already filters draft certs by _resolve_required_cert_types. The 3 new types appear in the same "Draft Certs" tile alongside CoC certs — operator drills in, sees the yellow banner, uploads PDF, clicks Issue.
Section 5 — Migration, edge cases, testing
Migration
Two post-migrate.py scripts, both idempotent:
-
fusion_plating_certificates/migrations/<version>/post-migrate.py—UPDATE res_partner SET x_fc_send_nadcap_cert = FALSE WHERE x_fc_send_nadcap_cert IS NULL(same for the other 2). Existing partners get explicit False values so the resolver branches predictably. -
fusion_plating/migrations/<version>/post-migrate.py— backfill all 5 newfusion.plating.process.nodeBooleans to TRUE on every existing row. Default True = inherit current behaviour for every existing recipe.
No data migration on existing fp.certificate rows — the 3 orphan types just become reachable.
Edge cases
| Scenario | Resolver behaviour |
|---|---|
| Customer wants thickness, passivation recipe (no thickness) | {coc, thickness} → recipe strips thickness → {coc} |
| Customer wants Nadcap, commodity recipe (no nadcap) | {coc, nadcap} → strip nadcap → {coc} |
| Customer wants nothing, recipe requires everything | {} → recipe can't add → {} (suppress-only rule) |
part.certificate_requirement='none' |
early-exit → {} regardless of partner/recipe |
part.certificate_requirement='coc', recipe says no coc |
{coc} → recipe strips coc → {} |
| Job has no recipe assigned | resolver skips Step 2; partner+part rules apply |
| Operator forgets to attach PDF on Nadcap cert | Issue raises UserError("Attach the supplier's PDF…") |
| Job re-runs Issue after attach | idempotent — already-issued certs skipped |
Manager-bypass
Existing fp_skip_cert_gate=True context flag keeps working — it gates the milestone advance, not the resolver itself. No new bypass needed.
Tests
Five test cases in fusion_plating_jobs/tests/test_recipe_cert_suppression.py:
test_recipe_suppresses_thickness— partner+thickness ON,recipe.requires_thickness_report=False→ result excludes thickness.test_recipe_suppresses_nadcap_for_commodity_part— partner+nadcap ON,recipe.requires_nadcap_cert=False→ no nadcap.test_recipe_cannot_add_certs_customer_didnt_want— partner all OFF, recipe all True → empty set.test_part_override_coc_recipe_suppresses—part.certificate_requirement='coc',recipe.requires_coc=False→ empty set.test_orphan_cert_issue_blocks_without_attachment— spawn Nadcap cert, click Issue with no attachment →UserError.
Module version bumps
fusion_plating_certificates— partner toggles + cert.action_issue guard + view changesfusion_plating— recipe Booleans + recipe view changes + migrationfusion_plating_jobs— resolver update + render guard + Issue precondition + test file
Smoke runbook on entech (post-deploy, manual)
- Flip
x_fc_send_nadcap_cert=Trueon one test partner → confirm new toggle visible on form. - Open a passivation recipe → flip
requires_thickness_report=False→ save. - Create an SO + job for that customer/recipe → walk to job done → confirm spawned cert set excludes thickness even though customer toggle is ON.
- Manually create a Nadcap cert → confirm yellow banner appears → click Issue without attachment → confirm
UserError→ attach PDF → click Issue → confirm cert finalized + emails customer.
Out of scope (deferred)
- Adding new cert types (FAIR / DFARS / CMRT / RoHS) — call this out as a future sub if a customer asks.
- Part-level granularity for the 3 orphan types (Q4 = no).
- Auto-rendering QWeb templates for orphan types (Q5 = manual attach).
- Configurable cert-type master table (Q2 option C — rejected as scope creep).
- Per-customer doc library for static Nadcap PDFs (Q5 option C — deferred).