diff --git a/fusion_iot/fusion_plating_iot/__init__.py b/fusion_iot/fusion_plating_iot/__init__.py index 51d8ddfe..01c43ce4 100644 --- a/fusion_iot/fusion_plating_iot/__init__.py +++ b/fusion_iot/fusion_plating_iot/__init__.py @@ -5,3 +5,4 @@ from . import models from . import controllers +from .hooks import post_init_hook diff --git a/fusion_iot/fusion_plating_iot/__manifest__.py b/fusion_iot/fusion_plating_iot/__manifest__.py index a1bb846a..93833cf6 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.3.0', + 'version': '19.0.1.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Wire physical tank sensors to Fusion Plating — live ' 'temperature / chemistry readings with auto quality holds ' @@ -45,11 +45,15 @@ Part of the Fusion Plating product family by Nexa Systems Inc. 'data': [ 'security/ir.model.access.csv', 'data/ir_config_parameter_data.xml', + 'data/fp_sensor_type_data.xml', + 'views/fp_sensor_type_views.xml', + 'views/fp_sensor_dashboard_views.xml', 'views/fp_tank_sensor_views.xml', 'views/fp_tank_reading_views.xml', 'views/fusion_plating_tank_views.xml', 'views/fp_iot_menu.xml', ], + 'post_init_hook': 'post_init_hook', 'installable': True, 'application': False, 'auto_install': False, diff --git a/fusion_iot/fusion_plating_iot/data/fp_sensor_type_data.xml b/fusion_iot/fusion_plating_iot/data/fp_sensor_type_data.xml new file mode 100644 index 00000000..8e22a77e --- /dev/null +++ b/fusion_iot/fusion_plating_iot/data/fp_sensor_type_data.xml @@ -0,0 +1,82 @@ + + + + + + Temperature + temperature + 10 + number + °C + fa-thermometer-half + + + + pH + ph + 20 + number + pH + fa-flask + + + + Conductivity + conductivity + 30 + number + µS/cm + fa-bolt + + + + Tank Level + level + 40 + number + cm + fa-tint + + + + Pressure + pressure + 50 + number + kPa + fa-tachometer + + + + Flow Rate + flow + 60 + number + L/min + fa-cogs + + + + Concentration + concentration + 70 + number + g/L + fa-vial + + + + Limit / Float Switch + switch + 100 + boolean + fa-toggle-on + + + diff --git a/fusion_iot/fusion_plating_iot/hooks.py b/fusion_iot/fusion_plating_iot/hooks.py new file mode 100644 index 00000000..d3637d2a --- /dev/null +++ b/fusion_iot/fusion_plating_iot/hooks.py @@ -0,0 +1,76 @@ +# -*- 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. + +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 + 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, + pH → pH probe, etc.). +""" +import logging +import uuid as _uuid + +_logger = logging.getLogger(__name__) + + +def post_init_hook(env): + _backfill_uuids(env) + _backfill_sensor_types(env) + + +def _backfill_uuids(env): + Sensor = env['fp.tank.sensor'] + missing = Sensor.search([('uuid', '=', False)]) + if not missing: + return + for s in missing: + s.sudo().write({'uuid': _uuid.uuid4().hex}) + _logger.info('fp.tank.sensor: populated UUID on %d existing records', + len(missing)) + + +def _backfill_sensor_types(env): + Sensor = env['fp.tank.sensor'] + Type = env['fp.sensor.type'] + + # Map device_kind → sensor-type code. Falls back to 'temperature' for + # the rare case someone set device_kind='other' on a probe that IS + # temperature (common on the pilot). + kind_to_code = { + 'ds18b20': 'temperature', + 'pt100': 'temperature', + 'pt1000': 'temperature', + 'ph': 'ph', + 'conductivity': 'conductivity', + 'level': 'level', + } + + missing = Sensor.search([('sensor_type_id', '=', False)]) + if not missing: + return + + # Resolve the types once up front + type_cache = {} + for code in set(kind_to_code.values()): + t = Type.search([('code', '=', code)], limit=1) + if t: + type_cache[code] = t.id + + updated = 0 + for s in missing: + code = kind_to_code.get(s.device_kind) + # Unmapped (device_kind='other') → try temperature as the most + # common fallback. Admin can correct in the UI. + type_id = type_cache.get(code) or type_cache.get('temperature') + if type_id: + s.sudo().write({'sensor_type_id': type_id}) + updated += 1 + _logger.info('fp.tank.sensor: set default sensor_type_id on %d records', + updated) diff --git a/fusion_iot/fusion_plating_iot/models/__init__.py b/fusion_iot/fusion_plating_iot/models/__init__.py index b5c98231..79ccae27 100644 --- a/fusion_iot/fusion_plating_iot/models/__init__.py +++ b/fusion_iot/fusion_plating_iot/models/__init__.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from . import fp_sensor_type +from . import fp_sensor_dashboard from . import fp_tank_sensor from . import fp_tank_reading from . import fusion_plating_tank diff --git a/fusion_iot/fusion_plating_iot/models/fp_sensor_dashboard.py b/fusion_iot/fusion_plating_iot/models/fp_sensor_dashboard.py new file mode 100644 index 00000000..1efbf364 --- /dev/null +++ b/fusion_iot/fusion_plating_iot/models/fp_sensor_dashboard.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +"""Sensor dashboard — a named group of related sensors. + +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. +""" + +from odoo import api, fields, models + + +class FpSensorDashboard(models.Model): + _name = 'fp.sensor.dashboard' + _description = 'Fusion Plating — Sensor Dashboard' + _inherit = ['mail.thread'] + _order = 'name' + + name = fields.Char(string='Name', required=True, tracking=True) + description = fields.Text(string='Description') + sensor_ids = fields.Many2many( + 'fp.tank.sensor', + 'fp_sensor_dashboard_rel', + 'dashboard_id', 'sensor_id', + string='Sensors', + ) + sensor_count = fields.Integer( + string='Members', compute='_compute_counts', + ) + out_of_spec_count = fields.Integer( + string='Sensors Out of Spec', compute='_compute_counts', + help='How many of this dashboard\'s sensors currently have a ' + 'last-reading that\'s out of spec.', + ) + color = fields.Integer(string='Colour Tag') + active = fields.Boolean(default=True) + company_id = fields.Many2one( + 'res.company', default=lambda self: self.env.company, + ) + + @api.depends('sensor_ids.last_reading_in_spec', 'sensor_ids.last_reading_at') + def _compute_counts(self): + for dash in self: + dash.sensor_count = len(dash.sensor_ids) + dash.out_of_spec_count = len(dash.sensor_ids.filtered( + lambda s: s.last_reading_at and not s.last_reading_in_spec + )) + + def action_view_sensors(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': f'Sensors — {self.name}', + 'res_model': 'fp.tank.sensor', + 'view_mode': 'list,form', + 'domain': [('id', 'in', self.sensor_ids.ids)], + 'target': 'current', + } + + def action_view_recent_readings(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': f'Readings — {self.name}', + 'res_model': 'fp.tank.reading', + 'view_mode': 'graph,list,form', + 'domain': [('sensor_id', 'in', self.sensor_ids.ids)], + 'target': 'current', + } diff --git a/fusion_iot/fusion_plating_iot/models/fp_sensor_type.py b/fusion_iot/fusion_plating_iot/models/fp_sensor_type.py new file mode 100644 index 00000000..2f20db41 --- /dev/null +++ b/fusion_iot/fusion_plating_iot/models/fp_sensor_type.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +"""Sensor-type taxonomy. + +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, +PT1000, thermocouple. + +Seeded defaults ship with the module (see data/fp_sensor_type_data.xml) +so users don't have to set them up from scratch. Admins add more via +Plating → Operations → Sensors → Sensor Types. +""" + +from odoo import fields, models + + +class FpSensorType(models.Model): + _name = 'fp.sensor.type' + _description = 'Fusion Plating — Sensor Type' + _order = 'sequence, name' + + name = fields.Char(string='Name', required=True, translate=True) + sequence = fields.Integer(default=10) + code = fields.Char( + string='Code', required=True, + help='Short machine-friendly identifier, e.g. "temperature", "ph".', + ) + measurement_type = fields.Selection( + [ + ('number', 'Number'), + ('boolean', 'Boolean'), + ('text', 'Text'), + ], + string='Measurement Type', required=True, default='number', + help='Most plating-shop sensors are numeric. Boolean is useful for ' + 'limit switches / float switches; text for things like batch ' + 'lot codes from a barcode reader.', + ) + default_uom = fields.Char( + string='Default Unit', + help='Unit string (°C, pH, µS/cm, etc.) inherited by sensors of ' + 'this type unless they carry a parameter-specific unit.', + ) + icon = fields.Char( + string='Icon', + help='Optional fa-* or oi-* icon name for use in dashboards.', + ) + sensor_ids = fields.One2many( + 'fp.tank.sensor', 'sensor_type_id', + string='Sensors of this type', + ) + sensor_count = fields.Integer( + string='Sensor Count', compute='_compute_sensor_count', + ) + active = fields.Boolean(default=True) + company_id = fields.Many2one( + 'res.company', + default=lambda self: self.env.company, + ) + + _sql_constraints = [ + ('fp_sensor_type_code_uniq', 'unique(code, company_id)', + 'Sensor-type code must be unique per company.'), + ] + + def _compute_sensor_count(self): + for rec in self: + rec.sensor_count = len(rec.sensor_ids) + + def action_view_sensors(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': f'Sensors — {self.name}', + 'res_model': 'fp.tank.sensor', + 'view_mode': 'list,form', + 'domain': [('sensor_type_id', '=', self.id)], + 'context': {'default_sensor_type_id': self.id}, + 'target': 'current', + } 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 c0004daf..ffa9c5e2 100644 --- a/fusion_iot/fusion_plating_iot/models/fp_tank_sensor.py +++ b/fusion_iot/fusion_plating_iot/models/fp_tank_sensor.py @@ -25,6 +25,22 @@ class FpTankSensor(models.Model): string='Sensor Name', required=True, 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 ' + '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.', + ) + sensor_type_id = fields.Many2one( + 'fp.sensor.type', + string='Sensor Type', + 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).', + ) active = fields.Boolean(default=True) # ------------------------------------------------------------------ @@ -57,7 +73,11 @@ class FpTankSensor(models.Model): ) # ------------------------------------------------------------------ - # Where this sensor lives + what it measures + # 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, + # waste-water discharge, etc. without forcing a fake "tank". # ------------------------------------------------------------------ tank_id = fields.Many2one( 'fusion.plating.tank', string='Tank', ondelete='cascade', @@ -67,6 +87,22 @@ class FpTankSensor(models.Model): 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 ' + '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 ' + '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. ' + '"North bay wall", "Effluent pipe exit", "Rinse tank #2 ' + 'roof". Shown alongside the structured location in views.', + ) parameter_id = fields.Many2one( 'fusion.plating.bath.parameter', string='Parameter Measured', required=True, @@ -74,6 +110,27 @@ class FpTankSensor(models.Model): 'etc.). Drives unit labelling + out-of-spec alerting against ' 'the parameter\'s target_min / target_max.', ) + effective_location = fields.Char( + string='Location', + compute='_compute_effective_location', store=True, + help='Display string for the sensor location — prefers tank, ' + 'then work_center, then facility, then free-text.', + ) + + @api.depends('tank_id', 'bath_id', 'work_center_id', 'facility_id', + 'location_name') + def _compute_effective_location(self): + for rec in self: + if rec.tank_id: + rec.effective_location = rec.tank_id.name + elif rec.work_center_id: + rec.effective_location = rec.work_center_id.name + elif rec.facility_id: + rec.effective_location = rec.facility_id.name + elif rec.location_name: + rec.effective_location = rec.location_name + else: + rec.effective_location = '' # ------------------------------------------------------------------ # Target / alerting behaviour — three concepts: @@ -185,8 +242,19 @@ class FpTankSensor(models.Model): ('fp_tank_sensor_serial_uniq', 'unique(device_serial)', 'Each hardware serial can only be mapped to one sensor.'), + ('fp_tank_sensor_uuid_uniq', + 'unique(uuid)', + 'UUID must be unique across all sensors.'), ] + @api.model_create_multi + def create(self, vals_list): + import uuid as _uuid + for vals in vals_list: + if not vals.get('uuid'): + vals['uuid'] = _uuid.uuid4().hex + return super().create(vals_list) + @api.depends('reading_ids') def _compute_reading_count(self): for rec in self: diff --git a/fusion_iot/fusion_plating_iot/security/ir.model.access.csv b/fusion_iot/fusion_plating_iot/security/ir.model.access.csv index a651e152..042b733c 100644 --- a/fusion_iot/fusion_plating_iot/security/ir.model.access.csv +++ b/fusion_iot/fusion_plating_iot/security/ir.model.access.csv @@ -5,3 +5,9 @@ fp_tank_sensor_manager,fp.tank.sensor manager,model_fp_tank_sensor,fusion_platin fp_tank_reading_operator,fp.tank.reading operator,model_fp_tank_reading,fusion_plating.group_fusion_plating_operator,1,0,1,0 fp_tank_reading_supervisor,fp.tank.reading supervisor,model_fp_tank_reading,fusion_plating.group_fusion_plating_supervisor,1,0,1,0 fp_tank_reading_manager,fp.tank.reading manager,model_fp_tank_reading,fusion_plating.group_fusion_plating_manager,1,1,1,1 +fp_sensor_type_operator,fp.sensor.type operator,model_fp_sensor_type,fusion_plating.group_fusion_plating_operator,1,0,0,0 +fp_sensor_type_supervisor,fp.sensor.type supervisor,model_fp_sensor_type,fusion_plating.group_fusion_plating_supervisor,1,0,0,0 +fp_sensor_type_manager,fp.sensor.type manager,model_fp_sensor_type,fusion_plating.group_fusion_plating_manager,1,1,1,1 +fp_sensor_dashboard_operator,fp.sensor.dashboard operator,model_fp_sensor_dashboard,fusion_plating.group_fusion_plating_operator,1,0,0,0 +fp_sensor_dashboard_supervisor,fp.sensor.dashboard supervisor,model_fp_sensor_dashboard,fusion_plating.group_fusion_plating_supervisor,1,1,1,0 +fp_sensor_dashboard_manager,fp.sensor.dashboard manager,model_fp_sensor_dashboard,fusion_plating.group_fusion_plating_manager,1,1,1,1 diff --git a/fusion_iot/fusion_plating_iot/views/fp_iot_menu.xml b/fusion_iot/fusion_plating_iot/views/fp_iot_menu.xml index 3d8cf395..6b52acf2 100644 --- a/fusion_iot/fusion_plating_iot/views/fp_iot_menu.xml +++ b/fusion_iot/fusion_plating_iot/views/fp_iot_menu.xml @@ -3,27 +3,39 @@ Copyright 2026 Nexa Systems Inc. License OPL-1 - Surface IoT sensors + readings under the existing Plating > - Operations menu. Not a top-level app — sensors are an extension - of bath/tank management, not a separate concern. + Single "Sensors" section under Plating > Operations. Consolidates + all sensor management into one place (formerly split across an + obsolete fusion_plating_sensors module). --> - - + + + + diff --git a/fusion_iot/fusion_plating_iot/views/fp_sensor_dashboard_views.xml b/fusion_iot/fusion_plating_iot/views/fp_sensor_dashboard_views.xml new file mode 100644 index 00000000..32585575 --- /dev/null +++ b/fusion_iot/fusion_plating_iot/views/fp_sensor_dashboard_views.xml @@ -0,0 +1,72 @@ + + + + + fp.sensor.dashboard.list + fp.sensor.dashboard + + + + + + + + + + + + + + fp.sensor.dashboard.form + fp.sensor.dashboard + +
+ +
+ + +
+
+

