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,10 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import fp_shopfloor_station
from . import fp_bake_oven
from . import fp_bake_window
from . import fp_first_piece_gate
from . import fp_operator_queue
from . import fp_tank

View File

@@ -0,0 +1,68 @@
# -*- 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 FpBakeOven(models.Model):
"""A bake oven master record.
Used by hydrogen embrittlement relief baking and other post-process bakes.
Carries a chart-recorder reference so traceability evidence can be stitched
to a bake window record by serial number.
"""
_name = 'fusion.plating.bake.oven'
_description = 'Fusion Plating — Bake Oven'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'facility_id, code'
name = fields.Char(
string='Name',
required=True,
tracking=True,
)
code = fields.Char(
string='Code',
required=True,
tracking=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
required=True,
ondelete='restrict',
tracking=True,
)
work_center_id = fields.Many2one(
'fusion.plating.work.center',
string='Work Center',
domain="[('facility_id','=',facility_id)]",
ondelete='restrict',
tracking=True,
)
target_temp_min = fields.Float(
string='Target Temp Min',
help='Lower bound of target oven temperature.',
)
target_temp_max = fields.Float(
string='Target Temp Max',
help='Upper bound of target oven temperature.',
)
chart_recorder_ref = fields.Char(
string='Chart Recorder Ref',
help='Serial / asset reference of the chart recorder providing trace evidence.',
)
active = fields.Boolean(
string='Active',
default=True,
)
_sql_constraints = [
(
'fp_bake_oven_code_facility_uniq',
'unique(code, facility_id)',
'Bake oven code must be unique within a facility.',
),
]

View File

