fix(fusion_iot): respect company temperature-unit preference in sensor UI

The sensor readings list always showed raw °C regardless of the
Plating Settings Temperature Unit preference (res.company.x_fc_default_temp_uom).
On a Fahrenheit-preferred shop, a 40°C reading should render as 105°F.

Fix: add display-aware computed fields alongside the canonical ones.

**fp.tank.reading**
- `value` / `unit` renamed with "(raw)" labels — these are the stored
  canonical values (always °C for temperature, because every
  temperature chip reports in Celsius natively)
- `display_value` + `display_unit` computed from company pref — only
  flips C→F when parameter_type='temperature' AND company pref='F';
  pH/conductivity/etc pass through untouched
- `display_name` now uses display_value so it reads naturally
  ("Sensor — 105.58 °F @ ...") regardless of region

**fp.tank.sensor**
- Mirrored the same pattern on the cached last-reading fields
- `last_reading_display` + `last_reading_display_unit` for lists
- `last_reading_value` hidden behind group_no_one (debug-only)

**Views**
- fp.tank.reading list: show display_value/display_unit, raw value
  hidden by default (toggle from column picker if needed)
- fp.tank.sensor list + form + tank inline: same pattern
- Raw value kept visible as an optional column so data engineers
  can still audit canonical storage

Why store canonical: spec thresholds (alert_min/max) live on the
sensor in °C. If the same Odoo serves a multi-region company
(Canada in C, US affiliate in F), switching a single preference
flips every UI without touching data. Alert logic keeps comparing
canonical values, so out-of-spec holds fire correctly regardless
of display unit.

Verified: 40.88°C raw → 105.58°F display on the live pilot probe
with company pref='F'. All 5 recent readings tested, display
fields updated correctly on every poll.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-20 16:13:17 -04:00
parent dd575135ae
commit 089cda71fe
6 changed files with 83 additions and 16 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — IoT Integration',
'version': '19.0.0.1.0',
'version': '19.0.0.2.0',
'category': 'Manufacturing/Plating',
'summary': 'Wire physical tank sensors to Fusion Plating — live '
'temperature / chemistry readings with auto quality holds '

View File