+
+ + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + Dashboards + fp.sensor.dashboard + list,form + + +
diff --git a/fusion_iot/fusion_plating_iot/views/fp_sensor_type_views.xml b/fusion_iot/fusion_plating_iot/views/fp_sensor_type_views.xml new file mode 100644 index 00000000..2fc752a6 --- /dev/null +++ b/fusion_iot/fusion_plating_iot/views/fp_sensor_type_views.xml @@ -0,0 +1,60 @@ + + + + + fp.sensor.type.list + fp.sensor.type + + + + + + + + + + + + + + + + fp.sensor.type.form + fp.sensor.type + +
+ +
+ +
+
+

+
+ + + + + + + + + + + + +
+
+
+
+ + + Sensor Types + fp.sensor.type + list,form + + +
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 249bab91..e388c128 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 @@ -10,14 +10,19 @@ fp.tank.sensor.list fp.tank.sensor - - - - + + + + + + + + @@ -55,18 +60,30 @@

+ + + + + + - - + + + - + + + + + + diff --git a/fusion_plating/fusion_plating_sensors/__init__.py b/fusion_plating/fusion_plating_sensors/__init__.py deleted file mode 100644 index 9db79dc2..00000000 --- a/fusion_plating/fusion_plating_sensors/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -from . import controllers -from . import models -from . import wizard diff --git a/fusion_plating/fusion_plating_sensors/__manifest__.py b/fusion_plating/fusion_plating_sensors/__manifest__.py deleted file mode 100644 index 0fbfd1b7..00000000 --- a/fusion_plating/fusion_plating_sensors/__manifest__.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -{ - 'name': 'Fusion Plating — Sensors', - 'version': '19.0.1.0.0', - 'category': 'Manufacturing/Plating', - 'summary': 'Tank and process sensor tracking with IoT API, dashboards, and alerts.', - 'description': """ -Fusion Plating — Sensors -======================== - -Chemistry and environmental sensor tracking for electroless nickel plating -and metal finishing operations. Replaces Steelhead Software sensor module. - -* Sensor type definitions (pH, %, g/L, PPM, conductivity, etc.) -* Individual sensors linked to tanks, work centres, and facilities -* Timestamped measurements (manual entry + IoT API) -* Sensor dashboards with alert thresholds -* Quick-measure wizard for operators -* JSON-RPC endpoint for automated data collection - -Part of the Fusion Plating product family by Nexa Systems Inc. - -Copyright (c) 2026 Nexa Systems Inc. All rights reserved. - """, - 'author': 'Nexa Systems Inc.', - 'website': 'https://www.nexasystems.ca', - 'maintainer': 'Nexa Systems Inc.', - 'support': 'support@nexasystems.ca', - 'license': 'OPL-1', - 'price': 0.00, - 'currency': 'CAD', - 'depends': [ - 'fusion_plating', - ], - 'data': [ - 'security/fp_sensor_security.xml', - 'security/ir.model.access.csv', - 'data/fp_sensor_sequence_data.xml', - 'views/fp_sensor_type_views.xml', - 'views/fp_sensor_views.xml', - 'views/fp_sensor_measurement_views.xml', - 'views/fp_sensor_dashboard_views.xml', - 'views/fp_sensor_measure_wizard_views.xml', - 'views/fp_sensor_menu.xml', - ], - 'installable': True, - 'application': False, - 'auto_install': False, -} diff --git a/fusion_plating/fusion_plating_sensors/controllers/__init__.py b/fusion_plating/fusion_plating_sensors/controllers/__init__.py deleted file mode 100644 index a6867039..00000000 --- a/fusion_plating/fusion_plating_sensors/controllers/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -from . import sensor_controller diff --git a/fusion_plating/fusion_plating_sensors/controllers/sensor_controller.py b/fusion_plating/fusion_plating_sensors/controllers/sensor_controller.py deleted file mode 100644 index d665161b..00000000 --- a/fusion_plating/fusion_plating_sensors/controllers/sensor_controller.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -import logging - -from odoo import http, fields -from odoo.http import request - -_logger = logging.getLogger(__name__) - - -class SensorController(http.Controller): - """JSON-RPC endpoint for IoT devices to push sensor readings.""" - - @http.route( - '/fp/sensor/measure', - type='jsonrpc', - auth='user', - methods=['POST'], - ) - def sensor_measure(self, uuid=None, value=None, value_text=None, - value_bool=None, effective_at=None, comment=None): - """Record a measurement from an IoT device or external API. - - Args: - uuid: Sensor UUID (required) - value: Numeric reading (for NUMBER sensors) - value_text: Text reading (for TEXT sensors) - value_bool: Boolean reading (for BOOLEAN sensors) - effective_at: ISO datetime string (optional, defaults to now) - comment: Optional note - - Returns: - dict with ok=True and measurement_id on success - """ - if not uuid: - return {'ok': False, 'error': 'uuid is required'} - - sensor = request.env['fp.sensor'].sudo().search( - [('uuid', '=', uuid)], limit=1, - ) - if not sensor: - return {'ok': False, 'error': f'No sensor with UUID {uuid}'} - - vals = { - 'sensor_id': sensor.id, - 'source': 'api', - 'creator_id': request.env.uid, - } - - if effective_at: - vals['effective_at'] = effective_at - if comment: - vals['comment'] = comment - - mtype = sensor.measurement_type - if mtype == 'number': - if value is None: - return {'ok': False, 'error': 'value is required for NUMBER sensors'} - vals['value'] = float(value) - elif mtype == 'text': - if value_text is None: - return {'ok': False, 'error': 'value_text is required for TEXT sensors'} - vals['value_text'] = str(value_text) - elif mtype == 'boolean': - if value_bool is None: - return {'ok': False, 'error': 'value_bool is required for BOOLEAN sensors'} - vals['value_bool'] = bool(value_bool) - - measurement = request.env['fp.sensor.measurement'].sudo().create(vals) - _logger.info( - 'Sensor %s (%s): recorded measurement %s via API', - sensor.name, uuid, measurement.name, - ) - return {'ok': True, 'measurement_id': measurement.id} diff --git a/fusion_plating/fusion_plating_sensors/data/fp_sensor_sequence_data.xml b/fusion_plating/fusion_plating_sensors/data/fp_sensor_sequence_data.xml deleted file mode 100644 index 630ddb98..00000000 --- a/fusion_plating/fusion_plating_sensors/data/fp_sensor_sequence_data.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Sensor Measurement - fp.sensor.measurement - SMEAS/%(year)s/ - 5 - 1 - - - - - diff --git a/fusion_plating/fusion_plating_sensors/models/__init__.py b/fusion_plating/fusion_plating_sensors/models/__init__.py deleted file mode 100644 index 6722e81f..00000000 --- a/fusion_plating/fusion_plating_sensors/models/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -from . import fp_sensor_type -from . import fp_sensor -from . import fp_sensor_measurement -from . import fp_sensor_dashboard -from . import fp_sensor_alert_rule diff --git a/fusion_plating/fusion_plating_sensors/models/fp_sensor.py b/fusion_plating/fusion_plating_sensors/models/fp_sensor.py deleted file mode 100644 index 97a214e2..00000000 --- a/fusion_plating/fusion_plating_sensors/models/fp_sensor.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -from odoo import api, fields, models - - -class FpSensor(models.Model): - """Individual measurement point. - - Each sensor represents a specific thing being measured at a specific - location, e.g. "Tank SP-7 pH" or "Waste Water Treatment pH". - Linked to a work centre (station) and/or tank for traceability. - UUID field enables IoT device integration. - """ - _name = 'fp.sensor' - _description = 'Fusion Plating — Sensor' - _inherit = ['mail.thread', 'mail.activity.mixin'] - _order = 'name' - - name = fields.Char( - string='Name', - required=True, - tracking=True, - help='Descriptive name, e.g. "Waste Water Treatment pH".', - ) - uuid = fields.Char( - string='UUID', - index=True, - copy=False, - help='Hardware identifier for IoT devices.', - ) - unit = fields.Char( - string='Unit', - help='Display unit for readings, e.g. "ph", "%", "g/L", "PPM", "L".', - ) - sensor_type_id = fields.Many2one( - 'fp.sensor.type', - string='Sensor Type', - required=True, - ondelete='restrict', - tracking=True, - ) - measurement_type = fields.Selection( - related='sensor_type_id.measurement_type', - string='Measurement Type', - store=True, - readonly=True, - ) - work_center_id = fields.Many2one( - 'fusion.plating.work.center', - string='Station', - ondelete='set null', - help='The work centre / station this sensor is attached to.', - ) - tank_id = fields.Many2one( - 'fusion.plating.tank', - string='Tank', - ondelete='set null', - ) - facility_id = fields.Many2one( - 'fusion.plating.facility', - string='Facility', - ondelete='set null', - ) - location_name = fields.Char( - string='Location', - help='Free-text location, e.g. "WaterTreatmentArea", "PLANT1.TankLine".', - ) - use_location = fields.Boolean( - string='Use Location?', - default=False, - ) - - # -- Computed from latest measurement -- - last_value = fields.Float( - string='Last Measurement', - compute='_compute_last_measurement', - store=True, - ) - last_value_text = fields.Char( - string='Last Text Value', - compute='_compute_last_measurement', - store=True, - ) - last_measured = fields.Datetime( - string='Last Measured', - compute='_compute_last_measurement', - store=True, - ) - - measurement_ids = fields.One2many( - 'fp.sensor.measurement', - 'sensor_id', - string='Measurements', - ) - measurement_count = fields.Integer( - string='Measurement Count', - compute='_compute_measurement_count', - ) - active = fields.Boolean(default=True) - company_id = fields.Many2one( - 'res.company', - string='Company', - default=lambda self: self.env.company, - ) - - _sql_constraints = [ - ('uuid_uniq', 'unique(uuid)', - 'A sensor with this UUID already exists.'), - ] - - @api.depends( - 'measurement_ids', - 'measurement_ids.value', - 'measurement_ids.value_text', - 'measurement_ids.effective_at', - ) - def _compute_last_measurement(self): - for sensor in self: - latest = self.env['fp.sensor.measurement'].search( - [('sensor_id', '=', sensor.id)], - order='effective_at desc, id desc', - limit=1, - ) - if latest: - sensor.last_value = latest.value - sensor.last_value_text = latest.value_text - sensor.last_measured = latest.effective_at - else: - sensor.last_value = 0.0 - sensor.last_value_text = False - sensor.last_measured = False - - def action_quick_measure(self): - """Open the quick measurement wizard.""" - self.ensure_one() - return { - 'type': 'ir.actions.act_window', - 'name': 'Record Measurement', - 'res_model': 'fp.sensor.measure.wizard', - 'view_mode': 'form', - 'target': 'new', - 'context': {'default_sensor_id': self.id}, - } - - def action_view_measurements(self): - """Open measurement list filtered to this sensor.""" - self.ensure_one() - return { - 'type': 'ir.actions.act_window', - 'name': f'Measurements — {self.name}', - 'res_model': 'fp.sensor.measurement', - 'view_mode': 'list,form', - 'domain': [('sensor_id', '=', self.id)], - 'context': {'default_sensor_id': self.id}, - } - - def _compute_measurement_count(self): - data = self.env['fp.sensor.measurement']._read_group( - [('sensor_id', 'in', self.ids)], - ['sensor_id'], - ['__count'], - ) - mapped = {sensor.id: count for sensor, count in data} - for sensor in self: - sensor.measurement_count = mapped.get(sensor.id, 0) diff --git a/fusion_plating/fusion_plating_sensors/models/fp_sensor_alert_rule.py b/fusion_plating/fusion_plating_sensors/models/fp_sensor_alert_rule.py deleted file mode 100644 index adf78d36..00000000 --- a/fusion_plating/fusion_plating_sensors/models/fp_sensor_alert_rule.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -from odoo import fields, models - - -class FpSensorAlertRule(models.Model): - """Threshold alert rule for a sensor. - - When a sensor's last reading exceeds threshold_high or falls - below threshold_low, the parent dashboard's alert_count increments. - """ - _name = 'fp.sensor.alert.rule' - _description = 'Fusion Plating — Sensor Alert Rule' - _order = 'sensor_id, id' - - sensor_id = fields.Many2one( - 'fp.sensor', - string='Sensor', - required=True, - ondelete='cascade', - ) - dashboard_id = fields.Many2one( - 'fp.sensor.dashboard', - string='Dashboard', - ondelete='cascade', - ) - threshold_high = fields.Float( - string='High Threshold', - help='Alert when sensor value exceeds this.', - ) - threshold_low = fields.Float( - string='Low Threshold', - help='Alert when sensor value falls below this.', - ) - active = fields.Boolean(default=True) - company_id = fields.Many2one( - 'res.company', - string='Company', - default=lambda self: self.env.company, - ) diff --git a/fusion_plating/fusion_plating_sensors/models/fp_sensor_dashboard.py b/fusion_plating/fusion_plating_sensors/models/fp_sensor_dashboard.py deleted file mode 100644 index ec51e9b5..00000000 --- a/fusion_plating/fusion_plating_sensors/models/fp_sensor_dashboard.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -from odoo import api, fields, models - - -class FpSensorDashboard(models.Model): - """Sensor chart grouping with alert monitoring. - - Groups multiple sensors into a named dashboard for trend - visualization and threshold alerting. - """ - _name = 'fp.sensor.dashboard' - _description = 'Fusion Plating — Sensor Dashboard' - _inherit = ['mail.thread'] - _order = 'name' - - name = fields.Char(string='Name', required=True, tracking=True) - sensor_ids = fields.Many2many( - 'fp.sensor', - 'fp_sensor_dashboard_sensor_rel', - 'dashboard_id', - 'sensor_id', - string='Sensors', - ) - alert_rule_ids = fields.One2many( - 'fp.sensor.alert.rule', - 'dashboard_id', - string='Alert Rules', - ) - member_count = fields.Integer( - string='Members', - compute='_compute_counts', - ) - alert_count = fields.Integer( - string='Alerts', - compute='_compute_counts', - ) - active = fields.Boolean(default=True) - company_id = fields.Many2one( - 'res.company', - string='Company', - default=lambda self: self.env.company, - ) - - @api.depends('sensor_ids', 'alert_rule_ids', 'alert_rule_ids.active') - def _compute_counts(self): - for dash in self: - dash.member_count = len(dash.sensor_ids) - active_rules = dash.alert_rule_ids.filtered('active') - alert_count = 0 - for rule in active_rules: - sensor = rule.sensor_id - val = sensor.last_value - if rule.threshold_high and val > rule.threshold_high: - alert_count += 1 - elif rule.threshold_low and val < rule.threshold_low: - alert_count += 1 - dash.alert_count = alert_count diff --git a/fusion_plating/fusion_plating_sensors/models/fp_sensor_measurement.py b/fusion_plating/fusion_plating_sensors/models/fp_sensor_measurement.py deleted file mode 100644 index 250d2b31..00000000 --- a/fusion_plating/fusion_plating_sensors/models/fp_sensor_measurement.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -from odoo import api, fields, models - - -class FpSensorMeasurement(models.Model): - """Timestamped sensor reading. - - High-volume model — no mail.thread to keep writes fast for IoT - ingestion. Three value columns cover all measurement types; - views branch on the sensor's measurement_type. - """ - _name = 'fp.sensor.measurement' - _description = 'Fusion Plating — Sensor Measurement' - _order = 'effective_at desc, id desc' - - name = fields.Char( - string='Reference', - readonly=True, - copy=False, - default='New', - ) - sensor_id = fields.Many2one( - 'fp.sensor', - string='Sensor', - required=True, - ondelete='cascade', - index=True, - ) - # Denormalised for list filtering / grouping - sensor_type_id = fields.Many2one( - related='sensor_id.sensor_type_id', - store=True, - readonly=True, - ) - measurement_type = fields.Selection( - related='sensor_id.measurement_type', - store=True, - readonly=True, - ) - unit = fields.Char( - related='sensor_id.unit', - readonly=True, - ) - - # -- Value columns (one per measurement type) -- - value = fields.Float( - string='Value', - help='Numeric measurement (for NUMBER type sensors).', - ) - value_text = fields.Char( - string='Text Value', - help='Text measurement (for TEXT type sensors).', - ) - value_bool = fields.Boolean( - string='Boolean Value', - help='Boolean measurement (for BOOLEAN type sensors).', - ) - - comment = fields.Text(string='Comment') - effective_at = fields.Datetime( - string='Effective At', - default=fields.Datetime.now, - required=True, - index=True, - help='Timestamp of when the measurement was actually taken.', - ) - source = fields.Selection( - [ - ('manual', 'Manual'), - ('api', 'API'), - ('iot', 'IoT Device'), - ], - string='Source', - default='manual', - required=True, - ) - creator_id = fields.Many2one( - 'res.users', - string='Creator', - default=lambda self: self.env.uid, - ) - company_id = fields.Many2one( - 'res.company', - string='Company', - default=lambda self: self.env.company, - ) - - @api.model_create_multi - def create(self, vals_list): - seq = self.env['ir.sequence'] - for vals in vals_list: - if vals.get('name', 'New') == 'New': - vals['name'] = seq.next_by_code('fp.sensor.measurement') or 'New' - return super().create(vals_list) diff --git a/fusion_plating/fusion_plating_sensors/models/fp_sensor_type.py b/fusion_plating/fusion_plating_sensors/models/fp_sensor_type.py deleted file mode 100644 index 21a0cc0c..00000000 --- a/fusion_plating/fusion_plating_sensors/models/fp_sensor_type.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -from odoo import fields, models - - -class FpSensorType(models.Model): - """Sensor measurement template. - - Defines what a sensor measures (pH, % Nickel Activity, conductivity, etc.) - and the data type of its readings. The unit lives on the sensor itself - because the same type (e.g. NUMBER) can have different units (ph, %, g/L). - """ - _name = 'fp.sensor.type' - _description = 'Fusion Plating — Sensor Type' - _order = 'name' - - name = fields.Char( - string='Name', - required=True, - help='Descriptive name, e.g. "Waste Water Effluent pH", ' - '"Tank SP-7 % Nickel Activity".', - ) - measurement_type = fields.Selection( - [ - ('number', 'Number'), - ('boolean', 'Boolean'), - ('text', 'Text'), - ], - string='Measurement Type', - required=True, - default='number', - ) - sensor_ids = fields.One2many( - 'fp.sensor', - 'sensor_type_id', - string='Sensors', - ) - sensor_count = fields.Integer( - string='Sensor Count', - compute='_compute_sensor_count', - ) - active = fields.Boolean(default=True) - company_id = fields.Many2one( - 'res.company', - string='Company', - default=lambda self: self.env.company, - ) - - def _compute_sensor_count(self): - for rec in self: - rec.sensor_count = len(rec.sensor_ids) - - def action_view_sensors(self): - """Open sensor list filtered to this type.""" - self.ensure_one() - return { - 'type': 'ir.actions.act_window', - 'name': f'Sensors — {self.name}', - 'res_model': 'fp.sensor', - 'view_mode': 'list,form', - 'domain': [('sensor_type_id', '=', self.id)], - 'context': {'default_sensor_type_id': self.id}, - } diff --git a/fusion_plating/fusion_plating_sensors/security/fp_sensor_security.xml b/fusion_plating/fusion_plating_sensors/security/fp_sensor_security.xml deleted file mode 100644 index b962b278..00000000 --- a/fusion_plating/fusion_plating_sensors/security/fp_sensor_security.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - Sensor Type: multi-company - - - [('company_id', 'in', company_ids + [False])] - - - - Sensor: multi-company - - - [('company_id', 'in', company_ids + [False])] - - - - Sensor Measurement: multi-company - - - [('company_id', 'in', company_ids + [False])] - - - - Sensor Dashboard: multi-company - - - [('company_id', 'in', company_ids + [False])] - - - - Sensor Alert Rule: multi-company - - - [('company_id', 'in', company_ids + [False])] - - - - diff --git a/fusion_plating/fusion_plating_sensors/security/ir.model.access.csv b/fusion_plating/fusion_plating_sensors/security/ir.model.access.csv deleted file mode 100644 index e2dfb03b..00000000 --- a/fusion_plating/fusion_plating_sensors/security/ir.model.access.csv +++ /dev/null @@ -1,17 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_fp_sensor_type_operator,fp.sensor.type.operator,model_fp_sensor_type,fusion_plating.group_fusion_plating_operator,1,0,0,0 -access_fp_sensor_type_supervisor,fp.sensor.type.supervisor,model_fp_sensor_type,fusion_plating.group_fusion_plating_supervisor,1,1,1,0 -access_fp_sensor_type_manager,fp.sensor.type.manager,model_fp_sensor_type,fusion_plating.group_fusion_plating_manager,1,1,1,1 -access_fp_sensor_operator,fp.sensor.operator,model_fp_sensor,fusion_plating.group_fusion_plating_operator,1,0,0,0 -access_fp_sensor_supervisor,fp.sensor.supervisor,model_fp_sensor,fusion_plating.group_fusion_plating_supervisor,1,1,1,0 -access_fp_sensor_manager,fp.sensor.manager,model_fp_sensor,fusion_plating.group_fusion_plating_manager,1,1,1,1 -access_fp_sensor_measurement_operator,fp.sensor.measurement.operator,model_fp_sensor_measurement,fusion_plating.group_fusion_plating_operator,1,1,1,0 -access_fp_sensor_measurement_supervisor,fp.sensor.measurement.supervisor,model_fp_sensor_measurement,fusion_plating.group_fusion_plating_supervisor,1,1,1,0 -access_fp_sensor_measurement_manager,fp.sensor.measurement.manager,model_fp_sensor_measurement,fusion_plating.group_fusion_plating_manager,1,1,1,1 -access_fp_sensor_dashboard_operator,fp.sensor.dashboard.operator,model_fp_sensor_dashboard,fusion_plating.group_fusion_plating_operator,1,0,0,0 -access_fp_sensor_dashboard_supervisor,fp.sensor.dashboard.supervisor,model_fp_sensor_dashboard,fusion_plating.group_fusion_plating_supervisor,1,1,1,0 -access_fp_sensor_dashboard_manager,fp.sensor.dashboard.manager,model_fp_sensor_dashboard,fusion_plating.group_fusion_plating_manager,1,1,1,1 -access_fp_sensor_alert_rule_operator,fp.sensor.alert.rule.operator,model_fp_sensor_alert_rule,fusion_plating.group_fusion_plating_operator,1,0,0,0 -access_fp_sensor_alert_rule_supervisor,fp.sensor.alert.rule.supervisor,model_fp_sensor_alert_rule,fusion_plating.group_fusion_plating_supervisor,1,1,1,0 -access_fp_sensor_alert_rule_manager,fp.sensor.alert.rule.manager,model_fp_sensor_alert_rule,fusion_plating.group_fusion_plating_manager,1,1,1,1 -access_fp_sensor_measure_wizard_operator,fp.sensor.measure.wizard.operator,model_fp_sensor_measure_wizard,fusion_plating.group_fusion_plating_operator,1,1,1,1 diff --git a/fusion_plating/fusion_plating_sensors/views/fp_sensor_dashboard_views.xml b/fusion_plating/fusion_plating_sensors/views/fp_sensor_dashboard_views.xml deleted file mode 100644 index 0c075233..00000000 --- a/fusion_plating/fusion_plating_sensors/views/fp_sensor_dashboard_views.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - fp.sensor.dashboard.form - fp.sensor.dashboard - -
- -
-

