folder rename

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

View File

@@ -0,0 +1,39 @@
# Fusion Plating — Safety (EHS)
Part of the Fusion Plating product family by Nexa Systems Inc.
This add-on layers a process-agnostic Environmental, Health and Safety
workspace on top of `fusion_plating`. It gives a plating shop a single
place to manage day-to-day occupational health and safety obligations
without depending on any jurisdiction-specific regulatory pack.
## Workspaces
| Workspace | Purpose |
|-----------|---------|
| SDS Library | Safety Data Sheet repository with version, hazard class, GHS pictograms, language, expiry tracking, and PDF attachment. |
| Chemical Inventory | Physical chemical containers with storage location, on-hand quantity, reorder point, and incompatibility relations. |
| Training Records | Per-employee training completions with auto-computed expiry and current/expiring/expired status. |
| Training Types | Master catalogue of training courses (WHMIS, TDG, first-aid, LOTO, confined space, etc.) with validity windows. |
| Exposure Monitoring | Air, biological, noise, and vibration sampling events with OEL reference and percent-of-limit. |
| JHSC | Joint Health & Safety Committee with worker and management reps, plus a meeting register. |
| Incident Register | Injury, near-miss, first-aid, lost-time, medical, property-damage, and environmental events with investigation, root cause, corrective action, and WSIB Form 7 flagging. |
| PPE Issuance | Per-employee PPE issuance log with replacement scheduling. |
## Installation
This module depends on `fusion_plating`, `hr`, and `product`.
```bash
docker exec odoo-dev-app odoo -d fusion-dev -u fusion_plating_safety --stop-after-init
```
## Conventions
- All field names on extended base Odoo models use the `x_fc_` prefix.
- Security groups are reused from `fusion_plating` (Operator / Supervisor / Manager).
- All copy is Canadian English.
- Theme-aware SCSS uses CSS variables only — no hex colours.
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
License: OPL-1 (Odoo Proprietary License v1.0)

View File

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

View File

@@ -0,0 +1,91 @@
# -*- 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 — Safety (EHS)',
'version': '19.0.1.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Occupational health and safety for plating shops: SDS library, '
'WHMIS/TDG training, exposure monitoring, JHSC, incidents, PPE, '
'chemical inventory.',
'description': """
Fusion Plating — Safety (EHS)
=============================
Part of the Fusion Plating product family by Nexa Systems Inc.
This add-on layers a process-agnostic Environmental, Health and Safety
workspace on top of the core Fusion Plating module. It gives a plating
shop a single place to manage day-to-day safety obligations of running a
metal-finishing facility without depending on any jurisdiction-specific
regulatory pack.
Included workspaces
-------------------
* SDS Library — Safety Data Sheet repository with version, hazard class,
GHS pictograms, language, expiry tracking and PDF attachment.
* Chemical Inventory — physical chemical containers with storage location,
on-hand quantity, reorder point and incompatibility relations.
* Training Matrix — training type catalogue (WHMIS, TDG, first-aid, LOTO,
confined space, etc.) plus per-employee records with auto-computed
expiry and current/expiring/expired status.
* Exposure Monitoring — air sampling, biological, noise and vibration
monitoring events with OEL reference, percent-of-limit and result
classification.
* JHSC — Joint Health & Safety Committee with worker and management
representatives, plus a meeting register with agenda, minutes and
action items.
* Incident Register — injury, near-miss, first-aid, lost-time, medical,
property damage and environmental events with investigation, root
cause, corrective action, WSIB Form 7 flagging and lost-time tracking.
* PPE Issuance — per-employee personal protective equipment issuance log
with replacement scheduling.
Designed to slot in alongside jurisdiction-specific compliance packs
(fusion_plating_compliance_on, fusion_plating_compliance_tor) without
duplicating their content.
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
""",
'author': 'Nexa Systems Inc.',
'website': 'https://www.nexasystems.ca',
'maintainer': 'Nexa Systems Inc.',
'support': 'support@nexasystems.ca',
'license': 'OPL-1',
'price': 0.00,
'currency': 'CAD',
'depends': [
'fusion_plating',
'hr',
'product',
],
'data': [
'security/fp_safety_security.xml',
'security/ir.model.access.csv',
'data/fp_sequence_data.xml',
'data/fp_training_type_data.xml',
'views/fp_sds_views.xml',
'views/fp_chemical_views.xml',
'views/fp_training_type_views.xml',
'views/fp_training_record_views.xml',
'views/fp_exposure_monitoring_views.xml',
'views/fp_jhsc_views.xml',
'views/fp_incident_views.xml',
'views/fp_ppe_issuance_views.xml',
'views/hr_employee_views.xml',
'views/fp_menu.xml',
],
'demo': [
'data/fp_demo_safety_data.xml',
],
'assets': {
'web.assets_backend': [
'fusion_plating_safety/static/src/scss/fusion_plating_safety.scss',
],
},
'installable': True,
'application': False,
'auto_install': False,
}

View File

@@ -0,0 +1,223 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 Nexa Systems Inc. — Demo safety data -->
<odoo noupdate="1">
<!-- ================================================================== -->
<!-- SDS — Safety Data Sheets -->
<!-- ================================================================== -->
<record id="demo_sds_chromic_acid" model="fusion.plating.sds">
<field name="name">SDS-CrO3-2025</field>
<field name="product_name">Chromic Acid</field>
<field name="supplier_name">Allied Chemical Supply</field>
<field name="cas_number">7738-94-5</field>
<field name="version">4.1</field>
<field name="issue_date" eval="DateTime.today() - timedelta(days=180)"/>
<field name="hazard_class">corrosive</field>
<field name="ghs_pictograms">GHS05,GHS06,GHS08</field>
<field name="language">en</field>
<field name="notes" type="html"><p>Corrosive and toxic. Known carcinogen (Cr VI). Handle with full PPE.</p></field>
</record>
<record id="demo_sds_nickel_sulfate" model="fusion.plating.sds">
<field name="name">SDS-NiSO4-2025</field>
<field name="product_name">Nickel Sulfate</field>
<field name="supplier_name">Great Lakes Chemicals</field>
<field name="cas_number">7786-81-4</field>
<field name="version">3.0</field>
<field name="issue_date" eval="DateTime.today() - timedelta(days=120)"/>
<field name="hazard_class">toxic</field>
<field name="ghs_pictograms">GHS06,GHS08</field>
<field name="language">en</field>
<field name="notes" type="html"><p>Toxic and sensitizer. Potential carcinogen. Avoid skin contact and inhalation.</p></field>
</record>
<record id="demo_sds_sulfuric_acid" model="fusion.plating.sds">
<field name="name">SDS-H2SO4-2025</field>
<field name="product_name">Sulfuric Acid</field>
<field name="supplier_name">Allied Chemical Supply</field>
<field name="cas_number">7664-93-9</field>
<field name="version">5.2</field>
<field name="issue_date" eval="DateTime.today() - timedelta(days=240)"/>
<field name="hazard_class">corrosive</field>
<field name="ghs_pictograms">GHS05</field>
<field name="language">both</field>
<field name="notes" type="html"><p>Highly corrosive. Causes severe burns. Use acid-resistant PPE.</p></field>
</record>
<record id="demo_sds_sodium_hydroxide" model="fusion.plating.sds">
<field name="name">SDS-NaOH-2025</field>
<field name="product_name">Sodium Hydroxide</field>
<field name="supplier_name">Great Lakes Chemicals</field>
<field name="cas_number">1310-73-2</field>
<field name="version">2.4</field>
<field name="issue_date" eval="DateTime.today() - timedelta(days=90)"/>
<field name="hazard_class">corrosive</field>
<field name="ghs_pictograms">GHS05</field>
<field name="language">en</field>
<field name="notes" type="html"><p>Corrosive. Causes severe skin burns and eye damage.</p></field>
</record>
<!-- ================================================================== -->
<!-- Chemicals — linked to SDS and facility -->
<!-- ================================================================== -->
<record id="demo_chemical_chromic_acid" model="fusion.plating.chemical">
<field name="name">Chromic Acid — Main Store</field>
<field name="sds_id" ref="demo_sds_chromic_acid"/>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="location">Acid Cabinet 1</field>
<field name="container_size">25.0</field>
<field name="container_uom">kg</field>
<field name="quantity_on_hand">18.5</field>
<field name="reorder_point">5.0</field>
</record>
<record id="demo_chemical_nickel_sulfate" model="fusion.plating.chemical">
<field name="name">Nickel Sulfate — Main Store</field>
<field name="sds_id" ref="demo_sds_nickel_sulfate"/>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="location">Chemical Storage Bay A</field>
<field name="container_size">50.0</field>
<field name="container_uom">kg</field>
<field name="quantity_on_hand">32.0</field>
<field name="reorder_point">10.0</field>
</record>
<record id="demo_chemical_sulfuric_acid" model="fusion.plating.chemical">
<field name="name">Sulfuric Acid — Main Store</field>
<field name="sds_id" ref="demo_sds_sulfuric_acid"/>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="location">Acid Cabinet 2</field>
<field name="container_size">20.0</field>
<field name="container_uom">L</field>
<field name="quantity_on_hand">12.0</field>
<field name="reorder_point">5.0</field>
</record>
<!-- ================================================================== -->
<!-- Incidents -->
<!-- ================================================================== -->
<record id="demo_incident_near_miss" model="fusion.plating.incident">
<field name="name">INC-DEMO-001</field>
<field name="incident_date" eval="DateTime.now() - timedelta(days=14)"/>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="incident_type">near_miss</field>
<field name="location">Chrome plating line 1</field>
<field name="description" type="html"><p>Worker slipped on wet floor near rinse station. No injury.</p></field>
<field name="immediate_action" type="html"><p>Area cleaned and non-slip mats placed around rinse station.</p></field>
<field name="state">draft</field>
</record>
<record id="demo_incident_first_aid" model="fusion.plating.incident">
<field name="name">INC-DEMO-002</field>
<field name="incident_date" eval="DateTime.now() - timedelta(days=7)"/>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="incident_type">first_aid</field>
<field name="location">Nickel plating area</field>
<field name="description" type="html"><p>Minor chemical splash on forearm during tank top-up. First aid administered on-site.</p></field>
<field name="immediate_action" type="html"><p>Affected area flushed with water for 15 minutes. First aid kit used.</p></field>
<field name="investigation" type="html"><p>Worker was not wearing full-length gloves during transfer. PPE policy reminder issued.</p></field>
<field name="state">investigation</field>
</record>
<record id="demo_incident_property_damage" model="fusion.plating.incident">
<field name="name">INC-DEMO-003</field>
<field name="incident_date" eval="DateTime.now() - timedelta(days=30)"/>
<field name="facility_id" ref="fusion_plating.demo_facility_east"/>
<field name="incident_type">property_damage</field>
<field name="location">Loading dock</field>
<field name="description" type="html"><p>Forklift struck chemical storage rack causing minor structural damage. No spill.</p></field>
<field name="immediate_action" type="html"><p>Area cordoned off. Structural assessment arranged.</p></field>
<field name="investigation" type="html"><p>Tight turning radius at dock entrance identified as contributing factor.</p></field>
<field name="root_cause" type="html"><p>Insufficient clearance between rack and dock pillar for forklift turning radius.</p></field>
<field name="corrective_action" type="html"><p>Rack relocated 1.5m further from pillar. Floor markings updated.</p></field>
<field name="state">closed</field>
</record>
<!-- ================================================================== -->
<!-- JHSC — Committee -->
<!-- ================================================================== -->
<record id="demo_jhsc_main" model="fusion.plating.jhsc">
<field name="name">Main Plant JHSC</field>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
</record>
<!-- ================================================================== -->
<!-- JHSC Meetings -->
<!-- ================================================================== -->
<record id="demo_jhsc_meeting_held" model="fusion.plating.jhsc.meeting">
<field name="name">Q1 2026 Safety Review</field>
<field name="jhsc_id" ref="demo_jhsc_main"/>
<field name="meeting_date" eval="DateTime.today() - timedelta(days=30)"/>
<field name="agenda" type="html"><p>1. Review of Q1 incidents<br/>2. PPE compliance audit results<br/>3. Ventilation assessment update</p></field>
<field name="minutes" type="html"><p>All agenda items reviewed. Two corrective actions assigned. Next meeting set for Q2.</p></field>
<field name="state">held</field>
</record>
<record id="demo_jhsc_meeting_planned" model="fusion.plating.jhsc.meeting">
<field name="name">Q2 2026 Safety Review</field>
<field name="jhsc_id" ref="demo_jhsc_main"/>
<field name="meeting_date" eval="DateTime.today() + timedelta(days=60)"/>
<field name="agenda" type="html"><p>1. Review of Q1 corrective actions<br/>2. Summer heat stress protocol<br/>3. Emergency drill scheduling</p></field>
<field name="state">planned</field>
</record>
<!-- ================================================================== -->
<!-- PPE Issuance -->
<!-- ================================================================== -->
<record id="demo_ppe_respirator" model="fusion.plating.ppe.issuance">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="issue_date" eval="DateTime.today() - timedelta(days=60)"/>
<field name="ppe_type">respirator</field>
<field name="size">M</field>
<field name="quantity">1</field>
<field name="next_replacement" eval="DateTime.today() + timedelta(days=120)"/>
</record>
<record id="demo_ppe_gloves" model="fusion.plating.ppe.issuance">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="issue_date" eval="DateTime.today() - timedelta(days=21)"/>
<field name="ppe_type">gloves</field>
<field name="size">L</field>
<field name="quantity">2</field>
<field name="next_replacement" eval="DateTime.today() + timedelta(days=30)"/>
</record>
<record id="demo_ppe_face_shield" model="fusion.plating.ppe.issuance">
<field name="employee_id" ref="hr.employee_admin"/>
<field name="issue_date" eval="DateTime.today() - timedelta(days=30)"/>
<field name="ppe_type">face_shield</field>
<field name="size">Standard</field>
<field name="quantity">1</field>
<field name="next_replacement" eval="DateTime.today() + timedelta(days=150)"/>
</record>
<!-- ================================================================== -->
<!-- Exposure Monitoring -->
<!-- ================================================================== -->
<record id="demo_exposure_chromium" model="fusion.plating.exposure.monitoring">
<field name="name">EXP-DEMO-001</field>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="sample_date" eval="DateTime.today() - timedelta(days=14)"/>
<field name="sample_type">personal_air</field>
<field name="substance">Chromium (VI)</field>
<field name="concentration">0.008</field>
<field name="uom">mg/m3</field>
<field name="oel_reference">Ontario Reg. 833 TWA</field>
<field name="oel_limit">0.025</field>
<field name="notes" type="html"><p>Personal air sample collected at chrome plating line 1 during normal operations.</p></field>
</record>
<record id="demo_exposure_nickel" model="fusion.plating.exposure.monitoring">
<field name="name">EXP-DEMO-002</field>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="sample_date" eval="DateTime.today() - timedelta(days=7)"/>
<field name="sample_type">personal_air</field>
<field name="substance">Nickel (soluble compounds)</field>
<field name="concentration">0.05</field>
<field name="uom">mg/m3</field>
<field name="oel_reference">Ontario Reg. 833 TWA</field>
<field name="oel_limit">0.1</field>
<field name="notes" type="html"><p>Personal air sample collected at nickel plating station during tank maintenance.</p></field>
</record>
</odoo>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo noupdate="1">
<record id="seq_fp_incident" model="ir.sequence">
<field name="name">Fusion Plating: Incident</field>
<field name="code">fusion.plating.incident</field>
<field name="prefix">INC/%(year)s%(month)s/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_fp_exposure_monitoring" model="ir.sequence">
<field name="name">Fusion Plating: Exposure Monitoring</field>
<field name="code">fusion.plating.exposure.monitoring</field>
<field name="prefix">EXP/%(year)s%(month)s/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</odoo>

