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

12 KiB
Raw Blame History

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 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' 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:

    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.pyUPDATE 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_suppressespart.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).