# -*- 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)