- -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
- - - - fp.sensor.dashboard.list - fp.sensor.dashboard - - - - - - - - - - - - Sensor Dashboards - fp.sensor.dashboard - list,form - -

- Create a sensor dashboard -

-

Group sensors into dashboards for monitoring and alerting.

-
-
- -
diff --git a/fusion_plating/fusion_plating_sensors/views/fp_sensor_measure_wizard_views.xml b/fusion_plating/fusion_plating_sensors/views/fp_sensor_measure_wizard_views.xml deleted file mode 100644 index 840f3873..00000000 --- a/fusion_plating/fusion_plating_sensors/views/fp_sensor_measure_wizard_views.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - fp.sensor.measure.wizard.form - fp.sensor.measure.wizard - -
- - - - - - - - - - -
-
-
-
-
- - - - Record Measurement - fp.sensor.measure.wizard - form - new - - -
diff --git a/fusion_plating/fusion_plating_sensors/views/fp_sensor_measurement_views.xml b/fusion_plating/fusion_plating_sensors/views/fp_sensor_measurement_views.xml deleted file mode 100644 index 535d2056..00000000 --- a/fusion_plating/fusion_plating_sensors/views/fp_sensor_measurement_views.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - fp.sensor.measurement.form - fp.sensor.measurement - -
- - - - - - - - - - - - - - - - - - - - - -
-
-
- - - - fp.sensor.measurement.list - fp.sensor.measurement - - - - - - - - - - - - - - - - - fp.sensor.measurement.search - fp.sensor.measurement - - - - - - - - - - - - - - - - - - - - - - - - Sensor Measurements - fp.sensor.measurement - list,form - - {'search_default_filter_today': 0} - -

