Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-14-promote-customer-spec-design.md
gsinghpal c96f27b96c feat(promote-customer-spec): NADCAP recipe lock (Phase A+)
Per client review: NADCAP-qualified recipes need manager-only edit
permission. Word-doc external approval workflow stays outside ERP;
this is the in-app enforcement.

- New field fp.process.node.is_locked (recipe root)
- write() override blocks non-manager edits when recipe root is_locked
  Lock checks via recipe_root_id so child ops/steps are also protected
  Manager bypass via group + env.su (sudo) bypass for system jobs
- Amber "LOCKED — Manager Edit Only" ribbon at top of recipe form
- Toggle on Specification & Bake page under "Change Control (NADCAP)"
- Spec doc updated with Decision 6.5 + backlog from client review:
  approvals list, doc control auto-sync, oven recorder sync, SOP
  word-doc workflow, final-inspection signoff on cert

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:55:07 -04:00

33 KiB

Promote Customer Specification, Retire Coating Config — Design

Date: 2026-05-14 Author: brainstorming session with Gurpreet Status: Draft — pending user review Backup branch: backup/pre-spec-recipe-collapse-2026-05-14 (1414ef2 on origin + gitea)


TL;DR

fp.coating.config ("Primary Treatment") is a half-baked sketch that conflates a customer-facing specification with an internal production process. The codebase already contains a properly-modelled aerospace specification entity (fusion.plating.customer.spec) with revision tracking, document URLs, AS9100 / Nadcap / N299 extensions, and 15 seeded industry specs. This design proposes:

  1. Retire fp.coating.config and fp.treatment entirely — no archive, no commented blocks, no "obsolete" module.
  2. Promote fusion.plating.customer.spec to the primary specification entity, exposed on the SO line as a picker labelled "Specification".
  3. Move process parameters (thickness, bake-relief, phosphorus level) from coating config onto the Recipe model where they semantically belong.
  4. Keep the spec ↔ recipe separation — both are pickers on the SO line. They auto-fill from part defaults so order entry stays single-click for repeat work.

The result is an aerospace-correct two-picker design ("Spec the customer cited" + "Recipe we used") that handles all 7 aerospace audit scenarios cleanly while preserving fast order entry for ENPlating's repeat-customer workflow.


Problem statement

ENPlating is an aerospace/defence plating shop running Fusion Plating in development. The current SO line UX presents two pickers that look conceptually identical to the operator:

  • "Primary Treatment" (fp.coating.config) — process type, phos level, thickness range, spec reference, cert level, bake settings, default recipe pointer
  • "Process Variant" (fusion.plating.process.node recipe root) — the production tree

The client has stated:

  • "Recipe and Primary Treatment are the same thing" (correctly observing the conceptual overlap)
  • Speed of order entry is paramount (repeat customers, repeat parts, AI auto-fill on the roadmap)
  • Per-step variation ("skip the bake") is rare and already handled by fp.job.node.override

The client serves aerospace and defence primes (Boeing, Lockheed, Northrop, RTX, etc. — to be confirmed). Audit posture matters. Source approval letters, AS9100, Nadcap, FAI / AS9102, spec revision tracking — these are real concerns even if the daily operator doesn't think about them.


Background — the two parallel models

fp.coating.config ("Primary Treatment")

  • Module: fusion_plating_configurator
  • Origin: built by the configurator team as an order-entry convenience
  • Seeded data: zero records ship with the module
  • Fields: name, process_type_id (single), phosphorus_level, thickness_min/max/uom, thickness_option_ids, spec_reference (free Char), certification_level, requires_bake_relief, bake_window_hours, bake_temperature(_uom), bake_duration_hours, pre_treatment_ids, post_treatment_ids, recipe_id (default recipe pointer), description, sequence, active
  • Audit features: none (no chatter, no revision tracking, no unique constraint on spec ref, no customer linkage)
  • Used by: SO line picker, direct order wizard, portal customer self-service, fp.job, fp.delivery, account.move.line, fp.pricing.rule, fp.quality.point, fp.certificate (auto-fill), reports
  • Seeded coating records: 0