View File

@@ -0,0 +1,103 @@
<?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.
Generic Canadian training-type catalogue. Jurisdiction-specific
compliance packs may add more.
-->
<odoo noupdate="1">
<record id="training_type_whmis_2015" model="fusion.plating.training.type">
<field name="name">WHMIS 2015</field>
<field name="code">WHMIS</field>
<field name="category">whmis</field>
<field name="validity_months">12</field>
<field name="required_for_roles">All workers exposed to hazardous products</field>
<field name="description" type="html"><p>Workplace Hazardous Materials Information System (WHMIS 2015 / GHS aligned). Retraining required annually and whenever a new hazardous product is introduced.</p></field>
</record>
<record id="training_type_tdg_road" model="fusion.plating.training.type">
<field name="name">TDG Road</field>
<field name="code">TDG</field>
<field name="category">tdg</field>
<field name="validity_months">36</field>
<field name="required_for_roles">Shipping clerks, drivers handling dangerous goods</field>
<field name="description" type="html"><p>Transportation of Dangerous Goods training for road transport. Certificate valid for 36 months.</p></field>
</record>
<record id="training_type_first_aid_cpr" model="fusion.plating.training.type">
<field name="name">Standard First Aid / CPR</field>
<field name="code">FA-CPR</field>
<field name="category">first_aid</field>
<field name="validity_months">36</field>
<field name="required_for_roles">Designated first-aid attendants</field>
<field name="description" type="html"><p>Standard First Aid with CPR Level C. Required to maintain the workplace first-aid roster.</p></field>
</record>
<record id="training_type_loto" model="fusion.plating.training.type">
<field name="name">Lockout / Tagout</field>
<field name="code">LOTO</field>
<field name="category">loto</field>
<field name="validity_months">24</field>
<field name="required_for_roles">Maintenance, electricians, anyone servicing energised equipment</field>
<field name="description" type="html"><p>Energy isolation procedures for equipment service and maintenance.</p></field>
</record>
<record id="training_type_confined_space" model="fusion.plating.training.type">
<field name="name">Confined Space Entry</field>
<field name="code">CSE</field>
<field name="category">confined_space</field>
<field name="validity_months">12</field>
<field name="required_for_roles">Tank cleaning crew, attendants, supervisors</field>
<field name="description" type="html"><p>Confined space awareness, entry, attendant and rescue procedures. Tank entry training required for plating shops.</p></field>
</record>
<record id="training_type_spill_response" model="fusion.plating.training.type">
<field name="name">Chemical Spill Response</field>
<field name="code">SPILL</field>
<field name="category">process_specific</field>
<field name="validity_months">12</field>
<field name="required_for_roles">Operators, supervisors</field>
<field name="description" type="html"><p>Identification, containment, neutralisation and reporting of plating chemistry spills.</p></field>
</record>
<record id="training_type_respirator_fit" model="fusion.plating.training.type">
<field name="name">Respirator Fit Test</field>
<field name="code">FIT</field>
<field name="category">ppe</field>
<field name="validity_months">12</field>
<field name="required_for_roles">Workers required to wear tight-fitting respirators</field>
<field name="description" type="html"><p>Quantitative or qualitative respirator fit testing per CSA Z94.4.</p></field>
</record>
<record id="training_type_forklift" model="fusion.plating.training.type">
<field name="name">Forklift Operator</field>
<field name="code">FL</field>
<field name="category">other</field>
<field name="validity_months">36</field>
<field name="required_for_roles">Material handlers</field>
<field name="description" type="html"><p>Powered industrial truck operator certification.</p></field>
</record>
<record id="training_type_jhsc_member" model="fusion.plating.training.type">
<field name="name">JHSC Member Training</field>
<field name="code">JHSC</field>
<field name="category">other</field>
<field name="validity_months">12</field>
<field name="required_for_roles">JHSC certified members</field>
<field name="description" type="html"><p>Joint Health and Safety Committee certification training (Part 1 / Part 2 refresh).</p></field>
</record>
<record id="training_type_supervisor_awareness" model="fusion.plating.training.type">
<field name="name">Supervisor Awareness</field>
<field name="code">SUP-AW</field>
<field name="category">other</field>
<field name="validity_months">0</field>
<field name="required_for_roles">All supervisors</field>
<field name="description" type="html"><p>One-time supervisor health and safety awareness training (e.g. Ontario "Supervisor Health and Safety Awareness in 5 Steps").</p></field>
</record>
</odoo>

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from . import fp_sds
from . import fp_chemical
from . import fp_training_type
from . import fp_training_record
from . import fp_exposure_monitoring
from . import fp_jhsc
from . import fp_jhsc_meeting
from . import fp_incident
from . import fp_ppe_issuance
from . import hr_employee

View File

