From e09913af5a382a6c3f0b160b17c1d4bd5dd8b491 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 27 May 2026 01:48:30 -0400 Subject: [PATCH] docs: spec for recipe-level cert suppression + aerospace cert-type parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../2026-05-27-recipe-cert-toggles-design.md | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 fusion_plating/docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md diff --git a/fusion_plating/docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md b/fusion_plating/docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md new file mode 100644 index 00000000..3775896b --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md @@ -0,0 +1,209 @@ +# 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: + +1. 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. +2. Close gaps on three orphan `fp.certificate.certificate_type` Selection 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_requirement` Selection — unchanged. +- `fp.certificate.certificate_type` Selection — 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`](../../fusion_plating_jobs/models/fp_job.py) at line 585. + +### New algorithm + +```python +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'` and `recipe.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 `` 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 `
` 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 + +1. **`_fp_render_and_attach_pdf` guard.** Currently always renders the CoC QWeb template. Add at the top: + ```python + if self.certificate_type != 'coc': + return # orphan types are manual-attach only + ``` + Makes intent explicit and prevents attempting to render a CoC template for a Nadcap cert. + +2. **`action_issue` precondition for orphan types.** Raise `UserError("Attach the supplier's PDF before issuing this certificate.")` when `certificate_type in ('nadcap_cert', 'mill_test', 'customer_specific')` AND `attachment_id` is empty. Better than a silent half-issued state. + +3. **Email-on-issue path unchanged.** The existing `Send to Customer` button reads `attachment_id` regardless 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: + +1. **`fusion_plating_certificates/migrations//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. + +2. **`fusion_plating/migrations//post-migrate.py`** — backfill all 5 new `fusion.plating.process.node` Booleans 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`: + +1. `test_recipe_suppresses_thickness` — partner+thickness ON, `recipe.requires_thickness_report=False` → result excludes thickness. +2. `test_recipe_suppresses_nadcap_for_commodity_part` — partner+nadcap ON, `recipe.requires_nadcap_cert=False` → no nadcap. +3. `test_recipe_cannot_add_certs_customer_didnt_want` — partner all OFF, recipe all True → empty set. +4. `test_part_override_coc_recipe_suppresses` — `part.certificate_requirement='coc'`, `recipe.requires_coc=False` → empty set. +5. `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 changes +- `fusion_plating` — recipe Booleans + recipe view changes + migration +- `fusion_plating_jobs` — resolver update + render guard + Issue precondition + test file + +### Smoke runbook on entech (post-deploy, manual) + +1. Flip `x_fc_send_nadcap_cert=True` on one test partner → confirm new toggle visible on form. +2. Open a passivation recipe → flip `requires_thickness_report=False` → save. +3. Create an SO + job for that customer/recipe → walk to job done → confirm spawned cert set excludes thickness even though customer toggle is ON. +4. 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).