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,