@@ -0,0 +1,96 @@
# -*- 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 FpChemical(models.Model):
"""Physical chemical container in the shop's chemical inventory.
A chemical record represents a managed container — drum, tote, jug,
cylinder — of a specific product, stored in a specific facility and
location, with on-hand quantity and reorder thresholds. It links to the
Safety Data Sheet that governs handling, and may optionally link to the
Odoo product/stock record when the same chemistry is also tracked as
inventory.
Storage compatibility (acid vs base, oxidizer vs flammable, etc.) is
captured via a self-referential many2many of incompatible chemicals so
a future workflow can warn on co-located storage.
"""
_name = 'fusion.plating.chemical'
_description = 'Fusion Plating — Chemical'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'name'
name = fields.Char(
string='Chemical',
required=True,
tracking=True,
)
sds_id = fields.Many2one(
'fusion.plating.sds',
string='Safety Data Sheet',
tracking=True,
)
product_id = fields.Many2one(
'product.product',
string='Stock Product',
help='Optional link to the Odoo product when the chemistry is also '
'tracked as inventory.',
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
tracking=True,
)
location = fields.Char(
string='Storage Location',
help='Free-text storage location, e.g. "Acid Cabinet 2" or "Drum bay B".',
)
container_size = fields.Float(
string='Container Size',
)
container_uom = fields.Char(
string='Container UoM',
help='Free-text unit of measure for the container size, '
'e.g. L, kg, lb, gal.',
)
quantity_on_hand = fields.Float(
string='Quantity On Hand',
tracking=True,
)
reorder_point = fields.Float(
string='Reorder Point',
help='When quantity on hand falls below this level, the chemical '
'should be reordered.',
)
incompatible_with_ids = fields.Many2many(
'fusion.plating.chemical',
'fp_chemical_incompat_rel',
'chemical_id',
'incompatible_id',
string='Incompatible With',
help='Chemicals that must not be stored next to this one.',
)
needs_reorder = fields.Boolean(
string='Needs Reorder',
compute='_compute_needs_reorder',
store=True,
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
active = fields.Boolean(default=True)
notes = fields.Html(string='Notes')
@api.depends('quantity_on_hand', 'reorder_point')
def _compute_needs_reorder(self):
for rec in self:
rec.needs_reorder = bool(
rec.reorder_point and rec.quantity_on_hand <= rec.reorder_point
)

View File

@@ -0,0 +1,139 @@
# -*- 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 FpExposureMonitoring(models.Model):
"""An exposure monitoring sample.
A monitoring event captures one measurement of a worker's or area's
exposure to a hazardous agent — air contaminant, biological marker,
noise or vibration. The result is compared against an Occupational
Exposure Limit (OEL), most commonly the time-weighted average (TWA)
or short-term exposure limit (STEL) published by the relevant
jurisdiction (e.g. Ontario Reg. 833 in Canada).
The percent-of-OEL is calculated automatically and the record is
classified as below, approaching or exceeding the limit, providing
an early warning when controls need to be tightened.
"""
_name = 'fusion.plating.exposure.monitoring'
_description = 'Fusion Plating — Exposure Monitoring'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'sample_date desc, id desc'
_rec_name = 'name'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self._default_name(),
tracking=True,
)
employee_id = fields.Many2one(
'hr.employee',
string='Employee',
ondelete='set null',
tracking=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
tracking=True,
)
sample_date = fields.Date(
string='Sample Date',
required=True,
default=fields.Date.context_today,
tracking=True,
)
sample_type = fields.Selection(
[
('personal_air', 'Personal Air'),
('area_air', 'Area Air'),
('biological', 'Biological'),
('noise', 'Noise'),
('vibration', 'Vibration'),
],
string='Sample Type',
default='personal_air',
required=True,
tracking=True,
)
substance = fields.Char(
string='Substance / Agent',
tracking=True,
)
concentration = fields.Float(
string='Concentration',
tracking=True,
)
uom = fields.Char(
string='Unit of Measure',
help='Free-text unit, e.g. mg/m3, ppm, dBA.',
)
oel_reference = fields.Char(
string='OEL Reference',
help='Citation for the exposure limit, e.g. "Ontario Reg. 833 TWA".',
)
oel_limit = fields.Float(
string='OEL Limit',
)
percent_of_oel = fields.Float(
string='% of OEL',
compute='_compute_percent_of_oel',
store=True,
)
result = fields.Selection(
[
('below', 'Below'),
('approaching', 'Approaching'),
('exceed', 'Exceeds'),
],
string='Result',
compute='_compute_result',
store=True,
tracking=True,
)
notes = fields.Html(
string='Notes',
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
active = fields.Boolean(default=True)
# ==========================================================================
# Defaults
# ==========================================================================
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.exposure.monitoring')
return seq or '/'
# ==========================================================================
# Computes
# ==========================================================================
@api.depends('concentration', 'oel_limit')
def _compute_percent_of_oel(self):
for rec in self:
if rec.oel_limit:
rec.percent_of_oel = (rec.concentration / rec.oel_limit) * 100.0
else:
rec.percent_of_oel = 0.0
@api.depends('percent_of_oel')
def _compute_result(self):
for rec in self:
if rec.percent_of_oel >= 100.0:
rec.result = 'exceed'
elif rec.percent_of_oel >= 50.0:
rec.result = 'approaching'
else:
rec.result = 'below'

View File

@@ -0,0 +1,142 @@
# -*- 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 FpIncident(models.Model):
"""A safety incident, near-miss, or environmental event.
The incident register is the central log of anything that did, or
almost did, harm a worker or the environment. Each record walks
through draft → investigation → closed and captures who was hurt,
where, what happened, the immediate response, the investigation
findings, the root cause and the corrective action.
For Ontario operations the WSIB Form 7 fields flag whether the
incident is reportable to the Workplace Safety and Insurance Board
and whether the form has actually been filed.
"""
_name = 'fusion.plating.incident'
_description = 'Fusion Plating — Incident'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'incident_date desc, id desc'
_rec_name = 'name'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self._default_name(),
tracking=True,
)
incident_date = fields.Datetime(
string='Incident Date',
required=True,
default=fields.Datetime.now,
tracking=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
tracking=True,
)
reported_by_id = fields.Many2one(
'res.users',
string='Reported By',
default=lambda self: self.env.user,
tracking=True,
)
incident_type = fields.Selection(
[
('injury', 'Injury'),
('near_miss', 'Near Miss'),
('first_aid', 'First Aid'),
('lost_time', 'Lost Time'),
('medical', 'Medical'),
('property_damage', 'Property Damage'),
('environmental', 'Environmental'),
('other', 'Other'),
],
string='Type',
default='near_miss',
required=True,
tracking=True,
)
employee_id = fields.Many2one(
'hr.employee',
string='Employee Involved',
ondelete='set null',
tracking=True,
)
location = fields.Char(
string='Location',
tracking=True,
)
description = fields.Html(
string='Description',
)
immediate_action = fields.Html(
string='Immediate Action',
)
investigation = fields.Html(
string='Investigation',
)
root_cause = fields.Html(
string='Root Cause',
)
corrective_action = fields.Html(
string='Corrective Action',
)
wsib_reportable = fields.Boolean(
string='WSIB Reportable',
tracking=True,
)
wsib_form_7_submitted = fields.Boolean(
string='WSIB Form 7 Submitted',
tracking=True,
)
lost_time_days = fields.Integer(
string='Lost-Time Days',
tracking=True,
)
state = fields.Selection(
[
('draft', 'Draft'),
('investigation', 'Investigation'),
('closed', 'Closed'),
],
string='Status',
default='draft',
required=True,
tracking=True,
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
active = fields.Boolean(default=True)
# ==========================================================================
# Defaults
# ==========================================================================
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.incident')
return seq or '/'
# ==========================================================================
# Actions
# ==========================================================================
def action_start_investigation(self):
self.write({'state': 'investigation'})
def action_close(self):
self.write({'state': 'closed'})
def action_reopen(self):
self.write({'state': 'draft'})

View File

@@ -0,0 +1,80 @@
# -*- 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 FpJhsc(models.Model):
"""A Joint Health and Safety Committee.
Most Canadian jurisdictions require workplaces above a certain employee
count to maintain a Joint Health and Safety Committee (JHSC) with at
least one worker representative and one management representative.
The committee meets on a regular cadence to review hazards, incidents,
and proposed improvements.
A site can have one or more committees (e.g. when multiple buildings
or shifts each maintain their own). Membership is tracked as overall
members plus the specific worker and management representative subsets.
"""
_name = 'fusion.plating.jhsc'
_description = 'Fusion Plating — JHSC'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'name'
name = fields.Char(
string='Committee',
required=True,
tracking=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
tracking=True,
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
active = fields.Boolean(default=True)
member_ids = fields.Many2many(
'hr.employee',
'fp_jhsc_member_rel',
'jhsc_id',
'employee_id',
string='Members',
)
worker_rep_ids = fields.Many2many(
'hr.employee',
'fp_jhsc_worker_rep_rel',
'jhsc_id',
'employee_id',
string='Worker Representatives',
)
mgmt_rep_ids = fields.Many2many(
'hr.employee',
'fp_jhsc_mgmt_rep_rel',
'jhsc_id',
'employee_id',
string='Management Representatives',
)
meeting_ids = fields.One2many(
'fusion.plating.jhsc.meeting',
'jhsc_id',
string='Meetings',
)
member_count = fields.Integer(
compute='_compute_counts',
)
meeting_count = fields.Integer(
compute='_compute_counts',
)
def _compute_counts(self):
for rec in self:
rec.member_count = len(rec.member_ids)
rec.meeting_count = len(rec.meeting_ids)

View File

@@ -0,0 +1,95 @@
# -*- 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 FpJhscMeeting(models.Model):
"""A scheduled or held JHSC meeting.
Captures the meeting's date, attendees, agenda, minutes, action items
and supporting attachments. The state field walks the meeting through
its lifecycle from planned through closed so the JHSC can audit which
meetings still need minutes posted.
"""
_name = 'fusion.plating.jhsc.meeting'
_description = 'Fusion Plating — JHSC Meeting'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'meeting_date desc, id desc'
_rec_name = 'name'
name = fields.Char(
string='Subject',
required=True,
tracking=True,
)
jhsc_id = fields.Many2one(
'fusion.plating.jhsc',
string='Committee',
required=True,
ondelete='cascade',
tracking=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
related='jhsc_id.facility_id',
store=True,
readonly=True,
)
meeting_date = fields.Date(
string='Meeting Date',
required=True,
default=fields.Date.context_today,
tracking=True,
)
attendee_ids = fields.Many2many(
'hr.employee',
'fp_jhsc_meeting_attendee_rel',
'meeting_id',
'employee_id',
string='Attendees',
)
agenda = fields.Html(
string='Agenda',
)
minutes = fields.Html(
string='Minutes',
)
action_items = fields.Html(
string='Action Items',
)
state = fields.Selection(
[
('planned', 'Planned'),
('held', 'Held'),
('minutes_ready', 'Minutes Ready'),
('closed', 'Closed'),
],
string='Status',
default='planned',
tracking=True,
required=True,
)
attachment_ids = fields.Many2many(
'ir.attachment',
'fp_jhsc_meeting_attachment_rel',
'meeting_id',
'attachment_id',
string='Attachments',
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
def action_mark_held(self):
self.write({'state': 'held'})
def action_post_minutes(self):
self.write({'state': 'minutes_ready'})
def action_close(self):
self.write({'state': 'closed'})

View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
class FpPpeIssuance(models.Model):
"""Per-employee Personal Protective Equipment issuance log.
Each record captures one issuance of a piece of PPE — respirator,
gloves, apron, face shield — to an employee, with the size, quantity,
issue date and the date the equipment is next due for replacement.
A historical PPE log demonstrates that the employer is providing
appropriate protective equipment, which is itself an obligation under
most occupational health and safety regulations.
"""
_name = 'fusion.plating.ppe.issuance'
_description = 'Fusion Plating — PPE Issuance'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'issue_date desc, id desc'
_rec_name = 'display_name'
employee_id = fields.Many2one(
'hr.employee',
string='Employee',
required=True,
ondelete='cascade',
tracking=True,
)
issue_date = fields.Date(
string='Issue Date',
required=True,
default=fields.Date.context_today,
tracking=True,
)
ppe_type = fields.Selection(
[
('respirator', 'Respirator'),
('gloves', 'Gloves'),
('apron', 'Apron'),
('face_shield', 'Face Shield'),
('safety_glasses', 'Safety Glasses'),
('boots', 'Boots'),
('hearing', 'Hearing Protection'),
('other', 'Other'),
],
string='PPE Type',
required=True,
default='gloves',
tracking=True,
)
size = fields.Char(
string='Size',
)
quantity = fields.Integer(
string='Quantity',
default=1,
)
next_replacement = fields.Date(
string='Next Replacement',
tracking=True,
)
notes = fields.Html(
string='Notes',
)
display_name = fields.Char(
compute='_compute_display_name',
store=True,
)
company_id = fields.Many2one(
'res.company',
related='employee_id.company_id',
store=True,
readonly=True,
)
active = fields.Boolean(default=True)
@api.depends('employee_id', 'ppe_type', 'issue_date')
def _compute_display_name(self):
ppe_label = dict(self._fields['ppe_type'].selection)
for rec in self:
parts = []
if rec.employee_id:
parts.append(rec.employee_id.name)
if rec.ppe_type:
parts.append(ppe_label.get(rec.ppe_type, rec.ppe_type))
if rec.issue_date:
parts.append(fields.Date.to_string(rec.issue_date))
rec.display_name = ''.join(parts) if parts else 'PPE Issuance'

View File

@@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
class FpSds(models.Model):
"""Safety Data Sheet library entry.
A Safety Data Sheet (SDS) is a 16-section document supplied by a chemical
manufacturer that describes the hazards, handling, storage, exposure
controls and emergency information for a product. Under the WHMIS 2015 /
GHS framework an SDS is considered current for three years from its
issue date — after which a refresh from the supplier is required.
Each SDS in the library carries supplier metadata, hazard classification,
GHS pictogram codes, language coverage and a link to the original PDF
via ir.attachment.
"""
_name = 'fusion.plating.sds'
_description = 'Fusion Plating — Safety Data Sheet'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'product_name, version desc, issue_date desc'
_rec_name = 'name'
name = fields.Char(
string='Reference',
required=True,
tracking=True,
help='Internal reference for this SDS entry, often the product name.',
)
product_name = fields.Char(
string='Product Name',
tracking=True,
)
supplier_name = fields.Char(
string='Supplier (Text)',
tracking=True,
help='Free-text supplier name as printed on the SDS, used when '
'no res.partner record exists yet.',
)
supplier_id = fields.Many2one(
'res.partner',
string='Supplier',
tracking=True,
)
cas_number = fields.Char(
string='CAS Number',
tracking=True,
)
version = fields.Char(
string='Version',
tracking=True,
)
issue_date = fields.Date(
string='Issue Date',
tracking=True,
)
expiry_date = fields.Date(
string='Expiry Date',
compute='_compute_expiry_date',
store=True,
tracking=True,
help='Computed as issue date + 3 years per WHMIS / GHS rule. '
'Refresh from the supplier is required before this date.',
)
state = fields.Selection(
[
('current', 'Current'),
('expiring_soon', 'Expiring Soon'),
('expired', 'Expired'),
('withdrawn', 'Withdrawn'),
],
string='Status',
compute='_compute_state',
store=True,
tracking=True,
)
hazard_class = fields.Selection(
[
('flammable', 'Flammable'),
('oxidizer', 'Oxidizer'),
('compressed_gas', 'Compressed Gas'),
('corrosive', 'Corrosive'),
('toxic', 'Toxic'),
('carcinogen', 'Carcinogen'),
('reproductive_toxin', 'Reproductive Toxin'),
('sensitizer', 'Sensitizer'),
('aquatic_toxin', 'Aquatic Toxin'),
('other', 'Other'),
],
string='Primary Hazard Class',
tracking=True,
)
ghs_pictograms = fields.Char(
string='GHS Pictograms',
help='Comma-separated GHS pictogram codes, e.g. GHS01,GHS02,GHS05.',
)
language = fields.Selection(
[
('en', 'English'),
('fr', 'French'),
('both', 'Bilingual (EN/FR)'),
],
string='Language',
default='en',
)
attachment_id = fields.Many2one(
'ir.attachment',
string='SDS Document',
help='The original SDS PDF supplied by the manufacturer.',
)
notes = fields.Html(
string='Notes',
)
withdrawn = fields.Boolean(
string='Withdrawn',
tracking=True,
help='Manually mark this SDS as withdrawn (e.g. product discontinued).',
)
active = fields.Boolean(default=True)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
chemical_ids = fields.One2many(
'fusion.plating.chemical',
'sds_id',
string='Chemicals',
)
chemical_count = fields.Integer(
compute='_compute_chemical_count',
)
# ==========================================================================
# Computes
# ==========================================================================
@api.depends('issue_date')
def _compute_expiry_date(self):
for rec in self:
if rec.issue_date:
rec.expiry_date = rec.issue_date + relativedelta(years=3)
else:
rec.expiry_date = False
@api.depends('expiry_date', 'withdrawn')
def _compute_state(self):
today = fields.Date.context_today(self)
warn_window = today + relativedelta(months=3)
for rec in self:
if rec.withdrawn:
rec.state = 'withdrawn'
elif not rec.expiry_date:
rec.state = 'current'
elif rec.expiry_date < today:
rec.state = 'expired'
elif rec.expiry_date <= warn_window:
rec.state = 'expiring_soon'
else:
rec.state = 'current'
def _compute_chemical_count(self):
for rec in self:
rec.chemical_count = len(rec.chemical_ids)
# ==========================================================================
# Actions
# ==========================================================================
def action_mark_withdrawn(self):
self.write({'withdrawn': True})
def action_mark_active(self):
self.write({'withdrawn': False})

View File

@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
class FpTrainingRecord(models.Model):
"""Per-employee record of a completed training course.
Each record links an employee to a training type and captures when the
course was completed, who delivered it, the certificate reference, and
optionally a numeric score. The expiry date is computed automatically
from the training type's validity window, and the state field rolls up
to current / expiring soon / expired so the training matrix can be
queried at a glance.
"""
_name = 'fusion.plating.training.record'
_description = 'Fusion Plating — Training Record'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'completion_date desc, id desc'
_rec_name = 'display_name'
employee_id = fields.Many2one(
'hr.employee',
string='Employee',
required=True,
ondelete='cascade',
tracking=True,
)
training_type_id = fields.Many2one(
'fusion.plating.training.type',
string='Training Type',
required=True,
ondelete='restrict',
tracking=True,
)
completion_date = fields.Date(
string='Completion Date',
required=True,
tracking=True,
)
expiry_date = fields.Date(
string='Expiry Date',
compute='_compute_expiry_date',
store=True,
)
trainer = fields.Char(
string='Trainer',
tracking=True,
)
certificate_ref = fields.Char(
string='Certificate Reference',
tracking=True,
)
score = fields.Float(
string='Score',
)
state = fields.Selection(
[
('current', 'Current'),
('expiring_soon', 'Expiring Soon'),
('expired', 'Expired'),
],
string='Status',
compute='_compute_state',
store=True,
)
attachment_id = fields.Many2one(
'ir.attachment',
string='Certificate',
)
notes = fields.Html(
string='Notes',
)
display_name = fields.Char(
compute='_compute_display_name',
store=True,
)
company_id = fields.Many2one(
'res.company',
related='employee_id.company_id',
store=True,
readonly=True,
)
active = fields.Boolean(default=True)
# ==========================================================================
# Computes
# ==========================================================================
@api.depends('completion_date', 'training_type_id', 'training_type_id.validity_months')
def _compute_expiry_date(self):
for rec in self:
if rec.completion_date and rec.training_type_id and rec.training_type_id.validity_months:
rec.expiry_date = rec.completion_date + relativedelta(months=rec.training_type_id.validity_months)
else:
rec.expiry_date = False
@api.depends('expiry_date')
def _compute_state(self):
today = fields.Date.context_today(self)
warn_window = today + relativedelta(months=2)
for rec in self:
if not rec.expiry_date:
rec.state = 'current'
elif rec.expiry_date < today:
rec.state = 'expired'
elif rec.expiry_date <= warn_window:
rec.state = 'expiring_soon'
else:
rec.state = 'current'
@api.depends('employee_id', 'training_type_id', 'completion_date')
def _compute_display_name(self):
for rec in self:
parts = []
if rec.employee_id:
parts.append(rec.employee_id.name)
if rec.training_type_id:
parts.append(rec.training_type_id.name)
if rec.completion_date:
parts.append(fields.Date.to_string(rec.completion_date))
rec.display_name = ''.join(parts) if parts else 'Training Record'

View File

@@ -0,0 +1,61 @@
# -*- 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 FpTrainingType(models.Model):
"""Master catalogue of training courses required in the shop.
A training type defines a class of certification — WHMIS 2015, TDG Road,
Standard First Aid / CPR, LOTO, Confined Space — together with how long
a completion remains valid before retraining is required. The validity
window drives the per-employee training record's automatic expiry.
Seed data ships a generic Canadian set; jurisdiction-specific compliance
packs may add more.
"""
_name = 'fusion.plating.training.type'
_description = 'Fusion Plating — Training Type'
_order = 'category, name'
name = fields.Char(
string='Training Type',
required=True,
)
code = fields.Char(
string='Code',
)
category = fields.Selection(
[
('whmis', 'WHMIS'),
('tdg', 'TDG'),
('first_aid', 'First Aid'),
('loto', 'Lockout / Tagout'),
('confined_space', 'Confined Space'),
('process_specific', 'Process Specific'),
('ppe', 'PPE'),
('ergonomic', 'Ergonomic'),
('other', 'Other'),
],
string='Category',
default='other',
required=True,
)
validity_months = fields.Integer(
string='Validity (Months)',
default=12,
help='How long a completion remains valid before retraining is '
'required. Set to 0 for one-time training that never expires.',
)
description = fields.Html(
string='Description',
)
required_for_roles = fields.Char(
string='Required For Roles',
help='Free-text list of job titles or roles that require this '
'training, e.g. "All operators, Supervisors".',
)
active = fields.Boolean(default=True)

View File

@@ -0,0 +1,75 @@
# -*- 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 HrEmployee(models.Model):
"""Adds Fusion Plating safety roll-ups to hr.employee.
Each employee gets reverse links to their training records, exposure
monitoring samples and incident records, plus a single boolean that
rolls up to True only when every required training course is current.
Field names use the x_fc_ prefix per the Fusion Central convention for
extensions to base Odoo models.
"""
_inherit = 'hr.employee'
x_fc_training_ids = fields.One2many(
'fusion.plating.training.record',
'employee_id',
string='Training Records',
)
x_fc_training_current = fields.Boolean(
string='Training Current',
compute='_compute_x_fc_training_current',
store=True,
help='True when every training record on this employee is currently '
'in date (no expired records).',
)
x_fc_exposure_ids = fields.One2many(
'fusion.plating.exposure.monitoring',
'employee_id',
string='Exposure Samples',
)
x_fc_incident_ids = fields.One2many(
'fusion.plating.incident',
'employee_id',
string='Incidents',
)
x_fc_ppe_ids = fields.One2many(
'fusion.plating.ppe.issuance',
'employee_id',
string='PPE Issued',
)
x_fc_training_count = fields.Integer(
compute='_compute_x_fc_safety_counts',
)
x_fc_exposure_count = fields.Integer(
compute='_compute_x_fc_safety_counts',
)
x_fc_incident_count = fields.Integer(
compute='_compute_x_fc_safety_counts',
)
x_fc_ppe_count = fields.Integer(
compute='_compute_x_fc_safety_counts',
)
@api.depends('x_fc_training_ids', 'x_fc_training_ids.state')
def _compute_x_fc_training_current(self):
for rec in self:
if not rec.x_fc_training_ids:
rec.x_fc_training_current = True
else:
rec.x_fc_training_current = not any(
t.state == 'expired' for t in rec.x_fc_training_ids
)
def _compute_x_fc_safety_counts(self):
for rec in self:
rec.x_fc_training_count = len(rec.x_fc_training_ids)
rec.x_fc_exposure_count = len(rec.x_fc_exposure_ids)
rec.x_fc_incident_count = len(rec.x_fc_incident_ids)
rec.x_fc_ppe_count = len(rec.x_fc_ppe_ids)

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<!-- ================================================================== -->
<!-- Multi-company isolation rules -->
<!-- All safety records are scoped per-company so a multi-company -->
<!-- shop sees only its own data. -->
<!-- ================================================================== -->
<record id="fp_safety_sds_company_rule" model="ir.rule">
<field name="name">Fusion Plating Safety: SDS — multi-company</field>
<field name="model_id" ref="model_fusion_plating_sds"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_safety_chemical_company_rule" model="ir.rule">
<field name="name">Fusion Plating Safety: Chemical — multi-company</field>
<field name="model_id" ref="model_fusion_plating_chemical"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_safety_training_record_company_rule" model="ir.rule">
<field name="name">Fusion Plating Safety: Training Record — multi-company</field>
<field name="model_id" ref="model_fusion_plating_training_record"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_safety_exposure_company_rule" model="ir.rule">
<field name="name">Fusion Plating Safety: Exposure — multi-company</field>
<field name="model_id" ref="model_fusion_plating_exposure_monitoring"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_safety_jhsc_company_rule" model="ir.rule">
<field name="name">Fusion Plating Safety: JHSC — multi-company</field>
<field name="model_id" ref="model_fusion_plating_jhsc"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_safety_jhsc_meeting_company_rule" model="ir.rule">
<field name="name">Fusion Plating Safety: JHSC Meeting — multi-company</field>
<field name="model_id" ref="model_fusion_plating_jhsc_meeting"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_safety_incident_company_rule" model="ir.rule">
<field name="name">Fusion Plating Safety: Incident — multi-company</field>
<field name="model_id" ref="model_fusion_plating_incident"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_safety_ppe_company_rule" model="ir.rule">
<field name="name">Fusion Plating Safety: PPE — multi-company</field>
<field name="model_id" ref="model_fusion_plating_ppe_issuance"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
</odoo>

View File

@@ -0,0 +1,28 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_fp_sds_operator,fp.sds.operator,model_fusion_plating_sds,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_sds_supervisor,fp.sds.supervisor,model_fusion_plating_sds,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_sds_manager,fp.sds.manager,model_fusion_plating_sds,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_chemical_operator,fp.chemical.operator,model_fusion_plating_chemical,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_chemical_supervisor,fp.chemical.supervisor,model_fusion_plating_chemical,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_chemical_manager,fp.chemical.manager,model_fusion_plating_chemical,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_training_type_operator,fp.training.type.operator,model_fusion_plating_training_type,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_training_type_supervisor,fp.training.type.supervisor,model_fusion_plating_training_type,fusion_plating.group_fusion_plating_supervisor,1,0,0,0
access_fp_training_type_manager,fp.training.type.manager,model_fusion_plating_training_type,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_training_record_operator,fp.training.record.operator,model_fusion_plating_training_record,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_training_record_supervisor,fp.training.record.supervisor,model_fusion_plating_training_record,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_training_record_manager,fp.training.record.manager,model_fusion_plating_training_record,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_exposure_operator,fp.exposure.monitoring.operator,model_fusion_plating_exposure_monitoring,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_exposure_supervisor,fp.exposure.monitoring.supervisor,model_fusion_plating_exposure_monitoring,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_exposure_manager,fp.exposure.monitoring.manager,model_fusion_plating_exposure_monitoring,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_jhsc_operator,fp.jhsc.operator,model_fusion_plating_jhsc,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_jhsc_supervisor,fp.jhsc.supervisor,model_fusion_plating_jhsc,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_jhsc_manager,fp.jhsc.manager,model_fusion_plating_jhsc,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_jhsc_meeting_operator,fp.jhsc.meeting.operator,model_fusion_plating_jhsc_meeting,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_jhsc_meeting_supervisor,fp.jhsc.meeting.supervisor,model_fusion_plating_jhsc_meeting,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_jhsc_meeting_manager,fp.jhsc.meeting.manager,model_fusion_plating_jhsc_meeting,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_incident_operator,fp.incident.operator,model_fusion_plating_incident,fusion_plating.group_fusion_plating_operator,1,1,1,0
access_fp_incident_supervisor,fp.incident.supervisor,model_fusion_plating_incident,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_incident_manager,fp.incident.manager,model_fusion_plating_incident,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_ppe_operator,fp.ppe.issuance.operator,model_fusion_plating_ppe_issuance,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_ppe_supervisor,fp.ppe.issuance.supervisor,model_fusion_plating_ppe_issuance,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_ppe_manager,fp.ppe.issuance.manager,model_fusion_plating_ppe_issuance,fusion_plating.group_fusion_plating_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fp_sds_operator fp.sds.operator model_fusion_plating_sds fusion_plating.group_fusion_plating_operator 1 0 0 0
3 access_fp_sds_supervisor fp.sds.supervisor model_fusion_plating_sds fusion_plating.group_fusion_plating_supervisor 1 1 1 0
4 access_fp_sds_manager fp.sds.manager model_fusion_plating_sds fusion_plating.group_fusion_plating_manager 1 1 1 1
5 access_fp_chemical_operator fp.chemical.operator model_fusion_plating_chemical fusion_plating.group_fusion_plating_operator 1 0 0 0
6 access_fp_chemical_supervisor fp.chemical.supervisor model_fusion_plating_chemical fusion_plating.group_fusion_plating_supervisor 1 1 1 0
7 access_fp_chemical_manager fp.chemical.manager model_fusion_plating_chemical fusion_plating.group_fusion_plating_manager 1 1 1 1
8 access_fp_training_type_operator fp.training.type.operator model_fusion_plating_training_type fusion_plating.group_fusion_plating_operator 1 0 0 0
9 access_fp_training_type_supervisor fp.training.type.supervisor model_fusion_plating_training_type fusion_plating.group_fusion_plating_supervisor 1 0 0 0
10 access_fp_training_type_manager fp.training.type.manager model_fusion_plating_training_type fusion_plating.group_fusion_plating_manager 1 1 1 1
11 access_fp_training_record_operator fp.training.record.operator model_fusion_plating_training_record fusion_plating.group_fusion_plating_operator 1 0 0 0
12 access_fp_training_record_supervisor fp.training.record.supervisor model_fusion_plating_training_record fusion_plating.group_fusion_plating_supervisor 1 1 1 0
13 access_fp_training_record_manager fp.training.record.manager model_fusion_plating_training_record fusion_plating.group_fusion_plating_manager 1 1 1 1
14 access_fp_exposure_operator fp.exposure.monitoring.operator model_fusion_plating_exposure_monitoring fusion_plating.group_fusion_plating_operator 1 0 0 0
15 access_fp_exposure_supervisor fp.exposure.monitoring.supervisor model_fusion_plating_exposure_monitoring fusion_plating.group_fusion_plating_supervisor 1 1 1 0
16 access_fp_exposure_manager fp.exposure.monitoring.manager model_fusion_plating_exposure_monitoring fusion_plating.group_fusion_plating_manager 1 1 1 1
17 access_fp_jhsc_operator fp.jhsc.operator model_fusion_plating_jhsc fusion_plating.group_fusion_plating_operator 1 0 0 0
18 access_fp_jhsc_supervisor fp.jhsc.supervisor model_fusion_plating_jhsc fusion_plating.group_fusion_plating_supervisor 1 1 1 0
19 access_fp_jhsc_manager fp.jhsc.manager model_fusion_plating_jhsc fusion_plating.group_fusion_plating_manager 1 1 1 1
20 access_fp_jhsc_meeting_operator fp.jhsc.meeting.operator model_fusion_plating_jhsc_meeting fusion_plating.group_fusion_plating_operator 1 0 0 0
21 access_fp_jhsc_meeting_supervisor fp.jhsc.meeting.supervisor model_fusion_plating_jhsc_meeting fusion_plating.group_fusion_plating_supervisor 1 1 1 0
22 access_fp_jhsc_meeting_manager fp.jhsc.meeting.manager model_fusion_plating_jhsc_meeting fusion_plating.group_fusion_plating_manager 1 1 1 1
23 access_fp_incident_operator fp.incident.operator model_fusion_plating_incident fusion_plating.group_fusion_plating_operator 1 1 1 0
24 access_fp_incident_supervisor fp.incident.supervisor model_fusion_plating_incident fusion_plating.group_fusion_plating_supervisor 1 1 1 0
25 access_fp_incident_manager fp.incident.manager model_fusion_plating_incident fusion_plating.group_fusion_plating_manager 1 1 1 1
26 access_fp_ppe_operator fp.ppe.issuance.operator model_fusion_plating_ppe_issuance fusion_plating.group_fusion_plating_operator 1 0 0 0
27 access_fp_ppe_supervisor fp.ppe.issuance.supervisor model_fusion_plating_ppe_issuance fusion_plating.group_fusion_plating_supervisor 1 1 1 0
28 access_fp_ppe_manager fp.ppe.issuance.manager model_fusion_plating_ppe_issuance fusion_plating.group_fusion_plating_manager 1 1 1 1

View File

@@ -0,0 +1,196 @@
// =============================================================================
// Fusion Plating — Safety (EHS) 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
//
// Hazard / severity tints use color-mix() against the Bootstrap theme
// tokens so a red 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
// -----------------------------------------------------------------------------
@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);
}
// -----------------------------------------------------------------------------
// SDS kanban — hazard-class tinted card
// -----------------------------------------------------------------------------
.o_fp_sds_kanban {
.o_fp_sds_card {
// Use a left border tint by hazard class — subtle, theme-aware.
border-left-width: 4px;
border-left-color: var(--bs-border-color);
&[data-hazard="flammable"],
&[data-hazard="oxidizer"] {
border-left-color: var(--bs-danger);
background-color: color-mix(in srgb, var(--bs-danger) 4%, var(--o-view-background-color, var(--bs-body-bg)));
}
&[data-hazard="corrosive"],
&[data-hazard="toxic"],
&[data-hazard="carcinogen"],
&[data-hazard="reproductive_toxin"] {
border-left-color: var(--bs-warning);
background-color: color-mix(in srgb, var(--bs-warning) 4%, var(--o-view-background-color, var(--bs-body-bg)));
}
&[data-hazard="compressed_gas"],
&[data-hazard="sensitizer"],
&[data-hazard="aquatic_toxin"] {
border-left-color: var(--bs-info, var(--o-action));
}
// Status overlay — expired wins over hazard tint.
&[data-state="expired"] {
border-left-color: var(--bs-danger);
opacity: 0.85;
}
&[data-state="expiring_soon"] {
border-left-color: var(--bs-warning);
}
&[data-state="withdrawn"] {
opacity: 0.6;
}
}
.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="current"] {
@include fp-tint(--bs-success);
}
&[data-state="expiring_soon"] {
@include fp-tint(--bs-warning);
}
&[data-state="expired"] {
@include fp-tint(--bs-danger);
}
&[data-state="withdrawn"] {
@include fp-tint(--bs-secondary-color);
}
}
}
// -----------------------------------------------------------------------------
// Training kanban — expiry indicator on the card
// -----------------------------------------------------------------------------
.o_fp_training_kanban {
.o_fp_training_card {
border-left-width: 4px;
border-left-color: var(--bs-success);
&[data-state="expiring_soon"] {
border-left-color: var(--bs-warning);
}
&[data-state="expired"] {
border-left-color: var(--bs-danger);
background-color: color-mix(in srgb, var(--bs-danger) 5%, var(--o-view-background-color, var(--bs-body-bg)));
}
}
.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="current"] {
@include fp-tint(--bs-success);
}
&[data-state="expiring_soon"] {
@include fp-tint(--bs-warning);
}
&[data-state="expired"] {
@include fp-tint(--bs-danger);
}
}
}
// -----------------------------------------------------------------------------
// Incident kanban — severity tint by type
// -----------------------------------------------------------------------------
.o_fp_incident_kanban {
.o_fp_incident_card {
border-left-width: 4px;
border-left-color: var(--bs-secondary-color);
// High-severity
&[data-type="injury"],
&[data-type="lost_time"],
&[data-type="medical"] {
border-left-color: var(--bs-danger);
background-color: color-mix(in srgb, var(--bs-danger) 5%, var(--o-view-background-color, var(--bs-body-bg)));
}
// Mid-severity
&[data-type="first_aid"],
&[data-type="property_damage"],
&[data-type="environmental"] {
border-left-color: var(--bs-warning);
}
// Low-severity
&[data-type="near_miss"] {
border-left-color: var(--bs-info, var(--o-action));
}
// Closed records dim
&[data-state="closed"] {
opacity: 0.65;
}
}
.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="draft"] {
@include fp-tint(--bs-secondary-color);
}
&[data-state="investigation"] {
@include fp-tint(--bs-warning);
}
&[data-state="closed"] {
@include fp-tint(--bs-success);
}
}
}

