mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
* Rename methods that fetch a tray to prevent confusion * Add methods to release a tray * The Kardex method to fetch a tray has to send "0" in the carrier and carrierNext field * The pick and inventory screens release the tray only when there is no next line, because the release is implicit when we fetch the next line, the put screen releases everytime because the operator may take time to start the next line and we don't know if they are going to scan a next line or not. * Exiting the screen or switching screen between put/pick/put-away has to release the tray as well.
278 lines
9.5 KiB
Python
278 lines
9.5 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"))
|
|
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
|
|
|
|
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.ref(action_xmlid).read()[0]
|
|
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()
|
|
|
|
|
|
class VerticalLiftShuttleManualBarcode(models.TransientModel):
|
|
_name = "vertical.lift.shuttle.manual.barcode"
|
|
_description = "Action to input a barcode"
|
|
|
|
barcode = fields.Char(string="Barcode")
|
|
|
|
def button_save(self):
|
|
active_id = self.env.context.get("active_id")
|
|
model = self.env.context.get("active_model")
|
|
record = self.env[model].browse(active_id).exists()
|
|
if not record:
|
|
return
|
|
if self.barcode:
|
|
record.on_barcode_scanned(self.barcode)
|