folder rename
This commit is contained in:
121
fusion_plating/fusion_plating/README.md
Normal file
121
fusion_plating/fusion_plating/README.md
Normal 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).
|
||||
7
fusion_plating/fusion_plating/__init__.py
Normal file
7
fusion_plating/fusion_plating/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
112
fusion_plating/fusion_plating/__manifest__.py
Normal file
112
fusion_plating/fusion_plating/__manifest__.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
Fusion Plating — Core
|
||||
=====================
|
||||
|
||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||
|
||||
Fusion Plating is a configurable, multi-tenant capable ERP for plating and metal
|
||||
finishing shops. This core module provides the process-agnostic foundation that
|
||||
every shop needs regardless of size, process mix, jurisdiction, or industry.
|
||||
|
||||
The core ships intentionally empty of region-specific or process-specific
|
||||
content — that comes from add-on modules:
|
||||
|
||||
* fusion_plating_process_en — Electroless nickel plating
|
||||
* fusion_plating_process_chrome — Chrome coating (hex or trivalent)
|
||||
* fusion_plating_process_anodize — Aluminum anodizing (Type II, III)
|
||||
* fusion_plating_process_black_oxide — Black oxidizing
|
||||
* fusion_plating_quality — QMS (NCR, CAPA, calibration, CoC, doc control)
|
||||
* fusion_plating_compliance — Generic compliance framework
|
||||
* fusion_plating_compliance_on — Ontario regulatory pack
|
||||
* fusion_plating_compliance_tor — Toronto Ch. 681 municipal pack
|
||||
* fusion_plating_safety — SDS, WHMIS/TDG training, JHSC, exposure
|
||||
* fusion_plating_shopfloor — Tablet operator stations, QR scanning
|
||||
* fusion_plating_portal — Customer portal
|
||||
* fusion_plating_aerospace — AS9100 + Nadcap AC7108 pack
|
||||
* fusion_plating_nuclear — CSA N299, CNSC, NQA-1 pack
|
||||
* fusion_plating_cgp — Controlled Goods Program pack
|
||||
* fusion_plating_logistics — Pickup & delivery
|
||||
* fusion_plating_culture — Values / fundamentals framework
|
||||
|
||||
Core concepts
|
||||
-------------
|
||||
* Facility — a physical site with its own tanks, operators, compliance profile
|
||||
* Process Type — extensible taxonomy of finishing processes
|
||||
* Work Center — production line or station within a facility
|
||||
* Tank — physical vessel with QR code and state
|
||||
* Bath — the chemistry currently in a tank, with its own lifecycle
|
||||
* Bath Log — daily chemistry readings with pass/fail vs target
|
||||
* KPI — configurable headline metrics per shop
|
||||
* Delegation Inbox — single pane of "things waiting for someone"
|
||||
|
||||
Design principles
|
||||
-----------------
|
||||
1. No client-specific strings in core.
|
||||
2. No region-specific data in core.
|
||||
3. No process-specific chemistry in core.
|
||||
4. Works on both Odoo Community and Enterprise editions.
|
||||
5. Theme-aware: respects user light/dark mode preference.
|
||||
6. Multi-facility, multi-company, multi-currency capable.
|
||||
|
||||
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
""",
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'maintainer': 'Nexa Systems Inc.',
|
||||
'support': 'support@nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'price': 0.00,
|
||||
'currency': 'CAD',
|
||||
'depends': [
|
||||
'base',
|
||||
'mail',
|
||||
'contacts',
|
||||
'product',
|
||||
'stock',
|
||||
'sale_management',
|
||||
'purchase',
|
||||
'hr',
|
||||
'uom',
|
||||
],
|
||||
'data': [
|
||||
'security/fp_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/fp_sequence_data.xml',
|
||||
'data/fp_process_category_data.xml',
|
||||
'views/fp_process_type_views.xml',
|
||||
'views/fp_work_center_views.xml',
|
||||
'views/fp_tank_views.xml',
|
||||
'views/fp_bath_log_views.xml',
|
||||
'views/fp_facility_views.xml',
|
||||
'views/fp_bath_views.xml',
|
||||
'views/fp_process_node_views.xml',
|
||||
'views/fp_menu.xml',
|
||||
'data/fp_recipe_enp_alum_basic.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'fusion_plating/static/src/scss/fusion_plating.scss',
|
||||
'fusion_plating/static/src/scss/recipe_tree_editor.scss',
|
||||
'fusion_plating/static/src/xml/recipe_tree_editor.xml',
|
||||
'fusion_plating/static/src/js/recipe_tree_editor.js',
|
||||
],
|
||||
},
|
||||
'demo': [
|
||||
'data/fp_demo_data.xml',
|
||||
'data/fp_demo_recipe_data.xml',
|
||||
],
|
||||
'images': ['static/description/icon.png'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': True,
|
||||
}
|
||||
5
fusion_plating/fusion_plating/controllers/__init__.py
Normal file
5
fusion_plating/fusion_plating/controllers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import recipe_controller
|
||||
188
fusion_plating/fusion_plating/controllers/recipe_controller.py
Normal file
188
fusion_plating/fusion_plating/controllers/recipe_controller.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FpRecipeController(http.Controller):
|
||||
"""JSON-RPC endpoints for the process recipe tree editor."""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Read — full tree
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/recipe/tree', type='jsonrpc', auth='user')
|
||||
def get_tree(self, recipe_id):
|
||||
"""Return the full nested tree for a recipe."""
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
recipe = Node.browse(int(recipe_id))
|
||||
if not recipe.exists():
|
||||
return {'ok': False, 'error': f'Recipe {recipe_id} not found.'}
|
||||
return {
|
||||
'ok': True,
|
||||
'recipe': {
|
||||
'id': recipe.id,
|
||||
'name': recipe.name,
|
||||
'code': recipe.code or '',
|
||||
'version': recipe.version,
|
||||
'process_type': recipe.process_type_id.name if recipe.process_type_id else '',
|
||||
},
|
||||
'tree': recipe.get_tree_data(),
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Create node
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/recipe/node/create', type='jsonrpc', auth='user')
|
||||
def create_node(self, parent_id, name, node_type='operation', vals=None):
|
||||
"""Create a new child node under parent_id."""
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
parent = Node.browse(int(parent_id))
|
||||
if not parent.exists():
|
||||
return {'ok': False, 'error': 'Parent node not found.'}
|
||||
|
||||
# Determine next sequence
|
||||
max_seq = max((c.sequence for c in parent.child_ids), default=0)
|
||||
data = {
|
||||
'name': name,
|
||||
'node_type': node_type,
|
||||
'parent_id': parent.id,
|
||||
'sequence': max_seq + 10,
|
||||
}
|
||||
if vals:
|
||||
data.update(vals)
|
||||
|
||||
try:
|
||||
new_node = Node.create(data)
|
||||
_logger.info('Recipe: created node %s (%s) under %s by uid %s',
|
||||
new_node.id, name, parent.id, request.env.uid)
|
||||
return {'ok': True, 'node_id': new_node.id}
|
||||
except Exception as exc:
|
||||
_logger.exception('Recipe create_node failed')
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Update node
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/recipe/node/write', type='jsonrpc', auth='user')
|
||||
def write_node(self, node_id, vals):
|
||||
"""Update fields on an existing node."""
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
node = Node.browse(int(node_id))
|
||||
if not node.exists():
|
||||
return {'ok': False, 'error': 'Node not found.'}
|
||||
|
||||
# Filter to allowed fields only
|
||||
allowed = {
|
||||
'name', 'code', 'node_type', 'icon', 'color',
|
||||
'process_type_id', 'work_center_id',
|
||||
'description', 'notes',
|
||||
'estimated_duration',
|
||||
'auto_complete', 'customer_visible', 'is_manual',
|
||||
'requires_signoff', 'opt_in_out', 'sequence', 'version',
|
||||
}
|
||||
safe_vals = {k: v for k, v in vals.items() if k in allowed}
|
||||
if not safe_vals:
|
||||
return {'ok': False, 'error': 'No valid fields to update.'}
|
||||
|
||||
try:
|
||||
node.write(safe_vals)
|
||||
return {'ok': True}
|
||||
except Exception as exc:
|
||||
_logger.exception('Recipe write_node failed')
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Delete node
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/recipe/node/unlink', type='jsonrpc', auth='user')
|
||||
def unlink_node(self, node_id):
|
||||
"""Delete a node and all its children (cascade)."""
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
node = Node.browse(int(node_id))
|
||||
if not node.exists():
|
||||
return {'ok': False, 'error': 'Node not found.'}
|
||||
if node.node_type == 'recipe':
|
||||
return {'ok': False, 'error': 'Cannot delete a recipe root from the tree editor. Use the list view.'}
|
||||
|
||||
try:
|
||||
name = node.name
|
||||
node.unlink()
|
||||
_logger.info('Recipe: deleted node %s (%s) by uid %s',
|
||||
node_id, name, request.env.uid)
|
||||
return {'ok': True}
|
||||
except Exception as exc:
|
||||
_logger.exception('Recipe unlink_node failed')
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reorder siblings
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/recipe/node/reorder', type='jsonrpc', auth='user')
|
||||
def reorder_nodes(self, node_ids):
|
||||
"""Bulk-update sequence for an ordered list of sibling node IDs."""
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
try:
|
||||
for idx, nid in enumerate(node_ids):
|
||||
Node.browse(int(nid)).write({'sequence': (idx + 1) * 10})
|
||||
return {'ok': True}
|
||||
except Exception as exc:
|
||||
_logger.exception('Recipe reorder failed')
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Move node to new parent
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/recipe/node/move', type='jsonrpc', auth='user')
|
||||
def move_node(self, node_id, new_parent_id):
|
||||
"""Move a node to a new parent (drag between sub-trees)."""
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
node = Node.browse(int(node_id))
|
||||
parent = Node.browse(int(new_parent_id))
|
||||
if not node.exists() or not parent.exists():
|
||||
return {'ok': False, 'error': 'Node or parent not found.'}
|
||||
|
||||
# Prevent moving a recipe root
|
||||
if node.node_type == 'recipe':
|
||||
return {'ok': False, 'error': 'Cannot move a recipe root.'}
|
||||
|
||||
# Prevent making a node its own descendant
|
||||
if f'/{node.id}/' in (parent.parent_path or ''):
|
||||
return {'ok': False, 'error': 'Cannot move a node under its own descendant.'}
|
||||
|
||||
try:
|
||||
max_seq = max((c.sequence for c in parent.child_ids), default=0)
|
||||
node.write({
|
||||
'parent_id': parent.id,
|
||||
'sequence': max_seq + 10,
|
||||
})
|
||||
return {'ok': True}
|
||||
except Exception as exc:
|
||||
_logger.exception('Recipe move_node failed')
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Duplicate recipe
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/recipe/duplicate', type='jsonrpc', auth='user')
|
||||
def duplicate_recipe(self, recipe_id):
|
||||
"""Deep-copy an entire recipe tree."""
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
recipe = Node.browse(int(recipe_id))
|
||||
if not recipe.exists():
|
||||
return {'ok': False, 'error': 'Recipe not found.'}
|
||||
if recipe.node_type != 'recipe':
|
||||
return {'ok': False, 'error': 'Can only duplicate recipe roots.'}
|
||||
|
||||
try:
|
||||
new_recipe = recipe.copy()
|
||||
return {'ok': True, 'recipe_id': new_recipe.id}
|
||||
except Exception as exc:
|
||||
_logger.exception('Recipe duplicate failed')
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
322
fusion_plating/fusion_plating/data/fp_demo_data.xml
Normal file
322
fusion_plating/fusion_plating/data/fp_demo_data.xml
Normal 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 & 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>
|
||||
262
fusion_plating/fusion_plating/data/fp_demo_recipe_data.xml
Normal file
262
fusion_plating/fusion_plating/data/fp_demo_recipe_data.xml
Normal file
@@ -0,0 +1,262 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Demo recipe: Electroless Nickel Plating — Steel Line
|
||||
-->
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- ===== ROOT: Electroless Nickel Plating — Steel Line ===== -->
|
||||
<record id="demo_recipe_en_steel" model="fusion.plating.process.node">
|
||||
<field name="name">Electroless Nickel Plating — Steel Line</field>
|
||||
<field name="code">EN_STEEL</field>
|
||||
<field name="node_type">recipe</field>
|
||||
<field name="icon">fa-flask</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="customer_visible">True</field>
|
||||
</record>
|
||||
|
||||
<!-- 1. Blasting -->
|
||||
<record id="demo_node_blasting" model="fusion.plating.process.node">
|
||||
<field name="name">Blasting</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_recipe_en_steel"/>
|
||||
<field name="icon">fa-bullseye</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="estimated_duration">30</field>
|
||||
<field name="is_manual">True</field>
|
||||
</record>
|
||||
<record id="demo_step_ready_blast" model="fusion.plating.process.node">
|
||||
<field name="name">Ready for Blast</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_blasting"/>
|
||||
<field name="icon">fa-clock-o</field>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
<record id="demo_step_blast" model="fusion.plating.process.node">
|
||||
<field name="name">Blast</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_blasting"/>
|
||||
<field name="icon">fa-fire</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
<!-- 2. Masking -->
|
||||
<record id="demo_node_masking" model="fusion.plating.process.node">
|
||||
<field name="name">Masking</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_recipe_en_steel"/>
|
||||
<field name="icon">fa-paint-brush</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="estimated_duration">45</field>
|
||||
<field name="is_manual">True</field>
|
||||
</record>
|
||||
<record id="demo_step_ready_mask" model="fusion.plating.process.node">
|
||||
<field name="name">Ready for Masking</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_masking"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
<record id="demo_step_mask" model="fusion.plating.process.node">
|
||||
<field name="name">Masking</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_masking"/>
|
||||
<field name="icon">fa-paint-brush</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
<!-- 3. Racking -->
|
||||
<record id="demo_node_racking" model="fusion.plating.process.node">
|
||||
<field name="name">Racking</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_recipe_en_steel"/>
|
||||
<field name="icon">fa-th</field>
|
||||
<field name="sequence">30</field>
|
||||
<field name="estimated_duration">20</field>
|
||||
</record>
|
||||
|
||||
<!-- 4. Steel Line (sub-process with many children) -->
|
||||
<record id="demo_node_steel_line" model="fusion.plating.process.node">
|
||||
<field name="name">Steel Line</field>
|
||||
<field name="node_type">sub_process</field>
|
||||
<field name="parent_id" ref="demo_recipe_en_steel"/>
|
||||
<field name="icon">fa-industry</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="auto_complete">True</field>
|
||||
</record>
|
||||
|
||||
<!-- 4a. Cleaner (sub-process inside Steel Line) -->
|
||||
<record id="demo_node_cleaner" model="fusion.plating.process.node">
|
||||
<field name="name">Cleaner</field>
|
||||
<field name="node_type">sub_process</field>
|
||||
<field name="parent_id" ref="demo_node_steel_line"/>
|
||||
<field name="icon">fa-shower</field>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
<record id="demo_step_soak_clean" model="fusion.plating.process.node">
|
||||
<field name="name">Soak Clean (S-3)</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_cleaner"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="estimated_duration">10</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
<record id="demo_step_electroclean" model="fusion.plating.process.node">
|
||||
<field name="name">Electroclean (S-3)</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_cleaner"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="estimated_duration">5</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
<record id="demo_step_primary_rinse_1" model="fusion.plating.process.node">
|
||||
<field name="name">Primary Rinse (S-4)</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_cleaner"/>
|
||||
<field name="sequence">30</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
|
||||
<!-- 4b. Acid Dip -->
|
||||
<record id="demo_node_acid_dip" model="fusion.plating.process.node">
|
||||
<field name="name">Acid Dip (S-5)</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_node_steel_line"/>
|
||||
<field name="icon">fa-flask</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="estimated_duration">5</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
|
||||
<!-- 4c. Nickel Strike -->
|
||||
<record id="demo_node_nickel_strike" model="fusion.plating.process.node">
|
||||
<field name="name">Nickel Strike (S-7 / SP-5)</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_node_steel_line"/>
|
||||
<field name="icon">fa-bolt</field>
|
||||
<field name="sequence">30</field>
|
||||
<field name="estimated_duration">8</field>
|
||||
<field name="is_manual">False</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
<!-- 4d. E-Nickel Plate (Mid Phos) -->
|
||||
<record id="demo_node_en_plate" model="fusion.plating.process.node">
|
||||
<field name="name">E-Nickel Plate (Mid Phos) (S-9)</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_node_steel_line"/>
|
||||
<field name="icon">fa-diamond</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="estimated_duration">90</field>
|
||||
<field name="is_manual">False</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
<record id="demo_step_rinse_after_plate" model="fusion.plating.process.node">
|
||||
<field name="name">Primary Rinse (S-11)</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_en_plate"/>
|
||||
<field name="sequence">10</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
<record id="demo_step_hot_rinse" model="fusion.plating.process.node">
|
||||
<field name="name">Hot Rinse (S-13)</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_en_plate"/>
|
||||
<field name="sequence">20</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
|
||||
<!-- 4e. Hot Water Porosity -->
|
||||
<record id="demo_node_porosity" model="fusion.plating.process.node">
|
||||
<field name="name">Hot Water Porosity (A-15)</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_node_steel_line"/>
|
||||
<field name="icon">fa-tint</field>
|
||||
<field name="sequence">50</field>
|
||||
<field name="estimated_duration">15</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
<!-- 4f. Dry -->
|
||||
<record id="demo_node_dry" model="fusion.plating.process.node">
|
||||
<field name="name">Dry</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_node_steel_line"/>
|
||||
<field name="icon">fa-sun-o</field>
|
||||
<field name="sequence">60</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
|
||||
<!-- 5. Oven Baking -->
|
||||
<record id="demo_node_oven_bake" model="fusion.plating.process.node">
|
||||
<field name="name">Oven Baking</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_recipe_en_steel"/>
|
||||
<field name="icon">fa-fire</field>
|
||||
<field name="sequence">50</field>
|
||||
<field name="estimated_duration">240</field>
|
||||
<field name="is_manual">False</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
<!-- 6. De-racking -->
|
||||
<record id="demo_node_derack" model="fusion.plating.process.node">
|
||||
<field name="name">De-Racking</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_recipe_en_steel"/>
|
||||
<field name="icon">fa-th</field>
|
||||
<field name="sequence">60</field>
|
||||
<field name="estimated_duration">15</field>
|
||||
</record>
|
||||
|
||||
<!-- 7. De-Masking -->
|
||||
<record id="demo_node_demask" model="fusion.plating.process.node">
|
||||
<field name="name">De-Masking</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_recipe_en_steel"/>
|
||||
<field name="icon">fa-eraser</field>
|
||||
<field name="sequence">70</field>
|
||||
<field name="estimated_duration">20</field>
|
||||
</record>
|
||||
|
||||
<!-- 8. Oven Bake (Post De-Rack) -->
|
||||
<record id="demo_node_post_bake" model="fusion.plating.process.node">
|
||||
<field name="name">Oven Bake (Post De-Rack)</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_recipe_en_steel"/>
|
||||
<field name="icon">fa-fire</field>
|
||||
<field name="sequence">80</field>
|
||||
<field name="estimated_duration">120</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
|
||||
<!-- 9. Post Plate Inspection -->
|
||||
<record id="demo_node_inspection" model="fusion.plating.process.node">
|
||||
<field name="name">Post Plate Inspection</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="demo_recipe_en_steel"/>
|
||||
<field name="icon">fa-search</field>
|
||||
<field name="sequence">90</field>
|
||||
<field name="estimated_duration">30</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
<record id="demo_step_ready_inspect" model="fusion.plating.process.node">
|
||||
<field name="name">Ready for Post Plate Inspection</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_inspection"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
<record id="demo_step_inspect" model="fusion.plating.process.node">
|
||||
<field name="name">Post Plate Inspection</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="demo_node_inspection"/>
|
||||
<field name="icon">fa-check-circle</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -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>
|
||||
233
fusion_plating/fusion_plating/data/fp_recipe_enp_alum_basic.xml
Normal file
233
fusion_plating/fusion_plating/data/fp_recipe_enp_alum_basic.xml
Normal file
@@ -0,0 +1,233 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Recipe: ENP-ALUM-BASIC (Electroless Nickel Plating — Aluminium Basic)
|
||||
Source: Client's Steelhead export
|
||||
-->
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<!-- ===== ROOT ===== -->
|
||||
<record id="recipe_enp_alum_basic" model="fusion.plating.process.node">
|
||||
<field name="name">ENP-ALUM-BASIC</field>
|
||||
<field name="code">ENP_ALUM_BASIC</field>
|
||||
<field name="node_type">recipe</field>
|
||||
<field name="icon">fa-flask</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="auto_complete">True</field>
|
||||
<field name="customer_visible">False</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 1. Masking ===== -->
|
||||
<record id="enp_ab_masking" model="fusion.plating.process.node">
|
||||
<field name="name">Masking</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="recipe_enp_alum_basic"/>
|
||||
<field name="icon">fa-paint-brush</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="auto_complete">True</field>
|
||||
<field name="is_manual">True</field>
|
||||
</record>
|
||||
<record id="enp_ab_masking_ready" model="fusion.plating.process.node">
|
||||
<field name="name">Ready For Masking</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_masking"/>
|
||||
<field name="icon">fa-clock-o</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="is_manual">True</field>
|
||||
</record>
|
||||
<record id="enp_ab_masking_do" model="fusion.plating.process.node">
|
||||
<field name="name">Masking</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_masking"/>
|
||||
<field name="icon">fa-paint-brush</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="is_manual">True</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 2. Racking ===== -->
|
||||
<record id="enp_ab_racking" model="fusion.plating.process.node">
|
||||
<field name="name">Racking</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="recipe_enp_alum_basic"/>
|
||||
<field name="icon">fa-th</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="auto_complete">True</field>
|
||||
</record>
|
||||
<record id="enp_ab_racking_ready" model="fusion.plating.process.node">
|
||||
<field name="name">Ready for Racking</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_racking"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
<record id="enp_ab_racking_do" model="fusion.plating.process.node">
|
||||
<field name="name">Racking</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_racking"/>
|
||||
<field name="icon">fa-th</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 3. Ready for processing ===== -->
|
||||
<record id="enp_ab_ready_processing" model="fusion.plating.process.node">
|
||||
<field name="name">Ready for processing</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="recipe_enp_alum_basic"/>
|
||||
<field name="icon">fa-clock-o</field>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 4. ENP-Alum Line (sub-process) ===== -->
|
||||
<record id="enp_ab_alum_line" model="fusion.plating.process.node">
|
||||
<field name="name">ENP-Alum Line</field>
|
||||
<field name="node_type">sub_process</field>
|
||||
<field name="parent_id" ref="recipe_enp_alum_basic"/>
|
||||
<field name="icon">fa-industry</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="auto_complete">True</field>
|
||||
</record>
|
||||
<!-- 4a. E-Nickel Plating -->
|
||||
<record id="enp_ab_enickel_plating" model="fusion.plating.process.node">
|
||||
<field name="name">E-Nickel Plating</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="enp_ab_alum_line"/>
|
||||
<field name="icon">fa-diamond</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 5. De-Masking ===== -->
|
||||
<record id="enp_ab_demasking" model="fusion.plating.process.node">
|
||||
<field name="name">De-Masking</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="recipe_enp_alum_basic"/>
|
||||
<field name="icon">fa-eraser</field>
|
||||
<field name="sequence">50</field>
|
||||
<field name="auto_complete">True</field>
|
||||
</record>
|
||||
<record id="enp_ab_demasking_ready" model="fusion.plating.process.node">
|
||||
<field name="name">Ready for De-Masking</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_demasking"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
<record id="enp_ab_demasking_do" model="fusion.plating.process.node">
|
||||
<field name="name">De-Masking</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_demasking"/>
|
||||
<field name="icon">fa-eraser</field>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 6. Oven baking ===== -->
|
||||
<record id="enp_ab_oven_bake" model="fusion.plating.process.node">
|
||||
<field name="name">Oven baking</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="recipe_enp_alum_basic"/>
|
||||
<field name="icon">fa-fire</field>
|
||||
<field name="sequence">60</field>
|
||||
<field name="auto_complete">True</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
<record id="enp_ab_oven_bake_ready" model="fusion.plating.process.node">
|
||||
<field name="name">Ready for bake</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_oven_bake"/>
|
||||
<field name="icon">fa-clock-o</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="is_manual">True</field>
|
||||
</record>
|
||||
<record id="enp_ab_oven_bake_do" model="fusion.plating.process.node">
|
||||
<field name="name">Bake</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_oven_bake"/>
|
||||
<field name="icon">fa-fire</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="is_manual">True</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 7. De-racking ===== -->
|
||||
<record id="enp_ab_deracking" model="fusion.plating.process.node">
|
||||
<field name="name">De-racking</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="recipe_enp_alum_basic"/>
|
||||
<field name="icon">fa-th</field>
|
||||
<field name="sequence">70</field>
|
||||
<field name="auto_complete">True</field>
|
||||
</record>
|
||||
<record id="enp_ab_deracking_ready" model="fusion.plating.process.node">
|
||||
<field name="name">Ready For DeRacking</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_deracking"/>
|
||||
<field name="icon">fa-clock-o</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="is_manual">True</field>
|
||||
</record>
|
||||
<record id="enp_ab_deracking_do" model="fusion.plating.process.node">
|
||||
<field name="name">DeRacking</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_deracking"/>
|
||||
<field name="icon">fa-th</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 8. Oven bake (Post de-rack) ===== -->
|
||||
<record id="enp_ab_post_bake" model="fusion.plating.process.node">
|
||||
<field name="name">Oven bake (Post de-rack)</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="recipe_enp_alum_basic"/>
|
||||
<field name="icon">fa-fire</field>
|
||||
<field name="sequence">80</field>
|
||||
<field name="auto_complete">True</field>
|
||||
<field name="is_manual">False</field>
|
||||
</record>
|
||||
<record id="enp_ab_post_bake_ready" model="fusion.plating.process.node">
|
||||
<field name="name">Ready for bake</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_post_bake"/>
|
||||
<field name="icon">fa-clock-o</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="is_manual">True</field>
|
||||
</record>
|
||||
<record id="enp_ab_post_bake_do" model="fusion.plating.process.node">
|
||||
<field name="name">Bake</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_post_bake"/>
|
||||
<field name="icon">fa-fire</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="is_manual">True</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== 9. Post-plate Inspection ===== -->
|
||||
<record id="enp_ab_inspection" model="fusion.plating.process.node">
|
||||
<field name="name">Post-plate Inspection</field>
|
||||
<field name="node_type">operation</field>
|
||||
<field name="parent_id" ref="recipe_enp_alum_basic"/>
|
||||
<field name="icon">fa-search</field>
|
||||
<field name="sequence">90</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
<record id="enp_ab_inspection_ready" model="fusion.plating.process.node">
|
||||
<field name="name">Ready for post-plate Inspection</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_inspection"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
<record id="enp_ab_inspection_do" model="fusion.plating.process.node">
|
||||
<field name="name">Post-plate Inspection</field>
|
||||
<field name="node_type">step</field>
|
||||
<field name="parent_id" ref="enp_ab_inspection"/>
|
||||
<field name="icon">fa-check-circle</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="requires_signoff">True</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
26
fusion_plating/fusion_plating/data/fp_sequence_data.xml
Normal file
26
fusion_plating/fusion_plating/data/fp_sequence_data.xml
Normal 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>
|
||||
16
fusion_plating/fusion_plating/models/__init__.py
Normal file
16
fusion_plating/fusion_plating/models/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import fp_process_category
|
||||
from . import fp_process_type
|
||||
from . import fp_facility
|
||||
from . import fp_work_center
|
||||
from . import fp_tank
|
||||
from . import fp_bath
|
||||
from . import fp_bath_log
|
||||
from . import fp_bath_log_line
|
||||
from . import fp_bath_parameter
|
||||
from . import fp_process_node
|
||||
from . import res_company
|
||||
269
fusion_plating/fusion_plating/models/fp_bath.py
Normal file
269
fusion_plating/fusion_plating/models/fp_bath.py
Normal 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.',
|
||||
),
|
||||
]
|
||||
144
fusion_plating/fusion_plating/models/fp_bath_log.py
Normal file
144
fusion_plating/fusion_plating/models/fp_bath_log.py
Normal 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)
|
||||
114
fusion_plating/fusion_plating/models/fp_bath_log_line.py
Normal file
114
fusion_plating/fusion_plating/models/fp_bath_log_line.py
Normal 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'
|
||||
88
fusion_plating/fusion_plating/models/fp_bath_parameter.py
Normal file
88
fusion_plating/fusion_plating/models/fp_bath_parameter.py
Normal 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.',
|
||||
),
|
||||
]
|
||||
102
fusion_plating/fusion_plating/models/fp_facility.py
Normal file
102
fusion_plating/fusion_plating/models/fp_facility.py
Normal 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]
|
||||
62
fusion_plating/fusion_plating/models/fp_process_category.py
Normal file
62
fusion_plating/fusion_plating/models/fp_process_category.py
Normal 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)
|
||||
401
fusion_plating/fusion_plating/models/fp_process_node.py
Normal file
401
fusion_plating/fusion_plating/models/fp_process_node.py
Normal file
@@ -0,0 +1,401 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class FpProcessNode(models.Model):
|
||||
"""A node in the process recipe tree.
|
||||
|
||||
Recipes are hierarchical templates that define how to plate a part.
|
||||
They are reusable across production orders and serve as the single
|
||||
source of truth for the shop's plating processes.
|
||||
|
||||
Node types
|
||||
----------
|
||||
* recipe — top-level root (e.g. "Electroless Nickel — Steel Line")
|
||||
* sub_process — a group of operations (e.g. "Steel Line", "Cleaner")
|
||||
* operation — a single production step (e.g. "Acid Dip", "Nickel Strike")
|
||||
* step — a sub-step within an operation (e.g. "Ready for Blast", "Blast")
|
||||
|
||||
Hierarchy uses Odoo's _parent_store for efficient tree queries.
|
||||
"""
|
||||
_name = 'fusion.plating.process.node'
|
||||
_description = 'Fusion Plating — Process Node'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_parent_store = True
|
||||
_parent_name = 'parent_id'
|
||||
_order = 'parent_path, sequence, id'
|
||||
_rec_name = 'display_name'
|
||||
|
||||
# ---- Identity & hierarchy ------------------------------------------------
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
help='Optional short code (e.g. EN_STEEL).',
|
||||
tracking=True,
|
||||
)
|
||||
node_type = fields.Selection(
|
||||
[
|
||||
('recipe', 'Recipe'),
|
||||
('sub_process', 'Sub-Process'),
|
||||
('operation', 'Operation'),
|
||||
('step', 'Step'),
|
||||
],
|
||||
string='Type',
|
||||
required=True,
|
||||
default='operation',
|
||||
tracking=True,
|
||||
)
|
||||
parent_id = fields.Many2one(
|
||||
'fusion.plating.process.node',
|
||||
string='Parent',
|
||||
ondelete='cascade',
|
||||
index=True,
|
||||
)
|
||||
parent_path = fields.Char(
|
||||
index=True,
|
||||
)
|
||||
child_ids = fields.One2many(
|
||||
'fusion.plating.process.node',
|
||||
'parent_id',
|
||||
string='Child Steps',
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
)
|
||||
depth = fields.Integer(
|
||||
string='Depth',
|
||||
compute='_compute_depth',
|
||||
store=True,
|
||||
)
|
||||
|
||||
# ---- Process references --------------------------------------------------
|
||||
|
||||
process_type_id = fields.Many2one(
|
||||
'fusion.plating.process.type',
|
||||
string='Process Type',
|
||||
ondelete='restrict',
|
||||
tracking=True,
|
||||
)
|
||||
work_center_id = fields.Many2one(
|
||||
'fusion.plating.work.center',
|
||||
string='Work Centre',
|
||||
ondelete='set null',
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
# ---- Content & metadata --------------------------------------------------
|
||||
|
||||
description = fields.Html(
|
||||
string='Description',
|
||||
help='Rich text instructions for this step.',
|
||||
)
|
||||
notes = fields.Text(
|
||||
string='Internal Notes',
|
||||
help='Internal notes (not shown to customers).',
|
||||
)
|
||||
icon = fields.Selection(
|
||||
[
|
||||
('fa-flask', 'Flask / Chemistry'),
|
||||
('fa-industry', 'Industry / Line'),
|
||||
('fa-sitemap', 'Sitemap / Process'),
|
||||
('fa-wrench', 'Wrench / Operation'),
|
||||
('fa-cog', 'Gear / General'),
|
||||
('fa-cogs', 'Gears / System'),
|
||||
('fa-paint-brush', 'Paint / Masking'),
|
||||
('fa-eraser', 'Eraser / De-Masking'),
|
||||
('fa-th', 'Grid / Racking'),
|
||||
('fa-fire', 'Fire / Bake'),
|
||||
('fa-bolt', 'Bolt / Electric'),
|
||||
('fa-diamond', 'Diamond / Plating'),
|
||||
('fa-tint', 'Tint / Rinse'),
|
||||
('fa-shower', 'Shower / Clean'),
|
||||
('fa-bullseye', 'Target / Blast'),
|
||||
('fa-search', 'Search / Inspect'),
|
||||
('fa-check-circle', 'Check / Approve'),
|
||||
('fa-clock-o', 'Clock / Wait'),
|
||||
('fa-sun-o', 'Sun / Dry'),
|
||||
('fa-thermometer-half', 'Temp / Heat'),
|
||||
('fa-eye', 'Eye / Visual'),
|
||||
('fa-hand-paper-o', 'Hand / Manual'),
|
||||
('fa-cube', 'Cube / Part'),
|
||||
('fa-shield', 'Shield / Protect'),
|
||||
],
|
||||
string='Icon',
|
||||
default='fa-cog',
|
||||
)
|
||||
color = fields.Integer(
|
||||
string='Colour',
|
||||
default=0,
|
||||
)
|
||||
|
||||
# ---- Timing --------------------------------------------------------------
|
||||
|
||||
estimated_duration = fields.Float(
|
||||
string='Estimated Duration (min)',
|
||||
help='Expected time in minutes.',
|
||||
)
|
||||
|
||||
# ---- Behaviour flags -----------------------------------------------------
|
||||
|
||||
auto_complete = fields.Boolean(
|
||||
string='Auto-Complete',
|
||||
default=False,
|
||||
help='Automatically marks done when all children complete.',
|
||||
)
|
||||
customer_visible = fields.Boolean(
|
||||
string='Customer Visible',
|
||||
default=True,
|
||||
help='Whether to show this step name to customers.',
|
||||
)
|
||||
is_manual = fields.Boolean(
|
||||
string='Manual Operation',
|
||||
default=True,
|
||||
help='Unchecked = automated (e.g. timed immersion).',
|
||||
)
|
||||
requires_signoff = fields.Boolean(
|
||||
string='Requires Sign-Off',
|
||||
default=False,
|
||||
help='Quality hold point — requires operator sign-off.',
|
||||
)
|
||||
opt_in_out = fields.Selection(
|
||||
[
|
||||
('disabled', 'Disabled'),
|
||||
('opt_in', 'Opt-In'),
|
||||
('opt_out', 'Opt-Out'),
|
||||
],
|
||||
string='Opt In/Out',
|
||||
default='disabled',
|
||||
help='Controls whether this step is optional for a given job.',
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
# ---- Lifecycle -----------------------------------------------------------
|
||||
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
version = fields.Integer(
|
||||
string='Version',
|
||||
default=1,
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
# ---- Computed fields -----------------------------------------------------
|
||||
|
||||
display_name = fields.Char(
|
||||
compute='_compute_display_name',
|
||||
store=True,
|
||||
recursive=True,
|
||||
)
|
||||
child_count = fields.Integer(
|
||||
string='Children',
|
||||
compute='_compute_child_count',
|
||||
)
|
||||
recipe_root_id = fields.Many2one(
|
||||
'fusion.plating.process.node',
|
||||
string='Recipe Root',
|
||||
compute='_compute_recipe_root_id',
|
||||
store=True,
|
||||
)
|
||||
|
||||
# ---- Operator inputs (one2many) ------------------------------------------
|
||||
|
||||
input_ids = fields.One2many(
|
||||
'fusion.plating.process.node.input',
|
||||
'node_id',
|
||||
string='Operator Inputs',
|
||||
)
|
||||
|
||||
# ---- SQL constraints -----------------------------------------------------
|
||||
|
||||
_sql_constraints = [
|
||||
('fp_process_node_code_uniq',
|
||||
'unique(code)',
|
||||
'Recipe node code must be unique.'),
|
||||
]
|
||||
|
||||
# ---- Computes ------------------------------------------------------------
|
||||
|
||||
@api.depends('name', 'code', 'parent_id.display_name')
|
||||
def _compute_display_name(self):
|
||||
for rec in self:
|
||||
if rec.parent_id and rec.node_type != 'recipe':
|
||||
rec.display_name = f'{rec.parent_id.display_name} / {rec.name}'
|
||||
else:
|
||||
rec.display_name = rec.name or ''
|
||||
|
||||
@api.depends('parent_path')
|
||||
def _compute_depth(self):
|
||||
for rec in self:
|
||||
rec.depth = (rec.parent_path or '').count('/') - 1
|
||||
|
||||
@api.depends('child_ids')
|
||||
def _compute_child_count(self):
|
||||
for rec in self:
|
||||
rec.child_count = len(rec.child_ids)
|
||||
|
||||
@api.depends('parent_path')
|
||||
def _compute_recipe_root_id(self):
|
||||
for rec in self:
|
||||
if rec.parent_path:
|
||||
root_id = int(rec.parent_path.split('/')[0])
|
||||
rec.recipe_root_id = root_id
|
||||
else:
|
||||
rec.recipe_root_id = rec.id
|
||||
|
||||
# ---- Constraints ---------------------------------------------------------
|
||||
|
||||
@api.constrains('parent_id')
|
||||
def _check_recursion_constraint(self):
|
||||
if not self._check_recursion():
|
||||
raise ValidationError(
|
||||
_('A process node cannot be its own ancestor.'))
|
||||
|
||||
# ---- Tree data for OWL component -----------------------------------------
|
||||
|
||||
def get_tree_data(self):
|
||||
"""Return full nested dict for the OWL recipe tree editor.
|
||||
|
||||
Called via the controller. Returns the tree rooted at `self`,
|
||||
recursively including all descendants.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self._node_to_dict()
|
||||
|
||||
def _node_to_dict(self, max_depth=10):
|
||||
"""Recursively convert this node + children to a dict."""
|
||||
if max_depth <= 0:
|
||||
return None
|
||||
children = []
|
||||
for child in self.child_ids.sorted('sequence'):
|
||||
child_dict = child._node_to_dict(max_depth=max_depth - 1)
|
||||
if child_dict:
|
||||
children.append(child_dict)
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name or '',
|
||||
'code': self.code or '',
|
||||
'node_type': self.node_type,
|
||||
'sequence': self.sequence,
|
||||
'depth': self.depth,
|
||||
'icon': self.icon or 'fa-cog',
|
||||
'color': self.color,
|
||||
'process_type': self.process_type_id.name if self.process_type_id else '',
|
||||
'process_type_id': self.process_type_id.id if self.process_type_id else False,
|
||||
'work_center': self.work_center_id.name if self.work_center_id else '',
|
||||
'work_center_id': self.work_center_id.id if self.work_center_id else False,
|
||||
'description': self.description or '',
|
||||
'notes': self.notes or '',
|
||||
'estimated_duration': self.estimated_duration,
|
||||
'auto_complete': self.auto_complete,
|
||||
'customer_visible': self.customer_visible,
|
||||
'is_manual': self.is_manual,
|
||||
'requires_signoff': self.requires_signoff,
|
||||
'version': self.version,
|
||||
'child_count': len(children),
|
||||
'opt_in_out': self.opt_in_out or 'disabled',
|
||||
'input_count': len(self.input_ids),
|
||||
'create_date': self.create_date.isoformat() if self.create_date else '',
|
||||
'create_uid_name': self.create_uid.name if self.create_uid else '',
|
||||
'write_date': self.write_date.isoformat() if self.write_date else '',
|
||||
'write_uid_name': self.write_uid.name if self.write_uid else '',
|
||||
'children': children,
|
||||
}
|
||||
|
||||
# ---- Actions -------------------------------------------------------------
|
||||
|
||||
def action_open_tree_editor(self):
|
||||
"""Open the OWL recipe tree editor for this recipe."""
|
||||
self.ensure_one()
|
||||
root = self if self.node_type == 'recipe' else self.recipe_root_id
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'fp_recipe_tree_editor',
|
||||
'name': f'Recipe — {root.name}',
|
||||
'context': {'recipe_id': root.id},
|
||||
}
|
||||
|
||||
# ---- Copy (deep-duplicate) -----------------------------------------------
|
||||
|
||||
def copy(self, default=None):
|
||||
"""Deep-copy: duplicates the node and all descendants."""
|
||||
default = dict(default or {})
|
||||
if self.node_type == 'recipe':
|
||||
default.setdefault('name', _('%s (Copy)', self.name))
|
||||
default.setdefault('code', f'{self.code}_copy' if self.code else False)
|
||||
new_node = super().copy(default)
|
||||
for child in self.child_ids.sorted('sequence'):
|
||||
child.copy({'parent_id': new_node.id})
|
||||
return new_node
|
||||
|
||||
|
||||
class FpProcessNodeInput(models.Model):
|
||||
"""An operator input definition attached to a process node.
|
||||
|
||||
These define what the operator needs to record when executing this
|
||||
step — temperature readings, visual inspections, timing, etc.
|
||||
"""
|
||||
_name = 'fusion.plating.process.node.input'
|
||||
_description = 'Fusion Plating — Process Node Input'
|
||||
_order = 'sequence, id'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
help='E.g. "Temperature Reading", "Visual Inspection".',
|
||||
)
|
||||
node_id = fields.Many2one(
|
||||
'fusion.plating.process.node',
|
||||
string='Process Node',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
input_type = fields.Selection(
|
||||
[
|
||||
('text', 'Text'),
|
||||
('number', 'Number'),
|
||||
('boolean', 'Yes / No'),
|
||||
('selection', 'Selection'),
|
||||
('photo', 'Photo'),
|
||||
],
|
||||
string='Input Type',
|
||||
required=True,
|
||||
default='text',
|
||||
)
|
||||
required = fields.Boolean(
|
||||
string='Required',
|
||||
default=False,
|
||||
)
|
||||
hint = fields.Char(
|
||||
string='Hint',
|
||||
help='Placeholder text shown to the operator.',
|
||||
)
|
||||
selection_options = fields.Text(
|
||||
string='Options',
|
||||
help='Comma-separated list of options (for Selection type).',
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence',
|
||||
default=10,
|
||||
)
|
||||
uom = fields.Char(
|
||||
string='Unit',
|
||||
help='Unit label (e.g. °C, min, psi).',
|
||||
)
|
||||
92
fusion_plating/fusion_plating/models/fp_process_type.py
Normal file
92
fusion_plating/fusion_plating/models/fp_process_type.py
Normal 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]
|
||||
170
fusion_plating/fusion_plating/models/fp_tank.py
Normal file
170
fusion_plating/fusion_plating/models/fp_tank.py
Normal 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)
|
||||
72
fusion_plating/fusion_plating/models/fp_work_center.py
Normal file
72
fusion_plating/fusion_plating/models/fp_work_center.py
Normal 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)
|
||||
30
fusion_plating/fusion_plating/models/res_company.py
Normal file
30
fusion_plating/fusion_plating/models/res_company.py
Normal 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)
|
||||
85
fusion_plating/fusion_plating/security/fp_security.xml
Normal file
85
fusion_plating/fusion_plating/security/fp_security.xml
Normal 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>
|
||||
34
fusion_plating/fusion_plating/security/ir.model.access.csv
Normal file
34
fusion_plating/fusion_plating/security/ir.model.access.csv
Normal file
@@ -0,0 +1,34 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_fp_process_category_operator,fp.process.category.operator,model_fusion_plating_process_category,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_process_category_manager,fp.process.category.manager,model_fusion_plating_process_category,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_process_type_operator,fp.process.type.operator,model_fusion_plating_process_type,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_process_type_manager,fp.process.type.manager,model_fusion_plating_process_type,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_bath_parameter_operator,fp.bath.parameter.operator,model_fusion_plating_bath_parameter,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_bath_parameter_manager,fp.bath.parameter.manager,model_fusion_plating_bath_parameter,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_facility_operator,fp.facility.operator,model_fusion_plating_facility,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_facility_supervisor,fp.facility.supervisor,model_fusion_plating_facility,group_fusion_plating_supervisor,1,0,0,0
|
||||
access_fp_facility_manager,fp.facility.manager,model_fusion_plating_facility,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_work_center_operator,fp.work.center.operator,model_fusion_plating_work_center,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_work_center_supervisor,fp.work.center.supervisor,model_fusion_plating_work_center,group_fusion_plating_supervisor,1,1,0,0
|
||||
access_fp_work_center_manager,fp.work.center.manager,model_fusion_plating_work_center,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_tank_operator,fp.tank.operator,model_fusion_plating_tank,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_tank_supervisor,fp.tank.supervisor,model_fusion_plating_tank,group_fusion_plating_supervisor,1,1,0,0
|
||||
access_fp_tank_manager,fp.tank.manager,model_fusion_plating_tank,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_bath_operator,fp.bath.operator,model_fusion_plating_bath,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_bath_supervisor,fp.bath.supervisor,model_fusion_plating_bath,group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_bath_manager,fp.bath.manager,model_fusion_plating_bath,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_bath_target_operator,fp.bath.target.operator,model_fusion_plating_bath_target,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_bath_target_supervisor,fp.bath.target.supervisor,model_fusion_plating_bath_target,group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_bath_target_manager,fp.bath.target.manager,model_fusion_plating_bath_target,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_bath_log_operator,fp.bath.log.operator,model_fusion_plating_bath_log,group_fusion_plating_operator,1,1,1,0
|
||||
access_fp_bath_log_supervisor,fp.bath.log.supervisor,model_fusion_plating_bath_log,group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_bath_log_manager,fp.bath.log.manager,model_fusion_plating_bath_log,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_bath_log_line_operator,fp.bath.log.line.operator,model_fusion_plating_bath_log_line,group_fusion_plating_operator,1,1,1,0
|
||||
access_fp_bath_log_line_supervisor,fp.bath.log.line.supervisor,model_fusion_plating_bath_log_line,group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_bath_log_line_manager,fp.bath.log.line.manager,model_fusion_plating_bath_log_line,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_process_node_operator,fp.process.node.operator,model_fusion_plating_process_node,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_process_node_supervisor,fp.process.node.supervisor,model_fusion_plating_process_node,group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_process_node_manager,fp.process.node.manager,model_fusion_plating_process_node,group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_process_node_input_operator,fp.process.node.input.operator,model_fusion_plating_process_node_input,group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_process_node_input_supervisor,fp.process.node.input.supervisor,model_fusion_plating_process_node_input,group_fusion_plating_supervisor,1,1,1,0
|
||||
access_fp_process_node_input_manager,fp.process.node.input.manager,model_fusion_plating_process_node_input,group_fusion_plating_manager,1,1,1,1
|
||||
|
BIN
fusion_plating/fusion_plating/static/description/icon.png
Normal file
BIN
fusion_plating/fusion_plating/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,488 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — Recipe Tree Editor (OWL backend client action)
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
//
|
||||
// Professional tree editor for process recipes. Renders the full
|
||||
// node hierarchy with connector lines, expand/collapse, click-to-edit
|
||||
// side panel, add/delete operations, and drag-and-drop reorder.
|
||||
//
|
||||
// Odoo 19 conventions:
|
||||
// * Backend OWL: static template + static props = ["*"]
|
||||
// * RPC: standalone rpc() from @web/core/network/rpc
|
||||
// * Registered under registry.category("actions") → "fp_recipe_tree_editor"
|
||||
// =============================================================================
|
||||
|
||||
import { Component, useState, useRef, onMounted, onWillUnmount } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
// ---- Node type metadata ---------------------------------------------------
|
||||
const NODE_TYPES = {
|
||||
recipe: { label: "Recipe", icon: "fa-flask", badgeClass: "o_fp_recipe_badge_recipe" },
|
||||
sub_process: { label: "Sub-Process", icon: "fa-sitemap", badgeClass: "o_fp_recipe_badge_sub" },
|
||||
operation: { label: "Operation", icon: "fa-wrench", badgeClass: "o_fp_recipe_badge_op" },
|
||||
step: { label: "Step", icon: "fa-dot-circle-o", badgeClass: "o_fp_recipe_badge_step" },
|
||||
};
|
||||
|
||||
const NODE_TYPE_OPTIONS = [
|
||||
{ value: "sub_process", label: "Sub-Process" },
|
||||
{ value: "operation", label: "Operation" },
|
||||
{ value: "step", label: "Step" },
|
||||
];
|
||||
|
||||
// ---- Icon picker options (curated for plating / manufacturing) -----------
|
||||
const ICON_OPTIONS = [
|
||||
{ value: "fa-flask", label: "Flask / Chemistry" },
|
||||
{ value: "fa-industry", label: "Industry / Line" },
|
||||
{ value: "fa-sitemap", label: "Sitemap / Process" },
|
||||
{ value: "fa-wrench", label: "Wrench / Operation" },
|
||||
{ value: "fa-cog", label: "Gear / General" },
|
||||
{ value: "fa-cogs", label: "Gears / System" },
|
||||
{ value: "fa-paint-brush", label: "Paint / Masking" },
|
||||
{ value: "fa-eraser", label: "Eraser / De-Masking" },
|
||||
{ value: "fa-th", label: "Grid / Racking" },
|
||||
{ value: "fa-fire", label: "Fire / Bake" },
|
||||
{ value: "fa-bolt", label: "Bolt / Electric" },
|
||||
{ value: "fa-diamond", label: "Diamond / Plating" },
|
||||
{ value: "fa-tint", label: "Tint / Rinse" },
|
||||
{ value: "fa-shower", label: "Shower / Clean" },
|
||||
{ value: "fa-bullseye", label: "Target / Blast" },
|
||||
{ value: "fa-search", label: "Search / Inspect" },
|
||||
{ value: "fa-check-circle", label: "Check / Approve" },
|
||||
{ value: "fa-clock-o", label: "Clock / Wait" },
|
||||
{ value: "fa-sun-o", label: "Sun / Dry" },
|
||||
{ value: "fa-thermometer-half", label: "Temp / Heat" },
|
||||
{ value: "fa-eye", label: "Eye / Visual" },
|
||||
{ value: "fa-hand-paper-o", label: "Hand / Manual" },
|
||||
{ value: "fa-cube", label: "Cube / Part" },
|
||||
{ value: "fa-shield", label: "Shield / Protect" },
|
||||
];
|
||||
|
||||
// ---- Auto-icon: guess the best icon from the node name ------------------
|
||||
const ICON_KEYWORDS = [
|
||||
{ pattern: /mask/i, icon: "fa-paint-brush" },
|
||||
{ pattern: /de-?mask|unmask/i, icon: "fa-eraser" },
|
||||
{ pattern: /rack/i, icon: "fa-th" },
|
||||
{ pattern: /de-?rack|unrack/i, icon: "fa-th" },
|
||||
{ pattern: /blast/i, icon: "fa-bullseye" },
|
||||
{ pattern: /bake|oven/i, icon: "fa-fire" },
|
||||
{ pattern: /clean|soak|wash/i, icon: "fa-shower" },
|
||||
{ pattern: /rinse/i, icon: "fa-tint" },
|
||||
{ pattern: /dry/i, icon: "fa-sun-o" },
|
||||
{ pattern: /nickel|plate|plat/i, icon: "fa-diamond" },
|
||||
{ pattern: /strike|electro/i, icon: "fa-bolt" },
|
||||
{ pattern: /acid|dip|etch/i, icon: "fa-flask" },
|
||||
{ pattern: /inspect|check|test/i, icon: "fa-search" },
|
||||
{ pattern: /ready|wait|queue/i, icon: "fa-clock-o" },
|
||||
{ pattern: /line|process/i, icon: "fa-industry" },
|
||||
{ pattern: /heat|temp/i, icon: "fa-thermometer-half" },
|
||||
{ pattern: /porosity/i, icon: "fa-tint" },
|
||||
];
|
||||
|
||||
function guessIcon(name) {
|
||||
if (!name) return "fa-cog";
|
||||
for (const rule of ICON_KEYWORDS) {
|
||||
if (rule.pattern.test(name)) return rule.icon;
|
||||
}
|
||||
return "fa-cog";
|
||||
}
|
||||
|
||||
export class RecipeTreeEditor extends Component {
|
||||
static template = "fusion_plating.RecipeTreeEditor";
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.action = useService("action");
|
||||
this.dialog = useService("dialog");
|
||||
|
||||
this.state = useState({
|
||||
recipe: null,
|
||||
tree: null,
|
||||
loading: false,
|
||||
saving: false,
|
||||
selectedNodeId: null,
|
||||
selectedNode: null,
|
||||
expandedNodes: {},
|
||||
showPanel: false,
|
||||
// Add-node form
|
||||
addingTo: null, // parent node id when "add" dialog is open
|
||||
newNodeName: "",
|
||||
newNodeType: "operation",
|
||||
});
|
||||
|
||||
this._recipeId = null;
|
||||
|
||||
onMounted(async () => {
|
||||
const ctx = this.props.action?.context || {};
|
||||
this._recipeId = ctx.recipe_id || null;
|
||||
if (this._recipeId) {
|
||||
await this.loadTree();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Data loading -------------------------------------------------------
|
||||
|
||||
async loadTree() {
|
||||
this.state.loading = true;
|
||||
try {
|
||||
const result = await rpc("/fp/recipe/tree", {
|
||||
recipe_id: this._recipeId,
|
||||
});
|
||||
if (result && result.ok) {
|
||||
this.state.recipe = result.recipe;
|
||||
this.state.tree = result.tree;
|
||||
// Auto-expand root node
|
||||
if (result.tree) {
|
||||
this.state.expandedNodes[result.tree.id] = true;
|
||||
}
|
||||
// Refresh selected node data if panel is open
|
||||
if (this.state.selectedNodeId) {
|
||||
this.state.selectedNode = this._findNode(
|
||||
result.tree, this.state.selectedNodeId
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.notification.add(
|
||||
result?.error || "Failed to load recipe.",
|
||||
{ type: "danger" }
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
this.notification.add(`Load failed: ${err.message || err}`, { type: "danger" });
|
||||
} finally {
|
||||
this.state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Tree traversal helpers ---------------------------------------------
|
||||
|
||||
_findNode(node, id) {
|
||||
if (!node) return null;
|
||||
if (node.id === id) return node;
|
||||
for (const child of (node.children || [])) {
|
||||
const found = this._findNode(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---- Expand / collapse --------------------------------------------------
|
||||
|
||||
isExpanded(nodeId) {
|
||||
return !!this.state.expandedNodes[nodeId];
|
||||
}
|
||||
|
||||
toggleExpand(nodeId) {
|
||||
this.state.expandedNodes[nodeId] = !this.state.expandedNodes[nodeId];
|
||||
}
|
||||
|
||||
// ---- Node selection (side panel) ----------------------------------------
|
||||
|
||||
selectNode(node) {
|
||||
if (this.state.selectedNodeId === node.id) {
|
||||
// Toggle panel off
|
||||
this.state.selectedNodeId = null;
|
||||
this.state.selectedNode = null;
|
||||
this.state.showPanel = false;
|
||||
} else {
|
||||
this.state.selectedNodeId = node.id;
|
||||
this.state.selectedNode = { ...node };
|
||||
this.state.showPanel = true;
|
||||
}
|
||||
}
|
||||
|
||||
closePanel() {
|
||||
this.state.selectedNodeId = null;
|
||||
this.state.selectedNode = null;
|
||||
this.state.showPanel = false;
|
||||
}
|
||||
|
||||
// ---- Node editing (panel save) ------------------------------------------
|
||||
|
||||
async saveNode() {
|
||||
const node = this.state.selectedNode;
|
||||
if (!node) return;
|
||||
this.state.saving = true;
|
||||
try {
|
||||
const vals = {
|
||||
name: node.name,
|
||||
icon: node.icon,
|
||||
node_type: node.node_type,
|
||||
estimated_duration: node.estimated_duration || 0,
|
||||
auto_complete: node.auto_complete,
|
||||
customer_visible: node.customer_visible,
|
||||
is_manual: node.is_manual,
|
||||
requires_signoff: node.requires_signoff,
|
||||
};
|
||||
const result = await rpc("/fp/recipe/node/write", {
|
||||
node_id: node.id,
|
||||
vals,
|
||||
});
|
||||
if (result && result.ok) {
|
||||
this.notification.add("Saved", { type: "success" });
|
||||
await this.loadTree();
|
||||
} else {
|
||||
this.notification.add(result?.error || "Save failed.", { type: "warning" });
|
||||
}
|
||||
} catch (err) {
|
||||
this.notification.add(`Save failed: ${err.message || err}`, { type: "danger" });
|
||||
} finally {
|
||||
this.state.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Add child node -----------------------------------------------------
|
||||
|
||||
startAddChild(parentId) {
|
||||
this.state.addingTo = parentId;
|
||||
this.state.newNodeName = "";
|
||||
this.state.newNodeType = "operation";
|
||||
// Auto-expand parent
|
||||
this.state.expandedNodes[parentId] = true;
|
||||
}
|
||||
|
||||
cancelAdd() {
|
||||
this.state.addingTo = null;
|
||||
}
|
||||
|
||||
async confirmAdd() {
|
||||
const name = (this.state.newNodeName || "").trim();
|
||||
if (!name) {
|
||||
this.notification.add("Name is required.", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
this.state.saving = true;
|
||||
try {
|
||||
const result = await rpc("/fp/recipe/node/create", {
|
||||
parent_id: this.state.addingTo,
|
||||
name: name,
|
||||
node_type: this.state.newNodeType,
|
||||
vals: { icon: guessIcon(name) },
|
||||
});
|
||||
if (result && result.ok) {
|
||||
this.notification.add(`Added "${name}"`, { type: "success" });
|
||||
this.state.addingTo = null;
|
||||
await this.loadTree();
|
||||
} else {
|
||||
this.notification.add(result?.error || "Add failed.", { type: "warning" });
|
||||
}
|
||||
} catch (err) {
|
||||
this.notification.add(`Add failed: ${err.message || err}`, { type: "danger" });
|
||||
} finally {
|
||||
this.state.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
onAddNameKey(ev) {
|
||||
if (ev.key === "Enter") {
|
||||
this.confirmAdd();
|
||||
} else if (ev.key === "Escape") {
|
||||
this.cancelAdd();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Delete node --------------------------------------------------------
|
||||
|
||||
async deleteNode(nodeId) {
|
||||
const node = this._findNode(this.state.tree, nodeId);
|
||||
if (!node) return;
|
||||
if (node.node_type === "recipe") {
|
||||
this.notification.add("Cannot delete the recipe root.", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
const childWarning = node.child_count > 0
|
||||
? ` and its ${node.child_count} child step(s)`
|
||||
: "";
|
||||
if (!confirm(`Delete "${node.name}"${childWarning}?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await rpc("/fp/recipe/node/unlink", { node_id: nodeId });
|
||||
if (result && result.ok) {
|
||||
this.notification.add(`Deleted "${node.name}"`, { type: "success" });
|
||||
if (this.state.selectedNodeId === nodeId) {
|
||||
this.closePanel();
|
||||
}
|
||||
await this.loadTree();
|
||||
} else {
|
||||
this.notification.add(result?.error || "Delete failed.", { type: "warning" });
|
||||
}
|
||||
} catch (err) {
|
||||
this.notification.add(`Delete failed: ${err.message || err}`, { type: "danger" });
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Drag & drop reorder ------------------------------------------------
|
||||
|
||||
onNodeDragStart(node, parentNode, ev) {
|
||||
if (node.node_type === "recipe") {
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
this._draggedNode = {
|
||||
id: node.id,
|
||||
parentId: parentNode ? parentNode.id : null,
|
||||
};
|
||||
ev.dataTransfer.effectAllowed = "move";
|
||||
ev.dataTransfer.setData("text/plain", String(node.id));
|
||||
requestAnimationFrame(() => {
|
||||
ev.target.classList.add("o_fp_recipe_drag_ghost");
|
||||
});
|
||||
}
|
||||
|
||||
onNodeDragEnd(ev) {
|
||||
this._draggedNode = null;
|
||||
ev.target.classList.remove("o_fp_recipe_drag_ghost");
|
||||
document.querySelectorAll(".o_fp_recipe_drop_target").forEach(el => {
|
||||
el.classList.remove("o_fp_recipe_drop_target");
|
||||
});
|
||||
}
|
||||
|
||||
onNodeDragOver(node, ev) {
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect = "move";
|
||||
ev.currentTarget.classList.add("o_fp_recipe_drop_target");
|
||||
}
|
||||
|
||||
onNodeDragLeave(ev) {
|
||||
if (!ev.currentTarget.contains(ev.relatedTarget)) {
|
||||
ev.currentTarget.classList.remove("o_fp_recipe_drop_target");
|
||||
}
|
||||
}
|
||||
|
||||
async onNodeDrop(targetNode, parentNode, ev) {
|
||||
ev.preventDefault();
|
||||
ev.currentTarget.classList.remove("o_fp_recipe_drop_target");
|
||||
const dragged = this._draggedNode;
|
||||
if (!dragged || dragged.id === targetNode.id) return;
|
||||
|
||||
// If dropping on a node with children, move into it
|
||||
// If dropping on a sibling, reorder within parent
|
||||
const targetParentId = parentNode ? parentNode.id : null;
|
||||
|
||||
if (dragged.parentId === targetParentId) {
|
||||
// Reorder within same parent — swap positions
|
||||
const siblings = parentNode
|
||||
? (parentNode.children || [])
|
||||
: [this.state.tree];
|
||||
const ids = siblings.map(c => c.id);
|
||||
const fromIdx = ids.indexOf(dragged.id);
|
||||
const toIdx = ids.indexOf(targetNode.id);
|
||||
if (fromIdx === -1 || toIdx === -1) return;
|
||||
ids.splice(fromIdx, 1);
|
||||
ids.splice(toIdx, 0, dragged.id);
|
||||
|
||||
try {
|
||||
const result = await rpc("/fp/recipe/node/reorder", { node_ids: ids });
|
||||
if (result && result.ok) {
|
||||
await this.loadTree();
|
||||
}
|
||||
} catch (err) {
|
||||
this.notification.add(`Reorder failed: ${err.message}`, { type: "danger" });
|
||||
}
|
||||
} else {
|
||||
// Move to new parent
|
||||
try {
|
||||
const result = await rpc("/fp/recipe/node/move", {
|
||||
node_id: dragged.id,
|
||||
new_parent_id: targetNode.id,
|
||||
});
|
||||
if (result && result.ok) {
|
||||
this.state.expandedNodes[targetNode.id] = true;
|
||||
await this.loadTree();
|
||||
} else {
|
||||
this.notification.add(result?.error || "Move failed.", { type: "warning" });
|
||||
}
|
||||
} catch (err) {
|
||||
this.notification.add(`Move failed: ${err.message}`, { type: "danger" });
|
||||
}
|
||||
}
|
||||
this._draggedNode = null;
|
||||
}
|
||||
|
||||
// ---- Navigation ---------------------------------------------------------
|
||||
|
||||
onBackToList() {
|
||||
this.action.doAction("fusion_plating.action_fp_process_recipe");
|
||||
}
|
||||
|
||||
onOpenForm(nodeId) {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "fusion.plating.process.node",
|
||||
res_id: nodeId,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
});
|
||||
}
|
||||
|
||||
async onDuplicate() {
|
||||
if (!this._recipeId) return;
|
||||
try {
|
||||
const result = await rpc("/fp/recipe/duplicate", {
|
||||
recipe_id: this._recipeId,
|
||||
});
|
||||
if (result && result.ok) {
|
||||
this.notification.add("Recipe duplicated.", { type: "success" });
|
||||
this._recipeId = result.recipe_id;
|
||||
await this.loadTree();
|
||||
} else {
|
||||
this.notification.add(result?.error || "Duplicate failed.", { type: "warning" });
|
||||
}
|
||||
} catch (err) {
|
||||
this.notification.add(`Duplicate failed: ${err.message}`, { type: "danger" });
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Helpers ------------------------------------------------------------
|
||||
|
||||
getNodeTypeMeta(type) {
|
||||
return NODE_TYPES[type] || NODE_TYPES.operation;
|
||||
}
|
||||
|
||||
getNodeTypeOptions() {
|
||||
return NODE_TYPE_OPTIONS;
|
||||
}
|
||||
|
||||
getIconOptions() {
|
||||
return ICON_OPTIONS;
|
||||
}
|
||||
|
||||
formatTimeAgo(isoStr) {
|
||||
if (!isoStr) return "";
|
||||
const date = new Date(isoStr);
|
||||
const now = new Date();
|
||||
let diff = Math.floor((now - date) / 1000); // seconds
|
||||
if (diff < 0) diff = 0;
|
||||
const parts = [];
|
||||
const weeks = Math.floor(diff / 604800);
|
||||
diff %= 604800;
|
||||
const days = Math.floor(diff / 86400);
|
||||
diff %= 86400;
|
||||
const hours = Math.floor(diff / 3600);
|
||||
diff %= 3600;
|
||||
const minutes = Math.floor(diff / 60);
|
||||
const seconds = diff % 60;
|
||||
if (weeks) parts.push(`${weeks}w`);
|
||||
if (days) parts.push(`${days}d`);
|
||||
if (hours) parts.push(`${hours}h`);
|
||||
if (minutes) parts.push(`${minutes}m`);
|
||||
parts.push(`${seconds}s`);
|
||||
return parts.join(" ") + " ago";
|
||||
}
|
||||
|
||||
formatDuration(minutes) {
|
||||
if (!minutes) return "";
|
||||
if (minutes < 60) return `${Math.round(minutes)}m`;
|
||||
const h = Math.floor(minutes / 60);
|
||||
const m = Math.round(minutes % 60);
|
||||
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("actions").add("fp_recipe_tree_editor", RecipeTreeEditor);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
// =============================================================================
|
||||
// Fusion Plating — Recipe Tree Editor
|
||||
// Copyright 2026 Nexa Systems Inc.
|
||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||
//
|
||||
// THEME AWARENESS
|
||||
// ---------------
|
||||
// All colours from CSS custom properties + SCSS $border-color.
|
||||
// Works in both light and dark mode.
|
||||
// =============================================================================
|
||||
|
||||
// ---- Root container ---------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background: var(--o-view-background-color, var(--bs-body-bg));
|
||||
}
|
||||
|
||||
// ---- Header -----------------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
background: var(--bs-body-bg);
|
||||
border-bottom: 1px solid $border-color;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.o_fp_recipe_header_left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.o_fp_recipe_back_btn {
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.o_fp_recipe_title {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.o_fp_recipe_version_badge {
|
||||
background: var(--bs-secondary-color);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.o_fp_recipe_header_right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Body (tree + panel) layout ---------------------------------------------
|
||||
|
||||
.o_fp_recipe_body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.o_fp_recipe_tree_area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 24px 24px 40px;
|
||||
}
|
||||
|
||||
// ---- Side panel -------------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_panel {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
transition: width 0.2s ease;
|
||||
border-left: 1px solid $border-color;
|
||||
background: var(--bs-body-bg);
|
||||
|
||||
&.o_fp_recipe_panel_open {
|
||||
width: 340px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.o_fp_recipe_panel_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_recipe_panel_body {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Connector lines --------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_connector {
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: $border-color;
|
||||
margin-left: 22px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
// ---- Node card --------------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_node {
|
||||
position: relative;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: $border-color;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
max-width: 520px;
|
||||
cursor: pointer;
|
||||
background: var(--bs-body-bg);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
transition: box-shadow 0.15s, border-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
border-color: var(--o-action, var(--bs-primary));
|
||||
}
|
||||
|
||||
&.o_fp_recipe_node_selected {
|
||||
border-color: var(--o-action, var(--bs-primary));
|
||||
box-shadow: 0 0 0 2px rgba(var(--bs-primary-rgb, 13, 110, 253), 0.2);
|
||||
}
|
||||
|
||||
// Node type left accent
|
||||
&.o_fp_recipe_node_recipe {
|
||||
border-left: 5px solid var(--bs-primary);
|
||||
}
|
||||
&.o_fp_recipe_node_sub_process {
|
||||
border-left: 5px solid var(--bs-info);
|
||||
}
|
||||
&.o_fp_recipe_node_operation {
|
||||
border-left: 5px solid var(--bs-success);
|
||||
}
|
||||
&.o_fp_recipe_node_step {
|
||||
border-left: 5px solid var(--bs-secondary);
|
||||
}
|
||||
|
||||
// Drag states
|
||||
&.o_fp_recipe_drag_ghost {
|
||||
opacity: 0.35;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
&.o_fp_recipe_drop_target {
|
||||
border-color: var(--o-action, var(--bs-primary));
|
||||
background: color-mix(in srgb, var(--o-action, var(--bs-primary)) 6%, var(--bs-body-bg));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Drag handle ------------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_drag_handle {
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--bs-secondary-color);
|
||||
cursor: grab;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
font-size: 0.85rem;
|
||||
|
||||
.o_fp_recipe_node:hover & {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Node header row --------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_node_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.o_fp_recipe_toggle_btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--bs-secondary-color);
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
font-size: 0.75rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_recipe_toggle_spacer {
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.o_fp_recipe_node_icon {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: 0.9rem;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.o_fp_recipe_node_name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--bs-body-color);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.o_fp_recipe_node_badge {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.o_fp_recipe_badge_recipe {
|
||||
background: var(--bs-primary);
|
||||
color: #fff;
|
||||
}
|
||||
&.o_fp_recipe_badge_sub {
|
||||
background: var(--bs-info);
|
||||
color: #fff;
|
||||
}
|
||||
&.o_fp_recipe_badge_op {
|
||||
background: var(--bs-success);
|
||||
color: #fff;
|
||||
}
|
||||
&.o_fp_recipe_badge_step {
|
||||
background: var(--bs-secondary);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Node meta row ----------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_node_meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--bs-secondary-color);
|
||||
padding-left: 28px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.o_fp_recipe_node_wc,
|
||||
.o_fp_recipe_node_duration {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.o_fp_recipe_node_icons {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--bs-secondary-color);
|
||||
|
||||
i {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Node action buttons ----------------------------------------------------
|
||||
|
||||
.o_fp_recipe_node_actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding-left: 28px;
|
||||
margin-top: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
|
||||
.o_fp_recipe_node:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.o_fp_recipe_add_btn {
|
||||
font-size: 0.72rem;
|
||||
color: var(--bs-success);
|
||||
border: 1px solid var(--bs-success);
|
||||
padding: 1px 8px;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: var(--bs-success);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_recipe_delete_btn {
|
||||
font-size: 0.72rem;
|
||||
color: var(--bs-danger);
|
||||
border: 1px solid transparent;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bs-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Add child form ---------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_add_form {
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
.o_fp_recipe_add_card {
|
||||
border: 1px dashed var(--bs-success);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
max-width: 520px;
|
||||
background: color-mix(in srgb, var(--bs-success) 4%, var(--bs-body-bg));
|
||||
}
|
||||
|
||||
// ---- Children container (indentation) ---------------------------------------
|
||||
|
||||
.o_fp_recipe_children {
|
||||
margin-left: 32px;
|
||||
padding-top: 0;
|
||||
position: relative;
|
||||
|
||||
// Vertical guide line
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 22px;
|
||||
top: 0;
|
||||
bottom: 16px;
|
||||
width: 2px;
|
||||
background: $border-color;
|
||||
border-radius: 1px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Tracking section -------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_tracking {
|
||||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
// ---- Icon picker ------------------------------------------------------------
|
||||
|
||||
.o_fp_recipe_icon_picker {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.o_fp_recipe_icon_btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.12s, background-color 0.12s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--o-action, var(--bs-primary));
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--o-action, var(--bs-primary));
|
||||
border-color: var(--o-action, var(--bs-primary));
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Responsive -------------------------------------------------------------
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.o_fp_recipe_tree_area {
|
||||
padding: 16px 12px 16px 24px;
|
||||
}
|
||||
|
||||
.o_fp_recipe_node {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.o_fp_recipe_panel.o_fp_recipe_panel_open {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.o_fp_recipe_children {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_plating.RecipeTreeEditor">
|
||||
<div class="o_fp_recipe_editor">
|
||||
|
||||
<!-- ========== HEADER ========== -->
|
||||
<div class="o_fp_recipe_header">
|
||||
<div class="o_fp_recipe_header_left">
|
||||
<button class="btn btn-link o_fp_recipe_back_btn"
|
||||
t-on-click="onBackToList" title="Back to list">
|
||||
<i class="fa fa-arrow-left me-1"/> Recipes
|
||||
</button>
|
||||
<h2 class="o_fp_recipe_title" t-if="state.recipe">
|
||||
<i class="fa fa-flask me-2"/>
|
||||
<t t-esc="state.recipe.name"/>
|
||||
<span class="badge rounded-pill o_fp_recipe_version_badge ms-2"
|
||||
t-if="state.recipe.version">
|
||||
v<t t-esc="state.recipe.version"/>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="o_fp_recipe_header_right" t-if="state.recipe">
|
||||
<span class="text-muted small me-3" t-if="state.recipe.process_type">
|
||||
<i class="fa fa-tag me-1"/>
|
||||
<t t-esc="state.recipe.process_type"/>
|
||||
</span>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1"
|
||||
t-on-click="onDuplicate" title="Duplicate recipe">
|
||||
<i class="fa fa-copy me-1"/> Duplicate
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
t-on-click="() => this.onOpenForm(state.recipe.id)"
|
||||
title="Edit in form view">
|
||||
<i class="fa fa-pencil me-1"/> Form View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== LOADING ========== -->
|
||||
<div class="text-center py-5" t-if="state.loading and !state.tree">
|
||||
<i class="fa fa-spinner fa-spin fa-2x"/>
|
||||
<p class="mt-2 text-muted">Loading recipe tree...</p>
|
||||
</div>
|
||||
|
||||
<!-- ========== NO RECIPE ========== -->
|
||||
<div class="text-center py-5" t-if="!state.loading and !_recipeId">
|
||||
<i class="fa fa-exclamation-triangle fa-3x text-muted"/>
|
||||
<p class="mt-3 text-muted">No recipe selected.</p>
|
||||
</div>
|
||||
|
||||
<!-- ========== TREE + PANEL LAYOUT ========== -->
|
||||
<div class="o_fp_recipe_body" t-if="state.tree">
|
||||
|
||||
<!-- Tree area -->
|
||||
<div class="o_fp_recipe_tree_area">
|
||||
<t t-call="fusion_plating.RecipeTreeNode">
|
||||
<t t-set="node" t-value="state.tree"/>
|
||||
<t t-set="parentNode" t-value="null"/>
|
||||
<t t-set="isFirst" t-value="true"/>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Side panel -->
|
||||
<div t-att-class="'o_fp_recipe_panel' + (state.showPanel ? ' o_fp_recipe_panel_open' : '')">
|
||||
<t t-if="state.showPanel and state.selectedNode">
|
||||
<div class="o_fp_recipe_panel_header">
|
||||
<h5>
|
||||
<i t-att-class="'fa ' + (state.selectedNode.icon || 'fa-cog') + ' me-2'"/>
|
||||
Edit Node
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-link" t-on-click="closePanel">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="o_fp_recipe_panel_body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Name</label>
|
||||
<input type="text" class="form-control"
|
||||
t-att-value="state.selectedNode.name"
|
||||
t-on-change="(ev) => { state.selectedNode.name = ev.target.value; }"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Type</label>
|
||||
<select class="form-select"
|
||||
t-on-change="(ev) => { state.selectedNode.node_type = ev.target.value; }">
|
||||
<option value="recipe"
|
||||
t-att-selected="state.selectedNode.node_type === 'recipe'">Recipe</option>
|
||||
<option value="sub_process"
|
||||
t-att-selected="state.selectedNode.node_type === 'sub_process'">Sub-Process</option>
|
||||
<option value="operation"
|
||||
t-att-selected="state.selectedNode.node_type === 'operation'">Operation</option>
|
||||
<option value="step"
|
||||
t-att-selected="state.selectedNode.node_type === 'step'">Step</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Icon</label>
|
||||
<div class="o_fp_recipe_icon_picker">
|
||||
<t t-foreach="getIconOptions()" t-as="ic" t-key="ic.value">
|
||||
<button t-att-class="'o_fp_recipe_icon_btn' + (state.selectedNode.icon === ic.value ? ' active' : '')"
|
||||
t-on-click.stop="() => { state.selectedNode.icon = ic.value; }"
|
||||
t-att-title="ic.label">
|
||||
<i t-att-class="'fa ' + ic.value"/>
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Duration (min)</label>
|
||||
<input type="number" class="form-control" min="0" step="1"
|
||||
t-att-value="state.selectedNode.estimated_duration || 0"
|
||||
t-on-change="(ev) => { state.selectedNode.estimated_duration = parseFloat(ev.target.value) || 0; }"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold d-block">Flags</label>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="fp_chk_manual"
|
||||
t-att-checked="state.selectedNode.is_manual"
|
||||
t-on-change="(ev) => { state.selectedNode.is_manual = ev.target.checked; }"/>
|
||||
<label class="form-check-label" for="fp_chk_manual">Manual operation</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="fp_chk_auto"
|
||||
t-att-checked="state.selectedNode.auto_complete"
|
||||
t-on-change="(ev) => { state.selectedNode.auto_complete = ev.target.checked; }"/>
|
||||
<label class="form-check-label" for="fp_chk_auto">Auto-complete</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="fp_chk_signoff"
|
||||
t-att-checked="state.selectedNode.requires_signoff"
|
||||
t-on-change="(ev) => { state.selectedNode.requires_signoff = ev.target.checked; }"/>
|
||||
<label class="form-check-label" for="fp_chk_signoff">Requires sign-off</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="fp_chk_visible"
|
||||
t-att-checked="state.selectedNode.customer_visible"
|
||||
t-on-change="(ev) => { state.selectedNode.customer_visible = ev.target.checked; }"/>
|
||||
<label class="form-check-label" for="fp_chk_visible">Customer visible</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Opt In/Out</label>
|
||||
<select class="form-select"
|
||||
t-on-change="(ev) => { state.selectedNode.opt_in_out = ev.target.value; }">
|
||||
<option value="disabled"
|
||||
t-att-selected="state.selectedNode.opt_in_out === 'disabled'">Disabled</option>
|
||||
<option value="opt_in"
|
||||
t-att-selected="state.selectedNode.opt_in_out === 'opt_in'">Opt-In</option>
|
||||
<option value="opt_out"
|
||||
t-att-selected="state.selectedNode.opt_in_out === 'opt_out'">Opt-Out</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Info -->
|
||||
<div class="text-muted small mb-2" t-if="state.selectedNode.work_center">
|
||||
<i class="fa fa-building me-1"/>
|
||||
<t t-esc="state.selectedNode.work_center"/>
|
||||
</div>
|
||||
<div class="text-muted small mb-2" t-if="state.selectedNode.process_type">
|
||||
<i class="fa fa-tag me-1"/>
|
||||
<t t-esc="state.selectedNode.process_type"/>
|
||||
</div>
|
||||
<div class="text-muted small mb-2"
|
||||
t-if="state.selectedNode.input_count">
|
||||
<i class="fa fa-keyboard-o me-1"/>
|
||||
<t t-esc="state.selectedNode.input_count"/> operator input(s)
|
||||
</div>
|
||||
<!-- Tracking -->
|
||||
<div class="o_fp_recipe_tracking mt-3 pt-3" t-if="state.selectedNode.create_date">
|
||||
<div class="text-muted small mb-1">
|
||||
<i class="fa fa-calendar-plus-o me-1"/>
|
||||
Created <t t-esc="formatTimeAgo(state.selectedNode.create_date)"/>
|
||||
<t t-if="state.selectedNode.create_uid_name">
|
||||
by <strong t-esc="state.selectedNode.create_uid_name"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="text-muted small" t-if="state.selectedNode.write_date">
|
||||
<i class="fa fa-pencil me-1"/>
|
||||
Updated <t t-esc="formatTimeAgo(state.selectedNode.write_date)"/>
|
||||
<t t-if="state.selectedNode.write_uid_name">
|
||||
by <strong t-esc="state.selectedNode.write_uid_name"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button class="btn btn-primary flex-fill"
|
||||
t-on-click="saveNode"
|
||||
t-att-disabled="state.saving">
|
||||
<i t-att-class="state.saving ? 'fa fa-spinner fa-spin me-1' : 'fa fa-check me-1'"/>
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary"
|
||||
t-on-click="() => this.onOpenForm(state.selectedNode.id)"
|
||||
title="Open full form">
|
||||
<i class="fa fa-external-link"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- ========== RECURSIVE NODE TEMPLATE ========== -->
|
||||
<t t-name="fusion_plating.RecipeTreeNode">
|
||||
<!-- Connector line (skip for root) -->
|
||||
<div class="o_fp_recipe_connector" t-if="!isFirst"/>
|
||||
|
||||
<!-- Node card -->
|
||||
<div t-att-class="'o_fp_recipe_node'
|
||||
+ (state.selectedNodeId === node.id ? ' o_fp_recipe_node_selected' : '')
|
||||
+ ' o_fp_recipe_node_' + node.node_type"
|
||||
t-att-draggable="node.node_type !== 'recipe' ? 'true' : 'false'"
|
||||
t-on-dragstart="(ev) => this.onNodeDragStart(node, parentNode, ev)"
|
||||
t-on-dragend="(ev) => this.onNodeDragEnd(ev)"
|
||||
t-on-dragover="(ev) => this.onNodeDragOver(node, ev)"
|
||||
t-on-dragleave="(ev) => this.onNodeDragLeave(ev)"
|
||||
t-on-drop="(ev) => this.onNodeDrop(node, parentNode, ev)"
|
||||
t-on-click.stop="() => this.selectNode(node)">
|
||||
|
||||
<!-- Drag handle (non-root only) -->
|
||||
<span class="o_fp_recipe_drag_handle" t-if="node.node_type !== 'recipe'">
|
||||
<i class="fa fa-grip-vertical"/>
|
||||
</span>
|
||||
|
||||
<!-- Node header row -->
|
||||
<div class="o_fp_recipe_node_header">
|
||||
<!-- Expand/collapse toggle -->
|
||||
<button class="o_fp_recipe_toggle_btn"
|
||||
t-if="node.children and node.children.length"
|
||||
t-on-click.stop="() => this.toggleExpand(node.id)">
|
||||
<i t-att-class="isExpanded(node.id) ? 'fa fa-chevron-down' : 'fa fa-chevron-right'"/>
|
||||
</button>
|
||||
<span class="o_fp_recipe_toggle_spacer" t-else=""/>
|
||||
|
||||
<!-- Icon -->
|
||||
<i t-att-class="'o_fp_recipe_node_icon fa ' + (node.icon || 'fa-cog')"/>
|
||||
|
||||
<!-- Name -->
|
||||
<span class="o_fp_recipe_node_name">
|
||||
<t t-esc="node.name"/>
|
||||
</span>
|
||||
|
||||
<!-- Type badge -->
|
||||
<span t-att-class="'badge o_fp_recipe_node_badge ' + getNodeTypeMeta(node.node_type).badgeClass">
|
||||
<t t-esc="getNodeTypeMeta(node.node_type).label"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Meta row: work centre, duration, capability icons -->
|
||||
<div class="o_fp_recipe_node_meta">
|
||||
<span class="o_fp_recipe_node_wc" t-if="node.work_center">
|
||||
<i class="fa fa-building me-1"/>
|
||||
<t t-esc="node.work_center"/>
|
||||
</span>
|
||||
<span class="o_fp_recipe_node_duration" t-if="node.estimated_duration">
|
||||
<i class="fa fa-clock-o me-1"/>
|
||||
<t t-esc="formatDuration(node.estimated_duration)"/>
|
||||
</span>
|
||||
<!-- Capability icons -->
|
||||
<span class="o_fp_recipe_node_icons">
|
||||
<i class="fa fa-hand-paper-o" t-if="node.is_manual" title="Manual"/>
|
||||
<i class="fa fa-bolt" t-if="!node.is_manual" title="Automated"/>
|
||||
<i class="fa fa-check-square" t-if="node.requires_signoff" title="Requires sign-off"/>
|
||||
<i class="fa fa-eye" t-if="node.customer_visible" title="Customer visible"/>
|
||||
<i class="fa fa-magic" t-if="node.auto_complete" title="Auto-complete"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons row -->
|
||||
<div class="o_fp_recipe_node_actions">
|
||||
<button class="btn btn-sm o_fp_recipe_add_btn"
|
||||
t-on-click.stop="() => this.startAddChild(node.id)"
|
||||
title="Add child step">
|
||||
<i class="fa fa-plus me-1"/> Add Step
|
||||
</button>
|
||||
<button class="btn btn-sm o_fp_recipe_delete_btn"
|
||||
t-if="node.node_type !== 'recipe'"
|
||||
t-on-click.stop="() => this.deleteNode(node.id)"
|
||||
title="Delete">
|
||||
<i class="fa fa-trash"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add child inline form -->
|
||||
<div class="o_fp_recipe_add_form" t-if="state.addingTo === node.id">
|
||||
<div class="o_fp_recipe_connector"/>
|
||||
<div class="o_fp_recipe_add_card">
|
||||
<input type="text" class="form-control form-control-sm mb-2"
|
||||
placeholder="New step name..."
|
||||
t-att-value="state.newNodeName"
|
||||
t-on-input="(ev) => { state.newNodeName = ev.target.value; }"
|
||||
t-on-keydown="onAddNameKey"/>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm flex-shrink-1"
|
||||
style="max-width: 140px;"
|
||||
t-on-change="(ev) => { state.newNodeType = ev.target.value; }">
|
||||
<t t-foreach="getNodeTypeOptions()" t-as="opt" t-key="opt.value">
|
||||
<option t-att-value="opt.value"
|
||||
t-att-selected="state.newNodeType === opt.value"
|
||||
t-esc="opt.label"/>
|
||||
</t>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-primary" t-on-click="confirmAdd">
|
||||
<i class="fa fa-check"/>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" t-on-click="cancelAdd">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Children (recursive) -->
|
||||
<div class="o_fp_recipe_children" t-if="node.children and node.children.length and isExpanded(node.id)">
|
||||
<t t-foreach="node.children" t-as="child" t-key="child.id">
|
||||
<t t-call="fusion_plating.RecipeTreeNode">
|
||||
<t t-set="node" t-value="child"/>
|
||||
<t t-set="parentNode" t-value="node"/>
|
||||
<t t-set="isFirst" t-value="false"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
127
fusion_plating/fusion_plating/views/fp_bath_log_views.xml
Normal file
127
fusion_plating/fusion_plating/views/fp_bath_log_views.xml
Normal 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','>=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="This Week" name="week"
|
||||
domain="[('log_date','>=', (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>
|
||||
198
fusion_plating/fusion_plating/views/fp_bath_views.xml
Normal file
198
fusion_plating/fusion_plating/views/fp_bath_views.xml
Normal 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>
|
||||
156
fusion_plating/fusion_plating/views/fp_facility_views.xml
Normal file
156
fusion_plating/fusion_plating/views/fp_facility_views.xml
Normal 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>
|
||||
83
fusion_plating/fusion_plating/views/fp_menu.xml
Normal file
83
fusion_plating/fusion_plating/views/fp_menu.xml
Normal file
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== ROOT APP MENU ===== -->
|
||||
<menuitem id="menu_fp_root"
|
||||
name="Plating"
|
||||
sequence="46"
|
||||
web_icon="fusion_plating,static/description/icon.png"
|
||||
groups="group_fusion_plating_operator"/>
|
||||
|
||||
<!-- ===== OPERATIONS ===== -->
|
||||
<menuitem id="menu_fp_operations"
|
||||
name="Operations"
|
||||
parent="menu_fp_root"
|
||||
sequence="18"/>
|
||||
|
||||
<menuitem id="menu_fp_process_recipes"
|
||||
name="Process Recipes"
|
||||
parent="menu_fp_operations"
|
||||
action="action_fp_process_recipe"
|
||||
sequence="5"/>
|
||||
|
||||
<menuitem id="menu_fp_baths"
|
||||
name="Baths"
|
||||
parent="menu_fp_operations"
|
||||
action="action_fp_bath"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fp_bath_logs"
|
||||
name="Chemistry Logs"
|
||||
parent="menu_fp_operations"
|
||||
action="action_fp_bath_log"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fp_tanks"
|
||||
name="Tanks"
|
||||
parent="menu_fp_operations"
|
||||
action="action_fp_tank"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- ===== CONFIGURATION ===== -->
|
||||
<menuitem id="menu_fp_config"
|
||||
name="Configuration"
|
||||
parent="menu_fp_root"
|
||||
sequence="90"
|
||||
groups="group_fusion_plating_manager"/>
|
||||
|
||||
<menuitem id="menu_fp_facilities"
|
||||
name="Facilities"
|
||||
parent="menu_fp_config"
|
||||
action="action_fp_facility"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fp_work_centers"
|
||||
name="Work Centers"
|
||||
parent="menu_fp_config"
|
||||
action="action_fp_work_center"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fp_process_categories"
|
||||
name="Process Categories"
|
||||
parent="menu_fp_config"
|
||||
action="action_fp_process_category"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_fp_process_types"
|
||||
name="Process Types"
|
||||
parent="menu_fp_config"
|
||||
action="action_fp_process_type"
|
||||
sequence="40"/>
|
||||
|
||||
<menuitem id="menu_fp_bath_parameters"
|
||||
name="Bath Parameters"
|
||||
parent="menu_fp_config"
|
||||
action="action_fp_bath_parameter"
|
||||
sequence="50"/>
|
||||
|
||||
</odoo>
|
||||
184
fusion_plating/fusion_plating/views/fp_process_node_views.xml
Normal file
184
fusion_plating/fusion_plating/views/fp_process_node_views.xml
Normal file
@@ -0,0 +1,184 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!-- ===== TREE (LIST) VIEW — Recipes only ===== -->
|
||||
<record id="view_fp_process_node_tree" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.process.node.tree</field>
|
||||
<field name="model">fusion.plating.process.node</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Process Recipes" default_order="sequence, name">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="code" optional="show"/>
|
||||
<field name="name"/>
|
||||
<field name="node_type" widget="badge"
|
||||
decoration-info="node_type == 'recipe'"
|
||||
decoration-success="node_type == 'operation'"
|
||||
decoration-warning="node_type == 'sub_process'"
|
||||
decoration-muted="node_type == 'step'"/>
|
||||
<field name="process_type_id" optional="show"/>
|
||||
<field name="work_center_id" optional="show"/>
|
||||
<field name="child_count" string="Steps"/>
|
||||
<field name="version" optional="hide"/>
|
||||
<field name="active" column_invisible="True"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== FORM VIEW ===== -->
|
||||
<record id="view_fp_process_node_form" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.process.node.form</field>
|
||||
<field name="model">fusion.plating.process.node</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Process Node">
|
||||
<header>
|
||||
<button name="action_open_tree_editor" type="object"
|
||||
string="Open Tree Editor" class="btn-primary"
|
||||
icon="fa-sitemap"
|
||||
invisible="node_type != 'recipe'"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_open_tree_editor" type="object"
|
||||
class="oe_stat_button" icon="fa-sitemap"
|
||||
invisible="node_type != 'recipe'">
|
||||
<field name="child_count" widget="statinfo"
|
||||
string="Steps"/>
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived"
|
||||
bg_color="text-bg-danger"
|
||||
invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Node name..."/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Classification">
|
||||
<field name="code"/>
|
||||
<field name="node_type"/>
|
||||
<field name="process_type_id"/>
|
||||
<field name="work_center_id"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="icon"/>
|
||||
</group>
|
||||
<group string="Behaviour">
|
||||
<field name="sequence"/>
|
||||
<field name="estimated_duration"/>
|
||||
<field name="auto_complete"/>
|
||||
<field name="customer_visible"/>
|
||||
<field name="is_manual"/>
|
||||
<field name="requires_signoff"/>
|
||||
<field name="opt_in_out"/>
|
||||
<field name="version"/>
|
||||
<field name="active" invisible="True"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Tracking">
|
||||
<field name="create_date" string="Created"/>
|
||||
<field name="create_uid" string="Created By"/>
|
||||
<field name="write_date" string="Last Updated"/>
|
||||
<field name="write_uid" string="Updated By"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Description" name="description">
|
||||
<field name="description" widget="html"/>
|
||||
</page>
|
||||
<page string="Operator Inputs" name="inputs">
|
||||
<field name="input_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="input_type"/>
|
||||
<field name="required"/>
|
||||
<field name="hint"/>
|
||||
<field name="uom"/>
|
||||
<field name="selection_options"
|
||||
invisible="input_type != 'selection'"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Child Steps" name="children">
|
||||
<field name="child_ids">
|
||||
<list default_order="sequence, name">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="node_type" widget="badge"/>
|
||||
<field name="work_center_id"/>
|
||||
<field name="estimated_duration"/>
|
||||
<field name="child_count" string="Sub-Steps"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Notes" name="notes">
|
||||
<field name="notes" placeholder="Internal notes..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== SEARCH VIEW ===== -->
|
||||
<record id="view_fp_process_node_search" model="ir.ui.view">
|
||||
<field name="name">fusion.plating.process.node.search</field>
|
||||
<field name="model">fusion.plating.process.node</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="process_type_id"/>
|
||||
<field name="work_center_id"/>
|
||||
<filter name="recipes_only" string="Recipes"
|
||||
domain="[('node_type', '=', 'recipe')]"/>
|
||||
<filter name="sub_processes" string="Sub-Processes"
|
||||
domain="[('node_type', '=', 'sub_process')]"/>
|
||||
<filter name="operations" string="Operations"
|
||||
domain="[('node_type', '=', 'operation')]"/>
|
||||
<filter name="archived" string="Archived"
|
||||
domain="[('active', '=', False)]"/>
|
||||
<filter name="group_type" string="Type"
|
||||
context="{'group_by': 'node_type'}"/>
|
||||
<filter name="group_process" string="Process Type"
|
||||
context="{'group_by': 'process_type_id'}"/>
|
||||
<filter name="group_wc" string="Work Centre"
|
||||
context="{'group_by': 'work_center_id'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== WINDOW ACTION — Recipe list ===== -->
|
||||
<record id="action_fp_process_recipe" model="ir.actions.act_window">
|
||||
<field name="name">Process Recipes</field>
|
||||
<field name="res_model">fusion.plating.process.node</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('node_type', '=', 'recipe')]</field>
|
||||
<field name="context">{'default_node_type': 'recipe', 'search_default_recipes_only': 1}</field>
|
||||
<field name="search_view_id" ref="view_fp_process_node_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first process recipe
|
||||
</p>
|
||||
<p>
|
||||
Recipes define the step-by-step process for plating parts.
|
||||
Each recipe is a reusable template with nested operations
|
||||
and sub-processes.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== CLIENT ACTION — OWL Tree Editor ===== -->
|
||||
<record id="action_fp_recipe_tree_editor" model="ir.actions.client">
|
||||
<field name="name">Recipe Tree Editor</field>
|
||||
<field name="tag">fp_recipe_tree_editor</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
240
fusion_plating/fusion_plating/views/fp_process_type_views.xml
Normal file
240
fusion_plating/fusion_plating/views/fp_process_type_views.xml
Normal 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>
|
||||
168
fusion_plating/fusion_plating/views/fp_tank_views.xml
Normal file
168
fusion_plating/fusion_plating/views/fp_tank_views.xml
Normal 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>
|
||||
92
fusion_plating/fusion_plating/views/fp_work_center_views.xml
Normal file
92
fusion_plating/fusion_plating/views/fp_work_center_views.xml
Normal 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>
|
||||
Reference in New Issue
Block a user