168 lines
5.0 KiB
Python
168 lines
5.0 KiB
Python
# -*- 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)
|