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,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)