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.
{
'name': 'Fusion Plating IoT Integration',
'name': 'Fusion Plating - IoT Integration',
'version': '19.0.2.0.0',
'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 '
'on out-of-spec.',
'description': """
Fusion Plating IoT Integration
Fusion Plating - IoT Integration
================================
Bridges the generic `iot` module (IoT Box + device management) to
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``).
* ``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
falls outside the tank/bath's target range (per
``fusion.plating.bath.parameter`` spec).

View File

@@ -40,7 +40,7 @@ _logger = logging.getLogger(__name__)
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
if not raw:
return OdooDatetime.now()
@@ -62,20 +62,20 @@ class FpIotIngestController(http.Controller):
methods=['POST'], csrf=False, save_session=False)
def ingest(self, **_kwargs):
"""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
# in Settings → Technical → System Parameters.
expected = request.env['ir.config_parameter'].sudo().get_param(
'fusion_plating_iot.ingest_token', ''
)
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(
json.dumps({'ok': False, 'error': 'token_not_configured'}),
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.
header_token = request.httprequest.headers.get('X-FP-IOT-Token', '')
raw = request.httprequest.get_data(as_text=True) or ''
@@ -124,10 +124,10 @@ class FpIotIngestController(http.Controller):
except (TypeError, ValueError):
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
# 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.
if not sensor.active:
skipped_interval += 1

View File

@@ -3,7 +3,7 @@
Copyright 2026 Nexa Systems Inc.
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.
-->
<odoo noupdate="1">

View File

@@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# 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
so re-runs don't overwrite user-edited values.
What it does:
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).
2. Sets a default `sensor_type_id` on sensors that don't have one yet,
inferring from `device_kind` (DS18B20 / PT100 / PT1000 → Temperature,
@@ -78,7 +78,7 @@ def _backfill_sensor_types(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.
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)
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.')
return
@@ -117,7 +117,7 @@ def _seed_entech_tanks_and_sensors(env):
)
if not temp_param or not ph_param:
_logger.warning('Sub 7 seed: temperature / pH bath parameters '
'not found skipping. Seed bath parameters first.')
'not found - skipping. Seed bath parameters first.')
return
plan = []
@@ -147,7 +147,7 @@ def _seed_entech_tanks_and_sensors(env):
('parameter_id.parameter_type', '=', 'temperature'),
]):
Sensor.create({
'name': '%s Temperature' % tank.name,
'name': '%s - Temperature' % tank.name,
'tank_id': tank.id,
'parameter_id': temp_param.id,
'device_kind': 'ds18b20',
@@ -160,7 +160,7 @@ def _seed_entech_tanks_and_sensors(env):
('parameter_id.parameter_type', '=', 'ph'),
]):
Sensor.create({
'name': '%s pH' % tank.name,
'name': '%s - pH' % tank.name,
'tank_id': tank.id,
'parameter_id': ph_param.id,
'device_kind': 'ph',

View File

@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# 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
trending chart + alert count. A dashboard is just a logical grouping;
actual rendering happens in the UI.
@@ -14,7 +14,7 @@ from odoo import api, fields, models
class FpSensorDashboard(models.Model):
_name = 'fp.sensor.dashboard'
_description = 'Fusion Plating Sensor Dashboard'
_description = 'Fusion Plating - Sensor Dashboard'
_inherit = ['mail.thread']
_order = 'name'
@@ -52,7 +52,7 @@ class FpSensorDashboard(models.Model):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': f'Sensors {self.name}',
'name': f'Sensors - {self.name}',
'res_model': 'fp.tank.sensor',
'view_mode': 'list,form',
'domain': [('id', 'in', self.sensor_ids.ids)],
@@ -63,7 +63,7 @@ class FpSensorDashboard(models.Model):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': f'Readings {self.name}',
'name': f'Readings - {self.name}',
'res_model': 'fp.tank.reading',
'view_mode': 'graph,list,form',
'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.
Types describe WHAT the sensor measures (temperature, pH, conductivity,
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.
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):
_name = 'fp.sensor.type'
_description = 'Fusion Plating Sensor Type'
_description = 'Fusion Plating - Sensor Type'
_order = 'sequence, name'
name = fields.Char(string='Name', required=True, translate=True)
@@ -74,7 +74,7 @@ class FpSensorType(models.Model):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': f'Sensors {self.name}',
'name': f'Sensors - {self.name}',
'res_model': 'fp.tank.sensor',
'view_mode': 'list,form',
'domain': [('sensor_type_id', '=', self.id)],

View File

@@ -5,7 +5,7 @@
"""Time-series of sensor readings.
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
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
`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
sustained excursion) tracked via the sensor's most-recent
sustained excursion) - tracked via the sensor's most-recent
`last_reading_in_spec` flag.
"""
@@ -26,7 +26,7 @@ _logger = logging.getLogger(__name__)
class FpTankReading(models.Model):
_name = 'fp.tank.reading'
_description = 'Fusion Plating Tank Sensor Reading'
_description = 'Fusion Plating - Tank Sensor Reading'
_order = 'reading_at desc, id desc'
_rec_name = 'display_name'
@@ -34,7 +34,7 @@ class FpTankReading(models.Model):
'fp.tank.sensor', string='Sensor', required=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.
tank_id = fields.Many2one(
'fusion.plating.tank', string='Tank',
@@ -56,7 +56,7 @@ class FpTankReading(models.Model):
value = fields.Float(
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 '
'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).',
@@ -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
# and form views should show these; internal spec comparisons use
# `value` so thresholds stay consistent across regions.
@@ -84,7 +84,7 @@ class FpTankReading(models.Model):
@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.
# 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()
@@ -96,7 +96,7 @@ class FpTankReading(models.Model):
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.
# ------------------------------------------------------------------
deviation_from_target = fields.Float(
@@ -184,10 +184,10 @@ class FpTankReading(models.Model):
for r in self:
sensor = r.sensor_id.name or 'sensor'
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.
# ------------------------------------------------------------------
@api.model_create_multi
@@ -197,7 +197,7 @@ class FpTankReading(models.Model):
try:
rec._evaluate_spec()
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
# and carry on.
_logger.exception(
@@ -256,7 +256,7 @@ class FpTankReading(models.Model):
'hold_reason': 'out_of_spec',
'description': description,
'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,
# 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,
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.
"""
@@ -18,17 +18,17 @@ from odoo import api, fields, models
class FpTankSensor(models.Model):
_name = 'fp.tank.sensor'
_description = 'Fusion Plating Tank Sensor'
_description = 'Fusion Plating - Tank Sensor'
_order = 'tank_id, parameter_id'
name = fields.Char(
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(
string='UUID',
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 '
'device_serial. Every measurement tied to this UUID remains '
'part of the same logical history.',
@@ -36,7 +36,7 @@ class FpTankSensor(models.Model):
sensor_type_id = fields.Many2one(
'fp.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 '
'a DS18B20, PT100, or thermocouple; device_kind captures '
'the hardware, sensor_type_id captures the role).',
@@ -44,7 +44,7 @@ class FpTankSensor(models.Model):
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
# /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
# sensors are bath-mounted) but allow the other three as
# alternatives so we can mount probes on ovens, ambient air,
@@ -84,22 +84,22 @@ class FpTankSensor(models.Model):
)
bath_id = fields.Many2one(
'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.',
)
work_center_id = fields.Many2one(
'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.',
)
facility_id = fields.Many2one(
'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).',
)
location_name = fields.Char(
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 '
'roof". Shown alongside the structured location in views.',
)
@@ -113,7 +113,7 @@ class FpTankSensor(models.Model):
effective_location = fields.Char(
string='Location',
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.',
)
@@ -133,7 +133,7 @@ class FpTankSensor(models.Model):
rec.effective_location = ''
# ------------------------------------------------------------------
# Target / alerting behaviour three concepts:
# 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)
@@ -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
# 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. '
'Leave blank to inherit the company-wide default. Readings '
'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.',
)
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
# ------------------------------------------------------------------
# Effective target resolves override → parameter default → 0
# Effective target - resolves override → parameter default → 0
# ------------------------------------------------------------------
effective_target = fields.Float(
string='Effective Setpoint',
@@ -253,7 +253,7 @@ class FpTankSensor(models.Model):
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
# 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',
@@ -308,7 +308,7 @@ class FpTankSensor(models.Model):
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):
"""Return (min, max) floats. Zero means 'no bound'."""
@@ -324,7 +324,7 @@ class FpTankSensor(models.Model):
def _get_setpoint(self):
"""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".
"""
self.ensure_one()
@@ -336,7 +336,7 @@ class FpTankSensor(models.Model):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': f'Readings {self.name}',
'name': f'Readings - {self.name}',
'res_model': 'fp.tank.reading',
'view_mode': 'list,form,graph',
'domain': [('sensor_id', '=', self.id)],

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# 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

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# 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.
from odoo import fields, models

View File

@@ -59,7 +59,7 @@
invisible="last_reading_in_spec or not last_reading_at"
bg_color="text-bg-danger"/>
<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>
<group>
<group string="Identity">
@@ -73,10 +73,10 @@
<field name="device_serial" placeholder="28-abc123def456"/>
<field name="iot_device_id"
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 string="Location fill in ONE (first one set wins for display)">
<group string="Location - fill in ONE (first one set wins for display)">
<group>
<field name="tank_id" options="{'no_create': True}"/>
<field name="bath_id" options="{'no_create': True}"/>
@@ -91,7 +91,7 @@
<group string="Setpoint &amp; Alerting">
<group string="Target (setpoint)">
<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_unit" readonly="1"/>
</group>

View File

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