folder rename

This commit is contained in:
gsinghpal
2026-04-16 20:53:53 -04:00
parent 3f3ddcbab4
commit 7c7ef06057
634 changed files with 0 additions and 0 deletions

View File

@@ -1,345 +0,0 @@
# Fusion Plating — Claude Code Instructions
## Project
Fusion Plating is a multi-module Odoo 19 ERP for electroless nickel plating and metal finishing shops. Built by Nexa Systems for EN Technologies (the client). Replaces Steelhead Software.
## Module Structure (30 modules)
```
fusion_plating/ — Core: facilities, process types, tanks, baths, chemistry, recipes
fusion_plating_batch/ — Rack/barrel batch tracking (FpBatch, FpBatchChemistry)
fusion_plating_kpi/ — KPI definitions, daily auto-compute, dashboard views
fusion_plating_configurator/ — Quotation configurator, pricing engine, part catalog, 3D viewer
fusion_plating_receiving/ — Parts receiving, inspection, damage logging
fusion_plating_invoicing/ — Invoice strategies (deposit/progress/net/COD), account holds
fusion_plating_certificates/ — Certificate registry (CoC, thickness reports), Fischerscope data
fusion_plating_notifications/ — Auto-email engine, notification templates, audit log
fusion_plating_shopfloor/ — Tablet UI, plant overview kanban, process tree visualization
fusion_plating_portal/ — Customer portal + self-service configurator wizard
fusion_plating_reports/ — PDF reports (WO margin, discharge sample, CoC, etc.)
fusion_plating_compliance/ — Compliance framework, jurisdictions
fusion_plating_compliance_on/ — Ontario compliance reference data (data-only, no menus)
fusion_plating_compliance_tor/ — Toronto bylaw discharge limits (data-only, no menus)
fusion_plating_aerospace/ — AS9100 / Nadcap
fusion_plating_nuclear/ — CSA N299 / CNSC
fusion_plating_cgp/ — Controlled Goods Program
fusion_plating_safety/ — SDS, WHMIS, JHSC
fusion_plating_quality/ — QMS (NCR, CAPA, calibration)
fusion_plating_logistics/ — Pickup & delivery, chain of custody
fusion_plating_culture/ — Values / fundamentals
fusion_plating_bridge_mrp/ — MRP integration (recipe→WO, portal job, work order priorities)
fusion_plating_bridge_sign/ — Digital signatures
fusion_plating_bridge_quality/ — Quality bridge
fusion_plating_bridge_documents/ — Odoo Documents integration (NCR, CAPA, FAIR, Doc Control)
fusion_plating_process_en/ — Electroless nickel process pack
fusion_plating_process_chrome/ — Chrome process pack
fusion_plating_process_anodize/ — Anodizing process pack
fusion_plating_process_black_oxide/ — Black oxide process pack
fusion_tasks/ — Local delivery dispatch (GPS, maps, driver scheduling)
```
## Menu Structure (Plating App)
The Plating app (`menu_fp_root`, seq 46) has these top-level menus:
| Seq | Menu | Module | Children |
|-----|------|--------|----------|
| 3 | KPIs | fusion_plating_kpi | KPIs, KPI History, Production/Quality/Finance dashboards |
| 5 | Sales | fusion_plating_configurator + portal | Quotations, Sale Orders, Customers, Part Catalog, Quote Requests, Portal Jobs |
| 8 | Configurator | fusion_plating_configurator | New Quote, Coating Configs, Pricing Rules, Treatments |
| 12 | Shop Floor | fusion_plating_shopfloor | Plant Overview, Tablet Station, Bake Windows, First-Piece Gates |
| 15 | Receiving | fusion_plating_receiving | All Receiving, Pending Inspection, Discrepancies |
| 18 | Operations | fusion_plating (core) | Process Recipes, Production Priorities (bridge_mrp), Batches (batch), Baths, Chemistry Logs, Tanks |
| 25 | Certificates | fusion_plating_certificates | All, CoC, Thickness Reports |
| 30 | Quality | fusion_plating_quality | Holds, NCRs, CAPAs, FAIR, Audits, Doc Control |
| 40 | Compliance | fusion_plating_compliance | Permits, Discharge, Waste, Calendar, Spills, Config |
| 45 | Safety | fusion_plating_safety | SDS, Training, Exposure, JHSC, Incidents, PPE |
| 50 | Logistics | fusion_plating_logistics + fusion_tasks | Pickups, Deliveries, Routes, CoC, POD, Field Tasks, Task Map, Task Calendar |
| 60 | Aerospace | fusion_plating_aerospace | AS9100, Nadcap, Counterfeit, Config Items, Risk |
| 65 | Nuclear | fusion_plating_nuclear | Program, ITP, 10CFR21, Pedigree, CNSC |
| 70 | CGP | fusion_plating_cgp | Registration, AI, PSA, Visitors, Goods, Shipments, Security, Access Log |
| 80 | Culture | fusion_plating_culture | Values, Recognitions |
| 90 | Configuration | fusion_plating (core) + many | Facilities, Work Centres, Process Categories/Types, Bath Params, Stations, Ovens, Invoice Strategy, Account Holds, Training Types, Chemicals, Notification Templates/Log, Calibration, Specs, AVL, Value Sets/Rotations, N299 Levels, Vehicles |
**Field Service** (`fusion_tasks`) also has its own standalone root app (seq 45) with Map View, Tasks, Calendar, Configuration. The same task actions are also accessible under Plating > Logistics.
**Key rule**: Sales menu is unified in `fusion_plating_configurator`. Portal module adds Quote Requests + Portal Jobs as children (referencing `fusion_plating_configurator.menu_fp_sales`). Do NOT create a separate Sales menu in portal.
## Critical Rules — Odoo 19
1. **NEVER code from memory** — Read reference files from the server first.
2. **Backend OWL**: `static template`, `static props = ["*"]`, standalone `rpc()` from `@web/core/network/rpc`. NOT `useService("rpc")`.
3. **HTTP routes**: `type="jsonrpc"` — NOT `type="json"` (deprecated in Odoo 19).
4. **Search views**: NO `group expand="0"`, NO `string` attribute on `<search>`, NO `<group string="...">` wrapper for group-by filters. Use bare `<group>` for group-by.
5. **res.config.settings**: Only boolean/integer/float/char/selection/many2one/datetime. NO Date fields.
6. **res.groups**: Use `privilege_id` (NOT `category_id`). `user_ids` is OK but the deprecated `users` alias is NOT. Always include `sequence` field.
7. **Field params**: `parent_path` does NOT accept `unaccent` parameter in Odoo 19.
8. **SCSS borders**: Use `$border-color` (SCSS variable) for card borders, NOT `color-mix()` in border shorthand — the SCSS compiler drops it. `color-mix()` works fine in `background-color`, `box-shadow`, etc.
9. **Theme awareness**: All colours must use CSS custom properties (`var(--bs-body-bg)`, `var(--bs-body-color)`, `var(--bs-border-color)`, `var(--bs-secondary-color)`, `var(--o-action)`). NO hardcoded hex. See `fusion_plating_shopfloor.scss` as the gold standard.
10. **XML comments**: No double-hyphens (`--`) inside `<!-- -->` comments — invalid XML, causes lxml parse error.
11. **XML data ordering**: Window actions must be defined BEFORE `<menuitem>` elements that reference them in the same file.
12. **Module install on new modules**: Use `--update=base` alongside `-i MODULE` to ensure Odoo rescans the addons path and finds the new module directory.
13. **Implied group cascade**: `implied_ids` on `res.groups` does NOT reliably propagate to users on module install. Always include `user_ids` to explicitly assign admin, or fix via SQL post-install.
## Naming
- **New custom models** (post-2026-04): `fp.*` prefix (e.g. `fp.part.catalog`, `fp.certificate`)
- **Existing custom models**: Keep `fusion.plating.*` (e.g. `fusion.plating.portal.job`, `fusion.plating.delivery`)
- **New fields on standard Odoo models**: `x_fc_*` prefix
- **Legacy fields**: `x_studio_*`
- Canadian English for all user-facing text
- SCSS class prefix: `o_fp_*` (shopfloor: `o_fp_po_*`, `o_fp_pt_*`; recipes: `o_fp_recipe_*`)
- Monetary fields: always pair with `currency_id` field on the same model
## Process Recipe System (NEW — v19.0.2.x)
**Model**: `fusion.plating.process.node` (in `fusion_plating` core)
- Hierarchical tree with `_parent_store = True`
- Node types: `recipe`, `sub_process`, `operation`, `step`
- Companion model: `fusion.plating.process.node.input` (operator inputs)
- `icon` is a Selection field (24 curated plating icons), NOT a Char
- Auto-icon: JS `guessIcon(name)` maps keywords → icons when adding nodes
- OWL tree editor: registered as `fp_recipe_tree_editor` client action
- Controller: `fusion_plating/controllers/recipe_controller.py` (7 endpoints)
- SCSS: `fusion_plating/static/src/scss/recipe_tree_editor.scss`
### Recipe Endpoints
```
POST /fp/recipe/tree — full nested tree for OWL editor
POST /fp/recipe/node/create — add child node
POST /fp/recipe/node/write — update fields
POST /fp/recipe/node/unlink — delete + cascade
POST /fp/recipe/node/reorder — bulk sequence update
POST /fp/recipe/node/move — change parent_id
POST /fp/recipe/duplicate — deep-copy recipe
```
### Steelhead Features Status
| Feature | Status |
|---------|--------|
| Hierarchical process tree | Done |
| Node types (recipe/sub/op/step) | Done |
| Auto-complete flag | Done |
| Customer visible flag | Done |
| Manual/automated flag | Done |
| Requires sign-off | Done |
| Opt In/Out (disabled/opt-in/opt-out) | Done |
| Icon picker | Done |
| Time tracking (created/updated with seconds) | Done |
| Operator inputs | Done |
| Description (rich text) | Done |
| File attachments (via mail.thread) | Done |
| OWL tree editor with drag-drop | Done |
| Tags | Not yet |
| Dashboard Transitions | Not yet |
| Treatment Groups / Choices | Not yet |
| Go To Node Options | Not yet |
| Spec Fields | Not yet |
### Client Recipes Created
- `ENP-ALUM-BASIC` — Electroless Nickel Plating Aluminium Basic (9 operations, 15 steps). Data file: `fusion_plating/data/fp_recipe_enp_alum_basic.xml`
## Plant Overview Dashboard
- OWL client action: `fp_plant_overview` in `fusion_plating_shopfloor`
- Kanban columns = work centres, cards = active `mrp.workorder` records
- Drag & drop between columns (writes `workcenter_id` on the work order)
- Endpoint: `POST /fp/shopfloor/plant_overview`
- Move endpoint: `POST /fp/shopfloor/plant_overview/move_card`
- Auto-refreshes every 30s
## Deployment
### odoo-entech (LXC 111 on pve-worker5)
- **Type**: Native Odoo (apt package, NOT Docker)
- **IP**: 10.200.1.26
- **DB**: `admin` (PostgreSQL local, user `odoo`)
- **Config**: `/etc/odoo/odoo.conf`
- **Addons**: `/mnt/extra-addons/custom/` (fusion_plating modules live here)
- **Service**: `systemctl {start|stop|restart} odoo`
- **Update command**:
```bash
ssh pve-worker5 "pct exec 111 -- bash -c 'systemctl stop odoo && su - odoo -s /bin/bash -c \"/usr/bin/odoo -c /etc/odoo/odoo.conf -d admin -u MODULE_NAME --stop-after-init\" && systemctl start odoo'"
```
- **Copy files**: `cat LOCAL_FILE | ssh pve-worker5 "pct exec 111 -- bash -c 'cat > /mnt/extra-addons/custom/REMOTE_PATH'"`
- **IMPORTANT**: Must pass `-c /etc/odoo/odoo.conf` or Odoo won't find the repackaged enterprise addons
### odoo-trial (VM 316 on pve-worker1)
- **Type**: Docker (container `odoo-trial-app`, db `odoo-trial-db`)
- **DB**: `trial` (user `odoo`)
- **Host addons path**: `/opt/odoo/custom-addons/` → mounts as `/mnt/extra-addons/` in Docker
- **Docker network**: `odoo_odoo-network`
- **Copy files** (base64 pipe through qm guest exec):
```bash
B64=$(base64 -w0 "LOCAL_FILE")
ssh pve-worker1 "qm guest exec 316 -- bash -c 'echo $B64 | base64 -d > /opt/odoo/custom-addons/REMOTE_PATH'"
```
- **Clear asset cache** (required after SCSS/JS changes):
```bash
ssh pve-worker1 "qm guest exec 316 -- bash -c \"docker exec odoo-trial-db psql -U odoo -d trial -c \\\"DELETE FROM ir_attachment WHERE url LIKE '%/web/assets/%';\\\"\""
```
- **Update command**:
```bash
ssh pve-worker1 "qm guest exec 316 -- bash -c 'docker stop odoo-trial-app && docker run --rm --network odoo_odoo-network -v odoo_odoo-data:/var/lib/odoo -v /opt/odoo/custom-addons:/mnt/extra-addons -v /opt/odoo/enterprise-addons:/mnt/enterprise-addons -v /opt/odoo/odoo.conf:/etc/odoo/odoo.conf odoo:19 odoo -d trial -u MODULE_NAME --stop-after-init && docker start odoo-trial-app'"
```
### Git Push
```bash
cd K:/Github/Odoo-Modules/fusion-plating && git push origin main
```
Pushes to both GitHub and Gitea (nexasystems.ca) via multiple remotes.
## Supabase Knowledge Base
Project: `nexasystems` (id: `ikvdlqkbqsitabxidvnq`)
- `fusionapps.decisions` — past architecture decisions
- `fusionapps.issues` — known issues and fixes
- `fusionapps.code_snippets` — reference code
- `fusionapps.quick_commands` — deployment and admin commands
- `fusionapps.vm_registry` — VM inventory
- `fusionapps.proxmox_nodes` — cluster node specs
## End-to-End Business Workflow
### Full Lifecycle (What Exists Today)
```
┌─ QUOTATION ──────────────────────────────────────────────────────┐
│ 1. Customer submits RFQ on portal [DONE] │
│ → FpQuoteRequest (state: new → under_review → quoted) │
│ → Model: fusion_plating_portal/models/fp_quote_request.py │
│ │
│ 2. Customer accepts → "Create Sale Order" button [DONE] │
│ → action_create_sale_order() creates SO with lines │
│ → Links SO origin back to RFQ ref │
│ │
│ 3. SO confirmed → MRP creates Manufacturing Order [DONE] │
│ → Standard Odoo sale_mrp flow │
└──────────────────────────────────────────────────────────────────┘
┌─ MANUFACTURING ──────────────────────────────────────────────────┐
│ 4. MO confirmed → Portal Job auto-created [DONE] │
│ → MrpProduction.action_confirm() override │
│ → Creates FpPortalJob (state: in_progress) │
│ → Links via x_fc_portal_job_id │
│ │
│ 5. Planner assigns recipe + configures steps [DONE] │
│ → x_fc_recipe_id set on MO │
│ → Opens fp.recipe.config.wizard for opt-in/out │
│ → Creates fusion.plating.job.node.override records │
│ │
│ 6. Work orders generated from recipe [DONE] │
│ → _generate_workorders_from_recipe() in bridge_mrp │
│ → One WO per operation node, steps become WO instructions │
│ → Respects opt-in/out overrides from job.node.override │
│ │
│ 7. Operators execute WOs on shopfloor [DONE] │
│ → Plant Overview kanban (drag between work centres) │
│ → Batch chemistry tracking (FpBatch + FpBatchChemistry) │
│ → Quality holds (FpQualityHold → FpNcr → FpCapa) │
│ │
│ 8. MO marked done → Portal job ready_to_ship [DONE] │
│ → MrpProduction.button_mark_done() override │
│ → Auto-creates FpDelivery (draft) │
└──────────────────────────────────────────────────────────────────┘
┌─ SHIPPING & INVOICING ───────────────────────────────────────────┐
│ 9. CoC report generated [DONE] │
│ → report_coc.xml (PDF with job info, certification, sig) │
│ → Attached to delivery + portal job │
│ │
│ 10. Delivery scheduled & executed [DONE] │
│ → FpDelivery: draft → scheduled → en_route → delivered │
│ → Chain of custody auto-logged (FpChainOfCustody) │
│ → Proof of delivery captured (FpProofOfDelivery) │
│ → Routes with stops (FpRoute + FpRouteStop) │
│ │
│ 11. Delivery marked → Portal job shipped [DONE] │
│ → FpDelivery.action_mark_delivered() override │
│ → Sets actual_ship_date + tracking_ref on portal job │
│ │
│ 12. Account hold check before invoicing [DONE] │
│ → x_fc_account_hold on res.partner (fusion_plating_invoicing)│
│ → Blocks SO confirm, invoice post, shipping for non-managers │
│ │
│ 13. Invoice posted → Portal job complete [DONE] │
│ → AccountMove.action_post() override │
│ → Sets invoice_ref on portal job, state → complete │
│ │
│ 14. Auto-email with CoC + Invoice + Tracking [DONE] │
│ → fusion_plating_notifications module │
│ → fp.notification.template (configurable per trigger event) │
│ → fp.notification.log (audit trail) │
└──────────────────────────────────────────────────────────────────┘
┌─ CUSTOMER PORTAL ────────────────────────────────────────────────┐
│ 15. Customer sees on portal [DONE] │
│ → Job progress bar (received → complete) │
│ → CoC download, invoice access, tracking ref │
│ → Quote request history │
└──────────────────────────────────────────────────────────────────┘
```
### Per-Job Recipe Overrides (v19.0.2.0.0 bridge_mrp)
- `x_fc_recipe_id` on `mrp.production` → links MO to recipe
- `fusion.plating.job.node.override` → per-job opt-in/out decisions
- `fp.recipe.config.wizard` → checklist wizard for planner
- "Overrides" stat button on MO form
- Located in `fusion_plating_bridge_mrp`
### All Gaps Resolved (2026-04-12/13)
| Gap | Resolution | Module |
|-----|-----------|--------|
| **Recipe → Work Orders** | `_generate_workorders_from_recipe()` — one WO per operation, steps become instructions | `fusion_plating_bridge_mrp` v2.1.0 |
| **Account Hold Check** | `x_fc_account_hold` on res.partner, blocks SO/invoice/shipping for non-managers | `fusion_plating_invoicing` |
| **Auto-Email Package** | `fp.notification.template` + `fp.notification.log` with hooks on SO confirm, receiving, invoice | `fusion_plating_notifications` |
| **Quotation Configurator** | Part catalog, coating configs, pricing engine, 3D STL viewer, portal wizard | `fusion_plating_configurator` |
| **Parts Receiving** | Receiving records, inspection, damage logging, SO auto-create, MRP soft gate | `fusion_plating_receiving` |
| **Certificate Registry** | Unified fp.certificate with thickness readings, CoC/thickness/Nadcap types | `fusion_plating_certificates` |
| **Local Delivery** | Forked fusion_tasks with GPS/maps, stripped of claims/sync, delivery-specific fields | `fusion_tasks` |
### Architectural Decisions Made
1. **Recipe → WO**: One WO per `operation` node, child `step` nodes become numbered instructions in WO description
2. **Account hold**: Manual flag on `res.partner` (auto from aging is roadmap)
3. **Email triggers**: SO confirmed, parts received, invoice posted (configurable per trigger)
4. **Configurator**: Custom build with formula-based pricing, estimator override, portal self-service wizard
5. **Model naming**: New models use `fp.*` prefix, existing keep `fusion.plating.*`
6. **Security groups**: Role-based (Estimator, Receiving, Accounting, Shop Manager) layered on existing privilege hierarchy (Operator→Supervisor→Manager→Admin)
### Key Models Quick Reference
| Model | Module | Purpose |
|-------|--------|---------|
| `fusion.plating.process.node` | `fusion_plating` | Recipe tree (template) |
| `fusion.plating.process.node.input` | `fusion_plating` | Operator input definitions |
| `fusion.plating.job.node.override` | `fusion_plating_bridge_mrp` | Per-job opt-in/out |
| `fp.part.catalog` | `fusion_plating_configurator` | Customer part library (geometry, material) |
| `fp.coating.config` | `fusion_plating_configurator` | Coating configuration templates |
| `fp.treatment` | `fusion_plating_configurator` | Pre/post treatment steps |
| `fp.pricing.rule` | `fusion_plating_configurator` | Formula-based pricing engine |
| `fp.pricing.complexity.surcharge` | `fusion_plating_configurator` | Complexity surcharge lines |
| `fp.quote.configurator` | `fusion_plating_configurator` | Configurator session + price calc |
| `fp.receiving` | `fusion_plating_receiving` | Parts receiving record |
| `fp.receiving.line` | `fusion_plating_receiving` | Per-part receiving detail |
| `fp.receiving.damage` | `fusion_plating_receiving` | Damage log entry |
| `fp.invoice.strategy.default` | `fusion_plating_invoicing` | Customer-level invoice strategy |
| `fp.certificate` | `fusion_plating_certificates` | Certificate registry (CoC, thickness, etc.) |
| `fp.thickness.reading` | `fusion_plating_certificates` | Fischerscope measurement data |
| `fp.notification.template` | `fusion_plating_notifications` | Configurable email notification |
| `fp.notification.log` | `fusion_plating_notifications` | Email audit trail |
| `fusion.plating.quote.request` | `fusion_plating_portal` | Customer RFQ |
| `fusion.plating.portal.job` | `fusion_plating_portal` | Portal-facing job tracker |
| `fusion.plating.customer.spec` | `fusion_plating_quality` | Spec library |
| `fusion.plating.quality.hold` | `fusion_plating_quality` | Parts on hold |
| `fusion.plating.ncr` | `fusion_plating_quality` | Non-conformance reports |
| `fusion.plating.capa` | `fusion_plating_quality` | Corrective actions |
| `fusion.plating.batch` | `fusion_plating_batch` | Rack/barrel batch tracking |
| `fusion.plating.kpi` | `fusion_plating_kpi` | KPI definition (OTD, yield, throughput, etc.) |
| `fusion.plating.kpi.value` | `fusion_plating_kpi` | KPI daily value (auto-computed or manual) |
| `fusion.plating.delivery` | `fusion_plating_logistics` | Delivery with chain of custody |
| `fusion.plating.pickup.request` | `fusion_plating_logistics` | Customer pickup requests |
| `fusion.plating.route` | `fusion_plating_logistics` | Driver routes with stops |
| `fusion.technician.task` | `fusion_tasks` | Local delivery task (GPS, maps) |
| `fusion.technician.location` | `fusion_tasks` | Driver GPS tracking |
## Repackaged Enterprise Modules
See `K:\Github\RePackaged-Odoo\CLAUDE.md` for full details. Key points:
- Odoo 19 enterprise modules repackaged for community edition
- All OEEL-1 licenses changed to LGPL-3
- Phone-home/telemetry gutted
- `web_enterprise` and `mail_enterprise` are installed on odoo-entech
- Addons path includes: `_dependencies`, `accounting`, `inventory_manufacturing`, `hr`, `sales`, `ai`, `fusion_backend`, `custom`, `website`

View File

@@ -1,244 +0,0 @@
# Existing Nickel Plating / Metal Finishing ERP Systems — What They Record
**Prepared:** 2026-04-09
**Purpose:** Benchmark what the industry standard plating ERPs already capture, so `fusion-plating` can match the table stakes and differentiate where it counts.
**Scope:** Commercial software used by actual metal finishing shops in North America. Focus on what they *track and retain*, not on marketing gloss.
---
## 1. The Landscape — Who's Actually in the Market
| Product | Focus | Notes |
|---------|-------|-------|
| **[Steelhead Technologies](https://gosteelhead.com/)** | Purpose-built for metal finishing (plating, anodizing, powder coat). | Most feature-complete plating-specific ERP. NADCAP Scanner is their flagship compliance tool. |
| **[Lab Wizard Cloud](https://lab-wizard.com/en/lab-wizard-cloud/)** | Plating bath chemistry LIMS/SPC. | Specialized in titration, bath tracking, SPC. Often used alongside a general ERP. Free tier for small shops. |
| **[ProShop ERP](https://proshoperp.com/)** | Shop-floor-first ERP with built-in QMS. Strong in aerospace / AS9100 / ITAR. | Paperless travelers, version-controlled work instructions. |
| **[PROPLATE™](https://proplate.pro/)** | Barrel plating and general metal finishing. | Smaller vendor; limited public detail. |
| **[Anoplex](https://www.anoplex.com/)** | Anodizing, plating, painting, NDT, passivation. | Cloud, solution control and chemical inventory. |
| **[PHTPlus](https://phtplus.com/about/)** | Plating, heat treating, finishing services. | Job-shop centric. |
| **[Fitfactory](https://www.fitfactory.com/metal-finishing-production-control)** | UK-based metal finishing production control. | NCR logging, chemical batch tracking for NADCAP. |
| **[JobBOSS² + uniPoint](https://www.ecisolutions.com/blog/manufacturing/jobboss/how-jobboss-and-unipoint-support-iso-9001-and-as9100-certification/)** | General job shop ERP + bolted-on QMS. | Widely used, not plating-specific but common in shops that do plating. |
| **[QT9 QMS / QT9 ERP](https://qt9software.com/as9100)** | Aerospace QMS first, ERP second. | Clean AS9100 workflows, customer portal. |
| **[Global Shop Solutions](https://www.globalshopsolutions.com/)** | General manufacturing ERP with AS9100 modules. | Large installed base, not plating-first. |
| **[Tudodesk](https://www.tudodesk.com/free-chrome-plating-shop-software)** | Chrome plating shop software. | Light-weight, SMB-focused, online quoting/billing. |
| **[Varland Plating portal](https://varland.com/technology)** | Example of a shop-built customer portal. | Useful as a reference for what customers expect to see. |
**Takeaway:** There is **no** single Canadian-made, Toronto-compliance-aware, electroless-nickel-first ERP on the market. The closest competitors are US-based and cover the quality/NADCAP side well but treat **Ontario sewer, RPRA hazardous-waste, ECA air, and Toronto P2 Plan obligations** as something handled outside the ERP. This is the opening.
---
## 2. Feature Inventory — What They Record
Grouped by concern. "✓" = explicitly documented on the vendor page or a credible review; blank = unclear or not mentioned.
### 2.1 Bath / Tank Chemistry Management
| Data point / feature | Steelhead | Lab Wizard | Anoplex | ProShop | PROPLATE |
|----------------------|-----------|------------|---------|---------|----------|
| Titration-driven concentration calculation | | ✓ | ✓ | | |
| Auto-calculated chemical addition (dosing) | | ✓ | ✓ | | |
| Trend visualization (concentration vs time) | ✓ | ✓ | | | |
| Out-of-spec alarm / email / channel alert | | ✓ | | | |
| Scheduled analysis reminder (per bath) | | ✓ | | | |
| Bath age / MTO (Metal Turnover) counter | | ✓ | | | |
| Phosphorus / orthophosphite buildup tracking | | ✓ | | | |
| Temperature / pH / agitation log | ✓ | ✓ | | | |
| Filter / carbon treatment / cleanup log | | ✓ | | | |
| Bath dump & makeup record | | ✓ | ✓ | | |
| IoT / sensor input integration | | ✓ | | | |
| SPC charts with CpK | | ✓ | | | |
| Audit-ready chemistry reports | | ✓ | | ✓ | |
**Reference:** Electroless nickel bath life is measured in **Metal Turnovers (MTO)**: MTO = cumulative Ni replenished ÷ (initial Ni × bath volume). A 5 g/L bath replenished with 5 g/L of Ni = 1 MTO. Most mid-phos EN baths run **59 MTO** before dump; orthophosphite builds at roughly **2430 g/L per MTO**, and phosphorus content in the deposit rises with bath age. Any serious EN plating system must track MTO, orthophosphite, and P-content bands. [Lab Wizard MTO article](https://lab-wizard.com/en/resources/knowledge/metal-turnovers/)
### 2.2 Production / Tank / Line Management
| Feature | Steelhead | ProShop | JobBOSS | PROPLATE | PHTPlus |
|---------|-----------|---------|---------|----------|---------|
| Tank QR code identification | ✓ | | | | |
| Tank time tracking "to the second" | ✓ | | | | |
| Multi-line capacity planning / scheduling | ✓ | ✓ | ✓ | ✓ | ✓ |
| Recipe / process card per part number | ✓ | ✓ | ✓ | ✓ | |
| Racking/packing instructions per part | ✓ | | | | |
| Real-time part-in-process location | ✓ | ✓ | ✓ | | |
| Mobile/tablet operator stations | ✓ | ✓ | | | |
| Photo documentation at workstation | ✓ | ✓ | | | |
| Paperless digital traveler | ✓ | ✓ | ✓ | ✓ | |
| Rework tracking by line / part / finish | ✓ | ✓ | ✓ | | |
### 2.3 Quality & Certification Records
| Feature | Steelhead | ProShop | QT9 | JobBOSS | Fitfactory |
|---------|-----------|---------|-----|---------|------------|
| ISO 9001 compliance workflows | ✓ | ✓ | ✓ | ✓ | ✓ |
| AS9100 Rev D aerospace workflows | ✓ | ✓ | ✓ | ✓ | |
| NADCAP AC7108 traceability (plating) | ✓ | | | | ✓ |
| NADCAP Scanner (cert digitization) | ✓ | | | | |
| ITAR / CMMC controls | | ✓ | ✓ | | |
| ISO 13485 (medical) | | ✓ | ✓ | | |
| Certificate of Conformance auto-generation | ✓ | ✓ | ✓ | ✓ | |
| First Article Inspection Report (FAIR) | | ✓ | ✓ | ✓ | |
| Non-conformance / hold tag workflow | ✓ | ✓ | ✓ | ✓ | ✓ |
| MRB (Material Review Board) routing | | ✓ | ✓ | ✓ | |
| CAPA / corrective action log | ✓ | ✓ | ✓ | ✓ | ✓ |
| Customer specification library | ✓ | ✓ | ✓ | ✓ | |
| Document / revision control | ✓ | ✓ | ✓ | ✓ | ✓ |
| Operator training tied to doc revision | | ✓ | ✓ | | |
| Internal audit schedule + tracking | | ✓ | ✓ | ✓ | |
| Management review agenda/minutes | | ✓ | ✓ | | |
| Supplier approved-vendor list (AVL) | ✓ | ✓ | ✓ | ✓ | |
| Supplier receiving inspection | ✓ | ✓ | ✓ | ✓ | |
| Calibration register (NIST-traceable) | | ✓ | ✓ | ✓ | |
| XRF thickness reading capture | | | | | ✓ |
| Cross-reference to customer specs (AMS, ASTM, MIL) | ✓ | ✓ | ✓ | ✓ | |
### 2.4 Shop Floor / Operator Experience
| Feature | Steelhead | ProShop |
|---------|-----------|---------|
| Touch-screen tablets at each station | ✓ | ✓ |
| Version-controlled setup sheets displayed live | | ✓ |
| Tool list / fixture list at station | ✓ | ✓ |
| Inspection plan linked to the operation | | ✓ |
| Real-time shop-floor dashboard | ✓ | ✓ |
| Operator badge sign-on, time-on-part | ✓ | ✓ |
| Automatic hold if prior step incomplete | ✓ | ✓ |
| Photo capture of defect / process step | ✓ | ✓ |
### 2.5 Customer Portal
| Feature | Steelhead | Varland | Tudodesk | QT9 |
|---------|-----------|---------|----------|-----|
| Near-real-time job status | ✓ | ✓ | ✓ | ✓ |
| Quote request / approval online | ✓ | | ✓ | ✓ |
| Upload drawings / specs | ✓ | | ✓ | |
| Download Certificate of Conformance PDF | ✓ | ✓ | | ✓ |
| Download packing lists / shipping PDF | ✓ | ✓ | ✓ | ✓ |
| Invoice access | ✓ | | ✓ | ✓ |
| Historical orders searchable | ✓ | ✓ (30 days) | ✓ | ✓ |
### 2.6 Environmental / Waste / Health & Safety
| Feature | Plating ERPs | Stand-alone EHS tools |
|---------|--------------|----------------------|
| SDS library with version + review date | ✗ mostly absent | ✓ (Chemical Safety, VelocityEHS, ERA-EHS) |
| Chemical inventory by location | partial (Anoplex, Steelhead) | ✓ |
| WHMIS training matrix | ✗ | ✓ (Cority, VelocityEHS) |
| Exposure monitoring records (air sampling) | ✗ | ✓ |
| Hazardous waste generation log | ✗ | ✓ (Wastelinq, IMEC, ERA, EHSTracks) |
| Cradle-to-grave manifest tracking | ✗ | ✓ (Wastelinq, IMEC) |
| Automated generator reporting | ✗ | partial |
| Sewer discharge monitoring log | ✗ | partial |
| Pollution Prevention Plan builder | ✗ | ✗ |
| Spill incident register | ✗ | ✓ |
| JHSC meeting / minutes | ✗ | partial |
**This is the biggest single gap in the industry.** Plating shops today run a plating ERP (Steelhead / ProShop / JobBOSS) for production, a bath-chemistry tool (Lab Wizard), and a *separate* EHS suite (ERA-EHS / VelocityEHS / Wastelinq) for environmental and waste compliance. Three logins, three sources of truth, and nothing in any of them is written against **Toronto Chapter 681** or **RPRA HWP Registry** specifically.
Vendors handling these separately:
- Waste: [Wastelinq](https://wastelinq.com/), [IMEC Technologies](https://www.imectechnologies.com/hazardous-waste-management-software/), [ERA-EHS Waste](https://www.era-environmental.com/solutions/environmental/waste), [Cority Waste Management](https://www.cority.com/environmental-cloud/waste-management-software/), [VelocityEHS](https://www.ehs.com/solution/environmental-compliance/waste-management/), [Sphera](https://sphera.com/solutions/product-stewardship/hazardous-material-management-for-the-u-s-government/hazardous-waste-management-software/), [Chemical Safety](https://chemicalsafety.com/hazardous-waste-management-software/)
---
## 3. Process-Step Records That Electroless Nickel Specifically Needs
Beyond the general plating ERP fields, electroless nickel has a handful of step-specific data points that *must* be captured to satisfy Nadcap AC7108 and customer specs like AMS 2404 / ASTM B733 / MIL-C-26074:
| Step | Data captured per job / lot | Why |
|------|------------------------------|-----|
| **Pre-clean / degrease** | Time, temperature, bath ID | Contamination risk to EN bath |
| **Acid activation / etch** | Time, concentration, temperature, bath ID | Major source of hydrogen absorption |
| **Strike / Wood's nickel** (stainless, aluminium) | Current, time, bath ID | Adhesion requirement |
| **EN plate** | Time-in, time-out, bath ID, MTO at plate, temperature, pH, Ni concentration, P concentration, target thickness, measured thickness | The core process — all of this is traceable for life-of-part. |
| **Rinse cascade** | Number of rinses, conductivity readings (for zero-discharge shops) | Drag-out management |
| **Passivation / chromate (optional)** | Bath ID, time, temperature | Corrosion spec compliance |
| **Hydrogen embrittlement relief bake** | Time-in, time-out, temperature profile, oven ID, bake duration (typ. 4 h at 375°F / 190°C per AMS 2759/9), recorder chart | **Mandatory within 14 h of plate for high-strength steel**; non-compliance = customer reject, or worse, part failure in service |
| **Thickness measurement** | XRF reading(s) per part/lot, instrument ID, calibration cert reference, P-content reading, operator | NIST-traceable, tamper-proof |
| **Adhesion test** | Method (bend, heat-quench, grit-blast), result, operator | Per Nadcap AC7108 |
| **Corrosion test** (if required) | Method (salt spray, CASS), duration, result | Per customer spec |
| **Final inspection** | Visual, dimensional, operator, photo (optional) | CoC support |
| **Certificate of Conformance** | Auto-generated, pulling every previous step | Customer deliverable |
Nadcap accreditation means *every* one of these is documented and traceable — [AC7108](https://www.newmethodplating.com/nadcap-accreditation-and-as7108-certification-for-chemical-processing/) explicitly audits against this. Existing plating ERPs cover most of it; **none** I found explicitly model the EN-specific fields (MTO, orthophosphite, P-content per MTO, bake time-window enforcement).
---
## 4. Gaps & Opportunities for `fusion-plating`
Stacking the compliance research from the companion document against this feature benchmark:
### 4.1 Gaps in the market
1. **No unified production + environmental compliance system.** Plating shops juggle 23 platforms; `fusion-plating` can be the first to put production, bath chemistry, waste manifests, sewer monitoring, and P2 Plan data under one roof.
2. **No Ontario/Toronto-native regulatory awareness.** Every existing system is built for US regs (EPA RCRA, OSHA) or UK regs. A module that speaks **Chapter 681 Table 1, RPRA HWP, O. Reg. 419/05, MECP ECA** out of the box is unique.
3. **No automated Toronto P2 Plan builder.** Six-year cycles, year-3 updates, 12-heavy-metal inventory — this is a compliance workflow, not a document. It should be a feature, not a Word template.
4. **No EN-specific chemistry engine.** MTO counter, orthophosphite projection, P-content prediction, bake-time-window enforcement (plate → bake must be ≤1 h) — none of the ERPs I examined model this natively.
5. **Delegation-first UX.** The user said the owner likes to delegate. Existing systems are built around a scheduler assigning work. `fusion-plating` can flip this: owner drops the inquiry in, the system routes it (sales → engineer → scheduler → supervisor → operator → inspector → shipper) and the owner sees *status*, not *details*.
### 4.2 Table-stakes `fusion-plating` must match (or it looks cheap)
- Paperless digital traveler
- QR-coded tanks and jobs
- Tablet shop-floor stations
- Customer portal (quote, status, CoC download, invoices)
- Certificate of Conformance auto-generation
- Non-conformance + CAPA workflow
- Document control with revision + trained-on matrix
- Calibration register
- AS9100 / NADCAP audit trail posture (traceability from raw material to shipped part)
- Supplier AVL and receiving inspection
### 4.3 Differentiators `fusion-plating` should own
- **Toronto Chapter 681 real-time discharge monitor** — log every lab report, trigger at 80 % of Table 1, force investigation before the next discharge.
- **Automated P2 Plan document pack** — pulls 6 years of pollutant inventory, calculates trends, pre-writes year-3 and year-6 updates.
- **RPRA HWP Registry handshake** — generator registration number, waste-stream profiles, manifest numbers, carrier approval expiry, annual report scheduling.
- **ECA compliance calendar** — every ECA condition (source testing, annual report, equipment maintenance) on a calendar with responsible party, overdue escalation.
- **EN bath chemistry engine** — MTO counter, orthophosphite projection, phosphorus content band forecast, automatic dump/makeup recommendation.
- **Bake-window enforcer** — when a high-strength-steel part finishes plating, the system starts a **clock** and alarms if the part isn't in an oven within the customer-spec window.
- **Ontario-aware training matrix** — WHMIS 2015 refresh flags, TDG 3-year recert, first-aid CPR renewal, JHSC member term tracking.
- **Delegation inbox** — every new task, quality hold, complaint, inspector letter, or bath alarm lands in a "someone has to do this" inbox and auto-routes by role.
---
## 5. Sources
### Plating / metal finishing ERPs
- [Steelhead Technologies — Compliance](https://gosteelhead.com/compliance)
- [Steelhead — Plating & Anodizing Software](https://gosteelhead.com/plating-anodizing-software)
- [Steelhead — Customer Portal](https://gosteelhead.com/customer-portal-0)
- [Steelhead — Quality Management](https://gosteelhead.com/resource-library/metal-finishing-quality-management-software)
- [Steelhead — NADCAP Scanner](https://www.pfonline.com/products/nadcap-scanner-provides-fully-digital-production-automated-certification-management)
- [Lab Wizard Cloud](https://lab-wizard.com/en/lab-wizard-cloud/)
- [Lab Wizard — Metal Turnovers (MTO) article](https://lab-wizard.com/en/resources/knowledge/metal-turnovers/)
- [ProShop ERP — Quality Management System](https://proshoperp.com/product/quality-management-system/)
- [ProShop ERP — Aerospace](https://proshoperp.com/industries/aerospace/)
- [ProShop ERP — MES](https://proshoperp.com/product/mes/)
- [PROPLATE™](https://proplate.pro/metal-finishing-erp-systems-and-job-shop-management-proplate-strategy-for-high-quality-scalable-operations/)
- [Anoplex Software](https://www.anoplex.com/)
- [PHTPlus](https://phtplus.com/about/)
- [Fitfactory — Metal Finishing Production Control](https://www.fitfactory.com/metal-finishing-production-control)
- [JobBOSS² + uniPoint for AS9100](https://www.ecisolutions.com/blog/manufacturing/jobboss/how-jobboss-and-unipoint-support-iso-9001-and-as9100-certification/)
- [QT9 QMS — AS9100](https://qt9software.com/as9100)
- [QT9 ERP — Customer Portal](https://qt9software.com/erp/customer-portal-software)
- [Global Shop Solutions — AS9100](https://www.globalshopsolutions.com/blog/as9100-certification-doesnt-have-to-be-hard-let-erp-do-the-heavy-lifting)
- [Tudodesk — Chrome Plating Shop Software](https://www.tudodesk.com/free-chrome-plating-shop-software)
- [Varland Plating — Technology & Customer Portal](https://varland.com/technology)
### Stand-alone environmental / waste platforms (what plating ERPs don't cover)
- [Wastelinq](https://wastelinq.com/)
- [IMEC Hazardous Waste Management](https://www.imectechnologies.com/hazardous-waste-management-software/)
- [Ecesis Waste Tracking](https://www.ecesis.net/Waste-Management-Software.aspx)
- [ERA-EHS Waste Management](https://www.era-environmental.com/solutions/environmental/waste)
- [Cority Waste Management](https://www.cority.com/environmental-cloud/waste-management-software/)
- [VelocityEHS Waste Management](https://www.ehs.com/solution/environmental-compliance/waste-management/)
- [Sphera Hazardous Waste Management](https://sphera.com/solutions/product-stewardship/hazardous-material-management-for-the-u-s-government/hazardous-waste-management-software/)
- [Chemical Safety Software](https://chemicalsafety.com/hazardous-waste-management-software/)
- [EHSTracks Waste Compliance](https://ehstracks.com/waste-compliance-software/)
### Process knowledge (EN-specific)
- [Electroless Nickel Plating — Products Finishing overview](https://www.pfonline.com/articles/electroless-nickel-plating)
- [Controlling Phosphorus Content — Products Finishing](https://www.pfonline.com/articles/controlling-phosphorus-content-in-electroless-nickel-phosphorus-coatings)
- [Electroless Nickel-Phosphorus Plating — Wikipedia](https://en.wikipedia.org/wiki/Electroless_nickel-phosphorus_plating)
- [Hydrogen Embrittlement & Electroplating — Sharretts Plating](https://www.sharrettsplating.com/blog/hydrogen-embrittlement-electroplating-what-you-need-to-know/)
- [EN Thickness & P-Content via XRF — VRXRF](https://www.vrxrf.com/blog/electroless-nickel-thickness-phosphorus-content-detection-guide/)
- [Measuring EN Plating with XRF — AZoM](https://www.azom.com/article.aspx?ArticleID=15524)
- [National Electroless Nickel — Specifications & Properties](https://www.nationalelectrolessnickel.com/specs.htm)
- [Nickel Institute — Nickel Plating Handbook (PDF)](https://nickelinstitute.org/media/lxxh1zwr/2023-nickelplatinghandbooka5_printablepdf.pdf)

View File

@@ -1,258 +0,0 @@
# Electroless Nickel Plating — Canadian Compliance & Record-Keeping Reference
**Location:** Toronto, Ontario, Canada
**Prepared:** 2026-04-09
**Purpose:** Pre-engagement reference for scoping an Odoo module covering workflow, compliance, and order management for an electroless nickel plating shop.
**Status:** Research document — verify jurisdiction-specific details with the client's environmental consultant and their current ECA before treating as authoritative.
---
## 1. Executive Summary
An electroless nickel plating operation in Toronto sits at the intersection of **federal, provincial, municipal, and industry-voluntary** compliance regimes. The binding obligations that must be reflected in any operations system are:
| # | Obligation | Authority | Why it matters to the system |
|---|------------|-----------|------------------------------|
| 1 | **Environmental Compliance Approval (ECA — Air)** | Ontario MECP | Every plating line, drying oven and gas-fired source must be operated within the conditions on the facility's ECA — the system must track operating parameters, maintenance, and source testing. |
| 2 | **Toronto Sewer Use By-law Ch. 681** — discharge limits + P2 Plan | City of Toronto | Nickel discharge cap **2 mg/L** to sanitary sewer, **0.08 mg/L** to storm sewer. Self-monitoring, sampling, and a six-year Pollution Prevention Plan are mandatory for metal finishers. |
| 3 | **Ontario Reg. 347 hazardous waste generator registration** | Ontario MECP via RPRA HWP Registry | Spent plating baths, sludges, rinses and filter media are subject wastes — generator registration, manifesting and reporting run through RPRA's Hazardous Waste Program Registry (replaced HWIN in 2023). |
| 4 | **CEPA Schedule 1 — nickel compounds** + NPRI reporting | Environment and Climate Change Canada | Oxidic, sulphidic and soluble inorganic nickel compounds are listed toxic substances. Releases and transfers are reportable to the National Pollutant Release Inventory annually if thresholds are met. |
| 5 | **OHSA + Reg. 833 exposure limits, WHMIS 2015** | Ontario Ministry of Labour | Worker exposure to nickel aerosols, acids and reducing agents must be assessed, controlled, monitored, and documented. SDS library and training records are mandatory. |
| 6 | **Transportation of Dangerous Goods (TDG)** | Transport Canada | Incoming acids, nickel salts, hypophosphite, and outgoing hazardous wastes need classified shipping documents, training certificates and means-of-containment records. |
Beyond law, customers will usually demand **ISO 9001** at minimum, and many will require **AS9100 / Nadcap AC7108** (aerospace electroless plating) or **CGP** (Controlled Goods Program, for defence work). These are not legally binding but functionally mandatory for the customer mix a Toronto shop typically serves.
---
## 2. Compliance Map by Jurisdiction
### 2.1 Federal (Canada)
#### 2.1.1 Canadian Environmental Protection Act (CEPA 1999)
- **Nickel compounds on Schedule 1** (toxic substances): oxidic, sulphidic and soluble inorganic nickel compounds. Triggers risk-management obligations and may trigger pollution-prevention (P2) planning notices from ECCC.
- **National Pollutant Release Inventory (NPRI)** — annual reporting for substances above thresholds. For Part 1A metals (nickel included), the standard threshold is **10 tonnes manufactured/processed/otherwise used at ≥1% concentration**, plus any alternate thresholds applicable to metal finishing. Smaller shops may still trigger the employee-hour threshold.
- **Records:** facility activity logs, substance inventories, calculation methods, supporting documentation — retain **3 years**.
#### 2.1.2 Transportation of Dangerous Goods Act (TDG) and Regulations
- Applies to incoming drums of nickel sulphate/sulphamate, acids, caustics, hypophosphite; outgoing hazardous waste shipments.
- Shipping documents (consignor, classification, UN number, PG, quantity, emergency info), **means-of-containment selection and inspection**, TDG **training certificates** (valid 3 years by road), placarding.
- **Records:** shipping documents retained for **2 years** (federal), training records for duration of employment + common practice is 5 years.
#### 2.1.3 Hazardous Products Act + Hazardous Products Regulations (WHMIS 2015 / GHS)
- SDS library maintained current (each SDS must be no older than 3 years or confirmed current).
- Container labels (supplier + workplace).
- Worker training documented; refreshed when new products or processes are introduced.
#### 2.1.4 Controlled Goods Program (CGP) — optional, required only for defence/aerospace work
- Registration with Public Services and Procurement Canada.
- Security assessments of personnel with access to controlled goods.
- Record-keeping of controlled technology and physical access.
---
### 2.2 Provincial (Ontario)
#### 2.2.1 Environmental Compliance Approval — Air (Ontario EPA, R.S.O. 1990, c. E.19)
- Electroless nickel plating facilities require an **ECA with Limited Operational Flexibility (air)** covering plating lines, drying ovens, and natural-gas-fired equipment.
- Conditions typically include source-specific emission limits, equipment inventories, maintenance protocols, source testing schedules, ambient-air impact assessments (ESDM report), and record-keeping clauses.
- Annual **Ontario Regulation 419/05** (Local Air Quality) compliance — emission summary and dispersion modelling report updates.
#### 2.2.2 Ontario Regulation 347 — General (Waste Management) / RPRA Hazardous Waste Program
- Generator registration and annual update via the **RPRA HWP Registry** (replaced MECP HWIN in 2023).
- Waste characterisation (ignitable, reactive, corrosive, leachate toxic, severely toxic, acute hazardous).
- Manifest system for every off-site hazardous-waste shipment — carrier, receiver, waste class.
- **Records:** generator reports and manifests retained per RPRA guidance (minimum **2 years**, longer if referenced by ECA or federal requirement).
#### 2.2.3 Ontario Water Resources Act + Environmental Protection Act (spills, releases)
- Spill-reporting obligation: any spill that may cause an adverse effect must be reported immediately to the Spills Action Centre.
- Site contingency plan required.
#### 2.2.4 Occupational Health and Safety Act + Regulation 833 (chemical/biological agents) + Reg. 851 (industrial establishments)
- **Note:** Nickel is **not** one of the 11 designated substances under O. Reg. 490/09. It is regulated as a general hazardous chemical agent under **Reg. 833** — the Ontario Table / ACGIH 2017 TLVs apply.
- Employers must limit exposure to or below the TWA and short-term limits, carry out exposure assessments, provide engineering controls and PPE, and document the program.
- **JHSC** (Joint Health and Safety Committee) required ≥ 20 workers; minutes retained.
- WSIB registration and premium reporting.
- First-aid regulation (Reg. 1101): kits, trained first-aiders, log of injuries.
#### 2.2.5 Technical Standards and Safety Authority (TSSA)
- Natural-gas-fired ovens, boilers, compressed-gas storage fall under TSSA — installation inspections, periodic maintenance, operator certifications.
#### 2.2.6 Fire Code (Ontario Reg. 213/07)
- Hazardous chemical storage classification, ventilation, spill containment, fire-separation, inspection log.
---
### 2.3 Municipal (City of Toronto)
#### 2.3.1 Sewer Use By-law — Toronto Municipal Code Chapter 681
*Last amended 15 May 2023.*
**Prohibited discharges (§ 681-2A(3))** — absolute prohibition regardless of concentration:
acute hazardous waste chemicals, combustible liquid, fuels, hauled waste (without permit), hazardous industrial waste, hazardous waste chemicals, pathological waste, PCBs, pesticides, reactive waste, severely toxic waste, ignitable waste, solid obstructing substances.
**Table 1 — Limits for Sanitary and Combined Sewers Discharge** (key parameters for a plating shop):
| Parameter | Limit | Unit |
|-----------|-------|------|
| pH | > 6.0 to < 11.5 | SU |
| Temperature | < 60 | °C |
| Biochemical oxygen demand (BOD) | 300 | mg/L |
| Suspended solids (total) | 350 | mg/L |
| Phosphorus (total) | 10 | mg/L |
| Cyanide (total) | 2 | mg/L |
| Fluoride | 10 | mg/L |
| Oil & grease (mineral & synthetic) | 15 | mg/L |
| Phenolics (4AAP) | 1.0 | mg/L |
| **Nickel (total)** | **2** | **mg/L** |
| Copper (total) | 2 | mg/L |
| Chromium (total) | 4 | mg/L |
| Chromium (hexavalent) | 2 | mg/L |
| Zinc (total) | 2 | mg/L |
| Lead (total) | 1 | mg/L |
| Cadmium (total) | 0.7 | mg/L |
| Cobalt (total) | 5 | mg/L |
| Arsenic (total) | 1 | mg/L |
| Mercury (total) | 0.01 | mg/L |
| Molybdenum (total) | 5 | mg/L |
| Aluminum (total) | 50 | mg/L |
| Manganese (total) | 5 | mg/L |
**Table 2 — Storm Sewer Discharge** limits are **dramatically stricter** (e.g. Nickel 0.08 mg/L, Zinc 0.04 mg/L, Mercury 0.0004 mg/L). Plating rinse and spill water must be kept out of storm sewers.
**§ 681-3 Prohibition of dilution** — cannot dilute to meet a limit.
**§ 681-5 Pollution Prevention Plan (P2)**:
- Required for every **subject sector industry** (metal finishing is listed) and for any industry discharging a **subject pollutant** (Table 3 — includes nickel, chromium, copper, zinc, cobalt, cadmium, arsenic, lead, molybdenum, mercury, selenium).
- First plan due within **1 year** of commencing operations.
- Full re-plan every **6 years**; update at **year 3**.
- Plan contents: process description, subject-pollutant inventory, current quantities discharged, current reduction activities, options evaluation, **3- and 6-year reduction targets**, implementation schedule.
- Non-compliance updates required within **90 days** of any new subject pollutant being discharged.
**§ 681-6 Agreements** — an **Industrial Waste Surcharge Agreement (IWSA)** or **Over-strength Agreement / Compliance Agreement** may be needed where a parameter cannot be met; subject to surcharge fees, self-monitoring and reporting.
**§ 681-8 Sampling and analytical requirements** — composite or grab sampling per City-approved Standard Methods; samples must be analysed by an **ISO/IEC 17025-accredited laboratory**.
**§ 681-13 Self-monitoring** — ongoing obligation to sample, record and report under the terms of any agreement or permit.
**§ 681-14.3 Document retention** — records referenced in the by-law must be retained per the by-law's own retention clause (verify current duration with City; historically **five years** is typical for sampling and P2 supporting records).
#### 2.3.2 Fire Services — hazardous-material inventory reporting, on-site emergency plan filed with Toronto Fire.
#### 2.3.3 Zoning & Building Permits — confirm current use permits, building compliance for process wastewater pre-treatment equipment.
---
### 2.4 Industry / Customer-Driven (Voluntary but Usually Mandatory in Practice)
| Standard | What it requires | Record obligations |
|----------|------------------|--------------------|
| **ISO 9001:2015** | Quality management system, document control, corrective actions, internal audits. | Controlled documents, CAPA, management review minutes, audit reports, training records. |
| **ISO 14001:2015** | Environmental management system, aspects/impacts register, legal register, operational controls, emergency preparedness. | EMS manual, objectives & targets, non-conformance log, monitoring results. |
| **AS9100 Rev D** | ISO 9001 + aerospace: risk, configuration management, counterfeit-part prevention, full traceability. | Certificate of Conformance per lot, full heat/batch traceability, calibration records. |
| **Nadcap AC7108 (electroless plating)** | Audit criteria specific to EN plating — process control, bath chemistry monitoring, thickness testing, adhesion testing, hydrogen embrittlement relief, corrosion testing. | Process control charts, bath analysis logs, operator certification, customer specification cross-reference. |
| **CGP (Controlled Goods Program)** | Personnel security screening, visitor control, physical security of controlled technology. | Security plan, personnel assessments, visitor logs, transfer records. |
---
## 3. Record-Keeping Obligations — Consolidated Table
| Record type | Source regulation | Minimum retention | Must be stored/searchable in the system? |
|-------------|-------------------|-------------------|--------------------------------------------|
| ECA-required source monitoring & maintenance | Ontario ECA condition | Life of ECA + specified years (often 5) | **Yes** — auto-generate compliance reports |
| Source-testing reports (stack tests) | ECA / Reg. 419/05 | 5 years | Yes |
| Waste generator reports + manifests | Reg. 347 / RPRA | 2 years minimum | **Yes** |
| Bath analysis / process control logs | Customer specs / Nadcap | 510 years (often life of part + 7) | **Yes** |
| Calibration records (thickness gauges, pH meters, thermocouples, balances) | ISO 9001 / AS9100 / Nadcap | Life of equipment + 35 years | **Yes** |
| Sewer self-monitoring samples & lab reports | By-law Ch. 681 | Per by-law (verify — typically 5 years) | **Yes** |
| Pollution Prevention Plan + updates | By-law Ch. 681 § 681-5 | 6-year cycle kept permanently on file | Yes |
| NPRI reports + calculation support | CEPA / NPRI | 3 years | Yes |
| TDG shipping documents | TDG Regs | 2 years (road) | **Yes** |
| TDG training certificates | TDG Regs | Duration of employment + 2 years | Yes |
| WHMIS SDS library | HPR / WHMIS 2015 | Current + rolling 3-year window | **Yes** |
| WHMIS training records | OHSA / HPR | Duration of employment; best practice 7 years | Yes |
| Exposure monitoring (air sampling for nickel, acids) | OHSA / Reg. 833 | Minimum 1 year in Ontario; best practice 30 years for carcinogens | **Yes** |
| JHSC minutes | OHSA | 1 year minimum; best practice permanent | Yes |
| First-aid log & incident investigations | OHSA / Reg. 1101 | 5 years (WSIB) | **Yes** |
| Preventive maintenance / equipment files | ISO 9001 | Life of equipment | **Yes** |
| Certificates of Conformance / customer part traceability | AS9100 / Nadcap | Per customer contract — commonly 710 years or part life | **Yes** |
| Internal audit reports | ISO 9001 / AS9100 | Typically 3 audit cycles | Yes |
| Management review minutes | ISO 9001 | 3 years minimum | Yes |
| Non-conformance / CAR / corrective action log | ISO 9001 / AS9100 | 5 years | **Yes** |
| Supplier approval / material certifications | AS9100 | Per customer contract (710 years common) | **Yes** |
**Bold "Yes"** flags the records where the module has to actively capture, validate, and make retrievable the data at the point of work — not just store a file somewhere.
---
## 4. How Compliance Translates into Module Features
Grouping the above into operational concerns the Odoo module must address:
### 4.1 Environmental compliance surface
- **Chemical inventory** tied to SDS library, expiry tracking, supplier certificates.
- **Bath chemistry log** — scheduled titration, pH, temperature, nickel concentration, hypophosphite concentration, contaminant tracking, bath life and rejuvenation events.
- **Waste generation log** — by waste stream, tied to manifests and RPRA registration.
- **Sewer monitoring log** — grab and composite samples, accredited-lab reports, automatic flagging if any result approaches an 80 % trigger of Table 1 limits.
- **Emission & energy log** (for ECA and NPRI) — consumption of nickel, hypophosphite, acids; calculation worksheets; annual NPRI roll-up.
- **Spill / incident register** with Spills Action Centre reporting workflow.
### 4.2 Worker safety surface
- **SDS library** with version control and review dates.
- **Training matrix** per employee × competency (WHMIS, TDG, confined-space, spill response, process-specific). Auto-flag expiring certs.
- **Exposure monitoring records** — air sampling, medical surveillance opt-in, hearing, respiratory fit-test.
- **JHSC meeting register** and corrective actions.
- **First-aid / injury / near-miss** register feeding WSIB claims.
### 4.3 Quality / customer surface
- **Customer & part master** with specification references (e.g. AMS 2404, ASTM B733, MIL-C-26074, customer-internal specs).
- **Router / process card** — every step, time, temperature, bath, concentration requirement, sign-off.
- **Traceability** — job → lot → bath → operator → equipment → test results → certificate of conformance.
- **Calibration register** with due-date alerts and out-of-tolerance impact assessment.
- **Non-conformance / CAR** workflow with root cause, containment, corrective and preventive actions.
- **Document control** (procedures, work instructions, forms) with revision history and trained-on-revision matrix.
- **Internal audit schedule** and management-review dashboard.
### 4.4 Order processing surface (where "the team worries about the work, not the system")
- **Quote → sales order → router → production → inspection → certificate → shipping → invoice** — one chain, one record.
- **Delegation and task routing** — the boss can drop an incoming order into the system and it self-assigns: sales confirms, engineer scopes, scheduler slots, supervisor releases, operator logs, inspector releases, shipper labels.
- **Customer portal** — drop drawings, approve quotes, see job status, download Certificates of Conformance.
- **Shop-floor dashboards** on tablets — "what's next on my line", "what's parked waiting for first-piece inspection", "what's my bath chemistry telling me to do in 30 minutes".
---
## 5. Items to Confirm with the Client
When the owner meeting happens, these are the questions whose answers change the module scope:
1. **Scope of work** — aerospace / defence / automotive / electronics / general industrial? Determines whether AS9100 / Nadcap / CGP are in scope.
2. **Existing ECA** — do they already hold one, when was it last updated, what are the specific conditions?
3. **Sewer permit / IWSA status** — are they discharging under a Compliance Agreement or at Table 1 limits?
4. **Waste volumes** — are they a Small Quantity Generator or Large? Drives RPRA reporting cadence.
5. **Certifications held** — ISO 9001? AS9100? Nadcap? CGP? Audit calendar?
6. **Existing systems** — is there an ERP today? Paper? Spreadsheets? What has to be migrated?
7. **Payroll / HR** — will fusion_payroll play into this or is that separate?
8. **Stakeholders** — owner, plant manager, quality manager, environment/EHS lead, sales, accounting. Who signs off on what?
9. **Shop-floor reality** — are operators comfortable with tablets? Rugged kiosks? Barcode / RFID?
10. **Integrations** — accounting (Odoo core?), customer EDI, shipping (Canada Post / courier APIs?), laboratory LIMS for bath analyses?
---
## 6. Sources
- [Ontario Environmental Compliance Approval guide](https://www.ontario.ca/page/environmental-compliance-approval)
- [Guide to Applying for an Environmental Compliance Approval (PDF)](https://dr6j45jk9xcmk.cloudfront.net/documents/962/5-8-2-eca-guide-en.pdf)
- [Toronto Municipal Code Chapter 681, Sewers (PDF, 15 May 2023)](https://www.toronto.ca/legdocs/municode/1184_681.pdf)
- [Toronto Sewers By-law program page](https://www.toronto.ca/services-payments/water-environment/water-sewer-related-permits-and-bylaws/sewers-by-law/)
- [Toronto Pollution Prevention (P2) Program](https://www.toronto.ca/services-payments/water-environment/water-sewer-related-permits-and-bylaws/sewers-by-law/pollution-prevention-p2-program/)
- [Ontario Reg. 347 Registration Guidance Manual](https://www.ontario.ca/document/registration-guidance-manual-generators-liquid-industrial-and-hazardous-waste)
- [RPRA Hazardous Waste Program Registry](https://rpra.ca/programs/hwp/)
- [Guide to the Designated Substances Regulation (O. Reg. 490/09)](https://www.ontario.ca/document/guide-designated-substances-workplace/overview-regulation) — note: nickel is not designated
- [Ontario OELs under Regulation 833](https://www.ontario.ca/page/current-occupational-exposure-limits-ontario-workplaces-under-regulation-833)
- [CEPA Schedule 1 — oxidic, sulphidic, soluble inorganic nickel compounds](https://www.canada.ca/en/environment-climate-change/services/management-toxic-substances/list-canadian-environmental-protection-act/oxidic-sulphidic-soluble-inorganic-nickel.html)
- [NPRI Substance list by threshold](https://www.canada.ca/en/environment-climate-change/services/national-pollutant-release-inventory/substances-list/threshold.html)
- [CCOHS Transportation of Dangerous Goods overview](https://www.ccohs.ca/oshanswers/legisl/tdg/tdg_overview.html)
- [Nadcap AC7108 Electroless Plating audit criteria — supplier references](https://www.newmethodplating.com/nadcap-accreditation-and-as7108-certification-for-chemical-processing/)
---
**This is a working reference. Verify all limits, thresholds and retention periods with an Ontario-licensed environmental consultant before incorporating them into a signed proposal or compliance undertaking.**

View File

@@ -1,854 +0,0 @@
# 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

View File

@@ -1,121 +0,0 @@
# Fusion Plating
**Core module of the Fusion Plating product family.**
A configurable, multi-tenant capable ERP for plating and metal-finishing shops,
built for Odoo 19 Community **and** Enterprise.
Copyright © 2026 Nexa Systems Inc.
License: OPL-1 (Odoo Proprietary License v1.0)
---
## What this module is
`fusion_plating` is the **process-agnostic foundation** that every plating or
metal-finishing shop needs, regardless of size, jurisdiction, process mix, or
industry. It provides:
- **Facility** — physical sites with their own tanks, operators, capabilities
- **Process Type** — extensible taxonomy (filled in by process packs)
- **Work Center** — lines and stations inside a facility
- **Tank** — physical vessel with QR code, state, bath history
- **Bath** — the chemistry currently in a tank, with its own lifecycle
- **Bath Parameter** — schema for chemistry readings
- **Bath Log** — daily/per-shift chemistry readings with pass/warn/fail rollup
- **Security** — Operator / Supervisor / Manager / Administrator roles
- **Theme-aware UI** — respects Odoo light/dark mode with zero duplication
## What this module is **not**
This core intentionally ships with:
- **No process chemistry** — install `fusion_plating_process_en`, `_chrome`,
`_anodize`, `_black_oxide` etc. to get actual process types and their
bath parameter schemas.
- **No regulatory data** — install `fusion_plating_compliance_<region>` to
get jurisdiction-specific limits, forms, and reporting workflows.
- **No industry specialisations** — install `fusion_plating_aerospace`,
`_nuclear`, `_cgp` etc. for industry-specific QMS overlays.
- **No client-specific strings** — everything is data-driven.
## Product family
| Module | Purpose | Status |
| --- | --- | --- |
| `fusion_plating` | Core (this module) | **MVP** |
| `fusion_plating_quality` | QMS: NCR, CAPA, doc control, calibration, CoC | planned |
| `fusion_plating_compliance` | Generic compliance framework | planned |
| `fusion_plating_compliance_on` | Ontario regulatory pack | planned |
| `fusion_plating_compliance_tor` | Toronto Ch. 681 municipal pack | planned |
| `fusion_plating_safety` | SDS, WHMIS/TDG, JHSC, exposure | planned |
| `fusion_plating_shopfloor` | Tablet operator stations, QR scanning, bake-window enforcer | planned |
| `fusion_plating_portal` | Customer portal | planned |
| `fusion_plating_process_en` | Electroless nickel — low/mid/high phos | planned |
| `fusion_plating_process_chrome` | Chrome coating (hex & trivalent) | planned |
| `fusion_plating_process_anodize` | Aluminum anodizing (Type II, III) | planned |
| `fusion_plating_process_black_oxide` | Black oxidizing | planned |
| `fusion_plating_aerospace` | AS9100 + Nadcap AC7108 | planned |
| `fusion_plating_nuclear` | CSA N299, CNSC, NQA-1 | planned |
| `fusion_plating_cgp` | Controlled Goods Program | planned |
| `fusion_plating_logistics` | Pickup & delivery routing | planned |
| `fusion_plating_culture` | Values / fundamentals framework | planned |
| `fusion_plating_bridge_sign` | EE bridge: e-sign CoC acceptance | planned |
| `fusion_plating_bridge_documents` | EE bridge: Documents workspace | planned |
| `fusion_plating_bridge_quality` | EE bridge: native `quality` module | planned |
## Installation
```bash
# Development
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating --stop-after-init
# Production — after rsync to target server
docker exec <odoo-container> odoo -d <db> -u fusion_plating --stop-after-init
```
No external Python dependencies. Depends only on standard Odoo 19 Community
base modules (`base`, `mail`, `contacts`, `product`, `stock`, `sale_management`,
`purchase`, `hr`, `uom`).
## Design principles
1. **Works on both Odoo Community and Enterprise.** Never depends on
`quality`, `documents`, `sign`, `studio`, or `mrp_plm`. EE-specific
integrations live in separate `fusion_plating_bridge_*` modules.
2. **No client-specific strings in core.** Configuration, not code.
3. **Regions are data, not code.** Sewer limits, waste classes, reporting
forms come from region packs.
4. **Processes are plug-ins.** New process (copper, zinc, tin) = new
`fusion_plating_process_*` module, core untouched.
5. **Dashboards are configured, not coded.** Shops pick their own headline KPIs.
6. **Theme-aware.** Uses Odoo/Bootstrap CSS variables. One source of truth
for colours; Odoo's theme engine decides light vs dark.
## Security groups
| Group | Intended for |
| --- | --- |
| **Operator** | Shop-floor staff. Reads reference data, writes chemistry logs. |
| **Supervisor** | Line supervisors. Manages baths, schedules jobs, reviews logs. |
| **Manager** | Quality, EHS, plant manager, engineer. Full CRUD on configuration. |
| **Administrator** | Owner, system admin. All manager rights + system settings. |
## Field naming convention
- New models use `fusion.plating.*` namespace.
- Fields on our own models use simple names (no prefix).
- Fields added to base Odoo models (`res.company`, `res.partner`,
`product.template`, etc.) use the `x_fc_` prefix per the repo convention.
## Developer notes
- All models inheriting from `mail.thread` use the Odoo 19 chatter pattern.
- Security follows the Odoo 19 `res.groups.privilege` pattern (module
category → privilege → groups), not the legacy `category_id`-on-group
pattern.
- Sequence numbers use `ir.sequence` seeded in `data/fp_sequence_data.xml`.
- SCSS uses `color-mix()` against CSS custom properties — never hardcodes
hex values. See `static/src/scss/fusion_plating.scss` for the theming
contract.
- No `group expand="0"` in search views (Odoo 19 incompatibility).
- No `category_id` or `users` field on `res.groups` (Odoo 19 incompatibility).

View File

@@ -1,7 +0,0 @@
# -*- 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 controllers
from . import models

View File

@@ -1,112 +0,0 @@
# -*- 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',
'version': '19.0.1.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
'description': """
Fusion Plating — Core
=====================
Part of the Fusion Plating product family by Nexa Systems Inc.
Fusion Plating is a configurable, multi-tenant capable ERP for plating and metal
finishing shops. This core module provides the process-agnostic foundation that
every shop needs regardless of size, process mix, jurisdiction, or industry.
The core ships intentionally empty of region-specific or process-specific
content — that comes from add-on modules:
* fusion_plating_process_en — Electroless nickel plating
* fusion_plating_process_chrome — Chrome coating (hex or trivalent)
* fusion_plating_process_anodize — Aluminum anodizing (Type II, III)
* fusion_plating_process_black_oxide — Black oxidizing
* fusion_plating_quality — QMS (NCR, CAPA, calibration, CoC, doc control)
* fusion_plating_compliance — Generic compliance framework
* fusion_plating_compliance_on — Ontario regulatory pack
* fusion_plating_compliance_tor — Toronto Ch. 681 municipal pack
* fusion_plating_safety — SDS, WHMIS/TDG training, JHSC, exposure
* fusion_plating_shopfloor — Tablet operator stations, QR scanning
* fusion_plating_portal — Customer portal
* fusion_plating_aerospace — AS9100 + Nadcap AC7108 pack
* fusion_plating_nuclear — CSA N299, CNSC, NQA-1 pack
* fusion_plating_cgp — Controlled Goods Program pack
* fusion_plating_logistics — Pickup & delivery
* fusion_plating_culture — Values / fundamentals framework
Core concepts
-------------
* Facility — a physical site with its own tanks, operators, compliance profile
* Process Type — extensible taxonomy of finishing processes
* Work Center — production line or station within a facility
* Tank — physical vessel with QR code and state
* Bath — the chemistry currently in a tank, with its own lifecycle
* Bath Log — daily chemistry readings with pass/fail vs target
* KPI — configurable headline metrics per shop
* Delegation Inbox — single pane of "things waiting for someone"
Design principles
-----------------
1. No client-specific strings in core.
2. No region-specific data in core.
3. No process-specific chemistry in core.
4. Works on both Odoo Community and Enterprise editions.
5. Theme-aware: respects user light/dark mode preference.
6. Multi-facility, multi-company, multi-currency capable.
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
""",
'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': [
'base',
'mail',
'contacts',
'product',
'stock',
'sale_management',
'purchase',
'hr',
'uom',
],
'data': [
'security/fp_security.xml',
'security/ir.model.access.csv',
'data/fp_sequence_data.xml',
'data/fp_process_category_data.xml',
'views/fp_process_type_views.xml',
'views/fp_work_center_views.xml',
'views/fp_tank_views.xml',
'views/fp_bath_log_views.xml',
'views/fp_facility_views.xml',
'views/fp_bath_views.xml',
'views/fp_process_node_views.xml',
'views/fp_menu.xml',
'data/fp_recipe_enp_alum_basic.xml',
],
'assets': {
'web.assets_backend': [
'fusion_plating/static/src/scss/fusion_plating.scss',
'fusion_plating/static/src/scss/recipe_tree_editor.scss',
'fusion_plating/static/src/xml/recipe_tree_editor.xml',
'fusion_plating/static/src/js/recipe_tree_editor.js',
],
},
'demo': [
'data/fp_demo_data.xml',
'data/fp_demo_recipe_data.xml',
],
'images': ['static/description/icon.png'],
'installable': True,
'auto_install': False,
'application': True,
}

View File

@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import recipe_controller

View File

@@ -1,188 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import logging
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
class FpRecipeController(http.Controller):
"""JSON-RPC endpoints for the process recipe tree editor."""
# ------------------------------------------------------------------
# Read — full tree
# ------------------------------------------------------------------
@http.route('/fp/recipe/tree', type='jsonrpc', auth='user')
def get_tree(self, recipe_id):
"""Return the full nested tree for a recipe."""
Node = request.env['fusion.plating.process.node']
recipe = Node.browse(int(recipe_id))
if not recipe.exists():
return {'ok': False, 'error': f'Recipe {recipe_id} not found.'}
return {
'ok': True,
'recipe': {
'id': recipe.id,
'name': recipe.name,
'code': recipe.code or '',
'version': recipe.version,
'process_type': recipe.process_type_id.name if recipe.process_type_id else '',
},
'tree': recipe.get_tree_data(),
}
# ------------------------------------------------------------------
# Create node
# ------------------------------------------------------------------
@http.route('/fp/recipe/node/create', type='jsonrpc', auth='user')
def create_node(self, parent_id, name, node_type='operation', vals=None):
"""Create a new child node under parent_id."""
Node = request.env['fusion.plating.process.node']
parent = Node.browse(int(parent_id))
if not parent.exists():
return {'ok': False, 'error': 'Parent node not found.'}
# Determine next sequence
max_seq = max((c.sequence for c in parent.child_ids), default=0)
data = {
'name': name,
'node_type': node_type,
'parent_id': parent.id,
'sequence': max_seq + 10,
}
if vals:
data.update(vals)
try:
new_node = Node.create(data)
_logger.info('Recipe: created node %s (%s) under %s by uid %s',
new_node.id, name, parent.id, request.env.uid)
return {'ok': True, 'node_id': new_node.id}
except Exception as exc:
_logger.exception('Recipe create_node failed')
return {'ok': False, 'error': str(exc)}
# ------------------------------------------------------------------
# Update node
# ------------------------------------------------------------------
@http.route('/fp/recipe/node/write', type='jsonrpc', auth='user')
def write_node(self, node_id, vals):
"""Update fields on an existing node."""
Node = request.env['fusion.plating.process.node']
node = Node.browse(int(node_id))
if not node.exists():
return {'ok': False, 'error': 'Node not found.'}
# Filter to allowed fields only
allowed = {
'name', 'code', 'node_type', 'icon', 'color',
'process_type_id', 'work_center_id',
'description', 'notes',
'estimated_duration',
'auto_complete', 'customer_visible', 'is_manual',
'requires_signoff', 'opt_in_out', 'sequence', 'version',
}
safe_vals = {k: v for k, v in vals.items() if k in allowed}
if not safe_vals:
return {'ok': False, 'error': 'No valid fields to update.'}
try:
node.write(safe_vals)
return {'ok': True}
except Exception as exc:
_logger.exception('Recipe write_node failed')
return {'ok': False, 'error': str(exc)}
# ------------------------------------------------------------------
# Delete node
# ------------------------------------------------------------------
@http.route('/fp/recipe/node/unlink', type='jsonrpc', auth='user')
def unlink_node(self, node_id):
"""Delete a node and all its children (cascade)."""
Node = request.env['fusion.plating.process.node']
node = Node.browse(int(node_id))
if not node.exists():
return {'ok': False, 'error': 'Node not found.'}
if node.node_type == 'recipe':
return {'ok': False, 'error': 'Cannot delete a recipe root from the tree editor. Use the list view.'}
try:
name = node.name
node.unlink()
_logger.info('Recipe: deleted node %s (%s) by uid %s',
node_id, name, request.env.uid)
return {'ok': True}
except Exception as exc:
_logger.exception('Recipe unlink_node failed')
return {'ok': False, 'error': str(exc)}
# ------------------------------------------------------------------
# Reorder siblings
# ------------------------------------------------------------------
@http.route('/fp/recipe/node/reorder', type='jsonrpc', auth='user')
def reorder_nodes(self, node_ids):
"""Bulk-update sequence for an ordered list of sibling node IDs."""
Node = request.env['fusion.plating.process.node']
try:
for idx, nid in enumerate(node_ids):
Node.browse(int(nid)).write({'sequence': (idx + 1) * 10})
return {'ok': True}
except Exception as exc:
_logger.exception('Recipe reorder failed')
return {'ok': False, 'error': str(exc)}
# ------------------------------------------------------------------
# Move node to new parent
# ------------------------------------------------------------------
@http.route('/fp/recipe/node/move', type='jsonrpc', auth='user')
def move_node(self, node_id, new_parent_id):
"""Move a node to a new parent (drag between sub-trees)."""
Node = request.env['fusion.plating.process.node']
node = Node.browse(int(node_id))
parent = Node.browse(int(new_parent_id))
if not node.exists() or not parent.exists():
return {'ok': False, 'error': 'Node or parent not found.'}
# Prevent moving a recipe root
if node.node_type == 'recipe':
return {'ok': False, 'error': 'Cannot move a recipe root.'}
# Prevent making a node its own descendant
if f'/{node.id}/' in (parent.parent_path or ''):
return {'ok': False, 'error': 'Cannot move a node under its own descendant.'}
try:
max_seq = max((c.sequence for c in parent.child_ids), default=0)
node.write({
'parent_id': parent.id,
'sequence': max_seq + 10,
})
return {'ok': True}
except Exception as exc:
_logger.exception('Recipe move_node failed')
return {'ok': False, 'error': str(exc)}
# ------------------------------------------------------------------
# Duplicate recipe
# ------------------------------------------------------------------
@http.route('/fp/recipe/duplicate', type='jsonrpc', auth='user')
def duplicate_recipe(self, recipe_id):
"""Deep-copy an entire recipe tree."""
Node = request.env['fusion.plating.process.node']
recipe = Node.browse(int(recipe_id))
if not recipe.exists():
return {'ok': False, 'error': 'Recipe not found.'}
if recipe.node_type != 'recipe':
return {'ok': False, 'error': 'Can only duplicate recipe roots.'}
try:
new_recipe = recipe.copy()
return {'ok': True, 'recipe_id': new_recipe.id}
except Exception as exc:
_logger.exception('Recipe duplicate failed')
return {'ok': False, 'error': str(exc)}

View File

@@ -1,322 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc. — DEMO DATA (temporary)
Remove this file and its manifest entry before production release.
-->
<odoo noupdate="1">
<!-- ========== DEMO PARTNERS ========== -->
<record id="demo_partner_aeroparts" model="res.partner">
<field name="name">AeroParts Manufacturing Inc.</field>
<field name="email">info@aeroparts.ca</field>
<field name="phone">905-555-0101</field>
<field name="city">Mississauga</field>
<field name="country_id" ref="base.ca"/>
<field name="company_type">company</field>
</record>
<record id="demo_partner_precision" model="res.partner">
<field name="name">Precision MFG Ltd.</field>
<field name="email">orders@precisionmfg.ca</field>
<field name="phone">416-555-0202</field>
<field name="city">Toronto</field>
<field name="country_id" ref="base.ca"/>
<field name="company_type">company</field>
</record>
<record id="demo_partner_opg" model="res.partner">
<field name="name">Ontario Power Generation</field>
<field name="email">procurement@opg.com</field>
<field name="phone">905-555-0303</field>
<field name="city">Pickering</field>
<field name="country_id" ref="base.ca"/>
<field name="company_type">company</field>
</record>
<!-- ========== FACILITIES ========== -->
<record id="demo_facility_main" model="fusion.plating.facility">
<field name="name">Fusion Plating — Main Plant</field>
<field name="code">FP-MAIN</field>
<field name="sequence">10</field>
</record>
<record id="demo_facility_east" model="fusion.plating.facility">
<field name="name">Fusion Plating — East Annex</field>
<field name="code">FP-EAST</field>
<field name="sequence">20</field>
</record>
<!-- ========== WORK CENTRES ========== -->
<record id="demo_wc_en_line" model="fusion.plating.work.center">
<field name="name">EN Plating Line</field>
<field name="code">WC-EN</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="capacity_per_day">80</field>
</record>
<record id="demo_wc_chrome_line" model="fusion.plating.work.center">
<field name="name">Chrome Line</field>
<field name="code">WC-CR</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="capacity_per_day">50</field>
</record>
<record id="demo_wc_anodize_line" model="fusion.plating.work.center">
<field name="name">Anodize Line</field>
<field name="code">WC-AN</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="capacity_per_day">120</field>
</record>
<record id="demo_wc_oxide_line" model="fusion.plating.work.center">
<field name="name">Black Oxide Line</field>
<field name="code">WC-BOX</field>
<field name="facility_id" ref="demo_facility_east"/>
<field name="capacity_per_day">60</field>
</record>
<record id="demo_wc_prep_line" model="fusion.plating.work.center">
<field name="name">Prep &amp; Clean Line</field>
<field name="code">WC-PREP</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="capacity_per_day">200</field>
</record>
<!-- ========== TANKS ========== -->
<!-- EN Line -->
<record id="demo_tank_en1" model="fusion.plating.tank">
<field name="name">EN Tank 1 — Mid-Phos</field>
<field name="code">T-EN-01</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_en_line"/>
<field name="current_process_id" ref="fusion_plating_process_en.ptype_en_mp"/>
<field name="volume">800</field>
<field name="volume_uom">l</field>
<field name="material">polypro</field>
<field name="heating_type">immersion</field>
<field name="has_filtration" eval="True"/>
<field name="state">in_use</field>
</record>
<record id="demo_tank_en2" model="fusion.plating.tank">
<field name="name">EN Tank 2 — High-Phos</field>
<field name="code">T-EN-02</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_en_line"/>
<field name="current_process_id" ref="fusion_plating_process_en.ptype_en_hp"/>
<field name="volume">600</field>
<field name="volume_uom">l</field>
<field name="material">polypro</field>
<field name="heating_type">immersion</field>
<field name="has_filtration" eval="True"/>
<field name="state">in_use</field>
</record>
<record id="demo_tank_en_strike" model="fusion.plating.tank">
<field name="name">EN Strike Tank</field>
<field name="code">T-EN-STK</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_en_line"/>
<field name="current_process_id" ref="fusion_plating_process_en.ptype_en_strike"/>
<field name="volume">300</field>
<field name="volume_uom">l</field>
<field name="material">polypro</field>
<field name="state">in_use</field>
</record>
<!-- Chrome Line -->
<record id="demo_tank_cr1" model="fusion.plating.tank">
<field name="name">Hard Chrome Tank 1</field>
<field name="code">T-CR-01</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_chrome_line"/>
<field name="current_process_id" ref="fusion_plating_process_chrome.ptype_cr_hard_hex"/>
<field name="volume">1200</field>
<field name="volume_uom">l</field>
<field name="material">lined_steel</field>
<field name="heating_type">immersion</field>
<field name="has_rectifier" eval="True"/>
<field name="has_filtration" eval="True"/>
<field name="state">in_use</field>
</record>
<record id="demo_tank_cr2" model="fusion.plating.tank">
<field name="name">Decorative Chrome Tank</field>
<field name="code">T-CR-02</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_chrome_line"/>
<field name="current_process_id" ref="fusion_plating_process_chrome.ptype_cr_dec_hex"/>
<field name="volume">500</field>
<field name="volume_uom">l</field>
<field name="material">lined_steel</field>
<field name="heating_type">immersion</field>
<field name="has_rectifier" eval="True"/>
<field name="state">in_use</field>
</record>
<record id="demo_tank_cr_strike" model="fusion.plating.tank">
<field name="name">Chrome Strike Tank</field>
<field name="code">T-CR-STK</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_chrome_line"/>
<field name="current_process_id" ref="fusion_plating_process_chrome.ptype_cr_strike"/>
<field name="volume">200</field>
<field name="volume_uom">l</field>
<field name="material">polypro</field>
<field name="has_rectifier" eval="True"/>
<field name="state">in_use</field>
</record>
<!-- Anodize Line -->
<record id="demo_tank_an1" model="fusion.plating.tank">
<field name="name">Type II Sulfuric Anodize</field>
<field name="code">T-AN-01</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_anodize_line"/>
<field name="current_process_id" ref="fusion_plating_process_anodize.ptype_an_type_ii"/>
<field name="volume">2000</field>
<field name="volume_uom">l</field>
<field name="material">polypro</field>
<field name="heating_type">jacket</field>
<field name="has_rectifier" eval="True"/>
<field name="has_filtration" eval="True"/>
<field name="state">in_use</field>
</record>
<record id="demo_tank_an2" model="fusion.plating.tank">
<field name="name">Type III Hardcoat Anodize</field>
<field name="code">T-AN-02</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_anodize_line"/>
<field name="current_process_id" ref="fusion_plating_process_anodize.ptype_an_type_iii"/>
<field name="volume">1500</field>
<field name="volume_uom">l</field>
<field name="material">polypro</field>
<field name="heating_type">jacket</field>
<field name="has_rectifier" eval="True"/>
<field name="has_filtration" eval="True"/>
<field name="state">in_use</field>
</record>
<record id="demo_tank_an_seal" model="fusion.plating.tank">
<field name="name">Hot Water Seal Tank</field>
<field name="code">T-AN-SEAL</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_anodize_line"/>
<field name="current_process_id" ref="fusion_plating_process_anodize.ptype_an_seal_hot"/>
<field name="volume">1000</field>
<field name="volume_uom">l</field>
<field name="material">ss</field>
<field name="heating_type">immersion</field>
<field name="state">in_use</field>
</record>
<record id="demo_tank_an_dye" model="fusion.plating.tank">
<field name="name">Dye Immersion Tank — Black</field>
<field name="code">T-AN-DYE</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_anodize_line"/>
<field name="current_process_id" ref="fusion_plating_process_anodize.ptype_an_dye"/>
<field name="volume">500</field>
<field name="volume_uom">l</field>
<field name="material">polypro</field>
<field name="state">in_use</field>
</record>
<!-- Black Oxide Line (East) -->
<record id="demo_tank_box1" model="fusion.plating.tank">
<field name="name">Hot Black Oxide Tank</field>
<field name="code">T-BOX-01</field>
<field name="facility_id" ref="demo_facility_east"/>
<field name="work_center_id" ref="demo_wc_oxide_line"/>
<field name="current_process_id" ref="fusion_plating_process_black_oxide.ptype_box_hot"/>
<field name="volume">400</field>
<field name="volume_uom">l</field>
<field name="material">ss</field>
<field name="heating_type">external</field>
<field name="state">in_use</field>
</record>
<record id="demo_tank_box_seal" model="fusion.plating.tank">
<field name="name">Sealing Oil Dip</field>
<field name="code">T-BOX-SEAL</field>
<field name="facility_id" ref="demo_facility_east"/>
<field name="work_center_id" ref="demo_wc_oxide_line"/>
<field name="current_process_id" ref="fusion_plating_process_black_oxide.ptype_box_seal_oil"/>
<field name="volume">300</field>
<field name="volume_uom">l</field>
<field name="material">ss</field>
<field name="state">in_use</field>
</record>
<!-- Maintenance tank -->
<record id="demo_tank_maint" model="fusion.plating.tank">
<field name="name">Rinse Tank 3 (Down for Repair)</field>
<field name="code">T-RN-03</field>
<field name="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_prep_line"/>
<field name="volume">400</field>
<field name="volume_uom">l</field>
<field name="material">polypro</field>
<field name="state">maintenance</field>
</record>
<!-- ========== BATHS ========== -->
<record id="demo_bath_en_mp" model="fusion.plating.bath">
<field name="name">EN Mid-Phos Bath A</field>
<field name="tank_id" ref="demo_tank_en1"/>
<field name="facility_id" ref="demo_facility_main"/>
<field name="process_type_id" ref="fusion_plating_process_en.ptype_en_mp"/>
<field name="state">operational</field>
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=14)).strftime('%Y-%m-%d')"/>
</record>
<record id="demo_bath_en_hp" model="fusion.plating.bath">
<field name="name">EN High-Phos Bath B</field>
<field name="tank_id" ref="demo_tank_en2"/>
<field name="facility_id" ref="demo_facility_main"/>
<field name="process_type_id" ref="fusion_plating_process_en.ptype_en_hp"/>
<field name="state">operational</field>
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=30)).strftime('%Y-%m-%d')"/>
</record>
<record id="demo_bath_cr_hard" model="fusion.plating.bath">
<field name="name">Hard Chrome Bath 1</field>
<field name="tank_id" ref="demo_tank_cr1"/>
<field name="facility_id" ref="demo_facility_main"/>
<field name="process_type_id" ref="fusion_plating_process_chrome.ptype_cr_hard_hex"/>
<field name="state">operational</field>
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=60)).strftime('%Y-%m-%d')"/>
</record>
<record id="demo_bath_an_typeii" model="fusion.plating.bath">
<field name="name">Sulfuric Anodize Bath</field>
<field name="tank_id" ref="demo_tank_an1"/>
<field name="facility_id" ref="demo_facility_main"/>
<field name="process_type_id" ref="fusion_plating_process_anodize.ptype_an_type_ii"/>
<field name="state">operational</field>
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=7)).strftime('%Y-%m-%d')"/>
</record>
<record id="demo_bath_box_hot" model="fusion.plating.bath">
<field name="name">Hot Black Oxide Bath</field>
<field name="tank_id" ref="demo_tank_box1"/>
<field name="facility_id" ref="demo_facility_east"/>
<field name="process_type_id" ref="fusion_plating_process_black_oxide.ptype_box_hot"/>
<field name="state">operational</field>
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=45)).strftime('%Y-%m-%d')"/>
</record>
<!-- Aged bath nearing dump -->
<record id="demo_bath_cr_dec" model="fusion.plating.bath">
<field name="name">Decorative Chrome Bath (aging)</field>
<field name="tank_id" ref="demo_tank_cr2"/>
<field name="facility_id" ref="demo_facility_main"/>
<field name="process_type_id" ref="fusion_plating_process_chrome.ptype_cr_dec_hex"/>
<field name="state">dump_scheduled</field>
<field name="makeup_date" eval="(DateTime.today() - timedelta(days=180)).strftime('%Y-%m-%d')"/>
<field name="dump_scheduled_date" eval="(DateTime.today() + timedelta(days=10)).strftime('%Y-%m-%d')"/>
</record>
</odoo>

View File

@@ -1,262 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Demo recipe: Electroless Nickel Plating — Steel Line
-->
<odoo>
<data noupdate="1">
<!-- ===== ROOT: Electroless Nickel Plating — Steel Line ===== -->
<record id="demo_recipe_en_steel" model="fusion.plating.process.node">
<field name="name">Electroless Nickel Plating — Steel Line</field>
<field name="code">EN_STEEL</field>
<field name="node_type">recipe</field>
<field name="icon">fa-flask</field>
<field name="sequence">10</field>
<field name="customer_visible">True</field>
</record>
<!-- 1. Blasting -->
<record id="demo_node_blasting" model="fusion.plating.process.node">
<field name="name">Blasting</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_recipe_en_steel"/>
<field name="icon">fa-bullseye</field>
<field name="sequence">10</field>
<field name="estimated_duration">30</field>
<field name="is_manual">True</field>
</record>
<record id="demo_step_ready_blast" model="fusion.plating.process.node">
<field name="name">Ready for Blast</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_blasting"/>
<field name="icon">fa-clock-o</field>
<field name="sequence">10</field>
</record>
<record id="demo_step_blast" model="fusion.plating.process.node">
<field name="name">Blast</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_blasting"/>
<field name="icon">fa-fire</field>
<field name="sequence">20</field>
<field name="requires_signoff">True</field>
</record>
<!-- 2. Masking -->
<record id="demo_node_masking" model="fusion.plating.process.node">
<field name="name">Masking</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_recipe_en_steel"/>
<field name="icon">fa-paint-brush</field>
<field name="sequence">20</field>
<field name="estimated_duration">45</field>
<field name="is_manual">True</field>
</record>
<record id="demo_step_ready_mask" model="fusion.plating.process.node">
<field name="name">Ready for Masking</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_masking"/>
<field name="sequence">10</field>
</record>
<record id="demo_step_mask" model="fusion.plating.process.node">
<field name="name">Masking</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_masking"/>
<field name="icon">fa-paint-brush</field>
<field name="sequence">20</field>
<field name="requires_signoff">True</field>
</record>
<!-- 3. Racking -->
<record id="demo_node_racking" model="fusion.plating.process.node">
<field name="name">Racking</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_recipe_en_steel"/>
<field name="icon">fa-th</field>
<field name="sequence">30</field>
<field name="estimated_duration">20</field>
</record>
<!-- 4. Steel Line (sub-process with many children) -->
<record id="demo_node_steel_line" model="fusion.plating.process.node">
<field name="name">Steel Line</field>
<field name="node_type">sub_process</field>
<field name="parent_id" ref="demo_recipe_en_steel"/>
<field name="icon">fa-industry</field>
<field name="sequence">40</field>
<field name="auto_complete">True</field>
</record>
<!-- 4a. Cleaner (sub-process inside Steel Line) -->
<record id="demo_node_cleaner" model="fusion.plating.process.node">
<field name="name">Cleaner</field>
<field name="node_type">sub_process</field>
<field name="parent_id" ref="demo_node_steel_line"/>
<field name="icon">fa-shower</field>
<field name="sequence">10</field>
</record>
<record id="demo_step_soak_clean" model="fusion.plating.process.node">
<field name="name">Soak Clean (S-3)</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_cleaner"/>
<field name="sequence">10</field>
<field name="estimated_duration">10</field>
<field name="is_manual">False</field>
</record>
<record id="demo_step_electroclean" model="fusion.plating.process.node">
<field name="name">Electroclean (S-3)</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_cleaner"/>
<field name="sequence">20</field>
<field name="estimated_duration">5</field>
<field name="is_manual">False</field>
</record>
<record id="demo_step_primary_rinse_1" model="fusion.plating.process.node">
<field name="name">Primary Rinse (S-4)</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_cleaner"/>
<field name="sequence">30</field>
<field name="is_manual">False</field>
</record>
<!-- 4b. Acid Dip -->
<record id="demo_node_acid_dip" model="fusion.plating.process.node">
<field name="name">Acid Dip (S-5)</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_node_steel_line"/>
<field name="icon">fa-flask</field>
<field name="sequence">20</field>
<field name="estimated_duration">5</field>
<field name="is_manual">False</field>
</record>
<!-- 4c. Nickel Strike -->
<record id="demo_node_nickel_strike" model="fusion.plating.process.node">
<field name="name">Nickel Strike (S-7 / SP-5)</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_node_steel_line"/>
<field name="icon">fa-bolt</field>
<field name="sequence">30</field>
<field name="estimated_duration">8</field>
<field name="is_manual">False</field>
<field name="requires_signoff">True</field>
</record>
<!-- 4d. E-Nickel Plate (Mid Phos) -->
<record id="demo_node_en_plate" model="fusion.plating.process.node">
<field name="name">E-Nickel Plate (Mid Phos) (S-9)</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_node_steel_line"/>
<field name="icon">fa-diamond</field>
<field name="sequence">40</field>
<field name="estimated_duration">90</field>
<field name="is_manual">False</field>
<field name="requires_signoff">True</field>
</record>
<record id="demo_step_rinse_after_plate" model="fusion.plating.process.node">
<field name="name">Primary Rinse (S-11)</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_en_plate"/>
<field name="sequence">10</field>
<field name="is_manual">False</field>
</record>
<record id="demo_step_hot_rinse" model="fusion.plating.process.node">
<field name="name">Hot Rinse (S-13)</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_en_plate"/>
<field name="sequence">20</field>
<field name="is_manual">False</field>
</record>
<!-- 4e. Hot Water Porosity -->
<record id="demo_node_porosity" model="fusion.plating.process.node">
<field name="name">Hot Water Porosity (A-15)</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_node_steel_line"/>
<field name="icon">fa-tint</field>
<field name="sequence">50</field>
<field name="estimated_duration">15</field>
<field name="requires_signoff">True</field>
</record>
<!-- 4f. Dry -->
<record id="demo_node_dry" model="fusion.plating.process.node">
<field name="name">Dry</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_node_steel_line"/>
<field name="icon">fa-sun-o</field>
<field name="sequence">60</field>
<field name="is_manual">False</field>
</record>
<!-- 5. Oven Baking -->
<record id="demo_node_oven_bake" model="fusion.plating.process.node">
<field name="name">Oven Baking</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_recipe_en_steel"/>
<field name="icon">fa-fire</field>
<field name="sequence">50</field>
<field name="estimated_duration">240</field>
<field name="is_manual">False</field>
<field name="requires_signoff">True</field>
</record>
<!-- 6. De-racking -->
<record id="demo_node_derack" model="fusion.plating.process.node">
<field name="name">De-Racking</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_recipe_en_steel"/>
<field name="icon">fa-th</field>
<field name="sequence">60</field>
<field name="estimated_duration">15</field>
</record>
<!-- 7. De-Masking -->
<record id="demo_node_demask" model="fusion.plating.process.node">
<field name="name">De-Masking</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_recipe_en_steel"/>
<field name="icon">fa-eraser</field>
<field name="sequence">70</field>
<field name="estimated_duration">20</field>
</record>
<!-- 8. Oven Bake (Post De-Rack) -->
<record id="demo_node_post_bake" model="fusion.plating.process.node">
<field name="name">Oven Bake (Post De-Rack)</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_recipe_en_steel"/>
<field name="icon">fa-fire</field>
<field name="sequence">80</field>
<field name="estimated_duration">120</field>
<field name="is_manual">False</field>
</record>
<!-- 9. Post Plate Inspection -->
<record id="demo_node_inspection" model="fusion.plating.process.node">
<field name="name">Post Plate Inspection</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="demo_recipe_en_steel"/>
<field name="icon">fa-search</field>
<field name="sequence">90</field>
<field name="estimated_duration">30</field>
<field name="requires_signoff">True</field>
</record>
<record id="demo_step_ready_inspect" model="fusion.plating.process.node">
<field name="name">Ready for Post Plate Inspection</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_inspection"/>
<field name="sequence">10</field>
</record>
<record id="demo_step_inspect" model="fusion.plating.process.node">
<field name="name">Post Plate Inspection</field>
<field name="node_type">step</field>
<field name="parent_id" ref="demo_node_inspection"/>
<field name="icon">fa-check-circle</field>
<field name="sequence">20</field>
<field name="requires_signoff">True</field>
</record>
</data>
</odoo>

View File

@@ -1,70 +0,0 @@
<?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.
Seed process categories. Categories are the one pinch of generic
taxonomy core ships with — specific process types themselves are
loaded by process packs (fusion_plating_process_en, etc.).
-->
<odoo noupdate="1">
<record id="pcat_plating" model="fusion.plating.process.category">
<field name="name">Plating</field>
<field name="code">plating</field>
<field name="sequence">10</field>
<field name="description">Deposition of a metallic layer onto a substrate, either electrolytically or autocatalytically.</field>
</record>
<record id="pcat_anodizing" model="fusion.plating.process.category">
<field name="name">Anodizing</field>
<field name="code">anodizing</field>
<field name="sequence">20</field>
<field name="description">Electrochemical conversion of a substrate surface into an oxide layer (typically aluminum).</field>
</record>
<record id="pcat_coating" model="fusion.plating.process.category">
<field name="name">Coating</field>
<field name="code">coating</field>
<field name="sequence">30</field>
<field name="description">Non-metallic or hybrid surface coating (paint, powder, PTFE composite, etc.).</field>
</record>
<record id="pcat_conversion" model="fusion.plating.process.category">
<field name="name">Conversion Coating</field>
<field name="code">conversion</field>
<field name="sequence">40</field>
<field name="description">Chemical reaction forming a protective film from the substrate itself (chromate, phosphate, black oxide).</field>
</record>
<record id="pcat_prep" model="fusion.plating.process.category">
<field name="name">Preparation</field>
<field name="code">prep</field>
<field name="sequence">50</field>
<field name="description">Cleaning, degreasing, etching, activation — surface prep before the main finishing step.</field>
</record>
<record id="pcat_strip" model="fusion.plating.process.category">
<field name="name">Stripping</field>
<field name="code">strip</field>
<field name="sequence">60</field>
<field name="description">Chemical or electrolytic removal of an existing coating.</field>
</record>
<record id="pcat_post" model="fusion.plating.process.category">
<field name="name">Post-Treatment</field>
<field name="code">post</field>
<field name="sequence">70</field>
<field name="description">Sealing, dyeing, heat treatment, embrittlement relief, passivation.</field>
</record>
<record id="pcat_other" model="fusion.plating.process.category">
<field name="name">Other</field>
<field name="code">other</field>
<field name="sequence">100</field>
<field name="description">Catch-all for processes that do not fit the standard categories.</field>
</record>
</odoo>

View File

@@ -1,233 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Recipe: ENP-ALUM-BASIC (Electroless Nickel Plating — Aluminium Basic)
Source: Client's Steelhead export
-->
<odoo>
<data noupdate="0">
<!-- ===== ROOT ===== -->
<record id="recipe_enp_alum_basic" model="fusion.plating.process.node">
<field name="name">ENP-ALUM-BASIC</field>
<field name="code">ENP_ALUM_BASIC</field>
<field name="node_type">recipe</field>
<field name="icon">fa-flask</field>
<field name="sequence">10</field>
<field name="auto_complete">True</field>
<field name="customer_visible">False</field>
</record>
<!-- ===== 1. Masking ===== -->
<record id="enp_ab_masking" model="fusion.plating.process.node">
<field name="name">Masking</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="recipe_enp_alum_basic"/>
<field name="icon">fa-paint-brush</field>
<field name="sequence">10</field>
<field name="auto_complete">True</field>
<field name="is_manual">True</field>
</record>
<record id="enp_ab_masking_ready" model="fusion.plating.process.node">
<field name="name">Ready For Masking</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_masking"/>
<field name="icon">fa-clock-o</field>
<field name="sequence">10</field>
<field name="is_manual">True</field>
</record>
<record id="enp_ab_masking_do" model="fusion.plating.process.node">
<field name="name">Masking</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_masking"/>
<field name="icon">fa-paint-brush</field>
<field name="sequence">20</field>
<field name="is_manual">True</field>
<field name="requires_signoff">True</field>
</record>
<!-- ===== 2. Racking ===== -->
<record id="enp_ab_racking" model="fusion.plating.process.node">
<field name="name">Racking</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="recipe_enp_alum_basic"/>
<field name="icon">fa-th</field>
<field name="sequence">20</field>
<field name="auto_complete">True</field>
</record>
<record id="enp_ab_racking_ready" model="fusion.plating.process.node">
<field name="name">Ready for Racking</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_racking"/>
<field name="sequence">10</field>
</record>
<record id="enp_ab_racking_do" model="fusion.plating.process.node">
<field name="name">Racking</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_racking"/>
<field name="icon">fa-th</field>
<field name="sequence">20</field>
<field name="requires_signoff">True</field>
</record>
<!-- ===== 3. Ready for processing ===== -->
<record id="enp_ab_ready_processing" model="fusion.plating.process.node">
<field name="name">Ready for processing</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="recipe_enp_alum_basic"/>
<field name="icon">fa-clock-o</field>
<field name="sequence">30</field>
</record>
<!-- ===== 4. ENP-Alum Line (sub-process) ===== -->
<record id="enp_ab_alum_line" model="fusion.plating.process.node">
<field name="name">ENP-Alum Line</field>
<field name="node_type">sub_process</field>
<field name="parent_id" ref="recipe_enp_alum_basic"/>
<field name="icon">fa-industry</field>
<field name="sequence">40</field>
<field name="auto_complete">True</field>
</record>
<!-- 4a. E-Nickel Plating -->
<record id="enp_ab_enickel_plating" model="fusion.plating.process.node">
<field name="name">E-Nickel Plating</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="enp_ab_alum_line"/>
<field name="icon">fa-diamond</field>
<field name="sequence">10</field>
<field name="requires_signoff">True</field>
<field name="is_manual">False</field>
</record>
<!-- ===== 5. De-Masking ===== -->
<record id="enp_ab_demasking" model="fusion.plating.process.node">
<field name="name">De-Masking</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="recipe_enp_alum_basic"/>
<field name="icon">fa-eraser</field>
<field name="sequence">50</field>
<field name="auto_complete">True</field>
</record>
<record id="enp_ab_demasking_ready" model="fusion.plating.process.node">
<field name="name">Ready for De-Masking</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_demasking"/>
<field name="sequence">10</field>
</record>
<record id="enp_ab_demasking_do" model="fusion.plating.process.node">
<field name="name">De-Masking</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_demasking"/>
<field name="icon">fa-eraser</field>
<field name="sequence">20</field>
</record>
<!-- ===== 6. Oven baking ===== -->
<record id="enp_ab_oven_bake" model="fusion.plating.process.node">
<field name="name">Oven baking</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="recipe_enp_alum_basic"/>
<field name="icon">fa-fire</field>
<field name="sequence">60</field>
<field name="auto_complete">True</field>
<field name="is_manual">False</field>
</record>
<record id="enp_ab_oven_bake_ready" model="fusion.plating.process.node">
<field name="name">Ready for bake</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_oven_bake"/>
<field name="icon">fa-clock-o</field>
<field name="sequence">10</field>
<field name="is_manual">True</field>
</record>
<record id="enp_ab_oven_bake_do" model="fusion.plating.process.node">
<field name="name">Bake</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_oven_bake"/>
<field name="icon">fa-fire</field>
<field name="sequence">20</field>
<field name="is_manual">True</field>
<field name="requires_signoff">True</field>
</record>
<!-- ===== 7. De-racking ===== -->
<record id="enp_ab_deracking" model="fusion.plating.process.node">
<field name="name">De-racking</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="recipe_enp_alum_basic"/>
<field name="icon">fa-th</field>
<field name="sequence">70</field>
<field name="auto_complete">True</field>
</record>
<record id="enp_ab_deracking_ready" model="fusion.plating.process.node">
<field name="name">Ready For DeRacking</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_deracking"/>
<field name="icon">fa-clock-o</field>
<field name="sequence">10</field>
<field name="is_manual">True</field>
</record>
<record id="enp_ab_deracking_do" model="fusion.plating.process.node">
<field name="name">DeRacking</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_deracking"/>
<field name="icon">fa-th</field>
<field name="sequence">20</field>
<field name="requires_signoff">True</field>
</record>
<!-- ===== 8. Oven bake (Post de-rack) ===== -->
<record id="enp_ab_post_bake" model="fusion.plating.process.node">
<field name="name">Oven bake (Post de-rack)</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="recipe_enp_alum_basic"/>
<field name="icon">fa-fire</field>
<field name="sequence">80</field>
<field name="auto_complete">True</field>
<field name="is_manual">False</field>
</record>
<record id="enp_ab_post_bake_ready" model="fusion.plating.process.node">
<field name="name">Ready for bake</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_post_bake"/>
<field name="icon">fa-clock-o</field>
<field name="sequence">10</field>
<field name="is_manual">True</field>
</record>
<record id="enp_ab_post_bake_do" model="fusion.plating.process.node">
<field name="name">Bake</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_post_bake"/>
<field name="icon">fa-fire</field>
<field name="sequence">20</field>
<field name="is_manual">True</field>
<field name="requires_signoff">True</field>
</record>
<!-- ===== 9. Post-plate Inspection ===== -->
<record id="enp_ab_inspection" model="fusion.plating.process.node">
<field name="name">Post-plate Inspection</field>
<field name="node_type">operation</field>
<field name="parent_id" ref="recipe_enp_alum_basic"/>
<field name="icon">fa-search</field>
<field name="sequence">90</field>
<field name="requires_signoff">True</field>
</record>
<record id="enp_ab_inspection_ready" model="fusion.plating.process.node">
<field name="name">Ready for post-plate Inspection</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_inspection"/>
<field name="sequence">10</field>
</record>
<record id="enp_ab_inspection_do" model="fusion.plating.process.node">
<field name="name">Post-plate Inspection</field>
<field name="node_type">step</field>
<field name="parent_id" ref="enp_ab_inspection"/>
<field name="icon">fa-check-circle</field>
<field name="sequence">20</field>
<field name="requires_signoff">True</field>
</record>
</data>
</odoo>

View File

@@ -1,26 +0,0 @@
<?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_bath" model="ir.sequence">
<field name="name">Fusion Plating: Bath</field>
<field name="code">fusion.plating.bath</field>
<field name="prefix">BATH/%(year)s/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_fp_bath_log" model="ir.sequence">
<field name="name">Fusion Plating: Bath Log</field>
<field name="code">fusion.plating.bath.log</field>
<field name="prefix">BLOG/%(year)s%(month)s/</field>
<field name="padding">6</field>
<field name="company_id" eval="False"/>
</record>
</odoo>

View File

@@ -1,16 +0,0 @@
# -*- 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_process_category
from . import fp_process_type
from . import fp_facility
from . import fp_work_center
from . import fp_tank
from . import fp_bath
from . import fp_bath_log
from . import fp_bath_log_line
from . import fp_bath_parameter
from . import fp_process_node
from . import res_company

View File

@@ -1,269 +0,0 @@
# -*- 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 FpBath(models.Model):
"""A specific batch of chemistry in a tank.
Baths have their own lifecycle independent of the tank:
new → operational → under_review → dump_scheduled → dumped
Each bath carries:
* its process type (which chemistry it runs)
* per-bath target ranges (may override process defaults)
* running MTO counter (set and maintained by the process pack)
* chemistry log history (one2many to fusion.plating.bath.log)
Process packs (fusion_plating_process_en, etc.) add process-specific
computed fields such as orthophosphite projection or P-content band
without touching the generic bath model.
"""
_name = 'fusion.plating.bath'
_description = 'Fusion Plating — Bath'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'state, makeup_date desc, id desc'
_rec_name = 'display_name'
name = fields.Char(
string='Reference',
required=True,
copy=False,
default=lambda self: self._default_name(),
tracking=True,
)
display_name = fields.Char(
compute='_compute_display_name',
store=True,
)
tank_id = fields.Many2one(
'fusion.plating.tank',
string='Tank',
required=True,
ondelete='restrict',
tracking=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
related='tank_id.facility_id',
store=True,
readonly=True,
)
process_type_id = fields.Many2one(
'fusion.plating.process.type',
string='Process',
required=True,
ondelete='restrict',
tracking=True,
)
company_id = fields.Many2one(
'res.company',
related='facility_id.company_id',
store=True,
readonly=True,
)
# ----- Lifecycle ------------------------------------------------------
state = fields.Selection(
[
('new', 'New'),
('operational', 'Operational'),
('under_review', 'Under Review'),
('dump_scheduled', 'Dump Scheduled'),
('dumped', 'Dumped'),
],
string='Status',
default='new',
tracking=True,
required=True,
)
status_color = fields.Integer(
string='Status Color',
compute='_compute_status_color',
help='Kanban colour index derived from state and chemistry health.',
)
makeup_date = fields.Datetime(
string='Makeup Date',
help='When this bath was made up (initial fresh charge).',
tracking=True,
)
makeup_by_id = fields.Many2one(
'res.users',
string='Made Up By',
tracking=True,
)
dump_scheduled_date = fields.Datetime(
string='Dump Scheduled',
tracking=True,
)
dumped_date = fields.Datetime(
string='Dumped Date',
tracking=True,
)
dump_reason = fields.Text(
string='Dump Reason',
)
notes = fields.Html(
string='Notes',
)
# ----- Chemistry target ranges (per-bath; override process defaults) --
target_line_ids = fields.One2many(
'fusion.plating.bath.target',
'bath_id',
string='Target Parameters',
copy=True,
)
# ----- Logs -----------------------------------------------------------
log_ids = fields.One2many(
'fusion.plating.bath.log',
'bath_id',
string='Chemistry Logs',
)
log_count = fields.Integer(
compute='_compute_log_count',
)
last_log_date = fields.Datetime(
compute='_compute_last_log',
store=True,
)
last_log_status = fields.Selection(
[
('ok', 'OK'),
('warning', 'Warning'),
('out_of_spec', 'Out of Spec'),
],
compute='_compute_last_log',
store=True,
)
# ----- Generic age / volume (process packs refine) --------------------
mto_count = fields.Float(
string='MTO',
default=0.0,
help='Metal Turnovers. Maintained by process packs that model '
'replenishment (e.g. fusion_plating_process_en).',
)
volume = fields.Float(
string='Volume',
help='Working volume (defaults to tank volume on makeup).',
)
active = fields.Boolean(default=True)
# ==========================================================================
# Defaults
# ==========================================================================
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.bath')
return seq or '/'
# ==========================================================================
# Computes
# ==========================================================================
@api.depends('name', 'process_type_id', 'tank_id')
def _compute_display_name(self):
for rec in self:
parts = [rec.name or '']
if rec.process_type_id:
parts.append(f'({rec.process_type_id.code})')
if rec.tank_id:
parts.append(f'@ {rec.tank_id.code}')
rec.display_name = ' '.join(p for p in parts if p)
def _compute_log_count(self):
for rec in self:
rec.log_count = len(rec.log_ids)
@api.depends('log_ids', 'log_ids.log_date', 'log_ids.status')
def _compute_last_log(self):
for rec in self:
last = rec.log_ids.sorted('log_date', reverse=True)[:1]
rec.last_log_date = last.log_date if last else False
rec.last_log_status = last.status if last else False
@api.depends('state', 'last_log_status')
def _compute_status_color(self):
"""Kanban colour index — neutral palette that works in light + dark.
Uses Odoo's built-in color index rather than hex codes, so themes
control the final rendering.
"""
# 0=no color, 4=green, 3=yellow, 2=orange, 1=red, 5=purple, 10=grey
for rec in self:
if rec.state == 'dumped':
rec.status_color = 10 # grey
elif rec.state == 'dump_scheduled':
rec.status_color = 2 # orange
elif rec.state == 'under_review':
rec.status_color = 3 # yellow
elif rec.state == 'new':
rec.status_color = 5 # purple
elif rec.last_log_status == 'out_of_spec':
rec.status_color = 1 # red
elif rec.last_log_status == 'warning':
rec.status_color = 3 # yellow
else:
rec.status_color = 4 # green
# ==========================================================================
# Actions
# ==========================================================================
def action_make_operational(self):
self.write({'state': 'operational'})
def action_mark_under_review(self):
self.write({'state': 'under_review'})
def action_schedule_dump(self):
self.write({
'state': 'dump_scheduled',
'dump_scheduled_date': fields.Datetime.now(),
})
def action_dump(self):
self.write({
'state': 'dumped',
'dumped_date': fields.Datetime.now(),
})
class FpBathTarget(models.Model):
"""Per-bath target range for a chemistry parameter."""
_name = 'fusion.plating.bath.target'
_description = 'Fusion Plating — Bath Target'
_order = 'bath_id, sequence, parameter_id'
bath_id = fields.Many2one(
'fusion.plating.bath',
string='Bath',
required=True,
ondelete='cascade',
)
parameter_id = fields.Many2one(
'fusion.plating.bath.parameter',
string='Parameter',
required=True,
ondelete='restrict',
)
sequence = fields.Integer(default=10)
target_min = fields.Float(string='Min')
target_max = fields.Float(string='Max')
uom = fields.Char(
related='parameter_id.uom',
readonly=True,
)
_sql_constraints = [
(
'fp_bath_target_uniq',
'unique(bath_id, parameter_id)',
'Each parameter can only be defined once per bath.',
),
]

View File

@@ -1,144 +0,0 @@
# -*- 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 FpBathLog(models.Model):
"""A daily / per-shift chemistry log for a bath.
One log record represents one sampling event: an operator walks to a
tank, runs titrations or reads instruments, and enters the results.
Each log has one or more lines (one per parameter).
Overall log status is rolled up from the lines:
* ok — every line is within target
* warning — at least one line is within warning tolerance
* out_of_spec — at least one line is outside target
"""
_name = 'fusion.plating.bath.log'
_description = 'Fusion Plating — Bath Chemistry Log'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'log_date desc, id desc'
_rec_name = 'display_name'
name = fields.Char(
string='Reference',
required=True,
copy=False,
default=lambda self: self._default_name(),
tracking=True,
)
display_name = fields.Char(
compute='_compute_display_name',
store=True,
)
bath_id = fields.Many2one(
'fusion.plating.bath',
string='Bath',
required=True,
ondelete='cascade',
tracking=True,
)
tank_id = fields.Many2one(
related='bath_id.tank_id',
store=True,
readonly=True,
)
facility_id = fields.Many2one(
related='bath_id.facility_id',
store=True,
readonly=True,
)
process_type_id = fields.Many2one(
related='bath_id.process_type_id',
store=True,
readonly=True,
)
company_id = fields.Many2one(
related='bath_id.company_id',
store=True,
readonly=True,
)
log_date = fields.Datetime(
string='Logged At',
default=fields.Datetime.now,
required=True,
tracking=True,
)
operator_id = fields.Many2one(
'res.users',
string='Operator',
default=lambda self: self.env.user,
tracking=True,
)
shift = fields.Selection(
[
('day', 'Day'),
('evening', 'Evening'),
('night', 'Night'),
],
string='Shift',
)
line_ids = fields.One2many(
'fusion.plating.bath.log.line',
'log_id',
string='Readings',
copy=True,
)
status = fields.Selection(
[
('ok', 'OK'),
('warning', 'Warning'),
('out_of_spec', 'Out of Spec'),
],
string='Status',
compute='_compute_status',
store=True,
tracking=True,
)
status_color = fields.Integer(
compute='_compute_status_color',
)
notes = fields.Text(
string='Notes',
)
# ==========================================================================
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.bath.log')
return seq or '/'
@api.depends('name', 'bath_id', 'log_date')
def _compute_display_name(self):
for rec in self:
parts = []
if rec.bath_id:
parts.append(rec.bath_id.name)
if rec.log_date:
parts.append(fields.Datetime.to_string(rec.log_date))
rec.display_name = ''.join(parts) if parts else rec.name
@api.depends('line_ids', 'line_ids.status')
def _compute_status(self):
for rec in self:
statuses = set(rec.line_ids.mapped('status'))
if 'out_of_spec' in statuses:
rec.status = 'out_of_spec'
elif 'warning' in statuses:
rec.status = 'warning'
else:
rec.status = 'ok'
@api.depends('status')
def _compute_status_color(self):
# Kanban color indexes: 0 default, 1 red, 3 yellow, 4 green
mapping = {'ok': 4, 'warning': 3, 'out_of_spec': 1}
for rec in self:
rec.status_color = mapping.get(rec.status, 0)

View File

@@ -1,114 +0,0 @@
# -*- 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 FpBathLogLine(models.Model):
"""A single parameter reading on a bath log.
Each line = one titration result or one sensor reading. Target ranges
are pulled from the bath's per-bath overrides if present, otherwise
from the parameter's defaults on fusion.plating.bath.parameter.
Status is computed per line (ok / warning / out_of_spec) and rolled
up to the parent log.
"""
_name = 'fusion.plating.bath.log.line'
_description = 'Fusion Plating — Bath Log Reading'
_order = 'log_id, sequence, id'
log_id = fields.Many2one(
'fusion.plating.bath.log',
string='Log',
required=True,
ondelete='cascade',
index=True,
)
bath_id = fields.Many2one(
related='log_id.bath_id',
store=True,
readonly=True,
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
parameter_id = fields.Many2one(
'fusion.plating.bath.parameter',
string='Parameter',
required=True,
ondelete='restrict',
)
parameter_code = fields.Char(
related='parameter_id.code',
store=True,
readonly=True,
)
uom = fields.Char(
related='parameter_id.uom',
readonly=True,
)
value = fields.Float(
string='Value',
required=True,
)
target_min = fields.Float(
string='Target Min',
compute='_compute_targets',
store=True,
)
target_max = fields.Float(
string='Target Max',
compute='_compute_targets',
store=True,
)
status = fields.Selection(
[
('ok', 'OK'),
('warning', 'Warning'),
('out_of_spec', 'Out of Spec'),
],
string='Status',
compute='_compute_status',
store=True,
)
notes = fields.Char(
string='Notes',
)
# ==========================================================================
@api.depends('parameter_id', 'log_id.bath_id')
def _compute_targets(self):
"""Resolve target range: per-bath override first, parameter default second."""
for rec in self:
tmin = tmax = 0.0
if rec.log_id.bath_id and rec.parameter_id:
override = rec.log_id.bath_id.target_line_ids.filtered(
lambda t: t.parameter_id.id == rec.parameter_id.id
)[:1]
if override:
tmin, tmax = override.target_min, override.target_max
else:
tmin = rec.parameter_id.target_min
tmax = rec.parameter_id.target_max
rec.target_min = tmin
rec.target_max = tmax
@api.depends('value', 'target_min', 'target_max', 'parameter_id.warning_tolerance')
def _compute_status(self):
for rec in self:
if rec.target_min == 0.0 and rec.target_max == 0.0:
rec.status = 'ok'
continue
v, lo, hi = rec.value, rec.target_min, rec.target_max
if v < lo or v > hi:
rec.status = 'out_of_spec'
continue
tol_pct = (rec.parameter_id.warning_tolerance or 0.0) / 100.0
span = max(hi - lo, 1e-9)
if tol_pct > 0 and (v - lo < span * tol_pct or hi - v < span * tol_pct):
rec.status = 'warning'
else:
rec.status = 'ok'

View File

@@ -1,88 +0,0 @@
# -*- 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 FpBathParameter(models.Model):
"""Definition of a bath chemistry parameter.
Parameters are process-agnostic at the schema level (e.g. "Temperature",
"pH", "Nickel concentration"). Each process type references a set of
parameters via fusion.plating.process.type.parameter_ids. Actual target
ranges per bath are stored on fusion.plating.bath (per-bath overrides)
or on the bath recipe.
"""
_name = 'fusion.plating.bath.parameter'
_description = 'Fusion Plating — Bath Parameter'
_order = 'sequence, name'
name = fields.Char(
string='Parameter',
required=True,
translate=True,
help='Display name (e.g. "Nickel Concentration", "pH").',
)
code = fields.Char(
string='Code',
required=True,
help='Short code used in logs and exports (e.g. "Ni", "PH", "TEMP").',
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
parameter_type = fields.Selection(
[
('concentration', 'Concentration'),
('temperature', 'Temperature'),
('ph', 'pH'),
('conductivity', 'Conductivity'),
('turbidity', 'Turbidity'),
('ratio', 'Ratio'),
('count', 'Count / Age'),
('other', 'Other'),
],
string='Type',
required=True,
default='concentration',
)
uom = fields.Char(
string='Unit',
help='Display unit (e.g. "g/L", "°C", "pH", "MTO").',
)
target_min = fields.Float(
string='Default Target Min',
help='Default target minimum. Per-bath overrides are allowed.',
)
target_max = fields.Float(
string='Default Target Max',
help='Default target maximum. Per-bath overrides are allowed.',
)
warning_tolerance = fields.Float(
string='Warning Tolerance %',
default=10.0,
help='Distance from target limit at which a reading is flagged as warning.',
)
decimals = fields.Integer(
string='Decimals',
default=2,
)
description = fields.Text(
string='Description',
translate=True,
)
active = fields.Boolean(
string='Active',
default=True,
)
_sql_constraints = [
(
'fp_bath_parameter_code_uniq',
'unique(code)',
'Bath parameter code must be unique.',
),
]

View File

@@ -1,102 +0,0 @@
# -*- 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 FpFacility(models.Model):
"""A physical plating / finishing facility.
A company can operate 1..N facilities. Each facility has its own work
centers, tanks, operators, regulatory profile (ECA, sewer permit, waste
generator number), and capability footprint. Jobs are scheduled into
a facility based on capability matching.
Compliance add-on modules (fusion_plating_compliance_*) extend this
model with jurisdiction-specific fields via inheritance.
"""
_name = 'fusion.plating.facility'
_description = 'Fusion Plating — Facility'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'sequence, name'
name = fields.Char(
string='Facility',
required=True,
tracking=True,
)
code = fields.Char(
string='Code',
required=True,
tracking=True,
help='Short facility code used in job numbers and reports.',
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
default=lambda self: self.env.company,
)
partner_id = fields.Many2one(
'res.partner',
string='Address',
help='Partner holding the facility postal address and contact details.',
)
active = fields.Boolean(
string='Active',
default=True,
)
# ----- Capability -----------------------------------------------------
capability_ids = fields.Many2many(
'fusion.plating.process.type',
'fp_facility_capability_rel',
'facility_id',
'process_type_id',
string='Capabilities',
help='Process types this facility can perform.',
)
# ----- Child records --------------------------------------------------
work_center_ids = fields.One2many(
'fusion.plating.work.center',
'facility_id',
string='Work Centers',
)
tank_ids = fields.One2many(
'fusion.plating.tank',
'facility_id',
string='Tanks',
)
work_center_count = fields.Integer(
compute='_compute_counts',
)
tank_count = fields.Integer(
compute='_compute_counts',
)
capability_count = fields.Integer(
compute='_compute_counts',
)
_sql_constraints = [
(
'fp_facility_code_company_uniq',
'unique(code, company_id)',
'Facility code must be unique within a company.',
),
]
def _compute_counts(self):
for rec in self:
rec.work_center_count = len(rec.work_center_ids)
rec.tank_count = len(rec.tank_ids)
rec.capability_count = len(rec.capability_ids)
def name_get(self):
return [(rec.id, f'{rec.name} [{rec.code}]') for rec in self]

View File

@@ -1,62 +0,0 @@
# -*- 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 FpProcessCategory(models.Model):
"""High-level grouping of finishing process types.
Ships with a seed set (Plating, Anodizing, Coating, Conversion Coating,
Stripping, Other). Process packs reference these categories when they
load specific process types.
"""
_name = 'fusion.plating.process.category'
_description = 'Fusion Plating — Process Category'
_order = 'sequence, name'
name = fields.Char(
string='Category',
required=True,
translate=True,
)
code = fields.Char(
string='Code',
required=True,
help='Short identifier (e.g. "plating", "anodizing").',
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
description = fields.Text(
string='Description',
translate=True,
)
active = fields.Boolean(
string='Active',
default=True,
)
process_type_ids = fields.One2many(
'fusion.plating.process.type',
'category_id',
string='Process Types',
)
process_type_count = fields.Integer(
string='Process Types',
compute='_compute_process_type_count',
)
_sql_constraints = [
(
'fp_process_category_code_uniq',
'unique(code)',
'Process category code must be unique.',
),
]
def _compute_process_type_count(self):
for rec in self:
rec.process_type_count = len(rec.process_type_ids)

View File

@@ -1,401 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class FpProcessNode(models.Model):
"""A node in the process recipe tree.
Recipes are hierarchical templates that define how to plate a part.
They are reusable across production orders and serve as the single
source of truth for the shop's plating processes.
Node types
----------
* recipe — top-level root (e.g. "Electroless Nickel — Steel Line")
* sub_process — a group of operations (e.g. "Steel Line", "Cleaner")
* operation — a single production step (e.g. "Acid Dip", "Nickel Strike")
* step — a sub-step within an operation (e.g. "Ready for Blast", "Blast")
Hierarchy uses Odoo's _parent_store for efficient tree queries.
"""
_name = 'fusion.plating.process.node'
_description = 'Fusion Plating — Process Node'
_inherit = ['mail.thread', 'mail.activity.mixin']
_parent_store = True
_parent_name = 'parent_id'
_order = 'parent_path, sequence, id'
_rec_name = 'display_name'
# ---- Identity & hierarchy ------------------------------------------------
name = fields.Char(
string='Name',
required=True,
tracking=True,
)
code = fields.Char(
string='Code',
help='Optional short code (e.g. EN_STEEL).',
tracking=True,
)
node_type = fields.Selection(
[
('recipe', 'Recipe'),
('sub_process', 'Sub-Process'),
('operation', 'Operation'),
('step', 'Step'),
],
string='Type',
required=True,
default='operation',
tracking=True,
)
parent_id = fields.Many2one(
'fusion.plating.process.node',
string='Parent',
ondelete='cascade',
index=True,
)
parent_path = fields.Char(
index=True,
)
child_ids = fields.One2many(
'fusion.plating.process.node',
'parent_id',
string='Child Steps',
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
depth = fields.Integer(
string='Depth',
compute='_compute_depth',
store=True,
)
# ---- Process references --------------------------------------------------
process_type_id = fields.Many2one(
'fusion.plating.process.type',
string='Process Type',
ondelete='restrict',
tracking=True,
)
work_center_id = fields.Many2one(
'fusion.plating.work.center',
string='Work Centre',
ondelete='set null',
tracking=True,
)
# ---- Content & metadata --------------------------------------------------
description = fields.Html(
string='Description',
help='Rich text instructions for this step.',
)
notes = fields.Text(
string='Internal Notes',
help='Internal notes (not shown to customers).',
)
icon = fields.Selection(
[
('fa-flask', 'Flask / Chemistry'),
('fa-industry', 'Industry / Line'),
('fa-sitemap', 'Sitemap / Process'),
('fa-wrench', 'Wrench / Operation'),
('fa-cog', 'Gear / General'),
('fa-cogs', 'Gears / System'),
('fa-paint-brush', 'Paint / Masking'),
('fa-eraser', 'Eraser / De-Masking'),
('fa-th', 'Grid / Racking'),
('fa-fire', 'Fire / Bake'),
('fa-bolt', 'Bolt / Electric'),
('fa-diamond', 'Diamond / Plating'),
('fa-tint', 'Tint / Rinse'),
('fa-shower', 'Shower / Clean'),
('fa-bullseye', 'Target / Blast'),
('fa-search', 'Search / Inspect'),
('fa-check-circle', 'Check / Approve'),
('fa-clock-o', 'Clock / Wait'),
('fa-sun-o', 'Sun / Dry'),
('fa-thermometer-half', 'Temp / Heat'),
('fa-eye', 'Eye / Visual'),
('fa-hand-paper-o', 'Hand / Manual'),
('fa-cube', 'Cube / Part'),
('fa-shield', 'Shield / Protect'),
],
string='Icon',
default='fa-cog',
)
color = fields.Integer(
string='Colour',
default=0,
)
# ---- Timing --------------------------------------------------------------
estimated_duration = fields.Float(
string='Estimated Duration (min)',
help='Expected time in minutes.',
)
# ---- Behaviour flags -----------------------------------------------------
auto_complete = fields.Boolean(
string='Auto-Complete',
default=False,
help='Automatically marks done when all children complete.',
)
customer_visible = fields.Boolean(
string='Customer Visible',
default=True,
help='Whether to show this step name to customers.',
)
is_manual = fields.Boolean(
string='Manual Operation',
default=True,
help='Unchecked = automated (e.g. timed immersion).',
)
requires_signoff = fields.Boolean(
string='Requires Sign-Off',
default=False,
help='Quality hold point — requires operator sign-off.',
)
opt_in_out = fields.Selection(
[
('disabled', 'Disabled'),
('opt_in', 'Opt-In'),
('opt_out', 'Opt-Out'),
],
string='Opt In/Out',
default='disabled',
help='Controls whether this step is optional for a given job.',
tracking=True,
)
# ---- Lifecycle -----------------------------------------------------------
active = fields.Boolean(
string='Active',
default=True,
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
version = fields.Integer(
string='Version',
default=1,
tracking=True,
)
# ---- Computed fields -----------------------------------------------------
display_name = fields.Char(
compute='_compute_display_name',
store=True,
recursive=True,
)
child_count = fields.Integer(
string='Children',
compute='_compute_child_count',
)
recipe_root_id = fields.Many2one(
'fusion.plating.process.node',
string='Recipe Root',
compute='_compute_recipe_root_id',
store=True,
)
# ---- Operator inputs (one2many) ------------------------------------------
input_ids = fields.One2many(
'fusion.plating.process.node.input',
'node_id',
string='Operator Inputs',
)
# ---- SQL constraints -----------------------------------------------------
_sql_constraints = [
('fp_process_node_code_uniq',
'unique(code)',
'Recipe node code must be unique.'),
]
# ---- Computes ------------------------------------------------------------
@api.depends('name', 'code', 'parent_id.display_name')
def _compute_display_name(self):
for rec in self:
if rec.parent_id and rec.node_type != 'recipe':
rec.display_name = f'{rec.parent_id.display_name} / {rec.name}'
else:
rec.display_name = rec.name or ''
@api.depends('parent_path')
def _compute_depth(self):
for rec in self:
rec.depth = (rec.parent_path or '').count('/') - 1
@api.depends('child_ids')
def _compute_child_count(self):
for rec in self:
rec.child_count = len(rec.child_ids)
@api.depends('parent_path')
def _compute_recipe_root_id(self):
for rec in self:
if rec.parent_path:
root_id = int(rec.parent_path.split('/')[0])
rec.recipe_root_id = root_id
else:
rec.recipe_root_id = rec.id
# ---- Constraints ---------------------------------------------------------
@api.constrains('parent_id')
def _check_recursion_constraint(self):
if not self._check_recursion():
raise ValidationError(
_('A process node cannot be its own ancestor.'))
# ---- Tree data for OWL component -----------------------------------------
def get_tree_data(self):
"""Return full nested dict for the OWL recipe tree editor.
Called via the controller. Returns the tree rooted at `self`,
recursively including all descendants.
"""
self.ensure_one()
return self._node_to_dict()
def _node_to_dict(self, max_depth=10):
"""Recursively convert this node + children to a dict."""
if max_depth <= 0:
return None
children = []
for child in self.child_ids.sorted('sequence'):
child_dict = child._node_to_dict(max_depth=max_depth - 1)
if child_dict:
children.append(child_dict)
return {
'id': self.id,
'name': self.name or '',
'code': self.code or '',
'node_type': self.node_type,
'sequence': self.sequence,
'depth': self.depth,
'icon': self.icon or 'fa-cog',
'color': self.color,
'process_type': self.process_type_id.name if self.process_type_id else '',
'process_type_id': self.process_type_id.id if self.process_type_id else False,
'work_center': self.work_center_id.name if self.work_center_id else '',
'work_center_id': self.work_center_id.id if self.work_center_id else False,
'description': self.description or '',
'notes': self.notes or '',
'estimated_duration': self.estimated_duration,
'auto_complete': self.auto_complete,
'customer_visible': self.customer_visible,
'is_manual': self.is_manual,
'requires_signoff': self.requires_signoff,
'version': self.version,
'child_count': len(children),
'opt_in_out': self.opt_in_out or 'disabled',
'input_count': len(self.input_ids),
'create_date': self.create_date.isoformat() if self.create_date else '',
'create_uid_name': self.create_uid.name if self.create_uid else '',
'write_date': self.write_date.isoformat() if self.write_date else '',
'write_uid_name': self.write_uid.name if self.write_uid else '',
'children': children,
}
# ---- Actions -------------------------------------------------------------
def action_open_tree_editor(self):
"""Open the OWL recipe tree editor for this recipe."""
self.ensure_one()
root = self if self.node_type == 'recipe' else self.recipe_root_id
return {
'type': 'ir.actions.client',
'tag': 'fp_recipe_tree_editor',
'name': f'Recipe — {root.name}',
'context': {'recipe_id': root.id},
}
# ---- Copy (deep-duplicate) -----------------------------------------------
def copy(self, default=None):
"""Deep-copy: duplicates the node and all descendants."""
default = dict(default or {})
if self.node_type == 'recipe':
default.setdefault('name', _('%s (Copy)', self.name))
default.setdefault('code', f'{self.code}_copy' if self.code else False)
new_node = super().copy(default)
for child in self.child_ids.sorted('sequence'):
child.copy({'parent_id': new_node.id})
return new_node
class FpProcessNodeInput(models.Model):
"""An operator input definition attached to a process node.
These define what the operator needs to record when executing this
step — temperature readings, visual inspections, timing, etc.
"""
_name = 'fusion.plating.process.node.input'
_description = 'Fusion Plating — Process Node Input'
_order = 'sequence, id'
name = fields.Char(
string='Name',
required=True,
help='E.g. "Temperature Reading", "Visual Inspection".',
)
node_id = fields.Many2one(
'fusion.plating.process.node',
string='Process Node',
required=True,
ondelete='cascade',
)
input_type = fields.Selection(
[
('text', 'Text'),
('number', 'Number'),
('boolean', 'Yes / No'),
('selection', 'Selection'),
('photo', 'Photo'),
],
string='Input Type',
required=True,
default='text',
)
required = fields.Boolean(
string='Required',
default=False,
)
hint = fields.Char(
string='Hint',
help='Placeholder text shown to the operator.',
)
selection_options = fields.Text(
string='Options',
help='Comma-separated list of options (for Selection type).',
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
uom = fields.Char(
string='Unit',
help='Unit label (e.g. °C, min, psi).',
)

View File

@@ -1,92 +0,0 @@
# -*- 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 FpProcessType(models.Model):
"""Extensible finishing process taxonomy.
Core ships this model empty. Process packs (fusion_plating_process_en,
fusion_plating_process_chrome, etc.) load records via data XML with
noupdate so shops and customisations are preserved across upgrades.
Each process type has a category (plating / anodizing / conversion / etc.),
a reference to optional industry specs, and visual theming for the UI.
Chemistry parameter schemas are defined on fusion.plating.bath.parameter
and linked here via parameter_ids.
"""
_name = 'fusion.plating.process.type'
_description = 'Fusion Plating — Process Type'
_order = 'sequence, name'
name = fields.Char(
string='Process',
required=True,
translate=True,
help='Display name (e.g. "Electroless Nickel — Mid Phosphorus").',
)
code = fields.Char(
string='Code',
required=True,
help='Short unique code (e.g. "EN_MID", "HARD_CR", "ANO_II").',
)
category_id = fields.Many2one(
'fusion.plating.process.category',
string='Category',
required=True,
ondelete='restrict',
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
description = fields.Text(
string='Description',
translate=True,
)
active = fields.Boolean(
string='Active',
default=True,
)
# ----- Visual theming (kept neutral so it adapts to both light/dark) ----
# Uses Odoo's built-in kanban/list color index (0-11).
color = fields.Integer(
string='Color Index',
default=0,
help='Colour index used in kanban and list views.',
)
icon = fields.Char(
string='Icon',
help='Optional Font Awesome class (e.g. "fa-flask").',
default='fa-flask',
)
# ----- Chemistry & routing support ----------------------------------------
parameter_ids = fields.Many2many(
'fusion.plating.bath.parameter',
'fp_process_type_parameter_rel',
'process_type_id',
'parameter_id',
string='Bath Parameters',
help='Chemistry parameters tracked for baths running this process.',
)
hazard_notes = fields.Text(
string='Hazard Notes',
translate=True,
help='Process-level hazard awareness (e.g. Cr(VI) carcinogen, hypophosphite reducer).',
)
_sql_constraints = [
(
'fp_process_type_code_uniq',
'unique(code)',
'Process type code must be unique.',
),
]
def name_get(self):
return [(rec.id, f'{rec.name} [{rec.code}]') for rec in self]

View File

@@ -1,170 +0,0 @@
# -*- 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 FpTank(models.Model):
"""A physical vessel that holds a bath.
Tanks are long-lived assets. Baths come and go inside a tank. The
separation lets a shop dump an exhausted bath without losing the
tank's history, QR code, or equipment records.
Each tank carries a unique QR code for operator scanning at the
shop-floor station.
"""
_name = 'fusion.plating.tank'
_description = 'Fusion Plating — Tank'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'facility_id, work_center_id, sequence, code'
name = fields.Char(
string='Tank',
required=True,
tracking=True,
)
code = fields.Char(
string='Code',
required=True,
tracking=True,
help='Short unique tank identifier (e.g. "T-01", "EN-A1").',
)
qr_code = fields.Char(
string='QR Code',
help='Scannable identifier. Defaults to code, can be set to a longer URI.',
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
active = fields.Boolean(
string='Active',
default=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
required=True,
ondelete='restrict',
tracking=True,
)
work_center_id = fields.Many2one(
'fusion.plating.work.center',
string='Work Center',
domain="[('facility_id','=',facility_id)]",
ondelete='restrict',
tracking=True,
)
# ----- Physical properties --------------------------------------------
volume = fields.Float(
string='Volume',
help='Working volume.',
)
volume_uom = fields.Selection(
[
('l', 'Litres'),
('gal_us', 'US gallons'),
('gal_imp', 'Imperial gallons'),
('m3', 'Cubic metres'),
],
string='Volume Unit',
default='l',
)
material = fields.Selection(
[
('polypro', 'Polypropylene'),
('pvc', 'PVC'),
('pvdf', 'PVDF'),
('ss', 'Stainless Steel'),
('lined_steel', 'Lined Steel'),
('glass', 'Glass'),
('other', 'Other'),
],
string='Construction',
)
heating_type = fields.Selection(
[
('none', 'None'),
('immersion', 'Immersion Heater'),
('steam_coil', 'Steam Coil'),
('jacket', 'Jacketed'),
('external', 'External Heat Exchanger'),
],
string='Heating',
default='none',
)
has_filtration = fields.Boolean(
string='Has Filtration',
)
has_rectifier = fields.Boolean(
string='Has Rectifier',
help='Required for electrolytic processes (chrome, anodize, strike).',
)
# ----- State ----------------------------------------------------------
state = fields.Selection(
[
('empty', 'Empty'),
('filled', 'Filled'),
('in_use', 'In Use'),
('draining', 'Draining'),
('maintenance', 'Maintenance'),
('out_of_service', 'Out of Service'),
],
string='Status',
default='empty',
tracking=True,
)
# ----- Relations ------------------------------------------------------
bath_ids = fields.One2many(
'fusion.plating.bath',
'tank_id',
string='Bath History',
)
current_bath_id = fields.Many2one(
'fusion.plating.bath',
string='Current Bath',
compute='_compute_current_bath',
store=True,
)
current_process_id = fields.Many2one(
'fusion.plating.process.type',
string='Current Process',
related='current_bath_id.process_type_id',
store=True,
)
bath_count = fields.Integer(
compute='_compute_bath_count',
)
_sql_constraints = [
(
'fp_tank_code_facility_uniq',
'unique(code, facility_id)',
'Tank code must be unique within a facility.',
),
]
@api.depends('bath_ids', 'bath_ids.state')
def _compute_current_bath(self):
for rec in self:
active = rec.bath_ids.filtered(
lambda b: b.state in ('operational', 'under_review')
)
rec.current_bath_id = active[:1].id if active else False
def _compute_bath_count(self):
for rec in self:
rec.bath_count = len(rec.bath_ids)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('qr_code') and vals.get('code'):
vals['qr_code'] = f"FP-TANK:{vals['code']}"
return super().create(vals_list)

View File

@@ -1,72 +0,0 @@
# -*- 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 FpWorkCenter(models.Model):
"""A production line or station inside a facility.
Examples: "Line 1 - EN", "Anodize Line", "Prep Bay", "Bake Station",
"Inspection Booth", "Shipping Dock". Work centers group tanks and
provide scheduling capacity.
"""
_name = 'fusion.plating.work.center'
_description = 'Fusion Plating — Work Center'
_order = 'facility_id, sequence, name'
name = fields.Char(
string='Work Center',
required=True,
)
code = fields.Char(
string='Code',
required=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
required=True,
ondelete='cascade',
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
active = fields.Boolean(
string='Active',
default=True,
)
supported_process_ids = fields.Many2many(
'fusion.plating.process.type',
'fp_work_center_process_rel',
'work_center_id',
'process_type_id',
string='Supported Processes',
)
tank_ids = fields.One2many(
'fusion.plating.tank',
'work_center_id',
string='Tanks',
)
tank_count = fields.Integer(
compute='_compute_tank_count',
)
capacity_per_day = fields.Float(
string='Capacity / Day',
help='Theoretical throughput (parts, jobs, or square metres per day) — unit depends on shop.',
)
_sql_constraints = [
(
'fp_work_center_code_facility_uniq',
'unique(code, facility_id)',
'Work center code must be unique within a facility.',
),
]
def _compute_tank_count(self):
for rec in self:
rec.tank_count = len(rec.tank_ids)

View File

@@ -1,30 +0,0 @@
# -*- 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 ResCompany(models.Model):
_inherit = 'res.company'
# ----- Facility footprint for this legal entity ----------------------
x_fc_facility_ids = fields.One2many(
'fusion.plating.facility',
'company_id',
string='Plating Facilities',
)
x_fc_facility_count = fields.Integer(
string='# Facilities',
compute='_compute_x_fc_facility_count',
)
x_fc_default_facility_id = fields.Many2one(
'fusion.plating.facility',
string='Default Facility',
help='Facility used when the context does not specify one (single-site shops).',
)
def _compute_x_fc_facility_count(self):
for rec in self:
rec.x_fc_facility_count = len(rec.x_fc_facility_ids)

View File

@@ -1,85 +0,0 @@
<?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>
<!-- ================================================================== -->
<!-- MODULE CATEGORY -->
<!-- Odoo 19 organises privileges by ir.module.category. Without this, -->
<!-- groups fall into the generic Extra Rights list in user settings. -->
<!-- ================================================================== -->
<record id="module_category_fusion_plating" model="ir.module.category">
<field name="name">Fusion Plating</field>
<field name="sequence">46</field>
</record>
<!-- ================================================================== -->
<!-- PRIVILEGE (Odoo 19 res.groups.privilege) -->
<!-- Groups must reference this privilege_id so they render under a -->
<!-- "FUSION PLATING" section in user settings. -->
<!-- ================================================================== -->
<record id="res_groups_privilege_fusion_plating" model="res.groups.privilege">
<field name="name">Fusion Plating</field>
<field name="sequence">46</field>
<field name="category_id" ref="module_category_fusion_plating"/>
</record>
<!-- ================================================================== -->
<!-- OPERATOR (base shop-floor access) -->
<!-- Reads most reference data, writes chemistry logs. -->
<!-- ================================================================== -->
<record id="group_fusion_plating_operator" model="res.groups">
<field name="name">Operator</field>
<field name="sequence">10</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_plating"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<!-- ================================================================== -->
<!-- SUPERVISOR (line supervisor, team lead) -->
<!-- Can manage baths, schedule jobs, review logs. -->
<!-- ================================================================== -->
<record id="group_fusion_plating_supervisor" model="res.groups">
<field name="name">Supervisor</field>
<field name="sequence">20</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_plating"/>
<field name="implied_ids" eval="[(4, ref('group_fusion_plating_operator'))]"/>
</record>
<!-- ================================================================== -->
<!-- MANAGER (quality, EHS, plant manager, engineer) -->
<!-- Full CRUD on configuration objects. -->
<!-- ================================================================== -->
<record id="group_fusion_plating_manager" model="res.groups">
<field name="name">Manager</field>
<field name="sequence">30</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_plating"/>
<field name="implied_ids" eval="[(4, ref('group_fusion_plating_supervisor'))]"/>
</record>
<!-- ================================================================== -->
<!-- ADMINISTRATOR (owner, super-admin) -->
<!-- Everything a Manager can do, plus system-level settings. -->
<!-- ================================================================== -->
<record id="group_fusion_plating_admin" model="res.groups">
<field name="name">Administrator</field>
<field name="sequence">40</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_plating"/>
<field name="implied_ids" eval="[(4, ref('group_fusion_plating_manager'))]"/>
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
<!-- ================================================================== -->
<!-- RECORD RULE — Multi-company isolation on facilities -->
<!-- ================================================================== -->
<record id="fp_facility_company_rule" model="ir.rule">
<field name="name">Fusion Plating: Facility — multi-company</field>
<field name="model_id" ref="model_fusion_plating_facility"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
</odoo>

View File

@@ -1,34 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_fp_process_category_operator,fp.process.category.operator,model_fusion_plating_process_category,group_fusion_plating_operator,1,0,0,0
access_fp_process_category_manager,fp.process.category.manager,model_fusion_plating_process_category,group_fusion_plating_manager,1,1,1,1
access_fp_process_type_operator,fp.process.type.operator,model_fusion_plating_process_type,group_fusion_plating_operator,1,0,0,0
access_fp_process_type_manager,fp.process.type.manager,model_fusion_plating_process_type,group_fusion_plating_manager,1,1,1,1
access_fp_bath_parameter_operator,fp.bath.parameter.operator,model_fusion_plating_bath_parameter,group_fusion_plating_operator,1,0,0,0
access_fp_bath_parameter_manager,fp.bath.parameter.manager,model_fusion_plating_bath_parameter,group_fusion_plating_manager,1,1,1,1
access_fp_facility_operator,fp.facility.operator,model_fusion_plating_facility,group_fusion_plating_operator,1,0,0,0
access_fp_facility_supervisor,fp.facility.supervisor,model_fusion_plating_facility,group_fusion_plating_supervisor,1,0,0,0
access_fp_facility_manager,fp.facility.manager,model_fusion_plating_facility,group_fusion_plating_manager,1,1,1,1
access_fp_work_center_operator,fp.work.center.operator,model_fusion_plating_work_center,group_fusion_plating_operator,1,0,0,0
access_fp_work_center_supervisor,fp.work.center.supervisor,model_fusion_plating_work_center,group_fusion_plating_supervisor,1,1,0,0
access_fp_work_center_manager,fp.work.center.manager,model_fusion_plating_work_center,group_fusion_plating_manager,1,1,1,1
access_fp_tank_operator,fp.tank.operator,model_fusion_plating_tank,group_fusion_plating_operator,1,0,0,0
access_fp_tank_supervisor,fp.tank.supervisor,model_fusion_plating_tank,group_fusion_plating_supervisor,1,1,0,0
access_fp_tank_manager,fp.tank.manager,model_fusion_plating_tank,group_fusion_plating_manager,1,1,1,1
access_fp_bath_operator,fp.bath.operator,model_fusion_plating_bath,group_fusion_plating_operator,1,0,0,0
access_fp_bath_supervisor,fp.bath.supervisor,model_fusion_plating_bath,group_fusion_plating_supervisor,1,1,1,0
access_fp_bath_manager,fp.bath.manager,model_fusion_plating_bath,group_fusion_plating_manager,1,1,1,1
access_fp_bath_target_operator,fp.bath.target.operator,model_fusion_plating_bath_target,group_fusion_plating_operator,1,0,0,0
access_fp_bath_target_supervisor,fp.bath.target.supervisor,model_fusion_plating_bath_target,group_fusion_plating_supervisor,1,1,1,0
access_fp_bath_target_manager,fp.bath.target.manager,model_fusion_plating_bath_target,group_fusion_plating_manager,1,1,1,1
access_fp_bath_log_operator,fp.bath.log.operator,model_fusion_plating_bath_log,group_fusion_plating_operator,1,1,1,0
access_fp_bath_log_supervisor,fp.bath.log.supervisor,model_fusion_plating_bath_log,group_fusion_plating_supervisor,1,1,1,0
access_fp_bath_log_manager,fp.bath.log.manager,model_fusion_plating_bath_log,group_fusion_plating_manager,1,1,1,1
access_fp_bath_log_line_operator,fp.bath.log.line.operator,model_fusion_plating_bath_log_line,group_fusion_plating_operator,1,1,1,0
access_fp_bath_log_line_supervisor,fp.bath.log.line.supervisor,model_fusion_plating_bath_log_line,group_fusion_plating_supervisor,1,1,1,0
access_fp_bath_log_line_manager,fp.bath.log.line.manager,model_fusion_plating_bath_log_line,group_fusion_plating_manager,1,1,1,1
access_fp_process_node_operator,fp.process.node.operator,model_fusion_plating_process_node,group_fusion_plating_operator,1,0,0,0
access_fp_process_node_supervisor,fp.process.node.supervisor,model_fusion_plating_process_node,group_fusion_plating_supervisor,1,1,1,0
access_fp_process_node_manager,fp.process.node.manager,model_fusion_plating_process_node,group_fusion_plating_manager,1,1,1,1
access_fp_process_node_input_operator,fp.process.node.input.operator,model_fusion_plating_process_node_input,group_fusion_plating_operator,1,0,0,0
access_fp_process_node_input_supervisor,fp.process.node.input.supervisor,model_fusion_plating_process_node_input,group_fusion_plating_supervisor,1,1,1,0
access_fp_process_node_input_manager,fp.process.node.input.manager,model_fusion_plating_process_node_input,group_fusion_plating_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fp_process_category_operator fp.process.category.operator model_fusion_plating_process_category group_fusion_plating_operator 1 0 0 0
3 access_fp_process_category_manager fp.process.category.manager model_fusion_plating_process_category group_fusion_plating_manager 1 1 1 1
4 access_fp_process_type_operator fp.process.type.operator model_fusion_plating_process_type group_fusion_plating_operator 1 0 0 0
5 access_fp_process_type_manager fp.process.type.manager model_fusion_plating_process_type group_fusion_plating_manager 1 1 1 1
6 access_fp_bath_parameter_operator fp.bath.parameter.operator model_fusion_plating_bath_parameter group_fusion_plating_operator 1 0 0 0
7 access_fp_bath_parameter_manager fp.bath.parameter.manager model_fusion_plating_bath_parameter group_fusion_plating_manager 1 1 1 1
8 access_fp_facility_operator fp.facility.operator model_fusion_plating_facility group_fusion_plating_operator 1 0 0 0
9 access_fp_facility_supervisor fp.facility.supervisor model_fusion_plating_facility group_fusion_plating_supervisor 1 0 0 0
10 access_fp_facility_manager fp.facility.manager model_fusion_plating_facility group_fusion_plating_manager 1 1 1 1
11 access_fp_work_center_operator fp.work.center.operator model_fusion_plating_work_center group_fusion_plating_operator 1 0 0 0
12 access_fp_work_center_supervisor fp.work.center.supervisor model_fusion_plating_work_center group_fusion_plating_supervisor 1 1 0 0
13 access_fp_work_center_manager fp.work.center.manager model_fusion_plating_work_center group_fusion_plating_manager 1 1 1 1
14 access_fp_tank_operator fp.tank.operator model_fusion_plating_tank group_fusion_plating_operator 1 0 0 0
15 access_fp_tank_supervisor fp.tank.supervisor model_fusion_plating_tank group_fusion_plating_supervisor 1 1 0 0
16 access_fp_tank_manager fp.tank.manager model_fusion_plating_tank group_fusion_plating_manager 1 1 1 1
17 access_fp_bath_operator fp.bath.operator model_fusion_plating_bath group_fusion_plating_operator 1 0 0 0
18 access_fp_bath_supervisor fp.bath.supervisor model_fusion_plating_bath group_fusion_plating_supervisor 1 1 1 0
19 access_fp_bath_manager fp.bath.manager model_fusion_plating_bath group_fusion_plating_manager 1 1 1 1
20 access_fp_bath_target_operator fp.bath.target.operator model_fusion_plating_bath_target group_fusion_plating_operator 1 0 0 0
21 access_fp_bath_target_supervisor fp.bath.target.supervisor model_fusion_plating_bath_target group_fusion_plating_supervisor 1 1 1 0
22 access_fp_bath_target_manager fp.bath.target.manager model_fusion_plating_bath_target group_fusion_plating_manager 1 1 1 1
23 access_fp_bath_log_operator fp.bath.log.operator model_fusion_plating_bath_log group_fusion_plating_operator 1 1 1 0
24 access_fp_bath_log_supervisor fp.bath.log.supervisor model_fusion_plating_bath_log group_fusion_plating_supervisor 1 1 1 0
25 access_fp_bath_log_manager fp.bath.log.manager model_fusion_plating_bath_log group_fusion_plating_manager 1 1 1 1
26 access_fp_bath_log_line_operator fp.bath.log.line.operator model_fusion_plating_bath_log_line group_fusion_plating_operator 1 1 1 0
27 access_fp_bath_log_line_supervisor fp.bath.log.line.supervisor model_fusion_plating_bath_log_line group_fusion_plating_supervisor 1 1 1 0
28 access_fp_bath_log_line_manager fp.bath.log.line.manager model_fusion_plating_bath_log_line group_fusion_plating_manager 1 1 1 1
29 access_fp_process_node_operator fp.process.node.operator model_fusion_plating_process_node group_fusion_plating_operator 1 0 0 0
30 access_fp_process_node_supervisor fp.process.node.supervisor model_fusion_plating_process_node group_fusion_plating_supervisor 1 1 1 0
31 access_fp_process_node_manager fp.process.node.manager model_fusion_plating_process_node group_fusion_plating_manager 1 1 1 1
32 access_fp_process_node_input_operator fp.process.node.input.operator model_fusion_plating_process_node_input group_fusion_plating_operator 1 0 0 0
33 access_fp_process_node_input_supervisor fp.process.node.input.supervisor model_fusion_plating_process_node_input group_fusion_plating_supervisor 1 1 1 0
34 access_fp_process_node_input_manager fp.process.node.input.manager model_fusion_plating_process_node_input group_fusion_plating_manager 1 1 1 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,488 +0,0 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating — Recipe Tree Editor (OWL backend client action)
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
// Professional tree editor for process recipes. Renders the full
// node hierarchy with connector lines, expand/collapse, click-to-edit
// side panel, add/delete operations, and drag-and-drop reorder.
//
// Odoo 19 conventions:
// * Backend OWL: static template + static props = ["*"]
// * RPC: standalone rpc() from @web/core/network/rpc
// * Registered under registry.category("actions") → "fp_recipe_tree_editor"
// =============================================================================
import { Component, useState, useRef, onMounted, onWillUnmount } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
import { useService } from "@web/core/utils/hooks";
// ---- Node type metadata ---------------------------------------------------
const NODE_TYPES = {
recipe: { label: "Recipe", icon: "fa-flask", badgeClass: "o_fp_recipe_badge_recipe" },
sub_process: { label: "Sub-Process", icon: "fa-sitemap", badgeClass: "o_fp_recipe_badge_sub" },
operation: { label: "Operation", icon: "fa-wrench", badgeClass: "o_fp_recipe_badge_op" },
step: { label: "Step", icon: "fa-dot-circle-o", badgeClass: "o_fp_recipe_badge_step" },
};
const NODE_TYPE_OPTIONS = [
{ value: "sub_process", label: "Sub-Process" },
{ value: "operation", label: "Operation" },
{ value: "step", label: "Step" },
];
// ---- Icon picker options (curated for plating / manufacturing) -----------
const ICON_OPTIONS = [
{ value: "fa-flask", label: "Flask / Chemistry" },
{ value: "fa-industry", label: "Industry / Line" },
{ value: "fa-sitemap", label: "Sitemap / Process" },
{ value: "fa-wrench", label: "Wrench / Operation" },
{ value: "fa-cog", label: "Gear / General" },
{ value: "fa-cogs", label: "Gears / System" },
{ value: "fa-paint-brush", label: "Paint / Masking" },
{ value: "fa-eraser", label: "Eraser / De-Masking" },
{ value: "fa-th", label: "Grid / Racking" },
{ value: "fa-fire", label: "Fire / Bake" },
{ value: "fa-bolt", label: "Bolt / Electric" },
{ value: "fa-diamond", label: "Diamond / Plating" },
{ value: "fa-tint", label: "Tint / Rinse" },
{ value: "fa-shower", label: "Shower / Clean" },
{ value: "fa-bullseye", label: "Target / Blast" },
{ value: "fa-search", label: "Search / Inspect" },
{ value: "fa-check-circle", label: "Check / Approve" },
{ value: "fa-clock-o", label: "Clock / Wait" },
{ value: "fa-sun-o", label: "Sun / Dry" },
{ value: "fa-thermometer-half", label: "Temp / Heat" },
{ value: "fa-eye", label: "Eye / Visual" },
{ value: "fa-hand-paper-o", label: "Hand / Manual" },
{ value: "fa-cube", label: "Cube / Part" },
{ value: "fa-shield", label: "Shield / Protect" },
];
// ---- Auto-icon: guess the best icon from the node name ------------------
const ICON_KEYWORDS = [
{ pattern: /mask/i, icon: "fa-paint-brush" },
{ pattern: /de-?mask|unmask/i, icon: "fa-eraser" },
{ pattern: /rack/i, icon: "fa-th" },
{ pattern: /de-?rack|unrack/i, icon: "fa-th" },
{ pattern: /blast/i, icon: "fa-bullseye" },
{ pattern: /bake|oven/i, icon: "fa-fire" },
{ pattern: /clean|soak|wash/i, icon: "fa-shower" },
{ pattern: /rinse/i, icon: "fa-tint" },
{ pattern: /dry/i, icon: "fa-sun-o" },
{ pattern: /nickel|plate|plat/i, icon: "fa-diamond" },
{ pattern: /strike|electro/i, icon: "fa-bolt" },
{ pattern: /acid|dip|etch/i, icon: "fa-flask" },
{ pattern: /inspect|check|test/i, icon: "fa-search" },
{ pattern: /ready|wait|queue/i, icon: "fa-clock-o" },
{ pattern: /line|process/i, icon: "fa-industry" },
{ pattern: /heat|temp/i, icon: "fa-thermometer-half" },
{ pattern: /porosity/i, icon: "fa-tint" },
];
function guessIcon(name) {
if (!name) return "fa-cog";
for (const rule of ICON_KEYWORDS) {
if (rule.pattern.test(name)) return rule.icon;
}
return "fa-cog";
}
export class RecipeTreeEditor extends Component {
static template = "fusion_plating.RecipeTreeEditor";
static props = ["*"];
setup() {
this.notification = useService("notification");
this.action = useService("action");
this.dialog = useService("dialog");
this.state = useState({
recipe: null,
tree: null,
loading: false,
saving: false,
selectedNodeId: null,
selectedNode: null,
expandedNodes: {},
showPanel: false,
// Add-node form
addingTo: null, // parent node id when "add" dialog is open
newNodeName: "",
newNodeType: "operation",
});
this._recipeId = null;
onMounted(async () => {
const ctx = this.props.action?.context || {};
this._recipeId = ctx.recipe_id || null;
if (this._recipeId) {
await this.loadTree();
}
});
}
// ---- Data loading -------------------------------------------------------
async loadTree() {
this.state.loading = true;
try {
const result = await rpc("/fp/recipe/tree", {
recipe_id: this._recipeId,
});
if (result && result.ok) {
this.state.recipe = result.recipe;
this.state.tree = result.tree;
// Auto-expand root node
if (result.tree) {
this.state.expandedNodes[result.tree.id] = true;
}
// Refresh selected node data if panel is open
if (this.state.selectedNodeId) {
this.state.selectedNode = this._findNode(
result.tree, this.state.selectedNodeId
);
}
} else {
this.notification.add(
result?.error || "Failed to load recipe.",
{ type: "danger" }
);
}
} catch (err) {
this.notification.add(`Load failed: ${err.message || err}`, { type: "danger" });
} finally {
this.state.loading = false;
}
}
// ---- Tree traversal helpers ---------------------------------------------
_findNode(node, id) {
if (!node) return null;
if (node.id === id) return node;
for (const child of (node.children || [])) {
const found = this._findNode(child, id);
if (found) return found;
}
return null;
}
// ---- Expand / collapse --------------------------------------------------
isExpanded(nodeId) {
return !!this.state.expandedNodes[nodeId];
}
toggleExpand(nodeId) {
this.state.expandedNodes[nodeId] = !this.state.expandedNodes[nodeId];
}
// ---- Node selection (side panel) ----------------------------------------
selectNode(node) {
if (this.state.selectedNodeId === node.id) {
// Toggle panel off
this.state.selectedNodeId = null;
this.state.selectedNode = null;
this.state.showPanel = false;
} else {
this.state.selectedNodeId = node.id;
this.state.selectedNode = { ...node };
this.state.showPanel = true;
}
}
closePanel() {
this.state.selectedNodeId = null;
this.state.selectedNode = null;
this.state.showPanel = false;
}
// ---- Node editing (panel save) ------------------------------------------
async saveNode() {
const node = this.state.selectedNode;
if (!node) return;
this.state.saving = true;
try {
const vals = {
name: node.name,
icon: node.icon,
node_type: node.node_type,
estimated_duration: node.estimated_duration || 0,
auto_complete: node.auto_complete,
customer_visible: node.customer_visible,
is_manual: node.is_manual,
requires_signoff: node.requires_signoff,
};
const result = await rpc("/fp/recipe/node/write", {
node_id: node.id,
vals,
});
if (result && result.ok) {
this.notification.add("Saved", { type: "success" });
await this.loadTree();
} else {
this.notification.add(result?.error || "Save failed.", { type: "warning" });
}
} catch (err) {
this.notification.add(`Save failed: ${err.message || err}`, { type: "danger" });
} finally {
this.state.saving = false;
}
}
// ---- Add child node -----------------------------------------------------
startAddChild(parentId) {
this.state.addingTo = parentId;
this.state.newNodeName = "";
this.state.newNodeType = "operation";
// Auto-expand parent
this.state.expandedNodes[parentId] = true;
}
cancelAdd() {
this.state.addingTo = null;
}
async confirmAdd() {
const name = (this.state.newNodeName || "").trim();
if (!name) {
this.notification.add("Name is required.", { type: "warning" });
return;
}
this.state.saving = true;
try {
const result = await rpc("/fp/recipe/node/create", {
parent_id: this.state.addingTo,
name: name,
node_type: this.state.newNodeType,
vals: { icon: guessIcon(name) },
});
if (result && result.ok) {
this.notification.add(`Added "${name}"`, { type: "success" });
this.state.addingTo = null;
await this.loadTree();
} else {
this.notification.add(result?.error || "Add failed.", { type: "warning" });
}
} catch (err) {
this.notification.add(`Add failed: ${err.message || err}`, { type: "danger" });
} finally {
this.state.saving = false;
}
}
onAddNameKey(ev) {
if (ev.key === "Enter") {
this.confirmAdd();
} else if (ev.key === "Escape") {
this.cancelAdd();
}
}
// ---- Delete node --------------------------------------------------------
async deleteNode(nodeId) {
const node = this._findNode(this.state.tree, nodeId);
if (!node) return;
if (node.node_type === "recipe") {
this.notification.add("Cannot delete the recipe root.", { type: "warning" });
return;
}
const childWarning = node.child_count > 0
? ` and its ${node.child_count} child step(s)`
: "";
if (!confirm(`Delete "${node.name}"${childWarning}?`)) {
return;
}
try {
const result = await rpc("/fp/recipe/node/unlink", { node_id: nodeId });
if (result && result.ok) {
this.notification.add(`Deleted "${node.name}"`, { type: "success" });
if (this.state.selectedNodeId === nodeId) {
this.closePanel();
}
await this.loadTree();
} else {
this.notification.add(result?.error || "Delete failed.", { type: "warning" });
}
} catch (err) {
this.notification.add(`Delete failed: ${err.message || err}`, { type: "danger" });
}
}
// ---- Drag & drop reorder ------------------------------------------------
onNodeDragStart(node, parentNode, ev) {
if (node.node_type === "recipe") {
ev.preventDefault();
return;
}
this._draggedNode = {
id: node.id,
parentId: parentNode ? parentNode.id : null,
};
ev.dataTransfer.effectAllowed = "move";
ev.dataTransfer.setData("text/plain", String(node.id));
requestAnimationFrame(() => {
ev.target.classList.add("o_fp_recipe_drag_ghost");
});
}
onNodeDragEnd(ev) {
this._draggedNode = null;
ev.target.classList.remove("o_fp_recipe_drag_ghost");
document.querySelectorAll(".o_fp_recipe_drop_target").forEach(el => {
el.classList.remove("o_fp_recipe_drop_target");
});
}
onNodeDragOver(node, ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
ev.currentTarget.classList.add("o_fp_recipe_drop_target");
}
onNodeDragLeave(ev) {
if (!ev.currentTarget.contains(ev.relatedTarget)) {
ev.currentTarget.classList.remove("o_fp_recipe_drop_target");
}
}
async onNodeDrop(targetNode, parentNode, ev) {
ev.preventDefault();
ev.currentTarget.classList.remove("o_fp_recipe_drop_target");
const dragged = this._draggedNode;
if (!dragged || dragged.id === targetNode.id) return;
// If dropping on a node with children, move into it
// If dropping on a sibling, reorder within parent
const targetParentId = parentNode ? parentNode.id : null;
if (dragged.parentId === targetParentId) {
// Reorder within same parent — swap positions
const siblings = parentNode
? (parentNode.children || [])
: [this.state.tree];
const ids = siblings.map(c => c.id);
const fromIdx = ids.indexOf(dragged.id);
const toIdx = ids.indexOf(targetNode.id);
if (fromIdx === -1 || toIdx === -1) return;
ids.splice(fromIdx, 1);
ids.splice(toIdx, 0, dragged.id);
try {
const result = await rpc("/fp/recipe/node/reorder", { node_ids: ids });
if (result && result.ok) {
await this.loadTree();
}
} catch (err) {
this.notification.add(`Reorder failed: ${err.message}`, { type: "danger" });
}
} else {
// Move to new parent
try {
const result = await rpc("/fp/recipe/node/move", {
node_id: dragged.id,
new_parent_id: targetNode.id,
});
if (result && result.ok) {
this.state.expandedNodes[targetNode.id] = true;
await this.loadTree();
} else {
this.notification.add(result?.error || "Move failed.", { type: "warning" });
}
} catch (err) {
this.notification.add(`Move failed: ${err.message}`, { type: "danger" });
}
}
this._draggedNode = null;
}
// ---- Navigation ---------------------------------------------------------
onBackToList() {
this.action.doAction("fusion_plating.action_fp_process_recipe");
}
onOpenForm(nodeId) {
this.action.doAction({
type: "ir.actions.act_window",
res_model: "fusion.plating.process.node",
res_id: nodeId,
views: [[false, "form"]],
target: "current",
});
}
async onDuplicate() {
if (!this._recipeId) return;
try {
const result = await rpc("/fp/recipe/duplicate", {
recipe_id: this._recipeId,
});
if (result && result.ok) {
this.notification.add("Recipe duplicated.", { type: "success" });
this._recipeId = result.recipe_id;
await this.loadTree();
} else {
this.notification.add(result?.error || "Duplicate failed.", { type: "warning" });
}
} catch (err) {
this.notification.add(`Duplicate failed: ${err.message}`, { type: "danger" });
}
}
// ---- Helpers ------------------------------------------------------------
getNodeTypeMeta(type) {
return NODE_TYPES[type] || NODE_TYPES.operation;
}
getNodeTypeOptions() {
return NODE_TYPE_OPTIONS;
}
getIconOptions() {
return ICON_OPTIONS;
}
formatTimeAgo(isoStr) {
if (!isoStr) return "";
const date = new Date(isoStr);
const now = new Date();
let diff = Math.floor((now - date) / 1000); // seconds
if (diff < 0) diff = 0;
const parts = [];
const weeks = Math.floor(diff / 604800);
diff %= 604800;
const days = Math.floor(diff / 86400);
diff %= 86400;
const hours = Math.floor(diff / 3600);
diff %= 3600;
const minutes = Math.floor(diff / 60);
const seconds = diff % 60;
if (weeks) parts.push(`${weeks}w`);
if (days) parts.push(`${days}d`);
if (hours) parts.push(`${hours}h`);
if (minutes) parts.push(`${minutes}m`);
parts.push(`${seconds}s`);
return parts.join(" ") + " ago";
}
formatDuration(minutes) {
if (!minutes) return "";
if (minutes < 60) return `${Math.round(minutes)}m`;
const h = Math.floor(minutes / 60);
const m = Math.round(minutes % 60);
return m > 0 ? `${h}h ${m}m` : `${h}h`;
}
}
registry.category("actions").add("fp_recipe_tree_editor", RecipeTreeEditor);

View File

@@ -1,173 +0,0 @@
// =============================================================================
// Fusion Plating — backend styles
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
// THEME AWARENESS
// ---------------
// This file NEVER hardcodes backgrounds or text colours. All surface colours
// come from Odoo / Bootstrap CSS custom properties so the component renders
// correctly in BOTH light and dark mode without any duplication:
//
// background: var(--bs-body-bg) // main surface
// surface: var(--o-view-background-color) // view canvas
// foreground: var(--bs-body-color) // main text
// muted text: var(--bs-secondary-color)
// border: var(--bs-border-color)
// primary: var(--o-action) // Odoo action/brand
//
// Semantic status colours (green / amber / red) use `color-mix()` against the
// Bootstrap theme token so a green badge is darker on light mode and brighter
// on dark mode automatically — one rule, two looks.
//
// We never target `.o_dark`, `html.dark`, or `@media (prefers-color-scheme)`
// to override colours. If you find yourself needing that, it's a smell — use
// a variable instead.
// =============================================================================
// -----------------------------------------------------------------------------
// Local helpers
// -----------------------------------------------------------------------------
// `color-mix()` lets us tint a semantic colour against the surface, so the
// result adapts to light or dark backgrounds automatically.
@mixin fp-tint($color-var, $amount: 12%) {
background-color: color-mix(in srgb, var(#{$color-var}) #{$amount}, transparent);
color: var(#{$color-var});
border: 1px solid color-mix(in srgb, var(#{$color-var}) 35%, transparent);
}
// -----------------------------------------------------------------------------
// Generic card surface used in kanban views (facility, tank, bath)
// -----------------------------------------------------------------------------
.o_fp_card {
background-color: var(--o-view-background-color, var(--bs-body-bg));
color: var(--bs-body-color);
border: 1px solid var(--bs-border-color);
border-radius: 10px;
padding: 12px 14px;
transition: border-color 120ms ease, box-shadow 120ms ease;
&:hover {
border-color: color-mix(in srgb, var(--o-action) 50%, var(--bs-border-color));
box-shadow: 0 2px 8px color-mix(in srgb, var(--bs-body-color) 8%, transparent);
}
.o_fp_card_title {
color: var(--bs-body-color);
font-size: 1rem;
line-height: 1.2;
}
.o_fp_card_stats {
color: var(--bs-body-color);
.text-muted,
.text-muted * {
color: var(--bs-secondary-color) !important;
}
}
}
// -----------------------------------------------------------------------------
// Tank kanban — state badge theming
// -----------------------------------------------------------------------------
.o_fp_tank_kanban {
.o_fp_tank_card {
// Let the left-border carry the state — subtle, theme-aware.
border-left-width: 4px;
&[data-state="empty"],
&[data-state="out_of_service"] {
border-left-color: var(--bs-secondary-color);
}
&[data-state="filled"] {
border-left-color: var(--bs-info, var(--o-action));
}
&[data-state="in_use"] {
border-left-color: var(--bs-success);
}
&[data-state="draining"],
&[data-state="maintenance"] {
border-left-color: var(--bs-warning);
}
}
.o_fp_badge {
padding: 2px 8px;
font-size: 0.72rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.02em;
border-radius: 999px;
&[data-state="empty"],
&[data-state="out_of_service"] {
@include fp-tint(--bs-secondary-color);
}
&[data-state="filled"] {
@include fp-tint(--bs-info);
}
&[data-state="in_use"] {
@include fp-tint(--bs-success);
}
&[data-state="draining"],
&[data-state="maintenance"] {
@include fp-tint(--bs-warning);
}
}
}
// -----------------------------------------------------------------------------
// Bath kanban — chemistry health dot
// -----------------------------------------------------------------------------
.o_fp_bath_kanban {
.o_fp_bath_card {
// A single left-border tint conveys chemistry health without colouring
// the entire card.
border-left-width: 4px;
border-left-color: var(--bs-success);
&[data-log-status="warning"] {
border-left-color: var(--bs-warning);
}
&[data-log-status="out_of_spec"] {
border-left-color: var(--bs-danger);
}
}
.o_fp_health_dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--bs-success);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-success) 25%, transparent);
&[data-status="warning"] {
background-color: var(--bs-warning);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-warning) 25%, transparent);
}
&[data-status="out_of_spec"] {
background-color: var(--bs-danger);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-danger) 25%, transparent);
}
}
}
// -----------------------------------------------------------------------------
// Facility kanban — stat strip spacing
// -----------------------------------------------------------------------------
.o_fp_facility_kanban {
.o_fp_card_stats {
padding-top: 8px;
border-top: 1px dashed var(--bs-border-color);
}
}

View File

@@ -1,433 +0,0 @@
// =============================================================================
// Fusion Plating — Recipe Tree Editor
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
// THEME AWARENESS
// ---------------
// All colours from CSS custom properties + SCSS $border-color.
// Works in both light and dark mode.
// =============================================================================
// ---- Root container ---------------------------------------------------------
.o_fp_recipe_editor {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
background: var(--o-view-background-color, var(--bs-body-bg));
}
// ---- Header -----------------------------------------------------------------
.o_fp_recipe_header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 12px 20px;
background: var(--bs-body-bg);
border-bottom: 1px solid $border-color;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
.o_fp_recipe_header_left {
display: flex;
align-items: center;
gap: 12px;
}
.o_fp_recipe_back_btn {
text-decoration: none;
font-weight: 500;
}
.o_fp_recipe_title {
margin: 0;
font-size: 1.2rem;
font-weight: 700;
color: var(--bs-body-color);
}
.o_fp_recipe_version_badge {
background: var(--bs-secondary-color);
color: #fff;
font-size: 0.7rem;
vertical-align: middle;
}
.o_fp_recipe_header_right {
display: flex;
align-items: center;
}
}
// ---- Body (tree + panel) layout ---------------------------------------------
.o_fp_recipe_body {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
.o_fp_recipe_tree_area {
flex: 1;
overflow-y: auto;
padding: 24px 24px 24px 40px;
}
// ---- Side panel -------------------------------------------------------------
.o_fp_recipe_panel {
width: 0;
overflow: hidden;
transition: width 0.2s ease;
border-left: 1px solid $border-color;
background: var(--bs-body-bg);
&.o_fp_recipe_panel_open {
width: 340px;
overflow-y: auto;
}
.o_fp_recipe_panel_header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid $border-color;
h5 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--bs-body-color);
}
}
.o_fp_recipe_panel_body {
padding: 16px;
}
}
// ---- Connector lines --------------------------------------------------------
.o_fp_recipe_connector {
width: 3px;
height: 16px;
background: $border-color;
margin-left: 22px;
border-radius: 2px;
}
// ---- Node card --------------------------------------------------------------
.o_fp_recipe_node {
position: relative;
border-width: 1px;
border-style: solid;
border-color: $border-color;
border-radius: 8px;
padding: 10px 14px;
max-width: 520px;
cursor: pointer;
background: var(--bs-body-bg);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
transition: box-shadow 0.15s, border-color 0.15s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
border-color: var(--o-action, var(--bs-primary));
}
&.o_fp_recipe_node_selected {
border-color: var(--o-action, var(--bs-primary));
box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb, 13, 110, 253), 0.2);
}
// Node type left accent
&.o_fp_recipe_node_recipe {
border-left: 5px solid var(--bs-primary);
}
&.o_fp_recipe_node_sub_process {
border-left: 5px solid var(--bs-info);
}
&.o_fp_recipe_node_operation {
border-left: 5px solid var(--bs-success);
}
&.o_fp_recipe_node_step {
border-left: 5px solid var(--bs-secondary);
}
// Drag states
&.o_fp_recipe_drag_ghost {
opacity: 0.35;
border-style: dashed;
}
&.o_fp_recipe_drop_target {
border-color: var(--o-action, var(--bs-primary));
background: color-mix(in srgb, var(--o-action, var(--bs-primary)) 6%, var(--bs-body-bg));
}
}
// ---- Drag handle ------------------------------------------------------------
.o_fp_recipe_drag_handle {
position: absolute;
left: -20px;
top: 50%;
transform: translateY(-50%);
color: var(--bs-secondary-color);
cursor: grab;
opacity: 0;
transition: opacity 0.15s;
font-size: 0.85rem;
.o_fp_recipe_node:hover & {
opacity: 0.6;
}
}
// ---- Node header row --------------------------------------------------------
.o_fp_recipe_node_header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.o_fp_recipe_toggle_btn {
background: none;
border: none;
color: var(--bs-secondary-color);
cursor: pointer;
width: 20px;
text-align: center;
padding: 0;
font-size: 0.75rem;
&:hover {
color: var(--bs-body-color);
}
}
.o_fp_recipe_toggle_spacer {
width: 20px;
flex-shrink: 0;
}
.o_fp_recipe_node_icon {
color: var(--bs-secondary-color);
font-size: 0.9rem;
width: 18px;
text-align: center;
flex-shrink: 0;
}
.o_fp_recipe_node_name {
font-weight: 600;
font-size: 0.9rem;
color: var(--bs-body-color);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.o_fp_recipe_node_badge {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
padding: 2px 8px;
border-radius: 4px;
flex-shrink: 0;
&.o_fp_recipe_badge_recipe {
background: var(--bs-primary);
color: #fff;
}
&.o_fp_recipe_badge_sub {
background: var(--bs-info);
color: #fff;
}
&.o_fp_recipe_badge_op {
background: var(--bs-success);
color: #fff;
}
&.o_fp_recipe_badge_step {
background: var(--bs-secondary);
color: #fff;
}
}
// ---- Node meta row ----------------------------------------------------------
.o_fp_recipe_node_meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 0.78rem;
color: var(--bs-secondary-color);
padding-left: 28px;
margin-bottom: 2px;
}
.o_fp_recipe_node_wc,
.o_fp_recipe_node_duration {
display: inline-flex;
align-items: center;
}
.o_fp_recipe_node_icons {
display: inline-flex;
gap: 6px;
font-size: 0.75rem;
color: var(--bs-secondary-color);
i {
opacity: 0.7;
}
}
// ---- Node action buttons ----------------------------------------------------
.o_fp_recipe_node_actions {
display: flex;
gap: 4px;
padding-left: 28px;
margin-top: 4px;
opacity: 0;
transition: opacity 0.15s;
.o_fp_recipe_node:hover & {
opacity: 1;
}
.o_fp_recipe_add_btn {
font-size: 0.72rem;
color: var(--bs-success);
border: 1px solid var(--bs-success);
padding: 1px 8px;
border-radius: 4px;
background: transparent;
&:hover {
background: var(--bs-success);
color: #fff;
}
}
.o_fp_recipe_delete_btn {
font-size: 0.72rem;
color: var(--bs-danger);
border: 1px solid transparent;
padding: 1px 6px;
border-radius: 4px;
background: transparent;
&:hover {
border-color: var(--bs-danger);
}
}
}
// ---- Add child form ---------------------------------------------------------
.o_fp_recipe_add_form {
padding-left: 28px;
}
.o_fp_recipe_add_card {
border: 1px dashed var(--bs-success);
border-radius: 8px;
padding: 10px 14px;
max-width: 520px;
background: color-mix(in srgb, var(--bs-success) 4%, var(--bs-body-bg));
}
// ---- Children container (indentation) ---------------------------------------
.o_fp_recipe_children {
margin-left: 32px;
padding-top: 0;
position: relative;
// Vertical guide line
&::before {
content: '';
position: absolute;
left: 22px;
top: 0;
bottom: 16px;
width: 2px;
background: $border-color;
border-radius: 1px;
opacity: 0.5;
}
}
// ---- Tracking section -------------------------------------------------------
.o_fp_recipe_tracking {
border-top: 1px solid $border-color;
}
// ---- Icon picker ------------------------------------------------------------
.o_fp_recipe_icon_picker {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.o_fp_recipe_icon_btn {
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid $border-color;
border-radius: 6px;
background: transparent;
color: var(--bs-secondary-color);
font-size: 0.9rem;
cursor: pointer;
transition: border-color 0.12s, background-color 0.12s;
&:hover {
border-color: var(--o-action, var(--bs-primary));
color: var(--bs-body-color);
}
&.active {
background: var(--o-action, var(--bs-primary));
border-color: var(--o-action, var(--bs-primary));
color: #fff;
}
}
// ---- Responsive -------------------------------------------------------------
@media (max-width: 768px) {
.o_fp_recipe_tree_area {
padding: 16px 12px 16px 24px;
}
.o_fp_recipe_node {
max-width: 100%;
}
.o_fp_recipe_panel.o_fp_recipe_panel_open {
width: 280px;
}
.o_fp_recipe_children {
margin-left: 20px;
}
}

View File

@@ -1,334 +0,0 @@
<?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.
-->
<templates xml:space="preserve">
<t t-name="fusion_plating.RecipeTreeEditor">
<div class="o_fp_recipe_editor">
<!-- ========== HEADER ========== -->
<div class="o_fp_recipe_header">
<div class="o_fp_recipe_header_left">
<button class="btn btn-link o_fp_recipe_back_btn"
t-on-click="onBackToList" title="Back to list">
<i class="fa fa-arrow-left me-1"/> Recipes
</button>
<h2 class="o_fp_recipe_title" t-if="state.recipe">
<i class="fa fa-flask me-2"/>
<t t-esc="state.recipe.name"/>
<span class="badge rounded-pill o_fp_recipe_version_badge ms-2"
t-if="state.recipe.version">
v<t t-esc="state.recipe.version"/>
</span>
</h2>
</div>
<div class="o_fp_recipe_header_right" t-if="state.recipe">
<span class="text-muted small me-3" t-if="state.recipe.process_type">
<i class="fa fa-tag me-1"/>
<t t-esc="state.recipe.process_type"/>
</span>
<button class="btn btn-sm btn-outline-secondary me-1"
t-on-click="onDuplicate" title="Duplicate recipe">
<i class="fa fa-copy me-1"/> Duplicate
</button>
<button class="btn btn-sm btn-outline-primary"
t-on-click="() => this.onOpenForm(state.recipe.id)"
title="Edit in form view">
<i class="fa fa-pencil me-1"/> Form View
</button>
</div>
</div>
<!-- ========== LOADING ========== -->
<div class="text-center py-5" t-if="state.loading and !state.tree">
<i class="fa fa-spinner fa-spin fa-2x"/>
<p class="mt-2 text-muted">Loading recipe tree...</p>
</div>
<!-- ========== NO RECIPE ========== -->
<div class="text-center py-5" t-if="!state.loading and !_recipeId">
<i class="fa fa-exclamation-triangle fa-3x text-muted"/>
<p class="mt-3 text-muted">No recipe selected.</p>
</div>
<!-- ========== TREE + PANEL LAYOUT ========== -->
<div class="o_fp_recipe_body" t-if="state.tree">
<!-- Tree area -->
<div class="o_fp_recipe_tree_area">
<t t-call="fusion_plating.RecipeTreeNode">
<t t-set="node" t-value="state.tree"/>
<t t-set="parentNode" t-value="null"/>
<t t-set="isFirst" t-value="true"/>
</t>
</div>
<!-- Side panel -->
<div t-att-class="'o_fp_recipe_panel' + (state.showPanel ? ' o_fp_recipe_panel_open' : '')">
<t t-if="state.showPanel and state.selectedNode">
<div class="o_fp_recipe_panel_header">
<h5>
<i t-att-class="'fa ' + (state.selectedNode.icon || 'fa-cog') + ' me-2'"/>
Edit Node
</h5>
<button class="btn btn-sm btn-link" t-on-click="closePanel">
<i class="fa fa-times"/>
</button>
</div>
<div class="o_fp_recipe_panel_body">
<div class="mb-3">
<label class="form-label fw-bold">Name</label>
<input type="text" class="form-control"
t-att-value="state.selectedNode.name"
t-on-change="(ev) => { state.selectedNode.name = ev.target.value; }"/>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Type</label>
<select class="form-select"
t-on-change="(ev) => { state.selectedNode.node_type = ev.target.value; }">
<option value="recipe"
t-att-selected="state.selectedNode.node_type === 'recipe'">Recipe</option>
<option value="sub_process"
t-att-selected="state.selectedNode.node_type === 'sub_process'">Sub-Process</option>
<option value="operation"
t-att-selected="state.selectedNode.node_type === 'operation'">Operation</option>
<option value="step"
t-att-selected="state.selectedNode.node_type === 'step'">Step</option>
</select>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Icon</label>
<div class="o_fp_recipe_icon_picker">
<t t-foreach="getIconOptions()" t-as="ic" t-key="ic.value">
<button t-att-class="'o_fp_recipe_icon_btn' + (state.selectedNode.icon === ic.value ? ' active' : '')"
t-on-click.stop="() => { state.selectedNode.icon = ic.value; }"
t-att-title="ic.label">
<i t-att-class="'fa ' + ic.value"/>
</button>
</t>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Duration (min)</label>
<input type="number" class="form-control" min="0" step="1"
t-att-value="state.selectedNode.estimated_duration || 0"
t-on-change="(ev) => { state.selectedNode.estimated_duration = parseFloat(ev.target.value) || 0; }"/>
</div>
<div class="mb-3">
<label class="form-label fw-bold d-block">Flags</label>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="fp_chk_manual"
t-att-checked="state.selectedNode.is_manual"
t-on-change="(ev) => { state.selectedNode.is_manual = ev.target.checked; }"/>
<label class="form-check-label" for="fp_chk_manual">Manual operation</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="fp_chk_auto"
t-att-checked="state.selectedNode.auto_complete"
t-on-change="(ev) => { state.selectedNode.auto_complete = ev.target.checked; }"/>
<label class="form-check-label" for="fp_chk_auto">Auto-complete</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="fp_chk_signoff"
t-att-checked="state.selectedNode.requires_signoff"
t-on-change="(ev) => { state.selectedNode.requires_signoff = ev.target.checked; }"/>
<label class="form-check-label" for="fp_chk_signoff">Requires sign-off</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="fp_chk_visible"
t-att-checked="state.selectedNode.customer_visible"
t-on-change="(ev) => { state.selectedNode.customer_visible = ev.target.checked; }"/>
<label class="form-check-label" for="fp_chk_visible">Customer visible</label>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Opt In/Out</label>
<select class="form-select"
t-on-change="(ev) => { state.selectedNode.opt_in_out = ev.target.value; }">
<option value="disabled"
t-att-selected="state.selectedNode.opt_in_out === 'disabled'">Disabled</option>
<option value="opt_in"
t-att-selected="state.selectedNode.opt_in_out === 'opt_in'">Opt-In</option>
<option value="opt_out"
t-att-selected="state.selectedNode.opt_in_out === 'opt_out'">Opt-Out</option>
</select>
</div>
<!-- Info -->
<div class="text-muted small mb-2" t-if="state.selectedNode.work_center">
<i class="fa fa-building me-1"/>
<t t-esc="state.selectedNode.work_center"/>
</div>
<div class="text-muted small mb-2" t-if="state.selectedNode.process_type">
<i class="fa fa-tag me-1"/>
<t t-esc="state.selectedNode.process_type"/>
</div>
<div class="text-muted small mb-2"
t-if="state.selectedNode.input_count">
<i class="fa fa-keyboard-o me-1"/>
<t t-esc="state.selectedNode.input_count"/> operator input(s)
</div>
<!-- Tracking -->
<div class="o_fp_recipe_tracking mt-3 pt-3" t-if="state.selectedNode.create_date">
<div class="text-muted small mb-1">
<i class="fa fa-calendar-plus-o me-1"/>
Created <t t-esc="formatTimeAgo(state.selectedNode.create_date)"/>
<t t-if="state.selectedNode.create_uid_name">
by <strong t-esc="state.selectedNode.create_uid_name"/>
</t>
</div>
<div class="text-muted small" t-if="state.selectedNode.write_date">
<i class="fa fa-pencil me-1"/>
Updated <t t-esc="formatTimeAgo(state.selectedNode.write_date)"/>
<t t-if="state.selectedNode.write_uid_name">
by <strong t-esc="state.selectedNode.write_uid_name"/>
</t>
</div>
</div>
<!-- Actions -->
<div class="d-flex gap-2 mt-4">
<button class="btn btn-primary flex-fill"
t-on-click="saveNode"
t-att-disabled="state.saving">
<i t-att-class="state.saving ? 'fa fa-spinner fa-spin me-1' : 'fa fa-check me-1'"/>
Save
</button>
<button class="btn btn-outline-secondary"
t-on-click="() => this.onOpenForm(state.selectedNode.id)"
title="Open full form">
<i class="fa fa-external-link"/>
</button>
</div>
</div>
</t>
</div>
</div>
</div>
</t>
<!-- ========== RECURSIVE NODE TEMPLATE ========== -->
<t t-name="fusion_plating.RecipeTreeNode">
<!-- Connector line (skip for root) -->
<div class="o_fp_recipe_connector" t-if="!isFirst"/>
<!-- Node card -->
<div t-att-class="'o_fp_recipe_node'
+ (state.selectedNodeId === node.id ? ' o_fp_recipe_node_selected' : '')
+ ' o_fp_recipe_node_' + node.node_type"
t-att-draggable="node.node_type !== 'recipe' ? 'true' : 'false'"
t-on-dragstart="(ev) => this.onNodeDragStart(node, parentNode, ev)"
t-on-dragend="(ev) => this.onNodeDragEnd(ev)"
t-on-dragover="(ev) => this.onNodeDragOver(node, ev)"
t-on-dragleave="(ev) => this.onNodeDragLeave(ev)"
t-on-drop="(ev) => this.onNodeDrop(node, parentNode, ev)"
t-on-click.stop="() => this.selectNode(node)">
<!-- Drag handle (non-root only) -->
<span class="o_fp_recipe_drag_handle" t-if="node.node_type !== 'recipe'">
<i class="fa fa-grip-vertical"/>
</span>
<!-- Node header row -->
<div class="o_fp_recipe_node_header">
<!-- Expand/collapse toggle -->
<button class="o_fp_recipe_toggle_btn"
t-if="node.children and node.children.length"
t-on-click.stop="() => this.toggleExpand(node.id)">
<i t-att-class="isExpanded(node.id) ? 'fa fa-chevron-down' : 'fa fa-chevron-right'"/>
</button>
<span class="o_fp_recipe_toggle_spacer" t-else=""/>
<!-- Icon -->
<i t-att-class="'o_fp_recipe_node_icon fa ' + (node.icon || 'fa-cog')"/>
<!-- Name -->
<span class="o_fp_recipe_node_name">
<t t-esc="node.name"/>
</span>
<!-- Type badge -->
<span t-att-class="'badge o_fp_recipe_node_badge ' + getNodeTypeMeta(node.node_type).badgeClass">
<t t-esc="getNodeTypeMeta(node.node_type).label"/>
</span>
</div>
<!-- Meta row: work centre, duration, capability icons -->
<div class="o_fp_recipe_node_meta">
<span class="o_fp_recipe_node_wc" t-if="node.work_center">
<i class="fa fa-building me-1"/>
<t t-esc="node.work_center"/>
</span>
<span class="o_fp_recipe_node_duration" t-if="node.estimated_duration">
<i class="fa fa-clock-o me-1"/>
<t t-esc="formatDuration(node.estimated_duration)"/>
</span>
<!-- Capability icons -->
<span class="o_fp_recipe_node_icons">
<i class="fa fa-hand-paper-o" t-if="node.is_manual" title="Manual"/>
<i class="fa fa-bolt" t-if="!node.is_manual" title="Automated"/>
<i class="fa fa-check-square" t-if="node.requires_signoff" title="Requires sign-off"/>
<i class="fa fa-eye" t-if="node.customer_visible" title="Customer visible"/>
<i class="fa fa-magic" t-if="node.auto_complete" title="Auto-complete"/>
</span>
</div>
<!-- Action buttons row -->
<div class="o_fp_recipe_node_actions">
<button class="btn btn-sm o_fp_recipe_add_btn"
t-on-click.stop="() => this.startAddChild(node.id)"
title="Add child step">
<i class="fa fa-plus me-1"/> Add Step
</button>
<button class="btn btn-sm o_fp_recipe_delete_btn"
t-if="node.node_type !== 'recipe'"
t-on-click.stop="() => this.deleteNode(node.id)"
title="Delete">
<i class="fa fa-trash"/>
</button>
</div>
</div>
<!-- Add child inline form -->
<div class="o_fp_recipe_add_form" t-if="state.addingTo === node.id">
<div class="o_fp_recipe_connector"/>
<div class="o_fp_recipe_add_card">
<input type="text" class="form-control form-control-sm mb-2"
placeholder="New step name..."
t-att-value="state.newNodeName"
t-on-input="(ev) => { state.newNodeName = ev.target.value; }"
t-on-keydown="onAddNameKey"/>
<div class="d-flex gap-2">
<select class="form-select form-select-sm flex-shrink-1"
style="max-width: 140px;"
t-on-change="(ev) => { state.newNodeType = ev.target.value; }">
<t t-foreach="getNodeTypeOptions()" t-as="opt" t-key="opt.value">
<option t-att-value="opt.value"
t-att-selected="state.newNodeType === opt.value"
t-esc="opt.label"/>
</t>
</select>
<button class="btn btn-sm btn-primary" t-on-click="confirmAdd">
<i class="fa fa-check"/>
</button>
<button class="btn btn-sm btn-outline-secondary" t-on-click="cancelAdd">
<i class="fa fa-times"/>
</button>
</div>
</div>
</div>
<!-- Children (recursive) -->
<div class="o_fp_recipe_children" t-if="node.children and node.children.length and isExpanded(node.id)">
<t t-foreach="node.children" t-as="child" t-key="child.id">
<t t-call="fusion_plating.RecipeTreeNode">
<t t-set="node" t-value="child"/>
<t t-set="parentNode" t-value="node"/>
<t t-set="isFirst" t-value="false"/>
</t>
</t>
</div>
</t>
</templates>

View File

@@ -1,127 +0,0 @@
<?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="view_fp_bath_log_list" model="ir.ui.view">
<field name="name">fp.bath.log.list</field>
<field name="model">fusion.plating.bath.log</field>
<field name="arch" type="xml">
<list string="Bath Logs"
decoration-success="status == 'ok'"
decoration-warning="status == 'warning'"
decoration-danger="status == 'out_of_spec'">
<field name="name"/>
<field name="log_date"/>
<field name="bath_id"/>
<field name="tank_id" optional="show"/>
<field name="process_type_id" optional="show"/>
<field name="operator_id"/>
<field name="shift" optional="hide"/>
<field name="status" widget="badge"
decoration-success="status == 'ok'"
decoration-warning="status == 'warning'"
decoration-danger="status == 'out_of_spec'"/>
</list>
</field>
</record>
<record id="view_fp_bath_log_form" model="ir.ui.view">
<field name="name">fp.bath.log.form</field>
<field name="model">fusion.plating.bath.log</field>
<field name="arch" type="xml">
<form string="Bath Log">
<sheet>
<div class="oe_title">
<h1><field name="name" readonly="1"/></h1>
</div>
<group>
<group>
<field name="bath_id"/>
<field name="tank_id" readonly="1"/>
<field name="process_type_id" readonly="1"/>
<field name="facility_id" readonly="1" groups="base.group_multi_company"/>
</group>
<group>
<field name="log_date"/>
<field name="operator_id"/>
<field name="shift"/>
<field name="status" readonly="1" widget="badge"
decoration-success="status == 'ok'"
decoration-warning="status == 'warning'"
decoration-danger="status == 'out_of_spec'"/>
</group>
</group>
<notebook>
<page string="Readings">
<field name="line_ids">
<list editable="bottom"
decoration-success="status == 'ok'"
decoration-warning="status == 'warning'"
decoration-danger="status == 'out_of_spec'">
<field name="sequence" widget="handle"/>
<field name="parameter_id"/>
<field name="value"/>
<field name="uom"/>
<field name="target_min"/>
<field name="target_max"/>
<field name="status" widget="badge"
decoration-success="status == 'ok'"
decoration-warning="status == 'warning'"
decoration-danger="status == 'out_of_spec'"/>
<field name="notes"/>
</list>
</field>
</page>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_bath_log_search" model="ir.ui.view">
<field name="name">fp.bath.log.search</field>
<field name="model">fusion.plating.bath.log</field>
<field name="arch" type="xml">
<search string="Bath Logs">
<field name="name"/>
<field name="bath_id"/>
<field name="tank_id"/>
<field name="process_type_id"/>
<field name="operator_id"/>
<separator/>
<filter string="OK" name="ok" domain="[('status','=','ok')]"/>
<filter string="Warning" name="warn" domain="[('status','=','warning')]"/>
<filter string="Out of Spec" name="oos" domain="[('status','=','out_of_spec')]"/>
<separator/>
<filter string="Today" name="today"
domain="[('log_date','&gt;=', context_today().strftime('%Y-%m-%d'))]"/>
<filter string="This Week" name="week"
domain="[('log_date','&gt;=', (context_today() - relativedelta(days=7)).strftime('%Y-%m-%d'))]"/>
<group>
<filter string="Bath" name="group_bath" context="{'group_by':'bath_id'}"/>
<filter string="Tank" name="group_tank" context="{'group_by':'tank_id'}"/>
<filter string="Process" name="group_process" context="{'group_by':'process_type_id'}"/>
<filter string="Operator" name="group_op" context="{'group_by':'operator_id'}"/>
<filter string="Status" name="group_status" context="{'group_by':'status'}"/>
<filter string="Day" name="group_day" context="{'group_by':'log_date:day'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_bath_log" model="ir.actions.act_window">
<field name="name">Bath Logs</field>
<field name="res_model">fusion.plating.bath.log</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_bath_log_search"/>
</record>
</odoo>

View File

@@ -1,198 +0,0 @@
<?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="view_fp_bath_list" model="ir.ui.view">
<field name="name">fp.bath.list</field>
<field name="model">fusion.plating.bath</field>
<field name="arch" type="xml">
<list string="Baths" decoration-muted="state == 'dumped'"
decoration-warning="last_log_status == 'warning'"
decoration-danger="last_log_status == 'out_of_spec'">
<field name="name"/>
<field name="tank_id"/>
<field name="process_type_id"/>
<field name="facility_id" groups="base.group_multi_company"/>
<field name="state" widget="badge"
decoration-success="state == 'operational'"
decoration-info="state == 'new'"
decoration-warning="state == 'under_review'"
decoration-danger="state == 'dump_scheduled'"
decoration-muted="state == 'dumped'"/>
<field name="mto_count"/>
<field name="last_log_date"/>
<field name="last_log_status" widget="badge"
decoration-success="last_log_status == 'ok'"
decoration-warning="last_log_status == 'warning'"
decoration-danger="last_log_status == 'out_of_spec'"/>
<field name="makeup_date" optional="hide"/>
</list>
</field>
</record>
<record id="view_fp_bath_form" model="ir.ui.view">
<field name="name">fp.bath.form</field>
<field name="model">fusion.plating.bath</field>
<field name="arch" type="xml">
<form string="Bath">
<header>
<button name="action_make_operational" string="Set Operational" type="object"
class="oe_highlight" invisible="state != 'new'"/>
<button name="action_mark_under_review" string="Flag for Review" type="object"
invisible="state not in ('operational',)"/>
<button name="action_schedule_dump" string="Schedule Dump" type="object"
invisible="state not in ('operational','under_review')"/>
<button name="action_dump" string="Dump" type="object"
invisible="state != 'dump_scheduled'"/>
<field name="state" widget="statusbar"
statusbar_visible="new,operational,under_review,dump_scheduled,dumped"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(action_fp_bath_log)d" type="action" class="oe_stat_button" icon="fa-flask"
context="{'search_default_bath_id': id}">
<field name="log_count" widget="statinfo" string="Logs"/>
</button>
</div>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" readonly="state != 'new'"/></h1>
</div>
<group>
<group>
<field name="tank_id"/>
<field name="process_type_id"/>
<field name="facility_id" readonly="1"/>
<field name="volume"/>
</group>
<group>
<field name="makeup_date"/>
<field name="makeup_by_id"/>
<field name="mto_count" readonly="1"/>
<field name="last_log_date" readonly="1"/>
<field name="last_log_status" readonly="1" widget="badge"
decoration-success="last_log_status == 'ok'"
decoration-warning="last_log_status == 'warning'"
decoration-danger="last_log_status == 'out_of_spec'"/>
</group>
</group>
<notebook>
<page string="Target Ranges">
<field name="target_line_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="parameter_id"/>
<field name="target_min"/>
<field name="target_max"/>
<field name="uom"/>
</list>
</field>
<p class="text-muted mt-2">
Per-bath target overrides. If empty, the parameter's default range is used.
</p>
</page>
<page string="Chemistry Logs">
<field name="log_ids" readonly="1">
<list decoration-success="status == 'ok'"
decoration-warning="status == 'warning'"
decoration-danger="status == 'out_of_spec'">
<field name="name"/>
<field name="log_date"/>
<field name="operator_id"/>
<field name="shift"/>
<field name="status"/>
</list>
</field>
</page>
<page string="Notes">
<field name="notes"/>
</page>
<page string="Dump" invisible="state not in ('dump_scheduled','dumped')">
<group>
<field name="dump_scheduled_date"/>
<field name="dumped_date"/>
<field name="dump_reason"/>
</group>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_bath_kanban" model="ir.ui.view">
<field name="name">fp.bath.kanban</field>
<field name="model">fusion.plating.bath</field>
<field name="arch" type="xml">
<kanban default_group_by="state" class="o_fp_bath_kanban">
<field name="id"/>
<field name="name"/>
<field name="tank_id"/>
<field name="process_type_id"/>
<field name="state"/>
<field name="last_log_status"/>
<field name="mto_count"/>
<field name="status_color"/>
<templates>
<t t-name="card">
<div class="o_fp_card o_fp_bath_card"
t-att-data-log-status="record.last_log_status.raw_value">
<div class="d-flex justify-content-between align-items-start">
<strong class="o_fp_card_title"><field name="name"/></strong>
<span class="o_fp_health_dot"
t-att-data-status="record.last_log_status.raw_value or 'ok'"/>
</div>
<div class="small text-muted">
<field name="process_type_id"/>
</div>
<div class="small"><i class="fa fa-flask me-1 text-muted"/><field name="tank_id"/></div>
<div class="d-flex justify-content-between mt-2 small">
<span class="text-muted">MTO</span>
<span class="fw-bold"><field name="mto_count"/></span>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_fp_bath_search" model="ir.ui.view">
<field name="name">fp.bath.search</field>
<field name="model">fusion.plating.bath</field>
<field name="arch" type="xml">
<search string="Baths">
<field name="name"/>
<field name="tank_id"/>
<field name="process_type_id"/>
<field name="facility_id"/>
<separator/>
<filter string="Operational" name="operational" domain="[('state','=','operational')]"/>
<filter string="Under Review" name="review" domain="[('state','=','under_review')]"/>
<filter string="Out of Spec" name="oos" domain="[('last_log_status','=','out_of_spec')]"/>
<filter string="Warning" name="warn" domain="[('last_log_status','=','warning')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Facility" name="group_facility" context="{'group_by':'facility_id'}"/>
<filter string="Process" name="group_process" context="{'group_by':'process_type_id'}"/>
<filter string="Tank" name="group_tank" context="{'group_by':'tank_id'}"/>
<filter string="Status" name="group_state" context="{'group_by':'state'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_bath" model="ir.actions.act_window">
<field name="name">Baths</field>
<field name="res_model">fusion.plating.bath</field>
<field name="view_mode">kanban,list,form</field>
<field name="search_view_id" ref="view_fp_bath_search"/>
</record>
</odoo>

View File

@@ -1,156 +0,0 @@
<?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="view_fp_facility_list" model="ir.ui.view">
<field name="name">fp.facility.list</field>
<field name="model">fusion.plating.facility</field>
<field name="arch" type="xml">
<list string="Facilities">
<field name="sequence" widget="handle"/>
<field name="code"/>
<field name="name"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="work_center_count"/>
<field name="tank_count"/>
<field name="capability_count"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<record id="view_fp_facility_form" model="ir.ui.view">
<field name="name">fp.facility.form</field>
<field name="model">fusion.plating.facility</field>
<field name="arch" type="xml">
<form string="Facility">
<header/>
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
<div class="oe_button_box" name="button_box">
<button name="%(action_fp_tank)d" type="action" class="oe_stat_button" icon="fa-flask"
context="{'search_default_facility_id': id}">
<field name="tank_count" widget="statinfo" string="Tanks"/>
</button>
<button name="%(action_fp_work_center)d" type="action" class="oe_stat_button" icon="fa-cogs"
context="{'search_default_facility_id': id}">
<field name="work_center_count" widget="statinfo" string="Work Centers"/>
</button>
</div>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Site A — Mississauga"/></h1>
<div class="text-muted">
<field name="code" placeholder="SITE-A"/>
</div>
</div>
<group>
<group>
<field name="company_id" groups="base.group_multi_company"/>
<field name="partner_id"/>
<field name="sequence"/>
</group>
<group>
<field name="active" widget="boolean_toggle"/>
</group>
</group>
<notebook>
<page string="Capabilities">
<field name="capability_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<p class="text-muted mt-2">
Process types this facility can perform. Install process packs
(EN, chrome, anodize, black oxide) to populate the list.
</p>
</page>
<page string="Work Centers">
<field name="work_center_ids">
<list>
<field name="sequence" widget="handle"/>
<field name="code"/>
<field name="name"/>
<field name="tank_count"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</page>
<page string="Tanks">
<field name="tank_ids">
<list>
<field name="code"/>
<field name="name"/>
<field name="work_center_id"/>
<field name="current_process_id"/>
<field name="state"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_facility_kanban" model="ir.ui.view">
<field name="name">fp.facility.kanban</field>
<field name="model">fusion.plating.facility</field>
<field name="arch" type="xml">
<kanban class="o_fp_facility_kanban">
<field name="id"/>
<field name="name"/>
<field name="code"/>
<field name="tank_count"/>
<field name="work_center_count"/>
<field name="capability_count"/>
<templates>
<t t-name="card">
<div class="o_fp_card">
<div class="d-flex align-items-start justify-content-between">
<div>
<strong class="o_fp_card_title"><field name="name"/></strong>
<div class="text-muted small"><field name="code"/></div>
</div>
<i class="fa fa-industry text-muted" aria-hidden="true"/>
</div>
<div class="d-flex gap-3 mt-3 o_fp_card_stats">
<div class="text-center">
<div class="fw-bold"><field name="work_center_count"/></div>
<div class="small text-muted">Lines</div>
</div>
<div class="text-center">
<div class="fw-bold"><field name="tank_count"/></div>
<div class="small text-muted">Tanks</div>
</div>
<div class="text-center">
<div class="fw-bold"><field name="capability_count"/></div>
<div class="small text-muted">Processes</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_fp_facility" model="ir.actions.act_window">
<field name="name">Facilities</field>
<field name="res_model">fusion.plating.facility</field>
<field name="view_mode">kanban,list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first facility
</p>
<p>
A facility is a physical site with its own tanks, work centers,
operators, and regulatory profile. A single-site shop has one
facility; a multi-site operator has several.
</p>
</field>
</record>
</odoo>

View File

@@ -1,83 +0,0 @@
<?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>
<!-- ===== ROOT APP MENU ===== -->
<menuitem id="menu_fp_root"
name="Plating"
sequence="46"
web_icon="fusion_plating,static/description/icon.png"
groups="group_fusion_plating_operator"/>
<!-- ===== OPERATIONS ===== -->
<menuitem id="menu_fp_operations"
name="Operations"
parent="menu_fp_root"
sequence="18"/>
<menuitem id="menu_fp_process_recipes"
name="Process Recipes"
parent="menu_fp_operations"
action="action_fp_process_recipe"
sequence="5"/>
<menuitem id="menu_fp_baths"
name="Baths"
parent="menu_fp_operations"
action="action_fp_bath"
sequence="10"/>
<menuitem id="menu_fp_bath_logs"
name="Chemistry Logs"
parent="menu_fp_operations"
action="action_fp_bath_log"
sequence="20"/>
<menuitem id="menu_fp_tanks"
name="Tanks"
parent="menu_fp_operations"
action="action_fp_tank"
sequence="30"/>
<!-- ===== CONFIGURATION ===== -->
<menuitem id="menu_fp_config"
name="Configuration"
parent="menu_fp_root"
sequence="90"
groups="group_fusion_plating_manager"/>
<menuitem id="menu_fp_facilities"
name="Facilities"
parent="menu_fp_config"
action="action_fp_facility"
sequence="10"/>
<menuitem id="menu_fp_work_centers"
name="Work Centers"
parent="menu_fp_config"
action="action_fp_work_center"
sequence="20"/>
<menuitem id="menu_fp_process_categories"
name="Process Categories"
parent="menu_fp_config"
action="action_fp_process_category"
sequence="30"/>
<menuitem id="menu_fp_process_types"
name="Process Types"
parent="menu_fp_config"
action="action_fp_process_type"
sequence="40"/>
<menuitem id="menu_fp_bath_parameters"
name="Bath Parameters"
parent="menu_fp_config"
action="action_fp_bath_parameter"
sequence="50"/>
</odoo>

View File

@@ -1,184 +0,0 @@
<?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>
<!-- ===== TREE (LIST) VIEW — Recipes only ===== -->
<record id="view_fp_process_node_tree" model="ir.ui.view">
<field name="name">fusion.plating.process.node.tree</field>
<field name="model">fusion.plating.process.node</field>
<field name="arch" type="xml">
<list string="Process Recipes" default_order="sequence, name">
<field name="sequence" widget="handle"/>
<field name="code" optional="show"/>
<field name="name"/>
<field name="node_type" widget="badge"
decoration-info="node_type == 'recipe'"
decoration-success="node_type == 'operation'"
decoration-warning="node_type == 'sub_process'"
decoration-muted="node_type == 'step'"/>
<field name="process_type_id" optional="show"/>
<field name="work_center_id" optional="show"/>
<field name="child_count" string="Steps"/>
<field name="version" optional="hide"/>
<field name="active" column_invisible="True"/>
</list>
</field>
</record>
<!-- ===== FORM VIEW ===== -->
<record id="view_fp_process_node_form" model="ir.ui.view">
<field name="name">fusion.plating.process.node.form</field>
<field name="model">fusion.plating.process.node</field>
<field name="arch" type="xml">
<form string="Process Node">
<header>
<button name="action_open_tree_editor" type="object"
string="Open Tree Editor" class="btn-primary"
icon="fa-sitemap"
invisible="node_type != 'recipe'"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_open_tree_editor" type="object"
class="oe_stat_button" icon="fa-sitemap"
invisible="node_type != 'recipe'">
<field name="child_count" widget="statinfo"
string="Steps"/>
</button>
</div>
<widget name="web_ribbon" title="Archived"
bg_color="text-bg-danger"
invisible="active"/>
<div class="oe_title">
<h1>
<field name="name" placeholder="Node name..."/>
</h1>
</div>
<group>
<group string="Classification">
<field name="code"/>
<field name="node_type"/>
<field name="process_type_id"/>
<field name="work_center_id"/>
<field name="parent_id"/>
<field name="icon"/>
</group>
<group string="Behaviour">
<field name="sequence"/>
<field name="estimated_duration"/>
<field name="auto_complete"/>
<field name="customer_visible"/>
<field name="is_manual"/>
<field name="requires_signoff"/>
<field name="opt_in_out"/>
<field name="version"/>
<field name="active" invisible="True"/>
</group>
</group>
<group>
<group string="Tracking">
<field name="create_date" string="Created"/>
<field name="create_uid" string="Created By"/>
<field name="write_date" string="Last Updated"/>
<field name="write_uid" string="Updated By"/>
</group>
</group>
<notebook>
<page string="Description" name="description">
<field name="description" widget="html"/>
</page>
<page string="Operator Inputs" name="inputs">
<field name="input_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="input_type"/>
<field name="required"/>
<field name="hint"/>
<field name="uom"/>
<field name="selection_options"
invisible="input_type != 'selection'"/>
</list>
</field>
</page>
<page string="Child Steps" name="children">
<field name="child_ids">
<list default_order="sequence, name">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="node_type" widget="badge"/>
<field name="work_center_id"/>
<field name="estimated_duration"/>
<field name="child_count" string="Sub-Steps"/>
</list>
</field>
</page>
<page string="Notes" name="notes">
<field name="notes" placeholder="Internal notes..."/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- ===== SEARCH VIEW ===== -->
<record id="view_fp_process_node_search" model="ir.ui.view">
<field name="name">fusion.plating.process.node.search</field>
<field name="model">fusion.plating.process.node</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="code"/>
<field name="process_type_id"/>
<field name="work_center_id"/>
<filter name="recipes_only" string="Recipes"
domain="[('node_type', '=', 'recipe')]"/>
<filter name="sub_processes" string="Sub-Processes"
domain="[('node_type', '=', 'sub_process')]"/>
<filter name="operations" string="Operations"
domain="[('node_type', '=', 'operation')]"/>
<filter name="archived" string="Archived"
domain="[('active', '=', False)]"/>
<filter name="group_type" string="Type"
context="{'group_by': 'node_type'}"/>
<filter name="group_process" string="Process Type"
context="{'group_by': 'process_type_id'}"/>
<filter name="group_wc" string="Work Centre"
context="{'group_by': 'work_center_id'}"/>
</search>
</field>
</record>
<!-- ===== WINDOW ACTION — Recipe list ===== -->
<record id="action_fp_process_recipe" model="ir.actions.act_window">
<field name="name">Process Recipes</field>
<field name="res_model">fusion.plating.process.node</field>
<field name="view_mode">list,form</field>
<field name="domain">[('node_type', '=', 'recipe')]</field>
<field name="context">{'default_node_type': 'recipe', 'search_default_recipes_only': 1}</field>
<field name="search_view_id" ref="view_fp_process_node_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first process recipe
</p>
<p>
Recipes define the step-by-step process for plating parts.
Each recipe is a reusable template with nested operations
and sub-processes.
</p>
</field>
</record>
<!-- ===== CLIENT ACTION — OWL Tree Editor ===== -->
<record id="action_fp_recipe_tree_editor" model="ir.actions.client">
<field name="name">Recipe Tree Editor</field>
<field name="tag">fp_recipe_tree_editor</field>
</record>
</odoo>

View File

@@ -1,240 +0,0 @@
<?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>
<!-- ===== Process Category ===== -->
<record id="view_fp_process_category_list" model="ir.ui.view">
<field name="name">fp.process.category.list</field>
<field name="model">fusion.plating.process.category</field>
<field name="arch" type="xml">
<list string="Process Categories">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="code"/>
<field name="process_type_count"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<record id="view_fp_process_category_form" model="ir.ui.view">
<field name="name">fp.process.category.form</field>
<field name="model">fusion.plating.process.category</field>
<field name="arch" type="xml">
<form string="Process Category">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Plating"/></h1>
</div>
<group>
<group>
<field name="code"/>
<field name="sequence"/>
</group>
<group>
<field name="active" widget="boolean_toggle"/>
</group>
</group>
<notebook>
<page string="Description">
<field name="description" placeholder="What this category represents..."/>
</page>
<page string="Process Types">
<field name="process_type_ids">
<list>
<field name="sequence" widget="handle"/>
<field name="code"/>
<field name="name"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_fp_process_category" model="ir.actions.act_window">
<field name="name">Process Categories</field>
<field name="res_model">fusion.plating.process.category</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Define process categories
</p>
<p>
Categories group related finishing processes (plating, anodizing,
conversion coatings, etc.). Process packs reference these categories
when they load specific process types.
</p>
</field>
</record>
<!-- ===== Process Type ===== -->
<record id="view_fp_process_type_list" model="ir.ui.view">
<field name="name">fp.process.type.list</field>
<field name="model">fusion.plating.process.type</field>
<field name="arch" type="xml">
<list string="Process Types">
<field name="sequence" widget="handle"/>
<field name="code"/>
<field name="name"/>
<field name="category_id"/>
<field name="icon" optional="hide"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<record id="view_fp_process_type_form" model="ir.ui.view">
<field name="name">fp.process.type.form</field>
<field name="model">fusion.plating.process.type</field>
<field name="arch" type="xml">
<form string="Process Type">
<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. Electroless Nickel — Mid Phosphorus"/></h1>
<div class="text-muted">
<field name="code" placeholder="EN_MID"/>
</div>
</div>
<group>
<group>
<field name="category_id"/>
<field name="sequence"/>
<field name="icon"/>
</group>
<group>
<field name="color" widget="color_picker"/>
<field name="active" widget="boolean_toggle"/>
</group>
</group>
<notebook>
<page string="Description">
<field name="description" placeholder="Short description of the process..."/>
</page>
<page string="Bath Parameters">
<field name="parameter_ids">
<list>
<field name="sequence" widget="handle"/>
<field name="code"/>
<field name="name"/>
<field name="parameter_type"/>
<field name="uom"/>
<field name="target_min"/>
<field name="target_max"/>
</list>
</field>
</page>
<page string="Hazard Notes">
<field name="hazard_notes"
placeholder="Process-level hazard awareness (e.g. Cr(VI) carcinogen, hypophosphite reducer)..."/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_fp_process_type_search" model="ir.ui.view">
<field name="name">fp.process.type.search</field>
<field name="model">fusion.plating.process.type</field>
<field name="arch" type="xml">
<search string="Process Types">
<field name="name"/>
<field name="code"/>
<field name="category_id"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Category" name="group_category" context="{'group_by':'category_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_process_type" model="ir.actions.act_window">
<field name="name">Process Types</field>
<field name="res_model">fusion.plating.process.type</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_process_type_search"/>
<field name="context">{'search_default_group_category': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No process types yet
</p>
<p>
Install a process pack (EN, Chrome, Anodize, Black Oxide) to load
pre-configured process types, or create your own.
</p>
</field>
</record>
<!-- ===== Bath Parameter ===== -->
<record id="view_fp_bath_parameter_list" model="ir.ui.view">
<field name="name">fp.bath.parameter.list</field>
<field name="model">fusion.plating.bath.parameter</field>
<field name="arch" type="xml">
<list string="Bath Parameters">
<field name="sequence" widget="handle"/>
<field name="code"/>
<field name="name"/>
<field name="parameter_type"/>
<field name="uom"/>
<field name="target_min"/>
<field name="target_max"/>
<field name="warning_tolerance"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<record id="view_fp_bath_parameter_form" model="ir.ui.view">
<field name="name">fp.bath.parameter.form</field>
<field name="model">fusion.plating.bath.parameter</field>
<field name="arch" type="xml">
<form string="Bath Parameter">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Nickel Concentration"/></h1>
<div class="text-muted">
<field name="code" placeholder="Ni"/>
</div>
</div>
<group>
<group>
<field name="parameter_type"/>
<field name="uom"/>
<field name="decimals"/>
</group>
<group>
<field name="target_min"/>
<field name="target_max"/>
<field name="warning_tolerance"/>
<field name="active" widget="boolean_toggle"/>
</group>
</group>
<group string="Description">
<field name="description" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_fp_bath_parameter" model="ir.actions.act_window">
<field name="name">Bath Parameters</field>
<field name="res_model">fusion.plating.bath.parameter</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@@ -1,168 +0,0 @@
<?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="view_fp_tank_list" model="ir.ui.view">
<field name="name">fp.tank.list</field>
<field name="model">fusion.plating.tank</field>
<field name="arch" type="xml">
<list string="Tanks">
<field name="facility_id"/>
<field name="work_center_id"/>
<field name="code"/>
<field name="name"/>
<field name="current_process_id"/>
<field name="state" widget="badge"
decoration-success="state == 'in_use'"
decoration-info="state == 'filled'"
decoration-warning="state in ('draining', 'maintenance')"
decoration-muted="state in ('empty', 'out_of_service')"/>
<field name="material" optional="hide"/>
<field name="volume" optional="show"/>
<field name="volume_uom" optional="show"/>
<field name="active" widget="boolean_toggle" optional="hide"/>
</list>
</field>
</record>
<record id="view_fp_tank_form" model="ir.ui.view">
<field name="name">fp.tank.form</field>
<field name="model">fusion.plating.tank</field>
<field name="arch" type="xml">
<form string="Tank">
<header>
<field name="state" widget="statusbar"
statusbar_visible="empty,filled,in_use,draining,maintenance"/>
</header>
<sheet>
<widget name="web_ribbon" title="Out of Service" bg_color="text-bg-danger"
invisible="state != 'out_of_service'"/>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. EN Plating Tank A1"/></h1>
<div class="text-muted">
<field name="code" placeholder="T-01"/>
</div>
</div>
<group>
<group string="Location">
<field name="facility_id"/>
<field name="work_center_id"/>
<field name="sequence"/>
</group>
<group string="Current Bath">
<field name="current_bath_id" readonly="1"/>
<field name="current_process_id" readonly="1"/>
<field name="qr_code"/>
</group>
</group>
<notebook>
<page string="Physical">
<group>
<group>
<field name="volume"/>
<field name="volume_uom"/>
<field name="material"/>
</group>
<group>
<field name="heating_type"/>
<field name="has_filtration"/>
<field name="has_rectifier"/>
</group>
</group>
</page>
<page string="Bath History">
<field name="bath_ids">
<list decoration-muted="state == 'dumped'">
<field name="name"/>
<field name="process_type_id"/>
<field name="state"/>
<field name="makeup_date"/>
<field name="mto_count"/>
<field name="last_log_date"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_tank_kanban" model="ir.ui.view">
<field name="name">fp.tank.kanban</field>
<field name="model">fusion.plating.tank</field>
<field name="arch" type="xml">
<kanban class="o_fp_tank_kanban">
<field name="id"/>
<field name="code"/>
<field name="name"/>
<field name="state"/>
<field name="current_bath_id"/>
<field name="current_process_id"/>
<field name="facility_id"/>
<field name="work_center_id"/>
<templates>
<t t-name="card">
<div class="o_fp_card o_fp_tank_card" t-att-data-state="record.state.raw_value">
<div class="d-flex align-items-start justify-content-between">
<div>
<strong class="o_fp_card_title"><field name="code"/></strong>
<div class="small text-muted"><field name="name"/></div>
</div>
<span class="badge o_fp_badge" t-att-data-state="record.state.raw_value">
<field name="state"/>
</span>
</div>
<div class="mt-2 small">
<div><i class="fa fa-flask me-1 text-muted"/><field name="current_process_id"/></div>
<div class="text-muted"><field name="work_center_id"/></div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_fp_tank_search" model="ir.ui.view">
<field name="name">fp.tank.search</field>
<field name="model">fusion.plating.tank</field>
<field name="arch" type="xml">
<search string="Tanks">
<field name="name"/>
<field name="code"/>
<field name="qr_code"/>
<field name="facility_id"/>
<field name="work_center_id"/>
<field name="current_process_id"/>
<separator/>
<filter string="In Use" name="in_use" domain="[('state','=','in_use')]"/>
<filter string="Filled" name="filled" domain="[('state','=','filled')]"/>
<filter string="Maintenance" name="maintenance" domain="[('state','=','maintenance')]"/>
<filter string="Out of Service" name="out" domain="[('state','=','out_of_service')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Facility" name="group_facility" context="{'group_by':'facility_id'}"/>
<filter string="Work Center" name="group_wc" context="{'group_by':'work_center_id'}"/>
<filter string="Process" name="group_process" context="{'group_by':'current_process_id'}"/>
<filter string="Status" name="group_state" context="{'group_by':'state'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_tank" model="ir.actions.act_window">
<field name="name">Tanks</field>
<field name="res_model">fusion.plating.tank</field>
<field name="view_mode">kanban,list,form</field>
<field name="search_view_id" ref="view_fp_tank_search"/>
</record>
</odoo>

View File

@@ -1,92 +0,0 @@
<?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="view_fp_work_center_list" model="ir.ui.view">
<field name="name">fp.work.center.list</field>
<field name="model">fusion.plating.work.center</field>
<field name="arch" type="xml">
<list string="Work Centers">
<field name="facility_id"/>
<field name="sequence" widget="handle"/>
<field name="code"/>
<field name="name"/>
<field name="tank_count"/>
<field name="capacity_per_day"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<record id="view_fp_work_center_form" model="ir.ui.view">
<field name="name">fp.work.center.form</field>
<field name="model">fusion.plating.work.center</field>
<field name="arch" type="xml">
<form string="Work Center">
<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. Line 1 — EN Plating"/></h1>
<div class="text-muted">
<field name="code" placeholder="LINE-1"/>
</div>
</div>
<group>
<group>
<field name="facility_id"/>
<field name="sequence"/>
</group>
<group>
<field name="capacity_per_day"/>
<field name="active" widget="boolean_toggle"/>
</group>
</group>
<notebook>
<page string="Supported Processes">
<field name="supported_process_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</page>
<page string="Tanks">
<field name="tank_ids">
<list>
<field name="code"/>
<field name="name"/>
<field name="current_process_id"/>
<field name="state"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_fp_work_center_search" model="ir.ui.view">
<field name="name">fp.work.center.search</field>
<field name="model">fusion.plating.work.center</field>
<field name="arch" type="xml">
<search string="Work Centers">
<field name="name"/>
<field name="code"/>
<field name="facility_id"/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Facility" name="group_facility" context="{'group_by':'facility_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_work_center" model="ir.actions.act_window">
<field name="name">Work Centers</field>
<field name="res_model">fusion.plating.work.center</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_work_center_search"/>
</record>
</odoo>

View File

@@ -1,46 +0,0 @@
# Fusion Plating — Aerospace (AS9100 + Nadcap AC7108)
Part of the Fusion Plating product family by Nexa Systems Inc.
Industry pack that adds the aerospace-specific records a plating shop needs
to support AS9100 Rev D certification and Nadcap AC7108 chemical-processing
accreditation on top of the Fusion Plating QMS module.
## What this module adds
- **AS9100 Rev D clause catalog** — hierarchical, seeded with the main
clauses 4 through 10 and the aerospace-critical 8.1.1 through 8.1.4
family (Operational Risk Management, Configuration Management, Product
Safety, Counterfeit Parts Prevention).
- **Nadcap AC7108 audit records** — PRI-assigned auditor flag, merit and
NCR counts, accreditation start/end dates, checklist selection covering
AC7108 base and slash sheets 10/11/12/13/14.
- **Counterfeit parts prevention log** — supports the AS9100 §8.1.4
requirement and GIDEP reporting.
- **Configuration management baseline** — records and tracks change
history for configuration items (AS9100 §8.1.2).
- **Risk register** — likelihood × impact scoring with automatic risk
level classification (AS9100 §8.1.1).
- **FAIR extension** — adds AS9102 form 1/2/3 attachments, drawing
revision, and customer approval signature/date.
- **Customer specification extension** — aerospace flag, AS9100 clause
mapping, Nadcap requirement flag, PRI file code, customer approval
requirement flag.
## Seed data
- ~25 AS9100 Rev D clauses.
- Industry specs loaded with process-pack links: AMS 2404, ASTM B733,
MIL-C-26074, MIL-A-8625, QQ-C-320, MIL-DTL-13924, AMS 2700, AMS 2759/9,
AMS-QQ-P-416, BAC 5709, PRI AS7108.
## Dependencies
Requires all four process packs (EN, chrome, anodize, black oxide) plus
`fusion_plating_quality`. The process-pack dependency guarantees the
seeded customer-spec process-type references always resolve.
## License
OPL-1 (Odoo Proprietary License v1.0). Copyright (c) 2026 Nexa Systems
Inc. All rights reserved.

View File

@@ -1,6 +0,0 @@
# -*- 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

View File

@@ -1,93 +0,0 @@
# -*- 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 — Aerospace (AS9100 + Nadcap)',
'version': '19.0.1.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Aerospace industry pack: AS9100 Rev D clause library, Nadcap AC7108 '
'audits, counterfeit parts prevention, config management, risk register, '
'FAIR/AS9102 extensions, industry spec library (AMS, ASTM, MIL, BAC).',
'description': """
Fusion Plating — Aerospace (AS9100 + Nadcap AC7108)
====================================================
Part of the Fusion Plating product family by Nexa Systems Inc.
This industry pack layers aerospace-specific records, workflows and seed
data on top of the Fusion Plating quality module (QMS). It is the
baseline every AS9100-certified plating / metal finishing shop needs on
top of the generic QMS.
Records added
-------------
* AS9100 Rev D clause catalog (hierarchical, seeded with the main
clauses 4 through 10 plus the aerospace-critical 8.1.18.1.4 family)
* Nadcap AC7108 audit records (PRI-assigned auditors, merit/NCR counts,
accreditation start/end, checklist selection covering AC7108 base and
slash sheets 10/11/12/13/14)
* Counterfeit parts prevention log (AS9100 §8.1.4, GIDEP reporting)
* Configuration management baseline (AS9100 §8.1.2)
* Risk register with likelihood × impact scoring (AS9100 §8.1.1)
* FAIR extension for AS9102 forms 1/2/3 and customer approval
* Customer specification extension for aerospace flags, AS9100 clause
mapping, Nadcap flag and PRI file code
Seed data
---------
* ~25 AS9100 Rev D clauses with a nested parent/child structure
* Industry specs: AMS 2404, ASTM B733, MIL-C-26074, MIL-A-8625,
QQ-C-320, MIL-DTL-13924, AMS 2700, AMS 2759/9, AMS-QQ-P-416,
BAC 5709, PRI AS7108
Dependencies
------------
Depends on all four process packs (EN, chrome, anodize, black oxide)
plus the quality QMS module, so the seeded customer-spec ↔ process-type
relationships always resolve.
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
""",
'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_quality',
'fusion_plating_process_en',
'fusion_plating_process_chrome',
'fusion_plating_process_anodize',
'fusion_plating_process_black_oxide',
],
'data': [
'security/fp_aerospace_security.xml',
'security/ir.model.access.csv',
'data/fp_sequence_data.xml',
'data/fp_as9100_clause_data.xml',
'data/fp_customer_spec_data.xml',
'views/fp_as9100_clause_views.xml',
'views/fp_nadcap_audit_views.xml',
'views/fp_counterfeit_views.xml',
'views/fp_config_item_views.xml',
'views/fp_risk_views.xml',
'views/fp_customer_spec_views.xml',
'views/fp_fair_views.xml',
'views/fp_menu.xml',
],
'demo': [
'data/fp_demo_aerospace_data.xml',
],
'assets': {
'web.assets_backend': [
'fusion_plating_aerospace/static/src/scss/fusion_plating_aerospace.scss',
],
},
'installable': True,
'application': False,
'auto_install': False,
}

View File

@@ -1,230 +0,0 @@
<?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.
Seed AS9100 Rev D clause hierarchy. Only the main top-level clauses
(410) and the aerospace-critical 8.1.x family are included — a shop
can extend this with the finer-grained sub-clauses via user data.
-->
<odoo noupdate="1">
<!-- ===== 4. CONTEXT OF THE ORGANIZATION ===== -->
<record id="as9100_clause_4" model="fusion.plating.as9100.clause">
<field name="name">Context of the organization</field>
<field name="code">4</field>
<field name="standard">as9100d</field>
<field name="category">leadership</field>
<field name="description" type="html"><p>Understanding the organization and its context, interested parties, scope of the quality management system, and the QMS and its processes.</p></field>
</record>
<!-- ===== 5. LEADERSHIP ===== -->
<record id="as9100_clause_5" model="fusion.plating.as9100.clause">
<field name="name">Leadership</field>
<field name="code">5</field>
<field name="standard">as9100d</field>
<field name="category">leadership</field>
<field name="description" type="html"><p>Top-management leadership and commitment, quality policy, and organizational roles, responsibilities and authorities.</p></field>
</record>
<!-- ===== 6. PLANNING ===== -->
<record id="as9100_clause_6" model="fusion.plating.as9100.clause">
<field name="name">Planning</field>
<field name="code">6</field>
<field name="standard">as9100d</field>
<field name="category">planning</field>
<field name="description" type="html"><p>Actions to address risks and opportunities, quality objectives, and planning of changes.</p></field>
</record>
<!-- ===== 7. SUPPORT ===== -->
<record id="as9100_clause_7" model="fusion.plating.as9100.clause">
<field name="name">Support</field>
<field name="code">7</field>
<field name="standard">as9100d</field>
<field name="category">support</field>
<field name="description" type="html"><p>Resources, competence, awareness, communication, and documented information.</p></field>
</record>
<!-- ===== 8. OPERATION ===== -->
<record id="as9100_clause_8" model="fusion.plating.as9100.clause">
<field name="name">Operation</field>
<field name="code">8</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="description" type="html"><p>Operational planning and control of products and services, including design, external provision, production, release and nonconforming-output control.</p></field>
</record>
<record id="as9100_clause_8_1" model="fusion.plating.as9100.clause">
<field name="name">Operational planning and control</field>
<field name="code">8.1</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8"/>
<field name="description" type="html"><p>Planning, implementation and control of the processes needed to meet the requirements for the provision of products and services.</p></field>
</record>
<record id="as9100_clause_8_1_1" model="fusion.plating.as9100.clause">
<field name="name">Operational risk management</field>
<field name="code">8.1.1</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8_1"/>
<field name="description" type="html"><p>Establish, implement, and maintain a process for managing operational risks to the achievement of applicable requirements that includes assignment of responsibilities, definition of risk criteria, identification and assessment of risks, mitigation actions, and acceptance of residual risk.</p></field>
</record>
<record id="as9100_clause_8_1_2" model="fusion.plating.as9100.clause">
<field name="name">Configuration management</field>
<field name="code">8.1.2</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8_1"/>
<field name="description" type="html"><p>Establish a configuration-management process appropriate to the product with planning, identification, change control, status accounting and audits to ensure consistent product identification, traceability and control of physical and functional characteristics throughout the product life cycle.</p></field>
</record>
<record id="as9100_clause_8_1_3" model="fusion.plating.as9100.clause">
<field name="name">Product safety</field>
<field name="code">8.1.3</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8_1"/>
<field name="description" type="html"><p>Plan, implement and control the processes needed to assure product safety during the entire product life cycle, including hazard identification, risk assessment, management of identified risks, communication of risks to affected parties, and reporting and lessons learned.</p></field>
</record>
<record id="as9100_clause_8_1_4" model="fusion.plating.as9100.clause">
<field name="name">Prevention of counterfeit parts</field>
<field name="code">8.1.4</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8_1"/>
<field name="description" type="html"><p>Plan, implement and control processes, appropriate to the organization and the product, for the prevention of counterfeit or suspect-counterfeit part use and their inclusion in products delivered to the customer. Includes training, source controls, material and part verification, in-process control, quarantine, and reporting.</p></field>
</record>
<record id="as9100_clause_8_2" model="fusion.plating.as9100.clause">
<field name="name">Requirements for products and services</field>
<field name="code">8.2</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8"/>
<field name="description" type="html"><p>Customer communication, determination and review of requirements, and changes to requirements for products and services.</p></field>
</record>
<record id="as9100_clause_8_2_3_1" model="fusion.plating.as9100.clause">
<field name="name">Customer communication</field>
<field name="code">8.2.3.1</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8_2"/>
<field name="description" type="html"><p>Review of requirements for products and services — ensuring the organization's ability to meet requirements before committing to supply. Handling of enquiries, contracts or order handling, customer feedback including complaints, and specific requirements for contingency actions.</p></field>
</record>
<record id="as9100_clause_8_3" model="fusion.plating.as9100.clause">
<field name="name">Design and development of products and services</field>
<field name="code">8.3</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8"/>
<field name="description" type="html"><p>Design and development planning, inputs, controls, outputs and changes.</p></field>
</record>
<record id="as9100_clause_8_4" model="fusion.plating.as9100.clause">
<field name="name">Control of externally provided processes, products and services</field>
<field name="code">8.4</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8"/>
<field name="description" type="html"><p>Evaluation, selection, monitoring, and re-evaluation of external providers; type and extent of control; information for external providers.</p></field>
</record>
<record id="as9100_clause_8_5" model="fusion.plating.as9100.clause">
<field name="name">Production and service provision</field>
<field name="code">8.5</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8"/>
<field name="description" type="html"><p>Control of production and service provision, identification and traceability, property belonging to customers or external providers, preservation, post-delivery activities and control of changes.</p></field>
</record>
<record id="as9100_clause_8_5_1_3" model="fusion.plating.as9100.clause">
<field name="name">Post-delivery support</field>
<field name="code">8.5.1.3</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8_5"/>
<field name="description" type="html"><p>Determine and meet requirements for post-delivery activities associated with products and services, including collection and analysis of in-service data, actions to be taken as a result of product problems, control and updating of technical documentation, approval and control of repairs, and controls required for off-site work.</p></field>
</record>
<record id="as9100_clause_8_6" model="fusion.plating.as9100.clause">
<field name="name">Release of products and services</field>
<field name="code">8.6</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8"/>
<field name="description" type="html"><p>Implement planned arrangements, at appropriate stages, to verify that product and service requirements have been met, including First Article Inspection requirements.</p></field>
</record>
<record id="as9100_clause_8_7" model="fusion.plating.as9100.clause">
<field name="name">Control of nonconforming outputs</field>
<field name="code">8.7</field>
<field name="standard">as9100d</field>
<field name="category">operation</field>
<field name="parent_id" ref="as9100_clause_8"/>
<field name="description" type="html"><p>Ensure that nonconforming outputs are identified and controlled to prevent unintended use or delivery. Includes segregation, authority for disposition, and use of concessions.</p></field>
</record>
<!-- ===== 9. PERFORMANCE EVALUATION ===== -->
<record id="as9100_clause_9" model="fusion.plating.as9100.clause">
<field name="name">Performance evaluation</field>
<field name="code">9</field>
<field name="standard">as9100d</field>
<field name="category">performance</field>
<field name="description" type="html"><p>Monitoring, measurement, analysis and evaluation; internal audit; management review.</p></field>
</record>
<record id="as9100_clause_9_2" model="fusion.plating.as9100.clause">
<field name="name">Internal audit</field>
<field name="code">9.2</field>
<field name="standard">as9100d</field>
<field name="category">performance</field>
<field name="parent_id" ref="as9100_clause_9"/>
<field name="description" type="html"><p>Conduct internal audits at planned intervals to provide information on whether the QMS conforms to the organization's own requirements, the requirements of AS9100, and is effectively implemented and maintained.</p></field>
</record>
<record id="as9100_clause_9_3" model="fusion.plating.as9100.clause">
<field name="name">Management review</field>
<field name="code">9.3</field>
<field name="standard">as9100d</field>
<field name="category">performance</field>
<field name="parent_id" ref="as9100_clause_9"/>
<field name="description" type="html"><p>Top management reviews the organization's QMS at planned intervals to ensure its continuing suitability, adequacy, effectiveness and alignment with the strategic direction of the organization.</p></field>
</record>
<!-- ===== 10. IMPROVEMENT ===== -->
<record id="as9100_clause_10" model="fusion.plating.as9100.clause">
<field name="name">Improvement</field>
<field name="code">10</field>
<field name="standard">as9100d</field>
<field name="category">improvement</field>
<field name="description" type="html"><p>General; nonconformity and corrective action; continual improvement.</p></field>
</record>
<record id="as9100_clause_10_2" model="fusion.plating.as9100.clause">
<field name="name">Nonconformity and corrective action</field>
<field name="code">10.2</field>
<field name="standard">as9100d</field>
<field name="category">improvement</field>
<field name="parent_id" ref="as9100_clause_10"/>
<field name="description" type="html"><p>React to nonconformities, evaluate the need for action to eliminate the causes, implement any action needed, review the effectiveness of any corrective action taken, update risks and opportunities, and make changes to the QMS if necessary.</p></field>
</record>
<record id="as9100_clause_10_3" model="fusion.plating.as9100.clause">
<field name="name">Continual improvement</field>
<field name="code">10.3</field>
<field name="standard">as9100d</field>
<field name="category">improvement</field>
<field name="parent_id" ref="as9100_clause_10"/>
<field name="description" type="html"><p>Continually improve the suitability, adequacy, and effectiveness of the QMS, including considering outputs of analysis and evaluation, and management review.</p></field>
</record>
</odoo>

View File

@@ -1,151 +0,0 @@
<?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.
Seed aerospace industry specifications as fusion.plating.customer.spec
records with spec_type='industry'. All four process packs (EN, chrome,
anodize, black oxide) are listed as hard dependencies in the manifest
so the ``ref()`` calls below always resolve at install time.
-->
<odoo noupdate="1">
<!-- ===== AMS 2404 — Electroless Nickel Plating ===== -->
<record id="spec_ams_2404" model="fusion.plating.customer.spec">
<field name="code">AMS 2404</field>
<field name="name">Electroless Nickel Plating</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
<field name="x_fc_nadcap_required" eval="True"/>
<field name="x_fc_requires_first_article" eval="True"/>
<field name="process_type_ids" eval="[(6, 0, [
ref('fusion_plating_process_en.ptype_en_lp'),
ref('fusion_plating_process_en.ptype_en_mp'),
ref('fusion_plating_process_en.ptype_en_hp'),
])]"/>
</record>
<!-- ===== ASTM B733 — Autocatalytic (Electroless) Nickel-Phosphorus Coatings ===== -->
<record id="spec_astm_b733" model="fusion.plating.customer.spec">
<field name="code">ASTM B733</field>
<field name="name">Autocatalytic (Electroless) Nickel-Phosphorus Coatings</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
<field name="x_fc_requires_first_article" eval="True"/>
<field name="process_type_ids" eval="[(6, 0, [
ref('fusion_plating_process_en.ptype_en_lp'),
ref('fusion_plating_process_en.ptype_en_mp'),
ref('fusion_plating_process_en.ptype_en_hp'),
])]"/>
</record>
<!-- ===== MIL-C-26074 — Electroless Nickel Coatings ===== -->
<record id="spec_mil_c_26074" model="fusion.plating.customer.spec">
<field name="code">MIL-C-26074</field>
<field name="name">Electroless Nickel Coatings</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
<field name="x_fc_requires_first_article" eval="True"/>
<field name="process_type_ids" eval="[(6, 0, [
ref('fusion_plating_process_en.ptype_en_lp'),
ref('fusion_plating_process_en.ptype_en_mp'),
ref('fusion_plating_process_en.ptype_en_hp'),
])]"/>
</record>
<!-- ===== MIL-A-8625 — Anodic Coatings for Aluminum ===== -->
<record id="spec_mil_a_8625" model="fusion.plating.customer.spec">
<field name="code">MIL-A-8625</field>
<field name="name">Anodic Coatings for Aluminum and Aluminum Alloys</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
<field name="x_fc_nadcap_required" eval="True"/>
<field name="x_fc_requires_first_article" eval="True"/>
<field name="process_type_ids" eval="[(6, 0, [
ref('fusion_plating_process_anodize.ptype_an_type_i'),
ref('fusion_plating_process_anodize.ptype_an_type_ii'),
ref('fusion_plating_process_anodize.ptype_an_type_ii_dye'),
ref('fusion_plating_process_anodize.ptype_an_type_iii'),
])]"/>
</record>
<!-- ===== QQ-C-320 — Chromium Plating (Electrodeposited) ===== -->
<record id="spec_qq_c_320" model="fusion.plating.customer.spec">
<field name="code">QQ-C-320</field>
<field name="name">Chromium Plating (Electrodeposited)</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
<field name="x_fc_nadcap_required" eval="True"/>
<field name="x_fc_requires_first_article" eval="True"/>
<field name="process_type_ids" eval="[(6, 0, [
ref('fusion_plating_process_chrome.ptype_cr_hard_hex'),
ref('fusion_plating_process_chrome.ptype_cr_dec_hex'),
])]"/>
</record>
<!-- ===== MIL-DTL-13924 — Black Oxide Coating on Ferrous Metals ===== -->
<record id="spec_mil_dtl_13924" model="fusion.plating.customer.spec">
<field name="code">MIL-DTL-13924</field>
<field name="name">Black Oxide Coating on Ferrous Metals</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
<field name="process_type_ids" eval="[(6, 0, [
ref('fusion_plating_process_black_oxide.ptype_box_hot'),
ref('fusion_plating_process_black_oxide.ptype_box_mid'),
ref('fusion_plating_process_black_oxide.ptype_box_rt'),
])]"/>
</record>
<!-- ===== AMS 2700 — Passivation of Corrosion-Resistant Steels ===== -->
<record id="spec_ams_2700" model="fusion.plating.customer.spec">
<field name="code">AMS 2700</field>
<field name="name">Passivation of Corrosion-Resistant Steels</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
</record>
<!-- ===== AMS 2759/9 — Hydrogen Embrittlement Relief Bake ===== -->
<record id="spec_ams_2759" model="fusion.plating.customer.spec">
<field name="code">AMS 2759/9</field>
<field name="name">Hydrogen Embrittlement Relief (Baking) of Steel Parts</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
</record>
<!-- ===== AMS-QQ-P-416 — Cadmium Plating (Electrodeposited) ===== -->
<record id="spec_ams_qq_p_416" model="fusion.plating.customer.spec">
<field name="code">AMS-QQ-P-416</field>
<field name="name">Cadmium Plating (Electrodeposited)</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
<field name="x_fc_nadcap_required" eval="True"/>
<field name="x_fc_requires_first_article" eval="True"/>
</record>
<!-- ===== BAC 5709 — Boeing Electroless Nickel Plating ===== -->
<record id="spec_bac_5709" model="fusion.plating.customer.spec">
<field name="code">BAC 5709</field>
<field name="name">Boeing — Electroless Nickel Plating</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
<field name="x_fc_nadcap_required" eval="True"/>
<field name="x_fc_requires_first_article" eval="True"/>
<field name="x_fc_customer_approval_required" eval="True"/>
<field name="process_type_ids" eval="[(6, 0, [
ref('fusion_plating_process_en.ptype_en_mp'),
ref('fusion_plating_process_en.ptype_en_hp'),
])]"/>
</record>
<!-- ===== PRI AS7108 — Nadcap Chemical Processing Accreditation ===== -->
<record id="spec_pri_as7108" model="fusion.plating.customer.spec">
<field name="code">PRI AS7108</field>
<field name="name">Nadcap Chemical Processing Accreditation</field>
<field name="spec_type">industry</field>
<field name="x_fc_is_aerospace" eval="True"/>
<field name="x_fc_nadcap_required" eval="True"/>
</record>
</odoo>

View File

@@ -1,150 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 Nexa Systems Inc. — Demo data for Fusion Plating Aerospace -->
<odoo noupdate="1">
<!-- ════════════════════════════════════════════════════════════════
Nadcap AC7108 Audits
════════════════════════════════════════════════════════════════ -->
<!-- 1. Scheduled audit — electroless nickel checklist -->
<record id="demo_nadcap_audit_scheduled" model="fusion.plating.nadcap.audit">
<field name="name">NADCAP-DEMO-001</field>
<field name="audit_date" eval="(DateTime.today() + timedelta(days=45)).strftime('%Y-%m-%d')"/>
<field name="auditor_name">R. Patel</field>
<field name="pri_auditor" eval="True"/>
<field name="checklist">ac7108_10_electroless</field>
<field name="state">scheduled</field>
<field name="notes" type="html"><p>Scheduled electroless nickel slash-sheet audit for Q3 cycle.</p></field>
</record>
<!-- 2. In-progress audit — chrome plating checklist -->
<record id="demo_nadcap_audit_in_progress" model="fusion.plating.nadcap.audit">
<field name="name">NADCAP-DEMO-002</field>
<field name="audit_date" eval="(DateTime.today() - timedelta(days=3)).strftime('%Y-%m-%d')"/>
<field name="auditor_name">J. Moreno</field>
<field name="pri_auditor" eval="True"/>
<field name="checklist">ac7108_12_chrome</field>
<field name="state">in_progress</field>
<field name="notes" type="html"><p>Chrome plating audit currently underway. Day 2 of on-site review.</p></field>
</record>
<!-- 3. Closed audit — base checklist, accredited, 2 merits, 0 NCRs -->
<record id="demo_nadcap_audit_closed" model="fusion.plating.nadcap.audit">
<field name="name">NADCAP-DEMO-003</field>
<field name="audit_date" eval="(DateTime.today() - timedelta(days=120)).strftime('%Y-%m-%d')"/>
<field name="auditor_name">S. Whitfield</field>
<field name="pri_auditor" eval="True"/>
<field name="checklist">ac7108_base</field>
<field name="result">accredited</field>
<field name="merit_count">2</field>
<field name="ncr_count">0</field>
<field name="accreditation_start" eval="(DateTime.today() - timedelta(days=90)).strftime('%Y-%m-%d')"/>
<field name="accreditation_end" eval="(DateTime.today() + timedelta(days=640)).strftime('%Y-%m-%d')"/>
<field name="state">closed</field>
<field name="notes" type="html"><p>Base checklist audit completed with full accreditation. Two merit observations noted for SPC controls and operator training programme.</p></field>
</record>
<!-- ════════════════════════════════════════════════════════════════
Risk Register Entries
════════════════════════════════════════════════════════════════ -->
<!-- 1. Operational risk — identified -->
<record id="demo_risk_operational" model="fusion.plating.risk">
<field name="name">RISK-DEMO-001</field>
<field name="title">Tank temperature controller failure</field>
<field name="description" type="html"><p>Single-point failure risk on Line 2 EN tank — temperature controller is end-of-life with no redundancy.</p></field>
<field name="category">operational</field>
<field name="likelihood">3</field>
<field name="impact">4</field>
<field name="state">identified</field>
<field name="review_date" eval="(DateTime.today() + timedelta(days=30)).strftime('%Y-%m-%d')"/>
</record>
<!-- 2. Supply chain risk — assessed -->
<record id="demo_risk_supply_chain" model="fusion.plating.risk">
<field name="name">RISK-DEMO-002</field>
<field name="title">Single-source nickel sulphamate supply</field>
<field name="description" type="html"><p>Only one approved vendor for nickel sulphamate concentrate. Lead time has increased to 12 weeks.</p></field>
<field name="category">supply_chain</field>
<field name="likelihood">2</field>
<field name="impact">5</field>
<field name="state">assessed</field>
<field name="mitigation_plan" type="html"><p>Qualify secondary supplier (Atotech). Target completion by Q4.</p></field>
<field name="review_date" eval="(DateTime.today() + timedelta(days=60)).strftime('%Y-%m-%d')"/>
</record>
<!-- 3. Quality risk — treated -->
<record id="demo_risk_quality" model="fusion.plating.risk">
<field name="name">RISK-DEMO-003</field>
<field name="title">Thickness measurement repeatability</field>
<field name="description" type="html"><p>XRF gauge R&amp;R study showed marginal repeatability on thin deposits (&lt; 5 µm).</p></field>
<field name="category">quality</field>
<field name="likelihood">4</field>
<field name="impact">3</field>
<field name="state">treated</field>
<field name="mitigation_plan" type="html"><p>Calibrated with new certified reference standards. Operator training refresher completed. Re-running R&amp;R study next week.</p></field>
<field name="review_date" eval="(DateTime.today() + timedelta(days=14)).strftime('%Y-%m-%d')"/>
</record>
<!-- 4. Customer risk — monitored -->
<record id="demo_risk_customer" model="fusion.plating.risk">
<field name="name">RISK-DEMO-004</field>
<field name="title">Prime OEM contract renewal uncertainty</field>
<field name="description" type="html"><p>Largest aerospace customer contract expires in 6 months. RFQ response due next quarter.</p></field>
<field name="category">customer</field>
<field name="likelihood">2</field>
<field name="impact">2</field>
<field name="state">monitored</field>
<field name="mitigation_plan" type="html"><p>Account review meetings scheduled monthly. Preparing competitive pricing and capacity expansion proposal.</p></field>
<field name="review_date" eval="(DateTime.today() + timedelta(days=21)).strftime('%Y-%m-%d')"/>
</record>
<!-- ════════════════════════════════════════════════════════════════
Configuration Management Items
════════════════════════════════════════════════════════════════ -->
<!-- 1. EN plating procedure -->
<record id="demo_config_item_en" model="fusion.plating.config.item">
<field name="name">Electroless Nickel Plating Procedure</field>
<field name="code">CFG-EN-001</field>
<field name="baseline_revision">A</field>
<field name="current_revision">C</field>
<field name="approval_date" eval="(DateTime.today() - timedelta(days=60)).strftime('%Y-%m-%d')"/>
<field name="change_history" type="html">
<p><strong>Rev C</strong> — Updated rinse-water conductivity limits per customer spec.<br/>
<strong>Rev B</strong> — Added mid-coat thickness checkpoint.<br/>
<strong>Rev A</strong> — Initial baseline release.</p>
</field>
<field name="notes" type="html"><p>Covers AMS 2404 and MIL-C-26074 requirements for medium-phosphorus EN deposits.</p></field>
</record>
<!-- 2. Chrome plating procedure -->
<record id="demo_config_item_chrome" model="fusion.plating.config.item">
<field name="name">Chrome Plating Procedure</field>
<field name="code">CFG-CR-001</field>
<field name="baseline_revision">A</field>
<field name="current_revision">B</field>
<field name="approval_date" eval="(DateTime.today() - timedelta(days=30)).strftime('%Y-%m-%d')"/>
<field name="change_history" type="html">
<p><strong>Rev B</strong> — Revised current-density parameters for thin hard chrome.<br/>
<strong>Rev A</strong> — Initial baseline release.</p>
</field>
<field name="notes" type="html"><p>Covers QQ-C-320 and AMS 2460 requirements for hard chrome deposits on aerospace components.</p></field>
</record>
<!-- ════════════════════════════════════════════════════════════════
Counterfeit Parts Prevention
════════════════════════════════════════════════════════════════ -->
<record id="demo_counterfeit_investigation" model="fusion.plating.counterfeit.prevention">
<field name="name">CFT-DEMO-001</field>
<field name="incident_date" eval="(DateTime.today() - timedelta(days=10)).strftime('%Y-%m-%d')"/>
<field name="part_number">NAS1149-C0363A</field>
<field name="lot_serial">LOT-2026-04-A</field>
<field name="detection_method">Certificate of conformance discrepancy flagged during receiving inspection</field>
<field name="disposition">investigation</field>
<field name="gidep_reported" eval="False"/>
<field name="notes" type="html"><p>Supplier certificate references an outdated spec revision. Material quarantined pending metallurgical lab verification. GIDEP report to be filed if counterfeit is confirmed.</p></field>
</record>
</odoo>

View File

@@ -1,34 +0,0 @@
<?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_nadcap_audit" model="ir.sequence">
<field name="name">Fusion Plating: Nadcap Audit</field>
<field name="code">fusion.plating.nadcap.audit</field>
<field name="prefix">NADCAP/%(year)s/</field>
<field name="padding">3</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_fp_counterfeit" model="ir.sequence">
<field name="name">Fusion Plating: Counterfeit Prevention</field>
<field name="code">fusion.plating.counterfeit.prevention</field>
<field name="prefix">CFT/%(year)s/</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_fp_risk" model="ir.sequence">
<field name="name">Fusion Plating: Risk Register</field>
<field name="code">fusion.plating.risk</field>
<field name="prefix">RISK/%(year)s/</field>
<field name="padding">3</field>
<field name="company_id" eval="False"/>
</record>
</odoo>

View File

@@ -1,12 +0,0 @@
# -*- 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_as9100_clause
from . import fp_nadcap_audit
from . import fp_counterfeit_prevention
from . import fp_config_item
from . import fp_risk
from . import fp_customer_spec
from . import fp_fair

View File

@@ -1,96 +0,0 @@
# -*- 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 FpAs9100Clause(models.Model):
"""AS9100 Rev D clause catalog.
A flat catalogue of clauses and sub-clauses from the AS9100 Rev D
standard, plus related standards (ISO 9001:2015, etc.). Used by
customer specifications and audit findings to pin a requirement to
the specific paragraph of the standard it satisfies.
"""
_name = 'fusion.plating.as9100.clause'
_description = 'Fusion Plating — AS9100 Clause'
_order = 'standard, code, id'
_parent_store = True
_rec_name = 'display_name'
name = fields.Char(
string='Name',
required=True,
translate=True,
)
display_name = fields.Char(
compute='_compute_display_name',
store=True,
)
code = fields.Char(
string='Clause Code',
required=True,
help='Clause reference number, e.g. 8.1.2.',
)
parent_id = fields.Many2one(
'fusion.plating.as9100.clause',
string='Parent Clause',
ondelete='cascade',
index=True,
)
parent_path = fields.Char(index=True, unaccent=False)
child_ids = fields.One2many(
'fusion.plating.as9100.clause',
'parent_id',
string='Sub-clauses',
)
description = fields.Html(
string='Description',
translate=True,
)
standard = fields.Selection(
[
('as9100d', 'AS9100 Rev D'),
('iso9001_2015', 'ISO 9001:2015'),
('other', 'Other'),
],
string='Standard',
default='as9100d',
required=True,
)
category = fields.Selection(
[
('leadership', 'Leadership'),
('planning', 'Planning'),
('support', 'Support'),
('operation', 'Operation'),
('performance', 'Performance Evaluation'),
('improvement', 'Improvement'),
],
string='Category',
)
notes = fields.Html(
string='Notes',
translate=True,
)
active = fields.Boolean(default=True)
_sql_constraints = [
(
'fp_as9100_clause_code_std_uniq',
'unique(code, standard)',
'A clause code must be unique per standard.',
),
]
@api.depends('code', 'name')
def _compute_display_name(self):
for rec in self:
parts = []
if rec.code:
parts.append(rec.code)
if rec.name:
parts.append(rec.name)
rec.display_name = ''.join(parts) if parts else ''

View File

@@ -1,73 +0,0 @@
# -*- 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 FpConfigItem(models.Model):
"""Configuration management baseline item.
Tracks a configuration item (product, process, document, tooling,
etc.) under formal configuration management control per AS9100
§8.1.2. Each item has a baseline revision, the current in-use
revision, and a change history log for traceability.
"""
_name = 'fusion.plating.config.item'
_description = 'Fusion Plating — Configuration Management Item'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'name, id'
name = fields.Char(
string='Name',
required=True,
tracking=True,
)
code = fields.Char(
string='Code',
required=True,
tracking=True,
)
baseline_revision = fields.Char(
string='Baseline Revision',
tracking=True,
help='The frozen baseline revision this item starts from.',
)
current_revision = fields.Char(
string='Current Revision',
tracking=True,
help='The revision currently in effect — usually the most '
'recently approved change.',
)
approved_by_id = fields.Many2one(
'res.users',
string='Approved By',
tracking=True,
)
approval_date = fields.Date(
string='Approval Date',
tracking=True,
)
change_history = fields.Html(
string='Change History',
help='Manual log of baseline changes. Use dated entries to keep '
'full traceability for auditors.',
)
notes = fields.Html(
string='Notes',
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
active = fields.Boolean(default=True)
_sql_constraints = [
(
'fp_config_item_code_uniq',
'unique(code, company_id)',
'Configuration item codes must be unique per company.',
),
]

View File

@@ -1,87 +0,0 @@
# -*- 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 FpCounterfeitPrevention(models.Model):
"""Counterfeit parts prevention incident log.
Records an incident where a suspected or confirmed counterfeit /
fraudulent part, material, or raw stock was detected in the supply
chain. Required by AS9100 §8.1.4 Counterfeit Parts Prevention.
"""
_name = 'fusion.plating.counterfeit.prevention'
_description = 'Fusion Plating — Counterfeit Parts Prevention Log'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'incident_date desc, id desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self._default_name(),
tracking=True,
)
incident_date = fields.Date(
string='Incident Date',
default=lambda self: fields.Date.context_today(self),
tracking=True,
)
supplier_id = fields.Many2one(
'res.partner',
string='Supplier',
tracking=True,
)
part_number = fields.Char(
string='Part Number',
tracking=True,
)
lot_serial = fields.Char(
string='Lot / Serial',
tracking=True,
)
detection_method = fields.Char(
string='Detection Method',
help='How the counterfeit item was detected (receiving inspection, '
'lab test, certificate discrepancy, etc.).',
)
disposition = fields.Selection(
[
('returned', 'Returned to Supplier'),
('destroyed', 'Destroyed / Quarantined'),
('investigation', 'Under Investigation'),
],
string='Disposition',
default='investigation',
tracking=True,
)
gidep_reported = fields.Boolean(
string='GIDEP Reported',
tracking=True,
help='Reported to the Government Industry Data Exchange Program.',
)
notes = fields.Html(
string='Notes',
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
active = fields.Boolean(default=True)
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.counterfeit.prevention')
return seq or '/'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name') or vals.get('name') == '/':
vals['name'] = self._default_name()
return super().create(vals_list)

View File

@@ -1,54 +0,0 @@
# -*- 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 FpCustomerSpec(models.Model):
"""Aerospace extension to the quality customer-spec model.
Adds the small set of aerospace-specific flags and linkages the
quality module deliberately keeps out of its generic implementation.
"""
_inherit = 'fusion.plating.customer.spec'
x_fc_is_aerospace = fields.Boolean(
string='Aerospace Spec',
tracking=True,
help='Check to mark this specification as aerospace-relevant so '
'it shows up in aerospace filters and dashboards.',
)
x_fc_as9100_clause_ids = fields.Many2many(
'fusion.plating.as9100.clause',
'fp_customer_spec_as9100_clause_rel',
'spec_id',
'clause_id',
string='Related AS9100 Clauses',
help='AS9100 Rev D clauses this specification maps to.',
)
x_fc_nadcap_required = fields.Boolean(
string='Nadcap Required',
tracking=True,
help='This specification can only be satisfied by a Nadcap-accredited '
'supplier.',
)
x_fc_requires_first_article = fields.Boolean(
string='Requires First Article',
tracking=True,
help='Running this specification on a new part number or revision '
'requires a full FAIR on file.',
)
x_fc_pri_file_code = fields.Char(
string='PRI File Code',
tracking=True,
help='Performance Review Institute file code assigned to this '
'specification, when known.',
)
x_fc_customer_approval_required = fields.Boolean(
string='Customer Approval Required',
tracking=True,
help='Each job against this spec requires explicit customer '
'approval before shipping.',
)

View File

@@ -1,54 +0,0 @@
# -*- 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 FpFair(models.Model):
"""Aerospace extension to the quality FAIR model.
Adds the AS9102 form 1/2/3 attachment slots and the customer-approval
signature block that aerospace customers routinely require on top of
the generic first-article report.
"""
_inherit = 'fusion.plating.fair'
x_fc_as9102_form1 = fields.Binary(
string='AS9102 Form 1 (Part Accountability)',
help='Scanned / generated copy of AS9102 Form 1 — part '
'accountability header.',
)
x_fc_as9102_form1_filename = fields.Char(string='Form 1 Filename')
x_fc_as9102_form2 = fields.Binary(
string='AS9102 Form 2 (Product Accountability)',
help='Scanned / generated copy of AS9102 Form 2 — product '
'accountability, raw material / process / inspection records.',
)
x_fc_as9102_form2_filename = fields.Char(string='Form 2 Filename')
x_fc_as9102_form3 = fields.Binary(
string='AS9102 Form 3 (Characteristic Accountability)',
help='Scanned / generated copy of AS9102 Form 3 — characteristic '
'accountability, verification, and compatibility evaluation.',
)
x_fc_as9102_form3_filename = fields.Char(string='Form 3 Filename')
x_fc_drawing_revision = fields.Char(
string='Drawing Revision',
tracking=True,
help='Customer drawing revision this FAIR is against — must match '
'the revision shipped to the customer.',
)
x_fc_customer_approval_id = fields.Many2one(
'res.users',
string='Customer Approver',
tracking=True,
help='User who recorded the customer-side approval of this FAIR.',
)
x_fc_customer_approval_date = fields.Date(
string='Customer Approval Date',
tracking=True,
)

View File

@@ -1,132 +0,0 @@
# -*- 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 FpNadcapAudit(models.Model):
"""Nadcap AC7108 chemical-processing audit record.
Tracks a single Nadcap audit against AC7108 (base checklist) and its
slash sheets. This is deliberately kept separate from the generic
fusion.plating.audit model in the quality module because Nadcap has
its own accreditation lifecycle, PRI-assigned auditors, and
merit/NCR tracking that don't map cleanly to the generic audit flow.
"""
_name = 'fusion.plating.nadcap.audit'
_description = 'Fusion Plating — Nadcap AC7108 Audit'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'audit_date desc, id desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self._default_name(),
tracking=True,
)
audit_date = fields.Date(
string='Audit Date',
tracking=True,
)
auditor_name = fields.Char(
string='Auditor',
tracking=True,
help='Name of the individual auditor performing the assessment.',
)
pri_auditor = fields.Boolean(
string='PRI-Assigned Auditor',
tracking=True,
help='Checked when the auditor was assigned by the Performance '
'Review Institute rather than a self-nomination.',
)
checklist = fields.Selection(
[
('ac7108_base', 'AC7108 — Base Checklist'),
('ac7108_10_electroless', 'AC7108/10 — Electroless Nickel'),
('ac7108_11_brush', 'AC7108/11 — Brush Plating'),
('ac7108_12_chrome', 'AC7108/12 — Chromium Plating'),
('ac7108_13_anodize', 'AC7108/13 — Anodize'),
('ac7108_14_conv', 'AC7108/14 — Conversion Coating'),
],
string='Checklist',
default='ac7108_base',
required=True,
tracking=True,
)
result = fields.Selection(
[
('accredited', 'Accredited'),
('conditional', 'Conditional'),
('failed', 'Failed'),
],
string='Result',
tracking=True,
)
merit_count = fields.Integer(
string='Merits',
help='Count of merit-worthy observations recorded during the '
'audit (positive findings).',
)
ncr_count = fields.Integer(
string='NCRs',
help='Non-conformance reports raised during the audit.',
)
audit_report_attachment = fields.Many2one(
'ir.attachment',
string='Audit Report',
help='Final audit report PDF.',
)
accreditation_start = fields.Date(
string='Accreditation Start',
tracking=True,
)
accreditation_end = fields.Date(
string='Accreditation End',
tracking=True,
)
state = fields.Selection(
[
('scheduled', 'Scheduled'),
('in_progress', 'In Progress'),
('report_issued', 'Report Issued'),
('closed', 'Closed'),
],
string='Status',
default='scheduled',
required=True,
tracking=True,
)
notes = fields.Html(
string='Notes',
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
active = fields.Boolean(default=True)
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.nadcap.audit')
return seq or '/'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name') or vals.get('name') == '/':
vals['name'] = self._default_name()
return super().create(vals_list)
def action_start(self):
self.write({'state': 'in_progress'})
def action_issue_report(self):
self.write({'state': 'report_issued'})
def action_close(self):
self.write({'state': 'closed'})

View File

@@ -1,176 +0,0 @@
# -*- 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 FpRisk(models.Model):
"""Risk register entry.
Implements the operational risk management requirement from AS9100
Rev D §8.1.1. Each risk gets scored on a 15 likelihood and 15
impact scale; the multiplicative score drives the level classification
(low / medium / high / critical) used for escalation and reporting.
"""
_name = 'fusion.plating.risk'
_description = 'Fusion Plating — Risk Register Entry'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'risk_score desc, id desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self._default_name(),
tracking=True,
)
title = fields.Char(
string='Title',
required=True,
tracking=True,
)
description = fields.Html(
string='Description',
)
category = fields.Selection(
[
('operational', 'Operational'),
('supply_chain', 'Supply Chain'),
('quality', 'Quality'),
('safety', 'Safety'),
('environmental', 'Environmental'),
('financial', 'Financial'),
('customer', 'Customer'),
('regulatory', 'Regulatory'),
],
string='Category',
default='operational',
required=True,
tracking=True,
)
likelihood = fields.Selection(
[
('1', '1 — Very Low'),
('2', '2 — Low'),
('3', '3 — Medium'),
('4', '4 — High'),
('5', '5 — Very High'),
],
string='Likelihood',
default='3',
required=True,
tracking=True,
)
impact = fields.Selection(
[
('1', '1 — Very Low'),
('2', '2 — Low'),
('3', '3 — Medium'),
('4', '4 — High'),
('5', '5 — Very High'),
],
string='Impact',
default='3',
required=True,
tracking=True,
)
risk_score = fields.Integer(
string='Score',
compute='_compute_risk_score',
store=True,
help='Likelihood × Impact (125).',
)
risk_level = fields.Selection(
[
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
('critical', 'Critical'),
],
string='Level',
compute='_compute_risk_level',
store=True,
tracking=True,
)
mitigation_plan = fields.Html(
string='Mitigation Plan',
)
owner_id = fields.Many2one(
'res.users',
string='Owner',
default=lambda self: self.env.user,
tracking=True,
)
review_date = fields.Date(
string='Next Review',
tracking=True,
)
state = fields.Selection(
[
('identified', 'Identified'),
('assessed', 'Assessed'),
('treated', 'Treated'),
('monitored', 'Monitored'),
('closed', 'Closed'),
],
string='Status',
default='identified',
required=True,
tracking=True,
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
active = fields.Boolean(default=True)
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.risk')
return seq or '/'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name') or vals.get('name') == '/':
vals['name'] = self._default_name()
return super().create(vals_list)
@api.depends('likelihood', 'impact')
def _compute_risk_score(self):
for rec in self:
try:
likely = int(rec.likelihood or 0)
impact = int(rec.impact or 0)
except (TypeError, ValueError):
likely, impact = 0, 0
rec.risk_score = likely * impact
@api.depends('risk_score')
def _compute_risk_level(self):
for rec in self:
score = rec.risk_score or 0
if score >= 20:
rec.risk_level = 'critical'
elif score >= 12:
rec.risk_level = 'high'
elif score >= 6:
rec.risk_level = 'medium'
else:
rec.risk_level = 'low'
def action_assess(self):
self.write({'state': 'assessed'})
def action_treat(self):
self.write({'state': 'treated'})
def action_monitor(self):
self.write({'state': 'monitored'})
def action_close(self):
self.write({'state': 'closed'})

View File

@@ -1,23 +0,0 @@
<?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>
<!--
This module reuses the core groups from fusion_plating:
fusion_plating.group_fusion_plating_operator
fusion_plating.group_fusion_plating_supervisor
fusion_plating.group_fusion_plating_manager
fusion_plating.group_fusion_plating_admin
No new res.groups records are introduced here. All access control
is expressed in security/ir.model.access.csv via those existing
groups, so a single user role works across the core, the QMS and
the aerospace pack.
-->
</odoo>

View File

@@ -1,16 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_fp_as9100_clause_operator,fp.as9100.clause.operator,model_fusion_plating_as9100_clause,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_as9100_clause_supervisor,fp.as9100.clause.supervisor,model_fusion_plating_as9100_clause,fusion_plating.group_fusion_plating_supervisor,1,0,0,0
access_fp_as9100_clause_manager,fp.as9100.clause.manager,model_fusion_plating_as9100_clause,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_nadcap_audit_operator,fp.nadcap.audit.operator,model_fusion_plating_nadcap_audit,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_nadcap_audit_supervisor,fp.nadcap.audit.supervisor,model_fusion_plating_nadcap_audit,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_nadcap_audit_manager,fp.nadcap.audit.manager,model_fusion_plating_nadcap_audit,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_counterfeit_operator,fp.counterfeit.operator,model_fusion_plating_counterfeit_prevention,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_counterfeit_supervisor,fp.counterfeit.supervisor,model_fusion_plating_counterfeit_prevention,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_counterfeit_manager,fp.counterfeit.manager,model_fusion_plating_counterfeit_prevention,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_config_item_operator,fp.config.item.operator,model_fusion_plating_config_item,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_config_item_supervisor,fp.config.item.supervisor,model_fusion_plating_config_item,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_config_item_manager,fp.config.item.manager,model_fusion_plating_config_item,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_risk_operator,fp.risk.operator,model_fusion_plating_risk,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_risk_supervisor,fp.risk.supervisor,model_fusion_plating_risk,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_risk_manager,fp.risk.manager,model_fusion_plating_risk,fusion_plating.group_fusion_plating_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fp_as9100_clause_operator fp.as9100.clause.operator model_fusion_plating_as9100_clause fusion_plating.group_fusion_plating_operator 1 0 0 0
3 access_fp_as9100_clause_supervisor fp.as9100.clause.supervisor model_fusion_plating_as9100_clause fusion_plating.group_fusion_plating_supervisor 1 0 0 0
4 access_fp_as9100_clause_manager fp.as9100.clause.manager model_fusion_plating_as9100_clause fusion_plating.group_fusion_plating_manager 1 1 1 1
5 access_fp_nadcap_audit_operator fp.nadcap.audit.operator model_fusion_plating_nadcap_audit fusion_plating.group_fusion_plating_operator 1 0 0 0
6 access_fp_nadcap_audit_supervisor fp.nadcap.audit.supervisor model_fusion_plating_nadcap_audit fusion_plating.group_fusion_plating_supervisor 1 1 1 0
7 access_fp_nadcap_audit_manager fp.nadcap.audit.manager model_fusion_plating_nadcap_audit fusion_plating.group_fusion_plating_manager 1 1 1 1
8 access_fp_counterfeit_operator fp.counterfeit.operator model_fusion_plating_counterfeit_prevention fusion_plating.group_fusion_plating_operator 1 0 0 0
9 access_fp_counterfeit_supervisor fp.counterfeit.supervisor model_fusion_plating_counterfeit_prevention fusion_plating.group_fusion_plating_supervisor 1 1 1 0
10 access_fp_counterfeit_manager fp.counterfeit.manager model_fusion_plating_counterfeit_prevention fusion_plating.group_fusion_plating_manager 1 1 1 1
11 access_fp_config_item_operator fp.config.item.operator model_fusion_plating_config_item fusion_plating.group_fusion_plating_operator 1 0 0 0
12 access_fp_config_item_supervisor fp.config.item.supervisor model_fusion_plating_config_item fusion_plating.group_fusion_plating_supervisor 1 1 1 0
13 access_fp_config_item_manager fp.config.item.manager model_fusion_plating_config_item fusion_plating.group_fusion_plating_manager 1 1 1 1
14 access_fp_risk_operator fp.risk.operator model_fusion_plating_risk fusion_plating.group_fusion_plating_operator 1 0 0 0
15 access_fp_risk_supervisor fp.risk.supervisor model_fusion_plating_risk fusion_plating.group_fusion_plating_supervisor 1 1 1 0
16 access_fp_risk_manager fp.risk.manager model_fusion_plating_risk fusion_plating.group_fusion_plating_manager 1 1 1 1

View File

@@ -1,109 +0,0 @@
// =============================================================================
// Fusion Plating — Aerospace pack styles
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
// THEME AWARENESS
// ---------------
// This file uses ONLY Bootstrap / Odoo CSS custom properties so it renders
// correctly in both light and dark mode:
//
// background: var(--bs-body-bg)
// surface: var(--o-view-background-color)
// foreground: var(--bs-body-color)
// muted text: var(--bs-secondary-color)
// border: var(--bs-border-color)
// brand: var(--o-action)
//
// Semantic status colours are tinted via `color-mix()` against the Bootstrap
// theme tokens so a green badge adapts automatically between light and dark.
// =============================================================================
// -----------------------------------------------------------------------------
// Local helper — tint a semantic colour against the surface.
// -----------------------------------------------------------------------------
@mixin fp-aero-tint($color-var, $amount: 12%) {
background-color: color-mix(in srgb, var(#{$color-var}) #{$amount}, transparent);
color: var(#{$color-var});
border: 1px solid color-mix(in srgb, var(#{$color-var}) 35%, transparent);
}
// -----------------------------------------------------------------------------
// Risk register card — severity-tinted callout on the risk form
// -----------------------------------------------------------------------------
.o_fp_risk_card {
background-color: var(--o-view-background-color, var(--bs-body-bg));
color: var(--bs-body-color);
border: 1px solid var(--bs-border-color);
border-left-width: 4px;
border-radius: 8px;
padding: 12px 14px;
margin-bottom: 12px;
transition: border-color 120ms ease, box-shadow 120ms ease;
&[data-level="low"] {
border-left-color: var(--bs-success);
background-color: color-mix(in srgb, var(--bs-success) 6%, transparent);
}
&[data-level="medium"] {
border-left-color: var(--bs-info, var(--o-action));
background-color: color-mix(in srgb, var(--bs-info, var(--o-action)) 6%, transparent);
}
&[data-level="high"] {
border-left-color: var(--bs-warning);
background-color: color-mix(in srgb, var(--bs-warning) 8%, transparent);
}
&[data-level="critical"] {
border-left-color: var(--bs-danger);
background-color: color-mix(in srgb, var(--bs-danger) 10%, transparent);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--bs-danger) 20%, transparent);
}
}
// -----------------------------------------------------------------------------
// Nadcap badge — compact accreditation indicator on the audit form header
// -----------------------------------------------------------------------------
.o_fp_nadcap_badge {
display: inline-block;
padding: 3px 10px;
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
border-radius: 999px;
@include fp-aero-tint(--o-action);
}
// -----------------------------------------------------------------------------
// AS9100 clause tree — subtle hierarchy cueing for nested clause codes
// -----------------------------------------------------------------------------
.o_fp_as9100_tree {
.o_data_row {
// Tighten row padding a touch so long clause names stay legible
// without dominating the screen.
--fp-aero-row-pad: 6px;
> td {
padding-top: var(--fp-aero-row-pad);
padding-bottom: var(--fp-aero-row-pad);
}
}
// Monospace the clause code column for easy scanning of 8.1.2.3 style ids.
td[name="code"] {
font-family: var(--bs-font-monospace, monospace);
color: var(--bs-body-color);
}
// Muted parent column — it's context, not the focus.
td[name="parent_id"] {
color: var(--bs-secondary-color);
}
}

View File

@@ -1,93 +0,0 @@
<?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="view_fp_as9100_clause_list" model="ir.ui.view">
<field name="name">fp.as9100.clause.list</field>
<field name="model">fusion.plating.as9100.clause</field>
<field name="arch" type="xml">
<list string="AS9100 Clauses" class="o_fp_as9100_tree">
<field name="code"/>
<field name="name"/>
<field name="standard" widget="badge"/>
<field name="category"/>
<field name="parent_id"/>
</list>
</field>
</record>
<record id="view_fp_as9100_clause_form" model="ir.ui.view">
<field name="name">fp.as9100.clause.form</field>
<field name="model">fusion.plating.as9100.clause</field>
<field name="arch" type="xml">
<form string="AS9100 Clause">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="code"/>
<field name="standard"/>
<field name="category"/>
</group>
<group>
<field name="parent_id"/>
<field name="active"/>
</group>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="Sub-clauses">
<field name="child_ids">
<list>
<field name="code"/>
<field name="name"/>
</list>
</field>
</page>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_fp_as9100_clause_search" model="ir.ui.view">
<field name="name">fp.as9100.clause.search</field>
<field name="model">fusion.plating.as9100.clause</field>
<field name="arch" type="xml">
<search string="AS9100 Clauses">
<field name="code"/>
<field name="name"/>
<separator/>
<filter string="AS9100D" name="as9100d" domain="[('standard','=','as9100d')]"/>
<filter string="ISO 9001:2015" name="iso9001" domain="[('standard','=','iso9001_2015')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Standard" name="group_standard" context="{'group_by':'standard'}"/>
<filter string="Category" name="group_category" context="{'group_by':'category'}"/>
<filter string="Parent" name="group_parent" context="{'group_by':'parent_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_as9100_clause" model="ir.actions.act_window">
<field name="name">AS9100 Clauses</field>
<field name="res_model">fusion.plating.as9100.clause</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_as9100_clause_search"/>
</record>
</odoo>

View File

@@ -1,84 +0,0 @@
<?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="view_fp_config_item_list" model="ir.ui.view">
<field name="name">fp.config.item.list</field>
<field name="model">fusion.plating.config.item</field>
<field name="arch" type="xml">
<list string="Configuration Items">
<field name="code"/>
<field name="name"/>
<field name="baseline_revision"/>
<field name="current_revision"/>
<field name="approved_by_id"/>
<field name="approval_date"/>
</list>
</field>
</record>
<record id="view_fp_config_item_form" model="ir.ui.view">
<field name="name">fp.config.item.form</field>
<field name="model">fusion.plating.config.item</field>
<field name="arch" type="xml">
<form string="Configuration Item">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="code"/>
<field name="baseline_revision"/>
<field name="current_revision"/>
</group>
<group>
<field name="approved_by_id"/>
<field name="approval_date"/>
<field name="active"/>
</group>
</group>
<notebook>
<page string="Change History">
<field name="change_history"/>
</page>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_config_item_search" model="ir.ui.view">
<field name="name">fp.config.item.search</field>
<field name="model">fusion.plating.config.item</field>
<field name="arch" type="xml">
<search string="Configuration Items">
<field name="code"/>
<field name="name"/>
<field name="approved_by_id"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Approver" name="group_approver" context="{'group_by':'approved_by_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_config_item" model="ir.actions.act_window">
<field name="name">Configuration Items</field>
<field name="res_model">fusion.plating.config.item</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_config_item_search"/>
</record>
</odoo>

View File

@@ -1,92 +0,0 @@
<?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="view_fp_counterfeit_list" model="ir.ui.view">
<field name="name">fp.counterfeit.list</field>
<field name="model">fusion.plating.counterfeit.prevention</field>
<field name="arch" type="xml">
<list string="Counterfeit Parts Log">
<field name="name"/>
<field name="incident_date"/>
<field name="supplier_id"/>
<field name="part_number"/>
<field name="lot_serial"/>
<field name="disposition" widget="badge"
decoration-warning="disposition == 'investigation'"
decoration-danger="disposition == 'destroyed'"
decoration-info="disposition == 'returned'"/>
<field name="gidep_reported" widget="boolean"/>
</list>
</field>
</record>
<record id="view_fp_counterfeit_form" model="ir.ui.view">
<field name="name">fp.counterfeit.form</field>
<field name="model">fusion.plating.counterfeit.prevention</field>
<field name="arch" type="xml">
<form string="Counterfeit Prevention Log">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" readonly="1"/></h1>
</div>
<group>
<group>
<field name="incident_date"/>
<field name="supplier_id"/>
<field name="part_number"/>
<field name="lot_serial"/>
</group>
<group>
<field name="detection_method"/>
<field name="disposition"/>
<field name="gidep_reported"/>
</group>
</group>
<notebook>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_counterfeit_search" model="ir.ui.view">
<field name="name">fp.counterfeit.search</field>
<field name="model">fusion.plating.counterfeit.prevention</field>
<field name="arch" type="xml">
<search string="Counterfeit Incidents">
<field name="name"/>
<field name="supplier_id"/>
<field name="part_number"/>
<separator/>
<filter string="Under Investigation" name="investigation" domain="[('disposition','=','investigation')]"/>
<filter string="Returned" name="returned" domain="[('disposition','=','returned')]"/>
<filter string="Destroyed" name="destroyed" domain="[('disposition','=','destroyed')]"/>
<separator/>
<filter string="GIDEP Reported" name="gidep" domain="[('gidep_reported','=',True)]"/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Supplier" name="group_supplier" context="{'group_by':'supplier_id'}"/>
<filter string="Disposition" name="group_disposition" context="{'group_by':'disposition'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_counterfeit" model="ir.actions.act_window">
<field name="name">Counterfeit Parts Log</field>
<field name="res_model">fusion.plating.counterfeit.prevention</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_counterfeit_search"/>
</record>
</odoo>

View File

@@ -1,70 +0,0 @@
<?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>
<!--
Extend the quality customer-spec form with the aerospace flag
block. Sits below the existing "Applicable Processes" group so
aerospace-only users can flip the flag + link clauses without
touching the base metadata.
-->
<record id="view_fp_customer_spec_form_inherit_aerospace" model="ir.ui.view">
<field name="name">fp.customer.spec.form.inherit.aerospace</field>
<field name="model">fusion.plating.customer.spec</field>
<field name="inherit_id" ref="fusion_plating_quality.view_fp_customer_spec_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='applicable_processes']" position="after">
<group string="Aerospace" name="aerospace">
<group>
<field name="x_fc_is_aerospace"/>
<field name="x_fc_nadcap_required"/>
<field name="x_fc_requires_first_article"/>
</group>
<group>
<field name="x_fc_pri_file_code"/>
<field name="x_fc_customer_approval_required"/>
</group>
</group>
<group string="AS9100 Clauses" name="as9100_clauses">
<field name="x_fc_as9100_clause_ids" widget="many2many_tags" nolabel="1"/>
</group>
</xpath>
</field>
</record>
<!-- Extend the list with a tiny aerospace indicator column. -->
<record id="view_fp_customer_spec_list_inherit_aerospace" model="ir.ui.view">
<field name="name">fp.customer.spec.list.inherit.aerospace</field>
<field name="model">fusion.plating.customer.spec</field>
<field name="inherit_id" ref="fusion_plating_quality.view_fp_customer_spec_list"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='spec_type']" position="after">
<field name="x_fc_is_aerospace" string="Aero" optional="show"/>
<field name="x_fc_nadcap_required" string="Nadcap" optional="hide"/>
</xpath>
</field>
</record>
<!-- Add aerospace filters to the spec search view. -->
<record id="view_fp_customer_spec_search_inherit_aerospace" model="ir.ui.view">
<field name="name">fp.customer.spec.search.inherit.aerospace</field>
<field name="model">fusion.plating.customer.spec</field>
<field name="inherit_id" ref="fusion_plating_quality.view_fp_customer_spec_search"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='internal']" position="after">
<separator/>
<filter string="Aerospace" name="aerospace"
domain="[('x_fc_is_aerospace','=',True)]"/>
<filter string="Nadcap Required" name="nadcap"
domain="[('x_fc_nadcap_required','=',True)]"/>
<filter string="FAI Required" name="fai_required"
domain="[('x_fc_requires_first_article','=',True)]"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,62 +0,0 @@
<?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>
<!--
Extend the quality FAIR form with the AS9102 form attachments
and customer-approval signature block. Added as a notebook page
so the base layout is untouched.
-->
<record id="view_fp_fair_form_inherit_aerospace" model="ir.ui.view">
<field name="name">fp.fair.form.inherit.aerospace</field>
<field name="model">fusion.plating.fair</field>
<field name="inherit_id" ref="fusion_plating_quality.view_fp_fair_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="AS9102 Forms" name="as9102_forms">
<group>
<group string="Form 1 — Part Accountability">
<field name="x_fc_as9102_form1"
filename="x_fc_as9102_form1_filename"/>
<field name="x_fc_as9102_form1_filename" invisible="1"/>
</group>
<group string="Form 2 — Product Accountability">
<field name="x_fc_as9102_form2"
filename="x_fc_as9102_form2_filename"/>
<field name="x_fc_as9102_form2_filename" invisible="1"/>
</group>
</group>
<group>
<group string="Form 3 — Characteristic Accountability">
<field name="x_fc_as9102_form3"
filename="x_fc_as9102_form3_filename"/>
<field name="x_fc_as9102_form3_filename" invisible="1"/>
</group>
<group string="Drawing &amp; Customer Approval">
<field name="x_fc_drawing_revision"/>
<field name="x_fc_customer_approval_id"/>
<field name="x_fc_customer_approval_date"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
<!-- Drawing revision column on the list for quick scanning. -->
<record id="view_fp_fair_list_inherit_aerospace" model="ir.ui.view">
<field name="name">fp.fair.list.inherit.aerospace</field>
<field name="model">fusion.plating.fair</field>
<field name="inherit_id" ref="fusion_plating_quality.view_fp_fair_list"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='part_revision']" position="after">
<field name="x_fc_drawing_revision" string="Dwg Rev" optional="show"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,46 +0,0 @@
<?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>
<!-- ===== AEROSPACE (parent submenu under the Plating app) ===== -->
<menuitem id="menu_fp_aerospace"
name="Aerospace"
parent="fusion_plating.menu_fp_root"
sequence="60"
groups="fusion_plating.group_fusion_plating_operator"/>
<menuitem id="menu_fp_aerospace_as9100"
name="AS9100 Clauses"
parent="menu_fp_aerospace"
action="action_fp_as9100_clause"
sequence="10"/>
<menuitem id="menu_fp_aerospace_nadcap"
name="Nadcap Audits"
parent="menu_fp_aerospace"
action="action_fp_nadcap_audit"
sequence="20"/>
<menuitem id="menu_fp_aerospace_counterfeit"
name="Counterfeit Log"
parent="menu_fp_aerospace"
action="action_fp_counterfeit"
sequence="30"/>
<menuitem id="menu_fp_aerospace_config_items"
name="Configuration Items"
parent="menu_fp_aerospace"
action="action_fp_config_item"
sequence="40"/>
<menuitem id="menu_fp_aerospace_risk"
name="Risk Register"
parent="menu_fp_aerospace"
action="action_fp_risk"
sequence="50"/>
</odoo>

View File

@@ -1,125 +0,0 @@
<?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="view_fp_nadcap_audit_list" model="ir.ui.view">
<field name="name">fp.nadcap.audit.list</field>
<field name="model">fusion.plating.nadcap.audit</field>
<field name="arch" type="xml">
<list string="Nadcap Audits"
decoration-success="result == 'accredited'"
decoration-warning="result == 'conditional'"
decoration-danger="result == 'failed'">
<field name="name"/>
<field name="audit_date"/>
<field name="checklist"/>
<field name="auditor_name"/>
<field name="pri_auditor" widget="boolean"/>
<field name="merit_count"/>
<field name="ncr_count"/>
<field name="accreditation_end"/>
<field name="result" widget="badge"
decoration-success="result == 'accredited'"
decoration-warning="result == 'conditional'"
decoration-danger="result == 'failed'"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
<record id="view_fp_nadcap_audit_form" model="ir.ui.view">
<field name="name">fp.nadcap.audit.form</field>
<field name="model">fusion.plating.nadcap.audit</field>
<field name="arch" type="xml">
<form string="Nadcap Audit">
<header>
<button name="action_start" string="Start Audit" type="object"
class="oe_highlight" invisible="state != 'scheduled'"/>
<button name="action_issue_report" string="Issue Report" type="object"
class="oe_highlight" invisible="state != 'in_progress'"/>
<button name="action_close" string="Close" type="object"
invisible="state != 'report_issued'"/>
<field name="state" widget="statusbar"
statusbar_visible="scheduled,in_progress,report_issued,closed"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" readonly="1"/></h1>
</div>
<div class="d-flex gap-2 mb-2" invisible="not checklist">
<span class="o_fp_nadcap_badge">
<field name="checklist"/>
</span>
</div>
<group>
<group>
<field name="audit_date"/>
<field name="auditor_name"/>
<field name="pri_auditor"/>
<field name="checklist"/>
</group>
<group>
<field name="result"/>
<field name="merit_count"/>
<field name="ncr_count"/>
<field name="audit_report_attachment"/>
</group>
</group>
<group string="Accreditation">
<group>
<field name="accreditation_start"/>
</group>
<group>
<field name="accreditation_end"/>
</group>
</group>
<notebook>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_nadcap_audit_search" model="ir.ui.view">
<field name="name">fp.nadcap.audit.search</field>
<field name="model">fusion.plating.nadcap.audit</field>
<field name="arch" type="xml">
<search string="Nadcap Audits">
<field name="name"/>
<field name="auditor_name"/>
<separator/>
<filter string="Scheduled" name="scheduled" domain="[('state','=','scheduled')]"/>
<filter string="In Progress" name="in_progress" domain="[('state','=','in_progress')]"/>
<filter string="Closed" name="closed" domain="[('state','=','closed')]"/>
<separator/>
<filter string="Accredited" name="accredited" domain="[('result','=','accredited')]"/>
<filter string="Failed" name="failed" domain="[('result','=','failed')]"/>
<separator/>
<filter string="PRI Auditor" name="pri_auditor" domain="[('pri_auditor','=',True)]"/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Status" name="group_state" context="{'group_by':'state'}"/>
<filter string="Checklist" name="group_checklist" context="{'group_by':'checklist'}"/>
<filter string="Result" name="group_result" context="{'group_by':'result'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_nadcap_audit" model="ir.actions.act_window">
<field name="name">Nadcap Audits</field>
<field name="res_model">fusion.plating.nadcap.audit</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_nadcap_audit_search"/>
</record>
</odoo>

View File

@@ -1,127 +0,0 @@
<?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="view_fp_risk_list" model="ir.ui.view">
<field name="name">fp.risk.list</field>
<field name="model">fusion.plating.risk</field>
<field name="arch" type="xml">
<list string="Risk Register"
decoration-success="risk_level == 'low'"
decoration-info="risk_level == 'medium'"
decoration-warning="risk_level == 'high'"
decoration-danger="risk_level == 'critical'">
<field name="name"/>
<field name="title"/>
<field name="category"/>
<field name="likelihood"/>
<field name="impact"/>
<field name="risk_score"/>
<field name="risk_level" widget="badge"
decoration-success="risk_level == 'low'"
decoration-info="risk_level == 'medium'"
decoration-warning="risk_level == 'high'"
decoration-danger="risk_level == 'critical'"/>
<field name="owner_id"/>
<field name="review_date"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
<record id="view_fp_risk_form" model="ir.ui.view">
<field name="name">fp.risk.form</field>
<field name="model">fusion.plating.risk</field>
<field name="arch" type="xml">
<form string="Risk">
<header>
<button name="action_assess" string="Assess" type="object"
class="oe_highlight" invisible="state != 'identified'"/>
<button name="action_treat" string="Treat" type="object"
class="oe_highlight" invisible="state != 'assessed'"/>
<button name="action_monitor" string="Monitor" type="object"
invisible="state != 'treated'"/>
<button name="action_close" string="Close" type="object"
invisible="state not in ('monitored','treated')"/>
<field name="state" widget="statusbar"
statusbar_visible="identified,assessed,treated,monitored,closed"/>
</header>
<sheet>
<div class="oe_title">
<label for="title"/>
<h1><field name="title" placeholder="Short risk title..."/></h1>
<div class="text-muted"><field name="name" readonly="1"/></div>
</div>
<div class="o_fp_risk_card">
<group>
<group>
<field name="category"/>
<field name="likelihood"/>
<field name="impact"/>
</group>
<group>
<field name="risk_score"/>
<field name="risk_level" widget="badge"
decoration-success="risk_level == 'low'"
decoration-info="risk_level == 'medium'"
decoration-warning="risk_level == 'high'"
decoration-danger="risk_level == 'critical'"/>
<field name="owner_id"/>
<field name="review_date"/>
</group>
</group>
</div>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="Mitigation Plan">
<field name="mitigation_plan"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_risk_search" model="ir.ui.view">
<field name="name">fp.risk.search</field>
<field name="model">fusion.plating.risk</field>
<field name="arch" type="xml">
<search string="Risks">
<field name="name"/>
<field name="title"/>
<field name="owner_id"/>
<separator/>
<filter string="Critical" name="critical" domain="[('risk_level','=','critical')]"/>
<filter string="High" name="high" domain="[('risk_level','=','high')]"/>
<filter string="Medium" name="medium" domain="[('risk_level','=','medium')]"/>
<filter string="Low" name="low" domain="[('risk_level','=','low')]"/>
<separator/>
<filter string="Open" name="open" domain="[('state','not in',['closed'])]"/>
<filter string="Closed" name="closed" domain="[('state','=','closed')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Level" name="group_level" context="{'group_by':'risk_level'}"/>
<filter string="Category" name="group_category" context="{'group_by':'category'}"/>
<filter string="Owner" name="group_owner" context="{'group_by':'owner_id'}"/>
<filter string="Status" name="group_state" context="{'group_by':'state'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_risk" model="ir.actions.act_window">
<field name="name">Risk Register</field>
<field name="res_model">fusion.plating.risk</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_risk_search"/>
</record>
</odoo>

Binary file not shown.

View File

@@ -1,6 +0,0 @@
# -*- 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

View File

@@ -1,30 +0,0 @@
# -*- 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 — Batch Processing',
'version': '19.0.1.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Group parts into rack or barrel loads for tank processing.',
'author': 'Nexa Systems Inc.',
'website': 'https://www.nexasystems.ca',
'maintainer': 'Nexa Systems Inc.',
'support': 'support@nexasystems.ca',
'license': 'OPL-1',
'depends': [
'fusion_plating',
],
'data': [
'security/fp_batch_security.xml',
'security/ir.model.access.csv',
'data/fp_sequence_data.xml',
'views/fp_batch_chemistry_views.xml',
'views/fp_batch_views.xml',
'views/fp_menu.xml',
],
'installable': True,
'auto_install': False,
'application': False,
}

View File

@@ -1,17 +0,0 @@
<?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_batch" model="ir.sequence">
<field name="name">Fusion Plating: Batch</field>
<field name="code">fusion.plating.batch</field>
<field name="prefix">BATCH-</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
</odoo>

View File

@@ -1,7 +0,0 @@
# -*- 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_batch
from . import fp_batch_chemistry

View File

@@ -1,141 +0,0 @@
# -*- 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 FpBatch(models.Model):
"""A rack or barrel load of parts being processed through a tank.
Lifecycle:
draft → loading → in_process → unloading → complete
(any non-complete state) → cancelled
"""
_name = 'fusion.plating.batch'
_description = 'Plating Batch (Rack/Barrel Load)'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'create_date desc'
name = fields.Char(
string='Batch Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self.env['ir.sequence'].next_by_code(
'fusion.plating.batch') or '/',
tracking=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
required=True,
tracking=True,
)
bath_id = fields.Many2one(
'fusion.plating.bath',
string='Bath',
required=True,
tracking=True,
)
tank_id = fields.Many2one(
'fusion.plating.tank',
string='Tank',
related='bath_id.tank_id',
store=True,
readonly=True,
)
process_type_id = fields.Many2one(
related='bath_id.process_type_id',
store=True,
readonly=True,
)
state = fields.Selection(
selection=[
('draft', 'Draft'),
('loading', 'Loading'),
('in_process', 'In Process'),
('unloading', 'Unloading'),
('complete', 'Complete'),
('cancelled', 'Cancelled'),
],
string='Status',
default='draft',
required=True,
tracking=True,
)
rack_ref = fields.Char(string='Rack / Barrel Ref')
part_count = fields.Integer(string='Part Count')
start_time = fields.Datetime(string='Process Start', tracking=True)
end_time = fields.Datetime(string='Process End', tracking=True)
duration_minutes = fields.Float(
string='Duration (min)',
compute='_compute_duration',
store=True,
)
chemistry_ids = fields.One2many(
'fusion.plating.batch.chemistry',
'batch_id',
string='Chemistry Readings',
)
chemistry_count = fields.Integer(
string='Readings',
compute='_compute_chemistry_count',
)
operator_id = fields.Many2one(
'res.users',
string='Operator',
default=lambda self: self.env.user,
tracking=True,
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
notes = fields.Html(string='Notes')
active = fields.Boolean(default=True)
# -------------------------------------------------------------------------
# Compute
# -------------------------------------------------------------------------
@api.depends('start_time', 'end_time')
def _compute_duration(self):
for rec in self:
if rec.start_time and rec.end_time:
delta = rec.end_time - rec.start_time
rec.duration_minutes = delta.total_seconds() / 60.0
else:
rec.duration_minutes = 0.0
@api.depends('chemistry_ids')
def _compute_chemistry_count(self):
for rec in self:
rec.chemistry_count = len(rec.chemistry_ids)
# -------------------------------------------------------------------------
# Actions
# -------------------------------------------------------------------------
def action_start_loading(self):
self.write({'state': 'loading'})
def action_start_process(self):
self.write({
'state': 'in_process',
'start_time': fields.Datetime.now(),
})
def action_start_unloading(self):
self.write({
'state': 'unloading',
'end_time': fields.Datetime.now(),
})
def action_complete(self):
self.write({'state': 'complete'})
def action_cancel(self):
self.write({'state': 'cancelled'})

View File

@@ -1,75 +0,0 @@
# -*- 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 FpBatchChemistry(models.Model):
"""A single chemistry reading taken during batch processing."""
_name = 'fusion.plating.batch.chemistry'
_description = 'Batch Chemistry Reading'
_order = 'reading_time desc, id desc'
batch_id = fields.Many2one(
'fusion.plating.batch',
string='Batch',
required=True,
ondelete='cascade',
)
parameter_id = fields.Many2one(
'fusion.plating.bath.parameter',
string='Parameter',
required=True,
)
value = fields.Float(string='Value', required=True)
reading_time = fields.Datetime(
string='Reading Time',
default=fields.Datetime.now,
)
status = fields.Selection(
selection=[
('pass', 'Pass'),
('warning', 'Warning'),
('fail', 'Fail'),
],
string='Status',
compute='_compute_status',
store=True,
)
notes = fields.Char(string='Notes')
@api.depends('parameter_id', 'value')
def _compute_status(self):
"""Compare value against parameter target range.
Uses the parameter's default target range and warning tolerance.
A reading within [target_min, target_max] is a pass. If it falls
within the warning tolerance band outside that range, it is a
warning. Otherwise it is a fail.
"""
for rec in self:
if not rec.parameter_id:
rec.status = 'pass'
continue
param = rec.parameter_id
target_min = param.target_min
target_max = param.target_max
if not target_min and not target_max:
rec.status = 'pass'
continue
# Value within target range = pass
if target_min <= rec.value <= target_max:
rec.status = 'pass'
continue
# Calculate warning band from tolerance %
tolerance = (param.warning_tolerance or 0.0) / 100.0
span = target_max - target_min if target_max != target_min else abs(target_max) or 1.0
margin = span * tolerance
warning_min = target_min - margin
warning_max = target_max + margin
if warning_min <= rec.value <= warning_max:
rec.status = 'warning'
else:
rec.status = 'fail'

View File

@@ -1,19 +0,0 @@
<?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 RULE — Multi-company isolation on batches -->
<!-- ================================================================== -->
<record id="fp_batch_company_rule" model="ir.rule">
<field name="name">Fusion Plating: Batch — multi-company</field>
<field name="model_id" ref="model_fusion_plating_batch"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
</odoo>

View File

@@ -1,7 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_fp_batch_operator,fp.batch.operator,model_fusion_plating_batch,fusion_plating.group_fusion_plating_operator,1,1,1,0
access_fp_batch_supervisor,fp.batch.supervisor,model_fusion_plating_batch,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_batch_manager,fp.batch.manager,model_fusion_plating_batch,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_batch_chemistry_operator,fp.batch.chemistry.operator,model_fusion_plating_batch_chemistry,fusion_plating.group_fusion_plating_operator,1,1,1,0
access_fp_batch_chemistry_supervisor,fp.batch.chemistry.supervisor,model_fusion_plating_batch_chemistry,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_batch_chemistry_manager,fp.batch.chemistry.manager,model_fusion_plating_batch_chemistry,fusion_plating.group_fusion_plating_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fp_batch_operator fp.batch.operator model_fusion_plating_batch fusion_plating.group_fusion_plating_operator 1 1 1 0
3 access_fp_batch_supervisor fp.batch.supervisor model_fusion_plating_batch fusion_plating.group_fusion_plating_supervisor 1 1 1 0
4 access_fp_batch_manager fp.batch.manager model_fusion_plating_batch fusion_plating.group_fusion_plating_manager 1 1 1 1
5 access_fp_batch_chemistry_operator fp.batch.chemistry.operator model_fusion_plating_batch_chemistry fusion_plating.group_fusion_plating_operator 1 1 1 0
6 access_fp_batch_chemistry_supervisor fp.batch.chemistry.supervisor model_fusion_plating_batch_chemistry fusion_plating.group_fusion_plating_supervisor 1 1 1 0
7 access_fp_batch_chemistry_manager fp.batch.chemistry.manager model_fusion_plating_batch_chemistry fusion_plating.group_fusion_plating_manager 1 1 1 1

View File

@@ -1,87 +0,0 @@
<?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>
<!-- ===== LIST ===== -->
<record id="view_fp_batch_chemistry_list" model="ir.ui.view">
<field name="name">fp.batch.chemistry.list</field>
<field name="model">fusion.plating.batch.chemistry</field>
<field name="arch" type="xml">
<list string="Chemistry Readings"
decoration-success="status == 'pass'"
decoration-warning="status == 'warning'"
decoration-danger="status == 'fail'">
<field name="batch_id"/>
<field name="parameter_id"/>
<field name="value"/>
<field name="reading_time"/>
<field name="status" widget="badge"
decoration-success="status == 'pass'"
decoration-warning="status == 'warning'"
decoration-danger="status == 'fail'"/>
<field name="notes"/>
</list>
</field>
</record>
<!-- ===== FORM ===== -->
<record id="view_fp_batch_chemistry_form" model="ir.ui.view">
<field name="name">fp.batch.chemistry.form</field>
<field name="model">fusion.plating.batch.chemistry</field>
<field name="arch" type="xml">
<form string="Chemistry Reading">
<sheet>
<group>
<group>
<field name="batch_id"/>
<field name="parameter_id"/>
<field name="value"/>
</group>
<group>
<field name="reading_time"/>
<field name="status" widget="badge"
decoration-success="status == 'pass'"
decoration-warning="status == 'warning'"
decoration-danger="status == 'fail'"/>
<field name="notes"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- ===== SEARCH ===== -->
<record id="view_fp_batch_chemistry_search" model="ir.ui.view">
<field name="name">fp.batch.chemistry.search</field>
<field name="model">fusion.plating.batch.chemistry</field>
<field name="arch" type="xml">
<search string="Chemistry Readings">
<field name="batch_id"/>
<field name="parameter_id"/>
<separator/>
<filter string="Pass" name="pass" domain="[('status','=','pass')]"/>
<filter string="Warning" name="warning" domain="[('status','=','warning')]"/>
<filter string="Fail" name="fail" domain="[('status','=','fail')]"/>
<group>
<filter string="Batch" name="group_batch" context="{'group_by':'batch_id'}"/>
<filter string="Parameter" name="group_parameter" context="{'group_by':'parameter_id'}"/>
<filter string="Status" name="group_status" context="{'group_by':'status'}"/>
</group>
</search>
</field>
</record>
<!-- ===== ACTION ===== -->
<record id="action_fp_batch_chemistry" model="ir.actions.act_window">
<field name="name">Chemistry Readings</field>
<field name="res_model">fusion.plating.batch.chemistry</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_batch_chemistry_search"/>
</record>
</odoo>

View File

@@ -1,202 +0,0 @@
<?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>
<!-- ===== LIST ===== -->
<record id="view_fp_batch_list" model="ir.ui.view">
<field name="name">fp.batch.list</field>
<field name="model">fusion.plating.batch</field>
<field name="arch" type="xml">
<list string="Batches"
decoration-muted="state == 'cancelled'"
decoration-success="state == 'complete'">
<field name="name"/>
<field name="facility_id"/>
<field name="bath_id"/>
<field name="tank_id" optional="show"/>
<field name="rack_ref" optional="show"/>
<field name="part_count"/>
<field name="operator_id" widget="many2one_avatar_user"/>
<field name="state" widget="badge"
decoration-info="state == 'draft'"
decoration-warning="state == 'loading'"
decoration-primary="state == 'in_process'"
decoration-success="state in ('unloading','complete')"
decoration-danger="state == 'cancelled'"/>
<field name="duration_minutes" optional="show" widget="float_time"/>
<field name="start_time" optional="hide"/>
<field name="end_time" optional="hide"/>
</list>
</field>
</record>
<!-- ===== FORM ===== -->
<record id="view_fp_batch_form" model="ir.ui.view">
<field name="name">fp.batch.form</field>
<field name="model">fusion.plating.batch</field>
<field name="arch" type="xml">
<form string="Batch">
<header>
<button name="action_start_loading" string="Start Loading"
type="object" class="oe_highlight"
invisible="state != 'draft'"/>
<button name="action_start_process" string="Start Process"
type="object" class="oe_highlight"
invisible="state != 'loading'"/>
<button name="action_start_unloading" string="Unload"
type="object" class="oe_highlight"
invisible="state != 'in_process'"/>
<button name="action_complete" string="Complete"
type="object" class="oe_highlight"
invisible="state != 'unloading'"/>
<button name="action_cancel" string="Cancel"
type="object"
invisible="state in ('complete','cancelled')"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,loading,in_process,unloading,complete"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(fusion_plating_batch.action_fp_batch_chemistry)d"
type="action" class="oe_stat_button" icon="fa-flask"
context="{'search_default_batch_id': id}">
<field name="chemistry_count" widget="statinfo" string="Readings"/>
</button>
</div>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" readonly="1"/></h1>
</div>
<group>
<group>
<field name="facility_id"/>
<field name="bath_id" domain="[('facility_id','=',facility_id)]"/>
<field name="tank_id"/>
<field name="process_type_id"/>
</group>
<group>
<field name="rack_ref"/>
<field name="part_count"/>
<field name="operator_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<group>
<group>
<field name="start_time"/>
<field name="end_time"/>
<field name="duration_minutes" widget="float_time"/>
</group>
</group>
<notebook>
<page string="Chemistry Readings">
<field name="chemistry_ids">
<list editable="bottom">
<field name="parameter_id"/>
<field name="value"/>
<field name="reading_time"/>
<field name="status" widget="badge"
decoration-success="status == 'pass'"
decoration-warning="status == 'warning'"
decoration-danger="status == 'fail'"/>
<field name="notes"/>
</list>
</field>
</page>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- ===== KANBAN ===== -->
<record id="view_fp_batch_kanban" model="ir.ui.view">
<field name="name">fp.batch.kanban</field>
<field name="model">fusion.plating.batch</field>
<field name="arch" type="xml">
<kanban default_group_by="state" class="o_fp_batch_kanban">
<field name="id"/>
<field name="name"/>
<field name="facility_id"/>
<field name="bath_id"/>
<field name="rack_ref"/>
<field name="part_count"/>
<field name="state"/>
<field name="operator_id"/>
<field name="duration_minutes"/>
<templates>
<t t-name="card">
<div class="o_fp_card o_fp_batch_card">
<div class="d-flex justify-content-between align-items-start">
<strong class="o_fp_card_title"><field name="name"/></strong>
</div>
<div class="small text-muted">
<field name="bath_id"/>
</div>
<div class="small">
<i class="fa fa-cubes me-1 text-muted"/>
<field name="rack_ref"/>
</div>
<div class="d-flex justify-content-between mt-2 small">
<span class="text-muted">Parts</span>
<span class="fw-bold"><field name="part_count"/></span>
</div>
<div class="d-flex justify-content-between small">
<span class="text-muted">Operator</span>
<field name="operator_id" widget="many2one_avatar_user"/>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- ===== SEARCH ===== -->
<record id="view_fp_batch_search" model="ir.ui.view">
<field name="name">fp.batch.search</field>
<field name="model">fusion.plating.batch</field>
<field name="arch" type="xml">
<search string="Batches">
<field name="name"/>
<field name="bath_id"/>
<field name="facility_id"/>
<field name="rack_ref"/>
<field name="operator_id"/>
<separator/>
<filter string="Draft" name="draft" domain="[('state','=','draft')]"/>
<filter string="Loading" name="loading" domain="[('state','=','loading')]"/>
<filter string="In Process" name="in_process" domain="[('state','=','in_process')]"/>
<filter string="Unloading" name="unloading" domain="[('state','=','unloading')]"/>
<filter string="Complete" name="complete" domain="[('state','=','complete')]"/>
<filter string="Cancelled" name="cancelled" domain="[('state','=','cancelled')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group>
<filter string="Facility" name="group_facility" context="{'group_by':'facility_id'}"/>
<filter string="Bath" name="group_bath" context="{'group_by':'bath_id'}"/>
<filter string="Status" name="group_state" context="{'group_by':'state'}"/>
<filter string="Operator" name="group_operator" context="{'group_by':'operator_id'}"/>
</group>
</search>
</field>
</record>
<!-- ===== ACTION ===== -->
<record id="action_fp_batch" model="ir.actions.act_window">
<field name="name">Batches</field>
<field name="res_model">fusion.plating.batch</field>
<field name="view_mode">kanban,list,form</field>
<field name="search_view_id" ref="view_fp_batch_search"/>
<field name="context">{'search_default_draft': 1, 'search_default_loading': 1, 'search_default_in_process': 1, 'search_default_unloading': 1}</field>
</record>
</odoo>

View File

@@ -1,16 +0,0 @@
<?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>
<!-- Batches menu item under Plating > Operations -->
<menuitem id="menu_fp_batches"
name="Batches"
parent="fusion_plating.menu_fp_operations"
action="action_fp_batch"
sequence="15"/>
</odoo>

View File

@@ -1,64 +0,0 @@
# Fusion Plating — Documents Bridge (Enterprise)
Part of the Fusion Plating product family by Nexa Systems Inc.
## What this module does
When both `fusion_plating_quality` and Odoo Enterprise's `documents` module are
installed, this bridge **auto-installs** and wires the two together so that
every attachment dropped on a Fusion Plating quality record (NCR, CAPA, FAIR,
or Controlled Document) is automatically mirrored into a dedicated Documents
workspace with the right tag applied.
No manual file uploads, no duplicate copies to keep in sync — the bridge takes
care of it on `ir.attachment.create()`.
## What it creates
* **Workspace**: `Plating — Quality` (a `documents.document` with `type='folder'`)
* **Facet**: `Record Type` (a `documents.facet` scoped to the workspace)
* **Tags**: `NCR`, `CAPA`, `FAIR`, `Doc Control` under that facet
* **Smart button**: a `Documents` stat button on every NCR / CAPA / FAIR /
Controlled Document form view, opening the filtered Documents kanban for
that record
## How it works
1. A user attaches a file to an NCR (or any of the other supported records)
via the chatter or a wizard.
2. The bridge's `ir.attachment.create()` override inspects `res_model` and,
if it matches one of the supported quality models, creates a mirror
`documents.document` record:
* Placed inside the `Plating — Quality` workspace
* Tagged with the corresponding record type tag
* Linked back to the original `ir.attachment` via `attachment_id`
3. On the quality record form, the smart button reads a computed Many2many
(`x_fc_document_ids`) that searches `documents.document` by the underlying
attachment's `res_model` + `res_id` — no duplication, no storage overhead.
## Safety & robustness
* The bridge never blocks attachment creation. Any exception raised while
creating the mirror `documents.document` record is caught and logged — the
user's upload always succeeds.
* All references to the workspace folder and tags use
`env.ref(..., raise_if_not_found=False)`. If the data records are ever
removed or renamed, the bridge degrades gracefully (no mirror created,
logged warning).
* The bridge never modifies `fusion_plating`, `fusion_plating_quality`, or
the EE `documents` module. It is purely additive.
## Dependencies
* `fusion_plating_quality`
* `documents` (Odoo Enterprise)
## Auto-install
`auto_install = True` — the bridge installs automatically whenever both
dependencies are present in the same database, and stays dormant otherwise.
## Copyright
Copyright 2026 Nexa Systems Inc. All rights reserved.
Licensed under OPL-1 (Odoo Proprietary License v1.0).

View File

@@ -1,6 +0,0 @@
# -*- 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

View File

@@ -1,71 +0,0 @@
# -*- 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 — Documents Bridge (EE)',
'version': '19.0.1.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Enterprise bridge: auto-promotes Fusion Plating quality attachments '
'(NCR, CAPA, FAIR, Doc Control) into Odoo EE Documents with a tagged '
'workspace. Auto-installs when both modules are present.',
'description': """
Fusion Plating — Documents Bridge (Enterprise)
==============================================
Part of the Fusion Plating product family by Nexa Systems Inc.
This bridge module connects the native Fusion Plating QMS (`fusion_plating_quality`)
with the Odoo Enterprise `documents` module. When both modules are installed the
bridge installs automatically and takes care of the plumbing so that every
attachment dropped on an NCR, CAPA, FAIR, or Controlled Document record is
promoted into a dedicated "Plating — Quality" workspace and tagged by record
type for easy retrieval, review, and audit export.
What it does
------------
* Creates a dedicated Documents workspace: "Plating — Quality"
* Creates a "Record Type" facet with four tags: NCR, CAPA, FAIR, Doc Control
* Overrides `ir.attachment.create()` so attachments added to supported quality
records are silently mirrored as `documents.document` records in the
workspace and tagged with the appropriate record type
* Adds a "Documents" smart button on each NCR, CAPA, FAIR, and Doc Control form
view that opens the filtered Documents kanban for that record
* Ships with `auto_install = True` so no manual install step is required — the
bridge activates as soon as both pre-requisite modules are present
Why this module exists
----------------------
The Community-Edition-compatible `fusion_plating_quality` module intentionally
does NOT depend on the Enterprise `documents` module. On Enterprise deployments
this bridge provides the richer Documents-app experience (workspaces, tags,
bulk download, preview, sharing) without ever touching the core or quality
modules — both stay CE-safe and upgradable.
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
""",
'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_quality',
'documents',
],
'data': [
'security/ir.model.access.csv',
'data/documents_folder_data.xml',
'data/documents_tag_data.xml',
'views/fp_ncr_views.xml',
'views/fp_capa_views.xml',
'views/fp_fair_views.xml',
'views/fp_doc_control_views.xml',
],
'installable': True,
'application': False,
'auto_install': True,
}

View File

@@ -1,24 +0,0 @@
<?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.
Workspace folder for Fusion Plating QMS attachments.
Odoo 19 EE unified the Documents data model: folders are simply
``documents.document`` records with ``type = 'folder'`` and a null
``folder_id`` (root) or a parent folder reference. This matches the
Odoo 18.x / 19.x behaviour where the Documents kanban is driven by
a single model rather than a separate ``documents.folder`` model.
-->
<odoo noupdate="1">
<record id="documents_folder_plating_quality" model="documents.document">
<field name="name">Plating — Quality</field>
<field name="type">folder</field>
<field name="folder_id" eval="False"/>
</record>
</odoo>

View File

@@ -1,50 +0,0 @@
<?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.
Tags used by the bridge to categorise mirrored quality attachments.
Odoo 19 EE organises tags under a two-level hierarchy:
documents.facet (a.k.a. "category" — scoped to a folder/workspace)
└── documents.tag (individual tags, required to have a facet_id)
We create one facet called "Record Type" inside the Plating — Quality
workspace, then four tags beneath it — one per supported quality
record type.
-->
<odoo noupdate="1">
<record id="documents_facet_record_type" model="documents.facet">
<field name="name">Record Type</field>
<field name="folder_id" ref="documents_folder_plating_quality"/>
<field name="sequence">10</field>
</record>
<record id="documents_tag_ncr" model="documents.tag">
<field name="name">NCR</field>
<field name="facet_id" ref="documents_facet_record_type"/>
<field name="sequence">10</field>
</record>
<record id="documents_tag_capa" model="documents.tag">
<field name="name">CAPA</field>
<field name="facet_id" ref="documents_facet_record_type"/>
<field name="sequence">20</field>
</record>
<record id="documents_tag_fair" model="documents.tag">
<field name="name">FAIR</field>
<field name="facet_id" ref="documents_facet_record_type"/>
<field name="sequence">30</field>
</record>
<record id="documents_tag_doc_control" model="documents.tag">
<field name="name">Doc Control</field>
<field name="facet_id" ref="documents_facet_record_type"/>
<field name="sequence">40</field>
</record>
</odoo>

View File

@@ -1,10 +0,0 @@
# -*- 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 ir_attachment
from . import fp_ncr
from . import fp_capa
from . import fp_fair
from . import fp_doc_control

View File

@@ -1,65 +0,0 @@
# -*- 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
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
class FpCapa(models.Model):
"""Bridge extension: expose Documents workspace on CAPAs."""
_inherit = 'fusion.plating.capa'
x_fc_document_ids = fields.Many2many(
'documents.document',
'fp_bridge_capa_document_rel',
'capa_id',
'document_id',
string='Quality Documents',
compute='_compute_x_fc_document_ids',
store=False,
help='Documents in the Plating — Quality workspace mirrored from '
'attachments on this CAPA.',
)
x_fc_document_count = fields.Integer(
string='# Documents',
compute='_compute_x_fc_document_ids',
store=False,
)
@api.depends('message_attachment_count')
def _compute_x_fc_document_ids(self):
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
for rec in self:
if not Document:
rec.x_fc_document_ids = False
rec.x_fc_document_count = 0
continue
docs = Document.sudo().search([
('attachment_id.res_model', '=', 'fusion.plating.capa'),
('attachment_id.res_id', '=', rec.id),
])
rec.x_fc_document_ids = docs
rec.x_fc_document_count = len(docs)
def action_view_documents(self):
self.ensure_one()
folder_id = self._get_default_folder_id()
ctx = {}
if folder_id:
ctx['default_folder_id'] = folder_id
ctx['searchpanel_default_folder_id'] = folder_id
return {
'name': _('Quality Documents'),
'type': 'ir.actions.act_window',
'res_model': 'documents.document',
'view_mode': 'kanban,list,form',
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
'context': ctx,
}
def _get_default_folder_id(self):
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
return folder.id if folder else 0

View File

@@ -1,84 +0,0 @@
# -*- 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
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
class FpDocControl(models.Model):
"""Bridge extension: expose Documents workspace on Controlled Documents.
Doc Control already carries a native ``attachment_ids`` Many2many; the
bridge additionally exposes the `documents.document` mirror so users can
jump straight into the Documents app to use its preview, tag, share,
and lock features.
"""
_inherit = 'fusion.plating.doc.control'
x_fc_document_ids = fields.Many2many(
'documents.document',
'fp_bridge_doc_control_document_rel',
'doc_id',
'document_id',
string='Quality Documents',
compute='_compute_x_fc_document_ids',
store=False,
help='Documents in the Plating — Quality workspace mirrored from '
'attachments on this controlled document record.',
)
x_fc_document_count = fields.Integer(
string='# Documents',
compute='_compute_x_fc_document_ids',
store=False,
)
@api.depends('attachment_ids', 'message_attachment_count')
def _compute_x_fc_document_ids(self):
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
for rec in self:
if not Document:
rec.x_fc_document_ids = False
rec.x_fc_document_count = 0
continue
# Pull in both chatter attachments (matched via res_model/res_id) and
# any documents whose underlying ir.attachment is in the native
# attachment_ids M2m on this controlled document record.
native_attachment_ids = rec.attachment_ids.ids
if native_attachment_ids:
domain = [
'|',
'&', ('attachment_id.res_model', '=', 'fusion.plating.doc.control'),
('attachment_id.res_id', '=', rec.id),
('attachment_id', 'in', native_attachment_ids),
]
else:
domain = [
('attachment_id.res_model', '=', 'fusion.plating.doc.control'),
('attachment_id.res_id', '=', rec.id),
]
docs = Document.sudo().search(domain)
rec.x_fc_document_ids = docs
rec.x_fc_document_count = len(docs)
def action_view_documents(self):
self.ensure_one()
folder_id = self._get_default_folder_id()
ctx = {}
if folder_id:
ctx['default_folder_id'] = folder_id
ctx['searchpanel_default_folder_id'] = folder_id
return {
'name': _('Quality Documents'),
'type': 'ir.actions.act_window',
'res_model': 'documents.document',
'view_mode': 'kanban,list,form',
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
'context': ctx,
}
def _get_default_folder_id(self):
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
return folder.id if folder else 0

View File

@@ -1,65 +0,0 @@
# -*- 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
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
class FpFair(models.Model):
"""Bridge extension: expose Documents workspace on FAIRs."""
_inherit = 'fusion.plating.fair'
x_fc_document_ids = fields.Many2many(
'documents.document',
'fp_bridge_fair_document_rel',
'fair_id',
'document_id',
string='Quality Documents',
compute='_compute_x_fc_document_ids',
store=False,
help='Documents in the Plating — Quality workspace mirrored from '
'attachments on this FAIR.',
)
x_fc_document_count = fields.Integer(
string='# Documents',
compute='_compute_x_fc_document_ids',
store=False,
)
@api.depends('message_attachment_count')
def _compute_x_fc_document_ids(self):
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
for rec in self:
if not Document:
rec.x_fc_document_ids = False
rec.x_fc_document_count = 0
continue
docs = Document.sudo().search([
('attachment_id.res_model', '=', 'fusion.plating.fair'),
('attachment_id.res_id', '=', rec.id),
])
rec.x_fc_document_ids = docs
rec.x_fc_document_count = len(docs)
def action_view_documents(self):
self.ensure_one()
folder_id = self._get_default_folder_id()
ctx = {}
if folder_id:
ctx['default_folder_id'] = folder_id
ctx['searchpanel_default_folder_id'] = folder_id
return {
'name': _('Quality Documents'),
'type': 'ir.actions.act_window',
'res_model': 'documents.document',
'view_mode': 'kanban,list,form',
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
'context': ctx,
}
def _get_default_folder_id(self):
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
return folder.id if folder else 0

View File

@@ -1,71 +0,0 @@
# -*- 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
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
class FpNcr(models.Model):
"""Bridge extension: expose Documents workspace on NCRs.
Adds a reverse link to any `documents.document` records that were
created by the bridge override on `ir.attachment`, plus a smart
button action that opens the filtered Documents kanban for the
current NCR.
"""
_inherit = 'fusion.plating.ncr'
x_fc_document_ids = fields.Many2many(
'documents.document',
'fp_bridge_ncr_document_rel',
'ncr_id',
'document_id',
string='Quality Documents',
compute='_compute_x_fc_document_ids',
store=False,
help='Documents in the Plating — Quality workspace mirrored from '
'attachments on this NCR.',
)
x_fc_document_count = fields.Integer(
string='# Documents',
compute='_compute_x_fc_document_ids',
store=False,
)
@api.depends('message_attachment_count')
def _compute_x_fc_document_ids(self):
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
for rec in self:
if not Document:
rec.x_fc_document_ids = False
rec.x_fc_document_count = 0
continue
docs = Document.sudo().search([
('attachment_id.res_model', '=', 'fusion.plating.ncr'),
('attachment_id.res_id', '=', rec.id),
])
rec.x_fc_document_ids = docs
rec.x_fc_document_count = len(docs)
def action_view_documents(self):
self.ensure_one()
folder_id = self._get_default_folder_id()
ctx = {}
if folder_id:
ctx['default_folder_id'] = folder_id
ctx['searchpanel_default_folder_id'] = folder_id
return {
'name': _('Quality Documents'),
'type': 'ir.actions.act_window',
'res_model': 'documents.document',
'view_mode': 'kanban,list,form',
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
'context': ctx,
}
def _get_default_folder_id(self):
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
return folder.id if folder else 0

View File

@@ -1,122 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import logging
from odoo import api, models
_logger = logging.getLogger(__name__)
# Map of quality-module res_model -> bridge tag XML id.
# Kept module-level so both create() and write() can consult it cheaply.
_QUALITY_MODELS_TO_TAG = {
'fusion.plating.ncr': 'fusion_plating_bridge_documents.documents_tag_ncr',
'fusion.plating.capa': 'fusion_plating_bridge_documents.documents_tag_capa',
'fusion.plating.fair': 'fusion_plating_bridge_documents.documents_tag_fair',
'fusion.plating.doc.control': 'fusion_plating_bridge_documents.documents_tag_doc_control',
}
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
class IrAttachment(models.Model):
"""Bridge ir.attachment with Odoo EE `documents.document`.
Whenever an attachment is created on one of the Fusion Plating QMS
record types (NCR, CAPA, FAIR, Doc Control) we silently mirror it as
a `documents.document` record inside the "Plating — Quality"
workspace, tagged with the corresponding record type. The original
`ir.attachment` record is untouched and continues to live on the
quality record as before — the bridge is purely additive.
Design notes
------------
* We resolve the folder and tag XML ids via ``env.ref`` with
``raise_if_not_found=False`` so that a partial install, a missing
demo record, or a future schema change can never break attachment
creation on a quality record — the worst case is that the
`documents.document` mirror record isn't created and a line goes
to the log.
* The write is wrapped in a broad try/except for the same reason:
user-facing attachment creation must never be blocked by a bridge
failure.
* We use ``sudo()`` on the `documents.document` create because the
user uploading the attachment may not have write access to the
Documents app — the bridge is a system-level convenience.
"""
_inherit = 'ir.attachment'
@api.model_create_multi
def create(self, vals_list):
attachments = super().create(vals_list)
try:
self._fusion_plating_bridge_promote_to_documents(attachments)
except Exception: # pragma: no cover - defensive only
_logger.exception(
"Fusion Plating Documents bridge: failed to promote attachments %s",
attachments.ids,
)
return attachments
def _fusion_plating_bridge_promote_to_documents(self, attachments):
"""Create `documents.document` mirror records for quality attachments.
Silently skips if:
- the documents module isn't in the registry (defensive, the
manifest already depends on it but this module may be tested
on CE)
- the target folder hasn't been created yet
- the attachment is not attached to a quality record
"""
if 'documents.document' not in self.env:
return
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
if not folder:
_logger.warning(
"Fusion Plating Documents bridge: target folder %s not found",
_FOLDER_XMLID,
)
return
Document = self.env['documents.document'].sudo()
# Cache tag lookups across the batch so we don't hit env.ref per attachment.
tag_cache = {}
for att in attachments:
if att.res_model not in _QUALITY_MODELS_TO_TAG:
continue
# Skip attachments linked to a specific field (e.g. image_1920) —
# those are UI artefacts, not user-uploaded docs.
if att.res_field:
continue
# Skip records that have no concrete res_id (drafts being built).
if not att.res_id:
continue
tag_xmlid = _QUALITY_MODELS_TO_TAG[att.res_model]
if tag_xmlid not in tag_cache:
tag = self.env.ref(tag_xmlid, raise_if_not_found=False)
tag_cache[tag_xmlid] = tag.id if tag else False
tag_id = tag_cache[tag_xmlid]
doc_vals = {
'attachment_id': att.id,
'folder_id': folder.id,
'name': att.name or 'Untitled',
}
if tag_id:
doc_vals['tag_ids'] = [(4, tag_id)]
try:
Document.create(doc_vals)
except Exception: # pragma: no cover - defensive only
_logger.exception(
"Fusion Plating Documents bridge: could not create "
"documents.document for attachment id=%s (res_model=%s, res_id=%s)",
att.id, att.res_model, att.res_id,
)

View File

@@ -1,8 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_documents_document_fp_operator,documents.document.fp.operator,documents.model_documents_document,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_documents_document_fp_supervisor,documents.document.fp.supervisor,documents.model_documents_document,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_documents_document_fp_manager,documents.document.fp.manager,documents.model_documents_document,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_documents_tag_fp_operator,documents.tag.fp.operator,documents.model_documents_tag,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_documents_tag_fp_manager,documents.tag.fp.manager,documents.model_documents_tag,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_documents_facet_fp_operator,documents.facet.fp.operator,documents.model_documents_facet,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_documents_facet_fp_manager,documents.facet.fp.manager,documents.model_documents_facet,fusion_plating.group_fusion_plating_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_documents_document_fp_operator documents.document.fp.operator documents.model_documents_document fusion_plating.group_fusion_plating_operator 1 0 0 0
3 access_documents_document_fp_supervisor documents.document.fp.supervisor documents.model_documents_document fusion_plating.group_fusion_plating_supervisor 1 1 1 0
4 access_documents_document_fp_manager documents.document.fp.manager documents.model_documents_document fusion_plating.group_fusion_plating_manager 1 1 1 1
5 access_documents_tag_fp_operator documents.tag.fp.operator documents.model_documents_tag fusion_plating.group_fusion_plating_operator 1 0 0 0
6 access_documents_tag_fp_manager documents.tag.fp.manager documents.model_documents_tag fusion_plating.group_fusion_plating_manager 1 1 1 1
7 access_documents_facet_fp_operator documents.facet.fp.operator documents.model_documents_facet fusion_plating.group_fusion_plating_operator 1 0 0 0
8 access_documents_facet_fp_manager documents.facet.fp.manager documents.model_documents_facet fusion_plating.group_fusion_plating_manager 1 1 1 1

View File

@@ -1,30 +0,0 @@
<?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.
The upstream fusion_plating_quality CAPA form does not ship with a
button_box. We inject one at the top of the <sheet> so our stat button
has somewhere to live. Adding via the sheet xpath keeps the core form
untouched.
-->
<odoo>
<record id="view_fp_capa_form_bridge_documents" model="ir.ui.view">
<field name="name">fp.capa.form.bridge.documents</field>
<field name="model">fusion.plating.capa</field>
<field name="inherit_id" ref="fusion_plating_quality.view_fp_capa_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet/div[hasclass('oe_title')]" position="before">
<div class="oe_button_box" name="button_box">
<button name="action_view_documents" type="object"
class="oe_stat_button" icon="fa-folder-open">
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
</button>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,29 +0,0 @@
<?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.
The upstream fusion_plating_quality Doc Control form does not ship
with a button_box. We inject one at the top of the <sheet> so our
stat button has somewhere to live.
-->
<odoo>
<record id="view_fp_doc_control_form_bridge_documents" model="ir.ui.view">
<field name="name">fp.doc.control.form.bridge.documents</field>
<field name="model">fusion.plating.doc.control</field>
<field name="inherit_id" ref="fusion_plating_quality.view_fp_doc_control_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet/div[hasclass('oe_title')]" position="before">
<div class="oe_button_box" name="button_box">
<button name="action_view_documents" type="object"
class="oe_stat_button" icon="fa-folder-open">
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
</button>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,29 +0,0 @@
<?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.
The upstream fusion_plating_quality FAIR form does not ship with a
button_box. We inject one at the top of the <sheet> so our stat button
has somewhere to live.
-->
<odoo>
<record id="view_fp_fair_form_bridge_documents" model="ir.ui.view">
<field name="name">fp.fair.form.bridge.documents</field>
<field name="model">fusion.plating.fair</field>
<field name="inherit_id" ref="fusion_plating_quality.view_fp_fair_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet/div[hasclass('oe_title')]" position="before">
<div class="oe_button_box" name="button_box">
<button name="action_view_documents" type="object"
class="oe_stat_button" icon="fa-folder-open">
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
</button>
</div>
</xpath>
</field>
</record>
</odoo>

Some files were not shown because too many files have changed in this diff Show More