View File

@@ -0,0 +1,109 @@
<?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_chemical_list" model="ir.ui.view">
<field name="name">fp.chemical.list</field>
<field name="model">fusion.plating.chemical</field>
<field name="arch" type="xml">
<list string="Chemicals" decoration-warning="needs_reorder">
<field name="name"/>
<field name="sds_id"/>
<field name="facility_id"/>
<field name="location"/>
<field name="container_size"/>
<field name="container_uom"/>
<field name="quantity_on_hand"/>
<field name="reorder_point"/>
<field name="needs_reorder"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<record id="view_fp_chemical_form" model="ir.ui.view">
<field name="name">fp.chemical.form</field>
<field name="model">fusion.plating.chemical</field>
<field name="arch" type="xml">
<form string="Chemical">
<sheet>
<widget name="web_ribbon" title="Reorder" bg_color="text-bg-warning" invisible="not needs_reorder"/>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. 50% Sodium Hydroxide"/></h1>
</div>
<group>
<group>
<field name="sds_id"/>
<field name="product_id"/>
<field name="facility_id"/>
<field name="location" placeholder="Acid Cabinet 2"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="active" widget="boolean_toggle"/>
</group>
<group>
<field name="container_size"/>
<field name="container_uom" placeholder="L"/>
<field name="quantity_on_hand"/>
<field name="reorder_point"/>
<field name="needs_reorder"/>
</group>
</group>
<notebook>
<page string="Incompatibilities">
<field name="incompatible_with_ids" widget="many2many_tags"/>
<p class="text-muted mt-2">
Chemicals that must not be stored next to this one
(e.g. acids vs bases, oxidizers vs flammables).
</p>
</page>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_chemical_search" model="ir.ui.view">
<field name="name">fp.chemical.search</field>
<field name="model">fusion.plating.chemical</field>
<field name="arch" type="xml">
<search string="Chemicals">
<field name="name"/>
<field name="sds_id"/>
<field name="facility_id"/>
<field name="location"/>
<filter string="Needs Reorder" name="filter_reorder" domain="[('needs_reorder', '=', True)]"/>
<filter string="Archived" name="filter_inactive" domain="[('active', '=', False)]"/>
<group>
<filter string="Facility" name="group_facility" context="{'group_by': 'facility_id'}"/>
<filter string="Location" name="group_location" context="{'group_by': 'location'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_chemical" model="ir.actions.act_window">
<field name="name">Chemicals</field>
<field name="res_model">fusion.plating.chemical</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_chemical_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a chemical
</p>
<p>
Track the physical chemicals stored on site, who supplies them,
where they live, and when they need to be reordered.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,109 @@
<?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_exposure_monitoring_list" model="ir.ui.view">
<field name="name">fp.exposure.monitoring.list</field>
<field name="model">fusion.plating.exposure.monitoring</field>
<field name="arch" type="xml">
<list string="Exposure Monitoring" decoration-danger="result == 'exceed'" decoration-warning="result == 'approaching'">
<field name="name"/>
<field name="sample_date"/>
<field name="employee_id"/>
<field name="facility_id"/>
<field name="sample_type"/>
<field name="substance"/>
<field name="concentration"/>
<field name="uom"/>
<field name="oel_limit"/>
<field name="percent_of_oel"/>
<field name="result"/>
</list>
</field>
</record>
<record id="view_fp_exposure_monitoring_form" model="ir.ui.view">
<field name="name">fp.exposure.monitoring.form</field>
<field name="model">fusion.plating.exposure.monitoring</field>
<field name="arch" type="xml">
<form string="Exposure Sample">
<header>
<field name="result" widget="statusbar" statusbar_visible="below,approaching,exceed"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name"/></h1>
</div>
<group>
<group string="Sample">
<field name="sample_date"/>
<field name="sample_type"/>
<field name="employee_id"/>
<field name="facility_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group string="Result">
<field name="substance"/>
<field name="concentration"/>
<field name="uom" placeholder="mg/m3"/>
<field name="oel_reference" placeholder="Ontario Reg. 833 TWA"/>
<field name="oel_limit"/>
<field name="percent_of_oel"/>
</group>
</group>
<notebook>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_exposure_monitoring_search" model="ir.ui.view">
<field name="name">fp.exposure.monitoring.search</field>
<field name="model">fusion.plating.exposure.monitoring</field>
<field name="arch" type="xml">
<search string="Exposure Samples">
<field name="name"/>
<field name="employee_id"/>
<field name="facility_id"/>
<field name="substance"/>
<filter string="Below" name="filter_below" domain="[('result', '=', 'below')]"/>
<filter string="Approaching" name="filter_approaching" domain="[('result', '=', 'approaching')]"/>
<filter string="Exceeds" name="filter_exceed" domain="[('result', '=', 'exceed')]"/>
<group>
<filter string="Sample Type" name="group_sample_type" context="{'group_by': 'sample_type'}"/>
<filter string="Substance" name="group_substance" context="{'group_by': 'substance'}"/>
<filter string="Employee" name="group_employee" context="{'group_by': 'employee_id'}"/>
<filter string="Result" name="group_result" context="{'group_by': 'result'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_exposure_monitoring" model="ir.actions.act_window">
<field name="name">Exposure Monitoring</field>
<field name="res_model">fusion.plating.exposure.monitoring</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_exposure_monitoring_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Log an exposure sample
</p>
<p>
Capture air, biological, noise and vibration sampling results
and compare them automatically against the applicable
Occupational Exposure Limit.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,161 @@
<?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_incident_list" model="ir.ui.view">
<field name="name">fp.incident.list</field>
<field name="model">fusion.plating.incident</field>
<field name="arch" type="xml">
<list string="Incident Register" decoration-danger="incident_type in ('injury', 'lost_time', 'medical')" decoration-warning="incident_type == 'first_aid'">
<field name="name"/>
<field name="incident_date"/>
<field name="incident_type"/>
<field name="facility_id"/>
<field name="employee_id"/>
<field name="location"/>
<field name="lost_time_days"/>
<field name="wsib_reportable"/>
<field name="state"/>
</list>
</field>
</record>
<record id="view_fp_incident_form" model="ir.ui.view">
<field name="name">fp.incident.form</field>
<field name="model">fusion.plating.incident</field>
<field name="arch" type="xml">
<form string="Incident">
<header>
<button name="action_start_investigation" type="object" string="Start Investigation" invisible="state != 'draft'"/>
<button name="action_close" type="object" string="Close" invisible="state != 'investigation'"/>
<button name="action_reopen" type="object" string="Reopen" invisible="state != 'closed'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,investigation,closed"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name"/></h1>
</div>
<group>
<group string="Incident">
<field name="incident_date"/>
<field name="incident_type"/>
<field name="facility_id"/>
<field name="location"/>
<field name="employee_id"/>
<field name="reported_by_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group string="WSIB / Outcome">
<field name="wsib_reportable"/>
<field name="wsib_form_7_submitted" invisible="not wsib_reportable"/>
<field name="lost_time_days"/>
</group>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="Immediate Action">
<field name="immediate_action"/>
</page>
<page string="Investigation">
<field name="investigation"/>
</page>
<page string="Root Cause">
<field name="root_cause"/>
</page>
<page string="Corrective Action">
<field name="corrective_action"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_incident_kanban" model="ir.ui.view">
<field name="name">fp.incident.kanban</field>
<field name="model">fusion.plating.incident</field>
<field name="arch" type="xml">
<kanban class="o_fp_incident_kanban">
<field name="id"/>
<field name="name"/>
<field name="incident_type"/>
<field name="incident_date"/>
<field name="facility_id"/>
<field name="employee_id"/>
<field name="state"/>
<templates>
<t t-name="card">
<div class="o_fp_card o_fp_incident_card" t-att-data-type="record.incident_type.raw_value" 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="name"/></strong>
<div class="text-muted small"><field name="incident_type"/></div>
</div>
<i class="fa fa-exclamation-triangle text-muted" aria-hidden="true"/>
</div>
<div class="mt-2 small">
<div><field name="incident_date"/></div>
<div><field name="facility_id"/></div>
<div><field name="employee_id"/></div>
<span class="o_fp_badge mt-2 d-inline-block" t-att-data-state="record.state.raw_value">
<field name="state"/>
</span>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_fp_incident_search" model="ir.ui.view">
<field name="name">fp.incident.search</field>
<field name="model">fusion.plating.incident</field>
<field name="arch" type="xml">
<search string="Incidents">
<field name="name"/>
<field name="employee_id"/>
<field name="facility_id"/>
<field name="location"/>
<filter string="Draft" name="filter_draft" domain="[('state', '=', 'draft')]"/>
<filter string="Investigation" name="filter_investigation" domain="[('state', '=', 'investigation')]"/>
<filter string="Closed" name="filter_closed" domain="[('state', '=', 'closed')]"/>
<separator/>
<filter string="WSIB Reportable" name="filter_wsib" domain="[('wsib_reportable', '=', True)]"/>
<filter string="Lost-Time" name="filter_lost_time" domain="[('incident_type', '=', 'lost_time')]"/>
<filter string="Near Miss" name="filter_near_miss" domain="[('incident_type', '=', 'near_miss')]"/>
<group>
<filter string="Type" name="group_type" context="{'group_by': 'incident_type'}"/>
<filter string="Facility" name="group_facility" context="{'group_by': 'facility_id'}"/>
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_incident" model="ir.actions.act_window">
<field name="name">Incident Register</field>
<field name="res_model">fusion.plating.incident</field>
<field name="view_mode">kanban,list,form</field>
<field name="search_view_id" ref="view_fp_incident_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Log an incident, near-miss or first-aid event
</p>
<p>
Capture every safety event — even close calls — and walk it
through investigation to closure with a documented root
cause and corrective action.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,186 @@
<?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>
<!-- ===================================================================== -->
<!-- JHSC -->
<!-- ===================================================================== -->
<record id="view_fp_jhsc_list" model="ir.ui.view">
<field name="name">fp.jhsc.list</field>
<field name="model">fusion.plating.jhsc</field>
<field name="arch" type="xml">
<list string="JHSC">
<field name="name"/>
<field name="facility_id"/>
<field name="member_count"/>
<field name="meeting_count"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<record id="view_fp_jhsc_form" model="ir.ui.view">
<field name="name">fp.jhsc.form</field>
<field name="model">fusion.plating.jhsc</field>
<field name="arch" type="xml">
<form string="JHSC">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Site A JHSC"/></h1>
</div>
<group>
<group>
<field name="facility_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="active" widget="boolean_toggle"/>
</group>
<group>
<field name="member_count"/>
<field name="meeting_count"/>
</group>
</group>
<notebook>
<page string="Members">
<field name="member_ids" widget="many2many_tags"/>
</page>
<page string="Worker Representatives">
<field name="worker_rep_ids" widget="many2many_tags"/>
</page>
<page string="Management Representatives">
<field name="mgmt_rep_ids" widget="many2many_tags"/>
</page>
<page string="Meetings">
<field name="meeting_ids">
<list>
<field name="meeting_date"/>
<field name="name"/>
<field name="state"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_jhsc_search" model="ir.ui.view">
<field name="name">fp.jhsc.search</field>
<field name="model">fusion.plating.jhsc</field>
<field name="arch" type="xml">
<search string="JHSC">
<field name="name"/>
<field name="facility_id"/>
<filter string="Archived" name="filter_inactive" domain="[('active', '=', False)]"/>
<group>
<filter string="Facility" name="group_facility" context="{'group_by': 'facility_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_jhsc" model="ir.actions.act_window">
<field name="name">JHSC</field>
<field name="res_model">fusion.plating.jhsc</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_jhsc_search"/>
</record>
<!-- ===================================================================== -->
<!-- JHSC Meetings -->
<!-- ===================================================================== -->
<record id="view_fp_jhsc_meeting_list" model="ir.ui.view">
<field name="name">fp.jhsc.meeting.list</field>
<field name="model">fusion.plating.jhsc.meeting</field>
<field name="arch" type="xml">
<list string="JHSC Meetings">
<field name="meeting_date"/>
<field name="name"/>
<field name="jhsc_id"/>
<field name="facility_id"/>
<field name="state"/>
</list>
</field>
</record>
<record id="view_fp_jhsc_meeting_form" model="ir.ui.view">
<field name="name">fp.jhsc.meeting.form</field>
<field name="model">fusion.plating.jhsc.meeting</field>
<field name="arch" type="xml">
<form string="JHSC Meeting">
<header>
<button name="action_mark_held" type="object" string="Mark Held" invisible="state != 'planned'"/>
<button name="action_post_minutes" type="object" string="Post Minutes" invisible="state not in ('held',)"/>
<button name="action_close" type="object" string="Close" invisible="state not in ('minutes_ready',)"/>
<field name="state" widget="statusbar" statusbar_visible="planned,held,minutes_ready,closed"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Q1 2026 JHSC Meeting"/></h1>
</div>
<group>
<group>
<field name="jhsc_id"/>
<field name="meeting_date"/>
<field name="facility_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="Attendees">
<field name="attendee_ids" widget="many2many_tags"/>
</page>
<page string="Agenda">
<field name="agenda"/>
</page>
<page string="Minutes">
<field name="minutes"/>
</page>
<page string="Action Items">
<field name="action_items"/>
</page>
<page string="Attachments">
<field name="attachment_ids" widget="many2many_binary"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_jhsc_meeting_search" model="ir.ui.view">
<field name="name">fp.jhsc.meeting.search</field>
<field name="model">fusion.plating.jhsc.meeting</field>
<field name="arch" type="xml">
<search string="JHSC Meetings">
<field name="name"/>
<field name="jhsc_id"/>
<field name="facility_id"/>
<filter string="Planned" name="filter_planned" domain="[('state', '=', 'planned')]"/>
<filter string="Held" name="filter_held" domain="[('state', '=', 'held')]"/>
<filter string="Minutes Ready" name="filter_minutes" domain="[('state', '=', 'minutes_ready')]"/>
<filter string="Closed" name="filter_closed" domain="[('state', '=', 'closed')]"/>
<group>
<filter string="Committee" name="group_jhsc" context="{'group_by': 'jhsc_id'}"/>
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_jhsc_meeting" model="ir.actions.act_window">
<field name="name">JHSC Meetings</field>
<field name="res_model">fusion.plating.jhsc.meeting</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_jhsc_meeting_search"/>
</record>
</odoo>

