diff --git a/fusion_iot/fusion_plating_iot/__manifest__.py b/fusion_iot/fusion_plating_iot/__manifest__.py
index 929ef302..a1bb846a 100644
--- a/fusion_iot/fusion_plating_iot/__manifest__.py
+++ b/fusion_iot/fusion_plating_iot/__manifest__.py
@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — IoT Integration',
- 'version': '19.0.0.2.0',
+ 'version': '19.0.0.3.0',
'category': 'Manufacturing/Plating',
'summary': 'Wire physical tank sensors to Fusion Plating — live '
'temperature / chemistry readings with auto quality holds '
diff --git a/fusion_iot/fusion_plating_iot/models/fp_tank_reading.py b/fusion_iot/fusion_plating_iot/models/fp_tank_reading.py
index 0035eb8e..8d982d3f 100644
--- a/fusion_iot/fusion_plating_iot/models/fp_tank_reading.py
+++ b/fusion_iot/fusion_plating_iot/models/fp_tank_reading.py
@@ -95,6 +95,66 @@ class FpTankReading(models.Model):
r.display_value = r.value
r.display_unit = r.parameter_id.uom or ''
+ # ------------------------------------------------------------------
+ # Deviation from setpoint — signed Δ from the sensor's effective target
+ # in the company-preferred unit. Zero if no setpoint defined.
+ # ------------------------------------------------------------------
+ deviation_from_target = fields.Float(
+ string='Δ from Setpoint',
+ compute='_compute_deviation',
+ digits=(12, 2),
+ help='Signed difference between this reading and the sensor\'s '
+ 'setpoint, in the company-preferred unit. Positive = above '
+ 'setpoint, negative = below. Zero if no setpoint is defined.',
+ )
+ deviation_band = fields.Selection(
+ [
+ ('none', 'No setpoint'),
+ ('on', 'On target (±1°)'),
+ ('near', 'Near target (±3°)'),
+ ('far', 'Far from target (>3°)'),
+ ('out', 'Out of spec'),
+ ],
+ string='Band',
+ compute='_compute_deviation',
+ help='Coarse band for quick visual scanning in lists.',
+ )
+
+ @api.depends('value', 'display_value', 'in_spec',
+ 'sensor_id', 'sensor_id.target_value_override',
+ 'sensor_id.parameter_id.target_value',
+ 'sensor_id.parameter_id.parameter_type')
+ def _compute_deviation(self):
+ pref = self.env.company.x_fc_default_temp_uom or 'C'
+ for r in self:
+ sensor = r.sensor_id
+ if not sensor:
+ r.deviation_from_target = 0.0
+ r.deviation_band = 'none'
+ continue
+ raw_sp = sensor._get_setpoint()
+ if not raw_sp:
+ r.deviation_from_target = 0.0
+ r.deviation_band = 'none'
+ continue
+ # Convert setpoint + value to display unit for the delta so the
+ # number shown matches what operators expect (°F if pref=F).
+ ptype = (sensor.parameter_id.parameter_type or '').lower()
+ if ptype == 'temperature' and pref == 'F':
+ sp_disp = raw_sp * 9.0 / 5.0 + 32.0
+ else:
+ sp_disp = raw_sp
+ delta = r.display_value - sp_disp
+ r.deviation_from_target = round(delta, 2)
+ if not r.in_spec:
+ r.deviation_band = 'out'
+ elif abs(delta) <= 1.0:
+ r.deviation_band = 'on'
+ elif abs(delta) <= 3.0:
+ r.deviation_band = 'near'
+ else:
+ r.deviation_band = 'far'
+
source = fields.Selection(
[
('iot_proxy', 'IoT Proxy (Pi)'),
diff --git a/fusion_iot/fusion_plating_iot/models/fp_tank_sensor.py b/fusion_iot/fusion_plating_iot/models/fp_tank_sensor.py
index ee734cc7..c0004daf 100644
--- a/fusion_iot/fusion_plating_iot/models/fp_tank_sensor.py
+++ b/fusion_iot/fusion_plating_iot/models/fp_tank_sensor.py
@@ -76,12 +76,24 @@ class FpTankSensor(models.Model):
)
# ------------------------------------------------------------------
- # Alerting behaviour
+ # Target / alerting behaviour — three concepts:
+ # setpoint → what we CONTROL TOWARD (dashboards, PID, trend baseline)
+ # alert min → lower alarm boundary (quality hold fires below)
+ # alert max → upper alarm boundary (quality hold fires above)
+ #
+ # All three have a parameter-level default + per-sensor override
+ # pattern so users can fine-tune per-tank without touching the
+ # shop-wide spec. Zero on an override field = "inherit from parameter".
# ------------------------------------------------------------------
+ target_value_override = fields.Float(
+ string='Setpoint / Optimum (override)', digits=(10, 4),
+ help='Optional per-sensor override of the parameter\'s default '
+ 'setpoint. Leave 0 to inherit from bath.parameter.target_value.',
+ )
alert_on_out_of_spec = fields.Boolean(
string='Alert on Out-of-Spec', default=True,
help='If checked, a fusion.plating.quality.hold is auto-created '
- 'when a reading falls outside the parameter target range.',
+ 'when a reading falls outside the alert_min/alert_max band.',
)
alert_min_override = fields.Float(
string='Alert Min (override)', digits=(10, 4),
@@ -94,6 +106,37 @@ class FpTankSensor(models.Model):
'specific sensor.',
)
+ # ------------------------------------------------------------------
+ # Effective target — resolves override → parameter default → 0
+ # ------------------------------------------------------------------
+ effective_target = fields.Float(
+ string='Effective Setpoint',
+ compute='_compute_effective_target',
+ digits=(12, 2),
+ help='The setpoint currently in use, after applying any per-sensor '
+ 'override. Displayed in the company-preferred unit.',
+ )
+ effective_target_unit = fields.Char(
+ string='Setpoint Unit',
+ compute='_compute_effective_target',
+ )
+
+ @api.depends('target_value_override', 'parameter_id.target_value',
+ 'parameter_id.parameter_type', 'parameter_id.uom')
+ def _compute_effective_target(self):
+ pref = self.env.company.x_fc_default_temp_uom or 'C'
+ for rec in self:
+ raw = rec.target_value_override or (
+ rec.parameter_id.target_value if rec.parameter_id else 0.0
+ )
+ ptype = (rec.parameter_id.parameter_type or '').lower()
+ if ptype == 'temperature' and pref == 'F':
+ rec.effective_target = raw * 9.0 / 5.0 + 32.0 if raw else 0.0
+ rec.effective_target_unit = '°F'
+ else:
+ rec.effective_target = raw
+ rec.effective_target_unit = rec.parameter_id.uom or ''
+
# ------------------------------------------------------------------
# Cached latest-reading fields (for quick display in list views)
# ------------------------------------------------------------------
@@ -163,6 +206,17 @@ class FpTankSensor(models.Model):
)
return (lo or 0.0, hi or 0.0)
+ def _get_setpoint(self):
+ """Canonical (raw) setpoint used for deviation calcs.
+
+ Returns 0.0 if no setpoint is set at either level — callers should
+ treat 0 as "no target defined, skip deviation display".
+ """
+ self.ensure_one()
+ return self.target_value_override or (
+ self.parameter_id.target_value if self.parameter_id else 0.0
+ ) or 0.0
+
def action_view_readings(self):
self.ensure_one()
return {
diff --git a/fusion_iot/fusion_plating_iot/views/fp_tank_reading_views.xml b/fusion_iot/fusion_plating_iot/views/fp_tank_reading_views.xml
index e2023b95..48fc1de6 100644
--- a/fusion_iot/fusion_plating_iot/views/fp_tank_reading_views.xml
+++ b/fusion_iot/fusion_plating_iot/views/fp_tank_reading_views.xml
@@ -10,15 +10,22 @@
fp.tank.reading
+ default_order="reading_at desc"
+ decoration-danger="deviation_band == 'out'"
+ decoration-warning="deviation_band == 'far'"
+ decoration-info="deviation_band == 'near'"
+ decoration-success="deviation_band == 'on'"
+ decoration-muted="deviation_band == 'none'">
+
+
-
+
diff --git a/fusion_iot/fusion_plating_iot/views/fp_tank_sensor_views.xml b/fusion_iot/fusion_plating_iot/views/fp_tank_sensor_views.xml
index fe03d2ac..249bab91 100644
--- a/fusion_iot/fusion_plating_iot/views/fp_tank_sensor_views.xml
+++ b/fusion_iot/fusion_plating_iot/views/fp_tank_sensor_views.xml
@@ -69,13 +69,19 @@
-
-
+
+
+
+
+
+
+
+ help="Leave 0 to inherit from the bath parameter's target_min. Readings below this fire a quality hold."/>
+ help="Leave 0 to inherit from the bath parameter's target_max. Readings above this fire a quality hold."/>
diff --git a/fusion_iot/fusion_plating_iot/views/fusion_plating_tank_views.xml b/fusion_iot/fusion_plating_iot/views/fusion_plating_tank_views.xml
index d9a0c04c..6ea521a2 100644
--- a/fusion_iot/fusion_plating_iot/views/fusion_plating_tank_views.xml
+++ b/fusion_iot/fusion_plating_iot/views/fusion_plating_tank_views.xml
@@ -24,6 +24,8 @@
+
+
diff --git a/fusion_plating/fusion_plating/__manifest__.py b/fusion_plating/fusion_plating/__manifest__.py
index b37c9437..a301d85e 100644
--- a/fusion_plating/fusion_plating/__manifest__.py
+++ b/fusion_plating/fusion_plating/__manifest__.py
@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating',
- 'version': '19.0.5.4.0',
+ 'version': '19.0.5.5.0',
'category': 'Manufacturing/Plating',
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
'description': """
diff --git a/fusion_plating/fusion_plating/models/fp_bath_parameter.py b/fusion_plating/fusion_plating/models/fp_bath_parameter.py
index 3e980d28..0f6b618f 100644
--- a/fusion_plating/fusion_plating/models/fp_bath_parameter.py
+++ b/fusion_plating/fusion_plating/models/fp_bath_parameter.py
@@ -61,6 +61,13 @@ class FpBathParameter(models.Model):
string='Default Target Max',
help='Default target maximum. Per-bath overrides are allowed.',
)
+ target_value = fields.Float(
+ string='Default Setpoint / Optimum',
+ help='The IDEAL operating value — what the heater/chiller controls '
+ 'toward, what dashboards compare against. Sits between '
+ 'target_min and target_max. Per-sensor override via '
+ 'fp.tank.sensor.target_value_override.',
+ )
warning_tolerance = fields.Float(
string='Warning Tolerance %',
default=10.0,