@@ -0,0 +1,288 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from datetime import timedelta
from odoo import api, fields, models
class FpBakeWindow(models.Model):
"""Hydrogen embrittlement relief bake window enforcer.
When a high-strength-steel part exits a plating tank, a clock starts.
The customer / specification defines a window (typically 1 to 4 hours)
inside which the relief bake MUST begin. Missing the window requires
scrap or rework — there is no retroactive fix.
This model is the headline differentiator of the shop-floor module.
A cron job updates state every 5 minutes so the kanban board on the
tablet always reflects current jeopardy.
"""
_name = 'fusion.plating.bake.window'
_description = 'Fusion Plating — Bake Window'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'bake_required_by, id desc'
_rec_name = 'name'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self._default_name(),
tracking=True,
)
bath_id = fields.Many2one(
'fusion.plating.bath',
string='Bath',
required=True,
ondelete='restrict',
tracking=True,
)
tank_id = fields.Many2one(
'fusion.plating.tank',
related='bath_id.tank_id',
store=True,
readonly=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
related='bath_id.facility_id',
store=True,
readonly=True,
)
company_id = fields.Many2one(
'res.company',
related='bath_id.company_id',
store=True,
readonly=True,
)
# ----- Job identity ---------------------------------------------------
part_ref = fields.Char(
string='Part Reference',
tracking=True,
)
lot_ref = fields.Char(
string='Lot Reference',
tracking=True,
)
customer_ref = fields.Char(
string='Customer Reference',
tracking=True,
)
quantity = fields.Integer(
string='Quantity',
)
# ----- The clock ------------------------------------------------------
plate_exit_time = fields.Datetime(
string='Plate Exit Time',
required=True,
default=fields.Datetime.now,
tracking=True,
help='Moment the part left the plating tank. Starts the bake-window clock.',
)
window_hours = fields.Float(
string='Window (hours)',
default=1.0,
required=True,
tracking=True,
help='Customer-specified window inside which the relief bake must begin.',
)
bake_required_by = fields.Datetime(
string='Bake Required By',
compute='_compute_required_by',
store=True,
tracking=True,
)
# ----- The bake -------------------------------------------------------
bake_start_time = fields.Datetime(
string='Bake Start Time',
tracking=True,
)
bake_end_time = fields.Datetime(
string='Bake End Time',
tracking=True,
)
bake_temp = fields.Float(
string='Bake Temp',
)
bake_duration_hours = fields.Float(
string='Bake Duration (hours)',
)
oven_id = fields.Many2one(
'fusion.plating.bake.oven',
string='Oven',
ondelete='restrict',
tracking=True,
)
operator_id = fields.Many2one(
'res.users',
string='Operator',
default=lambda self: self.env.user,
tracking=True,
)
# ----- State / display ------------------------------------------------
state = fields.Selection(
[
('awaiting_bake', 'Awaiting Bake'),
('bake_in_progress', 'Bake In Progress'),
('baked', 'Baked'),
('missed_window', 'Missed Window'),
('scrapped', 'Scrapped'),
],
string='Status',
default='awaiting_bake',
required=True,
tracking=True,
)
status_color = fields.Integer(
string='Status Color',
compute='_compute_status_color',
help='Kanban colour index. Green if plenty of time, yellow at 75% '
'of the window consumed, red if missed.',
)
time_remaining_display = fields.Char(
string='Time Remaining',
compute='_compute_time_remaining',
help='HH:MM:SS countdown to bake_required_by.',
)
notes = fields.Html(
string='Notes',
)
active = fields.Boolean(
default=True,
)
# ==========================================================================
# Defaults
# ==========================================================================
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.bake.window')
return seq or '/'
# ==========================================================================
# Computes
# ==========================================================================
@api.depends('plate_exit_time', 'window_hours')
def _compute_required_by(self):
for rec in self:
if rec.plate_exit_time and rec.window_hours:
rec.bake_required_by = rec.plate_exit_time + timedelta(
hours=rec.window_hours
)
else:
rec.bake_required_by = False
@api.depends('state', 'plate_exit_time', 'window_hours', 'bake_required_by',
'bake_start_time')
def _compute_status_color(self):
"""Kanban colour index — neutral palette that works in light + dark.
0=no color, 1=red, 2=orange, 3=yellow, 4=green, 5=purple, 10=grey
"""
now = fields.Datetime.now()
for rec in self:
if rec.state == 'baked':
rec.status_color = 4 # green
elif rec.state == 'scrapped':
rec.status_color = 1 # red
elif rec.state == 'missed_window':
rec.status_color = 1 # red
elif rec.state == 'bake_in_progress':
rec.status_color = 5 # purple
elif rec.state == 'awaiting_bake' and rec.bake_required_by:
if now >= rec.bake_required_by:
rec.status_color = 1 # red — missed
elif rec.plate_exit_time and rec.window_hours:
elapsed = (now - rec.plate_exit_time).total_seconds()
total = rec.window_hours * 3600.0
pct = (elapsed / total) if total else 0.0
if pct >= 0.75:
rec.status_color = 3 # yellow
elif pct >= 0.5:
rec.status_color = 2 # orange (warning lean)
else:
rec.status_color = 4 # green
else:
rec.status_color = 4
else:
rec.status_color = 0
@api.depends('bake_required_by', 'state')
def _compute_time_remaining(self):
now = fields.Datetime.now()
for rec in self:
if rec.state in ('baked', 'scrapped'):
rec.time_remaining_display = ''
continue
if not rec.bake_required_by:
rec.time_remaining_display = ''
continue
delta = rec.bake_required_by - now
seconds = int(delta.total_seconds())
if seconds <= 0:
rec.time_remaining_display = 'OVERDUE'
continue
hours = seconds // 3600
minutes = (seconds % 3600) // 60
secs = seconds % 60
rec.time_remaining_display = f"{hours:02d}:{minutes:02d}:{secs:02d}"
# ==========================================================================
# Actions
# ==========================================================================
def action_start_bake(self):
for rec in self:
rec.write({
'state': 'bake_in_progress',
'bake_start_time': fields.Datetime.now(),
})
def action_end_bake(self):
for rec in self:
vals = {
'state': 'baked',
'bake_end_time': fields.Datetime.now(),
}
if rec.bake_start_time:
delta = fields.Datetime.now() - rec.bake_start_time
vals['bake_duration_hours'] = delta.total_seconds() / 3600.0
rec.write(vals)
def action_scrap(self):
self.write({'state': 'scrapped'})
# ==========================================================================
# Cron
# ==========================================================================
@api.model
def _cron_update_states(self):
"""Flip awaiting_bake records past their window to missed_window."""
now = fields.Datetime.now()
candidates = self.search([
('state', '=', 'awaiting_bake'),
('bake_required_by', '!=', False),
('bake_required_by', '<', now),
])
if candidates:
candidates.write({'state': 'missed_window'})
for rec in candidates:
rec.message_post(
body=(
f"Bake window missed: required by "
f"{fields.Datetime.to_string(rec.bake_required_by)}, "
f"now {fields.Datetime.to_string(now)}."
)
)
# Touching status_color/time_remaining is automatic via compute on read,
# but we trigger a recompute marker so kanban refreshes pick up colour
# changes within the 5-minute window.
return True

View File