- No measurements recorded yet -

-

Measurements are recorded manually via the sensor form or automatically via the IoT API.

-
-
- -
diff --git a/fusion_plating/fusion_plating_sensors/views/fp_sensor_menu.xml b/fusion_plating/fusion_plating_sensors/views/fp_sensor_menu.xml deleted file mode 100644 index 4812675c..00000000 --- a/fusion_plating/fusion_plating_sensors/views/fp_sensor_menu.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/fusion_plating/fusion_plating_sensors/views/fp_sensor_type_views.xml b/fusion_plating/fusion_plating_sensors/views/fp_sensor_type_views.xml deleted file mode 100644 index bc5036a6..00000000 --- a/fusion_plating/fusion_plating_sensors/views/fp_sensor_type_views.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - fp.sensor.type.form - fp.sensor.type - -
- -
- -
- - - - - - - - - - -
-
-
-
- - - - fp.sensor.type.list - fp.sensor.type - - - - - - - - - - - - fp.sensor.type.search - fp.sensor.type - - - - - - - - - - - - - - - - - Sensor Types - fp.sensor.type - list,form - - -

- Create a sensor type -

-

Sensor types define what a sensor measures (Number, Text, or Boolean).

-
-
- -
diff --git a/fusion_plating/fusion_plating_sensors/views/fp_sensor_views.xml b/fusion_plating/fusion_plating_sensors/views/fp_sensor_views.xml deleted file mode 100644 index 9af6856d..00000000 --- a/fusion_plating/fusion_plating_sensors/views/fp_sensor_views.xml +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - fp.sensor.form - fp.sensor - -
-
-
- -
- -
-
-

