# -*- 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.', ), ]