From cbd73acaeeafd9e058469ad79a5c94f8eefa84a3 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 11 Jun 2019 16:50:48 +0200 Subject: [PATCH 01/19] Add stock_vertical_lift module --- stock_vertical_lift_kardex/__init__.py | 1 + stock_vertical_lift_kardex/__manifest__.py | 17 +++++++++++++++++ stock_vertical_lift_kardex/models/__init__.py | 1 + .../models/vertical_lift_shuttle.py | 14 ++++++++++++++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 2 ++ stock_vertical_lift_kardex/readme/ROADMAP.rst | 1 + 7 files changed, 37 insertions(+) create mode 100644 stock_vertical_lift_kardex/__init__.py create mode 100644 stock_vertical_lift_kardex/__manifest__.py create mode 100644 stock_vertical_lift_kardex/models/__init__.py create mode 100644 stock_vertical_lift_kardex/models/vertical_lift_shuttle.py create mode 100644 stock_vertical_lift_kardex/readme/CONTRIBUTORS.rst create mode 100644 stock_vertical_lift_kardex/readme/DESCRIPTION.rst create mode 100644 stock_vertical_lift_kardex/readme/ROADMAP.rst diff --git a/stock_vertical_lift_kardex/__init__.py b/stock_vertical_lift_kardex/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/stock_vertical_lift_kardex/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_vertical_lift_kardex/__manifest__.py b/stock_vertical_lift_kardex/__manifest__.py new file mode 100644 index 000000000..35670055d --- /dev/null +++ b/stock_vertical_lift_kardex/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Vertical Lift - Kardex', + 'summary': 'Integrate with Kardex Remstar Vertical Lifts', + 'version': '12.0.1.0.0', + 'category': 'Stock', + 'author': 'Camptocamp, Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'depends': [ + 'stock_vertical_lift', + ], + 'website': 'https://www.camptocamp.com', + 'data': [], + 'installable': True, + 'development_status': 'Alpha', +} diff --git a/stock_vertical_lift_kardex/models/__init__.py b/stock_vertical_lift_kardex/models/__init__.py new file mode 100644 index 000000000..e99db92f6 --- /dev/null +++ b/stock_vertical_lift_kardex/models/__init__.py @@ -0,0 +1 @@ +from . import vertical_lift_shuttle diff --git a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py new file mode 100644 index 000000000..cfd3c74b9 --- /dev/null +++ b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py @@ -0,0 +1,14 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class VerticalLiftShuttle(models.Model): + _inherit = 'vertical.lift.shuttle' + + @api.model + def _selection_hardware(self): + values = super()._selection_hardware() + values += [('kardex', 'Kardex')] + return values diff --git a/stock_vertical_lift_kardex/readme/CONTRIBUTORS.rst b/stock_vertical_lift_kardex/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..48286263c --- /dev/null +++ b/stock_vertical_lift_kardex/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Guewen Baconnier diff --git a/stock_vertical_lift_kardex/readme/DESCRIPTION.rst b/stock_vertical_lift_kardex/readme/DESCRIPTION.rst new file mode 100644 index 000000000..c4b64e9b0 --- /dev/null +++ b/stock_vertical_lift_kardex/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +Add support for Kardex Remstar vertical lifts to the Vertical Lift +module. diff --git a/stock_vertical_lift_kardex/readme/ROADMAP.rst b/stock_vertical_lift_kardex/readme/ROADMAP.rst new file mode 100644 index 000000000..3d8aefd2e --- /dev/null +++ b/stock_vertical_lift_kardex/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Add support of the hardware From 24d41819d320dbd41ba96d998fd3adde0b1393d7 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 8 Oct 2019 17:07:20 +0200 Subject: [PATCH 02/19] Add method on location to fetch a tray * Add vertical_lift_shuttle_id field on stock.location, help to find the shuttle for a location * Add StockLocation.fetch_vertical_lift_tray(), that needs to be implemented in addons to send commands to the hardward to fetch a tray, and if existing show a cell (laser pointer, ...) * Add helpers on stock.move.line fetch_vertical_lift_tray_source() and fetch_vertical_lift_tray_dest() that fetch the tray directly from a move line's source or destination location --- stock_vertical_lift_kardex/models/__init__.py | 1 + .../models/stock_location.py | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 stock_vertical_lift_kardex/models/stock_location.py diff --git a/stock_vertical_lift_kardex/models/__init__.py b/stock_vertical_lift_kardex/models/__init__.py index e99db92f6..51a3830f6 100644 --- a/stock_vertical_lift_kardex/models/__init__.py +++ b/stock_vertical_lift_kardex/models/__init__.py @@ -1 +1,2 @@ +from . import stock_location from . import vertical_lift_shuttle diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py new file mode 100644 index 000000000..d1098d59a --- /dev/null +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -0,0 +1,56 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo import models + +_logger = logging.getLogger(__name__) + + +class StockLocation(models.Model): + _inherit = 'stock.location' + + def _hardware_kardex_prepare_payload(self, cell_location=None): + return "" + + def _hardware_vertical_lift_tray(self, cell_location=None): + """Send instructions 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.vertical_lift_shuttle_id.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 fetch / open + the tray. If a ``cell_location`` is passed and if the hardware supports + a way to show a cell (such as a laser pointer), it should send this + command as well. + + Useful information that could be needed for the drivers: + + * Any field of `self` (name, barcode, ...) which is the current tray. + * Any field of `cell_location` (name, barcode, ...) which is the cell + in the tray. + * ``self.vertical_lift_shuttle_id`` is the current Shuttle, where we + find details about the hardware, the current mode (pick, put, ...). + * ``self.tray_type_id`` is the kind of tray. + * ``self.tray_type_id.width_per_cell`` and + ``self.tray_type_id.depth_per_cell`` return the size of a cell in mm. + * ``cell_location.posx`` and ``posy`` are the coordinate from the + bottom-left of the tray. + * ``cell_location.tray_cell_center_position()`` returns the central + position of the cell in mm from the bottom-left of a tray. (distance + from left, distance from bottom). Can be used for instance for + highlighting the cell using a laser pointer. + """ + if self.vertical_lift_shuttle_id.hardware == "kardex": + payload = self._hardware_kardex_prepare_payload() + _logger.debug("Sending to kardex: {}", payload) + # TODO implement the communication with kardex + super()._hardware_vertical_lift_tray(cell_location=cell_location) From 18c6aa1dd33118ae273c112919cdd3f40fa7864b Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 4 Nov 2019 15:03:03 +0100 Subject: [PATCH 03/19] [IMP] abstract communication with shuttle --- .../models/stock_location.py | 38 +++++++++++++++++-- .../models/vertical_lift_shuttle.py | 17 +++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index d1098d59a..e307aa21c 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -11,10 +11,40 @@ class StockLocation(models.Model): _inherit = 'stock.location' def _hardware_kardex_prepare_payload(self, cell_location=None): - return "" + message_template = ("{code}|{hostId}|{addr}|{carrier}|{carrierNext}|" + "{x}|{y}|{boxType}|{Q}|{order}|{part}|{desc}|\r\n") + shuttle = self.vertical_lift_shuttle_id + if shuttle.mode == "pick": + code = "1" + elif shuttle.mode == "put": + code = "2" + elif shuttle.mode == "inventory": + code = "5" + else: + code = "61" # ping + if cell_location: + x, y = cell_location.tray_cell_center_position() + else: + x, y = '', '' + subst = { + 'code': code, + 'hostId': 'odoo', + 'addr': shuttle.name, + 'carrier': self.name, + 'carrierNext': '', + 'x': x, + 'y': y, + 'boxType': '', + 'Q': '', + 'order': '', + 'part': '', + 'desc': '', + } + payload = message_template.format(subst) + return payload.encode('iso-8859-1', 'replace') - def _hardware_vertical_lift_tray(self, cell_location=None): - """Send instructions to the vertical lift hardware + def _hardware_vertical_lift_tray_payload(self, cell_location=None): + """Prepare the 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 @@ -53,4 +83,4 @@ class StockLocation(models.Model): payload = self._hardware_kardex_prepare_payload() _logger.debug("Sending to kardex: {}", payload) # TODO implement the communication with kardex - super()._hardware_vertical_lift_tray(cell_location=cell_location) + super()._hardware_vertical_lift_tray_payload(cell_location=cell_location) diff --git a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py index cfd3c74b9..67dc1131d 100644 --- a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py @@ -12,3 +12,20 @@ class VerticalLiftShuttle(models.Model): values = super()._selection_hardware() values += [('kardex', 'Kardex')] return values + + def _hardware_recv_response(self, conn): + # the implementation uses messages delimited with \r\n + response = b'' + chunk = True + while chunk: + chunk = conn.recv(1) + response += chunk + if response.endswith(b'\r\n'): + break + return response + + def _check_server_response(self, payload, response): + payload = payload.decode('iso-8859-1') + response = response.decode('iso-8859-1') + code, sep, remaining = response.partition('|') + return code == "0" From ebcd872bb9e8369c1c8bc91d2e73f69148b7d10d Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Thu, 14 Nov 2019 10:57:42 +0100 Subject: [PATCH 04/19] [IMP] add a proxy to communicate with the kardex server --- stock_vertical_lift_kardex/__manifest__.py | 3 +- stock_vertical_lift_kardex/models/__init__.py | 1 + .../models/stock_location.py | 12 +- .../models/vertical_lift_shuttle.py | 62 +++++-- .../proxy/kardex-proxy.py | 175 ++++++++++++++++++ .../proxy/requirements.txt | 1 + stock_vertical_lift_kardex/proxy/test.py | 102 ++++++++++ stock_vertical_lift_kardex/requirements.txt | 1 + 8 files changed, 336 insertions(+), 21 deletions(-) create mode 100644 stock_vertical_lift_kardex/proxy/kardex-proxy.py create mode 100644 stock_vertical_lift_kardex/proxy/requirements.txt create mode 100644 stock_vertical_lift_kardex/proxy/test.py create mode 100644 stock_vertical_lift_kardex/requirements.txt diff --git a/stock_vertical_lift_kardex/__manifest__.py b/stock_vertical_lift_kardex/__manifest__.py index 35670055d..8ccd05222 100644 --- a/stock_vertical_lift_kardex/__manifest__.py +++ b/stock_vertical_lift_kardex/__manifest__.py @@ -11,7 +11,8 @@ 'stock_vertical_lift', ], 'website': 'https://www.camptocamp.com', - 'data': [], + 'data': [ + ], 'installable': True, 'development_status': 'Alpha', } diff --git a/stock_vertical_lift_kardex/models/__init__.py b/stock_vertical_lift_kardex/models/__init__.py index 51a3830f6..5a191bac8 100644 --- a/stock_vertical_lift_kardex/models/__init__.py +++ b/stock_vertical_lift_kardex/models/__init__.py @@ -1,2 +1,3 @@ from . import stock_location from . import vertical_lift_shuttle + diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index e307aa21c..39d66fc28 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -28,10 +28,10 @@ class StockLocation(models.Model): x, y = '', '' subst = { 'code': code, - 'hostId': 'odoo', + 'hostId': self.env['ir.sequence'].next_by_code('vertical.lift.command'), 'addr': shuttle.name, - 'carrier': self.name, - 'carrierNext': '', + 'carrier': self.level, + 'carrierNext': '0', 'x': x, 'y': y, 'boxType': '', @@ -40,7 +40,7 @@ class StockLocation(models.Model): 'part': '', 'desc': '', } - payload = message_template.format(subst) + payload = message_template.format(**subst) return payload.encode('iso-8859-1', 'replace') def _hardware_vertical_lift_tray_payload(self, cell_location=None): @@ -83,4 +83,6 @@ class StockLocation(models.Model): payload = self._hardware_kardex_prepare_payload() _logger.debug("Sending to kardex: {}", payload) # TODO implement the communication with kardex - super()._hardware_vertical_lift_tray_payload(cell_location=cell_location) + else: + payload = super()._hardware_vertical_lift_tray_payload(cell_location=cell_location) + return payload diff --git a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py index 67dc1131d..256439d35 100644 --- a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py @@ -4,6 +4,24 @@ from odoo import api, models +JMIF_STATUS = { + 0: 'success', + 101: 'common error', + 102: 'sequence number invalid', + 103: 'machine busy', + 104: 'timeout', + 105: 'max retry reached', + 106: 'carrier in use or undefined', + 107: 'cancelled', + 108: 'invalid user input data', + 201: 'request accepted and queued', + 202: 'request processing started / request active', + 203: 'carrier arrived, maybe overwritten by code 0', + 301: 'AO occupied with other try on move back (store / put)', + 302: 'AO occupied with other try on fetch (pick)', +} + + class VerticalLiftShuttle(models.Model): _inherit = 'vertical.lift.shuttle' @@ -13,19 +31,33 @@ class VerticalLiftShuttle(models.Model): values += [('kardex', 'Kardex')] return values - def _hardware_recv_response(self, conn): - # the implementation uses messages delimited with \r\n - response = b'' - chunk = True - while chunk: - chunk = conn.recv(1) - response += chunk - if response.endswith(b'\r\n'): - break - return response - - def _check_server_response(self, payload, response): - payload = payload.decode('iso-8859-1') - response = response.decode('iso-8859-1') + def _check_server_response(self, command): + response = command.answer code, sep, remaining = response.partition('|') - return code == "0" + code = int(code) + if code == 0: + return True + elif 1 <= code <= 99: + command.error = 'interface error %d' % code + return False + elif code in JMIF_STATUS and code < 200: + command.error = '%d: %s' % (code, JMIF_STATUS[code]) + return False + elif code in JMIF_STATUS and code < 300: + command.error = '%d: %s' % (code, JMIF_STATUS[code]) + return True + elif code in JMIF_STATUS: + command.error = '%d: %s' % (code, JMIF_STATUS[code]) + elif 501 <= code <= 999: + command.error = '%d: %s' % (code, 'MM260 Error') + elif 1000 <= code <= 32767: + command.error = '%d: %s' % ( + code, 'C2000TCP/C3000CGI machine error' + ) + elif 0xFF0 <= code == 0xFFF: + command.error = '%x: %s' % ( + code, 'C3000CGI machine error (global short)' + ) + elif 0xFFF < code: + command.error = '%x: %s' % (code, 'C3000CGI machine error (long)') + return False diff --git a/stock_vertical_lift_kardex/proxy/kardex-proxy.py b/stock_vertical_lift_kardex/proxy/kardex-proxy.py new file mode 100644 index 000000000..f568c5a91 --- /dev/null +++ b/stock_vertical_lift_kardex/proxy/kardex-proxy.py @@ -0,0 +1,175 @@ +#!/usr/bin/python3 +import argparse +import asyncio +import logging +import os +import ssl +import time + +import aiohttp + +_logger = logging.getLogger(__name__) + + +class KardexProxyProtocol(asyncio.Protocol): + def __init__(self, loop, queue, args): + _logger.info("Proxy created") + self.transport = None + self.buffer = b"" + self.queue = queue + self.loop = loop + self.args = args + + def connection_made(self, transport): + _logger.info("Proxy incoming cnx") + self.transport = transport + self.buffer = b"" + + def data_received(self, data): + self.buffer += data + _logger.info("Proxy: received %s", data) + if len(self.buffer) > 65535: + # prevent buffer overflow + self.transport.close() + + def eof_received(self): + _logger.info("Proxy: received EOF") + if self.buffer[-1] != b"\n": + # bad format -> close + self.transport.close() + data = ( + self.buffer.replace(b"\r\n", b"\n") + .replace(b"\n", b"\r\n") + .decode("iso-8859-1", "replace") + ) + self.loop.create_task(self.queue.put(data)) + self.buffer = b"" + + def connection_lost(self, exc): + self.transport = None + self.buffer = b"" + + +class KardexClientProtocol(asyncio.Protocol): + def __init__(self, loop, queue, args): + _logger.info("started kardex client") + self.loop = loop + self.queue = queue + self.args = args + self.transport = None + self.buffer = b"" + + def connection_made(self, transport): + self.transport = transport + _logger.info("connected to kardex server %r", transport) + + async def keepalive(self): + while True: + t = int(time.time()) + msg = "61|ping%d|SH1-1|0|0||||||||\r\n" % t + await self.send_message(msg) + await asyncio.sleep(20) + + async def send_message(self, message): + _logger.info("SEND %r", message) + message = message.encode("iso-8859-1") + self.transport.write(message) + + async def process_queue(self): + while True: + message = await self.queue.get() + await self.send_message(message) + + def data_received(self, data): + data = data.replace(b"\0", b"") + _logger.info("RECV %s", data) + self.buffer += data + if b"\r\n" in self.buffer: + msg, sep, rem = self.buffer.partition(b"\r\n") + self.buffer = rem + msg = msg.decode('iso-8859-1', 'replace').strip() + if msg.startswith('0|ping'): + _logger.info('ping ok') + else: + _logger.info('notify odoo: %s', msg) + self.loop.create_task(self.notify_odoo(msg)) + + def connection_lost(self, exc): + self.loop.stop() + + async def notify_odoo(self, msg): + url = self.args.odoo_url + "/vertical-lift" + async with aiohttp.ClientSession() as session: + params = {'answer': msg, 'secret': self.args.secret} + async with session.post(url, data=params) as resp: + resp_text = await resp.text() + _logger.info( + 'Reponse from Odoo: %s %s', resp.status, resp_text + ) + + +def main(args, ssl_context=None): + logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" + ) + loop = asyncio.get_event_loop() + queue = asyncio.Queue(loop=loop) + # create the main server + coro = loop.create_server( + lambda: KardexProxyProtocol(loop, queue, args), + host=args.host, + port=args.port + ) + loop.run_until_complete(coro) + + # create the connection to the JMIF client + if args.kardex_use_tls: + if ssl_context is None: + ssl_context = ssl.create_default_context() + else: + ssl_context = None + coro = loop.create_connection( + lambda: KardexClientProtocol(loop, queue, args), + host=args.kardex_host, + port=args.kardex_port, + ssl=ssl_context, + ) + transport, client = loop.run_until_complete(coro) + loop.create_task(client.keepalive()) + loop.create_task(client.process_queue()) + loop.run_forever() + loop.close() + + +def make_parser(): + listen_address = os.environ.get("INTERFACE", "0.0.0.0") + listen_port = int(os.environ.get("PORT", "7654")) + secret = os.environ.get("ODOO_CALLBACK_SECRET", "") + odoo_url = os.environ.get("ODOO_URL", "http://localhost:8069") + odoo_db = os.environ.get("ODOO_DB", "odoodb") + kardex_host = os.environ.get("KARDEX_HOST", "kardex") + kardex_port = int(os.environ.get("KARDEX_PORT", "9600")) + kardex_use_tls = ( + False + if os.environ.get("KARDEX_TLS", "") in ("", "0", "False", "FALSE") + else True + ) + parser = argparse.ArgumentParser() + arguments = [ + ("--host", listen_address, str), + ("--port", listen_port, int), + ("--odoo-url", odoo_url, str), + ("--odoo-db", odoo_db, str), + ("--secret", secret, str), + ("--kardex-host", kardex_host, str), + ("--kardex-port", kardex_port, str), + ("--kardex-use-tls", kardex_use_tls, bool), + ] + for name, default, type_ in arguments: + parser.add_argument(name, default=default, action="store", type=type_) + return parser + +if __name__ == "__main__": + parser = make_parser() + args = parser.parse_args() + main(args) diff --git a/stock_vertical_lift_kardex/proxy/requirements.txt b/stock_vertical_lift_kardex/proxy/requirements.txt new file mode 100644 index 000000000..ee4ba4f3d --- /dev/null +++ b/stock_vertical_lift_kardex/proxy/requirements.txt @@ -0,0 +1 @@ +aiohttp diff --git a/stock_vertical_lift_kardex/proxy/test.py b/stock_vertical_lift_kardex/proxy/test.py new file mode 100644 index 000000000..5b6ad5188 --- /dev/null +++ b/stock_vertical_lift_kardex/proxy/test.py @@ -0,0 +1,102 @@ +import socket +import asyncio +import logging +import time + +_logger = logging.getLogger('kardex.proxy') +logging.basicConfig(level=logging.DEBUG) + + +class KardexProxyProtocol(asyncio.Protocol): + def __init__(self, loop, queue): + _logger.info('Proxy created') + self.transport = None + self.buffer = b'' + self.queue = queue + self.loop = loop + + def connection_made(self, transport): + _logger.info('Proxy incoming cnx') + self.transport = transport + self.buffer = b'' + + def data_received(self, data): + self.buffer += data + _logger.info('Proxy: received %s', data) + if len(self.buffer) > 65535: + # prevent buffer overflow + self.transport.close() + + def eof_received(self): + _logger.info('Proxy: received EOF') + if self.buffer[-1] != b'\n': + # bad format -> close + self.transport.close() + data = self.buffer.replace(b'\r\n', b'\n').replace(b'\n', b'\r\n').decode('iso-8859-1', 'replace') + task = self.loop.create_task(self.queue.put(data)) + self.buffer = b'' + print('toto', task) + + def connection_lost(self, exc): + self.transport = None + self.buffer = b'' + + +class KardexClientProtocol(asyncio.Protocol): + def __init__(self, loop, queue): + _logger.info('started kardex client') + self.loop = loop + self.queue = queue + self.transport = None + self.buffer = b'' + + def connection_made(self, transport): + self.transport = transport + _logger.info('connected to kardex server %r', transport) + + async def keepalive(self): + while True: + t = int(time.time()) + msg = '61|ping%d|SH1-1|0|0||||||||\r\n' % t + await self.send_message(msg) + await asyncio.sleep(5) + + async def send_message(self, message): + _logger.info('SEND %s', message) + message = message.encode('iso-8859-1').ljust(1024, b'\0') + self.transport.write(message) + + async def process_queue(self): + while True: + message = await self.queue.get() + await self.send_message(message) + + def data_received(self, data): + data = data.replace(b'\0', b'') + _logger.info('RECV %s', data) + self.buffer += data + + def connection_lost(self, exc): + self.loop.stop() + + +if __name__ == '__main__': + _logger.info('starting') + loop = asyncio.get_event_loop() + loop.set_debug(1) + queue = asyncio.Queue(loop=loop) + coro = loop.create_server( + lambda: KardexProxyProtocol(loop, queue), + port=3000, + family=socket.AF_INET + ) + server = loop.run_until_complete(coro) + coro = loop.create_connection(lambda: KardexClientProtocol(loop, queue), + 'localhost', 9600) + transport, client = loop.run_until_complete(coro) + print('%r' % transport) + loop.create_task(client.keepalive()) + loop.create_task(client.process_queue()) + _logger.info('run loop') + loop.run_forever() + loop.close() diff --git a/stock_vertical_lift_kardex/requirements.txt b/stock_vertical_lift_kardex/requirements.txt new file mode 100644 index 000000000..ee4ba4f3d --- /dev/null +++ b/stock_vertical_lift_kardex/requirements.txt @@ -0,0 +1 @@ +aiohttp From f27c4d0de5d4f5fc9c62cd5a54913211543ddbc8 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 16 Dec 2019 14:22:45 +0100 Subject: [PATCH 05/19] [IMP] stock_vertical_lift: black, isort --- stock_vertical_lift_kardex/README.rst | 84 ++++ stock_vertical_lift_kardex/__manifest__.py | 25 +- stock_vertical_lift_kardex/models/__init__.py | 1 - .../models/stock_location.py | 41 +- .../models/vertical_lift_shuttle.py | 55 ++- .../proxy/kardex-proxy.py | 21 +- stock_vertical_lift_kardex/proxy/test.py | 64 +-- .../static/description/index.html | 433 ++++++++++++++++++ 8 files changed, 620 insertions(+), 104 deletions(-) create mode 100644 stock_vertical_lift_kardex/README.rst create mode 100644 stock_vertical_lift_kardex/static/description/index.html diff --git a/stock_vertical_lift_kardex/README.rst b/stock_vertical_lift_kardex/README.rst new file mode 100644 index 000000000..8d4a3f730 --- /dev/null +++ b/stock_vertical_lift_kardex/README.rst @@ -0,0 +1,84 @@ +====================== +Vertical Lift - Kardex +====================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_vertical_lift_kardex + :alt: OCA/stock-logistics-warehouse +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-13-0/stock-logistics-warehouse-13-0-stock_vertical_lift_kardex + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/153/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Add support for Kardex Remstar vertical lifts to the Vertical Lift +module. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Known issues / Roadmap +====================== + +* Add support of the hardware + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Guewen Baconnier + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/stock-logistics-warehouse `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_vertical_lift_kardex/__manifest__.py b/stock_vertical_lift_kardex/__manifest__.py index 8ccd05222..e973ec0b5 100644 --- a/stock_vertical_lift_kardex/__manifest__.py +++ b/stock_vertical_lift_kardex/__manifest__.py @@ -1,18 +1,15 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { - 'name': 'Vertical Lift - Kardex', - 'summary': 'Integrate with Kardex Remstar Vertical Lifts', - 'version': '12.0.1.0.0', - 'category': 'Stock', - 'author': 'Camptocamp, Odoo Community Association (OCA)', - 'license': 'AGPL-3', - 'depends': [ - 'stock_vertical_lift', - ], - 'website': 'https://www.camptocamp.com', - 'data': [ - ], - 'installable': True, - 'development_status': 'Alpha', + "name": "Vertical Lift - Kardex", + "summary": "Integrate with Kardex Remstar Vertical Lifts", + "version": "12.0.1.0.0", + "category": "Stock", + "author": "Camptocamp, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["stock_vertical_lift"], + "website": "https://www.camptocamp.com", + "data": [], + "installable": True, + "development_status": "Alpha", } diff --git a/stock_vertical_lift_kardex/models/__init__.py b/stock_vertical_lift_kardex/models/__init__.py index 5a191bac8..51a3830f6 100644 --- a/stock_vertical_lift_kardex/models/__init__.py +++ b/stock_vertical_lift_kardex/models/__init__.py @@ -1,3 +1,2 @@ from . import stock_location from . import vertical_lift_shuttle - diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index 39d66fc28..8b3cd73c2 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -2,17 +2,20 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging + from odoo import models _logger = logging.getLogger(__name__) class StockLocation(models.Model): - _inherit = 'stock.location' + _inherit = "stock.location" def _hardware_kardex_prepare_payload(self, cell_location=None): - message_template = ("{code}|{hostId}|{addr}|{carrier}|{carrierNext}|" - "{x}|{y}|{boxType}|{Q}|{order}|{part}|{desc}|\r\n") + message_template = ( + "{code}|{hostId}|{addr}|{carrier}|{carrierNext}|" + "{x}|{y}|{boxType}|{Q}|{order}|{part}|{desc}|\r\n" + ) shuttle = self.vertical_lift_shuttle_id if shuttle.mode == "pick": code = "1" @@ -25,23 +28,23 @@ class StockLocation(models.Model): if cell_location: x, y = cell_location.tray_cell_center_position() else: - x, y = '', '' + x, y = "", "" subst = { - 'code': code, - 'hostId': self.env['ir.sequence'].next_by_code('vertical.lift.command'), - 'addr': shuttle.name, - 'carrier': self.level, - 'carrierNext': '0', - 'x': x, - 'y': y, - 'boxType': '', - 'Q': '', - 'order': '', - 'part': '', - 'desc': '', + "code": code, + "hostId": self.env["ir.sequence"].next_by_code("vertical.lift.command"), + "addr": shuttle.name, + "carrier": self.level, + "carrierNext": "0", + "x": x, + "y": y, + "boxType": "", + "Q": "", + "order": "", + "part": "", + "desc": "", } payload = message_template.format(**subst) - return payload.encode('iso-8859-1', 'replace') + return payload.encode("iso-8859-1", "replace") def _hardware_vertical_lift_tray_payload(self, cell_location=None): """Prepare the message to be sent to the vertical lift hardware @@ -84,5 +87,7 @@ class StockLocation(models.Model): _logger.debug("Sending to kardex: {}", payload) # TODO implement the communication with kardex else: - payload = super()._hardware_vertical_lift_tray_payload(cell_location=cell_location) + payload = super()._hardware_vertical_lift_tray_payload( + cell_location=cell_location + ) return payload diff --git a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py index 256439d35..54e046dfd 100644 --- a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py @@ -3,61 +3,58 @@ from odoo import api, models - JMIF_STATUS = { - 0: 'success', - 101: 'common error', - 102: 'sequence number invalid', - 103: 'machine busy', - 104: 'timeout', - 105: 'max retry reached', - 106: 'carrier in use or undefined', - 107: 'cancelled', - 108: 'invalid user input data', - 201: 'request accepted and queued', - 202: 'request processing started / request active', - 203: 'carrier arrived, maybe overwritten by code 0', - 301: 'AO occupied with other try on move back (store / put)', - 302: 'AO occupied with other try on fetch (pick)', + 0: "success", + 101: "common error", + 102: "sequence number invalid", + 103: "machine busy", + 104: "timeout", + 105: "max retry reached", + 106: "carrier in use or undefined", + 107: "cancelled", + 108: "invalid user input data", + 201: "request accepted and queued", + 202: "request processing started / request active", + 203: "carrier arrived, maybe overwritten by code 0", + 301: "AO occupied with other try on move back (store / put)", + 302: "AO occupied with other try on fetch (pick)", } class VerticalLiftShuttle(models.Model): - _inherit = 'vertical.lift.shuttle' + _inherit = "vertical.lift.shuttle" @api.model def _selection_hardware(self): values = super()._selection_hardware() - values += [('kardex', 'Kardex')] + values += [("kardex", "Kardex")] return values def _check_server_response(self, command): response = command.answer - code, sep, remaining = response.partition('|') + code, sep, remaining = response.partition("|") code = int(code) if code == 0: return True elif 1 <= code <= 99: - command.error = 'interface error %d' % code + command.error = "interface error %d" % code return False elif code in JMIF_STATUS and code < 200: - command.error = '%d: %s' % (code, JMIF_STATUS[code]) + command.error = "%d: %s" % (code, JMIF_STATUS[code]) return False elif code in JMIF_STATUS and code < 300: - command.error = '%d: %s' % (code, JMIF_STATUS[code]) + command.error = "%d: %s" % (code, JMIF_STATUS[code]) return True elif code in JMIF_STATUS: - command.error = '%d: %s' % (code, JMIF_STATUS[code]) + command.error = "%d: %s" % (code, JMIF_STATUS[code]) elif 501 <= code <= 999: - command.error = '%d: %s' % (code, 'MM260 Error') + command.error = "%d: %s" % (code, "MM260 Error") elif 1000 <= code <= 32767: - command.error = '%d: %s' % ( - code, 'C2000TCP/C3000CGI machine error' - ) + command.error = "%d: %s" % (code, "C2000TCP/C3000CGI machine error") elif 0xFF0 <= code == 0xFFF: - command.error = '%x: %s' % ( - code, 'C3000CGI machine error (global short)' + command.error = "{:x}: {}".format( + code, "C3000CGI machine error (global short)" ) elif 0xFFF < code: - command.error = '%x: %s' % (code, 'C3000CGI machine error (long)') + command.error = "{:x}: {}".format(code, "C3000CGI machine error (long)") return False diff --git a/stock_vertical_lift_kardex/proxy/kardex-proxy.py b/stock_vertical_lift_kardex/proxy/kardex-proxy.py index f568c5a91..97607999d 100644 --- a/stock_vertical_lift_kardex/proxy/kardex-proxy.py +++ b/stock_vertical_lift_kardex/proxy/kardex-proxy.py @@ -87,11 +87,11 @@ class KardexClientProtocol(asyncio.Protocol): if b"\r\n" in self.buffer: msg, sep, rem = self.buffer.partition(b"\r\n") self.buffer = rem - msg = msg.decode('iso-8859-1', 'replace').strip() - if msg.startswith('0|ping'): - _logger.info('ping ok') + msg = msg.decode("iso-8859-1", "replace").strip() + if msg.startswith("0|ping"): + _logger.info("ping ok") else: - _logger.info('notify odoo: %s', msg) + _logger.info("notify odoo: %s", msg) self.loop.create_task(self.notify_odoo(msg)) def connection_lost(self, exc): @@ -100,12 +100,10 @@ class KardexClientProtocol(asyncio.Protocol): async def notify_odoo(self, msg): url = self.args.odoo_url + "/vertical-lift" async with aiohttp.ClientSession() as session: - params = {'answer': msg, 'secret': self.args.secret} + params = {"answer": msg, "secret": self.args.secret} async with session.post(url, data=params) as resp: resp_text = await resp.text() - _logger.info( - 'Reponse from Odoo: %s %s', resp.status, resp_text - ) + _logger.info("Reponse from Odoo: %s %s", resp.status, resp_text) def main(args, ssl_context=None): @@ -116,9 +114,7 @@ def main(args, ssl_context=None): queue = asyncio.Queue(loop=loop) # create the main server coro = loop.create_server( - lambda: KardexProxyProtocol(loop, queue, args), - host=args.host, - port=args.port + lambda: KardexProxyProtocol(loop, queue, args), host=args.host, port=args.port ) loop.run_until_complete(coro) @@ -160,7 +156,7 @@ def make_parser(): ("--port", listen_port, int), ("--odoo-url", odoo_url, str), ("--odoo-db", odoo_db, str), - ("--secret", secret, str), + ("--secret", secret, str), ("--kardex-host", kardex_host, str), ("--kardex-port", kardex_port, str), ("--kardex-use-tls", kardex_use_tls, bool), @@ -169,6 +165,7 @@ def make_parser(): parser.add_argument(name, default=default, action="store", type=type_) return parser + if __name__ == "__main__": parser = make_parser() args = parser.parse_args() diff --git a/stock_vertical_lift_kardex/proxy/test.py b/stock_vertical_lift_kardex/proxy/test.py index 5b6ad5188..a7899f21a 100644 --- a/stock_vertical_lift_kardex/proxy/test.py +++ b/stock_vertical_lift_kardex/proxy/test.py @@ -1,69 +1,74 @@ -import socket +# pylint: disable=W8116 import asyncio import logging +import socket import time -_logger = logging.getLogger('kardex.proxy') +_logger = logging.getLogger("kardex.proxy") logging.basicConfig(level=logging.DEBUG) class KardexProxyProtocol(asyncio.Protocol): def __init__(self, loop, queue): - _logger.info('Proxy created') + _logger.info("Proxy created") self.transport = None - self.buffer = b'' + self.buffer = b"" self.queue = queue self.loop = loop def connection_made(self, transport): - _logger.info('Proxy incoming cnx') + _logger.info("Proxy incoming cnx") self.transport = transport - self.buffer = b'' + self.buffer = b"" def data_received(self, data): self.buffer += data - _logger.info('Proxy: received %s', data) + _logger.info("Proxy: received %s", data) if len(self.buffer) > 65535: # prevent buffer overflow self.transport.close() def eof_received(self): - _logger.info('Proxy: received EOF') - if self.buffer[-1] != b'\n': + _logger.info("Proxy: received EOF") + if self.buffer[-1] != b"\n": # bad format -> close self.transport.close() - data = self.buffer.replace(b'\r\n', b'\n').replace(b'\n', b'\r\n').decode('iso-8859-1', 'replace') + data = ( + self.buffer.replace(b"\r\n", b"\n") + .replace(b"\n", b"\r\n") + .decode("iso-8859-1", "replace") + ) task = self.loop.create_task(self.queue.put(data)) - self.buffer = b'' - print('toto', task) + self.buffer = b"" + print("toto", task) def connection_lost(self, exc): self.transport = None - self.buffer = b'' + self.buffer = b"" class KardexClientProtocol(asyncio.Protocol): def __init__(self, loop, queue): - _logger.info('started kardex client') + _logger.info("started kardex client") self.loop = loop self.queue = queue self.transport = None - self.buffer = b'' + self.buffer = b"" def connection_made(self, transport): self.transport = transport - _logger.info('connected to kardex server %r', transport) + _logger.info("connected to kardex server %r", transport) async def keepalive(self): while True: t = int(time.time()) - msg = '61|ping%d|SH1-1|0|0||||||||\r\n' % t + msg = "61|ping%d|SH1-1|0|0||||||||\r\n" % t await self.send_message(msg) await asyncio.sleep(5) async def send_message(self, message): - _logger.info('SEND %s', message) - message = message.encode('iso-8859-1').ljust(1024, b'\0') + _logger.info("SEND %s", message) + message = message.encode("iso-8859-1").ljust(1024, b"\0") self.transport.write(message) async def process_queue(self): @@ -72,31 +77,30 @@ class KardexClientProtocol(asyncio.Protocol): await self.send_message(message) def data_received(self, data): - data = data.replace(b'\0', b'') - _logger.info('RECV %s', data) + data = data.replace(b"\0", b"") + _logger.info("RECV %s", data) self.buffer += data def connection_lost(self, exc): self.loop.stop() -if __name__ == '__main__': - _logger.info('starting') +if __name__ == "__main__": + _logger.info("starting") loop = asyncio.get_event_loop() loop.set_debug(1) queue = asyncio.Queue(loop=loop) coro = loop.create_server( - lambda: KardexProxyProtocol(loop, queue), - port=3000, - family=socket.AF_INET + lambda: KardexProxyProtocol(loop, queue), port=3000, family=socket.AF_INET ) server = loop.run_until_complete(coro) - coro = loop.create_connection(lambda: KardexClientProtocol(loop, queue), - 'localhost', 9600) + coro = loop.create_connection( + lambda: KardexClientProtocol(loop, queue), "localhost", 9600 + ) transport, client = loop.run_until_complete(coro) - print('%r' % transport) + print("%r" % transport) loop.create_task(client.keepalive()) loop.create_task(client.process_queue()) - _logger.info('run loop') + _logger.info("run loop") loop.run_forever() loop.close() diff --git a/stock_vertical_lift_kardex/static/description/index.html b/stock_vertical_lift_kardex/static/description/index.html new file mode 100644 index 000000000..ae2800eba --- /dev/null +++ b/stock_vertical_lift_kardex/static/description/index.html @@ -0,0 +1,433 @@ + + + + + + +Vertical Lift - Kardex + + + +
+