fusion.plating.customer.spec ("Customer Specification")

  • Module: fusion_plating_quality (with extensions in fusion_plating_aerospace + fusion_plating_nuclear)
  • Origin: built by the quality team as the auditable spec library
  • Seeded data: 11 aerospace specs + 4 nuclear specs ship with the module
  • Fields: code (e.g. AMS 2404), revision, effective_date, partner_id, process_type_ids (M2M), spec_type (industry/customer/internal), document_url, notes (Html), company_id, active, plus mail.thread + mail.activity.mixin
  • Aerospace extension fields: x_fc_is_aerospace, x_fc_as9100_clause_ids, x_fc_nadcap_required, x_fc_requires_first_article, x_fc_pri_file_code, x_fc_customer_approval_required
  • Nuclear extension fields: x_fc_is_nuclear, x_fc_n299_level_id, x_fc_nqa1_applicable, x_fc_extended_retention_years, x_fc_nuclear_customer_type
  • Audit features: mail.thread chatter, unique (code, revision, company_id) constraint, tracking on every key field
  • Used by: fp.job.customer_spec_id (parallel to coating_config_id, currently underused)
  • Seeded specs: AMS 2404, AMS 2700, AMS 2759, AMS QQ-P-416, ASTM B733, BAC 5709, MIL-A-8625, MIL-C-26074, MIL-DTL-13924, PRI AS7108, QQ-C-320, OPG SQAP, Bruce N299, AECL N299, Candu N299

Why both exist

Two different teams (configurator + quality) built parallel solutions to overlapping problems. Neither cleaned up after the other. The result is two records describing the same real-world artifact, with the better-architected one (Customer Spec) sitting unused in the daily flow while the weaker one (Coating Config) drives the UI.


Decisions

Decision 1 — Promote Customer Spec, retire Coating Config

fusion.plating.customer.spec becomes the primary specification entity. fp.coating.config is removed entirely (no archive, no obsolete flag, no commented blocks).

Rationale:

  • Customer Spec already has the audit infrastructure aerospace requires
  • Customer Spec already has aerospace + nuclear extension modules
  • Customer Spec already ships with real industry specs
  • The aerospace team has already invested in this model
  • Coating Config has 0 seed records, no audit trail, no customer linkage

Decision 2 — Two-picker SO line UX

The SO line carries two Many2one pickers:

Picker label Backing model Purpose
Specification fusion.plating.customer.spec What the customer cited on the PO
Recipe fusion.plating.process.node (root) How we make it

Both auto-fill from the part's defaults. For repeat customer + part combinations, order entry remains one click. For the rare phos swap (Mid → High) the estimator changes one dropdown.

Rationale:

  • Spec ≠ Process in aerospace doctrine — auditors expect the separation
  • Many-to-many in concept (one spec covers multiple recipes; one recipe satisfies multiple specs) — modelling them as one record breaks down
  • AI drawing detection populates one decision per field; both fields are independently AI-fillable
  • Existing seed data already shows the pattern: AMS 2404 already lists [ptype_en_lp, ptype_en_mp, ptype_en_hp] as applicable processes

Decision 3 — Move process parameters onto Recipe

Fields currently on Coating Config that describe the production process (not the customer requirement) move onto fusion.plating.process.node (recipe root):

Field From To
phosphorus_level fp.coating.config fusion.plating.process.node (recipe root)
thickness_min, thickness_max, thickness_uom fp.coating.config fusion.plating.process.node (recipe root)
thickness_option_ids fp.coating.config (fp.coating.thickness) re-parented as fp.recipe.thickness
requires_bake_relief fp.coating.config fusion.plating.process.node (recipe root)
bake_window_hours fp.coating.config fusion.plating.process.node (recipe root)
bake_temperature, bake_temperature_uom fp.coating.config fusion.plating.process.node (recipe root)
bake_duration_hours fp.coating.config fusion.plating.process.node (recipe root)
pre/post treatment lists fp.coating.config (M2M fp.treatment) already covered — recipe steps include pre/post operations

