# Promote Customer Specification — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Retire `fp.coating.config` + `fp.treatment` entirely, promote `fusion.plating.customer.spec` to the primary specification entity, and add a two-picker SO line UX (Specification + Recipe). **Architecture:** Phased migration in 5 stages — additive first (new fields/models), then re-key consumers (SO line, pricing, quality, job, cert), then re-key views/reports/portal/tablet, then final destructive removal. Each phase ends in a clean module install + smoke test + commit. No historical data to migrate (dev stage). No back-compat shims. **Tech Stack:** Odoo 19, Python, OWL (frontend), SCSS, QWeb (reports), Docker (local dev), entech LXC 111 (deployment target). **Spec:** [docs/superpowers/specs/2026-05-14-promote-customer-spec-design.md](../specs/2026-05-14-promote-customer-spec-design.md) **Backup:** `backup/pre-spec-recipe-collapse-2026-05-14` on origin + gitea (commit `1414ef2`). --- ## File Structure Map | Phase | Files Created | Files Modified | Files Deleted | |---|---|---|---| | **A** Foundation | `fusion_plating/models/fp_recipe_thickness.py`, `fusion_plating/views/fp_recipe_thickness_views.xml` | `fusion_plating/models/fp_process_node.py`, `fusion_plating/views/fp_process_node_views.xml`, `fusion_plating/models/__init__.py`, `fusion_plating/__manifest__.py`, `fusion_plating/security/ir.model.access.csv`, `fusion_plating_quality/models/fp_customer_spec.py`, `fusion_plating_quality/views/fp_customer_spec_views.xml`, `fusion_plating_quality/__manifest__.py` | — | | **B** SO Line UX | — | `fusion_plating_configurator/models/sale_order_line.py`, `fusion_plating_configurator/models/account_move_line.py`, `fusion_plating_configurator/models/fp_part_catalog.py`, `fusion_plating_configurator/wizard/fp_direct_order_line.py`, `fusion_plating_configurator/views/sale_order_views.xml`, `fusion_plating_configurator/views/fp_part_catalog_views.xml`, `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml`, `fusion_plating_configurator/__manifest__.py` | — | | **C** Pricing/Quality/Job/Cert | — | `fusion_plating_configurator/models/fp_pricing_rule.py`, `fusion_plating_configurator/views/fp_pricing_rule_views.xml`, `fusion_plating_quality/models/fp_quality_point.py`, `fusion_plating_quality/models/fp_quality_point_hooks.py`, `fusion_plating_quality/views/fp_quality_point_views.xml`, `fusion_plating_jobs/models/fp_job.py`, `fusion_plating_jobs/models/sale_order.py`, `fusion_plating_jobs/views/fp_job_form_inherit.xml`, `fusion_plating_certificates/models/fp_certificate.py`, manifests | — | | **D** Reports/Tablet/Portal | — | `fusion_plating_reports/report/report_fp_sale.xml`, `fusion_plating_reports/report/report_fp_wo_sticker.xml`, `fusion_plating_reports/report/report_fp_job_traveller.xml`, `fusion_plating_jobs/report/report_fp_job_traveller.xml`, `fusion_plating_jobs/report/report_fp_job_sticker.xml`, CoC EN/FR templates, `fusion_plating_shopfloor/controllers/shopfloor_controller.py`, `fusion_plating_portal/controllers/portal_configurator.py`, `fusion_plating_portal/views/fp_portal_configurator_templates.xml`, manifests | — | | **E** Removal | — | `fusion_plating_configurator/models/__init__.py`, `fusion_plating_configurator/__manifest__.py`, `fusion_plating_configurator/security/ir.model.access.csv`, `fusion_plating_configurator/views/fp_configurator_menu.xml`, `fusion_plating/models/fp_job.py` (drop coating field), `fusion_plating_logistics/models/fp_delivery.py` (re-point thickness FK) | `fusion_plating_configurator/models/fp_coating_config.py`, `fusion_plating_configurator/models/fp_coating_thickness.py`, `fusion_plating_configurator/models/fp_treatment.py`, `fusion_plating_configurator/views/fp_coating_config_views.xml`, `fusion_plating_configurator/views/fp_coating_thickness_views.xml`, `fusion_plating_configurator/views/fp_treatment_views.xml`, `fusion_plating_configurator/data/fp_treatment_data.xml` | --- ## Pre-Work (one-time) ### Task 0: Verify clean working tree + create feature branch **Files:** N/A (git operations only) - [ ] **Step 1: Verify working tree is clean** Run: `cd /Users/gurpreet/Github/Odoo-Modules && git status` Expected: `On branch main` + `nothing to commit, working tree clean` If dirty, stop and commit / stash uncommitted work before proceeding. - [ ] **Step 2: Verify backup branch exists** Run: `git branch -a | grep backup/pre-spec-recipe-collapse-2026-05-14` Expected: 3 lines — local + origin + gitea entries - [ ] **Step 3: Create feature branch off main** Run: ```bash git checkout -b feature/promote-customer-spec git push -u origin feature/promote-customer-spec git push -u gitea feature/promote-customer-spec ``` Expected: branch created locally + pushed to both remotes. --- ## Phase A — Recipe + Spec Foundation Goal: Add the new fields/models that ENABLE the new design. Nothing existing breaks; nothing existing is removed yet. Pure additive change. ### Task A1: Create `fp.recipe.thickness` model **Files:** - Create: `fusion_plating/models/fp_recipe_thickness.py` - [ ] **Step 1: Create the new model file** Write to `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/models/fp_recipe_thickness.py`: ```python # -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. from odoo import _, api, fields, models class FpRecipeThickness(models.Model): """Discrete thickness option offered for a recipe. Replaces fp.coating.thickness. The thickness picker on the SO line is scoped to the chosen recipe, so the operator only sees values that match what the recipe actually produces. """ _name = 'fp.recipe.thickness' _description = 'Fusion Plating — Recipe Thickness Option' _order = 'recipe_id, sequence, value' recipe_id = fields.Many2one( 'fusion.plating.process.node', string='Recipe', required=True, ondelete='cascade', domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", ) sequence = fields.Integer(default=10) value = fields.Float( string='Thickness', required=True, digits=(10, 4), ) uom = fields.Selection( [('mils', 'mils'), ('microns', 'microns'), ('inches', 'inches')], string='UoM', required=True, default='mils', ) label = fields.Char( string='Display Label', compute='_compute_label', store=True, help='Auto-formatted "0.0005 mils" string for the picker dropdown.', ) note = fields.Char(string='Note') active = fields.Boolean(default=True) @api.depends('value', 'uom') def _compute_label(self): for rec in self: rec.label = f'{rec.value:g} {rec.uom}' if rec.value else '' def name_get(self): return [(rec.id, rec.label or '?') for rec in self] ``` - [ ] **Step 2: Register the model in `__init__.py`** Add `from . import fp_recipe_thickness` to `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/models/__init__.py`. Run: `grep -n "fp_recipe_thickness\|fp_process_node" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/models/__init__.py` Expected: both lines present. If not present, add the import line right after the existing `fp_process_node` import (preserve alphabetical order if used). ### Task A2: Add view + ACL for `fp.recipe.thickness` **Files:** - Create: `fusion_plating/views/fp_recipe_thickness_views.xml` - Modify: `fusion_plating/security/ir.model.access.csv` - Modify: `fusion_plating/__manifest__.py` - [ ] **Step 1: Create view file** Write to `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/views/fp_recipe_thickness_views.xml`: ```xml fp.recipe.thickness.list fp.recipe.thickness ``` - [ ] **Step 2: Add ACL rows** Edit `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/security/ir.model.access.csv` — append at the end: ``` access_fp_recipe_thickness_user,fp.recipe.thickness.user,model_fp_recipe_thickness,base.group_user,1,0,0,0 access_fp_recipe_thickness_supervisor,fp.recipe.thickness.supervisor,model_fp_recipe_thickness,fusion_plating.group_fusion_plating_supervisor,1,1,1,0 access_fp_recipe_thickness_manager,fp.recipe.thickness.manager,model_fp_recipe_thickness,fusion_plating.group_fusion_plating_manager,1,1,1,1 ``` - [ ] **Step 3: Register view in manifest** Edit `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/__manifest__.py` `'data'` list — add `'views/fp_recipe_thickness_views.xml'` near other view files (after `fp_process_node_views.xml` if present). ### Task A3: Add fields to `fusion.plating.process.node` **Files:** - Modify: `fusion_plating/models/fp_process_node.py` - [ ] **Step 1: Add new fields to the model** Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/models/fp_process_node.py`. Find the section comment `# ---- Recipe-only fields (apply when node_type='recipe') -----------------` (around line 304) and add the following block right after it: ```python # ---- Spec-derived metadata (recipe-root only — Promote Customer Spec) ---- 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). Drives certificate annotation and hydrogen-' 'embrittlement risk assessment for bake-relief.', ) 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', help='Discrete thickness values offered to the estimator on the ' 'order line for jobs running this recipe.', ) # ---- Bake relief — AMS 2759/9 hydrogen embrittlement (recipe root) ---- requires_bake_relief = fields.Boolean( string='Requires Bake Relief', help='Hydrogen embrittlement relief bake required (high-strength ' 'steel ≥ HRC 31 in conjunction with this chemistry). 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='Bake Temp Unit', default='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 (lazy: see customer.spec) ---- applicable_spec_ids = fields.Many2many( 'fusion.plating.customer.spec', 'fp_customer_spec_recipe_rel', 'recipe_id', 'spec_id', string='Applicable Specifications', help='Customer / industry specifications this recipe is qualified to ' 'satisfy. Set on the spec record; mirrored here for navigation.', ) ``` The `bake_temperature_uom` default is hardcoded to `'F'` (Fahrenheit) rather than reading from `company.x_fc_default_temp_uom` — keeps the model self-contained. If the company-default lookup is needed later, switch to `default=lambda self: self.env.company.x_fc_default_temp_uom or 'F'`. ### Task A4: Surface new fields on the recipe form view **Files:** - Modify: `fusion_plating/views/fp_process_node_views.xml` - [ ] **Step 1: Find the recipe root form view** Run: `grep -n 'view_fp_process_node_form\|"fusion.plating.process.node"' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/views/fp_process_node_views.xml | head -10` Note the view ID(s) and form section line numbers. - [ ] **Step 2: Add a "Specification & Bake" notebook page** In the recipe form view (the one where `model="fusion.plating.process.node"` and the form shows a ``), add a new page right before the closing `` tag, visible only when the node is a recipe root: ```xml ``` The `invisible="node_type != 'recipe' or parent_id"` clause hides the page on non-recipe nodes (operations, steps) and on nested recipe references. ### Task A5: Add `recipe_ids` and `print_on_cert` to `fusion.plating.customer.spec` **Files:** - Modify: `fusion_plating_quality/models/fp_customer_spec.py` - [ ] **Step 1: Add the M2M and boolean fields** Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_quality/models/fp_customer_spec.py`. Add the following fields after the existing `notes` field: ```python 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.', ) 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.', ) ``` The relation table name `fp_customer_spec_recipe_rel` matches the inverse declared in Task A3 — they MUST match for Odoo to wire the M2M correctly. ### Task A6: Surface new fields on the customer spec form view **Files:** - Modify: `fusion_plating_quality/views/fp_customer_spec_views.xml` - [ ] **Step 1: Add the recipe linkage + print_on_cert to the form** Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_quality/views/fp_customer_spec_views.xml`. Find the form view's main `` block. Locate the existing `process_type_ids` field. Right after the group that holds it, add: ```xml ``` If the form uses a notebook structure, add the recipe block as a new page or alongside `process_type_ids` in the same page — match the existing layout. ### Task A7: Bump `fusion_plating` version **Files:** - Modify: `fusion_plating/__manifest__.py` - [ ] **Step 1: Bump version** Edit the `'version'` line — change from `'19.0.18.15.16'` to `'19.0.19.0.0'`. ### Task A8: Bump `fusion_plating_quality` version **Files:** - Modify: `fusion_plating_quality/__manifest__.py` - [ ] **Step 1: Bump version** Edit the `'version'` line — change from `'19.0.4.14.0'` to `'19.0.5.0.0'`. ### Task A9: Upgrade modules locally + smoke test - [ ] **Step 1: Upgrade `fusion_plating`** Run: ```bash docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating --stop-after-init 2>&1 | tail -30 ``` Expected: ends with `odoo.modules.loading: Modules loaded.` and exit code 0. No traceback. No `ParseError`. No `KeyError`. If it fails: read the traceback, identify the file + line, fix, retry. - [ ] **Step 2: Upgrade `fusion_plating_quality`** Run: ```bash docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_quality --stop-after-init 2>&1 | tail -30 ``` Expected: same — Modules loaded, no traceback. - [ ] **Step 3: Smoke test the recipe form** Open http://localhost:8069 in browser, log in, navigate Plating → Operations → Process Recipes. Open any recipe (e.g. ENP-ALUM-BASIC). Verify: - The new "Specification & Bake" notebook page appears - All fields are visible and editable: phosphorus_level, thickness_min/max/uom, thickness_option_ids, requires_bake_relief + bake settings, applicable_spec_ids - Setting `requires_bake_relief = True` reveals the bake parameter fields - Saving the form succeeds - [ ] **Step 4: Smoke test the customer spec form** Navigate Plating → Configuration → Quality & Documents → Customer Specs. Open AMS 2404. Verify: - The "Applicable Recipes" picker is visible - Adding a recipe (e.g. ENP-ALUM-BASIC) works and saves - The "Print on Certificate" toggle is visible and defaulted ON - Reopening the recipe shows AMS 2404 in its `applicable_spec_ids` (round-trip) ### Task A10: Commit Phase A - [ ] **Step 1: Stage + commit** Run: ```bash cd /Users/gurpreet/Github/Odoo-Modules git add fusion_plating/models/fp_recipe_thickness.py \ fusion_plating/views/fp_recipe_thickness_views.xml \ fusion_plating/models/__init__.py \ fusion_plating/models/fp_process_node.py \ fusion_plating/views/fp_process_node_views.xml \ fusion_plating/security/ir.model.access.csv \ fusion_plating/__manifest__.py \ fusion_plating_quality/models/fp_customer_spec.py \ fusion_plating_quality/views/fp_customer_spec_views.xml \ fusion_plating_quality/__manifest__.py git commit -m "$(cat <<'EOF' feat(promote-customer-spec): Phase A — recipe + spec foundation - Add fp.recipe.thickness model (replaces fp.coating.thickness, scoped to recipe) - Add spec metadata + bake-relief fields to fusion.plating.process.node (recipe root) - Add recipe_ids M2M + print_on_cert to fusion.plating.customer.spec - Surface new fields on recipe form ("Specification & Bake" notebook page) - Surface recipe linkage on customer spec form Pure additive — no existing code is removed yet. Foundation for Phases B-E. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` Expected: commit succeeds, working tree clean. --- ## Phase B — SO Line UX (Two-Picker) Goal: Add the `Specification` picker to the SO line and the direct order wizard. Hide (don't yet delete) the old `Primary Treatment` picker. Add part-level default for the spec. ### Task B1: Add `x_fc_customer_spec_id` to `sale.order.line` **Files:** - Modify: `fusion_plating_configurator/models/sale_order_line.py` - [ ] **Step 1: Add the new field** Open the file. Find the existing `x_fc_coating_config_id` field declaration (around line 62). Right after it, add: ```python x_fc_customer_spec_id = fields.Many2one( 'fusion.plating.customer.spec', string='Specification', help='Customer / industry specification the work is being shipped to ' '(e.g. AMS 2404 Rev D, BAC 5680 Rev E). Drives certificate ' 'auto-fill and FAI / Nadcap routing.', ) ``` - [ ] **Step 2: Add the auto-fill onchange from part default** Find the existing `_onchange_part_default_variant` method (around line 486). Append a sibling onchange right after it: ```python @api.onchange('x_fc_part_catalog_id') def _onchange_part_default_spec(self): """Pre-fill the line's specification from the part's default.""" for line in self: if (line.x_fc_part_catalog_id and line.x_fc_part_catalog_id.x_fc_default_customer_spec_id and not line.x_fc_customer_spec_id): line.x_fc_customer_spec_id = ( line.x_fc_part_catalog_id.x_fc_default_customer_spec_id ) ``` The `x_fc_default_customer_spec_id` field on the part is added in Task B3 below — it must exist before this onchange fires meaningfully. Since this is a new file save in the same phase, ordering is fine. - [ ] **Step 3: Carry the spec into the invoice line** Find the existing `_prepare_invoice_line` method (around line 458). Add this carry-over right before the `return vals`: ```python if self.x_fc_customer_spec_id: vals['x_fc_customer_spec_id'] = self.x_fc_customer_spec_id.id ``` ### Task B2: Add `x_fc_customer_spec_id` to `account.move.line` **Files:** - Modify: `fusion_plating_configurator/models/account_move_line.py` - [ ] **Step 1: Add the field** Open the file. Find the existing `x_fc_coating_config_id` field (or similar `x_fc_*` block near top). Add: ```python x_fc_customer_spec_id = fields.Many2one( 'fusion.plating.customer.spec', string='Specification', help='Carried from the SO line so customer-facing invoice PDF can ' 'render the spec reference next to the part number.', ) ``` ### Task B3: Add `x_fc_default_customer_spec_id` to `fp.part.catalog` **Files:** - Modify: `fusion_plating_configurator/models/fp_part_catalog.py` - [ ] **Step 1: Add the field** Open the file. Find the existing `x_fc_default_coating_config_id` field (around line 280). Right after it, add: ```python x_fc_default_customer_spec_id = fields.Many2one( 'fusion.plating.customer.spec', string='Default Specification', help='Default specification applied when this part is dropped on a ' 'direct order line. Operator can override per order.', ) ``` ### Task B4: Update SO line view — show Specification picker, hide Primary Treatment **Files:** - Modify: `fusion_plating_configurator/views/sale_order_views.xml` - [ ] **Step 1: Find the SO line tree/form** Run: `grep -n 'x_fc_coating_config_id\|x_fc_part_catalog_id' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml | head -20` Note the line numbers where `x_fc_coating_config_id` appears. - [ ] **Step 2: Add Specification picker, hide Primary Treatment** For each occurrence of ``, replace with: ```xml ``` Keeping the old field with `invisible="1"` ensures any onchange / domain that depends on it during Phase B doesn't break. The field will be deleted entirely in Phase E. - [ ] **Step 3: Update the thickness picker domain** Locate ``. The current domain reads `[('coating_config_id', '=', x_fc_coating_config_id)]`. Change to: ```xml ``` NOTE: this assumes `fp.coating.thickness` has been migrated to `fp.recipe.thickness` and the field model on `sale.order.line.x_fc_thickness_id` was switched. The model switch happens in Task B5 below. ### Task B5: Switch `sale.order.line.x_fc_thickness_id` to `fp.recipe.thickness` **Files:** - Modify: `fusion_plating_configurator/models/sale_order_line.py` - Modify: `fusion_plating_configurator/models/account_move_line.py` - [ ] **Step 1: Update the SO line thickness field comodel** In `sale_order_line.py`, find: ```python x_fc_thickness_id = fields.Many2one( 'fp.coating.thickness', string='Thickness', ondelete='set null', domain="[('coating_config_id', '=', x_fc_coating_config_id)]", help="Target coating thickness. Options come from the line's " 'coating configuration.', ) ``` Replace with: ```python x_fc_thickness_id = fields.Many2one( 'fp.recipe.thickness', string='Thickness', ondelete='set null', domain="[('recipe_id', '=', x_fc_process_variant_id)]", help="Target coating thickness. Options come from the line's recipe.", ) ``` - [ ] **Step 2: Update the onchange that clears thickness on coating change** Find `_onchange_coating_clears_thickness` (around line 578). Replace with: ```python @api.onchange('x_fc_process_variant_id') def _onchange_recipe_clears_thickness(self): """Clear the thickness picker when recipe changes. Thickness options are scoped to the recipe; a value carried over from a previous recipe would fail its domain. """ for line in self: if (line.x_fc_thickness_id and line.x_fc_thickness_id.recipe_id != line.x_fc_process_variant_id): line.x_fc_thickness_id = False ``` - [ ] **Step 3: Same change on `account.move.line`** In `account_move_line.py`, find the analogous `x_fc_thickness_id` field, change the comodel to `'fp.recipe.thickness'` and update the domain to `[('recipe_id', '=', x_fc_process_variant_id)]` if present (some invoice lines may not carry a process variant — leave domain blank if so). ### Task B6: Update part catalog form view **Files:** - Modify: `fusion_plating_configurator/views/fp_part_catalog_views.xml` - [ ] **Step 1: Add Default Specification field, hide Default Treatment** Run: `grep -n 'x_fc_default_coating_config_id\|x_fc_default_treatment_ids' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_part_catalog_views.xml` Replace each `` with: ```xml ``` Replace each `` with ``. ### Task B7: Update direct order wizard model + view **Files:** - Modify: `fusion_plating_configurator/wizard/fp_direct_order_line.py` - Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml` - [ ] **Step 1: Add `customer_spec_id` to wizard line model** In `fp_direct_order_line.py`, find the existing `coating_config_id` field declaration (around line 55). Right after it, add: ```python customer_spec_id = fields.Many2one( 'fusion.plating.customer.spec', string='Specification', help='Customer / industry specification the work ships to.', ) ``` - [ ] **Step 2: Add wizard auto-fill from part default** In the same file, find `_onchange_part_clears_variant` (around line 132). Inside the loop, before the `if not rec.part_catalog_id: continue` check, add: ```python # Pre-fill default spec from the part if line is empty if (rec.part_catalog_id and rec.part_catalog_id.x_fc_default_customer_spec_id and not rec.customer_spec_id): rec.customer_spec_id = rec.part_catalog_id.x_fc_default_customer_spec_id ``` - [ ] **Step 3: Carry spec onto SO line when wizard creates the order** Find the wizard's `_build_so_line_vals` method (or whatever method assembles vals for `sale.order.line.create`). Add this line in the vals dict: ```python 'x_fc_customer_spec_id': line.customer_spec_id.id if line.customer_spec_id else False, ``` If the method signature is unclear, run: `grep -n "x_fc_coating_config_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py` to find where the existing `x_fc_coating_config_id` is set on the SO line and add the spec line right next to it. - [ ] **Step 4: Update wizard view** In `fp_direct_order_wizard_views.xml`, find each `` and replace with: ```xml ``` ### Task B8: Bump `fusion_plating_configurator` version **Files:** - Modify: `fusion_plating_configurator/__manifest__.py` - [ ] **Step 1: Bump version** Change `'version': '19.0.18.10.4'` → `'19.0.19.0.0'`. ### Task B9: Upgrade + smoke test Phase B - [ ] **Step 1: Upgrade configurator** Run: ```bash docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init 2>&1 | tail -30 ``` Expected: clean upgrade, no traceback. - [ ] **Step 2: Smoke test SO line** Open http://localhost:8069. Plating → Sales & Quoting → Quotations. Create a new quote. Add a line. Verify: - Specification picker is visible (was Primary Treatment) - Pick a part with a default spec → Specification auto-fills - Pick a recipe → Thickness dropdown filters to recipe-scoped options - Save the quote — no errors - [ ] **Step 3: Smoke test direct order wizard** Plating → Sales & Quoting → Direct Order. Open the wizard. Add a line. Verify: - Specification picker is visible - Auto-fill from part default works - Submit → SO is created with the spec carried over - [ ] **Step 4: Smoke test part catalog** Plating → Configuration → Shop Setup → (or wherever Parts live). Open a part. Verify "Default Specification" field is visible. Pick AMS 2404, save. ### Task B10: Commit Phase B - [ ] **Step 1: Stage + commit** Run: ```bash cd /Users/gurpreet/Github/Odoo-Modules git add fusion_plating_configurator/ git commit -m "$(cat <<'EOF' feat(promote-customer-spec): Phase B — two-picker SO line UX - Add x_fc_customer_spec_id to sale.order.line + account.move.line - Add x_fc_default_customer_spec_id to fp.part.catalog - Add customer_spec_id to direct-order wizard line - Switch x_fc_thickness_id comodel from fp.coating.thickness to fp.recipe.thickness - Surface Specification picker on SO line, part form, wizard - Hide Primary Treatment + Default Treatment fields (deletion deferred to Phase E) - Auto-fill spec from part default on order entry Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Phase C — Pricing, Quality, Job, Cert Goal: Re-key the consumers of Coating Config (pricing rules, quality points, job records, certificate auto-fill) to read from Customer Spec / Recipe instead. ### Task C1: Add `customer_spec_id` + `recipe_id` to `fp.pricing.rule` **Files:** - Modify: `fusion_plating_configurator/models/fp_pricing_rule.py` - [ ] **Step 1: Add the new fields** Open the file. Find the existing `coating_config_id` field. Right after it, add: ```python customer_spec_id = fields.Many2one( 'fusion.plating.customer.spec', string='Specification', help='Match rule against the SO line specification. Combine with ' 'recipe_id for spec+recipe specific pricing.', ) recipe_id = fields.Many2one( 'fusion.plating.process.node', string='Recipe', domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", help='Match rule against the SO line recipe. Combine with ' 'customer_spec_id for spec+recipe specific pricing, or ' 'leave spec blank for recipe-tier pricing.', ) ``` - [ ] **Step 2: Update the rule lookup logic** Find the rule-matching method (likely `_find_matching_rule` or `find_price`). The exact file/method: Run: `grep -n "def _find_matching\|def find_price\|coating_config_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/fp_pricing_rule.py | head -20` Replace the lookup logic with this priority chain (most-specific-first): ```python @api.model def _find_matching_rule(self, customer_spec=None, recipe=None, partner=None, material=None, **kw): """Lookup priority (first match wins): 1. spec + recipe both set — exact match 2. spec set, recipe blank — spec-tier rule 3. recipe set, spec blank — recipe-tier rule 4. both blank, partner+material — generic fallback """ Rule = self.sudo() # 1. exact spec+recipe match if customer_spec and recipe: r = Rule.search([ ('active', '=', True), ('customer_spec_id', '=', customer_spec.id), ('recipe_id', '=', recipe.id), ], limit=1) if r: return r # 2. spec-tier (any recipe) if customer_spec: r = Rule.search([ ('active', '=', True), ('customer_spec_id', '=', customer_spec.id), ('recipe_id', '=', False), ], limit=1) if r: return r # 3. recipe-tier (any spec) if recipe: r = Rule.search([ ('active', '=', True), ('recipe_id', '=', recipe.id), ('customer_spec_id', '=', False), ], limit=1) if r: return r # 4. partner+material catch-all domain = [('active', '=', True), ('customer_spec_id', '=', False), ('recipe_id', '=', False)] if partner: domain.append(('partner_id', 'in', [False, partner.id])) if material: domain.append(('material_category', 'in', [False, material])) return Rule.search(domain, limit=1) ``` If the existing method takes `coating_config` as a parameter, update all callers in the same file (and elsewhere — grep for them) to pass `customer_spec` and `recipe` instead. Run after editing: `grep -rn "_find_matching_rule\|find_price.*coating" /Users/gurpreet/Github/Odoo-Modules/fusion_plating --include="*.py" | head -20` — fix every caller. ### Task C2: Update `fp.pricing.rule` view **Files:** - Modify: `fusion_plating_configurator/views/fp_pricing_rule_views.xml` - [ ] **Step 1: Add new pickers, hide old** Find each `` and replace with: ```xml ``` ### Task C3: Add `customer_spec_ids` + `recipe_ids` to `fp.quality.point` **Files:** - Modify: `fusion_plating_quality/models/fp_quality_point.py` - [ ] **Step 1: Add new M2M filters** Open the file. Find the existing `coating_config_ids` field (around line 66). Right after, add: ```python customer_spec_ids = fields.Many2many( 'fusion.plating.customer.spec', 'fp_quality_point_spec_rel', 'point_id', 'spec_id', string='Filter by Specifications', help='If set, this trigger only fires for SOs / jobs whose ' 'specification is in this list. Leave blank to ignore spec.', ) recipe_ids = fields.Many2many( 'fusion.plating.process.node', 'fp_quality_point_recipe_rel', 'point_id', 'recipe_id', domain="[('node_type', '=', 'recipe'), ('parent_id', '=', False)]", string='Filter by Recipes', help='If set, this trigger only fires for jobs running one of ' 'these recipes. Leave blank to ignore recipe.', ) ``` - [ ] **Step 2: Update the matcher in `_should_fire_for`** Find the existing matcher method (likely `_should_fire_for` or `_match`). The current `coating_config_ids` check looks like: ```python if self.coating_config_ids and ( not coating or coating not in self.coating_config_ids): return False ``` Add after it (don't remove yet — Phase E removes coating filter): ```python if self.customer_spec_ids and ( not customer_spec or customer_spec not in self.customer_spec_ids): return False if self.recipe_ids and ( not recipe or recipe not in self.recipe_ids): return False ``` Update the method signature to accept `customer_spec=None, recipe=None` parameters. ### Task C4: Update `fp_quality_point_hooks.py` to pass spec + recipe **Files:** - Modify: `fusion_plating_quality/models/fp_quality_point_hooks.py` - [ ] **Step 1: Update each hook to pass new keys** Find every place that reads `coating_config_id` and calls the matcher. Example (around line 82): ```python coating = getattr(job, 'coating_config_id', False) or False ``` Right next to it, add: ```python customer_spec = getattr(job, 'customer_spec_id', False) or False recipe = job.recipe_id if 'recipe_id' in job._fields else False ``` Then update the `_should_fire_for(...)` call to pass the new args: ```python if not point._should_fire_for( coating=coating, # legacy, removed in Phase E customer_spec=customer_spec, recipe=recipe): continue ``` Repeat for all 3 hook locations identified in the spec (line 82, 101, 126). For the SO-line hook (around line 55), do the same — read `x_fc_customer_spec_id` from order lines: ```python customer_specs = so.order_line.mapped('x_fc_customer_spec_id') \ if 'x_fc_customer_spec_id' in so.order_line._fields else False ``` ### Task C5: Update `fp.quality.point` view **Files:** - Modify: `fusion_plating_quality/views/fp_quality_point_views.xml` - [ ] **Step 1: Add new M2M widgets, hide old** Find each `` and replace with: ```xml ``` ### Task C6: Wire `fp.job.customer_spec_id` from SO line on confirm **Files:** - Modify: `fusion_plating_jobs/models/sale_order.py` - [ ] **Step 1: Update job-creation vals** Find the section that creates `fp.job` records on SO confirm. The block currently sets `coating_config_id`: Run: `grep -n "vals\[.coating_config_id.\]\|coating_config_id.*=.*coating" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_jobs/models/sale_order.py | head -10` For each location that sets `coating_config_id` on job vals, add immediately after: ```python # Promote Customer Spec — carry from SO line to job spec = getattr(line, 'x_fc_customer_spec_id', False) or False if not spec and 'x_fc_customer_spec_id' in self._fields: spec = self.x_fc_customer_spec_id or False if spec: vals['customer_spec_id'] = spec.id ``` - [ ] **Step 2: Update spec-resolution helper if present** Run: `grep -n "_resolve_customer_spec\|_fp_resolve_spec\|customer_spec.*resolve" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_jobs/models/sale_order.py` If a helper exists, ensure it picks up `x_fc_customer_spec_id` from the line first, then falls back to a partner-level default if any. If no helper exists, the inline pattern from Step 1 is sufficient. ### Task C7: Update `fp.certificate` auto-fill to read from `customer_spec_id` **Files:** - Modify: `fusion_plating_certificates/models/fp_certificate.py` - [ ] **Step 1: Switch the spec_reference source** Open the file. Find the `_fp_create_certificates` method and locate the line that reads coating config (around line 293): ```python cfg = getattr(so, 'x_fc_coating_config_id', False) ``` Replace the surrounding spec_reference assembly with: ```python # Promote Customer Spec — read spec from the job's customer_spec_id spec = getattr(job, 'customer_spec_id', False) or False if not spec and so: # Fall back to SO-line-level spec if job didn't carry it spec = (so.order_line.mapped('x_fc_customer_spec_id')[:1] if 'x_fc_customer_spec_id' in so.order_line._fields else False) if spec and spec.print_on_cert: spec_ref = spec.code or '' if spec.revision: spec_ref = f'{spec_ref} Rev {spec.revision}' vals['spec_reference'] = spec_ref vals['customer_spec_id'] = spec.id # if cert has this field ``` If `fp.certificate` doesn't yet have a `customer_spec_id` field, add it now (in the same file at the field declarations): ```python customer_spec_id = fields.Many2one( 'fusion.plating.customer.spec', string='Specification', help='Snapshot of the spec the cert was issued against.', ) ``` ### Task C8: Bump versions **Files:** - Modify: `fusion_plating_configurator/__manifest__.py` - Modify: `fusion_plating_quality/__manifest__.py` - Modify: `fusion_plating_jobs/__manifest__.py` - Modify: `fusion_plating_certificates/__manifest__.py` - [ ] **Step 1: Bump each** - `fusion_plating_configurator`: `19.0.19.0.0` → `19.0.19.1.0` - `fusion_plating_quality`: `19.0.5.0.0` → `19.0.5.1.0` - `fusion_plating_jobs`: `19.0.8.27.0` → `19.0.9.0.0` - `fusion_plating_certificates`: `19.0.5.6.0` → `19.0.6.0.0` ### Task C9: Upgrade + smoke test Phase C - [ ] **Step 1: Upgrade all four modules** Run: ```bash docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator,fusion_plating_quality,fusion_plating_jobs,fusion_plating_certificates --stop-after-init 2>&1 | tail -30 ``` Expected: all four upgrade cleanly. - [ ] **Step 2: Smoke test pricing** Plating → Configuration → Pricing & Billing → (Pricing Rules — find existing menu). Open a rule. Verify Specification + Recipe pickers appear. Create a test rule: customer_spec=AMS 2404, recipe=ENP-ALUM-BASIC, price = $1.00/sqft. Save. Then create a test SO with that spec + recipe and verify the unit price comes back as $1.00 (or whatever the rule says). - [ ] **Step 3: Smoke test quality point** Plating → Configuration → Recipes & Steps → Quality Points (or wherever). Open or create a point. Verify customer_spec_ids + recipe_ids pickers are visible. Set both to specific values and confirm only matching jobs trigger checks. - [ ] **Step 4: Smoke test job creation** Confirm a quote that has `x_fc_customer_spec_id` set on a line. After confirm, navigate to the resulting `fp.job`. Verify `customer_spec_id` is populated. - [ ] **Step 5: Smoke test cert** Mark the job done. Issue a cert (Certificates menu). Verify the `spec_reference` field on the cert reads "AMS 2404 Rev D" (or whatever spec was on the SO line). ### Task C10: Commit Phase C - [ ] **Step 1: Stage + commit** Run: ```bash cd /Users/gurpreet/Github/Odoo-Modules git add fusion_plating_configurator/ fusion_plating_quality/ fusion_plating_jobs/ fusion_plating_certificates/ git commit -m "$(cat <<'EOF' feat(promote-customer-spec): Phase C — pricing, quality, job, cert re-keyed - Add customer_spec_id + recipe_id to fp.pricing.rule with priority lookup - Add customer_spec_ids + recipe_ids M2M filters to fp.quality.point + hooks - Wire customer_spec_id from SO line to fp.job on confirm - Switch fp.certificate spec_reference auto-fill to read from customer_spec_id - Add customer_spec_id to fp.certificate for traceability snapshot Old coating_config_id paths still functional during transition; removed in Phase E. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Phase D — Reports, Tablet, Portal Goal: Update all customer-facing and operator-facing surfaces to print/display Specification + Recipe instead of Coating Config. ### Task D1: Update `report_fp_sale.xml` (sale acknowledgement) **Files:** - Modify: `fusion_plating_reports/report/report_fp_sale.xml` - [ ] **Step 1: Replace coating field references with spec** Run: `grep -n "x_fc_coating_config_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_sale.xml` For each occurrence inside `` and `` patterns, replace: ```xml ``` → ```xml ``` And: ```xml ``` → ```xml ``` Repeat for all 4 occurrences identified by the grep. ### Task D2: Update `report_fp_wo_sticker.xml` (WO sticker) **Files:** - Modify: `fusion_plating_reports/report/report_fp_wo_sticker.xml` - [ ] **Step 1: Replace coating references** Run: `grep -n "x_fc_coating_config_id\|_coating" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_fp_wo_sticker.xml` For each `t-set="_coating" t-value="..."` pattern, change: ```xml ``` → ```xml ``` For each `t-set="_coating" t-value="line.x_fc_coating_config_id"`: ```xml ``` For the comment block at top of file, update the variable list to reference `_spec` (`fusion.plating.customer.spec`) instead of `_coating` (`fp.coating.config`). For all rendering points that print `_coating.name`, change to `_spec.display_name` or `_spec.code` — pick whichever shows up in the cert format (likely `_spec.code` + ` Rev ` + `_spec.revision`). ### Task D3: Update `report_fp_job_traveller.xml` (in `fusion_plating_reports`) **Files:** - Modify: `fusion_plating_reports/report/report_fp_job_traveller.xml` - [ ] **Step 1: Replace coating reference** Find: ```xml ``` Replace with: ```xml ``` ### Task D4: Update `report_fp_job_traveller.xml` (in `fusion_plating_jobs`) **Files:** - Modify: `fusion_plating_jobs/report/report_fp_job_traveller.xml` - [ ] **Step 1: Replace coating reference** Find: ```xml ``` Replace with: ```xml ``` ### Task D5: Update `report_fp_job_sticker.xml` **Files:** - Modify: `fusion_plating_jobs/report/report_fp_job_sticker.xml` - [ ] **Step 1: Replace `_coating` t-set blocks** Find each block: ```xml ``` Replace with: ```xml ``` Then update any `_coating.name` references downstream to `_spec.display_name`. ### Task D6: Update CoC EN/FR templates **Files:** - Modify: `fusion_plating_reports/report/report_coc_en.xml` (or whichever filename holds the CoC body) - Modify: `fusion_plating_reports/report/report_coc_fr.xml` - Modify: `fusion_plating_reports/report/report_coc_chronological.xml` (if it references coating) - [ ] **Step 1: Locate coating refs in CoC templates** Run: `grep -rn "coating_config\|x_fc_coating" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_reports/report/report_coc*.xml` For each occurrence, replace coating field reads with the cert's own `spec_reference` field (which Phase C already populates from `customer_spec_id`). Example: ```xml ``` This may already be the case — the coating refs in CoC templates may have only been used for fall-back display. Verify by reading each match. ### Task D7: Update tablet/shopfloor controller payload **Files:** - Modify: `fusion_plating_shopfloor/controllers/shopfloor_controller.py` - [ ] **Step 1: Replace coating field reads** Run: `grep -n "coating_config_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py` At line ~1143 (job payload): ```python 'part_catalog_id', 'coating_config_id', ``` Change to: ```python 'part_catalog_id', 'customer_spec_id', ``` At line ~1554 (job lookup): ```python job.coating_config_id if 'coating_config_id' in job._fields else False ``` Change to: ```python job.customer_spec_id if 'customer_spec_id' in job._fields else False ``` If the field appears elsewhere in payload-building code, replace all occurrences. ### Task D8: Update portal configurator controller **Files:** - Modify: `fusion_plating_portal/controllers/portal_configurator.py` - [ ] **Step 1: Replace coating session keys with spec** Open the file. Find every `'coating_config_id'` string key. For each, do a pair of changes: Replace: ```python coating_id = int(kw.get('coating_config_id', 0)) if coating_id: session_data['coating_config_id'] = coating_id ``` With: ```python spec_id = int(kw.get('customer_spec_id', 0)) if spec_id: session_data['customer_spec_id'] = spec_id ``` Replace: ```python coatings = request.env['fp.coating.config'].sudo().search(...) ``` With: ```python specs = request.env['fusion.plating.customer.spec'].sudo().search( [('active', '=', True)], order='spec_type, code, revision', ) ``` Replace lookup blocks: ```python coating = request.env['fp.coating.config'].sudo().browse( session_data['coating_config_id'], ) ``` With: ```python spec = request.env['fusion.plating.customer.spec'].sudo().browse( session_data['customer_spec_id'], ) ``` For pricing rule resolution (around line 267): ```python if rule.coating_config_id: if rule.coating_config_id.id != coating.id: continue ``` Change to: ```python if rule.customer_spec_id: if rule.customer_spec_id.id != spec.id: continue ``` Replace template variable name `coatings` → `specs` and `coating` → `spec` consistently throughout. ### Task D9: Update portal template **Files:** - Modify: `fusion_plating_portal/views/fp_portal_configurator_templates.xml` - [ ] **Step 1: Replace template variable + form field names** Run: `grep -n "coating_config_id\|coatings" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_portal/views/fp_portal_configurator_templates.xml` For each occurrence: - `name="coating_config_id"` → `name="customer_spec_id"` - `id="coating_config_id"` → `id="customer_spec_id"` - `getElementById('coating_config_id')` → `getElementById('customer_spec_id')` - `dataset.coatingId` → `dataset.specId` - `t-foreach="coatings"` → `t-foreach="specs"` - `t-as="coating"` → `t-as="spec"` - `coating.name` → `spec.display_name` (or `spec.code` — pick by visual context) For customer-facing labels — keep "Coating" if the visible text reads to a customer. The internal field name changes; the customer-visible text is a separate decision (per spec open question #1). For now, keep the visible text as "Coating Specification" (compromise) and revisit after the client call. ### Task D10: Bump versions **Files:** - Modify: `fusion_plating_reports/__manifest__.py` - Modify: `fusion_plating_jobs/__manifest__.py` (already bumped in C — bump again to a new minor) - Modify: `fusion_plating_shopfloor/__manifest__.py` - Modify: `fusion_plating_portal/__manifest__.py` - [ ] **Step 1: Bump each** - `fusion_plating_reports`: `19.0.10.16.0` → `19.0.11.0.0` - `fusion_plating_jobs`: `19.0.9.0.0` → `19.0.9.1.0` - `fusion_plating_shopfloor`: `19.0.25.2.1` → `19.0.26.0.0` - `fusion_plating_portal`: `19.0.2.1.1` → `19.0.3.0.0` ### Task D11: Upgrade + smoke test Phase D - [ ] **Step 1: Upgrade all four modules** Run: ```bash docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_reports,fusion_plating_jobs,fusion_plating_shopfloor,fusion_plating_portal --stop-after-init 2>&1 | tail -30 ``` Expected: clean upgrade. - [ ] **Step 2: Smoke test sale ack PDF** Open a confirmed SO (the one created during Phase C smoke test). Print → Sale Order PDF. Verify the line shows the spec (e.g. "AMS 2404 Rev D") instead of empty/coating. - [ ] **Step 3: Smoke test WO sticker PDF** From the SO's smart button, open a job. Print → WO Sticker. Verify the spec is displayed. - [ ] **Step 4: Smoke test job traveller PDF** Same job → Print → Job Traveller. Verify spec is in the header. - [ ] **Step 5: Smoke test CoC PDF** Issue a cert from the job. Verify the cert PDF prints "AMS 2404 Rev D" in the spec field. - [ ] **Step 6: Smoke test tablet payload** Open Plating → Shop Floor → (Tablet station). Scan / open a job. Verify the job payload includes the spec info (visible in the UI as "Specification: AMS 2404 Rev D" or similar). - [ ] **Step 7: Smoke test portal** Open the portal customer-facing URL (e.g. /my/quote-request). Walk through the configurator — verify the picker is now showing Specifications instead of Coatings. Submit a quote. Verify it lands as an SO with `x_fc_customer_spec_id` populated. ### Task D12: Commit Phase D - [ ] **Step 1: Stage + commit** Run: ```bash cd /Users/gurpreet/Github/Odoo-Modules git add fusion_plating_reports/ fusion_plating_jobs/ fusion_plating_shopfloor/ fusion_plating_portal/ git commit -m "$(cat <<'EOF' feat(promote-customer-spec): Phase D — reports, tablet, portal updated - Replace coating_config_id refs with customer_spec_id in: - report_fp_sale.xml (sale ack) - report_fp_wo_sticker.xml (WO sticker) - report_fp_job_traveller.xml (jobs + reports modules) - report_fp_job_sticker.xml (job sticker) - CoC EN/FR templates - shopfloor_controller.py (tablet payload) - Portal configurator: switch from fp.coating.config to fusion.plating.customer.spec - Portal template: rename form field, dataset attrs, template vars Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Phase E — Final Removal Goal: Delete `fp.coating.config`, `fp.treatment`, `fp.coating.thickness`, and all hidden `x_fc_coating_config_id` / `x_fc_treatment_ids` fields. Module installs cleanly with zero references to dead code. ### Task E1: Drop `coating_config_id` from `fp.job` **Files:** - Modify: `fusion_plating_jobs/models/fp_job.py` - [ ] **Step 1: Remove the field declaration** Open the file. Find (around line 51): ```python coating_config_id = fields.Many2one( 'fp.coating.config', string='Coating Configuration', ondelete='restrict', ) ``` Delete the entire block. - [ ] **Step 2: Remove any code that READS this field** Run: `grep -n "coating_config_id" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_jobs/models/fp_job.py` For each line, evaluate and either delete or refactor to read from `customer_spec_id`. Common patterns: Around line 1001 + 1549: ```python coating = job.coating_config_id ``` These reads are likely for bake-relief logic. Replace with: ```python recipe = job.recipe_id # bake settings now live on recipe ``` And update the surrounding logic to read `recipe.requires_bake_relief`, `recipe.bake_window_hours`, etc., instead of `coating.requires_bake_relief`. ### Task E2: Drop `coating_config_id` references from `sale_order.py` **Files:** - Modify: `fusion_plating_jobs/models/sale_order.py` - [ ] **Step 1: Remove all coating_config refs** Run: `grep -n "x_fc_coating_config_id\|coating_config" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_jobs/models/sale_order.py` For each line, delete the line or refactor to use `x_fc_customer_spec_id` / `customer_spec_id`. Lines 356-491 in the original file have a chain of guards reading both line-level and SO-level coating fields — all of these can be removed since: 1. The SO-line `x_fc_coating_config_id` is being deleted in Task E5 2. The job's `coating_config_id` was deleted in Task E1 3. Spec resolution from line happens in Task C6 already Verify the resulting code only references `x_fc_customer_spec_id` or `customer_spec_id` after the cleanup. ### Task E3: Drop fields from `sale.order.line` **Files:** - Modify: `fusion_plating_configurator/models/sale_order_line.py` - [ ] **Step 1: Delete `x_fc_coating_config_id` field** Find: ```python x_fc_coating_config_id = fields.Many2one( 'fp.coating.config', string='Primary Treatment', ) ``` Delete the entire block. - [ ] **Step 2: Delete `x_fc_treatment_ids` field** Find: ```python x_fc_treatment_ids = fields.Many2many( 'fp.treatment', string='Additional Treatments', ) ``` Delete the entire block. ### Task E4: Drop fields from `account.move.line` + `fp.part.catalog` **Files:** - Modify: `fusion_plating_configurator/models/account_move_line.py` - Modify: `fusion_plating_configurator/models/fp_part_catalog.py` - [ ] **Step 1: Delete `x_fc_coating_config_id` from invoice line** In `account_move_line.py`, find and delete the `x_fc_coating_config_id` field block. If `_prepare_invoice_line` or similar carries the field, remove that line too. - [ ] **Step 2: Delete defaults from part catalog** In `fp_part_catalog.py`, delete: ```python x_fc_default_coating_config_id = fields.Many2one(...) x_fc_default_treatment_ids = fields.Many2many(...) ``` ### Task E5: Drop SO line view's hidden coating fields **Files:** - Modify: `fusion_plating_configurator/views/sale_order_views.xml` - Modify: `fusion_plating_configurator/views/fp_part_catalog_views.xml` - Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml` - [ ] **Step 1: Remove `` lines** Run: `grep -rn 'name="x_fc_coating_config_id"\|name="x_fc_treatment_ids"\|name="x_fc_default_coating_config_id"\|name="x_fc_default_treatment_ids"\|name="coating_config_id" invisible' /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/ /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/` For each match, delete the line. ### Task E6: Drop wizard model coating field **Files:** - Modify: `fusion_plating_configurator/wizard/fp_direct_order_line.py` - Modify: `fusion_plating_configurator/wizard/fp_direct_order_wizard.py` - [ ] **Step 1: Delete `coating_config_id` from wizard line** In `fp_direct_order_line.py`, find: ```python coating_config_id = fields.Many2one( 'fp.coating.config', string='Primary Treatment', help='...', ) ``` Delete the entire block. - [ ] **Step 2: Delete `treatment_ids` from wizard line** In the same file, find: ```python treatment_ids = fields.Many2many( 'fp.treatment', string='Additional Treatments', help='...', ) ``` Delete. - [ ] **Step 3: Update the recipe-resolution chain** In `_compute_effective_process` (around line 107), the resolution chain currently has: ```python @api.depends('process_variant_id', 'part_catalog_id.default_process_id', 'coating_config_id.recipe_id') def _compute_effective_process(self): ... cc_proc = (rec.coating_config_id.recipe_id if rec.coating_config_id else False) ``` Since `coating_config_id` no longer exists, simplify the resolution to: ```python @api.depends('process_variant_id', 'part_catalog_id.default_process_id') def _compute_effective_process(self): for rec in self: if rec.process_variant_id: rec.effective_process_id = rec.process_variant_id label = (rec.process_variant_id.variant_label or rec.process_variant_id.name) rec.effective_process_source = 'Variant: %s' % (label or 'unnamed') continue part_proc = (rec.part_catalog_id.default_process_id if rec.part_catalog_id else False) if part_proc: rec.effective_process_id = part_proc rec.effective_process_source = 'Part default' continue rec.effective_process_id = False rec.effective_process_source = False ``` - [ ] **Step 4: Remove all other coating refs in wizard** Run: `grep -n "coating_config\|treatment_ids" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_line.py /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py` Delete every remaining occurrence (auto-fill calls, vals dict entries, etc.). ### Task E7: Drop `coating_config_id` + `coating_config_ids` from pricing + quality **Files:** - Modify: `fusion_plating_configurator/models/fp_pricing_rule.py` - Modify: `fusion_plating_configurator/views/fp_pricing_rule_views.xml` - Modify: `fusion_plating_quality/models/fp_quality_point.py` - Modify: `fusion_plating_quality/models/fp_quality_point_hooks.py` - Modify: `fusion_plating_quality/views/fp_quality_point_views.xml` - [ ] **Step 1: Drop `coating_config_id` field from pricing rule** In `fp_pricing_rule.py`, delete: ```python coating_config_id = fields.Many2one('fp.coating.config', ...) ``` Verify no remaining method body reads `coating_config_id`. - [ ] **Step 2: Drop hidden field from pricing view** In `fp_pricing_rule_views.xml`, delete every `` line. - [ ] **Step 3: Drop `coating_config_ids` from quality point** In `fp_quality_point.py`, delete the `coating_config_ids` M2M block. - [ ] **Step 4: Drop coating refs from quality point hooks** In `fp_quality_point_hooks.py`, find each line that reads `coating_config_id` (lines 55, 82, 101, 126 in the original). Delete the `coating = ...` assignment lines and the `coating=coating,` arg in `_should_fire_for` calls. The matcher in `_should_fire_for` (Phase C added the new spec/recipe checks alongside the coating check). Now remove the coating check: ```python if self.coating_config_ids and ( not coating or coating not in self.coating_config_ids): return False ``` Delete this block. Update the method signature to drop `coating=None` parameter. - [ ] **Step 5: Drop hidden field from quality point view** In `fp_quality_point_views.xml`, delete every `` line. ### Task E8: Drop `fp.coating.config` model + view + ACL **Files:** - Delete: `fusion_plating_configurator/models/fp_coating_config.py` - Delete: `fusion_plating_configurator/views/fp_coating_config_views.xml` - Modify: `fusion_plating_configurator/models/__init__.py` - Modify: `fusion_plating_configurator/__manifest__.py` - Modify: `fusion_plating_configurator/security/ir.model.access.csv` - Modify: `fusion_plating_configurator/views/fp_configurator_menu.xml` - [ ] **Step 1: Delete model file** Run: `rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/fp_coating_config.py` - [ ] **Step 2: Delete view file** Run: `rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_coating_config_views.xml` - [ ] **Step 3: Remove import from models `__init__.py`** Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/__init__.py`. Delete the line: ```python from . import fp_coating_config ``` - [ ] **Step 4: Remove view from manifest data list** Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/__manifest__.py`. Find and delete: ```python 'views/fp_coating_config_views.xml', ``` - [ ] **Step 5: Remove ACL rows** Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/security/ir.model.access.csv`. Delete the 3 rows (lines 8-10): ``` access_fp_coating_config_operator,fp.coating.config.operator,model_fp_coating_config,... access_fp_coating_config_estimator,fp.coating.config.estimator,model_fp_coating_config,... access_fp_coating_config_manager,fp.coating.config.manager,model_fp_coating_config,... ``` - [ ] **Step 6: Remove menu item** Open `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml`. Find: ```xml ``` Delete the entire `` element. Also delete the related `` for the action (likely `action_fp_coating_config`) if defined in the same file. ### Task E9: Drop `fp.treatment` model + view + ACL + data **Files:** - Delete: `fusion_plating_configurator/models/fp_treatment.py` - Delete: `fusion_plating_configurator/views/fp_treatment_views.xml` - Delete: `fusion_plating_configurator/data/fp_treatment_data.xml` - Modify: `fusion_plating_configurator/models/__init__.py` - Modify: `fusion_plating_configurator/__manifest__.py` - Modify: `fusion_plating_configurator/security/ir.model.access.csv` - [ ] **Step 1: Delete model + view + data files** Run: ```bash rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/fp_treatment.py rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_treatment_views.xml rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/data/fp_treatment_data.xml ``` - [ ] **Step 2: Remove import from `__init__.py`** Delete `from . import fp_treatment` from `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/__init__.py`. - [ ] **Step 3: Remove view + data from manifest** In `__manifest__.py`, delete: ```python 'views/fp_treatment_views.xml', 'data/fp_treatment_data.xml', ``` - [ ] **Step 4: Remove ACL rows** In `ir.model.access.csv`, delete the 3 rows (lines 2-4): ``` access_fp_treatment_operator,fp.treatment.operator,model_fp_treatment,... access_fp_treatment_supervisor,fp.treatment.supervisor,model_fp_treatment,... access_fp_treatment_manager,fp.treatment.manager,model_fp_treatment,... ``` - [ ] **Step 5: Remove menu item if present** Run: `grep -n "menu_fp_treatment\|fp_treatment" /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_configurator_menu.xml` If a menuitem exists, delete the `` element (and the related `` for its action if local). ### Task E10: Drop `fp.coating.thickness` model + view + ACL **Files:** - Delete: `fusion_plating_configurator/models/fp_coating_thickness.py` - Delete: `fusion_plating_configurator/views/fp_coating_thickness_views.xml` - Modify: `fusion_plating_configurator/models/__init__.py` - Modify: `fusion_plating_configurator/__manifest__.py` - Modify: `fusion_plating_configurator/security/ir.model.access.csv` - [ ] **Step 1: Delete files** Run: ```bash rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/fp_coating_thickness.py rm /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/views/fp_coating_thickness_views.xml ``` - [ ] **Step 2: Remove import from `__init__.py`** Delete `from . import fp_coating_thickness` from `/Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating_configurator/models/__init__.py`. - [ ] **Step 3: Remove view from manifest** Delete `'views/fp_coating_thickness_views.xml',` from `__manifest__.py`. - [ ] **Step 4: Remove ACL rows** In `ir.model.access.csv`, delete the 3 rows (lines 51-53 in the original): ``` access_fp_coating_thickness_user,... access_fp_coating_thickness_estimator,... access_fp_coating_thickness_manager,... ``` ### Task E11: Re-point `fp.delivery.x_fc_thickness_id` to `fp.recipe.thickness` **Files:** - Modify: `fusion_plating_logistics/models/fp_delivery.py` - [ ] **Step 1: Switch comodel** Open the file. Find: ```python 'fp.coating.thickness', string='Thickness', ``` Change to: ```python 'fp.recipe.thickness', string='Thickness', ``` ### Task E12: Bump versions **Files:** - Modify: `fusion_plating_configurator/__manifest__.py` - Modify: `fusion_plating_jobs/__manifest__.py` - Modify: `fusion_plating_quality/__manifest__.py` - Modify: `fusion_plating_logistics/__manifest__.py` - [ ] **Step 1: Bump each** - `fusion_plating_configurator`: `19.0.19.1.0` → `19.0.20.0.0` - `fusion_plating_jobs`: `19.0.9.1.0` → `19.0.10.0.0` - `fusion_plating_quality`: `19.0.5.1.0` → `19.0.6.0.0` - `fusion_plating_logistics`: bump minor (read current with grep) ### Task E13: Final upgrade + comprehensive smoke test - [ ] **Step 1: Upgrade all touched modules in dependency order** Run: ```bash docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating,fusion_plating_quality,fusion_plating_configurator,fusion_plating_jobs,fusion_plating_certificates,fusion_plating_reports,fusion_plating_shopfloor,fusion_plating_portal,fusion_plating_logistics --stop-after-init 2>&1 | tail -50 ``` Expected: ALL modules upgrade cleanly, no traceback, no `KeyError: 'fp.coating.config'`, no `psycopg2.errors.UndefinedColumn`. If any error mentions `coating_config` or `treatment`, grep for the missed reference, fix it, retry. - [ ] **Step 2: Verify zero references remain** Run: ```bash grep -rn "coating_config_id\|fp\.coating\.config\|fp\.treatment\b\|fp\.coating\.thickness\|x_fc_treatment_ids\|x_fc_default_coating\|x_fc_default_treatment" /Users/gurpreet/Github/Odoo-Modules/fusion_plating --include="*.py" --include="*.xml" --include="*.csv" 2>/dev/null | grep -v "/scripts/\|/tests/\|/docs/" | head -50 ``` Expected: ZERO results. If any results appear, they must be addressed before this phase is complete (each one is a missed reference). Update the relevant file and re-run upgrade. - [ ] **Step 3: End-to-end smoke test (golden path)** Walk through the full lifecycle: 1. Plating → Sales & Quoting → Quotations → New 2. Add a customer (e.g. "Test Aerospace Inc") 3. Add a line: pick a part with a default spec, verify Specification + Recipe pre-fill 4. Confirm the order 5. Open the resulting `fp.job` (smart button on SO) 6. Verify `customer_spec_id` is populated 7. Open the operator tablet (Plating → Shop Floor → Tablet) 8. Walk through job → start → finish each step 9. Mark the job done 10. Issue a CoC (Plating → Quality → Certificates) 11. Verify the CoC PDF prints "Spec: AMS 2404 Rev D" (or whatever the test spec was) 12. Generate the invoice 13. Verify the invoice line carries the spec If any step fails, debug + fix before declaring Phase E complete. - [ ] **Step 4: Test the rare scenarios** 1. **Mid-Phos vs High-Phos same customer:** - Create two SOs for the same customer + part - SO1: Specification=AMS 2404, Recipe=EN-MP - SO2: Specification=AMS 2404, Recipe=EN-HP - Confirm both. Verify SO1 job auto-creates a bake-window record (Mid-Phos requires bake), SO2 does not (High-Phos doesn't). 2. **Spec revision:** - Create a new customer.spec record: code=AMS 2404, revision=E (Rev D already seeded) - Create a SO using Rev E - Issue CoC. Verify it prints "AMS 2404 Rev E" not "Rev D" 3. **Process freeze:** - Edit a recipe (mark a step "skip"). Save. Verify chatter on the recipe records the change. - Verify the customer.spec records are untouched (no spurious chatter on them). ### Task E14: Commit Phase E - [ ] **Step 1: Stage all changes (including deletions)** Run: ```bash cd /Users/gurpreet/Github/Odoo-Modules git add -A fusion_plating_configurator/ fusion_plating_jobs/ fusion_plating_quality/ fusion_plating_logistics/ git status ``` Verify deleted files appear with status `D` and modified files with `M`. - [ ] **Step 2: Commit** Run: ```bash git commit -m "$(cat <<'EOF' feat(promote-customer-spec): Phase E — final removal of coating config + treatment - Delete fp.coating.config model + view + ACL + menu - Delete fp.treatment model + view + ACL + seeded data - Delete fp.coating.thickness model + view + ACL (replaced by fp.recipe.thickness) - Drop x_fc_coating_config_id from sale.order.line, account.move.line, fp.part.catalog - Drop x_fc_treatment_ids from sale.order.line, fp.part.catalog - Drop coating_config_id from fp.job, fp.pricing.rule - Drop coating_config_ids M2M from fp.quality.point + matcher - Drop coating_config refs from sale_order.py, fp_job_step.py, hooks - Re-point fp.delivery.x_fc_thickness_id to fp.recipe.thickness Zero coating_config / treatment references remain in active codebase. Co-Authored-By: Claude Opus 4.7 (1M context) EOF )" ``` --- ## Post-Work — Push + (Optional) Deploy to Entech ### Task POST1: Push the feature branch - [ ] **Step 1: Push to both remotes** Run: ```bash cd /Users/gurpreet/Github/Odoo-Modules git push origin feature/promote-customer-spec git push gitea feature/promote-customer-spec ``` ### Task POST2: Open a pull request (optional, for review) - [ ] **Step 1: Open PR** Run: ```bash gh pr create --base main --head feature/promote-customer-spec \ --title "Promote Customer Spec, retire Coating Config" \ --body "$(cat <<'EOF' ## Summary - Retire fp.coating.config + fp.treatment entirely (no archive) - Promote fusion.plating.customer.spec to primary spec entity - Two-picker SO line UX: Specification + Recipe - Move process params (thickness, bake) onto Recipe model ## Spec [docs/superpowers/specs/2026-05-14-promote-customer-spec-design.md](https://github.com/gsinghpal/Odoo-Modules/blob/feature/promote-customer-spec/fusion_plating/docs/superpowers/specs/2026-05-14-promote-customer-spec-design.md) ## Backup Pre-change snapshot at `backup/pre-spec-recipe-collapse-2026-05-14`. ## Test plan - [ ] Recipe form shows new "Specification & Bake" notebook page - [ ] Customer Spec form shows recipe linkage + print_on_cert - [ ] SO line + direct order wizard show Specification picker - [ ] Pricing rule lookup respects spec + recipe priority chain - [ ] CoC PDF prints spec.code + revision - [ ] Mid-Phos job auto-creates bake-window; High-Phos does not - [ ] grep for `coating_config\|fp\.treatment` returns zero results 🤖 Generated with [Claude Code](https://claude.com/claude-code) EOF )" ``` ### Task POST3: Deploy to entech (optional, after review) - [ ] **Step 1: Sync code to entech** For each modified module, run (one example shown for `fusion_plating`): ```bash cat /Users/gurpreet/Github/Odoo-Modules/fusion_plating/fusion_plating/models/fp_process_node.py | \ ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/fusion_plating/models/fp_process_node.py'" ``` Repeat for each touched file. (For bulk sync, consider rsync or git pull on entech if a git remote is configured there.) - [ ] **Step 2: Stop entech, upgrade, restart** Run: ```bash ssh pve-worker5 "pct exec 111 -- bash -c ' systemctl stop odoo && \ su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u fusion_plating,fusion_plating_quality,fusion_plating_configurator,fusion_plating_jobs,fusion_plating_certificates,fusion_plating_reports,fusion_plating_shopfloor,fusion_plating_portal,fusion_plating_logistics --stop-after-init\" && \ systemctl start odoo '" ``` - [ ] **Step 3: Clear asset cache on entech** Run: ```bash ssh pve-worker5 "pct exec 111 -- bash -c \"sudo -u postgres psql -d admin -c \\\"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';\\\"\"" ``` - [ ] **Step 4: Verify entech health** Open https://entech-url/web (whatever the entech URL is). Log in. Walk through the same end-to-end smoke test from Task E13 Step 3. --- ## Self-Review Notes **Spec coverage check:** Every section of the design spec maps to at least one task: - Decision 1 (promote spec, retire coating) → covered by Phases A + E - Decision 2 (two-picker SO line) → Phase B - Decision 3 (move process params to recipe) → Phase A (Tasks A3-A4) - Decision 4 (delete fp.treatment) → Phase E (Task E9) - Decision 5 (naming) → applied throughout view edits - Decision 6 (M2M relationship) → Phase A (Tasks A3, A5) - All "Models removed entirely" → Phase E - All "Models modified" → Phases A, B, C - All "Re-keyed" items → Phase C, D - All UI/view changes → Phases B, C, D - Per-module impact summary → tasks distributed across phases match - 7 aerospace scenarios → validated implicitly by Phase E Step 4 (Mid/High Phos, spec rev, process freeze) **Placeholder scan:** No "TBD", "TODO", "implement later" patterns in the plan. Every code block is concrete. **Type consistency:** - M2M relation table `fp_customer_spec_recipe_rel` is consistent between `fusion.plating.customer.spec.recipe_ids` (Task A5) and `fusion.plating.process.node.applicable_spec_ids` (Task A3) ✓ - Field name `customer_spec_id` (singular M2O) used consistently on `sale.order.line`, `account.move.line`, `fp.job`, `fp.pricing.rule`, `fp.certificate`, `fp.delivery` ✓ - M2M fields use `_ids` suffix consistently (`recipe_ids`, `customer_spec_ids`, `applicable_spec_ids`, `process_type_ids`) ✓ - `fp.recipe.thickness.recipe_id` matches the inverse `fusion.plating.process.node.thickness_option_ids` ✓ **Open items deferred to client confirmation (per spec):** - Portal customer-facing label: kept as "Coating Specification" in Phase D Task D9 — revisit after client call - Pricing rule complexity: assumes both spec_id + recipe_id keys are useful; can simplify if client confirms only one matters - Source Approval Letter tracking: deferred to a future enhancement - DPAS / ITAR / DFARS extension: deferred --- ## Plan Complete — Execution Choice **Plan saved to** [`docs/superpowers/plans/2026-05-14-promote-customer-spec.md`](docs/superpowers/plans/2026-05-14-promote-customer-spec.md). **Two execution options:** **1. Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration. Good for this plan because each task is small and self-contained, and the surface area (10 modules) means a fresh context per task avoids confusion. **2. Inline Execution** — Execute tasks in this session using the executing-plans skill, batch execution with checkpoints. Faster overall but every task adds to context. **Which approach?**