View File

@@ -0,0 +1,71 @@
<?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>
<!-- ===== SAFETY (top-level under Plating) ===== -->
<menuitem id="menu_fp_safety_root"
name="Safety"
parent="fusion_plating.menu_fp_root"
sequence="45"
groups="fusion_plating.group_fusion_plating_operator"/>
<menuitem id="menu_fp_safety_sds"
name="SDS Library"
parent="menu_fp_safety_root"
action="action_fp_sds"
sequence="10"/>
<menuitem id="menu_fp_safety_training"
name="Training Records"
parent="menu_fp_safety_root"
action="action_fp_training_record"
sequence="20"/>
<menuitem id="menu_fp_safety_exposure"
name="Exposure Monitoring"
parent="menu_fp_safety_root"
action="action_fp_exposure_monitoring"
sequence="30"/>
<menuitem id="menu_fp_safety_jhsc"
name="JHSC"
parent="menu_fp_safety_root"
action="action_fp_jhsc"
sequence="40"/>
<menuitem id="menu_fp_safety_jhsc_meetings"
name="JHSC Meetings"
parent="menu_fp_safety_root"
action="action_fp_jhsc_meeting"
sequence="45"/>
<menuitem id="menu_fp_safety_incidents"
name="Incident Register"
parent="menu_fp_safety_root"
action="action_fp_incident"
sequence="50"/>
<menuitem id="menu_fp_safety_ppe"
name="PPE Issuance"
parent="menu_fp_safety_root"
action="action_fp_ppe_issuance"
sequence="60"/>
<!-- ===== Configuration (under existing Plating > Configuration) ===== -->
<menuitem id="menu_fp_safety_training_types"
name="Training Types"
parent="fusion_plating.menu_fp_config"
action="action_fp_training_type"
sequence="60"/>
<menuitem id="menu_fp_safety_chemicals"
name="Chemicals"
parent="fusion_plating.menu_fp_config"
action="action_fp_chemical"
sequence="70"/>
</odoo>