Rationale: these fields describe HOW we plate (the process), not WHAT we promised the customer (the spec).

Why bake-relief belongs on Recipe specifically — AMS 2759/9 says "bake if hardness ≥ HRC 31 AND hydrogen-embrittlement risk exists." The risk is determined by the plating chemistry (Mid-Phos = high HE risk; High-Phos = low; non-EN processes = none). The recipe author knows which chemistry their recipe uses and ticks requires_bake_relief once. The spec doesn't need to drive this — AMS 2759/9 is invoked universally when conditions are met. Bake-window auto-create logic on fp.job.button_mark_done reads from recipe.requires_bake_relief instead of coating.requires_bake_relief.

Decision 4 — Delete fp.treatment model entirely

fp.treatment (the small library of named pre/post operations: Bead Blast, Zincate, Bake, Passivate, etc.) is removed. Pre/post operations are already first-class steps in the recipe tree. The fp.step.template library (Sub 12a) plays the role of "approved step library" for shops that want one.

Decision 5 — Naming convention

Concept Label on screen Technical model
The customer-facing spec record "Specification" (formal, audit-friendly) fusion.plating.customer.spec (unchanged)
The same thing in tight UI spots (kanban chips, status bars) "Spec" (short form) (same)
The admin menu "Specifications" (renamed from "Customer Specs")
The production tree "Recipe" (unchanged) fusion.plating.process.node (unchanged)

Rejected alternatives:

  • "Plating Spec" — too narrow (won't cover anodize, masking, etc.)
  • "Coating Spec" — same problem; "coating" is the customer's word, not internal
  • "Process Spec" — collides verbally with "Process Recipe"
  • "Customer Spec" — fine but slightly off when the spec is a public industry standard
  • "Treatment" / "Coating Configuration" — what we're explicitly removing

Decision 6.5 — NADCAP recipe lock (added 2026-05-15 from client review)

After client validation of the design, ENPlating raised: "For NADCAP recipes, once it's in the system and we check it, only a manager profile should be able to change." Added to scope.

Implementation:

  • New field fusion.plating.process.node.is_locked Boolean (recipe root, but checked on all descendants via recipe_root_id)
  • write() override blocks modifications by non-manager users when the recipe root has is_locked=True
  • Manager bypass via env.user.has_group('fusion_plating.group_fusion_plating_manager') so the lock can be toggled off + edits made
  • env.su (sudo) also bypasses (for migrations / system jobs)
  • View: amber "LOCKED — Manager Edit Only" ribbon at top of recipe form when locked; is_locked toggle on the Specification & Bake page under "Change Control (NADCAP)" group

The Word-doc external approval workflow (REV 0, REV 1 in filenames on Engineering Drive) lives outside the ERP. The lock is the ERP-side enforcement point that prevents accidental in-app edits between approval cycles.

Decision 7 — Recipe ↔ Specification relationship

Many-to-many. One spec applies to multiple recipes; one recipe can satisfy multiple specs.

Implementation: add recipe_ids Many2many on fusion.plating.customer.spec, with reverse field on the recipe model.

# On fusion.plating.customer.spec
recipe_ids = fields.Many2many(
    'fusion.plating.process.node',
    'fp_customer_spec_recipe_rel',
    'spec_id',
    'recipe_id',
    domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]",
    string='Applicable Recipes',
)

The existing process_type_ids M2M on customer.spec stays — useful for spec-level filtering at the process-type level (e.g., "AMS 2404 covers EN-LP, EN-MP, EN-HP").


Detailed model changes

Models removed entirely

  • fp.coating.config — model definition, views, search, action, security rows
  • fp.treatment — model definition, views, search, action, security rows, seed data file

Models modified

fusion.plating.customer.spec (in fusion_plating_quality)

