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>
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:
- Retire
fp.coating.configandfp.treatmententirely — no archive, no commented blocks, no "obsolete" module. - Promote
fusion.plating.customer.specto the primary specification entity, exposed on the SO line as a picker labelled "Specification". - Move process parameters (thickness, bake-relief, phosphorus level) from coating config onto the Recipe model where they semantically belong.
- 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.noderecipe 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 infusion_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_lockedBoolean (recipe root, but checked on all descendants viarecipe_root_id) write()override blocks modifications by non-manager users when the recipe root hasis_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_lockedtoggle 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 rowsfp.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.thickness → fp.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):
customer_spec_idANDrecipe_idboth set → exact matchcustomer_spec_idset,recipe_idblank → spec-tier rule (e.g. "all AS9100 work +15%")recipe_idset,customer_spec_idblank → recipe-tier rule (e.g. "EN Mid-Phos $X/sqft")- 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:
- 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.
- 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. - Nadcap process freeze — Recipe edits trigger the existing sign-off workflow. Spec edits don't touch the recipe. Two clean audit trails.
- Source Approval Letter —
partner_idon customer.spec naturally surfaces "Boeing-approved specs" via filter. - First Article Inspection (AS9102) —
x_fc_requires_first_articleon customer.spec drives the gate. FAI tied to (part, spec) — matches AS9102 doctrine. - Customer source inspection — Specifications menu becomes the customer-facing audit view.
- DPAS / ITAR / DFARS — Future extension on customer.spec via a new module (
fusion_plating_compliance_exportor 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:
- 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").
- 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.rulekeeps bothcustomer_spec_idandrecipe_idkeys or just one. - 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.
- 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.
-
Customer Approvals List (Compliance → Aerospace → Approvals List menu) — small new model
fp.customer.approvaltracking 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?" -
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."
-
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_iotfamily. Driven by client S6: "How can we sync the oven recorders with the ERP?" -
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." -
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._fieldsguards) — clean removal - Archive / obsolete code patterns — clean deletion only
- Resurrecting
fp.treatmentfor any purpose - Adding a new
fp.spec.libraryorfp.process.specificationmodel —fusion.plating.customer.specIS 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.thickness→fp.recipe.thickness - Update views to surface new fields on recipe form
- Bump module version
Phase 2 — Customer Spec enhancements
- Add
recipe_idsM2M tofusion.plating.customer.spec - Add
print_on_certBoolean - Update views (form, list, search) to surface recipe linkage
- Bump module version
Phase 3 — SO line + wizard rewrite
- Add
x_fc_customer_spec_idtosale.order.line - Add
x_fc_default_customer_spec_idtofp.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_idtofp.pricing.rule - Add
customer_spec_ids+recipe_idstofp.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_certificatesto read fromcustomer_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.configmodel + view + data + ACL - Delete
fp.treatmentmodel + view + data + ACL - Delete
x_fc_coating_config_idfield 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:
- ✅ A new SO line on a brand-new DB shows ONLY two pickers (Specification + Recipe), each pre-fillable from part defaults
- ✅ The aerospace specs (AMS 2404, BAC 5709, MIL-C-26074, etc.) appear in the Specification dropdown out of the box
- ✅ Confirming an SO with a Mid-Phos recipe auto-creates a bake window; with a High-Phos recipe does not
- ✅ Issuing a CoC prints "Plated to {spec.code} Rev {spec.revision}" derived from the SO line's specification
- ✅ Pricing rule lookup returns a price based on the chosen spec + recipe combination
- ✅ Quality point auto-spawn fires on jobs matching its
customer_spec_ids/recipe_idsfilters - ✅ Tank, bath, chemistry log, IoT, compliance, maintenance modules unchanged and unaffected
- ✅ No
fp.coating.configorfp.treatmentreferences remain anywhere in the active codebase (grep returns zero results) - ✅ Reports (sale ack, WO sticker, job traveller, job sticker, CoC EN, CoC FR) all render correctly with spec + recipe data
- ✅ 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.