@@ -0,0 +1,128 @@
# -*- 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 FpFirstPieceGate(models.Model):
"""First-piece inspection gate per routing.
Aerospace, nuclear and many automotive customers require that the FIRST
piece off a routing be inspected and dispositioned BEFORE the rest of
the lot is allowed to run. This model captures that gate.
"""
_name = 'fusion.plating.first.piece.gate'
_description = 'Fusion Plating — First-Piece Inspection Gate'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'first_piece_produced desc, id desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self._default_name(),
tracking=True,
)
bath_id = fields.Many2one(
'fusion.plating.bath',
string='Bath',
ondelete='restrict',
tracking=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
related='bath_id.facility_id',
store=True,
readonly=True,
)
part_ref = fields.Char(
string='Part Reference',
tracking=True,
)
customer_ref = fields.Char(
string='Customer Reference',
tracking=True,
)
routing_first_run = fields.Boolean(
string='First Run of Routing',
help='Tick if this is the first time this routing runs this part.',
)
first_piece_produced = fields.Datetime(
string='First Piece Produced',
tracking=True,
)
first_piece_inspected = fields.Datetime(
string='First Piece Inspected',
tracking=True,
)
inspector_id = fields.Many2one(
'res.users',
string='Inspector',
tracking=True,
)
result = fields.Selection(
[
('pending', 'Pending'),
('pass', 'Pass'),
('fail', 'Fail'),
],
string='Result',
default='pending',
required=True,
tracking=True,
)
rest_of_lot_released = fields.Boolean(
string='Rest of Lot Released',
tracking=True,
)
notes = fields.Html(
string='Notes',
)
status_color = fields.Integer(
compute='_compute_status_color',
)
active = fields.Boolean(default=True)
# ==========================================================================
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.first.piece.gate')
return seq or '/'
@api.depends('result', 'rest_of_lot_released')
def _compute_status_color(self):
for rec in self:
if rec.result == 'fail':
rec.status_color = 1 # red
elif rec.result == 'pass' and rec.rest_of_lot_released:
rec.status_color = 4 # green
elif rec.result == 'pass':
rec.status_color = 3 # yellow — passed but not released
else:
rec.status_color = 5 # purple — pending
# ==========================================================================
# Actions
# ==========================================================================
def action_mark_pass(self):
self.write({
'result': 'pass',
'first_piece_inspected': fields.Datetime.now(),
'inspector_id': self.env.user.id,
})
def action_mark_fail(self):
self.write({
'result': 'fail',
'first_piece_inspected': fields.Datetime.now(),
'inspector_id': self.env.user.id,
})
def action_release_lot(self):
self.filtered(lambda r: r.result == 'pass').write({
'rest_of_lot_released': True,
})

View File

@@ -0,0 +1,119 @@
# -*- 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 FpOperatorQueue(models.TransientModel):
"""Transient operator next-up queue.
Built on demand from in-flight bake windows + bath log activities,
so the tablet always renders fresh state without persisting a queue
table that would drift from reality.
"""
_name = 'fusion.plating.operator.queue'
_description = 'Fusion Plating — Operator Next-Up Queue'
_order = 'priority desc, due_at, id'
operator_id = fields.Many2one(
'res.users',
string='Operator',
default=lambda self: self.env.user,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
)
work_center_id = fields.Many2one(
'fusion.plating.work.center',
string='Work Center',
)
label = fields.Char(string='Label')
description = fields.Text(string='Description')
priority = fields.Integer(string='Priority', default=0)
due_at = fields.Datetime(string='Due At')
source_model = fields.Char(string='Source Model')
source_id = fields.Integer(string='Source ID')
@api.model
def build_for_user(self, user_id=None, facility_id=None):
"""Build (and return) a transient queue snapshot for the given user."""
user_id = user_id or self.env.user.id
self.search([('operator_id', '=', user_id)]).unlink()
rows = []
bake_domain = [('state', 'in', ('awaiting_bake', 'bake_in_progress'))]
if facility_id:
bake_domain.append(('facility_id', '=', facility_id))
bakes = self.env['fusion.plating.bake.window'].search(
bake_domain, order='bake_required_by'
)
for bw in bakes:
rows.append({
'operator_id': user_id,
'facility_id': bw.facility_id.id,
'label': f"Bake: {bw.name}",
'description': (
f"Part {bw.part_ref or '?'} • Lot {bw.lot_ref or '?'}"
f"Required by {fields.Datetime.to_string(bw.bake_required_by) if bw.bake_required_by else '?'}"
),
'priority': 100 if bw.state == 'awaiting_bake' else 50,
'due_at': bw.bake_required_by,
'source_model': 'fusion.plating.bake.window',
'source_id': bw.id,
})
gate_domain = [('result', '=', 'pending')]
if facility_id:
gate_domain.append(('facility_id', '=', facility_id))
gates = self.env['fusion.plating.first.piece.gate'].search(gate_domain)
for g in gates:
rows.append({
'operator_id': user_id,
'facility_id': g.facility_id.id,
'label': f"First piece: {g.name}",
'description': f"Part {g.part_ref or '?'}",
'priority': 80,
'source_model': 'fusion.plating.first.piece.gate',
'source_id': g.id,
})
# ----- MRP work orders (if fusion_plating_bridge_mrp installed) -----
MrpWO = self.env.get('mrp.workorder')
if MrpWO is not None:
wo_domain = [('state', 'in', ('ready', 'progress'))]
if facility_id:
wo_domain.append(('workcenter_id.x_fc_facility_id', '=', facility_id))
work_orders = MrpWO.search(wo_domain, order='sequence, date_start')
for wo in work_orders:
rows.append({
'operator_id': user_id,
'facility_id': (
wo.workcenter_id.x_fc_facility_id.id
if hasattr(wo.workcenter_id, 'x_fc_facility_id')
and wo.workcenter_id.x_fc_facility_id
else False
),
'work_center_id': (
wo.workcenter_id.x_fc_fp_work_center_id.id
if hasattr(wo.workcenter_id, 'x_fc_fp_work_center_id')
and wo.workcenter_id.x_fc_fp_work_center_id
else False
),
'label': f"WO: {wo.display_name}",
'description': (
f"{wo.production_id.product_id.display_name or '?'}"
f"MO {wo.production_id.name or '?'}"
f"Qty {getattr(wo, 'qty_remaining', '') or getattr(wo, 'qty_production', '') or wo.production_id.product_qty}"
),
'priority': 90 if wo.state == 'ready' else 60,
'due_at': wo.date_start or False,
'source_model': 'mrp.workorder',
'source_id': wo.id,
})
if rows:
self.create(rows)
return self.search([('operator_id', '=', user_id)])

