diff --git a/fusion-plating/docs/superpowers/plans/2026-04-12-phase1-configurator-sales.md b/fusion-plating/docs/superpowers/plans/2026-04-12-phase1-configurator-sales.md
new file mode 100644
index 00000000..d6243f36
--- /dev/null
+++ b/fusion-plating/docs/superpowers/plans/2026-04-12-phase1-configurator-sales.md
@@ -0,0 +1,1738 @@
+# Phase 1: Configurator & Sales Integration — 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:** Build the `fusion_plating_configurator` module with part catalog, coating configs, pricing engine, and sale order integration — making the Fusion Plating app the single hub for the plating business with Sales as the default landing page.
+
+**Architecture:** New Odoo 19 module (`fusion_plating_configurator`) with 7 models, extending `sale.order` with plating-specific fields. Role-based security groups layered on the existing 4-level privilege hierarchy. Menu restructured so Sales is the default view. 3D viewer and portal are deferred to Phase 1B/1C.
+
+**Tech Stack:** Odoo 19, Python 3.12, PostgreSQL, OWL (for future 3D viewer), SCSS
+
+**Spec:** `docs/superpowers/specs/2026-04-12-entech-plating-workflow-design.md`
+
+**Existing patterns:** Follow `fusion_plating` core module conventions (see `fp_process_type.py`, `fp_security.xml`, `fp_menu.xml`).
+
+---
+
+## File Structure
+
+```
+fusion_plating_configurator/
+├── __init__.py
+├── __manifest__.py
+├── models/
+│ ├── __init__.py
+│ ├── fp_treatment.py # Pre/post treatment (simplest model)
+│ ├── fp_part_catalog.py # Customer part library
+│ ├── fp_coating_config.py # Coating configuration templates
+│ ├── fp_pricing_rule.py # Pricing engine rules
+│ ├── fp_pricing_complexity_surcharge.py # Complexity surcharge lines
+│ ├── fp_quote_configurator.py # Configurator session + price calc
+│ └── sale_order.py # sale.order extensions (x_fc_* fields)
+├── security/
+│ ├── fp_configurator_security.xml # Role-based groups
+│ └── ir.model.access.csv # Model-level ACL
+├── views/
+│ ├── fp_treatment_views.xml
+│ ├── fp_part_catalog_views.xml
+│ ├── fp_coating_config_views.xml
+│ ├── fp_pricing_rule_views.xml
+│ ├── fp_quote_configurator_views.xml
+│ ├── sale_order_views.xml # Custom plating SO views
+│ └── fp_configurator_menu.xml # Menus under Fusion Plating app
+├── data/
+│ └── fp_configurator_sequence_data.xml # Sequences (CFG-XXXXX)
+└── static/
+ └── description/
+ └── icon.png # Module icon (copy from core)
+```
+
+**Existing files modified:**
+- `fusion_plating/views/fp_menu.xml` — restructure to add Sales submenu as default
+
+---
+
+## Task 1: Module Scaffold
+
+**Files:**
+- Create: `fusion_plating_configurator/__init__.py`
+- Create: `fusion_plating_configurator/__manifest__.py`
+- Create: `fusion_plating_configurator/models/__init__.py`
+- Create: `fusion_plating_configurator/security/fp_configurator_security.xml`
+- Create: `fusion_plating_configurator/security/ir.model.access.csv`
+- Create: `fusion_plating_configurator/data/fp_configurator_sequence_data.xml`
+- Copy: `fusion_plating/static/description/icon.png` → `fusion_plating_configurator/static/description/icon.png`
+
+- [ ] **Step 1: Create top-level `__init__.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 . import models
+```
+
+- [ ] **Step 2: Create `__manifest__.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.
+
+{
+ 'name': 'Fusion Plating — Configurator',
+ 'version': '19.0.1.0.0',
+ 'category': 'Manufacturing/Plating',
+ 'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
+ 'description': """
+Fusion Plating — Configurator
+==============================
+
+Part of the Fusion Plating product family by Nexa Systems Inc.
+
+Provides:
+- Customer part catalog with geometry and material data
+- Coating configuration templates (process, thickness, spec)
+- Pre/post treatment library
+- Formula-based pricing engine with complexity surcharges
+- Configurator sessions that generate sale orders
+- Custom sale order views with plating-specific fields
+""",
+ 'author': 'Nexa Systems Inc.',
+ 'website': 'https://www.nexasystems.ca',
+ 'maintainer': 'Nexa Systems Inc.',
+ 'support': 'support@nexasystems.ca',
+ 'license': 'OPL-1',
+ 'price': 0.00,
+ 'currency': 'CAD',
+ 'depends': [
+ 'fusion_plating',
+ 'sale_management',
+ ],
+ 'data': [
+ 'security/fp_configurator_security.xml',
+ 'security/ir.model.access.csv',
+ 'data/fp_configurator_sequence_data.xml',
+ 'views/fp_treatment_views.xml',
+ 'views/fp_part_catalog_views.xml',
+ 'views/fp_coating_config_views.xml',
+ 'views/fp_pricing_rule_views.xml',
+ 'views/fp_quote_configurator_views.xml',
+ 'views/sale_order_views.xml',
+ 'views/fp_configurator_menu.xml',
+ ],
+ 'installable': True,
+ 'application': False,
+ 'auto_install': False,
+}
+```
+
+- [ ] **Step 3: Create `models/__init__.py`** (empty initially, populated as models are added)
+
+```python
+# -*- coding: utf-8 -*-
+# Copyright 2026 Nexa Systems Inc.
+# License OPL-1 (Odoo Proprietary License v1.0)
+# Part of the Fusion Plating product family.
+```
+
+- [ ] **Step 4: Create security XML with role-based groups**
+
+File: `security/fp_configurator_security.xml`
+
+```xml
+
+
+
+
+
+
+
+
+
+ Estimator
+
+
+
+
+
+
+
+
+ Shop Manager
+
+
+
+
+
+
+```
+
+- [ ] **Step 5: Create empty `ir.model.access.csv`** (header only, rows added per model)
+
+```csv
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+```
+
+- [ ] **Step 6: Create sequence data file**
+
+File: `data/fp_configurator_sequence_data.xml`
+
+```xml
+
+
+
+
+
+ Fusion Plating: Configurator
+ fp.quote.configurator
+ CFG-
+ 5
+
+
+
+
+```
+
+- [ ] **Step 7: Copy icon file**
+
+```bash
+cp fusion_plating/static/description/icon.png fusion_plating_configurator/static/description/icon.png
+```
+
+- [ ] **Step 8: Create placeholder view/menu XML files** (empty `` root so manifest doesn't error on install)
+
+Create each of: `fp_treatment_views.xml`, `fp_part_catalog_views.xml`, `fp_coating_config_views.xml`, `fp_pricing_rule_views.xml`, `fp_quote_configurator_views.xml`, `sale_order_views.xml`, `fp_configurator_menu.xml` with:
+
+```xml
+
+
+```
+
+- [ ] **Step 9: Install the empty module to verify scaffold**
+
+```bash
+docker exec odoo-dev-app odoo -d fusion-dev -i fusion_plating_configurator --stop-after-init
+```
+
+Expected: installs with no errors. Module appears in Apps list.
+
+- [ ] **Step 10: Commit**
+
+```bash
+git add fusion_plating_configurator/
+git commit -m "feat(configurator): module scaffold with security groups and sequences"
+```
+
+---
+
+## Task 2: `fp.treatment` Model + Views
+
+The simplest model — establishes the pattern for all others.
+
+**Files:**
+- Create: `fusion_plating_configurator/models/fp_treatment.py`
+- Modify: `fusion_plating_configurator/models/__init__.py`
+- Modify: `fusion_plating_configurator/security/ir.model.access.csv`
+- Modify: `fusion_plating_configurator/views/fp_treatment_views.xml`
+
+- [ ] **Step 1: Create `fp_treatment.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 fields, models
+
+
+class FpTreatment(models.Model):
+ """Pre- or post-treatment step (bead blast, zincate, bake, passivate, etc.).
+
+ Used by coating configurations to specify which preparation and
+ finishing steps are required for a given process.
+ """
+ _name = 'fp.treatment'
+ _description = 'Fusion Plating — Treatment'
+ _order = 'treatment_type, sequence, name'
+
+ name = fields.Char(
+ string='Treatment',
+ required=True,
+ help='e.g. "Bead Blast", "Zincate", "Hydrogen Embrittlement Bake"',
+ )
+ treatment_type = fields.Selection(
+ [('pre', 'Pre-Treatment'), ('post', 'Post-Treatment')],
+ string='Type',
+ required=True,
+ default='pre',
+ )
+ sequence = fields.Integer(string='Sequence', default=10)
+ default_duration_minutes = fields.Float(
+ string='Default Duration (min)',
+ help='Estimated duration per application in minutes.',
+ )
+ currency_id = fields.Many2one(
+ 'res.currency',
+ string='Currency',
+ default=lambda self: self.env.company.currency_id,
+ )
+ default_cost = fields.Monetary(
+ string='Default Cost',
+ currency_field='currency_id',
+ help='Default cost per application. Can be overridden on pricing rules.',
+ )
+ description = fields.Text(string='Description')
+ active = fields.Boolean(string='Active', default=True)
+
+ _sql_constraints = [
+ ('fp_treatment_name_type_uniq', 'unique(name, treatment_type)',
+ 'Treatment name must be unique per type.'),
+ ]
+```
+
+- [ ] **Step 2: Update `models/__init__.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 . import fp_treatment
+```
+
+- [ ] **Step 3: Add ACL rows to `ir.model.access.csv`**
+
+Append to the CSV:
+
+```csv
+access_fp_treatment_operator,fp.treatment.operator,model_fp_treatment,fusion_plating.group_fusion_plating_operator,1,0,0,0
+access_fp_treatment_supervisor,fp.treatment.supervisor,model_fp_treatment,fusion_plating.group_fusion_plating_supervisor,1,1,0,0
+access_fp_treatment_manager,fp.treatment.manager,model_fp_treatment,fusion_plating.group_fusion_plating_manager,1,1,1,1
+```
+
+- [ ] **Step 4: Create `fp_treatment_views.xml`**
+
+```xml
+
+
+
+
+
+
+ fp.treatment.list
+ fp.treatment
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ fp.treatment.form
+ fp.treatment
+
+
+
+
+
+
+
+ fp.treatment.search
+ fp.treatment
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Treatments
+ fp.treatment
+ list,form
+
+
+
+ No treatments defined yet
+
+
+ Add pre-treatment steps (bead blast, zincate, acid etch) and
+ post-treatment steps (bake, passivate, chromate seal).
+
+
+
+
+
+```
+
+- [ ] **Step 5: Upgrade module and verify**
+
+```bash
+docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init
+```
+
+Expected: no errors. Treatment model accessible via Python shell.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add fusion_plating_configurator/
+git commit -m "feat(configurator): fp.treatment model with views and ACL"
+```
+
+---
+
+## Task 3: `fp.part.catalog` Model + Views
+
+**Files:**
+- Create: `fusion_plating_configurator/models/fp_part_catalog.py`
+- Modify: `fusion_plating_configurator/models/__init__.py` — add `from . import fp_part_catalog`
+- Modify: `fusion_plating_configurator/security/ir.model.access.csv` — add 3 rows
+- Modify: `fusion_plating_configurator/views/fp_part_catalog_views.xml`
+
+- [ ] **Step 1: Create `fp_part_catalog.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 fields, models
+
+
+class FpPartCatalog(models.Model):
+ """Customer part library.
+
+ Stores geometry, material, and complexity data for parts that
+ customers send repeatedly. New orders reference existing catalog
+ entries for instant re-quoting; one-off parts create new entries.
+ """
+ _name = 'fp.part.catalog'
+ _description = 'Fusion Plating — Part Catalog'
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+ _order = 'partner_id, part_number, name'
+
+ name = fields.Char(
+ string='Part Name',
+ required=True,
+ tracking=True,
+ help='Descriptive name for this part.',
+ )
+ partner_id = fields.Many2one(
+ 'res.partner',
+ string='Customer',
+ required=True,
+ ondelete='cascade',
+ tracking=True,
+ domain="[('customer_rank', '>', 0)]",
+ )
+ part_number = fields.Char(
+ string='Part Number',
+ tracking=True,
+ help="Customer's part number (e.g. VS-R392007E01).",
+ )
+ revision = fields.Char(
+ string='Revision',
+ help='Revision letter or number (e.g. Rev: 1B).',
+ )
+ substrate_material = fields.Selection(
+ [
+ ('aluminium', 'Aluminium'),
+ ('steel', 'Steel'),
+ ('stainless', 'Stainless Steel'),
+ ('copper', 'Copper'),
+ ('titanium', 'Titanium'),
+ ('other', 'Other'),
+ ],
+ string='Substrate Material',
+ default='steel',
+ )
+ geometry_source = fields.Selection(
+ [
+ ('3d_model', '3D Model'),
+ ('manual', 'Manual Measurements'),
+ ('pdf_drawing', 'PDF Drawing'),
+ ],
+ string='Geometry Source',
+ default='manual',
+ )
+
+ # ----- File attachments -------------------------------------------------
+ model_attachment_id = fields.Many2one(
+ 'ir.attachment',
+ string='3D Model File',
+ help='STEP, STL, or IGES file.',
+ )
+ drawing_attachment_ids = fields.Many2many(
+ 'ir.attachment',
+ 'fp_part_catalog_drawing_rel',
+ 'part_catalog_id',
+ 'attachment_id',
+ string='PDF Drawings',
+ )
+
+ # ----- Geometry measurements --------------------------------------------
+ surface_area = fields.Float(
+ string='Surface Area',
+ digits=(12, 4),
+ help='Total surface area to be plated.',
+ )
+ surface_area_uom = fields.Selection(
+ [
+ ('sq_in', 'sq in'),
+ ('sq_ft', 'sq ft'),
+ ('sq_cm', 'sq cm'),
+ ('sq_m', 'sq m'),
+ ],
+ string='Surface Area UoM',
+ default='sq_in',
+ )
+ weight = fields.Float(
+ string='Weight (kg)',
+ digits=(12, 4),
+ help='Part weight for shipping cost calculation.',
+ )
+ dimensions_length = fields.Float(string='Length', digits=(12, 4))
+ dimensions_width = fields.Float(string='Width', digits=(12, 4))
+ dimensions_height = fields.Float(string='Height', digits=(12, 4))
+
+ # ----- Complexity -------------------------------------------------------
+ complexity = fields.Selection(
+ [
+ ('simple', 'Simple'),
+ ('moderate', 'Moderate'),
+ ('complex', 'Complex'),
+ ('very_complex', 'Very Complex'),
+ ],
+ string='Complexity',
+ default='simple',
+ )
+ masking_zones = fields.Integer(
+ string='Masking Zones',
+ help='Number of areas requiring masking (not plated).',
+ )
+ masking_description = fields.Text(
+ string='Masking Description',
+ help='e.g. "Mask threaded holes, mask bore ID"',
+ )
+ has_blind_holes = fields.Boolean(string='Has Blind Holes')
+ has_recesses = fields.Boolean(string='Has Recesses')
+ has_threads = fields.Boolean(string='Has Threads')
+
+ notes = fields.Html(string='Notes')
+ active = fields.Boolean(string='Active', default=True)
+
+ _sql_constraints = [
+ ('fp_part_catalog_partner_partnum_uniq',
+ 'unique(partner_id, part_number)',
+ 'Part number must be unique per customer.'),
+ ]
+```
+
+- [ ] **Step 2: Update `models/__init__.py`** — add `from . import fp_part_catalog`
+
+- [ ] **Step 3: Add ACL rows** — same pattern as treatment (operator=read, supervisor=read+write, manager=full)
+
+```csv
+access_fp_part_catalog_operator,fp.part.catalog.operator,model_fp_part_catalog,fusion_plating.group_fusion_plating_operator,1,0,0,0
+access_fp_part_catalog_estimator,fp.part.catalog.estimator,model_fp_part_catalog,group_fp_estimator,1,1,1,0
+access_fp_part_catalog_manager,fp.part.catalog.manager,model_fp_part_catalog,fusion_plating.group_fusion_plating_manager,1,1,1,1
+```
+
+- [ ] **Step 4: Create `fp_part_catalog_views.xml`** — list (columns: Customer, Part#, Rev, Material, Surface Area, Complexity), form (title block, geometry tab, complexity tab, attachments tab, chatter), search (filter by customer, material, complexity, group by customer)
+
+Follow the exact patterns from Task 2. Key additions:
+- List has `decoration-muted="not active"`
+- Form has `
` at bottom
+- Attachment fields use `widget="many2many_binary"` for drawing_attachment_ids
+
+- [ ] **Step 5: Upgrade and verify**
+
+```bash
+docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_configurator --stop-after-init
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add fusion_plating_configurator/
+git commit -m "feat(configurator): fp.part.catalog model — customer part library"
+```
+
+---
+
+## Task 4: `fp.coating.config` Model + Views
+
+**Files:**
+- Create: `fusion_plating_configurator/models/fp_coating_config.py`
+- Modify: `models/__init__.py`, `ir.model.access.csv`, `fp_coating_config_views.xml`
+
+- [ ] **Step 1: Create `fp_coating_config.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 fields, models
+
+
+class FpCoatingConfig(models.Model):
+ """Coating configuration template.
+
+ Defines a specific coating setup: process type, phosphorus level,
+ thickness range, spec reference, and required pre/post treatments.
+ Used by the configurator to drive pricing and recipe selection.
+ """
+ _name = 'fp.coating.config'
+ _description = 'Fusion Plating — Coating Configuration'
+ _order = 'sequence, name'
+
+ name = fields.Char(
+ string='Configuration',
+ required=True,
+ help='e.g. "EN Mid-Phos AMS 2404"',
+ )
+ process_type_id = fields.Many2one(
+ 'fusion.plating.process.type',
+ string='Process Type',
+ required=True,
+ ondelete='restrict',
+ )
+ recipe_id = fields.Many2one(
+ 'fusion.plating.process.node',
+ string='Default Recipe',
+ domain="[('node_type', '=', 'recipe')]",
+ help='Default recipe template for this coating configuration.',
+ )
+ 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.',
+ )
+ thickness_min = fields.Float(
+ string='Min Thickness',
+ digits=(10, 4),
+ help='Minimum coating thickness.',
+ )
+ thickness_max = fields.Float(
+ string='Max Thickness',
+ digits=(10, 4),
+ help='Maximum coating thickness.',
+ )
+ thickness_uom = fields.Selection(
+ [('mils', 'mils'), ('microns', 'microns'), ('inches', 'inches')],
+ string='Thickness UoM',
+ default='mils',
+ )
+ spec_reference = fields.Char(
+ string='Spec Reference',
+ help='e.g. "AMS 2404", "E499-303-00-005"',
+ )
+ certification_level = fields.Selection(
+ [
+ ('commercial', 'Commercial'),
+ ('mil_spec', 'Mil-Spec'),
+ ('nadcap', 'Nadcap'),
+ ('nuclear', 'Nuclear (CSA N299)'),
+ ],
+ string='Certification Level',
+ default='commercial',
+ )
+ pre_treatment_ids = fields.Many2many(
+ 'fp.treatment',
+ 'fp_coating_config_pre_treatment_rel',
+ 'config_id',
+ 'treatment_id',
+ string='Pre-Treatments',
+ domain="[('treatment_type', '=', 'pre')]",
+ )
+ post_treatment_ids = fields.Many2many(
+ 'fp.treatment',
+ 'fp_coating_config_post_treatment_rel',
+ 'config_id',
+ 'treatment_id',
+ string='Post-Treatments',
+ domain="[('treatment_type', '=', 'post')]",
+ )
+ sequence = fields.Integer(string='Sequence', default=10)
+ description = fields.Text(string='Description')
+ active = fields.Boolean(string='Active', default=True)
+```
+
+- [ ] **Step 2: Update `__init__.py`**, add ACL rows, create views XML
+
+Views: list (Name, Process, Phos Level, Thickness Range, Spec, Cert Level), form (title, two-column groups, notebook with treatments tab + description tab), search (filter by process type, cert level, phos level, group by process type).
+
+- [ ] **Step 3: Upgrade and verify**
+
+- [ ] **Step 4: Commit**
+
+```bash
+git commit -m "feat(configurator): fp.coating.config — coating configuration templates"
+```
+
+---
+
+## Task 5: `fp.pricing.rule` + `fp.pricing.complexity.surcharge` Models + Views
+
+**Files:**
+- Create: `fusion_plating_configurator/models/fp_pricing_rule.py`
+- Create: `fusion_plating_configurator/models/fp_pricing_complexity_surcharge.py`
+- Modify: `models/__init__.py`, `ir.model.access.csv`, `fp_pricing_rule_views.xml`
+
+- [ ] **Step 1: Create `fp_pricing_complexity_surcharge.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 fields, models
+
+
+class FpPricingComplexitySurcharge(models.Model):
+ """Complexity-based surcharge line on a pricing rule."""
+ _name = 'fp.pricing.complexity.surcharge'
+ _description = 'Fusion Plating — Pricing Complexity Surcharge'
+ _order = 'complexity'
+
+ rule_id = fields.Many2one(
+ 'fp.pricing.rule',
+ string='Pricing Rule',
+ required=True,
+ ondelete='cascade',
+ )
+ complexity = fields.Selection(
+ [
+ ('simple', 'Simple'),
+ ('moderate', 'Moderate'),
+ ('complex', 'Complex'),
+ ('very_complex', 'Very Complex'),
+ ],
+ string='Complexity',
+ required=True,
+ )
+ surcharge_percent = fields.Float(
+ string='Surcharge %',
+ help='Additional percentage on top of base price for this complexity level.',
+ )
+
+ _sql_constraints = [
+ ('fp_pricing_surcharge_rule_complexity_uniq',
+ 'unique(rule_id, complexity)',
+ 'Only one surcharge per complexity level per rule.'),
+ ]
+```
+
+- [ ] **Step 2: Create `fp_pricing_rule.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 fields, models
+
+
+class FpPricingRule(models.Model):
+ """Formula-based pricing rule.
+
+ Rules are matched by coating config, substrate material, and
+ certification level. The first matching rule (by sequence) wins.
+ Global rules (no filters set) act as fallbacks.
+ """
+ _name = 'fp.pricing.rule'
+ _description = 'Fusion Plating — Pricing Rule'
+ _order = 'sequence, id'
+
+ name = fields.Char(
+ string='Rule Name',
+ required=True,
+ help='Descriptive name for this pricing rule.',
+ )
+ coating_config_id = fields.Many2one(
+ 'fp.coating.config',
+ string='Coating Config',
+ help='Leave blank for a global rule that applies to all coatings.',
+ )
+ substrate_material = fields.Selection(
+ [
+ ('aluminium', 'Aluminium'),
+ ('steel', 'Steel'),
+ ('stainless', 'Stainless Steel'),
+ ('copper', 'Copper'),
+ ('titanium', 'Titanium'),
+ ('other', 'Other'),
+ ],
+ string='Substrate Material',
+ help='Leave blank to match all materials.',
+ )
+ certification_level = fields.Selection(
+ [
+ ('commercial', 'Commercial'),
+ ('mil_spec', 'Mil-Spec'),
+ ('nadcap', 'Nadcap'),
+ ('nuclear', 'Nuclear (CSA N299)'),
+ ],
+ string='Certification Level',
+ help='Leave blank to match all levels.',
+ )
+ pricing_method = fields.Selection(
+ [
+ ('per_sqin', 'Per Square Inch'),
+ ('per_sqft', 'Per Square Foot'),
+ ('per_piece', 'Per Piece'),
+ ('flat_rate', 'Flat Rate'),
+ ],
+ string='Pricing Method',
+ required=True,
+ default='per_sqin',
+ )
+ currency_id = fields.Many2one(
+ 'res.currency',
+ string='Currency',
+ default=lambda self: self.env.company.currency_id,
+ )
+ base_rate = fields.Monetary(
+ string='Base Rate',
+ currency_field='currency_id',
+ help='Price per unit (sq in, sq ft, piece, or flat).',
+ )
+ thickness_factor = fields.Float(
+ string='Thickness Factor',
+ default=1.0,
+ help='Multiplier per mil of coating thickness. 1.0 = no adjustment.',
+ )
+ complexity_surcharge_ids = fields.One2many(
+ 'fp.pricing.complexity.surcharge',
+ 'rule_id',
+ string='Complexity Surcharges',
+ )
+ masking_rate_per_zone = fields.Monetary(
+ string='Masking Rate / Zone',
+ currency_field='currency_id',
+ help='Additional charge per masking zone.',
+ )
+ setup_fee = fields.Monetary(
+ string='Setup Fee',
+ currency_field='currency_id',
+ help='One-time setup fee per batch.',
+ )
+ minimum_charge = fields.Monetary(
+ string='Minimum Charge',
+ currency_field='currency_id',
+ help='Floor price — quote will not go below this.',
+ )
+ rush_surcharge_percent = fields.Float(
+ string='Rush Surcharge %',
+ help='Premium percentage for rush orders.',
+ )
+ sequence = fields.Integer(string='Sequence', default=10)
+ active = fields.Boolean(string='Active', default=True)
+ notes = fields.Text(string='Notes')
+```
+
+- [ ] **Step 3: Update `__init__.py`**, add ACL rows for both models
+
+- [ ] **Step 4: Create `fp_pricing_rule_views.xml`** — list (Seq, Name, Coating, Substrate, Cert, Method, Base Rate, Min Charge), form (filters group, pricing group, surcharges inline list, notes), search (filter by method, coating, group by coating)
+
+- [ ] **Step 5: Upgrade and verify**
+
+- [ ] **Step 6: Commit**
+
+```bash
+git commit -m "feat(configurator): fp.pricing.rule — formula-based pricing engine"
+```
+
+---
+
+## Task 6: `sale.order` Extensions
+
+**Files:**
+- Create: `fusion_plating_configurator/models/sale_order.py`
+- Modify: `models/__init__.py`
+- Modify: `views/sale_order_views.xml`
+
+- [ ] **Step 1: Create `sale_order.py`** — extends sale.order with x_fc_* fields
+
+```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 SaleOrder(models.Model):
+ _inherit = 'sale.order'
+
+ # ----- Configurator link ------------------------------------------------
+ x_fc_configurator_id = fields.Many2one(
+ 'fp.quote.configurator',
+ string='Configurator',
+ copy=False,
+ )
+ x_fc_part_catalog_id = fields.Many2one(
+ 'fp.part.catalog',
+ string='Part',
+ )
+ x_fc_coating_config_id = fields.Many2one(
+ 'fp.coating.config',
+ string='Coating Configuration',
+ )
+
+ # ----- PO tracking ------------------------------------------------------
+ x_fc_po_number = fields.Char(
+ string='Customer PO #',
+ tracking=True,
+ )
+ x_fc_po_attachment_id = fields.Many2one(
+ 'ir.attachment',
+ string='PO Document',
+ )
+ x_fc_po_received = fields.Boolean(
+ string='PO Received',
+ tracking=True,
+ )
+ x_fc_po_override = fields.Boolean(
+ string='PO Override',
+ help='Manager override — proceed without formal PO (handshake deal).',
+ )
+ x_fc_po_override_reason = fields.Text(
+ string='Override Reason',
+ )
+
+ # ----- Invoice strategy -------------------------------------------------
+ x_fc_invoice_strategy = fields.Selection(
+ [
+ ('deposit', 'Deposit'),
+ ('progress', 'Progress Billing'),
+ ('net_terms', 'Net Terms'),
+ ('cod_prepay', 'COD / Prepay'),
+ ],
+ string='Invoice Strategy',
+ tracking=True,
+ )
+ x_fc_deposit_percent = fields.Float(
+ string='Deposit %',
+ help='Deposit percentage if strategy is Deposit.',
+ )
+
+ # ----- Job details ------------------------------------------------------
+ x_fc_rush_order = fields.Boolean(
+ string='Rush Order',
+ tracking=True,
+ )
+ x_fc_delivery_method = fields.Selection(
+ [
+ ('local_delivery', 'Local Delivery'),
+ ('shipping_partner', 'Shipping Partner'),
+ ('customer_pickup', 'Customer Pickup'),
+ ],
+ string='Delivery Method',
+ tracking=True,
+ )
+ x_fc_receiving_status = fields.Selection(
+ [
+ ('not_received', 'Not Received'),
+ ('partial', 'Partial'),
+ ('received', 'Received'),
+ ('inspected', 'Inspected'),
+ ],
+ string='Receiving Status',
+ default='not_received',
+ tracking=True,
+ )
+```
+
+- [ ] **Step 2: Create `sale_order_views.xml`** — inherit sale.order form to add a "Plating" notebook tab with the x_fc_* fields, inherit list view to add key columns
+
+```xml
+
+
+
+
+
+ sale.order.form.fp.configurator
+ sale.order
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ sale.order.list.fp
+ sale.order
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Quotations
+ sale.order
+ list,form,kanban
+ [('state', 'in', ('draft', 'sent'))]
+
+ {'default_x_fc_delivery_method': 'shipping_partner'}
+
+
+ Create a new quotation
+
+
+
+
+
+
+ Sale Orders
+ sale.order
+ list,form,kanban
+ [('state', 'in', ('sale', 'done'))]
+
+
+
+
+```
+
+- [ ] **Step 3: Upgrade and verify** — open an SO, confirm Plating tab appears
+
+- [ ] **Step 4: Commit**
+
+```bash
+git commit -m "feat(configurator): sale.order plating extensions + custom list/form views"
+```
+
+---
+
+## Task 7: `fp.quote.configurator` Model + Price Calculation
+
+**Files:**
+- Create: `fusion_plating_configurator/models/fp_quote_configurator.py`
+- Modify: `models/__init__.py`, `ir.model.access.csv`, `fp_quote_configurator_views.xml`
+
+- [ ] **Step 1: Create `fp_quote_configurator.py`** — the core model with `_compute_price()` and `action_create_quotation()`
+
+```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, _
+from odoo.exceptions import UserError
+
+
+class FpQuoteConfigurator(models.Model):
+ """Persistent configurator session.
+
+ Collects part geometry, coating config, and pricing inputs.
+ Calculates a price from matching pricing rules. The estimator
+ can override the calculated price. Creates a sale.order when confirmed.
+ """
+ _name = 'fp.quote.configurator'
+ _description = 'Fusion Plating — Quote Configurator'
+ _inherit = ['mail.thread']
+ _order = 'create_date desc'
+
+ name = fields.Char(
+ string='Reference',
+ readonly=True,
+ copy=False,
+ default='New',
+ )
+ state = fields.Selection(
+ [('draft', 'Draft'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled')],
+ string='Status',
+ default='draft',
+ tracking=True,
+ )
+ partner_id = fields.Many2one(
+ 'res.partner',
+ string='Customer',
+ required=True,
+ domain="[('customer_rank', '>', 0)]",
+ )
+ part_catalog_id = fields.Many2one(
+ 'fp.part.catalog',
+ string='Part (Catalog)',
+ domain="[('partner_id', '=', partner_id)]",
+ help='Select from this customer\'s part catalog, or leave blank for a one-off.',
+ )
+ coating_config_id = fields.Many2one(
+ 'fp.coating.config',
+ string='Coating Configuration',
+ required=True,
+ )
+ quantity = fields.Integer(string='Quantity', default=1, required=True)
+ batch_size = fields.Integer(
+ string='Batch Size',
+ help='Parts per rack or barrel load.',
+ )
+
+ # ----- Geometry (auto-filled from catalog or entered manually) ----------
+ surface_area = fields.Float(string='Surface Area', digits=(12, 4))
+ surface_area_uom = fields.Selection(
+ [('sq_in', 'sq in'), ('sq_ft', 'sq ft'), ('sq_cm', 'sq cm'), ('sq_m', 'sq m')],
+ string='Area UoM',
+ default='sq_in',
+ )
+ thickness_requested = fields.Float(string='Requested Thickness', digits=(10, 4))
+ masking_zones = fields.Integer(string='Masking Zones')
+ complexity = fields.Selection(
+ [('simple', 'Simple'), ('moderate', 'Moderate'),
+ ('complex', 'Complex'), ('very_complex', 'Very Complex')],
+ string='Complexity',
+ default='simple',
+ )
+ substrate_material = fields.Selection(
+ [('aluminium', 'Aluminium'), ('steel', 'Steel'), ('stainless', 'Stainless Steel'),
+ ('copper', 'Copper'), ('titanium', 'Titanium'), ('other', 'Other')],
+ string='Substrate',
+ default='steel',
+ )
+
+ # ----- Options ----------------------------------------------------------
+ rush_order = fields.Boolean(string='Rush Order')
+ turnaround_days = fields.Integer(string='Turnaround (days)')
+ delivery_method = fields.Selection(
+ [('local_delivery', 'Local Delivery'),
+ ('shipping_partner', 'Shipping Partner'),
+ ('customer_pickup', 'Customer Pickup')],
+ string='Delivery Method',
+ default='shipping_partner',
+ )
+
+ # ----- Pricing ----------------------------------------------------------
+ currency_id = fields.Many2one(
+ 'res.currency',
+ string='Currency',
+ default=lambda self: self.env.company.currency_id,
+ )
+ shipping_fee = fields.Monetary(string='Shipping Fee', currency_field='currency_id')
+ delivery_fee = fields.Monetary(string='Delivery Fee', currency_field='currency_id')
+ calculated_price = fields.Monetary(
+ string='Calculated Price',
+ currency_field='currency_id',
+ compute='_compute_price',
+ store=True,
+ )
+ price_breakdown_html = fields.Html(
+ string='Price Breakdown',
+ compute='_compute_price',
+ store=True,
+ )
+ estimator_override_price = fields.Monetary(
+ string='Final Price',
+ currency_field='currency_id',
+ help='Estimator can override the calculated price.',
+ )
+
+ # ----- SO link ----------------------------------------------------------
+ sale_order_id = fields.Many2one(
+ 'sale.order',
+ string='Sale Order',
+ readonly=True,
+ copy=False,
+ )
+ notes = fields.Text(string='Notes')
+
+ # -------------------------------------------------------------------------
+ # Auto-population from catalog
+ # -------------------------------------------------------------------------
+ @api.onchange('part_catalog_id')
+ def _onchange_part_catalog_id(self):
+ if self.part_catalog_id:
+ cat = self.part_catalog_id
+ self.surface_area = cat.surface_area
+ self.surface_area_uom = cat.surface_area_uom
+ self.complexity = cat.complexity
+ self.masking_zones = cat.masking_zones
+ self.substrate_material = cat.substrate_material
+
+ @api.onchange('coating_config_id')
+ def _onchange_coating_config_id(self):
+ if self.coating_config_id:
+ self.thickness_requested = self.coating_config_id.thickness_min
+
+ # -------------------------------------------------------------------------
+ # Price calculation
+ # -------------------------------------------------------------------------
+ @api.depends(
+ 'surface_area', 'surface_area_uom', 'thickness_requested',
+ 'masking_zones', 'complexity', 'substrate_material',
+ 'quantity', 'batch_size', 'rush_order',
+ 'shipping_fee', 'delivery_fee',
+ 'coating_config_id', 'coating_config_id.certification_level',
+ )
+ def _compute_price(self):
+ for rec in self:
+ if not rec.coating_config_id or not rec.surface_area:
+ rec.calculated_price = 0
+ rec.price_breakdown_html = ''
+ continue
+
+ rule = rec._find_matching_rule()
+ if not rule:
+ rec.calculated_price = 0
+ rec.price_breakdown_html = '
No matching pricing rule found.
'
+ continue
+
+ # --- Base calculation ---
+ area = rec._normalize_surface_area_to_sqin()
+ if rule.pricing_method == 'per_sqin':
+ unit_price = area * rule.base_rate
+ elif rule.pricing_method == 'per_sqft':
+ unit_price = (area / 144.0) * rule.base_rate
+ elif rule.pricing_method == 'per_piece':
+ unit_price = rule.base_rate
+ else: # flat_rate
+ unit_price = rule.base_rate
+
+ # --- Thickness factor ---
+ thickness = rec.thickness_requested or 1.0
+ unit_price *= (rule.thickness_factor * thickness) if rule.thickness_factor != 1.0 else 1.0
+
+ # --- Complexity surcharge ---
+ surcharge_pct = 0
+ for line in rule.complexity_surcharge_ids:
+ if line.complexity == rec.complexity:
+ surcharge_pct = line.surcharge_percent
+ break
+ unit_price *= (1 + surcharge_pct / 100.0)
+
+ # --- Masking ---
+ masking_cost = (rec.masking_zones or 0) * rule.masking_rate_per_zone
+
+ # --- Quantity ---
+ subtotal = (unit_price * rec.quantity) + masking_cost + rule.setup_fee
+
+ # --- Rush surcharge ---
+ rush_amount = 0
+ if rec.rush_order and rule.rush_surcharge_percent:
+ rush_amount = subtotal * (rule.rush_surcharge_percent / 100.0)
+ subtotal += rush_amount
+
+ # --- Minimum charge ---
+ if subtotal < rule.minimum_charge:
+ subtotal = rule.minimum_charge
+
+ # --- Delivery/shipping fees ---
+ total = subtotal + (rec.shipping_fee or 0) + (rec.delivery_fee or 0)
+
+ rec.calculated_price = total
+
+ # --- Build breakdown HTML ---
+ lines = [
+ f'
| Base ({rule.get_formfield_string("pricing_method")}) | '
+ f'${unit_price:,.2f} x {rec.quantity} |
',
+ ]
+ if masking_cost:
+ lines.append(f'
| Masking ({rec.masking_zones} zones) | '
+ f'${masking_cost:,.2f} |
')
+ if rule.setup_fee:
+ lines.append(f'
| Setup Fee | '
+ f'${rule.setup_fee:,.2f} |
')
+ if rush_amount:
+ lines.append(f'
| Rush Surcharge ({rule.rush_surcharge_percent}%) | '
+ f'${rush_amount:,.2f} |
')
+ if rec.shipping_fee:
+ lines.append(f'
| Shipping | '
+ f'${rec.shipping_fee:,.2f} |
')
+ if rec.delivery_fee:
+ lines.append(f'
| Delivery | '
+ f'${rec.delivery_fee:,.2f} |
')
+ lines.append(f'
| Total | '
+ f'${total:,.2f} |
')
+
+ rec.price_breakdown_html = (
+ f'
'
+ f'| Item | Amount |
'
+ f'{"".join(lines)}
'
+ f'
Rule: {rule.name} (seq {rule.sequence})
'
+ )
+
+ def _find_matching_rule(self):
+ """Find the first pricing rule matching this configurator's filters."""
+ domain = [('active', '=', True)]
+ # Build progressive filter — most specific first
+ rules = self.env['fp.pricing.rule'].search(domain, order='sequence, id')
+ cert_level = self.coating_config_id.certification_level if self.coating_config_id else False
+
+ best = None
+ best_score = -1
+ for rule in rules:
+ score = 0
+ # Check coating config filter
+ if rule.coating_config_id:
+ if rule.coating_config_id != self.coating_config_id:
+ continue
+ score += 4
+ # Check substrate filter
+ if rule.substrate_material:
+ if rule.substrate_material != self.substrate_material:
+ continue
+ score += 2
+ # Check certification filter
+ if rule.certification_level:
+ if rule.certification_level != cert_level:
+ continue
+ score += 1
+ # More specific rules preferred
+ if score > best_score:
+ best_score = score
+ best = rule
+ return best
+
+ def _normalize_surface_area_to_sqin(self):
+ """Convert surface area to square inches for calculation."""
+ area = self.surface_area or 0
+ uom = self.surface_area_uom
+ if uom == 'sq_ft':
+ return area * 144.0
+ elif uom == 'sq_cm':
+ return area * 0.155
+ elif uom == 'sq_m':
+ return area * 1550.0
+ return area # sq_in
+
+ # -------------------------------------------------------------------------
+ # Actions
+ # -------------------------------------------------------------------------
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if vals.get('name', 'New') == 'New':
+ vals['name'] = self.env['ir.sequence'].next_by_code(
+ 'fp.quote.configurator') or 'New'
+ return super().create(vals_list)
+
+ def action_create_quotation(self):
+ """Create a sale.order from this configurator session."""
+ self.ensure_one()
+ if self.state != 'draft':
+ raise UserError(_('Only draft configurators can create quotations.'))
+ if self.sale_order_id:
+ raise UserError(_('A quotation has already been created for this configurator.'))
+
+ price = self.estimator_override_price or self.calculated_price
+
+ # Find or create a generic service product for plating
+ product = self.env.ref(
+ 'fusion_plating_configurator.product_plating_service', raise_if_not_found=False
+ )
+ if not product:
+ product = self.env['product.product'].search(
+ [('default_code', '=', 'FP-SERVICE')], limit=1
+ )
+ if not product:
+ product = self.env['product.product'].create({
+ 'name': 'Plating Service',
+ 'default_code': 'FP-SERVICE',
+ 'type': 'service',
+ 'list_price': 0,
+ 'sale_ok': True,
+ 'purchase_ok': False,
+ })
+
+ so_vals = {
+ 'partner_id': self.partner_id.id,
+ 'x_fc_configurator_id': self.id,
+ 'x_fc_part_catalog_id': self.part_catalog_id.id if self.part_catalog_id else False,
+ 'x_fc_coating_config_id': self.coating_config_id.id,
+ 'x_fc_rush_order': self.rush_order,
+ 'x_fc_delivery_method': self.delivery_method,
+ 'origin': self.name,
+ 'order_line': [(0, 0, {
+ 'product_id': product.id,
+ 'name': (f'{self.coating_config_id.name} — '
+ f'{self.part_catalog_id.name or "Custom Part"} '
+ f'(x{self.quantity})'),
+ 'product_uom_qty': self.quantity,
+ 'price_unit': price / self.quantity if self.quantity else price,
+ })],
+ }
+ so = self.env['sale.order'].create(so_vals)
+ self.write({
+ 'sale_order_id': so.id,
+ 'state': 'confirmed',
+ })
+ self.message_post(
+ body=_('Sale Order
%s created.') % (so.id, so.name),
+ )
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sale.order',
+ 'res_id': so.id,
+ 'view_mode': 'form',
+ 'target': 'current',
+ }
+
+ def action_cancel(self):
+ self.write({'state': 'cancelled'})
+```
+
+- [ ] **Step 2: Add ACL rows, update `__init__.py`**
+
+- [ ] **Step 3: Create `fp_quote_configurator_views.xml`** — form with header buttons (Create Quotation / Cancel), partner + catalog fields, coating config, geometry group, pricing group with breakdown, notes
+
+- [ ] **Step 4: Upgrade and test** — create a pricing rule, then a configurator, verify price calculates
+
+- [ ] **Step 5: Commit**
+
+```bash
+git commit -m "feat(configurator): fp.quote.configurator — pricing engine + SO creation"
+```
+
+---
+
+## Task 8: Menu Restructure — Sales as Default Landing
+
+**Files:**
+- Modify: `fusion_plating_configurator/views/fp_configurator_menu.xml`
+
+- [ ] **Step 1: Create the full menu XML**
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Customers
+ res.partner
+ list,form,kanban
+ [('customer_rank', '>', 0)]
+ {'default_customer_rank': 1}
+
+
+
+```
+
+- [ ] **Step 2: Update Fusion Plating root menu default action**
+
+Modify `fusion_plating/views/fp_menu.xml` to set the root menu's `action` to `fusion_plating_configurator.action_fp_quotations` so Quotations is the default landing when opening Fusion Plating app. This requires the configurator module to be installed — add a conditional or move the action reference.
+
+Alternatively: set `sequence="1"` on the Sales submenu so it opens first when clicking "Plating" in the app drawer. Odoo opens the first visible menu item's action automatically.
+
+- [ ] **Step 3: Upgrade and verify** — click Plating app → should land on Quotations list
+
+- [ ] **Step 4: Commit**
+
+```bash
+git commit -m "feat(configurator): menu restructure — Sales as default landing in Fusion Plating"
+```
+
+---
+
+## Task 9: Seed Data — Default Treatments
+
+**Files:**
+- Create: `fusion_plating_configurator/data/fp_treatment_data.xml`
+- Modify: `__manifest__.py` — add to `data` list
+
+- [ ] **Step 1: Create seed data for common treatments**
+
+```xml
+
+
+
+
+
+ Alkaline Clean
+ pre
+ 10
+ 15
+
+
+ Acid Etch
+ pre
+ 20
+ 10
+
+
+ Zincate (Aluminium)
+ pre
+ 30
+ 5
+
+
+ Bead Blast
+ pre
+ 40
+ 20
+
+
+ Solvent Degrease
+ pre
+ 50
+ 10
+
+
+
+
+ Hydrogen Embrittlement Bake
+ post
+ 10
+ 240
+
+
+ Passivate
+ post
+ 20
+ 30
+
+
+ Chromate Seal
+ post
+ 30
+ 15
+
+
+
+```
+
+- [ ] **Step 2: Upgrade and verify** — treatments appear in the list
+
+- [ ] **Step 3: Commit**
+
+```bash
+git commit -m "feat(configurator): seed data — common pre/post treatments"
+```
+
+---
+
+## Task 10: Integration Test — Full Flow
+
+- [ ] **Step 1: Manual end-to-end test**
+
+1. Open Plating app → should land on Quotations
+2. Navigate to Configurator → Treatments → verify seed data loaded
+3. Create a Coating Configuration (EN Mid-Phos AMS 2404, link to EN process type)
+4. Create a Pricing Rule (per_sqin, $0.05/sqin, setup fee $50, rush 25%)
+5. Navigate to Configurator → New Quote
+6. Select customer, select coating config, enter surface area = 100 sqin, qty = 50
+7. Verify calculated price = (100 * 0.05 * 50) + 50 = $300
+8. Click "Create Quotation" → verify SO created with Plating tab populated
+9. Open SO → confirm x_fc_* fields are populated
+
+- [ ] **Step 2: Verify permissions**
+
+1. Log in as Operator user → should NOT see Sales or Configurator menus
+2. Log in as user with Estimator role → should see Sales and Configurator
+3. Log in as Shop Manager → should see everything
+
+- [ ] **Step 3: Final commit**
+
+```bash
+git add -A
+git commit -m "feat(configurator): Phase 1 complete — configurator + sales integration"
+```
+
+---
+
+## Summary
+
+| Task | What | Files | Estimated |
+|------|------|-------|-----------|
+| 1 | Module scaffold | 8 new files | Quick |
+| 2 | fp.treatment model + views | 4 files | Quick |
+| 3 | fp.part.catalog model + views | 4 files | Medium |
+| 4 | fp.coating.config model + views | 4 files | Medium |
+| 5 | fp.pricing.rule + surcharge | 5 files | Medium |
+| 6 | sale.order extensions + views | 3 files | Medium |
+| 7 | fp.quote.configurator + price calc | 4 files | Large |
+| 8 | Menu restructure | 2 files | Quick |
+| 9 | Seed data | 2 files | Quick |
+| 10 | Integration test | 0 files | Manual |