Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md
gsinghpal e09913af5a 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>
2026-05-27 01:48:30 -04:00

210 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).