View File

@@ -0,0 +1,75 @@
<?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_ppe_issuance_list" model="ir.ui.view">
<field name="name">fp.ppe.issuance.list</field>
<field name="model">fusion.plating.ppe.issuance</field>
<field name="arch" type="xml">
<list string="PPE Issuance">
<field name="employee_id"/>
<field name="ppe_type"/>
<field name="size"/>
<field name="quantity"/>
<field name="issue_date"/>
<field name="next_replacement"/>
</list>
</field>
</record>
<record id="view_fp_ppe_issuance_form" model="ir.ui.view">
<field name="name">fp.ppe.issuance.form</field>
<field name="model">fusion.plating.ppe.issuance</field>
<field name="arch" type="xml">
<form string="PPE Issuance">
<sheet>
<group>
<group>
<field name="employee_id"/>
<field name="ppe_type"/>
<field name="size"/>
<field name="quantity"/>
</group>
<group>
<field name="issue_date"/>
<field name="next_replacement"/>
</group>
</group>
<notebook>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_ppe_issuance_search" model="ir.ui.view">
<field name="name">fp.ppe.issuance.search</field>
<field name="model">fusion.plating.ppe.issuance</field>
<field name="arch" type="xml">
<search string="PPE Issuance">
<field name="employee_id"/>
<field name="ppe_type"/>
<group>
<filter string="Employee" name="group_employee" context="{'group_by': 'employee_id'}"/>
<filter string="PPE Type" name="group_ppe_type" context="{'group_by': 'ppe_type'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_ppe_issuance" model="ir.actions.act_window">
<field name="name">PPE Issuance</field>
<field name="res_model">fusion.plating.ppe.issuance</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_ppe_issuance_search"/>
</record>
</odoo>

