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:
@@ -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).
|
||||
Reference in New Issue
Block a user