182 lines
6.5 KiB
Python
182 lines
6.5 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 FpBathLogLine(models.Model):
|
|
"""A single parameter reading on a bath log.
|
|
|
|
Each line = one titration result or one sensor reading. Target ranges
|
|
are pulled from the bath's per-bath overrides if present, otherwise
|
|
from the parameter's defaults on fusion.plating.bath.parameter.
|
|
Status is computed per line (ok / warning / out_of_spec) and rolled
|
|
up to the parent log.
|
|
"""
|
|
_name = 'fusion.plating.bath.log.line'
|
|
_description = 'Fusion Plating — Bath Log Reading'
|
|
_order = 'log_id, sequence, id'
|
|
|
|
log_id = fields.Many2one(
|
|
'fusion.plating.bath.log',
|
|
string='Log',
|
|
required=True,
|
|
ondelete='cascade',
|
|
index=True,
|
|
)
|
|
bath_id = fields.Many2one(
|
|
related='log_id.bath_id',
|
|
store=True,
|
|
readonly=True,
|
|
)
|
|
sequence = fields.Integer(
|
|
string='Sequence',
|
|
default=10,
|
|
)
|
|
parameter_id = fields.Many2one(
|
|
'fusion.plating.bath.parameter',
|
|
string='Parameter',
|
|
required=True,
|
|
ondelete='restrict',
|
|
)
|
|
parameter_code = fields.Char(
|
|
related='parameter_id.code',
|
|
store=True,
|
|
readonly=True,
|
|
)
|
|
uom = fields.Char(
|
|
related='parameter_id.uom_display',
|
|
readonly=True,
|
|
string='Unit',
|
|
)
|
|
value = fields.Float(
|
|
string='Value',
|
|
required=True,
|
|
)
|
|
target_min = fields.Float(
|
|
string='Target Min',
|
|
compute='_compute_targets',
|
|
store=True,
|
|
)
|
|
target_max = fields.Float(
|
|
string='Target Max',
|
|
compute='_compute_targets',
|
|
store=True,
|
|
)
|
|
status = fields.Selection(
|
|
[
|
|
('ok', 'OK'),
|
|
('warning', 'Warning'),
|
|
('out_of_spec', 'Out of Spec'),
|
|
],
|
|
string='Status',
|
|
compute='_compute_status',
|
|
store=True,
|
|
)
|
|
notes = fields.Char(
|
|
string='Notes',
|
|
)
|
|
|
|
# ==========================================================================
|
|
@api.onchange('parameter_id')
|
|
def _onchange_parameter_prefill_value(self):
|
|
"""Pre-fill `value` from the tank's setpoint when the parameter is a
|
|
temperature reading.
|
|
|
|
This means the operator (or backend user) hits "add reading", picks
|
|
Temperature, and the tank's `default_temperature` lands in the value
|
|
column automatically — they confirm with one tap or nudge with
|
|
keyboard arrows. Avoids retyping the same number every shift.
|
|
|
|
Fires only when value is currently empty so the user's edits aren't
|
|
clobbered if they go back and pick a different parameter.
|
|
"""
|
|
for rec in self:
|
|
if not rec.parameter_id or rec.value:
|
|
continue
|
|
if rec.parameter_id.parameter_type != 'temperature':
|
|
continue
|
|
tank = rec.log_id.bath_id.tank_id
|
|
if tank and tank.default_temperature:
|
|
rec.value = tank.default_temperature
|
|
|
|
@api.depends('parameter_id', 'log_id.bath_id')
|
|
def _compute_targets(self):
|
|
"""Resolve target range: per-bath override first, parameter default second."""
|
|
for rec in self:
|
|
tmin = tmax = 0.0
|
|
if rec.log_id.bath_id and rec.parameter_id:
|
|
override = rec.log_id.bath_id.target_line_ids.filtered(
|
|
lambda t: t.parameter_id.id == rec.parameter_id.id
|
|
)[:1]
|
|
if override:
|
|
tmin, tmax = override.target_min, override.target_max
|
|
else:
|
|
tmin = rec.parameter_id.target_min
|
|
tmax = rec.parameter_id.target_max
|
|
rec.target_min = tmin
|
|
rec.target_max = tmax
|
|
|
|
@api.depends('value', 'target_min', 'target_max', 'parameter_id.warning_tolerance')
|
|
def _compute_status(self):
|
|
for rec in self:
|
|
if rec.target_min == 0.0 and rec.target_max == 0.0:
|
|
rec.status = 'ok'
|
|
continue
|
|
v, lo, hi = rec.value, rec.target_min, rec.target_max
|
|
if v < lo or v > hi:
|
|
rec.status = 'out_of_spec'
|
|
continue
|
|
tol_pct = (rec.parameter_id.warning_tolerance or 0.0) / 100.0
|
|
span = max(hi - lo, 1e-9)
|
|
if tol_pct > 0 and (v - lo < span * tol_pct or hi - v < span * tol_pct):
|
|
rec.status = 'warning'
|
|
else:
|
|
rec.status = 'ok'
|
|
|
|
# ------------------------------------------------------------------
|
|
# T1.2 — Auto-suggest replenishment on every log line
|
|
# ------------------------------------------------------------------
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
lines = super().create(vals_list)
|
|
lines._spawn_replenishment_suggestions()
|
|
return lines
|
|
|
|
def _spawn_replenishment_suggestions(self):
|
|
"""For every out-of-spec reading, run the matching replenishment
|
|
rule and create a pending suggestion the operator can apply."""
|
|
Rule = self.env['fusion.plating.bath.replenishment.rule']
|
|
Suggestion = self.env['fusion.plating.bath.replenishment.suggestion']
|
|
for line in self:
|
|
if not line.parameter_id or not line.log_id.bath_id:
|
|
continue
|
|
bath = line.log_id.bath_id
|
|
rules = Rule._find_rules(bath, line.parameter_id.id)
|
|
for rule in rules:
|
|
dose = rule._compute_dose(
|
|
line.value, line.target_min, line.target_max, bath.volume,
|
|
)
|
|
if dose <= 0:
|
|
continue
|
|
Suggestion.create({
|
|
'bath_id': bath.id,
|
|
'log_line_id': line.id,
|
|
'rule_id': rule.id,
|
|
'parameter_id': line.parameter_id.id,
|
|
'current_value': line.value,
|
|
'target_min': line.target_min,
|
|
'target_max': line.target_max,
|
|
'product_name': rule.product_name,
|
|
'dose_amount': dose,
|
|
'dose_uom': rule.dose_uom,
|
|
'state': 'pending',
|
|
})
|
|
bath.message_post(
|
|
body=f'Replenishment suggested: add {dose} {rule.dose_uom} '
|
|
f'of {rule.product_name} ({line.parameter_id.name} '
|
|
f'reading: {line.value})',
|
|
)
|