docs: address spec review findings — 5 critical, 8 major issues fixed

- Add model naming convention table (fp.* for new, fusion.plating.* for existing)
- Add fusion_plating_certificates as dedicated module with fp.thickness.reading model
- Fix complexity_surcharge: companion model instead of JSON text field
- Add recipe_id domain constraint [('node_type', '=', 'recipe')]
- Align security groups with existing 4-level privilege hierarchy
- Add currency_id to all monetary models
- Clarify fp.quote.configurator as persistent model with state lifecycle
- Fix canonical model names (fusion.plating.portal.job, fusion.plating.delivery)
- Add auto-population rules for invoice strategy and configurator defaults
- Lighten bridge_mrp deps: gates as mixins in receiving/invoicing modules
- Add deployment strategy for fusion_tasks (same server, not standalone)
- Add data migration section for existing quote request coexistence
- Add work centre mapping note (fusion.plating.work.center ↔ mrp.workcenter)
- Change x_fc_account_hold_date to Datetime for audit precision
- Add bilingual CoC implementation note (QWeb, not ir.translation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-12 18:05:42 -04:00
parent f69b3ac855
commit d424dfdb19

View File

@@ -11,6 +11,17 @@
Complete end-to-end ERP workflow for an electroless nickel plating shop, replacing Steelhead Software. The system covers customer inquiry through invoicing, with a quotation configurator, parts receiving/inspection, flexible invoicing strategies, automated email notifications, certificate management, and local delivery dispatch.
### Model Naming Convention
| Context | Convention | Example |
|---------|-----------|---------|
| New custom models (all new modules) | `fp.*` prefix | `fp.part.catalog`, `fp.certificate` |
| Existing custom models (already in codebase) | Keep `fusion.plating.*` | `fusion.plating.portal.job`, `fusion.plating.delivery` |
| New fields on standard Odoo models | `x_fc_*` prefix | `x_fc_po_number` on `sale.order` |
| Legacy fields (from Studio era) | `x_studio_*` | Preserved, not renamed |
The `fp.*` prefix is the new short-form convention for all models created in the new modules. Existing `fusion.plating.*` models are NOT renamed — they keep their current `_name`. All references in this spec use canonical `_name` values.
### Architecture: Approach 2 — Dedicated Modules per Sub-System
| Module | Purpose | Location |
@@ -19,10 +30,11 @@ Complete end-to-end ERP workflow for an electroless nickel plating shop, replaci
| `fusion_plating_receiving` | Parts receiving, inspection, damage logging, PO matching | `fusion-plating/` |
| `fusion_plating_invoicing` | Invoice strategy engine (deposit/progress/net/COD), account holds | `fusion-plating/` |
| `fusion_plating_notifications` | Auto-email engine, notification templates, audit log | `fusion-plating/` |
| `fusion_plating_certificates` | Certificate registry (CoC, thickness, Nadcap, customer-specific) | `fusion-plating/` |
| `fusion_tasks` | Local delivery dispatch (forked from Westin, stripped of claims) | `Entech Plating/` |
Plus updates to existing modules:
- `fusion_plating_bridge_mrp` — Recipe-to-WO generation, manufacturing gates
- `fusion_plating_bridge_mrp` — Recipe-to-WO generation (lightweight; gates live in their own modules)
- `fusion_plating_portal` — Customer-facing configurator UI, 3D preview
- `fusion_plating_reports` — CoC template updates, thickness report template
- `fusion_plating_quality` — Thickness reading model for Fischerscope data
@@ -43,7 +55,7 @@ Fusion Plating (app)
│ ├── Customers (res.partner, customer_rank > 0)
│ └── Part Catalog (fp.part.catalog)
├── Configurator
│ ├── New Quote (fp.quote.configurator wizard)
│ ├── New Quote (fp.quote.configurator, persistent model)
│ ├── Coating Configurations (fp.coating.config)
│ └── Pricing Rules (fp.pricing.rule)
├── Manufacturing
@@ -90,17 +102,21 @@ Fusion Plating (app)
### Permission-Based Visibility
| Group | Sees |
|-------|------|
| `fp_group_estimator` | Sales, Configurator, Customers, Part Catalog |
| `fp_group_shop_manager` | Everything (full menu) |
| `fp_group_shop_floor` | Manufacturing, Work Orders, Plant Overview only |
| `fp_group_receiving` | Receiving & Inspection, can view Sales (read-only) |
| `fp_group_shipping` | Shipping & Delivery, can view Sales (read-only) |
| `fp_group_quality` | Quality, can view Manufacturing |
| `fp_group_accounting` | Sales (invoicing fields), Reports |
The existing codebase defines a 4-level privilege hierarchy in `fusion_plating/security/fp_security.xml`: Operator → Supervisor → Manager → Administrator. These new groups are **role-based** and work **alongside** (not replacing) the existing privilege levels. A user has both a privilege level (what they can do: read/write/create/delete) and one or more roles (what they can see: which menus appear).
These are Fusion Plating security groups layered on top of standard Odoo groups.
| Role Group | Menu Visibility | Required Privilege Level |
|------------|----------------|------------------------|
| `fp_group_estimator` | Sales, Configurator, Customers, Part Catalog | Supervisor+ |
| `fp_group_shop_manager` | Everything (full menu) | Manager+ |
| `fp_group_shop_floor` | Manufacturing, Work Orders, Plant Overview only | Operator+ |
| `fp_group_receiving` | Receiving & Inspection, can view Sales (read-only) | Operator+ |
| `fp_group_shipping` | Shipping & Delivery, can view Sales (read-only) | Operator+ |
| `fp_group_quality` | Quality, can view Manufacturing | Supervisor+ |
| `fp_group_accounting` | Sales (invoicing fields), Reports | Supervisor+ |
Users are assigned to one or more role groups. The existing privilege hierarchy controls CRUD permissions; role groups control menu/view visibility. `fp_group_shop_manager` implies all other role groups (full access).
Standard Odoo groups are still required for underlying model access (e.g. `sales_team.group_sale_salesman` for SO access).
---
@@ -146,7 +162,7 @@ These are Fusion Plating security groups layered on top of standard Odoo groups.
|-------|------|-------------|
| `name` | Char | e.g. "EN Mid-Phos AMS 2404" |
| `process_type_id` | Many2one fusion.plating.process.type | Process type |
| `recipe_id` | Many2one fusion.plating.process.node | Default recipe |
| `recipe_id` | Many2one fusion.plating.process.node | Default recipe. **Domain: `[('node_type', '=', 'recipe')]`** |
| `phosphorus_level` | Selection | low_phos, mid_phos, high_phos, na |
| `thickness_min` | Float | Min thickness |
| `thickness_max` | Float | Max thickness |
@@ -164,6 +180,7 @@ These are Fusion Plating security groups layered on top of standard Odoo groups.
| `name` | Char | e.g. "Bead Blast", "Zincate", "Bake" |
| `treatment_type` | Selection | pre, post |
| `default_duration_minutes` | Float | Estimated duration |
| `currency_id` | Many2one res.currency | Company currency (default) |
| `default_cost` | Monetary | Cost per application |
#### `fp.pricing.rule` — Formula-Based Pricing Engine
@@ -175,9 +192,10 @@ These are Fusion Plating security groups layered on top of standard Odoo groups.
| `substrate_material` | Selection | Optional filter |
| `certification_level` | Selection | Optional filter |
| `pricing_method` | Selection | per_sqin, per_sqft, per_piece, flat_rate, formula |
| `currency_id` | Many2one res.currency | Company currency (default) |
| `base_rate` | Monetary | $ per unit |
| `thickness_factor` | Float | Multiplier per mil of thickness |
| `complexity_surcharge` | Text/JSON | Mapping complexity → surcharge % |
| `complexity_surcharge_ids` | One2many fp.pricing.complexity.surcharge | Surcharges by complexity level |
| `masking_rate_per_zone` | Monetary | $ per masking area |
| `setup_fee` | Monetary | One-time per batch |
| `minimum_charge` | Monetary | Floor price |
@@ -185,10 +203,22 @@ These are Fusion Plating security groups layered on top of standard Odoo groups.
| `sequence` | Integer | Priority — first matching rule wins |
| `active` | Boolean | |
#### `fp.quote.configurator` — The Configurator Session
#### `fp.pricing.complexity.surcharge` — Complexity-Based Surcharge Line
| Field | Type | Description |
|-------|------|-------------|
| `rule_id` | Many2one fp.pricing.rule | Parent rule (cascade) |
| `complexity` | Selection | simple, moderate, complex, very_complex |
| `surcharge_percent` | Float | Surcharge % for this complexity level |
#### `fp.quote.configurator` — The Configurator Session (Persistent Model)
This is a `models.Model` (NOT transient). Records persist for audit trail, re-quoting, and linking back from sale orders. The SO links back via `x_fc_configurator_id`.
| Field | Type | Description |
|-------|------|-------------|
| `name` | Char | Auto-sequence (CFG-00001) |
| `state` | Selection | draft, confirmed, cancelled |
| `partner_id` | Many2one res.partner | Customer |
| `part_catalog_id` | Many2one fp.part.catalog | For repeat parts |
| `coating_config_id` | Many2one fp.coating.config | Coating selection |
@@ -197,17 +227,21 @@ These are Fusion Plating security groups layered on top of standard Odoo groups.
| `surface_area` | Float | From catalog or entered |
| `thickness_requested` | Float | |
| `masking_zones` | Integer | |
| `complexity` | Selection | |
| `complexity` | Selection | simple, moderate, complex, very_complex |
| `rush_order` | Boolean | |
| `turnaround_days` | Integer | |
| `delivery_method` | Selection | local_delivery, shipping_partner, customer_pickup |
| `currency_id` | Many2one res.currency | Company currency (default) |
| `shipping_fee` | Monetary | |
| `delivery_fee` | Monetary | |
| `calculated_price` | Monetary | Computed from pricing rules |
| `price_breakdown_html` | Html | Rendered breakdown |
| `estimator_override_price` | Monetary | Final price (defaults to calculated) |
| `sale_order_id` | Many2one sale.order | Created SO (set on "Create Quotation") |
| `notes` | Text | |
**Lifecycle:** draft → (estimator builds quote) → confirmed (when SO created) → cancelled (if abandoned). Confirmed records are read-only. Re-quoting creates a new configurator record.
### Price Calculation Flow
```
@@ -258,7 +292,7 @@ Part Catalog (surface area, complexity, masking)
| Field | Type | Description |
|-------|------|-------------|
| `name` | Char | Auto-sequence (RCV-00001) |
| `sale_order_id` | Many2one sale.order | Required |
| `sale_order_id` | Many2one sale.order | Required. **Design decision:** one receiving record per SO. If a customer ships parts for multiple SOs in one box, create separate receiving records per SO (receiver splits the count). This keeps the SO↔receiving link clean and avoids Many2many complexity. |
| `partner_id` | Many2one res.partner | Related from SO |
| `po_number` | Char | Related from SO x_fc_po_number |
| `received_by_id` | Many2one res.users | Who logged it |
@@ -344,13 +378,29 @@ SO Confirmed → Receiving record auto-created (state=draft)
| `payment_term_id` | Many2one account.payment.term | |
| `notes` | Text | |
### Auto-Population Rules
When a customer is selected on a new SO:
1. Look up `fp.invoice.strategy.default` for that `partner_id`
2. If found → auto-fill `x_fc_invoice_strategy` and `x_fc_deposit_percent` from the default
3. If not found → leave blank (estimator must select manually)
4. Estimator can always override per order
When a coating config is selected in the configurator:
1. Auto-fill `thickness_requested` from `coating_config.thickness_min` (default to minimum)
2. Auto-fill surface area UOM from company default setting
When a part catalog entry is selected:
1. Auto-fill `surface_area`, `complexity`, `masking_zones`, `substrate_material` from the catalog entry
2. These can be overridden per-quote if the part has changed
### Account Hold (extends `res.partner`)
| Field | Type | Description |
|-------|------|-------------|
| `x_fc_account_hold` | Boolean | Manually set by accounting |
| `x_fc_account_hold_reason` | Text | Why hold was placed |
| `x_fc_account_hold_date` | Date | When placed |
| `x_fc_account_hold_date` | Datetime | When placed (Datetime for audit precision) |
| `x_fc_account_hold_by_id` | Many2one res.users | Who placed it |
### Account Hold Behaviour
@@ -460,11 +510,18 @@ Based on Fischerscope XDAL 600 output:
- Operator, date/time
- Data entry: manual for now, future Fischerscope CSV import
### Work Centre Mapping Note
The codebase has two work centre models: `fusion.plating.work.center` (core) and `mrp.workcenter` (standard MRP). Recipe nodes reference `fusion.plating.work.center`; MRP work orders use `mrp.workcenter`. The recipe-to-WO generation logic in `fusion_plating_bridge_mrp` must map between them. Each `fusion.plating.work.center` should have an `x_fc_mrp_workcenter_id` field linking to the corresponding `mrp.workcenter`. This mapping field should be added to the core module.
---
## 7. Certificate Registry — `fp.certificate`
## 7. `fusion_plating_certificates` — Certificate Registry
Unified model logging every certificate issued.
**Module owner:** `fusion_plating_certificates` (NEW dedicated module).
**Dependencies:** `fusion_plating`, `fusion_plating_portal`, `fusion_plating_reports`, `mrp`
This module owns `fp.certificate` and `fp.thickness.reading`. It depends on `fusion_plating_portal` for the `fusion.plating.portal.job` link and on `fusion_plating_reports` for report generation.
### Model: `fp.certificate`
@@ -475,7 +532,7 @@ Unified model logging every certificate issued.
| `partner_id` | Many2one res.partner | Customer |
| `sale_order_id` | Many2one sale.order | |
| `production_id` | Many2one mrp.production | |
| `portal_job_id` | Many2one fp.portal.job | |
| `portal_job_id` | Many2one fusion.plating.portal.job | Uses canonical model name |
| `part_number` | Char | Denormalized for fast search |
| `process_description` | Char | e.g. "ELECTROLESS NICKEL PLATING PER AMS 2404" |
| `spec_reference` | Char | |
@@ -491,9 +548,31 @@ Unified model logging every certificate issued.
| `void_reason` | Text | |
| `notes` | Html | |
### Model: `fp.thickness.reading` — Fischerscope Measurement Data
| Field | Type | Description |
|-------|------|-------------|
| `certificate_id` | Many2one fp.certificate | Parent certificate (cascade) |
| `production_id` | Many2one mrp.production | Link to MO (independent of cert) |
| `reading_number` | Integer | Reading sequence (n=1, n=2, n=3) |
| `nip_mils` | Float(10,4) | NiP thickness in mils |
| `ni_percent` | Float(6,3) | Nickel content % |
| `p_percent` | Float(6,4) | Phosphorus content % |
| `position_label` | Char | Where on the part this reading was taken |
| `equipment_model` | Char | e.g. "Fischerscope XDAL 600" |
| `product_ref` | Char | e.g. "2805031 / NiP/Al-alloys 2805030" |
| `calibration_std_ref` | Char | e.g. "NiP/Al STD SET SN 100174568" |
| `microscope_image_id` | Many2one ir.attachment | Microscope photo |
| `operator_id` | Many2one res.users | Who took the reading |
| `reading_datetime` | Datetime | When reading was taken |
| `measuring_time_seconds` | Integer | e.g. 120 |
**Statistical fields** (computed from reading lines per certificate):
- `mean_nip_mils`, `stddev_nip_mils`, `cov_percent`, `range_nip_mils` — computed on `fp.certificate` from its `thickness_reading_ids`
### Auto-Creation
When CoC or thickness report is generated, fp.certificate record auto-created with PDF attached.
When CoC or thickness report is generated, `fp.certificate` record auto-created with PDF attached.
### Views
@@ -501,6 +580,10 @@ When CoC or thickness report is generated, fp.certificate record auto-created wi
- **Search**: Quick filters by Customer, Certificate Type, Date Range, Part Number, PO Number. Group by: Customer, Type, Month, Issued By.
- **Form**: Certificate type badge, state buttons (Issue/Void), customer info, part details, tabs for Thickness Readings, Attachments, Notes. "Regenerate PDF" and "Send to Customer" buttons.
### CoC Bilingual Implementation
The bilingual EN/FR certification statement uses QWeb template logic with `t-if` on a `bilingual` flag (default: True for Canadian compliance). The English and French text blocks are both rendered in the same template — not using Odoo's `ir.translation` system, since both languages must appear on the same document simultaneously.
---
## 8. `fusion_tasks` (Entech Plating) — Local Delivery Dispatch
@@ -541,10 +624,10 @@ Renamed from `fusion.technician.task`.
|-------|------|-------------|
| `delivery_id` | Many2one fusion.plating.delivery | Links to logistics |
| `sale_order_id` | Many2one sale.order | |
| `portal_job_id` | Many2one fp.portal.job | |
| `portal_job_id` | Many2one fusion.plating.portal.job | Uses canonical model name |
| `partner_id` | Many2one res.partner | Customer |
| `driver_id` | Many2one hr.employee | Renamed from technician_id |
| `vehicle_id` | Many2one fusion.plating.vehicle | |
| `vehicle_id` | Many2one fusion.plating.vehicle | From logistics module |
| `packages_count` | Integer | Number of boxes/crates |
| `weight_total` | Float | Total weight |
| `requires_signature` | Boolean | POD required |
@@ -555,12 +638,14 @@ Renamed from `fusion.technician.task`.
- `action_mark_delivered()`: logs GPS + timestamp, captures signature/photo, updates fp.delivery → delivered, cascades to portal job → shipped, triggers shipment notification email
- `action_mark_failed()`: logs reason, creates follow-up activity
### Dependencies
### Dependencies & Deployment
```python
'depends': ['base', 'mail', 'hr', 'fusion_plating_logistics'],
```
**Deployment strategy:** `fusion_tasks` lives in a separate repo (`Entech Plating/`) but is deployed to the SAME server as the fusion_plating modules. Both repos are copied to `/mnt/extra-addons/custom/` on the target server. The `fusion_plating_logistics` dependency is therefore always available at install time. There is no standalone driver-only install scenario — drivers access the system via the same Odoo instance. The standalone "Delivery Dispatch" menu (below) provides a driver-focused view without needing a separate deployment.
### Menu Placement
Inside Fusion Plating: Shipping & Delivery → Local Delivery Tasks, Driver Map
@@ -683,23 +768,29 @@ fusion_plating (core)
│ └── depends: fusion_plating, sale_management
├── fusion_plating_receiving
│ └── depends: fusion_plating, sale_management
│ └── provides: mrp.production gate mixin (overrides action_confirm)
├── fusion_plating_invoicing
│ └── depends: fusion_plating, sale_management, account
│ └── provides: invoice strategy automation, account hold on res.partner
├── fusion_plating_notifications
│ └── depends: fusion_plating, fusion_plating_reports, mail
├── fusion_plating_bridge_mrp
│ └── depends: fusion_plating, fusion_plating_configurator,
│ fusion_plating_receiving, fusion_plating_invoicing,
│ mrp
├── fusion_plating_certificates
│ └── depends: fusion_plating, fusion_plating_portal,
│ fusion_plating_reports, mrp
├── fusion_plating_bridge_mrp (lighter — gates live in receiving/invoicing)
│ └── depends: fusion_plating, fusion_plating_configurator, mrp
│ └── soft-depends: fusion_plating_receiving, fusion_plating_invoicing
├── fusion_plating_portal
│ └── depends: fusion_plating, fusion_plating_configurator,
│ fusion_plating_notifications, portal
├── fusion_plating_logistics
│ └── depends: fusion_plating
└── fusion_tasks (Entech Plating — separate repo)
└── fusion_tasks (Entech Plating — separate repo, same server)
└── depends: fusion_plating_logistics, hr, mail
```
**Note on bridge_mrp:** The receiving gate and invoice strategy gates are implemented as lightweight mixins within `fusion_plating_receiving` and `fusion_plating_invoicing` respectively (each overrides `mrp.production` independently). This avoids funnelling all dependencies through bridge_mrp. Bridge_mrp focuses on recipe-to-WO generation and the configurator link.
---
## 11. Pricing Variables Reference
@@ -737,7 +828,20 @@ All 10 pricing variables that drive the configurator:
---
## 13. Roadmap Items (Not in Initial Build)
## 13. Data Migration: Existing Quote Request Flow
The existing `fusion.plating.quote.request` model has an `action_create_sale_order()` method that creates basic SOs. The new configurator introduces a parallel, richer path.
**Coexistence strategy:**
- The existing `action_create_sale_order()` on `fusion.plating.quote.request` remains functional — it is the "quick path" for simple quotes that don't need the full configurator
- The new configurator is the "full path" for detailed quotes with part catalog, coating config, and pricing rules
- When a quote request comes in via portal, the estimator chooses: use the configurator (creates `fp.quote.configurator` → SO) or use the quick path (existing `action_create_sale_order()`)
- Both paths create SOs with `x_fc_*` fields. The quick path leaves configurator-specific fields blank; the full path populates everything
- No existing data needs migration — the two paths coexist
---
## 14. Roadmap Items (Not in Initial Build)
- Claude Vision for PDF drawing measurement extraction
- Auto account hold computed from invoice aging