Files
stock-logistics-warehouse/stock_vertical_lift/models/vertical_lift_shuttle.py
SilvioC2C 702c876a23 [IMP] stock_vertical_lift: filter notifications by screen
If the same user opens multiple shuttles operations on different screens,
notifications for a specific shuttle will be displayed on all of them,
regardless of the shuttle operation that triggered the notification.

This commit allows filtering notifications per screen, so that only
screens displaying shuttle operations related to the current notifications
will display them.

Notifications that are not shuttle-related are not filtered out.
2024-02-19 14:55:33 +01:00

303 lines
11 KiB
Python

# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
import socket
import ssl
from odoo import _, fields, models
_logger = logging.getLogger(__name__)
class VerticalLiftShuttle(models.Model):
_name = "vertical.lift.shuttle"
_inherit = "barcodes.barcode_events_mixin"
_description = "Vertical Lift Shuttle"
name = fields.Char()
mode = fields.Selection(
[("pick", "Pick"), ("put", "Put"), ("inventory", "Inventory")],
default="pick",
required=True,
)
location_id = fields.Many2one(
comodel_name="stock.location",
required=True,
domain="[('vertical_lift_kind', '=', 'shuttle')]",
ondelete="restrict",
help="The Shuttle source location for Pick operations "
"and destination location for Put operations.",
)
hardware = fields.Selection(
selection="_selection_hardware", default="simulation", required=True
)
server = fields.Char(help="hostname or IP address of the server")
port = fields.Integer(
help="network port of the server on which to send the message"
)
use_tls = fields.Boolean(
help="set this if the server expects TLS wrapped communication"
)
command_ids = fields.One2many(
"vertical.lift.command", "shuttle_id", string="Hardware commands"
)
_sql_constraints = [
(
"location_id_unique",
"UNIQUE(location_id)",
"You cannot have two shuttles using the same location.",
)
]
def _selection_hardware(self):
return [("simulation", "Simulation")]
@property
def _model_for_mode(self):
return {
"pick": "vertical.lift.operation.pick",
"put": "vertical.lift.operation.put",
"inventory": "vertical.lift.operation.inventory",
}
@property
def _screen_view_for_mode(self):
return {
"pick": ("stock_vertical_lift." "vertical_lift_operation_pick_screen_view"),
"put": ("stock_vertical_lift." "vertical_lift_operation_put_screen_view"),
"inventory": (
"stock_vertical_lift." "vertical_lift_operation_inventory_screen_view"
),
}
def _hardware_send_message(self, payload):
"""default implementation for message sending
If in hardware is 'simulation' then display a simple message.
Otherwise defaults to connecting to server:port using a TCP socket
(optionnally wrapped with TLS) and sending the payload.
:param payload: a bytes object containing the payload
"""
self.ensure_one()
_logger.info("send %r", payload)
command_values = {"shuttle_id": self.id, "command": payload.decode()}
self.env["vertical.lift.command"].sudo().create(command_values)
if self.hardware == "simulation":
self.env.user.notify_info(
message=payload,
title=_("Lift Simulation"),
params=self._get_user_notification_params(),
)
return True
else:
conn = self._hardware_get_server_connection()
try:
offset = 0
while True:
size = conn.send(payload[offset:])
offset += size
if offset >= len(payload) or not size:
break
finally:
self._hardware_release_server_connection(conn)
def _hardware_response_callback(self, command):
"""should be called when a response is received from the hardware
:param response: a string
"""
success = self._check_server_response(command)
self._send_notification_refresh(success)
def _check_server_response(self, command):
"""Use this to check if the response is a success or a failure
:param payload: the payload sent
:param response: the response received
:return: True if the response is a succes, False otherwise
"""
return True
def _hardware_release_server_connection(self, conn):
conn.close()
def _hardware_get_server_connection(self):
"""This implementation will yield a new connection to the server
and close() it when exiting the context.
Override to match the communication protocol of your hardware"""
conn = socket.create_connection((self.server, self.port))
if self.use_tls:
ctx = ssl.create_default_context()
self._hardware_update_tls_context(ctx)
conntls = ctx.wrap_socket(conn, server_hostname=self.server)
return conntls
else:
return conn
def _hardware_update_tls_context(self, context):
"""Update the TLS context, e.g. to add a client certificate.
This method does nothing, override to match your communication
protocol."""
pass # noqa
def _operation_for_mode(self):
model = self._model_for_mode[self.mode]
record = self.env[model].search([("shuttle_id", "=", self.id)])
if not record:
record = self.env[model].create({"shuttle_id": self.id})
return record
def action_open_screen(self):
self.ensure_one()
assert self.mode in ("pick", "put", "inventory")
screen_xmlid = self._screen_view_for_mode[self.mode]
operation = self._operation_for_mode()
operation.on_screen_open()
return {
"type": "ir.actions.act_window",
"res_model": operation._name,
"views": [[self.env.ref(screen_xmlid).id, "form"]],
"res_id": operation.id,
"target": "fullscreen",
"flags": {
"withControlPanel": False,
"form_view_initial_mode": "edit",
"no_breadcrumbs": True,
},
}
def action_menu(self):
menu_xmlid = "stock_vertical_lift.vertical_lift_shuttle_form_menu"
return {
"type": "ir.actions.act_window",
"res_model": "vertical.lift.shuttle",
"views": [[self.env.ref(menu_xmlid).id, "form"]],
"name": _("Menu"),
"target": "new",
"res_id": self.id,
}
def action_back_to_settings(self):
self.release_vertical_lift_tray()
action_xmlid = "stock_vertical_lift.vertical_lift_shuttle_action"
action = self.env["ir.actions.act_window"]._for_xml_id(action_xmlid)
action["target"] = "main"
return action
def action_manual_barcode(self):
return {
"type": "ir.actions.act_window",
"res_model": "vertical.lift.shuttle.manual.barcode",
"view_mode": "form",
"name": _("Barcode"),
"target": "new",
}
# TODO: should the mode be changed on all the shuttles at the same time?
def switch_pick(self):
self.mode = "pick"
self.release_vertical_lift_tray()
return self.action_open_screen()
def switch_put(self):
self.mode = "put"
self.release_vertical_lift_tray()
return self.action_open_screen()
def switch_inventory(self):
self.mode = "inventory"
self.release_vertical_lift_tray()
return self.action_open_screen()
def _hardware_vertical_lift_release_tray_payload(self):
"""Prepare "release" message to be sent to the vertical lift hardware
Private method, this is where the implementation actually happens.
Addons can add their instructions based on the hardware used for
this location.
The hardware used for a location can be found in:
``self.hardware``
Each addon can implement its own mechanism depending of this value
and must call ``super``.
The method must send the command to the vertical lift to release (close)
the tray.
Returns a message in bytes, that will be sent through
``VerticalLiftShuttle._hardware_send_message()``.
"""
if self.hardware == "simulation":
message = _("Releasing tray")
return message.encode("utf-8")
else:
raise NotImplementedError()
def release_vertical_lift_tray(self):
"""Send instructions to the vertical lift hardware to close trays
The actual implementation of the method goes in the private method
``_hardware_vertical_lift_release_tray()``.
"""
self.ensure_one()
payload = self._hardware_vertical_lift_release_tray_payload()
return self._hardware_send_message(payload)
def _send_notification_refresh(self, success):
"""Send a refresh notification to the current opened screen
The form controller on the front-end side will instantaneously
refresh the form with the latest committed data.
It can be used for instance after a vertical lift hardware
event occurred to inform the user on their screen.
The method is private only to prevent xml/rpc calls to
interact with the screen.
"""
# XXX do we want to do something special in the notification?
self._operation_for_mode()._send_notification_refresh()
def _get_user_notification_params(self):
return {
"model": self._name,
"id": self.id,
"shuttle_info": self._get_user_notification_params_shuttle_info(),
}
def _get_user_notification_params_shuttle_info(self):
"""Returns a mapping between shuttle-related models and IDs
Used by JS to filter notifications to be displayed: this mapping is
set on all shuttle-related notifications, and JS takes care of comparing
the current web page info (model and ID) with the mapping held by each
notification.
IE: a notification is generated with this mapping:
{
"vertical.lift.shuttle": 1,
"vertical.lift.operation.inventory": 3,
"vertical.lift.operation.pick": 2,
"vertical.lift.operation.put": 4,
}
On web page <host>/web#id=2&model=vertical.lift.operation.pick&view_type=form
the notification is displayed (form view with matching model and ID).
On web page <host>/web#id=1&model=vertical.lift.operation.pick&view_type=form
the notification is not displayed (form view with matching model, but wrong ID).
"""
info = {self._name: self.id}
# Property ``_model_for_mode`` holds a mapping between shuttle mode and related
# shuttle mode specific models (``vertical.lift.operation.*``); most of the
# views on which shuttle notifications are triggered usually display one of
# these records, not a ``vertical.lift.shuttle`` one, therefore appending the
# shuttle ID to the notification itself may not be enough.
for model in self._model_for_mode.values():
info[model] = self.env[model].search([("shuttle_id", "=", self.id)]).id
return info