folder rename
This commit is contained in:
15
fusion_plating/fusion_plating_cgp/models/__init__.py
Normal file
15
fusion_plating/fusion_plating_cgp/models/__init__.py
Normal 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_cgp_registration
|
||||
from . import fp_cgp_authorized_individual
|
||||
from . import fp_cgp_psa
|
||||
from . import fp_cgp_visitor
|
||||
from . import fp_cgp_controlled_good
|
||||
from . import fp_cgp_receipt_shipment
|
||||
from . import fp_cgp_security_incident
|
||||
from . import fp_cgp_access_log
|
||||
from . import hr_employee
|
||||
from . import res_company
|
||||
@@ -0,0 +1,63 @@
|
||||
# -*- 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 FpCgpAccessLog(models.Model):
|
||||
"""Physical access log for CGP-controlled areas.
|
||||
|
||||
Every entry and exit from a controlled area must be logged so a
|
||||
timeline can be reconstructed in the event of an incident. In a
|
||||
typical shop the entries are created automatically by an access
|
||||
control system; this model is the lightweight hand-entry fallback.
|
||||
"""
|
||||
_name = 'fusion.plating.cgp.access.log'
|
||||
_description = 'Fusion Plating — CGP Access Log Entry'
|
||||
_order = 'access_datetime desc, id desc'
|
||||
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
string='Employee',
|
||||
)
|
||||
access_datetime = fields.Datetime(
|
||||
string='Date / Time',
|
||||
required=True,
|
||||
default=lambda self: fields.Datetime.now(),
|
||||
)
|
||||
access_point = fields.Char(
|
||||
string='Access Point',
|
||||
required=True,
|
||||
help='Door, gate, or zone name.',
|
||||
)
|
||||
entry_exit = fields.Selection(
|
||||
[
|
||||
('entry', 'Entry'),
|
||||
('exit', 'Exit'),
|
||||
],
|
||||
string='Direction',
|
||||
default='entry',
|
||||
required=True,
|
||||
)
|
||||
access_type = fields.Selection(
|
||||
[
|
||||
('employee', 'Employee'),
|
||||
('visitor_escorted', 'Visitor (Escorted)'),
|
||||
('visitor_unescorted', 'Visitor (Unescorted)'),
|
||||
],
|
||||
string='Access Type',
|
||||
default='employee',
|
||||
required=True,
|
||||
)
|
||||
related_visitor_id = fields.Many2one(
|
||||
'fusion.plating.cgp.visitor',
|
||||
string='Related Visitor',
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
notes = fields.Char(string='Notes')
|
||||
@@ -0,0 +1,103 @@
|
||||
# -*- 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 FpCgpAuthorizedIndividual(models.Model):
|
||||
"""Authorized Individual (AI) under the Controlled Goods Program.
|
||||
|
||||
Every registered entity must appoint at least one Authorized
|
||||
Individual. The AI is PSPC-trained and is responsible for the
|
||||
day-to-day application of the security plan: approving visitors,
|
||||
authorizing movements of controlled goods, and vetting personnel
|
||||
before granting access.
|
||||
"""
|
||||
_name = 'fusion.plating.cgp.authorized.individual'
|
||||
_description = 'Fusion Plating — CGP Authorized Individual'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'appointment_date desc, id desc'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
compute='_compute_name',
|
||||
store=True,
|
||||
)
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
string='Employee',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
appointment_date = fields.Date(
|
||||
string='Appointment Date',
|
||||
tracking=True,
|
||||
)
|
||||
psa_level = fields.Selection(
|
||||
[
|
||||
('standard', 'Standard'),
|
||||
('enhanced', 'Enhanced'),
|
||||
('security_clearance', 'Security Clearance'),
|
||||
],
|
||||
string='PSA Level',
|
||||
default='standard',
|
||||
tracking=True,
|
||||
help='Level of Personnel Security Assessment held by this AI.',
|
||||
)
|
||||
psa_expiry = fields.Date(
|
||||
string='PSA Expiry',
|
||||
tracking=True,
|
||||
)
|
||||
training_completed_date = fields.Date(
|
||||
string='PSPC Training Completed',
|
||||
tracking=True,
|
||||
help='Date the AI completed the mandatory PSPC training.',
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
('active', 'Active'),
|
||||
('inactive', 'Inactive'),
|
||||
('suspended', 'Suspended'),
|
||||
('revoked', 'Revoked'),
|
||||
],
|
||||
string='Status',
|
||||
default='active',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
notes = fields.Html(string='Notes')
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
'fp_cgp_ai_employee_uniq',
|
||||
'unique(employee_id)',
|
||||
'An employee can only be registered as an Authorized '
|
||||
'Individual once.',
|
||||
),
|
||||
]
|
||||
|
||||
@api.depends('employee_id', 'employee_id.name')
|
||||
def _compute_name(self):
|
||||
for rec in self:
|
||||
rec.name = rec.employee_id.name or 'New AI'
|
||||
|
||||
def action_activate(self):
|
||||
self.write({'state': 'active'})
|
||||
|
||||
def action_suspend(self):
|
||||
self.write({'state': 'suspended'})
|
||||
|
||||
def action_revoke(self):
|
||||
self.write({'state': 'revoked'})
|
||||
|
||||
def action_deactivate(self):
|
||||
self.write({'state': 'inactive'})
|
||||
@@ -0,0 +1,78 @@
|
||||
# -*- 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 FpCgpControlledGood(models.Model):
|
||||
"""Inventory of controlled goods currently handled by the shop.
|
||||
|
||||
This is intentionally a lightweight, hand-maintained register of what
|
||||
the shop actually processes — parts, assemblies, or materials that
|
||||
fall under the Schedule to the Defence Production Act. It is not a
|
||||
replacement for the Odoo stock module; it is the CGP-specific audit
|
||||
trail that an AI can show a PSPC inspector.
|
||||
"""
|
||||
_name = 'fusion.plating.cgp.controlled.good'
|
||||
_description = 'Fusion Plating — Controlled Good'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
description = fields.Html(string='Description')
|
||||
schedule_category = fields.Char(
|
||||
string='Schedule Category',
|
||||
tracking=True,
|
||||
help='Category under the Schedule to the Defence Production Act, '
|
||||
'e.g. "Group 2: Munitions" or a specific ECL entry.',
|
||||
)
|
||||
customer_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Customer',
|
||||
tracking=True,
|
||||
)
|
||||
current_quantity = fields.Float(
|
||||
string='Current Quantity',
|
||||
tracking=True,
|
||||
)
|
||||
location = fields.Char(
|
||||
string='Storage Location',
|
||||
tracking=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
('received', 'Received'),
|
||||
('in_process', 'In Process'),
|
||||
('in_storage', 'In Storage'),
|
||||
('shipped', 'Shipped'),
|
||||
('destroyed', 'Destroyed'),
|
||||
],
|
||||
string='Status',
|
||||
default='received',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
def action_mark_in_process(self):
|
||||
self.write({'state': 'in_process'})
|
||||
|
||||
def action_mark_in_storage(self):
|
||||
self.write({'state': 'in_storage'})
|
||||
|
||||
def action_mark_shipped(self):
|
||||
self.write({'state': 'shipped'})
|
||||
|
||||
def action_mark_destroyed(self):
|
||||
self.write({'state': 'destroyed'})
|
||||
127
fusion_plating/fusion_plating_cgp/models/fp_cgp_psa.py
Normal file
127
fusion_plating/fusion_plating_cgp/models/fp_cgp_psa.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# -*- 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 FpCgpPsa(models.Model):
|
||||
"""Personnel Security Assessment.
|
||||
|
||||
Every person — employee, contractor, or long-term visitor — who has
|
||||
access to controlled goods must have a current Personnel Security
|
||||
Assessment on file. PSAs are valid for up to five years and include
|
||||
a review of citizenship, criminal record, and loyalty considerations.
|
||||
|
||||
PSA records are restricted via ``ir.rule`` so that only the CGP
|
||||
Officer and Designated Official can see them. A regular plating
|
||||
manager cannot see personnel assessments.
|
||||
"""
|
||||
_name = 'fusion.plating.cgp.psa'
|
||||
_description = 'Fusion Plating — Personnel Security Assessment'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'assessment_date desc, id desc'
|
||||
|
||||
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',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
assessment_date = fields.Date(
|
||||
string='Assessment Date',
|
||||
tracking=True,
|
||||
)
|
||||
assessed_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Assessed By',
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True,
|
||||
)
|
||||
result = fields.Selection(
|
||||
[
|
||||
('pass', 'Pass'),
|
||||
('conditional_pass', 'Conditional Pass'),
|
||||
('fail', 'Fail'),
|
||||
],
|
||||
string='Result',
|
||||
tracking=True,
|
||||
)
|
||||
expiry_date = fields.Date(
|
||||
string='Expiry Date',
|
||||
tracking=True,
|
||||
help='PSAs are typically valid for five years.',
|
||||
)
|
||||
notes = fields.Html(
|
||||
string='Internal Notes',
|
||||
help='Internal notes — restricted to CGP Officer and above.',
|
||||
)
|
||||
document_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'fp_cgp_psa_attachment_rel',
|
||||
'psa_id',
|
||||
'attachment_id',
|
||||
string='Supporting Documents',
|
||||
help='Restricted-access supporting documents (criminal record '
|
||||
'check, references, etc.).',
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
('draft', 'Draft'),
|
||||
('in_progress', 'In Progress'),
|
||||
('completed', 'Completed'),
|
||||
('expired', 'Expired'),
|
||||
],
|
||||
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)
|
||||
|
||||
@api.model
|
||||
def _default_name(self):
|
||||
seq = self.env['ir.sequence'].next_by_code('fusion.plating.cgp.psa')
|
||||
return seq or '/'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('name') or vals.get('name') == '/':
|
||||
vals['name'] = self._default_name()
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.onchange('assessment_date')
|
||||
def _onchange_assessment_date(self):
|
||||
"""Default expiry to five years after assessment."""
|
||||
for rec in self:
|
||||
if rec.assessment_date and not rec.expiry_date:
|
||||
rec.expiry_date = rec.assessment_date + relativedelta(years=5)
|
||||
|
||||
def action_start(self):
|
||||
self.write({'state': 'in_progress'})
|
||||
|
||||
def action_complete(self):
|
||||
self.write({'state': 'completed'})
|
||||
|
||||
def action_expire(self):
|
||||
self.write({'state': 'expired'})
|
||||
|
||||
def action_reset_to_draft(self):
|
||||
self.write({'state': 'draft'})
|
||||
@@ -0,0 +1,125 @@
|
||||
# -*- 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 FpCgpReceiptShipment(models.Model):
|
||||
"""Movement log for controlled goods.
|
||||
|
||||
Every receipt, shipment, or internal transfer of a controlled good
|
||||
must be logged and authorized by an Authorized Individual. Cross-
|
||||
border shipments additionally require an export permit reference.
|
||||
"""
|
||||
_name = 'fusion.plating.cgp.receipt.shipment'
|
||||
_description = 'Fusion Plating — CGP Receipt / Shipment'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'date desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: self._default_name(),
|
||||
tracking=True,
|
||||
)
|
||||
movement_type = fields.Selection(
|
||||
[
|
||||
('receipt', 'Receipt'),
|
||||
('shipment', 'Shipment'),
|
||||
('internal_transfer', 'Internal Transfer'),
|
||||
],
|
||||
string='Movement Type',
|
||||
default='receipt',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
controlled_good_id = fields.Many2one(
|
||||
'fusion.plating.cgp.controlled.good',
|
||||
string='Controlled Good',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
date = fields.Datetime(
|
||||
string='Date',
|
||||
default=lambda self: fields.Datetime.now(),
|
||||
tracking=True,
|
||||
)
|
||||
quantity = fields.Float(
|
||||
string='Quantity',
|
||||
tracking=True,
|
||||
)
|
||||
from_party = fields.Char(
|
||||
string='From',
|
||||
tracking=True,
|
||||
)
|
||||
to_party = fields.Char(
|
||||
string='To',
|
||||
tracking=True,
|
||||
)
|
||||
carrier = fields.Char(
|
||||
string='Carrier',
|
||||
tracking=True,
|
||||
)
|
||||
manifest_ref = fields.Char(
|
||||
string='Manifest / Waybill',
|
||||
tracking=True,
|
||||
)
|
||||
authorized_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Authorized By',
|
||||
tracking=True,
|
||||
help='Must be an Authorized Individual.',
|
||||
)
|
||||
export_permit_ref = fields.Char(
|
||||
string='Export Permit Ref',
|
||||
tracking=True,
|
||||
help='Export permit reference when the movement crosses the '
|
||||
'Canadian border.',
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
('draft', 'Draft'),
|
||||
('authorized', 'Authorized'),
|
||||
('executed', 'Executed'),
|
||||
('closed', 'Closed'),
|
||||
],
|
||||
string='Status',
|
||||
default='draft',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
notes = fields.Html(string='Notes')
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.model
|
||||
def _default_name(self):
|
||||
seq = self.env['ir.sequence'].next_by_code('fusion.plating.cgp.movement')
|
||||
return seq or '/'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('name') or vals.get('name') == '/':
|
||||
vals['name'] = self._default_name()
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_authorize(self):
|
||||
self.write({'state': 'authorized'})
|
||||
|
||||
def action_execute(self):
|
||||
self.write({'state': 'executed'})
|
||||
|
||||
def action_close(self):
|
||||
self.write({'state': 'closed'})
|
||||
|
||||
def action_reset_to_draft(self):
|
||||
self.write({'state': 'draft'})
|
||||
116
fusion_plating/fusion_plating_cgp/models/fp_cgp_registration.py
Normal file
116
fusion_plating/fusion_plating_cgp/models/fp_cgp_registration.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# -*- 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 FpCgpRegistration(models.Model):
|
||||
"""Canadian Controlled Goods Program registration.
|
||||
|
||||
Every Canadian entity that examines, possesses, or transfers
|
||||
controlled goods must be registered with Public Services and
|
||||
Procurement Canada (PSPC) under the Defence Production Act.
|
||||
Registration is valid for five years and must be renewed before
|
||||
it lapses. A single registration is usually held per legal entity,
|
||||
so this record lives on ``res.company``.
|
||||
"""
|
||||
_name = 'fusion.plating.cgp.registration'
|
||||
_description = 'Fusion Plating — CGP Registration'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'registration_date desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
tracking=True,
|
||||
help='Label for this registration, e.g. "Acme Plating — CGP Reg."',
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.company,
|
||||
tracking=True,
|
||||
)
|
||||
registration_number = fields.Char(
|
||||
string='PSPC Registration Number',
|
||||
tracking=True,
|
||||
help='Registration number assigned by PSPC when the entity '
|
||||
'is admitted to the Controlled Goods Program.',
|
||||
)
|
||||
registration_date = fields.Date(
|
||||
string='Registration Date',
|
||||
tracking=True,
|
||||
)
|
||||
expiry_date = fields.Date(
|
||||
string='Expiry Date',
|
||||
tracking=True,
|
||||
help='CGP registrations are typically valid for five years.',
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
('pending', 'Pending'),
|
||||
('registered', 'Registered'),
|
||||
('suspended', 'Suspended'),
|
||||
('expired', 'Expired'),
|
||||
('revoked', 'Revoked'),
|
||||
],
|
||||
string='Status',
|
||||
default='pending',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
designated_official_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
string='Designated Official',
|
||||
tracking=True,
|
||||
help='Top-level person legally accountable for CGP compliance '
|
||||
'at this entity. Must have an active PSA on file.',
|
||||
)
|
||||
physical_address = fields.Char(
|
||||
string='Registered Address',
|
||||
tracking=True,
|
||||
help='Physical address on record with PSPC.',
|
||||
)
|
||||
security_plan_doc_id = fields.Many2one(
|
||||
'fusion.plating.doc.control',
|
||||
string='Security Plan',
|
||||
help='Link to the controlled document holding the current '
|
||||
'CGP Security Plan for this registration.',
|
||||
)
|
||||
last_compliance_review = fields.Date(
|
||||
string='Last Compliance Review',
|
||||
tracking=True,
|
||||
)
|
||||
next_compliance_review = fields.Date(
|
||||
string='Next Compliance Review',
|
||||
tracking=True,
|
||||
)
|
||||
notes = fields.Html(string='Notes')
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.onchange('registration_date')
|
||||
def _onchange_registration_date(self):
|
||||
"""Default expiry to five years after registration."""
|
||||
for rec in self:
|
||||
if rec.registration_date and not rec.expiry_date:
|
||||
rec.expiry_date = rec.registration_date + relativedelta(years=5)
|
||||
|
||||
def action_mark_registered(self):
|
||||
self.write({'state': 'registered'})
|
||||
|
||||
def action_suspend(self):
|
||||
self.write({'state': 'suspended'})
|
||||
|
||||
def action_expire(self):
|
||||
self.write({'state': 'expired'})
|
||||
|
||||
def action_revoke(self):
|
||||
self.write({'state': 'revoked'})
|
||||
|
||||
def action_reset_to_pending(self):
|
||||
self.write({'state': 'pending'})
|
||||
@@ -0,0 +1,124 @@
|
||||
# -*- 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 FpCgpSecurityIncident(models.Model):
|
||||
"""Security incident or breach under the Controlled Goods Program.
|
||||
|
||||
Any event that could have compromised the security of controlled
|
||||
goods — unauthorized access, a missing item, a visitor violation,
|
||||
a cyber intrusion — must be investigated and, depending on
|
||||
severity, reported to PSPC. Incident records are restricted via
|
||||
``ir.rule`` to the CGP Officer and above.
|
||||
"""
|
||||
_name = 'fusion.plating.cgp.security.incident'
|
||||
_description = 'Fusion Plating — CGP Security Incident'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'incident_date desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: self._default_name(),
|
||||
tracking=True,
|
||||
)
|
||||
incident_date = fields.Datetime(
|
||||
string='Incident Date',
|
||||
default=lambda self: fields.Datetime.now(),
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
discovered_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Discovered By',
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True,
|
||||
)
|
||||
incident_type = fields.Selection(
|
||||
[
|
||||
('unauthorized_access', 'Unauthorized Access'),
|
||||
('missing_item', 'Missing Item'),
|
||||
('documentation_error', 'Documentation Error'),
|
||||
('visitor_violation', 'Visitor Violation'),
|
||||
('cyber', 'Cyber Incident'),
|
||||
('other', 'Other'),
|
||||
],
|
||||
string='Incident Type',
|
||||
default='other',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
severity = fields.Selection(
|
||||
[
|
||||
('minor', 'Minor'),
|
||||
('major', 'Major'),
|
||||
('critical', 'Critical'),
|
||||
],
|
||||
string='Severity',
|
||||
default='minor',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
description = fields.Html(string='Description')
|
||||
containment = fields.Html(string='Containment Actions')
|
||||
reported_to_pspc = fields.Boolean(
|
||||
string='Reported to PSPC',
|
||||
tracking=True,
|
||||
)
|
||||
pspc_notification_date = fields.Date(
|
||||
string='PSPC Notification Date',
|
||||
tracking=True,
|
||||
)
|
||||
corrective_action = fields.Html(string='Corrective Action')
|
||||
state = fields.Selection(
|
||||
[
|
||||
('discovered', 'Discovered'),
|
||||
('investigating', 'Investigating'),
|
||||
('reported', 'Reported'),
|
||||
('closed', 'Closed'),
|
||||
],
|
||||
string='Status',
|
||||
default='discovered',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.model
|
||||
def _default_name(self):
|
||||
seq = self.env['ir.sequence'].next_by_code('fusion.plating.cgp.incident')
|
||||
return seq or '/'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get('name') or vals.get('name') == '/':
|
||||
vals['name'] = self._default_name()
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_investigate(self):
|
||||
self.write({'state': 'investigating'})
|
||||
|
||||
def action_report(self):
|
||||
self.write({
|
||||
'state': 'reported',
|
||||
'reported_to_pspc': True,
|
||||
'pspc_notification_date': fields.Date.context_today(self),
|
||||
})
|
||||
|
||||
def action_close(self):
|
||||
self.write({'state': 'closed'})
|
||||
|
||||
def action_reset(self):
|
||||
self.write({'state': 'discovered'})
|
||||
109
fusion_plating/fusion_plating_cgp/models/fp_cgp_visitor.py
Normal file
109
fusion_plating/fusion_plating_cgp/models/fp_cgp_visitor.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# -*- 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 FpCgpVisitor(models.Model):
|
||||
"""Visitor log entry under the Controlled Goods Program.
|
||||
|
||||
Every visitor to a site that handles controlled goods must be
|
||||
logged, approved by an Authorized Individual, and escorted if
|
||||
they do not have a PSA on file. Foreign nationals can only access
|
||||
controlled goods under very specific conditions.
|
||||
"""
|
||||
_name = 'fusion.plating.cgp.visitor'
|
||||
_description = 'Fusion Plating — CGP Visitor'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'visit_date desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Visitor Name',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
company_represented = fields.Char(
|
||||
string='Company Represented',
|
||||
tracking=True,
|
||||
)
|
||||
visit_date = fields.Datetime(
|
||||
string='Visit Start',
|
||||
required=True,
|
||||
default=lambda self: fields.Datetime.now(),
|
||||
tracking=True,
|
||||
)
|
||||
visit_end = fields.Datetime(
|
||||
string='Visit End',
|
||||
tracking=True,
|
||||
)
|
||||
purpose = fields.Text(
|
||||
string='Purpose',
|
||||
tracking=True,
|
||||
)
|
||||
host_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
string='Host',
|
||||
tracking=True,
|
||||
)
|
||||
is_canadian_citizen = fields.Boolean(
|
||||
string='Canadian Citizen',
|
||||
tracking=True,
|
||||
)
|
||||
visitor_psa_on_file = fields.Boolean(
|
||||
string='PSA On File',
|
||||
tracking=True,
|
||||
help='The visitor has a valid Personnel Security Assessment '
|
||||
'on file with PSPC or an approved foreign equivalent.',
|
||||
)
|
||||
approved_by_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Approved By',
|
||||
tracking=True,
|
||||
help='Must be an Authorized Individual.',
|
||||
)
|
||||
escort_required = fields.Boolean(
|
||||
string='Escort Required',
|
||||
default=True,
|
||||
tracking=True,
|
||||
)
|
||||
accessed_controlled_area = fields.Boolean(
|
||||
string='Accessed Controlled Area',
|
||||
tracking=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
('scheduled', 'Scheduled'),
|
||||
('checked_in', 'Checked In'),
|
||||
('checked_out', 'Checked Out'),
|
||||
('denied', 'Denied'),
|
||||
('cancelled', 'Cancelled'),
|
||||
],
|
||||
string='Status',
|
||||
default='scheduled',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
notes = fields.Html(string='Notes')
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
def action_check_in(self):
|
||||
self.write({'state': 'checked_in'})
|
||||
|
||||
def action_check_out(self):
|
||||
self.write({
|
||||
'state': 'checked_out',
|
||||
'visit_end': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
def action_deny(self):
|
||||
self.write({'state': 'denied'})
|
||||
|
||||
def action_cancel(self):
|
||||
self.write({'state': 'cancelled'})
|
||||
74
fusion_plating/fusion_plating_cgp/models/hr_employee.py
Normal file
74
fusion_plating/fusion_plating_cgp/models/hr_employee.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- 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 HrEmployee(models.Model):
|
||||
"""Extend hr.employee with CGP-specific fields.
|
||||
|
||||
Uses the ``x_fc_`` prefix on every new field, per Fusion Central
|
||||
convention for extensions to base Odoo models.
|
||||
"""
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
x_fc_cgp_psa_id = fields.Many2one(
|
||||
'fusion.plating.cgp.psa',
|
||||
string='Current PSA',
|
||||
help='The current Personnel Security Assessment on file for '
|
||||
'this employee.',
|
||||
)
|
||||
x_fc_cgp_psa_status = fields.Selection(
|
||||
[
|
||||
('not_assessed', 'Not Assessed'),
|
||||
('current', 'Current'),
|
||||
('expiring', 'Expiring'),
|
||||
('expired', 'Expired'),
|
||||
],
|
||||
string='PSA Status',
|
||||
compute='_compute_x_fc_cgp_psa_status',
|
||||
store=False,
|
||||
)
|
||||
x_fc_cgp_ai = fields.Boolean(
|
||||
string='Authorized Individual',
|
||||
help='This employee is an Authorized Individual under the '
|
||||
'Controlled Goods Program.',
|
||||
)
|
||||
x_fc_cgp_access_level = fields.Selection(
|
||||
[
|
||||
('none', 'No Access'),
|
||||
('general', 'General Access'),
|
||||
('controlled_area', 'Controlled Area'),
|
||||
('designated_official', 'Designated Official'),
|
||||
],
|
||||
string='CGP Access Level',
|
||||
default='none',
|
||||
help='Level of physical access this employee has to CGP areas.',
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
'x_fc_cgp_psa_id',
|
||||
'x_fc_cgp_psa_id.state',
|
||||
'x_fc_cgp_psa_id.expiry_date',
|
||||
)
|
||||
def _compute_x_fc_cgp_psa_status(self):
|
||||
today = fields.Date.context_today(self)
|
||||
warn_cutoff = today + relativedelta(months=3)
|
||||
for rec in self:
|
||||
psa = rec.x_fc_cgp_psa_id
|
||||
if not psa or psa.state != 'completed':
|
||||
rec.x_fc_cgp_psa_status = 'not_assessed'
|
||||
continue
|
||||
expiry = psa.expiry_date
|
||||
if not expiry:
|
||||
rec.x_fc_cgp_psa_status = 'current'
|
||||
elif expiry < today:
|
||||
rec.x_fc_cgp_psa_status = 'expired'
|
||||
elif expiry <= warn_cutoff:
|
||||
rec.x_fc_cgp_psa_status = 'expiring'
|
||||
else:
|
||||
rec.x_fc_cgp_psa_status = 'current'
|
||||
35
fusion_plating/fusion_plating_cgp/models/res_company.py
Normal file
35
fusion_plating/fusion_plating_cgp/models/res_company.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- 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 ResCompany(models.Model):
|
||||
"""Extend res.company with a link to the current CGP registration."""
|
||||
_inherit = 'res.company'
|
||||
|
||||
x_fc_cgp_registration_id = fields.Many2one(
|
||||
'fusion.plating.cgp.registration',
|
||||
string='CGP Registration',
|
||||
domain="[('company_id', '=', id)]",
|
||||
help='Currently active Controlled Goods Program registration '
|
||||
'for this company.',
|
||||
)
|
||||
x_fc_cgp_registered = fields.Boolean(
|
||||
string='CGP Registered',
|
||||
compute='_compute_x_fc_cgp_registered',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
'x_fc_cgp_registration_id',
|
||||
'x_fc_cgp_registration_id.state',
|
||||
)
|
||||
def _compute_x_fc_cgp_registered(self):
|
||||
for rec in self:
|
||||
reg = rec.x_fc_cgp_registration_id
|
||||
rec.x_fc_cgp_registered = bool(
|
||||
reg and reg.state == 'registered'
|
||||
)
|
||||
Reference in New Issue
Block a user