270 lines
8.8 KiB
Python
270 lines
8.8 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 FpTank(models.Model):
|
|
"""A physical vessel that holds a bath.
|
|
|
|
Tanks are long-lived assets. Baths come and go inside a tank. The
|
|
separation lets a shop dump an exhausted bath without losing the
|
|
tank's history, QR code, or equipment records.
|
|
|
|
Each tank carries a unique QR code for operator scanning at the
|
|
shop-floor station.
|
|
"""
|
|
_name = 'fusion.plating.tank'
|
|
_description = 'Fusion Plating — Tank'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_order = 'facility_id, section_id, sequence, code'
|
|
|
|
name = fields.Char(
|
|
string='Tank Name',
|
|
required=True,
|
|
tracking=True,
|
|
)
|
|
code = fields.Char(
|
|
string='Tank Number',
|
|
required=True,
|
|
tracking=True,
|
|
help='Short unique tank identifier (e.g. "T-01", "EN-A1").',
|
|
)
|
|
qr_code = fields.Char(
|
|
string='QR Code',
|
|
help='Scannable identifier. Defaults to code, can be set to a longer URI.',
|
|
)
|
|
sequence = fields.Integer(
|
|
string='Sequence',
|
|
default=10,
|
|
)
|
|
active = fields.Boolean(
|
|
string='Active',
|
|
default=True,
|
|
)
|
|
facility_id = fields.Many2one(
|
|
'fusion.plating.facility',
|
|
string='Facility',
|
|
required=True,
|
|
ondelete='restrict',
|
|
tracking=True,
|
|
)
|
|
section_id = fields.Many2one(
|
|
'fusion.plating.tank.section',
|
|
string='Section',
|
|
ondelete='set null',
|
|
tracking=True,
|
|
help='Free-form grouping (e.g. Steel Line, Aluminum Line, Specialty Line).',
|
|
)
|
|
work_center_id = fields.Many2one(
|
|
'fusion.plating.work.center',
|
|
string='Production Line',
|
|
domain="[('facility_id','=',facility_id)]",
|
|
ondelete='restrict',
|
|
tracking=True,
|
|
)
|
|
|
|
# ----- Physical properties --------------------------------------------
|
|
volume = fields.Float(
|
|
string='Volume',
|
|
help='Working volume.',
|
|
)
|
|
volume_uom = fields.Selection(
|
|
[
|
|
('l', 'Litres'),
|
|
('gal_us', 'US gallons'),
|
|
('gal_imp', 'Imperial gallons'),
|
|
('m3', 'Cubic metres'),
|
|
],
|
|
string='Volume Unit',
|
|
default=lambda self: {
|
|
'gal': 'gal_us',
|
|
'L': 'l',
|
|
'imp_gal': 'gal_imp',
|
|
}.get(self.env.company.x_fc_default_volume_uom or 'gal', 'gal_us'),
|
|
help='Inherited from company default (Settings → Fusion Plating → '
|
|
'Units of Measure). Overrideable per tank.',
|
|
)
|
|
material = fields.Selection(
|
|
[
|
|
('polypro', 'Polypropylene'),
|
|
('pvc', 'PVC'),
|
|
('pvdf', 'PVDF'),
|
|
('ss', 'Stainless Steel'),
|
|
('lined_steel', 'Lined Steel'),
|
|
('glass', 'Glass'),
|
|
('other', 'Other'),
|
|
],
|
|
string='Construction',
|
|
)
|
|
heating_type = fields.Selection(
|
|
[
|
|
('none', 'None'),
|
|
('immersion', 'Immersion Heater'),
|
|
('steam_coil', 'Steam Coil'),
|
|
('jacket', 'Jacketed'),
|
|
('external', 'External Heat Exchanger'),
|
|
],
|
|
string='Heating',
|
|
default='none',
|
|
)
|
|
has_filtration = fields.Boolean(
|
|
string='Has Filtration',
|
|
)
|
|
has_rectifier = fields.Boolean(
|
|
string='Has Rectifier',
|
|
help='Required for electrolytic processes (chrome, anodize, strike).',
|
|
)
|
|
|
|
# ----- State ----------------------------------------------------------
|
|
state = fields.Selection(
|
|
[
|
|
('empty', 'Empty'),
|
|
('filled', 'Filled'),
|
|
('in_use', 'In Use'),
|
|
('draining', 'Draining'),
|
|
('maintenance', 'Maintenance'),
|
|
('out_of_service', 'Out of Service'),
|
|
],
|
|
string='Status',
|
|
default='empty',
|
|
tracking=True,
|
|
)
|
|
|
|
# ----- Default temperature (used as pre-fill on bath log lines) -------
|
|
default_temperature = fields.Float(
|
|
string='Default Temperature',
|
|
digits=(6, 2),
|
|
tracking=True,
|
|
help='Operating temperature setpoint. Pre-fills the temperature '
|
|
'reading on new chemistry logs so the operator can confirm with '
|
|
'one tap. Use the up/down arrows on the input to nudge by 1 unit.',
|
|
)
|
|
default_temperature_uom = fields.Selection(
|
|
[('c', '°C'), ('f', '°F')],
|
|
string='Temperature Unit',
|
|
default='c',
|
|
tracking=True,
|
|
)
|
|
|
|
# ----- Relations ------------------------------------------------------
|
|
bath_ids = fields.One2many(
|
|
'fusion.plating.bath',
|
|
'tank_id',
|
|
string='Bath History',
|
|
)
|
|
current_bath_id = fields.Many2one(
|
|
'fusion.plating.bath',
|
|
string='Current Bath',
|
|
compute='_compute_current_bath',
|
|
store=True,
|
|
)
|
|
current_bath_process_id = fields.Many2one(
|
|
'fusion.plating.process.type',
|
|
string='Current Bath Process',
|
|
related='current_bath_id.process_type_id',
|
|
store=True,
|
|
help='Process derived from the active bath. The editable "Current '
|
|
'Process" overrides this when the operator needs to flag a '
|
|
'different process (e.g. between bath swaps).',
|
|
)
|
|
current_process_id = fields.Many2one(
|
|
'fusion.plating.process.type',
|
|
string='Current Process',
|
|
ondelete='restrict',
|
|
tracking=True,
|
|
help='User-settable process flag. Defaults to the active bath\'s '
|
|
'process; can be overridden when operating off-recipe.',
|
|
)
|
|
bath_count = fields.Integer(
|
|
compute='_compute_bath_count',
|
|
)
|
|
|
|
# ----- Compositions ---------------------------------------------------
|
|
composition_ids = fields.One2many(
|
|
'fusion.plating.tank.composition',
|
|
'tank_id',
|
|
string='Compositions',
|
|
)
|
|
active_composition_id = fields.Many2one(
|
|
'fusion.plating.tank.composition',
|
|
string='Active Composition',
|
|
domain="[('tank_id', '=', id)]",
|
|
tracking=True,
|
|
help='The composition currently in service. Switching is logged in '
|
|
'the chatter for full audit history.',
|
|
)
|
|
composition_count = fields.Integer(
|
|
compute='_compute_composition_count',
|
|
)
|
|
|
|
_sql_constraints = [
|
|
(
|
|
'fp_tank_code_facility_uniq',
|
|
'unique(code, facility_id)',
|
|
'Tank code must be unique within a facility.',
|
|
),
|
|
]
|
|
|
|
@api.depends('bath_ids', 'bath_ids.state')
|
|
def _compute_current_bath(self):
|
|
for rec in self:
|
|
active = rec.bath_ids.filtered(
|
|
lambda b: b.state in ('operational', 'under_review')
|
|
)
|
|
rec.current_bath_id = active[:1].id if active else False
|
|
|
|
def _compute_bath_count(self):
|
|
for rec in self:
|
|
rec.bath_count = len(rec.bath_ids)
|
|
|
|
@api.depends('composition_ids')
|
|
def _compute_composition_count(self):
|
|
for rec in self:
|
|
rec.composition_count = len(rec.composition_ids)
|
|
|
|
@api.onchange('current_bath_process_id')
|
|
def _onchange_seed_current_process(self):
|
|
"""Pre-fill the editable Current Process from the active bath when
|
|
the operator hasn't already set one — keeps the field useful out of
|
|
the box while still allowing manual override."""
|
|
for rec in self:
|
|
if not rec.current_process_id and rec.current_bath_process_id:
|
|
rec.current_process_id = rec.current_bath_process_id
|
|
|
|
@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-TANK:{vals['code']}"
|
|
return super().create(vals_list)
|
|
|
|
# ----- State transition actions ---------------------------------------
|
|
def _set_state(self, new_state, message):
|
|
for rec in self:
|
|
old = dict(rec._fields['state'].selection).get(rec.state, rec.state)
|
|
new = dict(rec._fields['state'].selection).get(new_state, new_state)
|
|
rec.state = new_state
|
|
rec.message_post(body=f"{message} ({old} → {new}) by {self.env.user.name}")
|
|
return True
|
|
|
|
def action_set_empty(self):
|
|
return self._set_state('empty', 'Tank marked Empty')
|
|
|
|
def action_set_filled(self):
|
|
return self._set_state('filled', 'Tank marked Filled')
|
|
|
|
def action_set_in_use(self):
|
|
return self._set_state('in_use', 'Tank marked In Use')
|
|
|
|
def action_set_draining(self):
|
|
return self._set_state('draining', 'Tank marked Draining')
|
|
|
|
def action_set_maintenance(self):
|
|
return self._set_state('maintenance', 'Tank marked for Maintenance')
|
|
|
|
def action_set_out_of_service(self):
|
|
return self._set_state('out_of_service', 'Tank marked Out of Service')
|