Files
Odoo-Modules/fusion_iot/iot/models/ir_actions_report.py
gsinghpal 6e964c230f feat(iot): repackaged Odoo iot modules + Fusion Plating sensor wrapper
Phase A of the IoT initiative — gets the server-side infrastructure
in place before the Raspberry Pi hardware arrives, so the iot admin
UI + /fp/iot/ingest endpoint are ready to accept the first real
temperature reading as soon as the Pi is wired up.

New top-level folder: fusion_iot/

1. **iot_base/** — Odoo S.A. iot_base module, copied from
   RePackaged-Odoo verbatim. LGPL-3 upstream, no changes needed.

2. **iot/** — Odoo S.A. iot module, repackaged:
   - `models/update.py` neutralised (removed the publisher_warranty
     IoT-Box-counting report that phones home to odoo.com for
     enterprise licence enforcement)
   - `iot_handlers/lib/load_worldline_library.sh` deleted (proprietary
     Worldline payment lib fetch from download.odoo.com, not needed)
   - `wizard/add_iot_box.py._connect_iot_box_with_pairing_code` —
     upstream called odoo.com's iot-proxy to resolve pairing codes;
     replaced with a no-op. Pi-side iot_drivers proxy registers
     directly with this Odoo server instead.
   - Manifest rebranded with an explicit changelog preamble.

3. **fusion_plating_iot/** — new plating-specific wrapper:
   - `fp.tank.sensor` — maps an iot.device (or a direct-HTTP-ingest
     sensor) to a fusion.plating.tank + fusion.plating.bath.parameter.
     Supports DS18B20, PT100/1000, pH, conductivity, level. Per-sensor
     alert_min/max overrides.
   - `fp.tank.reading` — append-only time-series. On create, evaluates
     against sensor's alert range. On in-spec → out-of-spec TRANSITION,
     auto-raises a fusion.plating.quality.hold (once per excursion,
     no spam during sustained out-of-spec).
   - `POST /fp/iot/ingest` — shared-secret HTTP endpoint for sensors
     bypassing the Pi proxy. Token via X-FP-IOT-Token header OR body.
     Accepts single-reading or batch payloads.
   - Menu under Plating → Operations → Sensors & Readings.
   - Tank form inherits get a Sensors tab inline.

Deployed to entech. Verified end-to-end:
- Install: iot_base + iot + fusion_plating_iot all 'installed'
- Smoke test: in-spec → out-of-spec → hold raised (HOLD-0010);
  continued excursion → NO duplicate hold; back-in-spec → NEW
  excursion → NEW hold (HOLD-0011) ✓
- HTTP endpoint: correct token → 200 accepted; wrong token → 401;
  unknown device_serial → 404; batch payload → 200 accepted=N ✓

Phase B (when Raspberry Pi hardware arrives): DS18B20 iot_handler
driver for the Pi-side iot_drivers proxy + systemd service on
vanilla Raspberry Pi OS + first live reading from physical probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:46:45 -04:00

93 lines
3.6 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo import fields, models, _
from odoo.exceptions import UserError
from lxml.etree import ParserError
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
device_ids = fields.Many2many('iot.device', string='IoT Devices', domain="[('type', '=', 'printer')]",
help='When setting a device here, the report will be printed through this device on the IoT Box')
def render_document(self, device_id_list, res_ids, data=None):
"""Render a document to be printed by the IoT Box through client
:param device_id_list: The list of device ids to print the document
:param res_ids: The list of record ids to print
:param data: The data to pass to the report
:return: The list of documents to print with information about the device
"""
device_ids = self.env['iot.device'].browse(device_id_list)
if len(device_id_list) != len(device_ids.exists()):
raise UserError(_(
"One of the printer used to print the document has been removed.\n"
"To reset printers, go to the IoT App, Configuration tab, \"Reset Linked Printers\" and retry the operation."
))
datas = self._render(self.report_name, res_ids, data=data)
data_bytes = datas[0]
data_base64 = base64.b64encode(data_bytes)
return [{
"iotBoxId": device.iot_id.id,
"deviceId": device.id,
"deviceIdentifier": device.identifier,
"deviceName": device.display_name,
"document": data_base64,
} for device in device_ids] # As it is called via JS, we format keys to camelCase
def report_action(self, docids, data=None, config=True):
result = super().report_action(docids, data, config)
if result.get('type') != 'ir.actions.report':
return result
device = self.device_ids and self.device_ids[0]
if data and data.get('device_id'):
device = self.env['iot.device'].browse(data['device_id'])
result['id'] = self.id
result['device_ids'] = device.mapped('identifier')
return result
def _get_readable_fields(self):
return super()._get_readable_fields() | {
"device_ids",
}
def get_action_wizard(self, selected_device_ids=None):
self.ensure_one()
wizard = self.env['select.printers.wizard'].create({
'display_device_ids': self.device_ids,
'device_ids': selected_device_ids
})
return {
'name': _("Select Printers for %s", self.name),
'res_id': wizard.id,
'type': 'ir.actions.act_window',
'res_model': 'select.printers.wizard',
'target': 'new',
'views': [[False, 'form']],
'context': {
'report_id': self.id,
},
}
def _render_qweb_pdf(self, report_ref, *args, **kwargs):
"""Override to ensure the user is informed when trying to print an empty report
without an IoT printer.
This can happen when trying to print delivery labels, that have empty reports used for assigning
IoT printers.
"""
try:
return super()._render_qweb_pdf(report_ref, *args, **kwargs)
except ParserError:
raise UserError(_(
"The report you are trying to print requires an IoT Box to be printed.\n"
"Make sure you linked the report '%s' to the corresponding IoT printer device.",
report_ref
))