Add fields to support what was on Coating Config:

# Process parameter helpers (optional; recipe is source of truth)
recipe_ids = fields.Many2many(
    'fusion.plating.process.node',
    'fp_customer_spec_recipe_rel',
    'spec_id', 'recipe_id',
    domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]",
    string='Applicable Recipes',
    help='Recipes that can produce work to this specification. '
         'Many-to-many — one spec can cover multiple processes; '
         'one recipe can satisfy multiple specs.',
)

# Spec-level cert auto-fill helper (optional override of recipe's spec line)
print_on_cert = fields.Boolean(
    string='Print on Certificate',
    default=True,
    help='When enabled, this spec\'s code+revision appear on the CoC '
         'when the spec is selected on the SO line.',
)

(Aerospace + nuclear extensions are already in place — no changes to those modules' fields.)

fusion.plating.process.node (in fusion_plating)

Add the process parameter fields previously on Coating Config:

# Recipe-only fields (apply when node_type='recipe' and parent_id is False)
phosphorus_level = fields.Selection(
    [('low_phos', 'Low Phosphorus (2-5%)'),
     ('mid_phos', 'Mid Phosphorus (6-9%)'),
     ('high_phos', 'High Phosphorus (10-13%)'),
     ('na', 'N/A')],
    string='Phosphorus Level',
    default='na',
    help='EN-specific. Set to N/A for non-EN processes (chrome, anodize, '
         'black oxide).',
)
thickness_min = fields.Float(string='Min Thickness', digits=(10, 4))
thickness_max = fields.Float(string='Max Thickness', digits=(10, 4))
thickness_uom = fields.Selection(
    [('mils', 'mils'), ('microns', 'microns'), ('inches', 'inches')],
    string='Thickness UoM', default='mils',
)
thickness_option_ids = fields.One2many(
    'fp.recipe.thickness',
    'recipe_id',
    string='Thickness Options',
)

# Bake relief (AMS 2759/9 hydrogen embrittlement)
requires_bake_relief = fields.Boolean(
    string='Requires Bake Relief',
    help='Hydrogen embrittlement relief bake required (high-strength steel, '
         'Rockwell C ≥ 31). When set, finishing the job auto-creates a '
         'bake window record and blocks shipment until bake is complete.',
)
bake_window_hours = fields.Float(
    string='Bake Window (hours)', default=4.0,
    help='Maximum time between plate exit and bake start. Typical 4h per '
         'AMS 2759/9.',
)
bake_temperature = fields.Float(
    string='Bake Temperature', default=375.0,
    help='Relief bake temperature. Default 375 (°F per AMS 2759/9 for '
         'steel ≥ HRC 40).',
)
bake_temperature_uom = fields.Selection(
    [('F', '°F'), ('C', '°C')],
    string='Temp Unit',
    default=lambda self: self.env.company.x_fc_default_temp_uom or 'F',
)
bake_duration_hours = fields.Float(
    string='Bake Duration (hours)', default=23.0,
    help='Minimum bake hold time at temperature. Typical 23h.',
)

# Reverse of customer.spec.recipe_ids
applicable_spec_ids = fields.Many2many(
    'fusion.plating.customer.spec',
    'fp_customer_spec_recipe_rel',
    'recipe_id', 'spec_id',
    string='Applicable Specifications',
)

These fields render only when node_type='recipe' and parent_id=False (i.e. the recipe root). Use invisible="node_type != 'recipe' or parent_id" on the form view.

fp.coating.thicknessfp.recipe.thickness

Renamed model. M2O coating_config_id becomes recipe_id pointing at fusion.plating.process.node.

class FpRecipeThickness(models.Model):
    _name = 'fp.recipe.thickness'
    _description = 'Fusion Plating — Recipe Thickness Option'
    _order = 'recipe_id, sequence'

    recipe_id = fields.Many2one(
        'fusion.plating.process.node',
        string='Recipe',
        required=True,
        ondelete='cascade',
        domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]",
    )
    # ... (existing thickness/uom/sequence/label fields)