- -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
- - - - fp.sensor.list - fp.sensor - - - - - - - - - - - - - - - - - - fp.sensor.search - fp.sensor - - - - - - - - - - - - - - - - - - - - - - - Sensors - fp.sensor - list,form - - -

- Create your first sensor -

-

Sensors track chemistry readings at tanks, work centres, and other locations.

-
-
- -
diff --git a/fusion_plating/fusion_plating_sensors/wizard/__init__.py b/fusion_plating/fusion_plating_sensors/wizard/__init__.py deleted file mode 100644 index 7fa9a7d0..00000000 --- a/fusion_plating/fusion_plating_sensors/wizard/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -from . import fp_sensor_measure_wizard diff --git a/fusion_plating/fusion_plating_sensors/wizard/fp_sensor_measure_wizard.py b/fusion_plating/fusion_plating_sensors/wizard/fp_sensor_measure_wizard.py deleted file mode 100644 index fda60037..00000000 --- a/fusion_plating/fusion_plating_sensors/wizard/fp_sensor_measure_wizard.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) - -from odoo import api, fields, models - - -class FpSensorMeasureWizard(models.TransientModel): - """Quick measurement entry wizard. - - Opened from the "+ Measure" button on the sensor form. - Pre-fills sensor and unit, operator enters value + date + comment. - """ - _name = 'fp.sensor.measure.wizard' - _description = 'Record Sensor Measurement' - - sensor_id = fields.Many2one( - 'fp.sensor', - string='Sensor', - required=True, - readonly=True, - ) - measurement_type = fields.Selection( - related='sensor_id.measurement_type', - readonly=True, - ) - unit = fields.Char( - related='sensor_id.unit', - readonly=True, - ) - value = fields.Float(string='Value') - value_text = fields.Char(string='Text Value') - value_bool = fields.Boolean(string='Boolean Value') - effective_at = fields.Datetime( - string='Date', - default=fields.Datetime.now, - required=True, - ) - comment = fields.Text(string='Comment') - - def action_confirm(self): - self.ensure_one() - vals = { - 'sensor_id': self.sensor_id.id, - 'effective_at': self.effective_at, - 'comment': self.comment, - 'source': 'manual', - } - mtype = self.sensor_id.measurement_type - if mtype == 'number': - vals['value'] = self.value - elif mtype == 'text': - vals['value_text'] = self.value_text - elif mtype == 'boolean': - vals['value_bool'] = self.value_bool - self.env['fp.sensor.measurement'].create(vals) - return {'type': 'ir.actions.act_window_close'}