@@ -54,14 +54,47 @@ class FpTankReading(models.Model):
default=fields.Datetime.now, index=True,
)
value = fields.Float(
string='Value', required=True, digits=(12, 4),
help='Numeric reading in the parameter\'s native unit (°C, pH, '
'µS/cm, etc.).',
string='Value (raw)', required=True, digits=(12, 4),
help='Stored reading in the sensor\'s canonical unit (for '
'temperature sensors this is always °C — the DS18B20 and '
'every other temperature chip reports in Celsius natively; '
'keeping storage canonical lets us switch display units '
'per-company without re-migrating history).',
)
unit = fields.Char(
string='Unit', related='parameter_id.uom', store=True,
string='Unit (raw)', related='parameter_id.uom', store=True,
)
# ------------------------------------------------------------------
# Display-aware fields — converted to the company's preferred unit
# (res.company.x_fc_default_temp_uom = 'F' or 'C'). Only the list
# and form views should show these; internal spec comparisons use
# `value` so thresholds stay consistent across regions.
# ------------------------------------------------------------------
display_value = fields.Float(
string='Value', compute='_compute_display',
digits=(12, 2),
help='Reading in the company-preferred unit (Settings → Plating → '
'Temperature Unit).',
)
display_unit = fields.Char(
string='Unit', compute='_compute_display',
)
@api.depends('value', 'parameter_id', 'parameter_id.parameter_type',
'parameter_id.uom')
def _compute_display(self):
# Read once per compute call — env.company rarely changes mid-batch.
pref = self.env.company.x_fc_default_temp_uom or 'C'
for r in self:
ptype = (r.parameter_id.parameter_type or '').lower()
if ptype == 'temperature' and pref == 'F':
r.display_value = r.value * 9.0 / 5.0 + 32.0
r.display_unit = '°F'
else:
r.display_value = r.value
r.display_unit = r.parameter_id.uom or ''
source = fields.Selection(
[
('iot_proxy', 'IoT Proxy (Pi)'),
@@ -86,13 +119,12 @@ class FpTankReading(models.Model):
string='Display', compute='_compute_display_name', store=True,
)
@api.depends('sensor_id', 'value', 'reading_at')
@api.depends('sensor_id', 'display_value', 'display_unit', 'reading_at')
def _compute_display_name(self):
for r in self:
sensor = r.sensor_id.name or 'sensor'
at = fields.Datetime.to_string(r.reading_at) if r.reading_at else ''
unit = r.unit or ''
r.display_name = f'{sensor}{r.value:.2f} {unit} @ {at}'
r.display_name = f'{sensor}{r.display_value:.2f} {r.display_unit} @ {at}'
# ------------------------------------------------------------------
# Create hook — evaluate against spec + raise a quality hold if we

View File

@@ -98,13 +98,38 @@ class FpTankSensor(models.Model):
# Cached latest-reading fields (for quick display in list views)
# ------------------------------------------------------------------
last_reading_value = fields.Float(
string='Latest Value', readonly=True, digits=(12, 4),
string='Latest Value (raw)', readonly=True, digits=(12, 4),
help='Stored in the sensor\'s canonical unit (°C for temperature).',
)
last_reading_at = fields.Datetime(string='Latest Reading', readonly=True)
last_reading_in_spec = fields.Boolean(
string='In Spec?', readonly=True,
help='Computed from the last reading vs alert_min/alert_max.',
)
# Display-aware version of last_reading_value — converted to company
# preferred unit (res.company.x_fc_default_temp_uom).
last_reading_display = fields.Float(
string='Latest Value',
compute='_compute_last_reading_display',
digits=(12, 2),
)
last_reading_display_unit = fields.Char(
string='Unit',
compute='_compute_last_reading_display',
)
@api.depends('last_reading_value', 'parameter_id',
'parameter_id.parameter_type', 'parameter_id.uom')
def _compute_last_reading_display(self):
pref = self.env.company.x_fc_default_temp_uom or 'C'
for rec in self:
ptype = (rec.parameter_id.parameter_type or '').lower()
if ptype == 'temperature' and pref == 'F':
rec.last_reading_display = rec.last_reading_value * 9.0 / 5.0 + 32.0
rec.last_reading_display_unit = '°F'
else:
rec.last_reading_display = rec.last_reading_value
rec.last_reading_display_unit = rec.parameter_id.uom or ''
reading_ids = fields.One2many(
'fp.tank.reading', 'sensor_id', string='Reading History',

View File

@@ -15,8 +15,9 @@
<field name="sensor_id"/>
<field name="tank_id" optional="show"/>
<field name="parameter_id" optional="hide"/>
<field name="value"/>
<field name="unit"/>
<field name="display_value"/>
<field name="display_unit"/>
<field name="value" optional="hide" string="Value (°C raw)"/>
<field name="in_spec" widget="boolean_toggle"/>
<field name="source" optional="hide"/>
<field name="hold_id" optional="show"/>
@@ -39,8 +40,10 @@
</group>
<group>
<field name="reading_at"/>
<field name="value"/>
<field name="unit" readonly="1"/>
<field name="display_value" readonly="1"/>
<field name="display_unit" readonly="1"/>
<field name="value" readonly="1"
string="Stored Value (°C, canonical)"/>
<field name="in_spec" readonly="1"/>
<field name="hold_id" readonly="1"/>
</group>

View File

@@ -19,7 +19,9 @@
<field name="parameter_id"/>
<field name="device_serial" optional="show"/>
<field name="iot_device_id" optional="hide"/>
<field name="last_reading_value"/>
<field name="last_reading_display"/>
<field name="last_reading_display_unit"/>
<field name="last_reading_value" optional="hide" string="Latest Raw (°C)"/>
<field name="last_reading_at"/>
<field name="last_reading_in_spec" widget="boolean_toggle"/>
<field name="reading_count"/>
@@ -76,7 +78,11 @@
help="Leave 0 to inherit from the bath parameter's target_max."/>
</group>
<group string="Most Recent Reading">
<field name="last_reading_value" readonly="1"/>
<field name="last_reading_display" readonly="1"/>
<field name="last_reading_display_unit" readonly="1"/>
<field name="last_reading_value" readonly="1"
string="Stored (°C canonical)"
groups="base.group_no_one"/>
<field name="last_reading_at" readonly="1"/>
<field name="last_reading_in_spec" readonly="1" widget="boolean_toggle"/>
</group>

View File

@@ -24,7 +24,8 @@
<field name="device_kind"/>
<field name="device_serial"/>
<field name="parameter_id"/>
<field name="last_reading_value"/>
<field name="last_reading_display"/>
<field name="last_reading_display_unit"/>
<field name="last_reading_at" readonly="1"/>
<field name="last_reading_in_spec" widget="boolean_toggle"/>
<field name="alert_on_out_of_spec" widget="boolean_toggle"/>