chore(plating): de-dash fusion_plating_iot too

Same em-dash -> hyphen sweep applied to fusion_plating_iot (lives under
fusion_iot/ so the main commit missed it). Comments/strings only; no
functional dashes in this module. Keeps git in sync with the in-place
de-dash already applied to entech.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-05 00:35:44 -04:00
parent 8c76a16366
commit 88e1e5e9bb
12 changed files with 66 additions and 66 deletions

View File

@@ -4,22 +4,22 @@
# Part of the Fusion Plating product family. # Part of the Fusion Plating product family.
{ {
'name': 'Fusion Plating IoT Integration', 'name': 'Fusion Plating - IoT Integration',
'version': '19.0.2.0.0', 'version': '19.0.2.0.0',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': 'Wire physical tank sensors to Fusion Plating live ' 'summary': 'Wire physical tank sensors to Fusion Plating - live '
'temperature / chemistry readings with auto quality holds ' 'temperature / chemistry readings with auto quality holds '
'on out-of-spec.', 'on out-of-spec.',
'description': """ 'description': """
Fusion Plating IoT Integration Fusion Plating - IoT Integration
================================ ================================
Bridges the generic `iot` module (IoT Box + device management) to Bridges the generic `iot` module (IoT Box + device management) to
plating-specific models: plating-specific models:
* ``fp.tank.sensor`` maps an ``iot.device`` to a * ``fp.tank.sensor`` - maps an ``iot.device`` to a
``fusion.plating.tank`` (or a ``fusion.plating.bath``). ``fusion.plating.tank`` (or a ``fusion.plating.bath``).
* ``fp.tank.reading`` time-series log of every sensor reading. * ``fp.tank.reading`` - time-series log of every sensor reading.
* Auto-creates a ``fusion.plating.quality.hold`` when a reading * Auto-creates a ``fusion.plating.quality.hold`` when a reading
falls outside the tank/bath's target range (per falls outside the tank/bath's target range (per
``fusion.plating.bath.parameter`` spec). ``fusion.plating.bath.parameter`` spec).

View File

@@ -40,7 +40,7 @@ _logger = logging.getLogger(__name__)
def _parse_read_at(raw): def _parse_read_at(raw):
"""Best-effort ISO-8601 parse fall back to 'now' on garbage input.""" """Best-effort ISO-8601 parse - fall back to 'now' on garbage input."""
from odoo.fields import Datetime as OdooDatetime from odoo.fields import Datetime as OdooDatetime
if not raw: if not raw:
return OdooDatetime.now() return OdooDatetime.now()
@@ -62,20 +62,20 @@ class FpIotIngestController(http.Controller):
methods=['POST'], csrf=False, save_session=False) methods=['POST'], csrf=False, save_session=False)
def ingest(self, **_kwargs): def ingest(self, **_kwargs):
"""Accept one-or-many sensor readings and land them in fp.tank.reading.""" """Accept one-or-many sensor readings and land them in fp.tank.reading."""
# Pull the shared secret from config configured at install via # Pull the shared secret from config - configured at install via
# data/ir_config_parameter_data.xml, but admins can rotate it # data/ir_config_parameter_data.xml, but admins can rotate it
# in Settings → Technical → System Parameters. # in Settings → Technical → System Parameters.
expected = request.env['ir.config_parameter'].sudo().get_param( expected = request.env['ir.config_parameter'].sudo().get_param(
'fusion_plating_iot.ingest_token', '' 'fusion_plating_iot.ingest_token', ''
) )
if not expected: if not expected:
_logger.warning('fp.iot.ingest: token not configured all requests rejected') _logger.warning('fp.iot.ingest: token not configured - all requests rejected')
return Response( return Response(
json.dumps({'ok': False, 'error': 'token_not_configured'}), json.dumps({'ok': False, 'error': 'token_not_configured'}),
status=503, content_type='application/json', status=503, content_type='application/json',
) )
# Accept token via either header or payload body some simple # Accept token via either header or payload body - some simple
# sensors can't easily set custom headers. # sensors can't easily set custom headers.
header_token = request.httprequest.headers.get('X-FP-IOT-Token', '') header_token = request.httprequest.headers.get('X-FP-IOT-Token', '')
raw = request.httprequest.get_data(as_text=True) or '' raw = request.httprequest.get_data(as_text=True) or ''
@@ -124,10 +124,10 @@ class FpIotIngestController(http.Controller):
except (TypeError, ValueError): except (TypeError, ValueError):
continue continue
# Sub 7 per-sensor rate-limit. Drop readings that arrive # Sub 7 - per-sensor rate-limit. Drop readings that arrive
# inside the sensor's effective polling interval so the Pi # inside the sensor's effective polling interval so the Pi
# agent can happily poll every 30 s while the log only # agent can happily poll every 30 s while the log only
# retains a row every 1530 min. Inactive sensors also # retains a row every 15-30 min. Inactive sensors also
# dropped so disabled tanks don't clutter the log. # dropped so disabled tanks don't clutter the log.
if not sensor.active: if not sensor.active:
skipped_interval += 1 skipped_interval += 1

