This commit is contained in:
gsinghpal
2026-04-12 09:09:50 -04:00
parent d07159b9b5
commit be611876ad
470 changed files with 41761 additions and 51 deletions

View File

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

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from . import models

View File

@@ -0,0 +1,106 @@
# -*- 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_menu.xml',
],
'assets': {
'web.assets_backend': [
'fusion_plating/static/src/scss/fusion_plating.scss',
],
},
'demo': [
'data/fp_demo_data.xml',
],
'images': ['static/description/icon.png'],
'installable': True,
'auto_install': False,
'application': True,
}

View File

@@ -0,0 +1,322 @@
<?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

@@ -0,0 +1,70 @@
<?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

@@ -0,0 +1,26 @@
<?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

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

View File

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,85 @@
<?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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,173 @@
// =============================================================================
// 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

@@ -0,0 +1,127 @@
<?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

@@ -0,0 +1,198 @@
<?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

@@ -0,0 +1,156 @@
<?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

@@ -0,0 +1,77 @@
<?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="10"/>
<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

@@ -0,0 +1,240 @@
<?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

@@ -0,0 +1,168 @@
<?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

@@ -0,0 +1,92 @@
<?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>