View File

@@ -0,0 +1,165 @@
<?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_sds_list" model="ir.ui.view">
<field name="name">fp.sds.list</field>
<field name="model">fusion.plating.sds</field>
<field name="arch" type="xml">
<list string="Safety Data Sheets" decoration-danger="state == 'expired'" decoration-warning="state == 'expiring_soon'" decoration-muted="state == 'withdrawn'">
<field name="name"/>
<field name="product_name"/>
<field name="supplier_name"/>
<field name="cas_number"/>
<field name="version"/>
<field name="issue_date"/>
<field name="expiry_date"/>
<field name="hazard_class"/>
<field name="language"/>
<field name="state"/>
</list>
</field>
</record>
<record id="view_fp_sds_form" model="ir.ui.view">
<field name="name">fp.sds.form</field>
<field name="model">fusion.plating.sds</field>
<field name="arch" type="xml">
<form string="Safety Data Sheet">
<header>
<button name="action_mark_withdrawn" type="object" string="Mark Withdrawn" invisible="withdrawn"/>
<button name="action_mark_active" type="object" string="Mark Active" invisible="not withdrawn"/>
<field name="state" widget="statusbar" statusbar_visible="current,expiring_soon,expired,withdrawn"/>
</header>
<sheet>
<widget name="web_ribbon" title="Withdrawn" bg_color="text-bg-secondary" invisible="not withdrawn"/>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Acme Bright Acid Copper"/></h1>
</div>
<group>
<group string="Product">
<field name="product_name"/>
<field name="cas_number"/>
<field name="hazard_class"/>
<field name="ghs_pictograms" placeholder="GHS01,GHS05,GHS07"/>
<field name="language"/>
</group>
<group string="Supplier &amp; Revision">
<field name="supplier_id"/>
<field name="supplier_name"/>
<field name="version"/>
<field name="issue_date"/>
<field name="expiry_date"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="active" widget="boolean_toggle"/>
</group>
</group>
<notebook>
<page string="Document">
<group>
<field name="attachment_id"/>
</group>
</page>
<page string="Notes">
<field name="notes"/>
</page>
<page string="Linked Chemicals">
<field name="chemical_ids">
<list>
<field name="name"/>
<field name="facility_id"/>
<field name="location"/>
<field name="quantity_on_hand"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_sds_kanban" model="ir.ui.view">
<field name="name">fp.sds.kanban</field>
<field name="model">fusion.plating.sds</field>
<field name="arch" type="xml">
<kanban class="o_fp_sds_kanban">
<field name="id"/>
<field name="name"/>
<field name="product_name"/>
<field name="supplier_name"/>
<field name="hazard_class"/>
<field name="state"/>
<field name="expiry_date"/>
<templates>
<t t-name="card">
<div class="o_fp_card o_fp_sds_card" t-att-data-hazard="record.hazard_class.raw_value" 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="name"/></strong>
<div class="text-muted small"><field name="product_name"/></div>
</div>
<i class="fa fa-flask text-muted" aria-hidden="true"/>
</div>
<div class="mt-2 small">
<div><field name="supplier_name"/></div>
<div class="text-muted">Expires: <field name="expiry_date"/></div>
<span class="o_fp_badge mt-2 d-inline-block" t-att-data-state="record.state.raw_value">
<field name="state"/>
</span>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_fp_sds_search" model="ir.ui.view">
<field name="name">fp.sds.search</field>
<field name="model">fusion.plating.sds</field>
<field name="arch" type="xml">
<search string="Safety Data Sheets">
<field name="name"/>
<field name="product_name"/>
<field name="supplier_name"/>
<field name="cas_number"/>
<filter string="Current" name="filter_current" domain="[('state', '=', 'current')]"/>
<filter string="Expiring Soon" name="filter_expiring" domain="[('state', '=', 'expiring_soon')]"/>
<filter string="Expired" name="filter_expired" domain="[('state', '=', 'expired')]"/>
<filter string="Withdrawn" name="filter_withdrawn" domain="[('state', '=', 'withdrawn')]"/>
<separator/>
<filter string="Archived" name="filter_inactive" domain="[('active', '=', False)]"/>
<group>
<filter string="Hazard Class" name="group_hazard" context="{'group_by': 'hazard_class'}"/>
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
<filter string="Supplier" name="group_supplier" context="{'group_by': 'supplier_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_sds" model="ir.actions.act_window">
<field name="name">SDS Library</field>
<field name="res_model">fusion.plating.sds</field>
<field name="view_mode">kanban,list,form</field>
<field name="search_view_id" ref="view_fp_sds_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a Safety Data Sheet
</p>
<p>
Maintain a current SDS for every hazardous product on site.
Under WHMIS / GHS each SDS is valid for three years from
its issue date — refresh from the supplier before expiry.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<record id="view_fp_training_record_list" model="ir.ui.view">
<field name="name">fp.training.record.list</field>
<field name="model">fusion.plating.training.record</field>
<field name="arch" type="xml">
<list string="Training Records" decoration-danger="state == 'expired'" decoration-warning="state == 'expiring_soon'">
<field name="employee_id"/>
<field name="training_type_id"/>
<field name="completion_date"/>
<field name="expiry_date"/>
<field name="trainer"/>
<field name="certificate_ref"/>
<field name="state"/>
</list>
</field>
</record>
<record id="view_fp_training_record_form" model="ir.ui.view">
<field name="name">fp.training.record.form</field>
<field name="model">fusion.plating.training.record</field>
<field name="arch" type="xml">
<form string="Training Record">
<header>
<field name="state" widget="statusbar" statusbar_visible="current,expiring_soon,expired"/>
</header>
<sheet>
<group>
<group>
<field name="employee_id"/>
<field name="training_type_id"/>
<field name="completion_date"/>
<field name="expiry_date"/>
</group>
<group>
<field name="trainer"/>
<field name="certificate_ref"/>
<field name="score"/>
<field name="attachment_id"/>
</group>
</group>
<notebook>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_training_record_kanban" model="ir.ui.view">
<field name="name">fp.training.record.kanban</field>
<field name="model">fusion.plating.training.record</field>
<field name="arch" type="xml">
<kanban class="o_fp_training_kanban">
<field name="id"/>
<field name="employee_id"/>
<field name="training_type_id"/>
<field name="completion_date"/>
<field name="expiry_date"/>
<field name="state"/>
<templates>
<t t-name="card">
<div class="o_fp_card o_fp_training_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="employee_id"/></strong>
<div class="text-muted small"><field name="training_type_id"/></div>
</div>
<i class="fa fa-graduation-cap text-muted" aria-hidden="true"/>
</div>
<div class="mt-2 small">
<div>Completed: <field name="completion_date"/></div>
<div>Expires: <field name="expiry_date"/></div>
<span class="o_fp_badge mt-2 d-inline-block" t-att-data-state="record.state.raw_value">
<field name="state"/>
</span>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_fp_training_record_search" model="ir.ui.view">
<field name="name">fp.training.record.search</field>
<field name="model">fusion.plating.training.record</field>
<field name="arch" type="xml">
<search string="Training Records">
<field name="employee_id"/>
<field name="training_type_id"/>
<field name="certificate_ref"/>
<filter string="Current" name="filter_current" domain="[('state', '=', 'current')]"/>
<filter string="Expiring Soon" name="filter_expiring" domain="[('state', '=', 'expiring_soon')]"/>
<filter string="Expired" name="filter_expired" domain="[('state', '=', 'expired')]"/>
<group>
<filter string="Employee" name="group_employee" context="{'group_by': 'employee_id'}"/>
<filter string="Training Type" name="group_type" context="{'group_by': 'training_type_id'}"/>
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_training_record" model="ir.actions.act_window">
<field name="name">Training Records</field>
<field name="res_model">fusion.plating.training.record</field>
<field name="view_mode">kanban,list,form</field>
<field name="search_view_id" ref="view_fp_training_record_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Log a training completion
</p>
<p>
Build the shop training matrix one completion at a time.
Records expire automatically based on the training type's
validity window.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<record id="view_fp_training_type_list" model="ir.ui.view">
<field name="name">fp.training.type.list</field>
<field name="model">fusion.plating.training.type</field>
<field name="arch" type="xml">
<list string="Training Types">
<field name="code"/>
<field name="name"/>
<field name="category"/>
<field name="validity_months"/>
<field name="required_for_roles"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<record id="view_fp_training_type_form" model="ir.ui.view">
<field name="name">fp.training.type.form</field>
<field name="model">fusion.plating.training.type</field>
<field name="arch" type="xml">
<form string="Training Type">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. WHMIS 2015"/></h1>
<div class="text-muted"><field name="code" placeholder="WHMIS"/></div>
</div>
<group>
<group>
<field name="category"/>
<field name="validity_months"/>
<field name="active" widget="boolean_toggle"/>
</group>
<group>
<field name="required_for_roles"/>
</group>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_fp_training_type_search" model="ir.ui.view">
<field name="name">fp.training.type.search</field>
<field name="model">fusion.plating.training.type</field>
<field name="arch" type="xml">
<search string="Training Types">
<field name="name"/>
<field name="code"/>
<filter string="Archived" name="filter_inactive" domain="[('active', '=', False)]"/>
<group>
<filter string="Category" name="group_category" context="{'group_by': 'category'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_training_type" model="ir.actions.act_window">
<field name="name">Training Types</field>
<field name="res_model">fusion.plating.training.type</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_training_type_search"/>
</record>
</odoo>