View File

@@ -3,7 +3,7 @@
Copyright 2026 Nexa Systems Inc. Copyright 2026 Nexa Systems Inc.
License OPL-1 License OPL-1
Default sensor types the common ones every plating shop needs. Default sensor types - the common ones every plating shop needs.
noupdate="1" so admins can tweak without losing edits on module upgrade. noupdate="1" so admins can tweak without losing edits on module upgrade.
--> -->
<odoo noupdate="1"> <odoo noupdate="1">

View File

@@ -1,14 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc. # Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
"""Post-install hook backfill new fields on existing live sensors. """Post-install hook - backfill new fields on existing live sensors.
Runs once on every install/upgrade. Idempotent: checks before writing Runs once on every install/upgrade. Idempotent: checks before writing
so re-runs don't overwrite user-edited values. so re-runs don't overwrite user-edited values.
What it does: What it does:
1. Populates `uuid` on any fp.tank.sensor record that doesn't have one 1. Populates `uuid` on any fp.tank.sensor record that doesn't have one
(for sensors created BEFORE the uuid field existed the create (for sensors created BEFORE the uuid field existed - the create
override only covers new records). override only covers new records).
2. Sets a default `sensor_type_id` on sensors that don't have one yet, 2. Sets a default `sensor_type_id` on sensors that don't have one yet,
inferring from `device_kind` (DS18B20 / PT100 / PT1000 → Temperature, inferring from `device_kind` (DS18B20 / PT100 / PT1000 → Temperature,
@@ -78,7 +78,7 @@ def _backfill_sensor_types(env):
def _seed_entech_tanks_and_sensors(env): def _seed_entech_tanks_and_sensors(env):
"""Sub 7 seed 25 tanks (5 small + 20 big; 10 big inactive) with """Sub 7 - seed 25 tanks (5 small + 20 big; 10 big inactive) with
one temperature and one pH sensor each. one temperature and one pH sensor each.
Idempotent: tanks keyed by `code` so re-runs skip existing rows. Idempotent: tanks keyed by `code` so re-runs skip existing rows.
@@ -103,7 +103,7 @@ def _seed_entech_tanks_and_sensors(env):
facility = Facility.search([], limit=1) facility = Facility.search([], limit=1)
if not facility: if not facility:
_logger.warning('Sub 7 seed: no fusion.plating.facility found ' _logger.warning('Sub 7 seed: no fusion.plating.facility found - '
'skipping tank seed. Create a facility first.') 'skipping tank seed. Create a facility first.')
return return
@@ -117,7 +117,7 @@ def _seed_entech_tanks_and_sensors(env):
) )
if not temp_param or not ph_param: if not temp_param or not ph_param:
_logger.warning('Sub 7 seed: temperature / pH bath parameters ' _logger.warning('Sub 7 seed: temperature / pH bath parameters '
'not found skipping. Seed bath parameters first.') 'not found - skipping. Seed bath parameters first.')
return return
plan = [] plan = []
@@ -147,7 +147,7 @@ def _seed_entech_tanks_and_sensors(env):
('parameter_id.parameter_type', '=', 'temperature'), ('parameter_id.parameter_type', '=', 'temperature'),
]): ]):
Sensor.create({ Sensor.create({
'name': '%s Temperature' % tank.name, 'name': '%s - Temperature' % tank.name,
'tank_id': tank.id, 'tank_id': tank.id,
'parameter_id': temp_param.id, 'parameter_id': temp_param.id,
'device_kind': 'ds18b20', 'device_kind': 'ds18b20',
@@ -160,7 +160,7 @@ def _seed_entech_tanks_and_sensors(env):
('parameter_id.parameter_type', '=', 'ph'), ('parameter_id.parameter_type', '=', 'ph'),
]): ]):
Sensor.create({ Sensor.create({
'name': '%s pH' % tank.name, 'name': '%s - pH' % tank.name,
'tank_id': tank.id, 'tank_id': tank.id,
'parameter_id': ph_param.id, 'parameter_id': ph_param.id,
'device_kind': 'ph', 'device_kind': 'ph',

View File

@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc. # Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
"""Sensor dashboard a named group of related sensors. """Sensor dashboard - a named group of related sensors.
Use case: a shop wants one "ENP Line Tanks 1-4" view that aggregates Use case: a shop wants one "ENP Line - Tanks 1-4" view that aggregates
temperature, pH, and level probes from four bath tanks into a single temperature, pH, and level probes from four bath tanks into a single
trending chart + alert count. A dashboard is just a logical grouping; trending chart + alert count. A dashboard is just a logical grouping;
actual rendering happens in the UI. actual rendering happens in the UI.
@@ -14,7 +14,7 @@ from odoo import api, fields, models
class FpSensorDashboard(models.Model): class FpSensorDashboard(models.Model):
_name = 'fp.sensor.dashboard' _name = 'fp.sensor.dashboard'
_description = 'Fusion Plating Sensor Dashboard' _description = 'Fusion Plating - Sensor Dashboard'
_inherit = ['mail.thread'] _inherit = ['mail.thread']
_order = 'name' _order = 'name'
@@ -52,7 +52,7 @@ class FpSensorDashboard(models.Model):
self.ensure_one() self.ensure_one()
return { return {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'name': f'Sensors {self.name}', 'name': f'Sensors - {self.name}',
'res_model': 'fp.tank.sensor', 'res_model': 'fp.tank.sensor',
'view_mode': 'list,form', 'view_mode': 'list,form',
'domain': [('id', 'in', self.sensor_ids.ids)], 'domain': [('id', 'in', self.sensor_ids.ids)],
@@ -63,7 +63,7 @@ class FpSensorDashboard(models.Model):
self.ensure_one() self.ensure_one()
return { return {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'name': f'Readings {self.name}', 'name': f'Readings - {self.name}',
'res_model': 'fp.tank.reading', 'res_model': 'fp.tank.reading',
'view_mode': 'graph,list,form', 'view_mode': 'graph,list,form',
'domain': [('sensor_id', 'in', self.sensor_ids.ids)], 'domain': [('sensor_id', 'in', self.sensor_ids.ids)],

View File

@@ -6,7 +6,7 @@
A richer taxonomy than the `device_kind` Selection on fp.tank.sensor. A richer taxonomy than the `device_kind` Selection on fp.tank.sensor.
Types describe WHAT the sensor measures (temperature, pH, conductivity, Types describe WHAT the sensor measures (temperature, pH, conductivity,
level, etc.) plus the data type of its readings. Same type can back level, etc.) plus the data type of its readings. Same type can back
multiple hardware models e.g. "Temperature" covers DS18B20, PT100, multiple hardware models - e.g. "Temperature" covers DS18B20, PT100,
PT1000, thermocouple. PT1000, thermocouple.
Seeded defaults ship with the module (see data/fp_sensor_type_data.xml) Seeded defaults ship with the module (see data/fp_sensor_type_data.xml)
@@ -19,7 +19,7 @@ from odoo import fields, models
class FpSensorType(models.Model): class FpSensorType(models.Model):
_name = 'fp.sensor.type' _name = 'fp.sensor.type'
_description = 'Fusion Plating Sensor Type' _description = 'Fusion Plating - Sensor Type'
_order = 'sequence, name' _order = 'sequence, name'
name = fields.Char(string='Name', required=True, translate=True) name = fields.Char(string='Name', required=True, translate=True)
@@ -74,7 +74,7 @@ class FpSensorType(models.Model):
self.ensure_one() self.ensure_one()
return { return {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'name': f'Sensors {self.name}', 'name': f'Sensors - {self.name}',
'res_model': 'fp.tank.sensor', 'res_model': 'fp.tank.sensor',
'view_mode': 'list,form', 'view_mode': 'list,form',
'domain': [('sensor_type_id', '=', self.id)], 'domain': [('sensor_type_id', '=', self.id)],

View File

@@ -5,7 +5,7 @@
"""Time-series of sensor readings. """Time-series of sensor readings.
Every POST to /fp/iot/ingest (or every broadcast from the iot proxy) Every POST to /fp/iot/ingest (or every broadcast from the iot proxy)
lands as a new row here. Kept intentionally append-only we never lands as a new row here. Kept intentionally append-only - we never
update or delete readings, which makes this the compliance log for update or delete readings, which makes this the compliance log for
bath-temperature history. bath-temperature history.
@@ -13,7 +13,7 @@ Auto-creates a fusion.plating.quality.hold when a reading falls
outside the sensor's alert range AND the sensor has outside the sensor's alert range AND the sensor has
`alert_on_out_of_spec` enabled. The hold is created once per `alert_on_out_of_spec` enabled. The hold is created once per
excursion (we don't spam a new hold for every reading during a excursion (we don't spam a new hold for every reading during a
sustained excursion) tracked via the sensor's most-recent sustained excursion) - tracked via the sensor's most-recent
`last_reading_in_spec` flag. `last_reading_in_spec` flag.
""" """
@@ -26,7 +26,7 @@ _logger = logging.getLogger(__name__)
class FpTankReading(models.Model): class FpTankReading(models.Model):
_name = 'fp.tank.reading' _name = 'fp.tank.reading'
_description = 'Fusion Plating Tank Sensor Reading' _description = 'Fusion Plating - Tank Sensor Reading'
_order = 'reading_at desc, id desc' _order = 'reading_at desc, id desc'
_rec_name = 'display_name' _rec_name = 'display_name'
@@ -34,7 +34,7 @@ class FpTankReading(models.Model):
'fp.tank.sensor', string='Sensor', required=True, 'fp.tank.sensor', string='Sensor', required=True,
ondelete='cascade', index=True, ondelete='cascade', index=True,
) )
# Denormalised for fast list views + kpi queries auto-filled at # Denormalised for fast list views + kpi queries - auto-filled at
# create time from sensor_id. Indexed for historical trending. # create time from sensor_id. Indexed for historical trending.
tank_id = fields.Many2one( tank_id = fields.Many2one(
'fusion.plating.tank', string='Tank', 'fusion.plating.tank', string='Tank',
@@ -56,7 +56,7 @@ class FpTankReading(models.Model):
value = fields.Float( value = fields.Float(
string='Value (raw)', required=True, digits=(12, 4), string='Value (raw)', required=True, digits=(12, 4),
help='Stored reading in the sensor\'s canonical unit (for ' help='Stored reading in the sensor\'s canonical unit (for '
'temperature sensors this is always °C the DS18B20 and ' 'temperature sensors this is always °C - the DS18B20 and '
'every other temperature chip reports in Celsius natively; ' 'every other temperature chip reports in Celsius natively; '
'keeping storage canonical lets us switch display units ' 'keeping storage canonical lets us switch display units '
'per-company without re-migrating history).', 'per-company without re-migrating history).',
@@ -66,7 +66,7 @@ class FpTankReading(models.Model):
) )
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Display-aware fields converted to the company's preferred unit # Display-aware fields - converted to the company's preferred unit
# (res.company.x_fc_default_temp_uom = 'F' or 'C'). Only the list # (res.company.x_fc_default_temp_uom = 'F' or 'C'). Only the list
# and form views should show these; internal spec comparisons use # and form views should show these; internal spec comparisons use
# `value` so thresholds stay consistent across regions. # `value` so thresholds stay consistent across regions.
@@ -84,7 +84,7 @@ class FpTankReading(models.Model):
@api.depends('value', 'parameter_id', 'parameter_id.parameter_type', @api.depends('value', 'parameter_id', 'parameter_id.parameter_type',
'parameter_id.uom') 'parameter_id.uom')
def _compute_display(self): def _compute_display(self):
# Read once per compute call env.company rarely changes mid-batch. # Read once per compute call - env.company rarely changes mid-batch.
pref = self.env.company.x_fc_default_temp_uom or 'C' pref = self.env.company.x_fc_default_temp_uom or 'C'
for r in self: for r in self:
ptype = (r.parameter_id.parameter_type or '').lower() ptype = (r.parameter_id.parameter_type or '').lower()
@@ -96,7 +96,7 @@ class FpTankReading(models.Model):
r.display_unit = r.parameter_id.uom_display or '' r.display_unit = r.parameter_id.uom_display or ''
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Deviation from setpoint signed Δ from the sensor's effective target # Deviation from setpoint - signed Δ from the sensor's effective target
# in the company-preferred unit. Zero if no setpoint defined. # in the company-preferred unit. Zero if no setpoint defined.
# ------------------------------------------------------------------ # ------------------------------------------------------------------
deviation_from_target = fields.Float( deviation_from_target = fields.Float(
@@ -184,10 +184,10 @@ class FpTankReading(models.Model):
for r in self: for r in self:
sensor = r.sensor_id.name or 'sensor' sensor = r.sensor_id.name or 'sensor'
at = fields.Datetime.to_string(r.reading_at) if r.reading_at else '' at = fields.Datetime.to_string(r.reading_at) if r.reading_at else ''
r.display_name = f'{sensor} {r.display_value:.2f} {r.display_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 # Create hook - evaluate against spec + raise a quality hold if we
# just crossed INTO an out-of-spec state. # just crossed INTO an out-of-spec state.
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@api.model_create_multi @api.model_create_multi
@@ -197,7 +197,7 @@ class FpTankReading(models.Model):
try: try:
rec._evaluate_spec() rec._evaluate_spec()
except Exception: except Exception:
# Never let alert-logic break the ingest path the # Never let alert-logic break the ingest path - the
# reading itself is what matters for compliance. Log # reading itself is what matters for compliance. Log
# and carry on. # and carry on.
_logger.exception( _logger.exception(
@@ -256,7 +256,7 @@ class FpTankReading(models.Model):
'hold_reason': 'out_of_spec', 'hold_reason': 'out_of_spec',
'description': description, 'description': description,
'qty_on_hold': 1, 'qty_on_hold': 1,
# state defaults to 'on_hold' leave it # state defaults to 'on_hold' - leave it
} }
# Attach facility + work-centre context if the tank has them, # Attach facility + work-centre context if the tank has them,
# so the hold is actionable from the shop floor (operator can # so the hold is actionable from the shop floor (operator can

View File

@@ -9,7 +9,7 @@ device registered via the iot_drivers proxy) is mapped to exactly one
tank or bath and measures ONE bath parameter (temperature, pH, tank or bath and measures ONE bath parameter (temperature, pH,
conductivity, etc.). conductivity, etc.).
The same tank can carry multiple sensors e.g. a temp probe and a pH The same tank can carry multiple sensors - e.g. a temp probe and a pH
probe. Each is its own fp.tank.sensor row. probe. Each is its own fp.tank.sensor row.
""" """
@@ -18,17 +18,17 @@ from odoo import api, fields, models
class FpTankSensor(models.Model): class FpTankSensor(models.Model):
_name = 'fp.tank.sensor' _name = 'fp.tank.sensor'
_description = 'Fusion Plating Tank Sensor' _description = 'Fusion Plating - Tank Sensor'
_order = 'tank_id, parameter_id' _order = 'tank_id, parameter_id'
name = fields.Char( name = fields.Char(
string='Sensor Name', required=True, string='Sensor Name', required=True,
help='Human label (e.g. "Tank 3 ENP temp").', help='Human label (e.g. "Tank 3 - ENP temp").',
) )
uuid = fields.Char( uuid = fields.Char(
string='UUID', string='UUID',
copy=False, readonly=True, index=True, copy=False, readonly=True, index=True,
help='Stable logical identifier survives hardware swaps. If a ' help='Stable logical identifier - survives hardware swaps. If a '
'probe dies and gets replaced, keep the UUID, change the ' 'probe dies and gets replaced, keep the UUID, change the '
'device_serial. Every measurement tied to this UUID remains ' 'device_serial. Every measurement tied to this UUID remains '
'part of the same logical history.', 'part of the same logical history.',
@@ -36,7 +36,7 @@ class FpTankSensor(models.Model):
sensor_type_id = fields.Many2one( sensor_type_id = fields.Many2one(
'fp.sensor.type', 'fp.sensor.type',
string='Sensor Type', string='Sensor Type',
help='Taxonomy temperature, pH, conductivity, etc. ' help='Taxonomy - temperature, pH, conductivity, etc. '
'Independent from hardware (a "temperature" sensor could be ' 'Independent from hardware (a "temperature" sensor could be '
'a DS18B20, PT100, or thermocouple; device_kind captures ' 'a DS18B20, PT100, or thermocouple; device_kind captures '
'the hardware, sensor_type_id captures the role).', 'the hardware, sensor_type_id captures the role).',
@@ -44,7 +44,7 @@ class FpTankSensor(models.Model):
active = fields.Boolean(default=True) active = fields.Boolean(default=True)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Physical device either an Odoo iot.device (proxied through the Pi) # Physical device - either an Odoo iot.device (proxied through the Pi)
# OR a direct-ingest sensor (skipping the proxy, posting straight to # OR a direct-ingest sensor (skipping the proxy, posting straight to
# /fp/iot/ingest with the shared secret + device_serial). # /fp/iot/ingest with the shared secret + device_serial).
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -73,7 +73,7 @@ class FpTankSensor(models.Model):
) )
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Where this sensor lives can be ANY of tank / work_center / # Where this sensor lives - can be ANY of tank / work_center /
# facility / named-location. Keep tank_id as the primary (most # facility / named-location. Keep tank_id as the primary (most
# sensors are bath-mounted) but allow the other three as # sensors are bath-mounted) but allow the other three as
# alternatives so we can mount probes on ovens, ambient air, # alternatives so we can mount probes on ovens, ambient air,
@@ -84,22 +84,22 @@ class FpTankSensor(models.Model):
) )
bath_id = fields.Many2one( bath_id = fields.Many2one(
'fusion.plating.bath', string='Bath', 'fusion.plating.bath', string='Bath',
help='Optional if the sensor is bound to a specific bath ' help='Optional - if the sensor is bound to a specific bath '
'chemistry rather than a physical tank.', 'chemistry rather than a physical tank.',
) )
work_center_id = fields.Many2one( work_center_id = fields.Many2one(
'fusion.plating.work.center', string='Work Centre', 'fusion.plating.work.center', string='Work Centre',
help='Alternative to tank_id use when the sensor is attached ' help='Alternative to tank_id - use when the sensor is attached '
'to a station, oven, or line rather than a bath tank.', 'to a station, oven, or line rather than a bath tank.',
) )
facility_id = fields.Many2one( facility_id = fields.Many2one(
'fusion.plating.facility', string='Facility', 'fusion.plating.facility', string='Facility',
help='Alternative to tank_id / work_center_id use for ' help='Alternative to tank_id / work_center_id - use for '
'facility-wide sensors (ambient, HVAC, perimeter).', 'facility-wide sensors (ambient, HVAC, perimeter).',
) )
location_name = fields.Char( location_name = fields.Char(
string='Location (free-text)', string='Location (free-text)',
help='Free-text override when none of the above fit e.g. ' help='Free-text override when none of the above fit - e.g. '
'"North bay wall", "Effluent pipe exit", "Rinse tank #2 ' '"North bay wall", "Effluent pipe exit", "Rinse tank #2 '
'roof". Shown alongside the structured location in views.', 'roof". Shown alongside the structured location in views.',
) )
@@ -113,7 +113,7 @@ class FpTankSensor(models.Model):
effective_location = fields.Char( effective_location = fields.Char(
string='Location', string='Location',
compute='_compute_effective_location', store=True, compute='_compute_effective_location', store=True,
help='Display string for the sensor location prefers tank, ' help='Display string for the sensor location - prefers tank, '
'then work_center, then facility, then free-text.', 'then work_center, then facility, then free-text.',
) )
@@ -133,7 +133,7 @@ class FpTankSensor(models.Model):
rec.effective_location = '' rec.effective_location = ''
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Target / alerting behaviour three concepts: # Target / alerting behaviour - three concepts:
# setpoint → what we CONTROL TOWARD (dashboards, PID, trend baseline) # setpoint → what we CONTROL TOWARD (dashboards, PID, trend baseline)
# alert min → lower alarm boundary (quality hold fires below) # alert min → lower alarm boundary (quality hold fires below)
# alert max → upper alarm boundary (quality hold fires above) # alert max → upper alarm boundary (quality hold fires above)
@@ -164,7 +164,7 @@ class FpTankSensor(models.Model):
) )
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Sub 7 per-sensor polling interval # Sub 7 - per-sensor polling interval
# #
# Blank on the sensor = inherit res.company default. The effective # Blank on the sensor = inherit res.company default. The effective
# value gates the ingest endpoint so too-frequent readings are # value gates the ingest endpoint so too-frequent readings are
@@ -175,7 +175,7 @@ class FpTankSensor(models.Model):
help='How often a reading from this sensor should be stored. ' help='How often a reading from this sensor should be stored. '
'Leave blank to inherit the company-wide default. Readings ' 'Leave blank to inherit the company-wide default. Readings '
'that arrive inside this interval are dropped by the ' 'that arrive inside this interval are dropped by the '
'ingest endpoint the database stays clean even if the ' 'ingest endpoint - the database stays clean even if the '
'Pi agent polls more often.', 'Pi agent polls more often.',
) )
poll_interval_display = fields.Char( poll_interval_display = fields.Char(
@@ -211,7 +211,7 @@ class FpTankSensor(models.Model):
return self.env.company.x_fc_default_poll_interval_minutes or 30 return self.env.company.x_fc_default_poll_interval_minutes or 30
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Effective target resolves override → parameter default → 0 # Effective target - resolves override → parameter default → 0
# ------------------------------------------------------------------ # ------------------------------------------------------------------
effective_target = fields.Float( effective_target = fields.Float(
string='Effective Setpoint', string='Effective Setpoint',
@@ -253,7 +253,7 @@ class FpTankSensor(models.Model):
string='In Spec?', readonly=True, string='In Spec?', readonly=True,
help='Computed from the last reading vs alert_min/alert_max.', help='Computed from the last reading vs alert_min/alert_max.',
) )
# Display-aware version of last_reading_value converted to company # Display-aware version of last_reading_value - converted to company
# preferred unit (res.company.x_fc_default_temp_uom). # preferred unit (res.company.x_fc_default_temp_uom).
last_reading_display = fields.Float( last_reading_display = fields.Float(
string='Latest Value', string='Latest Value',
@@ -308,7 +308,7 @@ class FpTankSensor(models.Model):
rec.reading_count = len(rec.reading_ids) rec.reading_count = len(rec.reading_ids)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Resolve effective alert range override wins, else bath.parameter # Resolve effective alert range - override wins, else bath.parameter
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _get_alert_range(self): def _get_alert_range(self):
"""Return (min, max) floats. Zero means 'no bound'.""" """Return (min, max) floats. Zero means 'no bound'."""
@@ -324,7 +324,7 @@ class FpTankSensor(models.Model):
def _get_setpoint(self): def _get_setpoint(self):
"""Canonical (raw) setpoint used for deviation calcs. """Canonical (raw) setpoint used for deviation calcs.
Returns 0.0 if no setpoint is set at either level callers should Returns 0.0 if no setpoint is set at either level - callers should
treat 0 as "no target defined, skip deviation display". treat 0 as "no target defined, skip deviation display".
""" """
self.ensure_one() self.ensure_one()
@@ -336,7 +336,7 @@ class FpTankSensor(models.Model):
self.ensure_one() self.ensure_one()
return { return {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'name': f'Readings {self.name}', 'name': f'Readings - {self.name}',
'res_model': 'fp.tank.reading', 'res_model': 'fp.tank.reading',
'view_mode': 'list,form,graph', 'view_mode': 'list,form,graph',
'domain': [('sensor_id', '=', self.id)], 'domain': [('sensor_id', '=', self.id)],

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family. # Part of the Fusion Plating product family.
# #
# Sub 7 company-wide default for the IoT sensor polling interval. # Sub 7 - company-wide default for the IoT sensor polling interval.
from odoo import fields, models from odoo import fields, models

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family. # Part of the Fusion Plating product family.
# #
# Sub 7 Expose the IoT polling default on the Fusion Plating # Sub 7 - Expose the IoT polling default on the Fusion Plating
# Settings page so admins manage it alongside other plating settings. # Settings page so admins manage it alongside other plating settings.
from odoo import fields, models from odoo import fields, models

View File

@@ -59,7 +59,7 @@
invisible="last_reading_in_spec or not last_reading_at" invisible="last_reading_in_spec or not last_reading_at"
bg_color="text-bg-danger"/> bg_color="text-bg-danger"/>
<div class="oe_title"> <div class="oe_title">
<h1><field name="name" placeholder="e.g. Tank 3 ENP temp"/></h1> <h1><field name="name" placeholder="e.g. Tank 3 - ENP temp"/></h1>
</div> </div>
<group> <group>
<group string="Identity"> <group string="Identity">
@@ -73,10 +73,10 @@
<field name="device_serial" placeholder="28-abc123def456"/> <field name="device_serial" placeholder="28-abc123def456"/>
<field name="iot_device_id" <field name="iot_device_id"
options="{'no_create': True}" options="{'no_create': True}"
help="Optional the iot.device auto-registered by the Pi proxy."/> help="Optional - the iot.device auto-registered by the Pi proxy."/>
</group> </group>
</group> </group>
<group string="Location fill in ONE (first one set wins for display)"> <group string="Location - fill in ONE (first one set wins for display)">
<group> <group>
<field name="tank_id" options="{'no_create': True}"/> <field name="tank_id" options="{'no_create': True}"/>
<field name="bath_id" options="{'no_create': True}"/> <field name="bath_id" options="{'no_create': True}"/>
@@ -91,7 +91,7 @@
<group string="Setpoint &amp; Alerting"> <group string="Setpoint &amp; Alerting">
<group string="Target (setpoint)"> <group string="Target (setpoint)">
<field name="target_value_override" <field name="target_value_override"
help="Leave 0 to inherit from bath parameter's Default Setpoint. This is the IDEAL operating value not an alarm threshold."/> help="Leave 0 to inherit from bath parameter's Default Setpoint. This is the IDEAL operating value - not an alarm threshold."/>
<field name="effective_target" readonly="1"/> <field name="effective_target" readonly="1"/>
<field name="effective_target_unit" readonly="1"/> <field name="effective_target_unit" readonly="1"/>
</group> </group>

View File

@@ -3,7 +3,7 @@
Copyright 2026 Nexa Systems Inc. Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0) License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family. Part of the Fusion Plating product family.
Sub 7 IoT default polling interval setting. Sub 7 - IoT default polling interval setting.
--> -->
<odoo> <odoo>