# EN Tech Plating — End-to-End Workflow Design Spec **Date:** 2026-04-12 **Author:** Nexa Systems (Claude-assisted) **Client:** EN Technologies (Electroless Nickel Technologies Inc.) **Status:** Approved for implementation planning --- ## 1. Overview 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 | |--------|---------|----------| | `fusion_plating_configurator` | Quotation configurator, 3D viewer, pricing engine, part catalog | `fusion-plating/` | | `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 (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 --- ## 2. Sales Integration — Fusion Plating as Single Hub The Fusion Plating app becomes the single workspace. Users never leave it. Sale orders are managed with custom plating-specific views. ### Menu Structure ``` Fusion Plating (app) ├── Sales (default landing) │ ├── Quotations (sale.order, state=draft/sent) │ ├── Sale Orders (sale.order, state=sale) │ ├── Customers (res.partner, customer_rank > 0) │ └── Part Catalog (fp.part.catalog) ├── Configurator │ ├── New Quote (fp.quote.configurator, persistent model) │ ├── Coating Configurations (fp.coating.config) │ └── Pricing Rules (fp.pricing.rule) ├── Manufacturing │ ├── Manufacturing Orders │ ├── Work Orders │ └── Plant Overview ├── Receiving & Inspection ├── Shipping & Delivery │ ├── Deliveries (fp.delivery) │ ├── Local Delivery Tasks (fusion.delivery.task) │ └── Routes (fp.route) ├── Certificates │ ├── All Certificates (fp.certificate) │ ├── Certificates of Conformance (filtered: type=coc) │ └── Thickness Reports (filtered: type=thickness_report) ├── Quality ├── Portal Jobs ├── Reports └── Configuration ``` ### Sale Order Extensions (fields on `sale.order`) - `x_fc_configurator_id` — link back to configurator session - `x_fc_part_catalog_id` — customer part being ordered - `x_fc_coating_config_id` — coating configuration - `x_fc_po_number` — customer PO reference (Char) - `x_fc_po_attachment_id` — uploaded PO document - `x_fc_po_received` — Boolean - `x_fc_po_override` — Boolean (manager override — proceed without PO) - `x_fc_po_override_reason` — Text - `x_fc_invoice_strategy` — Selection (deposit, progress, net_terms, cod_prepay) - `x_fc_deposit_percent` — Float - `x_fc_rush_order` — Boolean - `x_fc_delivery_method` — Selection (local_delivery, shipping_partner, customer_pickup) - `x_fc_receiving_status` — Selection (not_received, partial, received, inspected) — computed - Smart buttons: Portal Job, Manufacturing Order, Delivery, Receiving, Invoices, Certificates ### Custom Sale Order Views - **List**: Customer, PO#, Part, Coating, Qty, Total, Receiving Status, Job Status, Delivery Method - **Form**: inherits sale.order form, adds plating tabs (Part Details, Coating Config, Receiving, Job Tracking) - **Kanban**: cards grouped by stage (Draft → Quoted → PO Received → Parts Received → In Production → Shipped → Invoiced) ### Permission-Based Visibility 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). | 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). --- ## 3. `fusion_plating_configurator` — Quotation Configurator & Pricing Engine ### Users - **Primary:** Internal estimator (full control, detailed configurator, price override) - **Secondary:** Portal customer (simplified self-service, estimated pricing, 3D preview, lead gen) ### Core Models #### `fp.part.catalog` — Customer Part Library | Field | Type | Description | |-------|------|-------------| | `partner_id` | Many2one res.partner | Customer (required) | | `name` | Char | Part name/description | | `part_number` | Char | Customer's part number | | `revision` | Char | Revision letter/number | | `substrate_material` | Selection | aluminium, steel, stainless, copper, titanium, other | | `geometry_source` | Selection | 3d_model, manual, pdf_drawing | | `model_attachment_id` | Many2one ir.attachment | STEP/STL/IGES file | | `drawing_attachment_ids` | Many2many ir.attachment | PDF drawings | | `surface_area` | Float | Surface area value | | `surface_area_uom` | Selection | sq_in, sq_ft, sq_cm, sq_m | | `weight` | Float | For shipping cost calc | | `dimensions_length` | Float | Manual measurement | | `dimensions_width` | Float | Manual measurement | | `dimensions_height` | Float | Manual measurement | | `complexity` | Selection | simple, moderate, complex, very_complex | | `masking_zones` | Integer | Number of areas requiring masking | | `masking_description` | Text | e.g. "mask threaded holes" | | `has_blind_holes` | Boolean | Complexity flag | | `has_recesses` | Boolean | Complexity flag | | `has_threads` | Boolean | Complexity flag | | `notes` | Html | | | `active` | Boolean | Archivable | #### `fp.coating.config` — Coating Configuration Template | Field | Type | Description | |-------|------|-------------| | `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. **Domain: `[('node_type', '=', 'recipe')]`** | | `phosphorus_level` | Selection | low_phos, mid_phos, high_phos, na | | `thickness_min` | Float | Min thickness | | `thickness_max` | Float | Max thickness | | `thickness_uom` | Selection | mils, microns, inches | | `spec_reference` | Char | e.g. "AMS 2404" | | `certification_level` | Selection | commercial, mil_spec, nadcap, nuclear | | `pre_treatment_ids` | Many2many fp.treatment | Bead blast, zincate, etc. | | `post_treatment_ids` | Many2many fp.treatment | Bake, passivate, chromate, etc. | | `active` | Boolean | | #### `fp.treatment` — Pre/Post Treatment | Field | Type | Description | |-------|------|-------------| | `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 | Field | Type | Description | |-------|------|-------------| | `name` | Char | Rule description | | `coating_config_id` | Many2one fp.coating.config | Optional filter (global if blank) | | `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_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 | | `rush_surcharge_percent` | Float | Rush premium % | | `sequence` | Integer | Priority — first matching rule wins | | `active` | Boolean | | #### `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 | | `quantity` | Integer | Number of parts | | `batch_size` | Integer | Parts per rack/barrel | | `surface_area` | Float | From catalog or entered | | `thickness_requested` | Float | | | `masking_zones` | Integer | | | `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 ``` Part Catalog (surface area, complexity, masking) + Coating Config (process, thickness, spec level) + Pricing Rules (matched by coating + substrate + cert level) + Quantity / Batch Size + Rush surcharge (if applicable) + Delivery / Shipping fees = Calculated Price → Estimator reviews & overrides if needed → Final quote price ``` ### 3D Viewer - OWL component using **Three.js** for STL rendering and **OCCT (OpenCascade) WASM** for STEP parsing - Renders on both backend configurator form and portal page - Features: wireframe/solid toggle, rotate/zoom, surface area highlight - Server-side surface area calculation: Python `trimesh` (STL) / `cadquery`/`OCP` (STEP) - Fallback: manual measurements if server can't parse the file - Roadmap: Claude Vision for PDF drawing measurement extraction ### Portal Side (Customer-Facing) - Simplified wizard: upload part → select coating type → see estimated price range - Uses same `fp.quote.configurator` model with restricted fields - Customer sees estimated price range (not exact), 3D preview - Submits → creates `fp.quote.request` with configurator data attached - Internal estimator sees customer's config and refines it ### Configurator → Sale Order Flow 1. Estimator opens Configurator → builds quote 2. Clicks "Create Quotation" → sale.order created with all x_fc_* fields 3. SO line(s) auto-created (product = service product per coating type, qty, price = estimator's final price) 4. Estimator reviews SO → sends quotation 5. Customer accepts → confirms SO → triggers downstream flow --- ## 4. `fusion_plating_receiving` — Parts Receiving & Inspection ### Core Models #### `fp.receiving` — Receiving Record | Field | Type | Description | |-------|------|-------------| | `name` | Char | Auto-sequence (RCV-00001) | | `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 | | `received_date` | Datetime | Default=now | | `state` | Selection | draft, inspecting, accepted, discrepancy, resolved | | `expected_qty` | Integer | From SO line | | `received_qty` | Integer | Entered by receiver | | `qty_match` | Boolean | Computed: received == expected | | `carrier_name` | Char | Who delivered | | `carrier_tracking` | Char | Inbound tracking # | | `notes` | Html | | | `line_ids` | One2many fp.receiving.line | Per-part detail | | `damage_ids` | One2many fp.receiving.damage | Damage log | | `attachment_ids` | Many2many ir.attachment | Photos | #### `fp.receiving.line` — Per-Part Receiving Detail | Field | Type | Description | |-------|------|-------------| | `receiving_id` | Many2one fp.receiving | Cascade | | `part_catalog_id` | Many2one fp.part.catalog | Optional | | `part_number` | Char | | | `description` | Char | | | `expected_qty` | Integer | | | `received_qty` | Integer | | | `condition` | Selection | good, damaged, mixed | | `notes` | Text | | #### `fp.receiving.damage` — Damage Log Entry | Field | Type | Description | |-------|------|-------------| | `receiving_id` | Many2one fp.receiving | Cascade | | `description` | Text | What's damaged | | `severity` | Selection | cosmetic, functional, rejected | | `photo_ids` | Many2many ir.attachment | | | `action_required` | Selection | none, notify_customer, return_parts, proceed_as_is | | `customer_notified` | Boolean | | | `customer_response` | Text | | | `resolved` | Boolean | | ### Workflow ``` SO Confirmed → Receiving record auto-created (state=draft) → Parts arrive → receiver enters qty, inspects condition → Match + good → state=accepted → Mismatch or damage → state=discrepancy → SO flagged, follow-up activity created → Customer contacted → resolution logged → state=resolved → Accepted/Resolved → SO x_fc_receiving_status = 'received' → Manufacturing can proceed ``` ### Manufacturing Gate - `mrp.production.action_confirm()` checks `sale_order.x_fc_receiving_status` - If not received → warning dialog (manager can override) - Soft gate — warns, doesn't hard-block (flexibility for handshake deals, urgent jobs) --- ## 5. `fusion_plating_invoicing` — Invoice Strategy Engine ### Invoice Strategies | Strategy | Behaviour | |----------|-----------| | `deposit` | Deposit invoice for X% on SO confirmation. Balance after shipping. | | `progress` | Invoice per MO as each completes. | | `net_terms` | Single invoice after shipping. Payment on terms. | | `cod_prepay` | Full invoice on SO confirmation. Manufacturing blocked until paid. | ### Core Models #### `fp.invoice.strategy.default` — Customer-Level Default | Field | Type | Description | |-------|------|-------------| | `partner_id` | Many2one res.partner | Unique per customer | | `default_strategy` | Selection | deposit, progress, net_terms, cod_prepay | | `default_deposit_percent` | Float | e.g. 50.0 | | `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` | Datetime | When placed (Datetime for audit precision) | | `x_fc_account_hold_by_id` | Many2one res.users | Who placed it | ### Account Hold Behaviour | Action | Hold Active | Result | |--------|-------------|--------| | Create new SO | Yes | Warning banner. SO can still be created. | | Confirm SO | Yes | Blocked. Manager override available. | | Create invoice | Yes | Blocked. Manager override available. | | Ship / mark delivered | Yes | Blocked. Manager override available. | | Customer visits portal | Yes | No visible indication. | Roadmap: auto-hold computed from account.move aging. ### Invoice Automation - **Deposit:** SO confirmed → auto-create deposit invoice (X%) → balance invoice after shipping - **Progress:** Each MO done → invoice for that MO's portion → final invoice for remaining balance - **Net terms:** Delivery complete → auto-create full invoice → payment terms applied - **COD/Prepay:** SO confirmed → auto-create full invoice → MO blocked until payment reconciled ### Shipping Method Price Adjustment - Method changes after invoicing: - Draft invoice → amend the line - Posted invoice → supplementary invoice or credit note --- ## 6. `fusion_plating_notifications` — Auto-Email Engine ### Notification Triggers | Trigger Event | Email Name | Attachments | Recipient | |---------------|-----------|-------------|-----------| | Quotation sent | Quote Ready | Quote PDF | Customer contact | | SO confirmed | Order Confirmation | SO PDF | Customer contact | | Parts received | Parts Received | — | Customer contact | | MO complete | Ready for Pickup/Ship | — | Customer contact | | Delivery shipped (carrier) | Shipment Notification | CoC, Thickness Report, Invoice | Customer contact | | Delivery completed (local) | Delivery Confirmation | CoC, Thickness Report, Invoice, POD | Customer contact | | Invoice posted | Invoice Notification | Invoice PDF | Billing contact | | Deposit invoice created | Deposit Required | Deposit Invoice PDF | Billing contact | ### Core Models #### `fp.notification.template` — Configurable Email Templates | Field | Type | Description | |-------|------|-------------| | `name` | Char | Template name | | `trigger_event` | Selection | Event type | | `mail_template_id` | Many2one mail.template | Actual Odoo template | | `active` | Boolean | Can disable specific notifications | | `attach_coc` | Boolean | | | `attach_thickness_report` | Boolean | | | `attach_invoice` | Boolean | | | `attach_packing_list` | Boolean | | | `attach_pod` | Boolean | | | `cc_internal_ids` | Many2many res.users | Internal CCs | #### `fp.notification.log` — Audit Trail | Field | Type | Description | |-------|------|-------------| | `template_id` | Many2one fp.notification.template | | | `trigger_event` | Selection | | | `sale_order_id` | Many2one sale.order | | | `partner_id` | Many2one res.partner | | | `sent_date` | Datetime | | | `recipient_email` | Char | | | `attachment_names` | Text | Comma-separated list | | `status` | Selection | sent, failed, bounced | | `error_message` | Text | | | `mail_mail_id` | Many2one mail.mail | | ### Document Assembly (Shipment Email) 1. Find portal job linked to SO/MO 2. Generate CoC PDF (bilingual EN/FR, customer logo, Nadcap badge if applicable) 3. Attach thickness report if available 4. Attach invoice PDF 5. Include tracking info in email body (carrier tracking # or driver ETA) 6. Send to customer contact 7. Log in fp.notification.log ### CoC Report Updates - Customer logo placement (from partner.image_1920) - Nadcap badge (conditional) - EN Tech branding (replace Steelhead) - Recorded thickness field - Process description with spec references - Bilingual certification statement - Quantities: Shipped/Exp, NC Qty columns - Configurable certifying authority signature ### Thickness / Measurement Report (NEW template) Based on Fischerscope XDAL 600 output: - EN Tech header - Equipment info (model, product, application) - Microscope image (attached photo) - Reading data table (NiP mils, Ni %, P %) - Statistical summary (Mean, Std Dev, CoV%, Range) - Calibration standard reference - 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. `fusion_plating_certificates` — Certificate Registry **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` | Field | Type | Description | |-------|------|-------------| | `name` | Char | Auto-sequence (CERT-00001) | | `certificate_type` | Selection | coc, thickness_report, mill_test, nadcap_cert, customer_specific | | `partner_id` | Many2one res.partner | Customer | | `sale_order_id` | Many2one sale.order | | | `production_id` | Many2one mrp.production | | | `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 | | | `po_number` | Char | Customer PO ref | | `entech_wo_number` | Char | Internal WO # | | `quantity_shipped` | Integer | | | `issued_by_id` | Many2one res.users | | | `certified_by_id` | Many2one res.users | Signing authority | | `issue_date` | Date | Default=today | | `attachment_id` | Many2one ir.attachment | Generated PDF | | `thickness_reading_ids` | One2many fp.thickness.reading | Linked measurements | | `state` | Selection | draft, issued, voided | | `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. ### Views - **List** (default, newest first): Issue Date, Cert #, Type, Customer, Part #, PO #, Entech WO#, Process, Qty, Issued By, Status - **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 ### Fork & Strip Strategy **Remove:** - Cross-instance sync (fusion.task.sync.config, shadow tasks) - `fusion_claims.*` config parameters → rename to `fusion_tasks.*` - `sales_team` dependency - Irrelevant task types (repair, troubleshoot, assessment, ltc_visit, maintenance, installation) - All sync-related fields (x_fc_sync_*) **Keep:** - Google Maps integration (Leaflet.js map view) - GPS tracking (fusion.technician.location → fusion.driver.location) - Geocoding (_geocode_address()) - Route planning / scheduling / conflict avoidance - Push notifications (fusion.push.subscription) - Map view JS/SCSS/XML ### Adapted Model: `fusion.delivery.task` Renamed from `fusion.technician.task`. **Task Types** (reduced): - `delivery` — outbound delivery - `pickup` — collect parts from customer - `return` — return rejected/damaged parts - `rush` — same-day urgent **Status Workflow:** - `pending` → `scheduled` → `en_route` → `delivered` (or `failed`) **Key Fields:** | Field | Type | Description | |-------|------|-------------| | `delivery_id` | Many2one fusion.plating.delivery | Links to logistics | | `sale_order_id` | Many2one sale.order | | | `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 | From logistics module | | `packages_count` | Integer | Number of boxes/crates | | `weight_total` | Float | Total weight | | `requires_signature` | Boolean | POD required | | `requires_photo` | Boolean | Photo proof required | | `coc_attachment_id` | Many2one ir.attachment | CoC to hand to customer | **Delivery Integration:** - `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 & 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 Standalone (optional): Delivery Dispatch app for drivers. --- ## 9. Complete End-to-End Workflow ### Stage 1: Customer Inquiry - **Portal path:** Customer uploads 3D/PDF on portal → sees estimated price → submits → fp.quote.request created - **Email/phone path:** Estimator creates fp.quote.request manually, uploads customer's files - **Modules:** fusion_plating_portal, fusion_plating_configurator ### Stage 2: Quotation - Estimator opens Configurator inside Fusion Plating app - Selects/creates part in Part Catalog - 3D model → auto surface area + 3D preview; PDF → manual measurements - Selects Coating Configuration - Pricing engine calculates from rules; estimator reviews/overrides - Adds delivery/shipping fees, sets invoice strategy - "Create Quotation" → sale.order with all x_fc_* fields - Sends quotation → email notification with Quote PDF - **Modules:** fusion_plating_configurator, fusion_plating_notifications ### Stage 3: Order Confirmation - Customer accepts + submits PO (email or portal) - PO number entered on SO, PO document uploaded (or manager override for handshake deals) - SO confirmed: - Account hold check → blocked if hold active (manager override) - Invoice strategy fires (deposit/COD → auto-invoice; net_terms/progress → no invoice yet) - Email: Order Confirmation + SO PDF - Receiving record auto-created (draft) - Portal job auto-created (received) - **Modules:** fusion_plating_invoicing, fusion_plating_receiving, fusion_plating_bridge_mrp, fusion_plating_notifications ### Stage 4: Parts Receiving - Parts arrive → receiver opens receiving record - Counts parts, inspects condition - Good → accepted; damage/mismatch → discrepancy → follow-up → resolved - Email: Parts Received - Portal job → in_progress - **Modules:** fusion_plating_receiving, fusion_plating_notifications ### Stage 5: Manufacturing Planning - MO created from SO (standard sale_mrp) - Receiving gate: warns if parts not received - COD gate: warns if prepay not paid - Planner assigns recipe, configures opt-in/out steps - Recipe → Work Orders generated (one WO per operation node, steps = WO instructions) - **Modules:** fusion_plating_bridge_mrp ### Stage 6: Manufacturing Execution - Operators work WOs on shopfloor (Plant Overview kanban, timers, bath/tank assignment) - Quality holds if needed - All WOs done → MO done: - Portal job → ready_to_ship - fp.delivery auto-created (draft) - Thickness readings entered - CoC + thickness report generated → fp.certificate records created - Progress invoicing: if strategy=progress, invoice this MO's portion - **Modules:** fusion_plating_shopfloor, fusion_plating_bridge_mrp, fusion_plating_quality, fusion_plating_invoicing ### Stage 7: Shipping / Local Delivery **Shipping Partner (Purolator, FedEx, UPS, etc.):** - fp.delivery scheduled with carrier + tracking # - Packing list generated, delivery marked shipped - Module: fusion_plating_logistics **Local Delivery (EN Tech driver):** - fusion.delivery.task created, driver + vehicle assigned - Driver Map shows live GPS tracking - Driver delivers → signature/photo POD → cascades to fp.delivery - Module: fusion_tasks (Entech Plating) **Customer Pickup:** - Email: Ready for Pickup - Customer arrives → parts released → signature → fp.delivery marked delivered **All paths:** - Portal job → shipped - Email: CoC + Thickness Report + Invoice + Tracking/ETA - **Modules:** fusion_plating_logistics, fusion_tasks, fusion_plating_notifications ### Stage 8: Invoicing & Payment - Strategy determines timing: - deposit → balance invoice after shipping - progress → final invoice for remaining balance - net_terms → full invoice after shipping - cod_prepay → already invoiced & paid - Delivery method change after invoice → supplementary invoice or credit note - Invoice posted → portal job → complete → email with Invoice PDF - **Modules:** fusion_plating_invoicing, fusion_plating_bridge_mrp, fusion_plating_notifications ### Stage 9: Customer Portal - Full job lifecycle visible: progress bar (received → complete) - Documents tab: CoC, thickness report, invoice — downloadable - Part catalog: saved parts with 3D preview - Order history: past orders, re-order from catalog - Quote request history, tracking info, notification history - **Modules:** fusion_plating_portal --- ## 10. Module Dependency Graph ``` fusion_plating (core) ├── fusion_plating_configurator │ └── 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_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, 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 All 10 pricing variables that drive the configurator: 1. **Surface area** — more area = more chemistry consumed 2. **Coating type** — EN, chrome, anodize, black oxide (different bath costs) 3. **Thickness spec** — more passes/dwell time 4. **Substrate material** — aluminium needs zincate pre-treatment 5. **Quantity / batch size** — more parts per rack = lower per-unit cost 6. **Part complexity** — blind holes, recesses, masking areas 7. **Masking requirements** — labour-intensive 8. **Spec / certification level** — Nadcap/aerospace = more QC overhead 9. **Turnaround time** — rush = premium 10. **Pre/post treatment** — bead blast, bake, passivate --- ## 12. Key Architectural Decisions | Decision | Resolution | |----------|------------| | Configurator primary user | Internal estimator; portal is simplified lead-gen | | 3D file handling | STEP/STL auto surface area calc + 3D preview; PDF manual (Claude Vision roadmap) | | Pricing model | Formula-calculated with estimator override | | Part catalog | Customer part library for repeat business + one-off support | | PO requirement | Required before manufacturing, but manager override available | | Invoice strategies | All 4 supported (deposit, progress, net_terms, cod_prepay), configurable per order | | Account hold | Manual for now, auto from aging on roadmap | | Shipping decision | Set at quote time, changeable later with price adjustment | | Local delivery | Fork fusion_tasks, strip claims, keep GPS/maps | | Certificate management | Unified fp.certificate registry with filters, auto-creation on report generation | | Recipe → WO mapping | One WO per operation node, steps become WO instructions | --- ## 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 - Fischerscope CSV import (auto-populate thickness readings) - Multi-driver route optimization - Customer-specific certificate templates - Product configurator on portal (dynamic pricing preview) - Tags on recipe nodes - Dashboard transitions on recipe nodes - Treatment groups / choices on recipe nodes