Models with field deletions

Model Field removed
sale.order.line x_fc_coating_config_id
sale.order.line x_fc_treatment_ids
fp.part.catalog x_fc_default_coating_config_id
fp.part.catalog x_fc_default_treatment_ids
account.move.line x_fc_coating_config_id
fp.job coating_config_id
fp.pricing.rule coating_config_id
fp.quality.point coating_config_ids
fp.direct.order.line coating_config_id
fp.direct.order.line treatment_ids

Models with field additions

Model Field added Purpose
sale.order.line x_fc_customer_spec_id Many2one Specification picker on SO line
fp.part.catalog x_fc_default_customer_spec_id Many2one Per-part default spec
account.move.line x_fc_customer_spec_id Many2one Carries spec to invoice for cert reference
fp.pricing.rule customer_spec_id Many2one Primary key (replaces coating_config_id)
fp.pricing.rule recipe_id Many2one Secondary key for recipe-only rules

Pricing rule lookup priority (most specific → least specific):

  1. customer_spec_id AND recipe_id both set → exact match
  2. customer_spec_id set, recipe_id blank → spec-tier rule (e.g. "all AS9100 work +15%")
  3. recipe_id set, customer_spec_id blank → recipe-tier rule (e.g. "EN Mid-Phos $X/sqft")
  4. Both blank → catch-all (customer/material defaults)

The pricing engine returns the FIRST match in this order. Composability: a single quote can stack a base recipe rate + a spec surcharge by configuring two rules. | fp.quality.point | customer_spec_ids Many2many | Re-keyed QC trigger filter | | fp.quality.point | recipe_ids Many2many | Recipe-level QC trigger filter | | fp.direct.order.line | customer_spec_id Many2one | Wizard picker |

Models unchanged (already use customer.spec)

  • fp.job.customer_spec_id — already exists, will become the primary spec link
  • All aerospace + nuclear extension fields on customer.spec — unchanged

UI / view changes

SO line view

Replace the Primary Treatment + Treatments + Process Variant block with:

┌────────────────────────────────────────────────────────────┐
│ Part:           ABC-001 Rev A          [picker]            │
│ Specification:  AMS 2404 Rev D         [picker]            │
│ Recipe:         EN Mid-Phos #4         [picker]            │
│ Thickness:      0.0005-0.0008 in       [picker — scoped to recipe] │
│ Qty:            50                                         │
└────────────────────────────────────────────────────────────┘

Both Specification and Recipe pickers use default_get to pre-fill from the chosen part's defaults. The thickness picker domain becomes [('recipe_id', '=', x_fc_process_variant_id)] (was coating_config_id).

Direct order wizard

Same change: replace coating + treatments fields with specification picker. Rename "Primary Treatment" label → "Specification".

Job form

Remove coating_config_id field. Surface customer_spec_id (already exists) as the primary spec link with proper widget styling. Add smart button "View Spec" that opens the spec record.

Reports

Report Change
report_fp_sale.xml Print line.x_fc_customer_spec_id.code + revision instead of x_fc_coating_config_id.name
report_fp_wo_sticker.xml Same
report_fp_job_traveller.xml Same
report_fp_job_sticker.xml Same
report_coc_en.xml / report_coc_fr.xml Cert reads spec.code Rev rev instead of coating.spec_reference
Cert auto-fill (_fp_create_certificates) Read from job.customer_spec_id instead of coating_config_id

Portal

fusion_plating_portal/controllers/portal_configurator.py — change "Pick a Coating" picker to "Pick a Specification". Customer-facing label may stay "Coating" if partner.x_fc_portal_label_preference says so (settings-driven; defer to client preference).

Menus

  • Remove "Coating Configurations" menu item under Configuration → Materials & Tanks (or wherever it lives today)
  • Promote "Customer Specifications" under Configuration → Quality & Documents → rename to "Specifications"
  • Optionally surface "Specifications" higher in the menu tree (e.g. as a top-level Configuration tab) since it's now a primary admin entity

