mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
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.
303 lines
11 KiB
Python
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
|