docs: spec for recipe-level cert suppression + aerospace cert-type parity

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>
This commit is contained in:
gsinghpal
2026-05-27 01:48:30 -04:00
parent 416daa36d2
commit e09913af5a

View File

@@ -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 `<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
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/<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.
2. **`fusion_plating/migrations/<version>/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).