View File

@@ -0,0 +1,94 @@
# -*- 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 FpShopfloorStation(models.Model):
"""A registered shop-floor station: tablet, kiosk, desktop, or mobile.
Each station is identified by a unique code and a scannable QR identifier
so an operator can pair their device to a work centre with a single tap.
"""
_name = 'fusion.plating.shopfloor.station'
_description = 'Fusion Plating — Shop Floor Station'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'facility_id, work_center_id, code'
name = fields.Char(
string='Name',
required=True,
tracking=True,
)
code = fields.Char(
string='Code',
required=True,
tracking=True,
help='Short unique identifier (e.g. "TAB-EN-01").',
)
qr_code = fields.Char(
string='QR Code',
help='Scannable station identifier. Defaults to FP-STATION:<code>.',
)
active = fields.Boolean(
string='Active',
default=True,
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
required=True,
ondelete='restrict',
tracking=True,
)
work_center_id = fields.Many2one(
'fusion.plating.work.center',
string='Work Center',
domain="[('facility_id','=',facility_id)]",
ondelete='restrict',
tracking=True,
)
station_type = fields.Selection(
[
('tablet', 'Tablet'),
('kiosk', 'Kiosk'),
('desktop', 'Desktop'),
('mobile', 'Mobile'),
],
string='Station Type',
default='tablet',
required=True,
)
current_operator_id = fields.Many2one(
'res.users',
string='Current Operator',
tracking=True,
)
last_ping = fields.Datetime(
string='Last Ping',
)
notes = fields.Text(
string='Notes',
)
_sql_constraints = [
(
'fp_shopfloor_station_code_uniq',
'unique(code)',
'Shop-floor station code must be unique.',
),
]
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('qr_code') and vals.get('code'):
vals['qr_code'] = f"FP-STATION:{vals['code']}"
return super().create(vals_list)
def action_ping(self):
"""Bump the last_ping timestamp (called from the tablet client)."""
self.write({'last_ping': fields.Datetime.now()})
return True

View File

@@ -0,0 +1,54 @@
# -*- 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 FpTank(models.Model):
"""Extend the core tank with shop-floor helpers.
Adds a queue-size badge so the tank kanban can advertise jeopardy and
a one-tap action to launch the tablet view focused on this tank.
"""
_inherit = 'fusion.plating.tank'
x_fp_shopfloor_queue_size = fields.Integer(
string='Shopfloor Queue',
compute='_compute_shopfloor_queue_size',
help='Number of bake windows + first-piece gates currently waiting on '
'this tank\'s current bath.',
)
def _compute_shopfloor_queue_size(self):
BakeWindow = self.env['fusion.plating.bake.window']
Gate = self.env['fusion.plating.first.piece.gate']
for rec in self:
bath_ids = rec.bath_ids.ids
if not bath_ids:
rec.x_fp_shopfloor_queue_size = 0
continue
count = BakeWindow.search_count([
('bath_id', 'in', bath_ids),
('state', 'in', ('awaiting_bake', 'bake_in_progress')),
])
count += Gate.search_count([
('bath_id', 'in', bath_ids),
('result', '=', 'pending'),
])
rec.x_fp_shopfloor_queue_size = count
def action_open_tablet_view(self):
"""Open the tablet client action focused on this tank."""
self.ensure_one()
return {
'type': 'ir.actions.client',
'tag': 'fp_shopfloor_tablet',
'name': 'Shop Floor Tablet',
'params': {
'tank_id': self.id,
'qr_code': self.qr_code or '',
},
'target': 'fullscreen',
}