270 lines
8.3 KiB
Python
270 lines
8.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
# Part of the Fusion Plating product family.
|
|
|
|
from odoo import api, fields, models
|
|
|
|
|
|
class FpBath(models.Model):
|
|
"""A specific batch of chemistry in a tank.
|
|
|
|
Baths have their own lifecycle independent of the tank:
|
|
|
|
new → operational → under_review → dump_scheduled → dumped
|
|
|
|
Each bath carries:
|
|
* its process type (which chemistry it runs)
|
|
* per-bath target ranges (may override process defaults)
|
|
* running MTO counter (set and maintained by the process pack)
|
|
* chemistry log history (one2many to fusion.plating.bath.log)
|
|
|
|
Process packs (fusion_plating_process_en, etc.) add process-specific
|
|
computed fields such as orthophosphite projection or P-content band
|
|
without touching the generic bath model.
|
|
"""
|
|
_name = 'fusion.plating.bath'
|
|
_description = 'Fusion Plating — Bath'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_order = 'state, makeup_date desc, id desc'
|
|
_rec_name = 'display_name'
|
|
|
|
name = fields.Char(
|
|
string='Reference',
|
|
required=True,
|
|
copy=False,
|
|
default=lambda self: self._default_name(),
|
|
tracking=True,
|
|
)
|
|
display_name = fields.Char(
|
|
compute='_compute_display_name',
|
|
store=True,
|
|
)
|
|
tank_id = fields.Many2one(
|
|
'fusion.plating.tank',
|
|
string='Tank',
|
|
required=True,
|
|
ondelete='restrict',
|
|
tracking=True,
|
|
)
|
|
facility_id = fields.Many2one(
|
|
'fusion.plating.facility',
|
|
related='tank_id.facility_id',
|
|
store=True,
|
|
readonly=True,
|
|
)
|
|
process_type_id = fields.Many2one(
|
|
'fusion.plating.process.type',
|
|
string='Process',
|
|
required=True,
|
|
ondelete='restrict',
|
|
tracking=True,
|
|
)
|
|
company_id = fields.Many2one(
|
|
'res.company',
|
|
related='facility_id.company_id',
|
|
store=True,
|
|
readonly=True,
|
|
)
|
|
|
|
# ----- Lifecycle ------------------------------------------------------
|
|
state = fields.Selection(
|
|
[
|
|
('new', 'New'),
|
|
('operational', 'Operational'),
|
|
('under_review', 'Under Review'),
|
|
('dump_scheduled', 'Dump Scheduled'),
|
|
('dumped', 'Dumped'),
|
|
],
|
|
string='Status',
|
|
default='new',
|
|
tracking=True,
|
|
required=True,
|
|
)
|
|
status_color = fields.Integer(
|
|
string='Status Color',
|
|
compute='_compute_status_color',
|
|
help='Kanban colour index derived from state and chemistry health.',
|
|
)
|
|
makeup_date = fields.Datetime(
|
|
string='Makeup Date',
|
|
help='When this bath was made up (initial fresh charge).',
|
|
tracking=True,
|
|
)
|
|
makeup_by_id = fields.Many2one(
|
|
'res.users',
|
|
string='Made Up By',
|
|
tracking=True,
|
|
)
|
|
dump_scheduled_date = fields.Datetime(
|
|
string='Dump Scheduled',
|
|
tracking=True,
|
|
)
|
|
dumped_date = fields.Datetime(
|
|
string='Dumped Date',
|
|
tracking=True,
|
|
)
|
|
dump_reason = fields.Text(
|
|
string='Dump Reason',
|
|
)
|
|
notes = fields.Html(
|
|
string='Notes',
|
|
)
|
|
|
|
# ----- Chemistry target ranges (per-bath; override process defaults) --
|
|
target_line_ids = fields.One2many(
|
|
'fusion.plating.bath.target',
|
|
'bath_id',
|
|
string='Target Parameters',
|
|
copy=True,
|
|
)
|
|
|
|
# ----- Logs -----------------------------------------------------------
|
|
log_ids = fields.One2many(
|
|
'fusion.plating.bath.log',
|
|
'bath_id',
|
|
string='Chemistry Logs',
|
|
)
|
|
log_count = fields.Integer(
|
|
compute='_compute_log_count',
|
|
)
|
|
last_log_date = fields.Datetime(
|
|
compute='_compute_last_log',
|
|
store=True,
|
|
)
|
|
last_log_status = fields.Selection(
|
|
[
|
|
('ok', 'OK'),
|
|
('warning', 'Warning'),
|
|
('out_of_spec', 'Out of Spec'),
|
|
],
|
|
compute='_compute_last_log',
|
|
store=True,
|
|
)
|
|
|
|
# ----- Generic age / volume (process packs refine) --------------------
|
|
mto_count = fields.Float(
|
|
string='MTO',
|
|
default=0.0,
|
|
help='Metal Turnovers. Maintained by process packs that model '
|
|
'replenishment (e.g. fusion_plating_process_en).',
|
|
)
|
|
volume = fields.Float(
|
|
string='Volume',
|
|
help='Working volume (defaults to tank volume on makeup).',
|
|
)
|
|
|
|
active = fields.Boolean(default=True)
|
|
|
|
# ==========================================================================
|
|
# Defaults
|
|
# ==========================================================================
|
|
@api.model
|
|
def _default_name(self):
|
|
seq = self.env['ir.sequence'].next_by_code('fusion.plating.bath')
|
|
return seq or '/'
|
|
|
|
# ==========================================================================
|
|
# Computes
|
|
# ==========================================================================
|
|
@api.depends('name', 'process_type_id', 'tank_id')
|
|
def _compute_display_name(self):
|
|
for rec in self:
|
|
parts = [rec.name or '']
|
|
if rec.process_type_id:
|
|
parts.append(f'({rec.process_type_id.code})')
|
|
if rec.tank_id:
|
|
parts.append(f'@ {rec.tank_id.code}')
|
|
rec.display_name = ' '.join(p for p in parts if p)
|
|
|
|
def _compute_log_count(self):
|
|
for rec in self:
|
|
rec.log_count = len(rec.log_ids)
|
|
|
|
@api.depends('log_ids', 'log_ids.log_date', 'log_ids.status')
|
|
def _compute_last_log(self):
|
|
for rec in self:
|
|
last = rec.log_ids.sorted('log_date', reverse=True)[:1]
|
|
rec.last_log_date = last.log_date if last else False
|
|
rec.last_log_status = last.status if last else False
|
|
|
|
@api.depends('state', 'last_log_status')
|
|
def _compute_status_color(self):
|
|
"""Kanban colour index — neutral palette that works in light + dark.
|
|
|
|
Uses Odoo's built-in color index rather than hex codes, so themes
|
|
control the final rendering.
|
|
"""
|
|
# 0=no color, 4=green, 3=yellow, 2=orange, 1=red, 5=purple, 10=grey
|
|
for rec in self:
|
|
if rec.state == 'dumped':
|
|
rec.status_color = 10 # grey
|
|
elif rec.state == 'dump_scheduled':
|
|
rec.status_color = 2 # orange
|
|
elif rec.state == 'under_review':
|
|
rec.status_color = 3 # yellow
|
|
elif rec.state == 'new':
|
|
rec.status_color = 5 # purple
|
|
elif rec.last_log_status == 'out_of_spec':
|
|
rec.status_color = 1 # red
|
|
elif rec.last_log_status == 'warning':
|
|
rec.status_color = 3 # yellow
|
|
else:
|
|
rec.status_color = 4 # green
|
|
|
|
# ==========================================================================
|
|
# Actions
|
|
# ==========================================================================
|
|
def action_make_operational(self):
|
|
self.write({'state': 'operational'})
|
|
|
|
def action_mark_under_review(self):
|
|
self.write({'state': 'under_review'})
|
|
|
|
def action_schedule_dump(self):
|
|
self.write({
|
|
'state': 'dump_scheduled',
|
|
'dump_scheduled_date': fields.Datetime.now(),
|
|
})
|
|
|
|
def action_dump(self):
|
|
self.write({
|
|
'state': 'dumped',
|
|
'dumped_date': fields.Datetime.now(),
|
|
})
|
|
|
|
|
|
class FpBathTarget(models.Model):
|
|
"""Per-bath target range for a chemistry parameter."""
|
|
_name = 'fusion.plating.bath.target'
|
|
_description = 'Fusion Plating — Bath Target'
|
|
_order = 'bath_id, sequence, parameter_id'
|
|
|
|
bath_id = fields.Many2one(
|
|
'fusion.plating.bath',
|
|
string='Bath',
|
|
required=True,
|
|
ondelete='cascade',
|
|
)
|
|
parameter_id = fields.Many2one(
|
|
'fusion.plating.bath.parameter',
|
|
string='Parameter',
|
|
required=True,
|
|
ondelete='restrict',
|
|
)
|
|
sequence = fields.Integer(default=10)
|
|
target_min = fields.Float(string='Min')
|
|
target_max = fields.Float(string='Max')
|
|
uom = fields.Char(
|
|
related='parameter_id.uom',
|
|
readonly=True,
|
|
)
|
|
|
|
_sql_constraints = [
|
|
(
|
|
'fp_bath_target_uniq',
|
|
'unique(bath_id, parameter_id)',
|
|
'Each parameter can only be defined once per bath.',
|
|
),
|
|
]
|