diff --git a/iot_ux/__init__.py b/iot_ux/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/iot_ux/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/iot_ux/__manifest__.py b/iot_ux/__manifest__.py new file mode 100644 index 00000000..4d241e66 --- /dev/null +++ b/iot_ux/__manifest__.py @@ -0,0 +1,39 @@ +############################################################################## +# +# Copyright (C) 2026 ADHOC SA (http://www.adhoc.com.ar) +# All Rights Reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name": "IoT UX Enhancements", + "version": "19.0.1.0.0", + "category": "Generic Modules/Base", + "author": "ADHOC SA", + "license": "AGPL-3", + "depends": ["iot"], + "data": [ + "security/ir.model.access.csv", + "views/iot_views.xml", + "views/ir_actions_report.xml", + "views/res_users.xml", + ], + "assets": { + "web.assets_backend": [ + "iot_ux/static/src/*", + ], + }, + "installable": True, +} diff --git a/iot_ux/models/__init__.py b/iot_ux/models/__init__.py new file mode 100644 index 00000000..f57ff3ea --- /dev/null +++ b/iot_ux/models/__init__.py @@ -0,0 +1,3 @@ +from . import ir_actions_report +from . import res_users +from . import iot_device diff --git a/iot_ux/models/iot_device.py b/iot_ux/models/iot_device.py new file mode 100644 index 00000000..cc3b4125 --- /dev/null +++ b/iot_ux/models/iot_device.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class iotDevice(models.Model): + _inherit = "iot.device" + + iot_report_rule_ids = fields.One2many( + "ir.actions.report.iot.rule", + "device_id", + ) diff --git a/iot_ux/models/ir_actions_report.py b/iot_ux/models/ir_actions_report.py new file mode 100644 index 00000000..6cd24f22 --- /dev/null +++ b/iot_ux/models/ir_actions_report.py @@ -0,0 +1,62 @@ +from odoo import api, fields, models + + +class IrActionsReport(models.Model): + _inherit = "ir.actions.report" + + iot_rule_ids = fields.One2many("ir.actions.report.iot.rule", "report_id", string="IoT Report Rules") + + 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 + if user_rule := self.iot_rule_ids.filtered(lambda r: r.user_id == self.env.user): + result["id"] = self.id + result["device_ids"] = user_rule.device_id.mapped("identifier") + elif self.env.user.iot_device_id: + result["id"] = self.id + result["device_ids"] = self.env.user.iot_device_id.mapped("identifier") + + return result + + +class IrActionsReportIotRule(models.Model): + _name = "ir.actions.report.iot.rule" + _description = "Report IoT Rule" + + report_id = fields.Many2one("ir.actions.report", required=True) + user_id = fields.Many2one( + "res.users", + required=True, + ) + device_id = fields.Many2one("iot.device", ondelete="set null", domain="[('type', '=', 'printer')]") + skip_dialog = fields.Boolean(default=True) + active = fields.Boolean(default=True) + + _unique_report_users = models.Constraint( + "UNIQUE(report_id, user_id)", + "Only can have one IoT rule per report and user.", + ) + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + self.env["iot.channel"].sudo().send_message( + { + "user_ids": res.mapped("user_id").ids, + "report_ids": res.mapped("report_id").ids, + }, + "clear_local_storage", + ) + return res + + def write(self, vals): + res = super().write(vals) + self.env["iot.channel"].sudo().send_message( + { + "user_ids": self.mapped("user_id").ids, + "report_ids": self.mapped("report_id").ids, + }, + "clear_local_storage", + ) + return res diff --git a/iot_ux/models/res_users.py b/iot_ux/models/res_users.py new file mode 100644 index 00000000..85a88227 --- /dev/null +++ b/iot_ux/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + iot_device_id = fields.Many2one( + comodel_name="iot.device", + string="Default Printer", + domain="[('type', '=', 'printer')]", + ) diff --git a/iot_ux/security/ir.model.access.csv b/iot_ux/security/ir.model.access.csv new file mode 100644 index 00000000..d97de807 --- /dev/null +++ b/iot_ux/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_iot_device_rule_user,iot rule user,model_ir_actions_report_iot_rule,base.group_user,1,0,0,0 +access_iot_device_rule_admin,iot rule admin,model_ir_actions_report_iot_rule,iot.group_iot_admin,1,1,1,1 diff --git a/iot_ux/static/src/iot_report_action.js b/iot_ux/static/src/iot_report_action.js new file mode 100644 index 00000000..d7a5e246 --- /dev/null +++ b/iot_ux/static/src/iot_report_action.js @@ -0,0 +1,57 @@ +import { + IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY, + setReportIdInBrowserLocalStorage, +} from "@iot/client_action/delete_local_storage"; +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; +import { user } from "@web/core/user"; + +import { getSelectedPrintersForReport, printReport } from "@iot/iot_report_action"; + + +export async function getSelectedPrintersForReportUx(reportId, env) { + const { orm, action, ui } = env.services; + const deviceSettingsByReportId = JSON.parse(browser.localStorage.getItem(IOT_REPORT_PREFERENCE_LOCAL_STORAGE_KEY)); + const deviceSettings = deviceSettingsByReportId?.[reportId]; + if (!deviceSettings ) { + const rule= await orm.call( + "ir.actions.report.iot.rule", + "search_read", + [[['report_id', '=', reportId], ['user_id', '=', user.userId], ['device_id', '!=', false]], + ["skip_dialog", "device_id"]]); + if (rule.length > 0) { + const newDeviceSettings = { + selectedDevices: [rule[0].device_id[0]], + skipDialog: rule[0].skip_dialog, + }; + setReportIdInBrowserLocalStorage(reportId, newDeviceSettings); + } + } + return await getSelectedPrintersForReport(reportId, env); +} + +async function iotReportActionHandler(action, options, env) { + if (action.device_ids && action.device_ids.length) { + action.data ??= {}; + const args = [action.id, action.context.active_ids, action.data]; + const reportId = action.id; + const printerIds = await getSelectedPrintersForReportUx(reportId, env); + + if (!printerIds) { + // If the user does not select any printer, fall back to normal printing + return false; + } + + env.services.ui.block(); + // Try longpolling then websocket + await printReport(env, args, printerIds); + env.services.ui.unblock(); + + options.onClose?.(); + return true; + } +} + +registry + .category("ir.actions.report handlers") + .add("iot_report_action_handler", iotReportActionHandler, {force: true}); diff --git a/iot_ux/views/iot_views.xml b/iot_ux/views/iot_views.xml new file mode 100644 index 00000000..6dfba059 --- /dev/null +++ b/iot_ux/views/iot_views.xml @@ -0,0 +1,19 @@ + + + + report print + iot.device + + + + + + + + + + + + + + diff --git a/iot_ux/views/ir_actions_report.xml b/iot_ux/views/ir_actions_report.xml new file mode 100644 index 00000000..99a85ace --- /dev/null +++ b/iot_ux/views/ir_actions_report.xml @@ -0,0 +1,23 @@ + + + + report xml rules + ir.actions.report + + + + + + + + + + + + + + + + + + diff --git a/iot_ux/views/res_users.xml b/iot_ux/views/res_users.xml new file mode 100644 index 00000000..13c4cad3 --- /dev/null +++ b/iot_ux/views/res_users.xml @@ -0,0 +1,16 @@ + + + + res.users.preferences.form + res.users + + + + + + + + + + +