View File

@@ -0,0 +1,82 @@
<?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_hr_employee_form_inherit_fp_safety" model="ir.ui.view">
<field name="name">hr.employee.form.inherit.fp.safety</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Safety" name="fp_safety">
<group>
<group>
<field name="x_fc_training_current"/>
</group>
<group>
<field name="x_fc_training_count"/>
<field name="x_fc_exposure_count"/>
<field name="x_fc_incident_count"/>
<field name="x_fc_ppe_count"/>
</group>
</group>
<notebook>
<page string="Training">
<field name="x_fc_training_ids">
<list>
<field name="training_type_id"/>
<field name="completion_date"/>
<field name="expiry_date"/>
<field name="trainer"/>
<field name="certificate_ref"/>
<field name="state"/>
</list>
</field>
</page>
<page string="Exposure">
<field name="x_fc_exposure_ids">
<list>
<field name="name"/>
<field name="sample_date"/>
<field name="sample_type"/>
<field name="substance"/>
<field name="concentration"/>
<field name="oel_limit"/>
<field name="percent_of_oel"/>
<field name="result"/>
</list>
</field>
</page>
<page string="Incidents">
<field name="x_fc_incident_ids">
<list>
<field name="name"/>
<field name="incident_date"/>
<field name="incident_type"/>
<field name="facility_id"/>
<field name="state"/>
</list>
</field>
</page>
<page string="PPE Issued">
<field name="x_fc_ppe_ids">
<list>
<field name="issue_date"/>
<field name="ppe_type"/>
<field name="size"/>
<field name="quantity"/>
<field name="next_replacement"/>
</list>
</field>
</page>
</notebook>
</page>
</xpath>
</field>
</record>
</odoo>