Vertical Lift - Kardex

+ + +

Alpha License: AGPL-3 OCA/stock-logistics-warehouse Translate me on Weblate Try me on Runbot

+

Add support for Kardex Remstar vertical lifts to the Vertical Lift +module.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Known issues / Roadmap

+
    +
  • Add support of the hardware
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/stock-logistics-warehouse project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From 004eaef296b20f2b0092239b240c9565ce0b5a0b Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 16 Dec 2019 15:52:03 +0100 Subject: [PATCH 06/19] [MIG] stock_vertical_lift{,_kardex}: Migration to 13.0 --- stock_vertical_lift_kardex/__manifest__.py | 2 +- stock_vertical_lift_kardex/models/vertical_lift_shuttle.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/stock_vertical_lift_kardex/__manifest__.py b/stock_vertical_lift_kardex/__manifest__.py index e973ec0b5..4a1e76a8d 100644 --- a/stock_vertical_lift_kardex/__manifest__.py +++ b/stock_vertical_lift_kardex/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Vertical Lift - Kardex", "summary": "Integrate with Kardex Remstar Vertical Lifts", - "version": "12.0.1.0.0", + "version": "13.0.1.0.0", "category": "Stock", "author": "Camptocamp, Odoo Community Association (OCA)", "license": "AGPL-3", diff --git a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py index 54e046dfd..7d0d81fd5 100644 --- a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py @@ -1,7 +1,7 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, models +from odoo import models JMIF_STATUS = { 0: "success", @@ -24,7 +24,6 @@ JMIF_STATUS = { class VerticalLiftShuttle(models.Model): _inherit = "vertical.lift.shuttle" - @api.model def _selection_hardware(self): values = super()._selection_hardware() values += [("kardex", "Kardex")] From 355d19f7a6036557340674a36bfcdd325f0cd439 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 3 Mar 2020 13:25:50 +0100 Subject: [PATCH 07/19] Add +x on kardex-proxy.py script --- stock_vertical_lift_kardex/proxy/kardex-proxy.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 stock_vertical_lift_kardex/proxy/kardex-proxy.py diff --git a/stock_vertical_lift_kardex/proxy/kardex-proxy.py b/stock_vertical_lift_kardex/proxy/kardex-proxy.py old mode 100644 new mode 100755 From 6fc160d25f4b4bd1a9ed84dafac5cc63bb079cdc Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 22 Jun 2020 15:37:55 +0200 Subject: [PATCH 08/19] [IMP] Fix naming of the shuttle address we hardcode the ID of the gate for now, hence we support only a single gate. --- stock_vertical_lift_kardex/models/stock_location.py | 4 +++- stock_vertical_lift_kardex/readme/ROADMAP.rst | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index 8b3cd73c2..85af2c55d 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -32,7 +32,9 @@ class StockLocation(models.Model): subst = { "code": code, "hostId": self.env["ir.sequence"].next_by_code("vertical.lift.command"), - "addr": shuttle.name, + # hard code the gate for now. + # TODO proper handling of multiple gates for 1 lift. + "addr": shuttle.name + '-1', "carrier": self.level, "carrierNext": "0", "x": x, diff --git a/stock_vertical_lift_kardex/readme/ROADMAP.rst b/stock_vertical_lift_kardex/readme/ROADMAP.rst index 3d8aefd2e..a96fc55be 100644 --- a/stock_vertical_lift_kardex/readme/ROADMAP.rst +++ b/stock_vertical_lift_kardex/readme/ROADMAP.rst @@ -1 +1,2 @@ * Add support of the hardware +* handle multiple gates for one lift From aabb7a120d9d643ffae79072cc66cf49b5565e70 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 18 May 2020 15:40:00 +0200 Subject: [PATCH 09/19] stock_vertical_lift_kardex: add missing dependency we use the `level` field to materialize the tablar index -> this field is provided by `stock_location_position` --- stock_vertical_lift_kardex/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_vertical_lift_kardex/__manifest__.py b/stock_vertical_lift_kardex/__manifest__.py index 4a1e76a8d..3790ad904 100644 --- a/stock_vertical_lift_kardex/__manifest__.py +++ b/stock_vertical_lift_kardex/__manifest__.py @@ -7,7 +7,7 @@ "category": "Stock", "author": "Camptocamp, Odoo Community Association (OCA)", "license": "AGPL-3", - "depends": ["stock_vertical_lift"], + "depends": ["stock_vertical_lift", "stock_location_position"], "website": "https://www.camptocamp.com", "data": [], "installable": True, From af0e12231b3977db38bd47a22022a84966e12f03 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 22 Jun 2020 16:26:22 +0200 Subject: [PATCH 10/19] [FIX] laser pointer command The parameter was not passed in the method chain, loosing track of the cell to which the laser pointer should be pointed at. --- stock_vertical_lift_kardex/models/stock_location.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index 85af2c55d..4f8a91983 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -85,7 +85,9 @@ class StockLocation(models.Model): highlighting the cell using a laser pointer. """ if self.vertical_lift_shuttle_id.hardware == "kardex": - payload = self._hardware_kardex_prepare_payload() + payload = self._hardware_kardex_prepare_payload( + cell_location=cell_location + ) _logger.debug("Sending to kardex: {}", payload) # TODO implement the communication with kardex else: From 2f1233023c15e03f72f695606b7b7ca1bfc4fce1 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Tue, 23 Jun 2020 19:06:41 +0200 Subject: [PATCH 11/19] [IMP] raise exceptions when config NOK we need a level to fetch a tray we need the tray dimensions to point a cell --- stock_vertical_lift_kardex/models/stock_location.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index 4f8a91983..cabf25775 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -3,7 +3,7 @@ import logging -from odoo import models +from odoo import _, exceptions, models _logger = logging.getLogger(__name__) @@ -12,6 +12,11 @@ class StockLocation(models.Model): _inherit = "stock.location" def _hardware_kardex_prepare_payload(self, cell_location=None): + if self.level is False: + raise exceptions.UserError( + _('Shuttle tray %s has no level. ' + 'Please fix the configuration') % self.display_name + ) message_template = ( "{code}|{hostId}|{addr}|{carrier}|{carrierNext}|" "{x}|{y}|{boxType}|{Q}|{order}|{part}|{desc}|\r\n" @@ -27,6 +32,12 @@ class StockLocation(models.Model): code = "61" # ping if cell_location: x, y = cell_location.tray_cell_center_position() + if x == 0 and y == 0: + raise exceptions.UserError( + _('Cell location %s has no position. ' + 'Check if the dimensions of tray %s ' + 'are properly set in the tray type.') % (cell_location.display_name, self.name) + ) else: x, y = "", "" subst = { From 15d21a1b1912c4fd5260aab4d79049557a24d6b5 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Wed, 24 Jun 2020 17:13:22 +0200 Subject: [PATCH 12/19] [FIX] laser position are integers --- stock_vertical_lift_kardex/models/stock_location.py | 1 + 1 file changed, 1 insertion(+) diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index cabf25775..c2b6c99c9 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -38,6 +38,7 @@ class StockLocation(models.Model): 'Check if the dimensions of tray %s ' 'are properly set in the tray type.') % (cell_location.display_name, self.name) ) + x, y = int(x), int(y) else: x, y = "", "" subst = { From 2592d978232687d710cab049e1bf5cb40e15f2f1 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 29 Jun 2020 14:36:13 +0200 Subject: [PATCH 13/19] Rework workflows using a small state machine The documentation of the state machine is in VerticalLiftOperationBase._transitions. --- .../models/stock_location.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index c2b6c99c9..ef7379f7c 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -14,8 +14,8 @@ class StockLocation(models.Model): def _hardware_kardex_prepare_payload(self, cell_location=None): if self.level is False: raise exceptions.UserError( - _('Shuttle tray %s has no level. ' - 'Please fix the configuration') % self.display_name + _("Shuttle tray %s has no level. " "Please fix the configuration") + % self.display_name ) message_template = ( "{code}|{hostId}|{addr}|{carrier}|{carrierNext}|" @@ -34,9 +34,12 @@ class StockLocation(models.Model): x, y = cell_location.tray_cell_center_position() if x == 0 and y == 0: raise exceptions.UserError( - _('Cell location %s has no position. ' - 'Check if the dimensions of tray %s ' - 'are properly set in the tray type.') % (cell_location.display_name, self.name) + _( + "Cell location %s has no position. " + "Check if the dimensions of tray %s " + "are properly set in the tray type." + ) + % (cell_location.display_name, self.name) ) x, y = int(x), int(y) else: @@ -46,7 +49,7 @@ class StockLocation(models.Model): "hostId": self.env["ir.sequence"].next_by_code("vertical.lift.command"), # hard code the gate for now. # TODO proper handling of multiple gates for 1 lift. - "addr": shuttle.name + '-1', + "addr": shuttle.name + "-1", "carrier": self.level, "carrierNext": "0", "x": x, @@ -97,9 +100,7 @@ class StockLocation(models.Model): highlighting the cell using a laser pointer. """ if self.vertical_lift_shuttle_id.hardware == "kardex": - payload = self._hardware_kardex_prepare_payload( - cell_location=cell_location - ) + payload = self._hardware_kardex_prepare_payload(cell_location=cell_location) _logger.debug("Sending to kardex: {}", payload) # TODO implement the communication with kardex else: From b76cabdc87e0a46b8768700fb2fd0d2c82e2c851 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 21 Jul 2020 09:26:12 +0200 Subject: [PATCH 14/19] Add release (close) of vertical lift trays * 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. --- .../models/stock_location.py | 32 +++------- .../models/vertical_lift_shuttle.py | 64 +++++++++++++++++++ 2 files changed, 74 insertions(+), 22 deletions(-) diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index ef7379f7c..3b089845b 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -11,25 +11,13 @@ _logger = logging.getLogger(__name__) class StockLocation(models.Model): _inherit = "stock.location" - def _hardware_kardex_prepare_payload(self, cell_location=None): + def _hardware_kardex_prepare_fetch_payload(self, cell_location=None): if self.level is False: raise exceptions.UserError( _("Shuttle tray %s has no level. " "Please fix the configuration") % self.display_name ) - message_template = ( - "{code}|{hostId}|{addr}|{carrier}|{carrierNext}|" - "{x}|{y}|{boxType}|{Q}|{order}|{part}|{desc}|\r\n" - ) shuttle = self.vertical_lift_shuttle_id - if shuttle.mode == "pick": - code = "1" - elif shuttle.mode == "put": - code = "2" - elif shuttle.mode == "inventory": - code = "5" - else: - code = "61" # ping if cell_location: x, y = cell_location.tray_cell_center_position() if x == 0 and y == 0: @@ -45,7 +33,7 @@ class StockLocation(models.Model): else: x, y = "", "" subst = { - "code": code, + "code": shuttle._kardex_shuttle_code(), "hostId": self.env["ir.sequence"].next_by_code("vertical.lift.command"), # hard code the gate for now. # TODO proper handling of multiple gates for 1 lift. @@ -60,11 +48,10 @@ class StockLocation(models.Model): "part": "", "desc": "", } - payload = message_template.format(**subst) - return payload.encode("iso-8859-1", "replace") + return shuttle._hardware_kardex_format_template(subst) - def _hardware_vertical_lift_tray_payload(self, cell_location=None): - """Prepare the message to be sent to the vertical lift hardware + def _hardware_vertical_lift_fetch_tray_payload(self, cell_location=None): + """Prepare "fetch" 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 @@ -100,11 +87,12 @@ class StockLocation(models.Model): highlighting the cell using a laser pointer. """ if self.vertical_lift_shuttle_id.hardware == "kardex": - payload = self._hardware_kardex_prepare_payload(cell_location=cell_location) - _logger.debug("Sending to kardex: {}", payload) - # TODO implement the communication with kardex + payload = self._hardware_kardex_prepare_fetch_payload( + cell_location=cell_location + ) + _logger.debug("Sending to kardex (fetch): {}", payload) else: - payload = super()._hardware_vertical_lift_tray_payload( + payload = super()._hardware_vertical_lift_fetch_tray_payload( cell_location=cell_location ) return payload diff --git a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py index 7d0d81fd5..305e1eeeb 100644 --- a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py @@ -1,8 +1,13 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + from odoo import models +_logger = logging.getLogger(__name__) + + JMIF_STATUS = { 0: "success", 101: "common error", @@ -29,6 +34,65 @@ class VerticalLiftShuttle(models.Model): values += [("kardex", "Kardex")] return values + _kardex_message_template = ( + "{code}|{hostId}|{addr}|{carrier}|{carrierNext}|" + "{x}|{y}|{boxType}|{Q}|{order}|{part}|{desc}|\r\n" + ) + + def _hardware_kardex_format_template(self, values): + payload = self._kardex_message_template.format(**values) + return payload.encode("iso-8859-1", "replace") + + def _kardex_shuttle_code(self): + mapping = {"pick": "1", "put": "2", "inventory": "5"} + ping = "61" + return mapping.get(self.mode, ping) + + def _hardware_kardex_prepare_release_payload(self): + subst = { + "code": self._kardex_shuttle_code(), + "hostId": self.env["ir.sequence"].next_by_code("vertical.lift.command"), + # hard code the gate for now. + "addr": self.name + "-1", + "carrier": "0", + "carrierNext": "0", + "x": "0", + "y": "0", + "boxType": "", + "Q": "", + "order": "", + "part": "", + "desc": "", + } + return self._hardware_kardex_format_template(subst) + + 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.vertical_lift_shuttle_id.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 == "kardex": + payload = self._hardware_kardex_prepare_release_payload() + _logger.debug("Sending to kardex (release): {}", payload) + else: + payload = super()._hardware_vertical_lift_release_tray_payload() + return payload + def _check_server_response(self, command): response = command.answer code, sep, remaining = response.partition("|") From ae5d7087e50a7c32836627db54c99193c00b8536 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Tue, 29 Sep 2020 14:33:55 +0000 Subject: [PATCH 15/19] [UPD] Update stock_vertical_lift_kardex.pot --- .../i18n/stock_vertical_lift_kardex.pot | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 stock_vertical_lift_kardex/i18n/stock_vertical_lift_kardex.pot diff --git a/stock_vertical_lift_kardex/i18n/stock_vertical_lift_kardex.pot b/stock_vertical_lift_kardex/i18n/stock_vertical_lift_kardex.pot new file mode 100644 index 000000000..1717a3051 --- /dev/null +++ b/stock_vertical_lift_kardex/i18n/stock_vertical_lift_kardex.pot @@ -0,0 +1,38 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_vertical_lift_kardex +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_vertical_lift_kardex +#: code:addons/stock_vertical_lift_kardex/models/stock_location.py:0 +#, python-format +msgid "" +"Cell location %s has no position. Check if the dimensions of tray %s are " +"properly set in the tray type." +msgstr "" + +#. module: stock_vertical_lift_kardex +#: model:ir.model,name:stock_vertical_lift_kardex.model_stock_location +msgid "Inventory Locations" +msgstr "" + +#. module: stock_vertical_lift_kardex +#: code:addons/stock_vertical_lift_kardex/models/stock_location.py:0 +#, python-format +msgid "Shuttle tray %s has no level. Please fix the configuration" +msgstr "" + +#. module: stock_vertical_lift_kardex +#: model:ir.model,name:stock_vertical_lift_kardex.model_vertical_lift_shuttle +msgid "Vertical Lift Shuttle" +msgstr "" From 949c1b2fa57d1d6dd1db847d38a368aecbc003cd Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 29 Sep 2020 15:50:32 +0000 Subject: [PATCH 16/19] [UPD] README.rst --- stock_vertical_lift_kardex/README.rst | 1 + stock_vertical_lift_kardex/static/description/index.html | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/stock_vertical_lift_kardex/README.rst b/stock_vertical_lift_kardex/README.rst index 8d4a3f730..e98e10985 100644 --- a/stock_vertical_lift_kardex/README.rst +++ b/stock_vertical_lift_kardex/README.rst @@ -42,6 +42,7 @@ Known issues / Roadmap ====================== * Add support of the hardware +* handle multiple gates for one lift Bug Tracker =========== diff --git a/stock_vertical_lift_kardex/static/description/index.html b/stock_vertical_lift_kardex/static/description/index.html index ae2800eba..105691045 100644 --- a/stock_vertical_lift_kardex/static/description/index.html +++ b/stock_vertical_lift_kardex/static/description/index.html @@ -3,7 +3,7 @@ - + Vertical Lift - Kardex