Smart buttons

On res.partner (customer): add "Specifications" smart button → opens specs filtered to partner_id = self.

On fusion.plating.process.node (recipe root): add "Applicable Specifications" smart button → opens specs where recipe_ids includes this recipe.


Per-module impact summary

Module Impact Notes
fusion_plating High Add fields to fp.process.node + fp.recipe.thickness model + bake-relief logic refactor
fusion_plating_configurator Critical Delete fp.coating.config + fp.treatment + their views/data; refactor SO line + part catalog + direct order wizard + pricing
fusion_plating_quality Medium Add recipe_ids to customer.spec; add print_on_cert field; refactor quality.point trigger
fusion_plating_jobs Medium Remove coating_config_id from fp.job; sale_order.py spec resolution chain; pricing path
fusion_plating_certificates Medium Cert auto-fill reads from customer_spec_id
fusion_plating_reports Medium All 4 plating reports updated
fusion_plating_portal Medium Picker change, label decision
fusion_plating_shopfloor Low Tablet payload updated to read recipe + spec instead of coating
fusion_plating_logistics Low fp.delivery.x_fc_thickness_id — change M2O target from fp.coating.thickness to fp.recipe.thickness
fusion_plating_aerospace None Already extends customer.spec correctly
fusion_plating_nuclear None Already extends customer.spec correctly
fusion_plating_compliance* None Doesn't reference coating
Tank / bath / chemistry models None Keyed off process_type not coating
fusion_iot None No coating references
fusion_plating_bridge_maintenance None No coating references

Aerospace scenarios validated

The 7 aerospace scenarios from the brainstorming session all resolve cleanly:

  1. Same chemistry, different customer specs — multiple spec records (BAC 5680, LMS-3045, AMS 2404) all link to one Recipe. Cert prints the spec the customer cited; process freeze remains intact.
  2. Spec revision (BAC 5680 Rev D → Rev E) — separate spec records via (code, revision, company) unique constraint. Both can be active. Old POs reference old rev; new POs reference new rev. Recipe untouched.
  3. Nadcap process freeze — Recipe edits trigger the existing sign-off workflow. Spec edits don't touch the recipe. Two clean audit trails.
  4. Source Approval Letterpartner_id on customer.spec naturally surfaces "Boeing-approved specs" via filter.
  5. First Article Inspection (AS9102)x_fc_requires_first_article on customer.spec drives the gate. FAI tied to (part, spec) — matches AS9102 doctrine.
  6. Customer source inspection — Specifications menu becomes the customer-facing audit view.
  7. DPAS / ITAR / DFARS — Future extension on customer.spec via a new module (fusion_plating_compliance_export or similar).

