Compare commits
12 Commits
2b84c31a12
...
aec7659a2e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aec7659a2e | ||
|
|
a337a510c1 | ||
|
|
a5761b9863 | ||
|
|
d3e2614620 | ||
|
|
5143245f57 | ||
|
|
2fa7f2aa2e | ||
|
|
2e80fd3ca1 | ||
|
|
87325e2caf | ||
|
|
73b7325b46 | ||
|
|
dde970a2f5 | ||
|
|
d424dfdb19 | ||
|
|
f69b3ac855 |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,854 @@
|
||||
# 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
|
||||
6
fusion-plating/fusion_plating_configurator/__init__.py
Normal file
6
fusion-plating/fusion_plating_configurator/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# -*- 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
|
||||
52
fusion-plating/fusion_plating_configurator/__manifest__.py
Normal file
52
fusion-plating/fusion_plating_configurator/__manifest__.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- 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',
|
||||
'data/fp_treatment_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,
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="seq_fp_quote_configurator" model="ir.sequence">
|
||||
<field name="name">Fusion Plating: Configurator</field>
|
||||
<field name="code">fp.quote.configurator</field>
|
||||
<field name="prefix">CFG-</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<!-- Pre-treatments -->
|
||||
<record id="treatment_alkaline_clean" model="fp.treatment">
|
||||
<field name="name">Alkaline Clean</field>
|
||||
<field name="treatment_type">pre</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="default_duration_minutes">15</field>
|
||||
</record>
|
||||
<record id="treatment_acid_etch" model="fp.treatment">
|
||||
<field name="name">Acid Etch</field>
|
||||
<field name="treatment_type">pre</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="default_duration_minutes">10</field>
|
||||
</record>
|
||||
<record id="treatment_zincate" model="fp.treatment">
|
||||
<field name="name">Zincate (Aluminium)</field>
|
||||
<field name="treatment_type">pre</field>
|
||||
<field name="sequence">30</field>
|
||||
<field name="default_duration_minutes">5</field>
|
||||
</record>
|
||||
<record id="treatment_bead_blast" model="fp.treatment">
|
||||
<field name="name">Bead Blast</field>
|
||||
<field name="treatment_type">pre</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="default_duration_minutes">20</field>
|
||||
</record>
|
||||
<record id="treatment_degrease" model="fp.treatment">
|
||||
<field name="name">Solvent Degrease</field>
|
||||
<field name="treatment_type">pre</field>
|
||||
<field name="sequence">50</field>
|
||||
<field name="default_duration_minutes">10</field>
|
||||
</record>
|
||||
|
||||
<!-- Post-treatments -->
|
||||
<record id="treatment_bake" model="fp.treatment">
|
||||
<field name="name">Hydrogen Embrittlement Bake</field>
|
||||
<field name="treatment_type">post</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="default_duration_minutes">240</field>
|
||||
</record>
|
||||
<record id="treatment_passivate" model="fp.treatment">
|
||||
<field name="name">Passivate</field>
|
||||
<field name="treatment_type">post</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="default_duration_minutes">30</field>
|
||||
</record>
|
||||
<record id="treatment_chromate_seal" model="fp.treatment">
|
||||
<field name="name">Chromate Seal</field>
|
||||
<field name="treatment_type">post</field>
|
||||
<field name="sequence">30</field>
|
||||
<field name="default_duration_minutes">15</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- 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
|
||||
from . import fp_part_catalog
|
||||
from . import fp_coating_config
|
||||
from . import fp_pricing_complexity_surcharge
|
||||
from . import fp_pricing_rule
|
||||
from . import fp_quote_configurator
|
||||
from . import sale_order
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- 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))
|
||||
thickness_max = fields.Float(string='Max Thickness', digits=(10, 4))
|
||||
thickness_uom = fields.Selection(
|
||||
[('mils', 'mils'), ('microns', 'microns'), ('inches', 'inches')],
|
||||
string='Thickness UoM', default='mils',
|
||||
)
|
||||
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)
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- 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)
|
||||
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',
|
||||
)
|
||||
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',
|
||||
)
|
||||
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='Surface Area UoM', default='sq_in',
|
||||
)
|
||||
weight = fields.Float(string='Weight (kg)', digits=(12, 4))
|
||||
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 = 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.')
|
||||
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.'),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- 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.')
|
||||
|
||||
_sql_constraints = [
|
||||
('fp_pricing_surcharge_rule_complexity_uniq', 'unique(rule_id, complexity)',
|
||||
'Only one surcharge per complexity level per rule.'),
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- 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)
|
||||
coating_config_id = fields.Many2one('fp.coating.config', string='Coating Config',
|
||||
help='Leave blank for a global rule.')
|
||||
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')
|
||||
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.')
|
||||
rush_surcharge_percent = fields.Float(string='Rush Surcharge %')
|
||||
sequence = fields.Integer(string='Sequence', default=10)
|
||||
active = fields.Boolean(string='Active', default=True)
|
||||
notes = fields.Text(string='Notes')
|
||||
@@ -0,0 +1,354 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
import math
|
||||
|
||||
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 = '<p class="text-muted">No matching pricing rule found.</p>'
|
||||
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 scaling ---
|
||||
# thickness_factor is a per-mil multiplier. A factor of 1.0
|
||||
# means linear scaling by thickness (e.g. 3 mils = 3x price).
|
||||
# A factor of 0.8 gives a volume discount (3 mils = 2.4x).
|
||||
thickness = rec.thickness_requested or 1.0
|
||||
unit_price *= thickness * rule.thickness_factor
|
||||
|
||||
# --- 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 + batch setup fees ---
|
||||
num_batches = (
|
||||
math.ceil(rec.quantity / rec.batch_size) if rec.batch_size
|
||||
else 1
|
||||
)
|
||||
total_setup = rule.setup_fee * num_batches
|
||||
subtotal = (unit_price * rec.quantity) + masking_cost + total_setup
|
||||
|
||||
# --- 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 ---
|
||||
sym = rec.currency_id.symbol or '$'
|
||||
lines = []
|
||||
method_label = dict(
|
||||
rule._fields['pricing_method'].selection
|
||||
).get(rule.pricing_method, '')
|
||||
lines.append(
|
||||
'<tr><td>Base (%s)</td><td class="text-end">%s%.2f x %d</td></tr>'
|
||||
% (method_label, sym, unit_price, rec.quantity)
|
||||
)
|
||||
if masking_cost:
|
||||
lines.append(
|
||||
'<tr><td>Masking (%d zones)</td><td class="text-end">%s%.2f</td></tr>'
|
||||
% (rec.masking_zones, sym, masking_cost)
|
||||
)
|
||||
if total_setup:
|
||||
lines.append(
|
||||
'<tr><td>Setup Fee (x%d batches)</td><td class="text-end">%s%.2f</td></tr>'
|
||||
% (num_batches, sym, total_setup)
|
||||
)
|
||||
if rush_amount:
|
||||
lines.append(
|
||||
'<tr><td>Rush Surcharge (%.0f%%)</td><td class="text-end">%s%.2f</td></tr>'
|
||||
% (rule.rush_surcharge_percent, sym, rush_amount)
|
||||
)
|
||||
if rec.shipping_fee:
|
||||
lines.append(
|
||||
'<tr><td>Shipping</td><td class="text-end">%s%.2f</td></tr>'
|
||||
% (sym, rec.shipping_fee)
|
||||
)
|
||||
if rec.delivery_fee:
|
||||
lines.append(
|
||||
'<tr><td>Delivery</td><td class="text-end">%s%.2f</td></tr>'
|
||||
% (sym, rec.delivery_fee)
|
||||
)
|
||||
lines.append(
|
||||
'<tr class="fw-bold"><td>Total</td><td class="text-end">%s%.2f</td></tr>'
|
||||
% (sym, total)
|
||||
)
|
||||
|
||||
rec.price_breakdown_html = (
|
||||
'<table class="table table-sm"><thead><tr>'
|
||||
'<th>Item</th><th class="text-end">Amount</th></tr></thead>'
|
||||
'<tbody>%s</tbody></table>'
|
||||
'<p class="text-muted small">Rule: %s (seq %d)</p>'
|
||||
% (''.join(lines), rule.name, rule.sequence)
|
||||
)
|
||||
|
||||
def _find_matching_rule(self):
|
||||
"""Find the best pricing rule matching this configurator's filters.
|
||||
|
||||
Scores rules by specificity -- most specific match wins.
|
||||
If no rule matches filters, returns None.
|
||||
"""
|
||||
rules = self.env['fp.pricing.rule'].search(
|
||||
[('active', '=', True)], 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
|
||||
if rule.coating_config_id:
|
||||
if rule.coating_config_id != self.coating_config_id:
|
||||
continue
|
||||
score += 4
|
||||
if rule.substrate_material:
|
||||
if rule.substrate_material != self.substrate_material:
|
||||
continue
|
||||
score += 2
|
||||
if rule.certification_level:
|
||||
if rule.certification_level != cert_level:
|
||||
continue
|
||||
score += 1
|
||||
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['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,
|
||||
})
|
||||
|
||||
coating_name = self.coating_config_id.name if self.coating_config_id else ''
|
||||
part_name = self.part_catalog_id.name if self.part_catalog_id else 'Custom Part'
|
||||
|
||||
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': '%s — %s (x%d)' % (coating_name, part_name, 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 <a href="/odoo/sale-order/%s">%s</a> 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'})
|
||||
@@ -0,0 +1,51 @@
|
||||
# -*- 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.'),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- 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'
|
||||
|
||||
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')
|
||||
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')
|
||||
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.')
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="group_fp_estimator" model="res.groups">
|
||||
<field name="name">Estimator</field>
|
||||
<field name="category_id" ref="fusion_plating.module_category_fusion_plating"/>
|
||||
<field name="implied_ids" eval="[(4, ref('fusion_plating.group_fusion_plating_supervisor'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_fp_shop_manager" model="res.groups">
|
||||
<field name="name">Shop Manager</field>
|
||||
<field name="category_id" ref="fusion_plating.module_category_fusion_plating"/>
|
||||
<field name="implied_ids" eval="[
|
||||
(4, ref('fusion_plating.group_fusion_plating_manager')),
|
||||
(4, ref('group_fp_estimator')),
|
||||
]"/>
|
||||
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,19 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
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
|
||||
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,fusion_plating_configurator.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
|
||||
access_fp_coating_config_operator,fp.coating.config.operator,model_fp_coating_config,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_coating_config_estimator,fp.coating.config.estimator,model_fp_coating_config,fusion_plating_configurator.group_fp_estimator,1,1,1,0
|
||||
access_fp_coating_config_manager,fp.coating.config.manager,model_fp_coating_config,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_pricing_rule_operator,fp.pricing.rule.operator,model_fp_pricing_rule,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_pricing_rule_estimator,fp.pricing.rule.estimator,model_fp_pricing_rule,fusion_plating_configurator.group_fp_estimator,1,1,1,0
|
||||
access_fp_pricing_rule_manager,fp.pricing.rule.manager,model_fp_pricing_rule,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_pricing_surcharge_operator,fp.pricing.complexity.surcharge.operator,model_fp_pricing_complexity_surcharge,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_pricing_surcharge_estimator,fp.pricing.complexity.surcharge.estimator,model_fp_pricing_complexity_surcharge,fusion_plating_configurator.group_fp_estimator,1,1,1,0
|
||||
access_fp_pricing_surcharge_manager,fp.pricing.complexity.surcharge.manager,model_fp_pricing_complexity_surcharge,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_quote_configurator_operator,fp.quote.configurator.operator,model_fp_quote_configurator,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_quote_configurator_estimator,fp.quote.configurator.estimator,model_fp_quote_configurator,fusion_plating_configurator.group_fp_estimator,1,1,1,0
|
||||
access_fp_quote_configurator_manager,fp.quote.configurator.manager,model_fp_quote_configurator,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== Coating Configuration List View ===== -->
|
||||
<record id="view_fp_coating_config_list" model="ir.ui.view">
|
||||
<field name="name">fp.coating.config.list</field>
|
||||
<field name="model">fp.coating.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Coating Configurations" decoration-muted="not active">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="process_type_id"/>
|
||||
<field name="phosphorus_level"/>
|
||||
<field name="thickness_min"/>
|
||||
<field name="thickness_max"/>
|
||||
<field name="spec_reference"/>
|
||||
<field name="certification_level"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Coating Configuration Form View ===== -->
|
||||
<record id="view_fp_coating_config_form" model="ir.ui.view">
|
||||
<field name="name">fp.coating.config.form</field>
|
||||
<field name="model">fp.coating.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Coating Configuration">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. EN Mid-Phos AMS 2404"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="process_type_id"/>
|
||||
<field name="recipe_id"/>
|
||||
<field name="phosphorus_level"/>
|
||||
<field name="certification_level"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="thickness_min"/>
|
||||
<field name="thickness_max"/>
|
||||
<field name="thickness_uom"/>
|
||||
<field name="spec_reference"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Treatments" name="treatments">
|
||||
<group>
|
||||
<group string="Pre-Treatments">
|
||||
<field name="pre_treatment_ids" widget="many2many_tags" nolabel="1"/>
|
||||
</group>
|
||||
<group string="Post-Treatments">
|
||||
<field name="post_treatment_ids" widget="many2many_tags" nolabel="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Description" name="description">
|
||||
<field name="description" placeholder="Detailed description of this coating configuration..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
<group>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Coating Configuration Search View ===== -->
|
||||
<record id="view_fp_coating_config_search" model="ir.ui.view">
|
||||
<field name="name">fp.coating.config.search</field>
|
||||
<field name="model">fp.coating.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="process_type_id"/>
|
||||
<field name="spec_reference"/>
|
||||
<separator/>
|
||||
<filter string="Commercial" name="commercial" domain="[('certification_level','=','commercial')]"/>
|
||||
<filter string="Mil-Spec" name="mil_spec" domain="[('certification_level','=','mil_spec')]"/>
|
||||
<filter string="Nadcap" name="nadcap" domain="[('certification_level','=','nadcap')]"/>
|
||||
<filter string="Nuclear" name="nuclear" domain="[('certification_level','=','nuclear')]"/>
|
||||
<separator/>
|
||||
<filter string="Low Phosphorus" name="low_phos" domain="[('phosphorus_level','=','low_phos')]"/>
|
||||
<filter string="Mid Phosphorus" name="mid_phos" domain="[('phosphorus_level','=','mid_phos')]"/>
|
||||
<filter string="High Phosphorus" name="high_phos" domain="[('phosphorus_level','=','high_phos')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Process Type" name="group_process_type" context="{'group_by':'process_type_id'}"/>
|
||||
<filter string="Certification Level" name="group_cert_level" context="{'group_by':'certification_level'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Window Action ===== -->
|
||||
<record id="action_fp_coating_config" model="ir.actions.act_window">
|
||||
<field name="name">Coating Configurations</field>
|
||||
<field name="res_model">fp.coating.config</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_coating_config_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No coating configurations defined yet
|
||||
</p>
|
||||
<p>
|
||||
Define coating setups with process type, phosphorus level,
|
||||
thickness range, spec reference, and required treatments.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== SALES submenu under Fusion Plating root ===== -->
|
||||
<menuitem id="menu_fp_sales"
|
||||
name="Sales"
|
||||
parent="fusion_plating.menu_fp_root"
|
||||
sequence="1"
|
||||
groups="group_fp_estimator"/>
|
||||
|
||||
<menuitem id="menu_fp_quotations"
|
||||
name="Quotations"
|
||||
parent="menu_fp_sales"
|
||||
action="action_fp_quotations"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fp_sale_orders"
|
||||
name="Sale Orders"
|
||||
parent="menu_fp_sales"
|
||||
action="action_fp_sale_orders"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fp_customers"
|
||||
name="Customers"
|
||||
parent="menu_fp_sales"
|
||||
action="action_fp_customers"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_fp_part_catalog"
|
||||
name="Part Catalog"
|
||||
parent="menu_fp_sales"
|
||||
action="action_fp_part_catalog"
|
||||
sequence="40"/>
|
||||
|
||||
<!-- ===== CONFIGURATOR submenu ===== -->
|
||||
<menuitem id="menu_fp_configurator"
|
||||
name="Configurator"
|
||||
parent="fusion_plating.menu_fp_root"
|
||||
sequence="2"
|
||||
groups="group_fp_estimator"/>
|
||||
|
||||
<menuitem id="menu_fp_new_quote"
|
||||
name="New Quote"
|
||||
parent="menu_fp_configurator"
|
||||
action="action_fp_quote_configurator"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fp_coating_configs"
|
||||
name="Coating Configurations"
|
||||
parent="menu_fp_configurator"
|
||||
action="action_fp_coating_config"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fp_pricing_rules"
|
||||
name="Pricing Rules"
|
||||
parent="menu_fp_configurator"
|
||||
action="action_fp_pricing_rule"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_fp_treatments"
|
||||
name="Treatments"
|
||||
parent="menu_fp_configurator"
|
||||
action="action_fp_treatment"
|
||||
sequence="40"/>
|
||||
|
||||
<!-- ===== Customers action (for menu) ===== -->
|
||||
<record id="action_fp_customers" model="ir.actions.act_window">
|
||||
<field name="name">Customers</field>
|
||||
<field name="res_model">res.partner</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="domain">[('customer_rank', '>', 0)]</field>
|
||||
<field name="context">{'default_customer_rank': 1}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,141 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== Part Catalog List View ===== -->
|
||||
<record id="view_fp_part_catalog_list" model="ir.ui.view">
|
||||
<field name="name">fp.part.catalog.list</field>
|
||||
<field name="model">fp.part.catalog</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Part Catalog" decoration-muted="not active">
|
||||
<field name="partner_id"/>
|
||||
<field name="part_number"/>
|
||||
<field name="revision"/>
|
||||
<field name="substrate_material"/>
|
||||
<field name="surface_area"/>
|
||||
<field name="complexity"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Part Catalog Form View ===== -->
|
||||
<record id="view_fp_part_catalog_form" model="ir.ui.view">
|
||||
<field name="name">fp.part.catalog.form</field>
|
||||
<field name="model">fp.part.catalog</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Part Catalog">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. Valve Body Housing"/></h1>
|
||||
<field name="part_number" placeholder="Customer part number (e.g. VS-R392007E01)"/>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<field name="revision"/>
|
||||
<field name="substrate_material"/>
|
||||
<field name="geometry_source"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="surface_area"/>
|
||||
<field name="surface_area_uom"/>
|
||||
<field name="weight"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Dimensions & Complexity" name="dimensions">
|
||||
<group>
|
||||
<group>
|
||||
<field name="dimensions_length"/>
|
||||
<field name="dimensions_width"/>
|
||||
<field name="dimensions_height"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="complexity"/>
|
||||
<field name="masking_zones"/>
|
||||
<field name="has_blind_holes"/>
|
||||
<field name="has_recesses"/>
|
||||
<field name="has_threads"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="masking_description" placeholder="e.g. Mask threaded holes, mask bore ID"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Attachments" name="attachments">
|
||||
<group>
|
||||
<field name="model_attachment_id"/>
|
||||
<field name="drawing_attachment_ids" widget="many2many_binary"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Notes" name="notes">
|
||||
<field name="notes" placeholder="Additional notes about this part..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Part Catalog Search View ===== -->
|
||||
<record id="view_fp_part_catalog_search" model="ir.ui.view">
|
||||
<field name="name">fp.part.catalog.search</field>
|
||||
<field name="model">fp.part.catalog</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="part_number"/>
|
||||
<field name="partner_id"/>
|
||||
<separator/>
|
||||
<filter string="Aluminium" name="material_aluminium" domain="[('substrate_material','=','aluminium')]"/>
|
||||
<filter string="Steel" name="material_steel" domain="[('substrate_material','=','steel')]"/>
|
||||
<filter string="Stainless Steel" name="material_stainless" domain="[('substrate_material','=','stainless')]"/>
|
||||
<filter string="Copper" name="material_copper" domain="[('substrate_material','=','copper')]"/>
|
||||
<filter string="Titanium" name="material_titanium" domain="[('substrate_material','=','titanium')]"/>
|
||||
<filter string="Other" name="material_other" domain="[('substrate_material','=','other')]"/>
|
||||
<separator/>
|
||||
<filter string="Simple" name="complexity_simple" domain="[('complexity','=','simple')]"/>
|
||||
<filter string="Moderate" name="complexity_moderate" domain="[('complexity','=','moderate')]"/>
|
||||
<filter string="Complex" name="complexity_complex" domain="[('complexity','=','complex')]"/>
|
||||
<filter string="Very Complex" name="complexity_very_complex" domain="[('complexity','=','very_complex')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Customer" name="group_partner" context="{'group_by':'partner_id'}"/>
|
||||
<filter string="Material" name="group_material" context="{'group_by':'substrate_material'}"/>
|
||||
<filter string="Complexity" name="group_complexity" context="{'group_by':'complexity'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Window Action ===== -->
|
||||
<record id="action_fp_part_catalog" model="ir.actions.act_window">
|
||||
<field name="name">Part Catalog</field>
|
||||
<field name="res_model">fp.part.catalog</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_part_catalog_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No parts in the catalog yet
|
||||
</p>
|
||||
<p>
|
||||
Add customer parts with geometry, material, and complexity data
|
||||
for instant re-quoting on repeat orders.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== Pricing Rule List View ===== -->
|
||||
<record id="view_fp_pricing_rule_list" model="ir.ui.view">
|
||||
<field name="name">fp.pricing.rule.list</field>
|
||||
<field name="model">fp.pricing.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Pricing Rules" decoration-muted="not active">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="coating_config_id"/>
|
||||
<field name="substrate_material"/>
|
||||
<field name="certification_level"/>
|
||||
<field name="pricing_method"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
<field name="base_rate"/>
|
||||
<field name="minimum_charge"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Pricing Rule Form View ===== -->
|
||||
<record id="view_fp_pricing_rule_form" model="ir.ui.view">
|
||||
<field name="name">fp.pricing.rule.form</field>
|
||||
<field name="model">fp.pricing.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Pricing Rule">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. EN Mid-Phos Aluminium — Commercial"/></h1>
|
||||
</div>
|
||||
<group string="Filters">
|
||||
<group>
|
||||
<field name="coating_config_id"/>
|
||||
<field name="substrate_material"/>
|
||||
<field name="certification_level"/>
|
||||
</group>
|
||||
<group>
|
||||
<div class="text-muted" colspan="2">
|
||||
Leave filter fields blank to create a global rule
|
||||
that matches any configuration.
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Pricing">
|
||||
<group>
|
||||
<field name="pricing_method"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="base_rate"/>
|
||||
<field name="thickness_factor"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="masking_rate_per_zone"/>
|
||||
<field name="setup_fee"/>
|
||||
<field name="minimum_charge"/>
|
||||
<field name="rush_surcharge_percent"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Complexity Surcharges" name="surcharges">
|
||||
<field name="complexity_surcharge_ids">
|
||||
<list editable="bottom">
|
||||
<field name="complexity"/>
|
||||
<field name="surcharge_percent"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Notes" name="notes">
|
||||
<field name="notes" placeholder="Internal notes about this pricing rule..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
<group>
|
||||
<field name="sequence"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Pricing Rule Search View ===== -->
|
||||
<record id="view_fp_pricing_rule_search" model="ir.ui.view">
|
||||
<field name="name">fp.pricing.rule.search</field>
|
||||
<field name="model">fp.pricing.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="coating_config_id"/>
|
||||
<separator/>
|
||||
<filter string="Per Square Inch" name="per_sqin" domain="[('pricing_method','=','per_sqin')]"/>
|
||||
<filter string="Per Square Foot" name="per_sqft" domain="[('pricing_method','=','per_sqft')]"/>
|
||||
<filter string="Per Piece" name="per_piece" domain="[('pricing_method','=','per_piece')]"/>
|
||||
<filter string="Flat Rate" name="flat_rate" domain="[('pricing_method','=','flat_rate')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Coating Config" name="group_coating_config" context="{'group_by':'coating_config_id'}"/>
|
||||
<filter string="Pricing Method" name="group_pricing_method" context="{'group_by':'pricing_method'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Window Action ===== -->
|
||||
<record id="action_fp_pricing_rule" model="ir.actions.act_window">
|
||||
<field name="name">Pricing Rules</field>
|
||||
<field name="res_model">fp.pricing.rule</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_pricing_rule_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No pricing rules defined yet
|
||||
</p>
|
||||
<p>
|
||||
Define formula-based pricing rules matched by coating
|
||||
configuration, substrate material, and certification level.
|
||||
The first matching rule (by sequence) wins.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== Configurator Form View ===== -->
|
||||
<record id="view_fp_quote_configurator_form" model="ir.ui.view">
|
||||
<field name="name">fp.quote.configurator.form</field>
|
||||
<field name="model">fp.quote.configurator</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Quote Configurator">
|
||||
<header>
|
||||
<button name="action_create_quotation"
|
||||
string="Create Quotation"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
confirm="This will create a Sale Order from this configurator session. Continue?"
|
||||
invisible="state != 'draft'"/>
|
||||
<button name="action_cancel"
|
||||
string="Cancel"
|
||||
type="object"
|
||||
invisible="state != 'draft'"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<!-- Customer + Part / Coating + Quantity -->
|
||||
<group>
|
||||
<group string="Customer & Part">
|
||||
<field name="partner_id"/>
|
||||
<field name="part_catalog_id"/>
|
||||
</group>
|
||||
<group string="Coating & Quantity">
|
||||
<field name="coating_config_id"/>
|
||||
<field name="quantity"/>
|
||||
<field name="batch_size"/>
|
||||
</group>
|
||||
</group>
|
||||
<!-- Geometry / Options -->
|
||||
<group>
|
||||
<group string="Geometry">
|
||||
<field name="surface_area"/>
|
||||
<field name="surface_area_uom"/>
|
||||
<field name="thickness_requested"/>
|
||||
<field name="substrate_material"/>
|
||||
</group>
|
||||
<group string="Options">
|
||||
<field name="complexity"/>
|
||||
<field name="masking_zones"/>
|
||||
<field name="rush_order"/>
|
||||
<field name="turnaround_days"/>
|
||||
</group>
|
||||
</group>
|
||||
<!-- Delivery / Fees -->
|
||||
<group>
|
||||
<group string="Delivery & Fees">
|
||||
<field name="delivery_method"/>
|
||||
<field name="shipping_fee"/>
|
||||
<field name="delivery_fee"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Pricing"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="calculated_price" widget="monetary" readonly="1"
|
||||
class="fw-bold fs-4"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="estimator_override_price" widget="monetary"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="price_breakdown_html" readonly="1" nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Sale Order" name="sale_order">
|
||||
<group>
|
||||
<field name="sale_order_id" readonly="1"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Notes" name="notes">
|
||||
<field name="notes" placeholder="Internal notes about this quote..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Configurator List View ===== -->
|
||||
<record id="view_fp_quote_configurator_list" model="ir.ui.view">
|
||||
<field name="name">fp.quote.configurator.list</field>
|
||||
<field name="model">fp.quote.configurator</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Quote Configurators"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-muted="state == 'cancelled'"
|
||||
default_order="create_date desc">
|
||||
<field name="create_date" string="Date"/>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="coating_config_id"/>
|
||||
<field name="surface_area"/>
|
||||
<field name="quantity"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
<field name="calculated_price"/>
|
||||
<field name="estimator_override_price" string="Final Price"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'confirmed'"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-danger="state == 'cancelled'"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Configurator Search View ===== -->
|
||||
<record id="view_fp_quote_configurator_search" model="ir.ui.view">
|
||||
<field name="name">fp.quote.configurator.search</field>
|
||||
<field name="model">fp.quote.configurator</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="coating_config_id"/>
|
||||
<separator/>
|
||||
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
|
||||
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'confirmed')]"/>
|
||||
<filter string="Cancelled" name="cancelled" domain="[('state', '=', 'cancelled')]"/>
|
||||
<group>
|
||||
<filter string="Customer" name="group_customer" context="{'group_by': 'partner_id'}"/>
|
||||
<filter string="Coating Config" name="group_coating" context="{'group_by': 'coating_config_id'}"/>
|
||||
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Window Action ===== -->
|
||||
<record id="action_fp_quote_configurator" model="ir.actions.act_window">
|
||||
<field name="name">Quote Configurator</field>
|
||||
<field name="res_model">fp.quote.configurator</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_quote_configurator_search"/>
|
||||
<field name="context">{'search_default_draft': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new quote configurator session
|
||||
</p>
|
||||
<p>
|
||||
Select a customer and coating configuration, enter part geometry,
|
||||
and the pricing engine will calculate a quote. The estimator can
|
||||
override the calculated price before creating a sale order.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== Treatment List View ===== -->
|
||||
<record id="view_fp_treatment_list" model="ir.ui.view">
|
||||
<field name="name">fp.treatment.list</field>
|
||||
<field name="model">fp.treatment</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Treatments">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="treatment_type"/>
|
||||
<field name="default_duration_minutes"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
<field name="default_cost"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Treatment Form View ===== -->
|
||||
<record id="view_fp_treatment_form" model="ir.ui.view">
|
||||
<field name="name">fp.treatment.form</field>
|
||||
<field name="model">fp.treatment</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Treatment">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. Bead Blast"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="treatment_type"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="default_duration_minutes"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="default_cost"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="description" placeholder="Description of this treatment step..."/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Treatment Search View ===== -->
|
||||
<record id="view_fp_treatment_search" model="ir.ui.view">
|
||||
<field name="name">fp.treatment.search</field>
|
||||
<field name="model">fp.treatment</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<separator/>
|
||||
<filter string="Pre-Treatment" name="pre" domain="[('treatment_type','=','pre')]"/>
|
||||
<filter string="Post-Treatment" name="post" domain="[('treatment_type','=','post')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Type" name="group_type" context="{'group_by':'treatment_type'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Window Action ===== -->
|
||||
<record id="action_fp_treatment" model="ir.actions.act_window">
|
||||
<field name="name">Treatments</field>
|
||||
<field name="res_model">fp.treatment</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_treatment_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No treatments defined yet
|
||||
</p>
|
||||
<p>
|
||||
Add pre-treatment steps (bead blast, zincate, acid etch) and
|
||||
post-treatment steps (bake, passivate, chromate seal).
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== Inherit SO Form — add Plating tab ===== -->
|
||||
<record id="view_sale_order_form_fp" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.fp.configurator</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Plating" name="plating_tab">
|
||||
<group>
|
||||
<group string="Part & Coating">
|
||||
<field name="x_fc_configurator_id" readonly="1"/>
|
||||
<field name="x_fc_part_catalog_id"/>
|
||||
<field name="x_fc_coating_config_id"/>
|
||||
</group>
|
||||
<group string="Customer PO">
|
||||
<field name="x_fc_po_number"/>
|
||||
<field name="x_fc_po_attachment_id"/>
|
||||
<field name="x_fc_po_received"/>
|
||||
<field name="x_fc_po_override"
|
||||
groups="fusion_plating.group_fusion_plating_manager"/>
|
||||
<field name="x_fc_po_override_reason"
|
||||
invisible="not x_fc_po_override"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Invoicing">
|
||||
<field name="x_fc_invoice_strategy"/>
|
||||
<field name="x_fc_deposit_percent"
|
||||
invisible="x_fc_invoice_strategy != 'deposit'"/>
|
||||
</group>
|
||||
<group string="Delivery">
|
||||
<field name="x_fc_rush_order"/>
|
||||
<field name="x_fc_delivery_method"/>
|
||||
<field name="x_fc_receiving_status"/><!-- Will become computed when fusion_plating_receiving is installed -->
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Custom SO List View for Fusion Plating ===== -->
|
||||
<record id="view_sale_order_list_fp" model="ir.ui.view">
|
||||
<field name="name">sale.order.list.fp</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Sale Orders" decoration-info="state == 'draft'"
|
||||
decoration-muted="state == 'cancel'">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="x_fc_po_number"/>
|
||||
<field name="x_fc_part_catalog_id" optional="show"/>
|
||||
<field name="x_fc_coating_config_id" optional="show"/>
|
||||
<field name="amount_total" sum="Total"/>
|
||||
<field name="x_fc_receiving_status" widget="badge"
|
||||
decoration-warning="x_fc_receiving_status == 'not_received'"
|
||||
decoration-success="x_fc_receiving_status in ('received','inspected')"/>
|
||||
<field name="x_fc_delivery_method" optional="show"/>
|
||||
<field name="state" widget="badge"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Window Action — Quotations (for Fusion Plating menu) ===== -->
|
||||
<record id="action_fp_quotations" model="ir.actions.act_window">
|
||||
<field name="name">Quotations</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="domain">[('state', 'in', ('draft', 'sent'))]</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_fp')})]"/>
|
||||
<field name="context">{'default_x_fc_delivery_method': 'shipping_partner'}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new quotation
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Window Action — Confirmed Sale Orders ===== -->
|
||||
<record id="action_fp_sale_orders" model="ir.actions.act_window">
|
||||
<field name="name">Sale Orders</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="domain">[('state', 'in', ('sale', 'done'))]</field>
|
||||
<field name="view_ids" eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_fp')})]"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user