For the mid-phos / high-phos same-customer scenario:

  • Both lines pick the same Specification (e.g. AMS 2404)
  • Each line picks a different Recipe (Mid-Phos vs High-Phos)
  • Bake settings travel with the recipe (Mid-Phos requires bake; High-Phos doesn't)
  • One spec record serves both orders; no duplication

Open questions for client

Before finalizing implementation, confirm with the client:

  1. Customer-facing terminology on the portal: when his customers self-service quote, do they think they're ordering a "coating" or a "specification"? Determines whether the portal picker label says "Coating" or "Specification" on the customer-facing page (internal label stays "Specification").
  2. Pricing rule complexity: how many active pricing rules does ENPlating use? Are surcharges keyed to spec (AS9100 = +15%) or to recipe (Mid-Phos = $X/sqft)? Determines whether fp.pricing.rule keeps both customer_spec_id and recipe_id keys or just one.
  3. Customer Source Approvals: does ENPlating have formal Source Approval Letters from any primes? If yes, names + counts so we can scope a "Customer Approval" tracking enhancement.
  4. Retire the menu under Materials & Tanks? "Coating Configurations" menu likely sits there today — confirm safe to remove (vs hide for one release).

These are nice-to-haves; the design proceeds without their answers but the answers refine UX details.


Backlog from client review (2026-05-15) — separate sub-projects

These surfaced from the client's scenario walkthrough but are NOT part of this refactor. Tracked here so they aren't forgotten.

  1. Customer Approvals List (Compliance → Aerospace → Approvals List menu) — small new model fp.customer.approval tracking which customer specs the shop is source-approved for, with approval letter PDF, effective date, expiry date. Filterable by prime (Boeing/Lockheed/etc.). Driven by client S4 answer: "Can we maintain a list of approvals under Compliance > Aerospace (AS9100/NADCAP) > APPROVALS LIST?"

  2. Document Control auto-sync — every customer-facing artifact (PO, packing slip, invoice, certificate, photos) auto-saves to a doc control folder (Engineering Drive / SharePoint / OneDrive). Major Documents-integration project. Driven by client S6: "I need the ERP to download all the files... to our DOC control folder."

  3. Oven recorder data sync — pull chart-recorder data from the bake oven into the ERP and attach to the relevant job. IoT / hardware-integration project, lives in fusion_iot family. Driven by client S6: "How can we sync the oven recorders with the ERP?"

  4. Recipe SOP Word-doc workflow polish — recipes already accept attachments via mail.thread. Add a prominent "Current Approved SOP" attachment slot on the recipe form, with revision history visible. Driven by client S3 + S6: "submit the steps in Word format to the customer for approval... First submission will be REV 0. If we make changes the file will be saved REV 1."

  5. Final inspection signoff captured on certificate — already partially exists (signoff workflow on jobs); ensure the "who did final inspection" name lands on the cert PDF body. Driven by client S7.

Out of scope (explicitly NOT doing)

  • Data migration of existing coating config records (per user direction: dev-stage, no historical data to preserve)
  • Backwards-compatibility shims (if 'coating_config_id' in self._fields guards) — clean removal
  • Archive / obsolete code patterns — clean deletion only
  • Resurrecting fp.treatment for any purpose
  • Adding a new fp.spec.library or fp.process.specification model — fusion.plating.customer.spec IS the spec model
  • Building a Source Approval Letter tracker (deferred — flag for future enhancement after client confirms the need)
  • Building DPAS / ITAR / DFARS export-control tracking (deferred — separate compliance extension module when needed)

Risk analysis

Risk Severity Mitigation
Bake-relief logic accidentally regresses (compliance-grade) High Smoke test: create a Mid-Phos job, verify bake_window auto-creates with correct temp/duration. Create a High-Phos job, verify NO bake_window
Cert auto-fill loses spec_reference High Smoke test: complete a job with Spec=AMS 2404, verify cert PDF prints "Plated to AMS 2404"
Pricing rules silently fail (no rule matches new keys) Medium Re-key existing rules to new model in same commit; add unit test that lookup returns expected price
Portal customer-facing flow breaks Medium Manual smoke test of portal quote flow before deploy
Recipe-thickness picker domain breaks (orphan thickness records) Low Drop fp.coating.thickness rows; recreate as fp.recipe.thickness during implementation
QC trigger filter (Sub 12 quality point) misses jobs Medium Test job creation triggers expected QC checks
Reports fail to render (missing field) Low Update all 4 plating reports in same commit; smoke test each PDF generation

Rollback strategy: if catastrophic, git reset --hard backup/pre-spec-recipe-collapse-2026-05-14. The backup branch is pushed to both GitHub and Gitea.


Implementation phases (high-level)

The detailed implementation plan goes into a separate writing-plans artifact. High-level phases:

Phase 1 — Recipe model fields + thickness rename

  • Add new fields to fusion.plating.process.node (recipe root)
  • Rename fp.coating.thicknessfp.recipe.thickness
  • Update views to surface new fields on recipe form
  • Bump module version

Phase 2 — Customer Spec enhancements

  • Add recipe_ids M2M to fusion.plating.customer.spec
  • Add print_on_cert Boolean
  • Update views (form, list, search) to surface recipe linkage
  • Bump module version

Phase 3 — SO line + wizard rewrite

  • Add x_fc_customer_spec_id to sale.order.line
  • Add x_fc_default_customer_spec_id to fp.part.catalog
  • Update SO line view + direct order wizard
  • Auto-fill logic from part defaults
  • Domain scoping (thickness depends on recipe)

Phase 4 — Pricing + quality point re-keying

  • Add customer_spec_id + recipe_id to fp.pricing.rule
  • Add customer_spec_ids + recipe_ids to fp.quality.point
  • Update rule lookup logic in pricing engine
  • Update quality.point trigger hooks

Phase 5 — Job + cert + reports refactor

  • Drop fp.job.coating_config_id
  • Update fp.certificate._fp_create_certificates to read from customer_spec_id
  • Update all 4 plating reports
  • Smart buttons on partner + recipe

Phase 6 — Portal updates

  • Change portal coating picker → specification picker
  • Update portal templates + JS
  • Test portal quote flow end-to-end

Phase 7 — Removal

  • Delete fp.coating.config model + view + data + ACL
  • Delete fp.treatment model + view + data + ACL
  • Delete x_fc_coating_config_id field on SO line, account.move.line, etc.
  • Remove menu item
  • Remove old data files from manifest
  • Bump module version
  • Final smoke test: full order entry → SO → job → tablet → QC → cert → invoice

Each phase is a separate commit (or small set of commits) for clear rollback.


Success criteria

The work is complete when:

  1. A new SO line on a brand-new DB shows ONLY two pickers (Specification + Recipe), each pre-fillable from part defaults
  2. The aerospace specs (AMS 2404, BAC 5709, MIL-C-26074, etc.) appear in the Specification dropdown out of the box
  3. Confirming an SO with a Mid-Phos recipe auto-creates a bake window; with a High-Phos recipe does not
  4. Issuing a CoC prints "Plated to {spec.code} Rev {spec.revision}" derived from the SO line's specification
  5. Pricing rule lookup returns a price based on the chosen spec + recipe combination
  6. Quality point auto-spawn fires on jobs matching its customer_spec_ids / recipe_ids filters
  7. Tank, bath, chemistry log, IoT, compliance, maintenance modules unchanged and unaffected
  8. No fp.coating.config or fp.treatment references remain anywhere in the active codebase (grep returns zero results)
  9. Reports (sale ack, WO sticker, job traveller, job sticker, CoC EN, CoC FR) all render correctly with spec + recipe data
  10. Portal customer self-service quote flow completes end-to-end with the new Specification picker

Appendix A — Field reconciliation table

What ends up where for every Coating Config field:

Coating Config field Disposition New location
name Removed (record itself removed)
process_type_id (single) Removed Already on customer.spec as M2M process_type_ids
recipe_id Removed Replaced by customer.spec.recipe_ids M2M
phosphorus_level Moved fusion.plating.process.node.phosphorus_level
thickness_min, thickness_max, thickness_uom Moved fusion.plating.process.node.thickness_*
thickness_option_ids Re-parented fp.recipe.thickness (was fp.coating.thickness)
spec_reference Removed Replaced by customer.spec.code + revision
certification_level Removed Replaced by customer.spec.spec_type + aerospace flags
pre_treatment_ids, post_treatment_ids Removed Already covered by recipe steps
requires_bake_relief Moved fusion.plating.process.node.requires_bake_relief
bake_window_hours Moved fusion.plating.process.node.bake_window_hours
bake_temperature(_uom) Moved fusion.plating.process.node.bake_temperature(_uom)
bake_duration_hours Moved fusion.plating.process.node.bake_duration_hours
description Removed Recipe already has description on the root node
sequence, active Removed (record itself removed)
currency_id, default_cost Removed Pricing logic moves to fp.pricing.rule with new keys

Every field accounted for. Nothing dropped silently.