From 311fb24447bacb8ac4972bab6934dde3de982764 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 11 Jun 2019 16:50:48 +0200 Subject: [PATCH 01/41] Add stock_vertical_lift module --- .../odoo/addons/stock_vertical_lift | 1 + setup/stock_vertical_lift/setup.py | 6 + .../odoo/addons/stock_vertical_lift_kardex | 1 + setup/stock_vertical_lift_kardex/setup.py | 6 + stock_vertical_lift/README.rst | 134 +++++ stock_vertical_lift/__init__.py | 1 + stock_vertical_lift/__manifest__.py | 34 ++ stock_vertical_lift/demo/product_demo.xml | 32 ++ .../demo/stock_inventory_demo.xml | 20 + .../demo/stock_location_demo.xml | 116 +++++ .../demo/stock_picking_demo.xml | 30 ++ .../demo/vertical_lift_shuttle_demo.xml | 22 + stock_vertical_lift/images/O-BTN.release.svg | 55 ++ stock_vertical_lift/images/O-BTN.save.svg | 46 ++ stock_vertical_lift/models/__init__.py | 4 + stock_vertical_lift/models/stock_location.py | 40 ++ stock_vertical_lift/models/stock_move.py | 23 + stock_vertical_lift/models/stock_quant.py | 16 + .../models/vertical_lift_shuttle.py | 412 +++++++++++++++ stock_vertical_lift/readme/CONFIGURE.rst | 39 ++ stock_vertical_lift/readme/CONTRIBUTORS.rst | 1 + stock_vertical_lift/readme/DESCRIPTION.rst | 3 + stock_vertical_lift/readme/ROADMAP.rst | 3 + .../security/ir.model.access.csv | 3 + .../static/description/index.html | 490 ++++++++++++++++++ .../static/src/js/vertical_lift.js | 55 ++ .../static/src/scss/vertical_lift.scss | 124 +++++ stock_vertical_lift/tests/__init__.py | 2 + stock_vertical_lift/tests/common.py | 49 ++ stock_vertical_lift/tests/test_location.py | 40 ++ .../tests/test_vertical_lift_shuttle.py | 252 +++++++++ .../views/shuttle_screen_templates.xml | 17 + .../views/stock_location_views.xml | 35 ++ .../views/stock_vertical_lift_templates.xml | 11 + .../views/vertical_lift_shuttle_views.xml | 287 ++++++++++ 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 + 42 files changed, 2447 insertions(+) create mode 120000 setup/stock_vertical_lift/odoo/addons/stock_vertical_lift create mode 100644 setup/stock_vertical_lift/setup.py create mode 120000 setup/stock_vertical_lift_kardex/odoo/addons/stock_vertical_lift_kardex create mode 100644 setup/stock_vertical_lift_kardex/setup.py create mode 100644 stock_vertical_lift/README.rst create mode 100644 stock_vertical_lift/__init__.py create mode 100644 stock_vertical_lift/__manifest__.py create mode 100644 stock_vertical_lift/demo/product_demo.xml create mode 100644 stock_vertical_lift/demo/stock_inventory_demo.xml create mode 100644 stock_vertical_lift/demo/stock_location_demo.xml create mode 100644 stock_vertical_lift/demo/stock_picking_demo.xml create mode 100644 stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml create mode 100644 stock_vertical_lift/images/O-BTN.release.svg create mode 100644 stock_vertical_lift/images/O-BTN.save.svg create mode 100644 stock_vertical_lift/models/__init__.py create mode 100644 stock_vertical_lift/models/stock_location.py create mode 100644 stock_vertical_lift/models/stock_move.py create mode 100644 stock_vertical_lift/models/stock_quant.py create mode 100644 stock_vertical_lift/models/vertical_lift_shuttle.py create mode 100644 stock_vertical_lift/readme/CONFIGURE.rst create mode 100644 stock_vertical_lift/readme/CONTRIBUTORS.rst create mode 100644 stock_vertical_lift/readme/DESCRIPTION.rst create mode 100644 stock_vertical_lift/readme/ROADMAP.rst create mode 100644 stock_vertical_lift/security/ir.model.access.csv create mode 100644 stock_vertical_lift/static/description/index.html create mode 100644 stock_vertical_lift/static/src/js/vertical_lift.js create mode 100644 stock_vertical_lift/static/src/scss/vertical_lift.scss create mode 100644 stock_vertical_lift/tests/__init__.py create mode 100644 stock_vertical_lift/tests/common.py create mode 100644 stock_vertical_lift/tests/test_location.py create mode 100644 stock_vertical_lift/tests/test_vertical_lift_shuttle.py create mode 100644 stock_vertical_lift/views/shuttle_screen_templates.xml create mode 100644 stock_vertical_lift/views/stock_location_views.xml create mode 100644 stock_vertical_lift/views/stock_vertical_lift_templates.xml create mode 100644 stock_vertical_lift/views/vertical_lift_shuttle_views.xml 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/setup/stock_vertical_lift/odoo/addons/stock_vertical_lift b/setup/stock_vertical_lift/odoo/addons/stock_vertical_lift new file mode 120000 index 000000000000..61bf9f018226 --- /dev/null +++ b/setup/stock_vertical_lift/odoo/addons/stock_vertical_lift @@ -0,0 +1 @@ +../../../../stock_vertical_lift \ No newline at end of file diff --git a/setup/stock_vertical_lift/setup.py b/setup/stock_vertical_lift/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/stock_vertical_lift/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_vertical_lift_kardex/odoo/addons/stock_vertical_lift_kardex b/setup/stock_vertical_lift_kardex/odoo/addons/stock_vertical_lift_kardex new file mode 120000 index 000000000000..0c99c9c02ea6 --- /dev/null +++ b/setup/stock_vertical_lift_kardex/odoo/addons/stock_vertical_lift_kardex @@ -0,0 +1 @@ +../../../../stock_vertical_lift_kardex \ No newline at end of file diff --git a/setup/stock_vertical_lift_kardex/setup.py b/setup/stock_vertical_lift_kardex/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/stock_vertical_lift_kardex/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_vertical_lift/README.rst b/stock_vertical_lift/README.rst new file mode 100644 index 000000000000..a40a55da2046 --- /dev/null +++ b/stock_vertical_lift/README.rst @@ -0,0 +1,134 @@ +============= +Vertical Lift +============= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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/12.0/stock_vertical_lift + :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-12-0/stock-logistics-warehouse-12-0-stock_vertical_lift + :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/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Add configuration and dedicated screens to work with Vertical Lift +systems (such as Kardex Remstar, Modula, ...). Drivers for controlling +the lifts physically must be added by additional addons. + +.. 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: + +Configuration +============= + +General +~~~~~~~ + +In Inventory Settings, you must have: + + * Storage Locations + * Multi-Warehouses + * Multi-Step Routes + +Locations +~~~~~~~~~ + +Additional configuration parameters are added in Locations: + +* Sub-locations of a location with the "Is a Vertical Lift View Location" + activated are considered as "Shuttles". A shuttle is a vertical lift shelf. +* Sub-locations of shuttles are considered as "Trays", which is a tier of a + shuttle. When a tray is created, a tray type must be selected. When saved, the + tray location will automatically create as many sub-locations - called + "Cells" - as the tray type contains. +* The tray type of a tray can be changed as long as none of its cell contains + products. When changed, it archives the cells and creates new ones as + configured on the new tray type. + +Tray types +~~~~~~~~~~ + +Tray types can be configured in the Inventory settings. +A tray type defines how much cells a tray can hold. It is a square or rectangle +matrix of n cols * m rows. + +Vertical Lift Shuttles +~~~~~~~~~~~~~~~~~~~~~~ + +The Shuttles are the Vertical Lift Trays. One Shuttle entity has to be created +in Odoo for each physical shuttle. Depending of the subsidiary addons installed +(eg. Kardex), different options may be required (host address, ...). The base +addon only includes shuttles of kind "simulation" which will not send orders to +the hardware. + +Known issues / Roadmap +====================== + +* Extract the tray types and matrix widget in a module, they can be used + alone without vertical lift +* Consider merging the 'vertical_lift_kind' with the kind added by + stock_location_zone +* Complete Pick screen and workflow (currently enough for a demo, not for production) +* Implement Put-away screen and workflow +* Implement Inventory screen and workflow + +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/__init__.py b/stock_vertical_lift/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/stock_vertical_lift/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_vertical_lift/__manifest__.py b/stock_vertical_lift/__manifest__.py new file mode 100644 index 000000000000..e259e71f48db --- /dev/null +++ b/stock_vertical_lift/__manifest__.py @@ -0,0 +1,34 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Vertical Lift', + 'summary': 'Provides the core for integration with Vertical Lifts', + 'version': '12.0.1.0.0', + 'category': 'Stock', + 'author': 'Camptocamp, Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'depends': [ + 'stock', + 'barcodes', + 'base_sparse_field', + 'stock_location_tray', # OCA/stock-logistics-warehouse + 'web_notify', # OCA/web + ], + 'website': 'https://github.com/OCA/stock-logistics-warehouse', + 'demo': [ + 'demo/stock_location_demo.xml', + 'demo/vertical_lift_shuttle_demo.xml', + 'demo/product_demo.xml', + 'demo/stock_inventory_demo.xml', + 'demo/stock_picking_demo.xml', + ], + 'data': [ + 'views/stock_location_views.xml', + 'views/vertical_lift_shuttle_views.xml', + 'views/stock_vertical_lift_templates.xml', + 'views/shuttle_screen_templates.xml', + 'security/ir.model.access.csv', + ], + 'installable': True, + 'development_status': 'Alpha', +} diff --git a/stock_vertical_lift/demo/product_demo.xml b/stock_vertical_lift/demo/product_demo.xml new file mode 100644 index 000000000000..bfe8ab90d8e6 --- /dev/null +++ b/stock_vertical_lift/demo/product_demo.xml @@ -0,0 +1,32 @@ + + + + + RS200 + Running Socks + product + + 30.0 + 20.0 + 1.0 + none + + + + + + + RS300 + Recovery Socks + product + + 30.0 + 20.0 + 1.0 + none + + + + + + diff --git a/stock_vertical_lift/demo/stock_inventory_demo.xml b/stock_vertical_lift/demo/stock_inventory_demo.xml new file mode 100644 index 000000000000..4b17fcdcd6ec --- /dev/null +++ b/stock_vertical_lift/demo/stock_inventory_demo.xml @@ -0,0 +1,20 @@ + + + + + Starting Vertical Lift Inventory + + + + + + + 30.0 + + + + + + + + diff --git a/stock_vertical_lift/demo/stock_location_demo.xml b/stock_vertical_lift/demo/stock_location_demo.xml new file mode 100644 index 000000000000..78257a926b40 --- /dev/null +++ b/stock_vertical_lift/demo/stock_location_demo.xml @@ -0,0 +1,116 @@ + + + + + Vertical Lift + + + internal + + + + + + Shuttle 1 + + internal + + + + Tray 1A + T1A + + + internal + + + + Tray 1B + T1B + + + internal + + + + Tray 1C + T1C + + + internal + + + + Shuttle 2 + + internal + + + + Tray 2A + T2A + + + internal + + + + Tray 2B + T2B + + + internal + + + + Tray 2C + T2C + + + internal + + + + Tray 2D + T2D + + + internal + + + + Shuttle 3 + + internal + + + + Tray 3A + T3A + + + internal + + + + Tray 3B + T3B + + + internal + + + + + + stock_vertical_lift + + + diff --git a/stock_vertical_lift/demo/stock_picking_demo.xml b/stock_vertical_lift/demo/stock_picking_demo.xml new file mode 100644 index 000000000000..520d97925101 --- /dev/null +++ b/stock_vertical_lift/demo/stock_picking_demo.xml @@ -0,0 +1,30 @@ + + + + + + Outgoing shipment from Vertical Lift (demo) + + + + + + + + + + + + + + + + diff --git a/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml b/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml new file mode 100644 index 000000000000..435c80f3bcf5 --- /dev/null +++ b/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml @@ -0,0 +1,22 @@ + + + + + Shuttle 1 + + pick + + + + Shuttle 2 + + pick + + + + Shuttle 3 + + pick + + + diff --git a/stock_vertical_lift/images/O-BTN.release.svg b/stock_vertical_lift/images/O-BTN.release.svg new file mode 100644 index 000000000000..42535a126d9a --- /dev/null +++ b/stock_vertical_lift/images/O-BTN.release.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +O-BTN.release \ No newline at end of file diff --git a/stock_vertical_lift/images/O-BTN.save.svg b/stock_vertical_lift/images/O-BTN.save.svg new file mode 100644 index 000000000000..f32e290a8e53 --- /dev/null +++ b/stock_vertical_lift/images/O-BTN.save.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +O-BTN.save \ No newline at end of file diff --git a/stock_vertical_lift/models/__init__.py b/stock_vertical_lift/models/__init__.py new file mode 100644 index 000000000000..2a0e1d40b651 --- /dev/null +++ b/stock_vertical_lift/models/__init__.py @@ -0,0 +1,4 @@ +from . import vertical_lift_shuttle +from . import stock_location +from . import stock_move +from . import stock_quant diff --git a/stock_vertical_lift/models/stock_location.py b/stock_vertical_lift/models/stock_location.py new file mode 100644 index 000000000000..a8d19e0a03b3 --- /dev/null +++ b/stock_vertical_lift/models/stock_location.py @@ -0,0 +1,40 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class StockLocation(models.Model): + _inherit = "stock.location" + + vertical_lift_location = fields.Boolean( + 'Is a Vertical Lift View Location?', + default=False, + help="Check this box to use it as the view for Vertical" + " Lift Shuttles.", + ) + vertical_lift_kind = fields.Selection( + selection=[ + ('view', 'View'), + ('shuttle', 'Shuttle'), + ('tray', 'Tray'), + ('cell', 'Cell'), + ], + compute='_compute_vertical_lift_kind', + store=True, + ) + + @api.depends( + 'location_id', + 'location_id.vertical_lift_kind', + 'vertical_lift_location', + ) + def _compute_vertical_lift_kind(self): + tree = {'view': 'shuttle', 'shuttle': 'tray', 'tray': 'cell'} + for location in self: + if location.vertical_lift_location: + location.vertical_lift_kind = 'view' + continue + kind = tree.get(location.location_id.vertical_lift_kind) + if kind: + location.vertical_lift_kind = kind diff --git a/stock_vertical_lift/models/stock_move.py b/stock_vertical_lift/models/stock_move.py new file mode 100644 index 000000000000..1210d6b09c1b --- /dev/null +++ b/stock_vertical_lift/models/stock_move.py @@ -0,0 +1,23 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class StockMove(models.Model): + _inherit = 'stock.move' + + @api.multi + def write(self, vals): + result = super().write(vals) + if 'state' in vals: + # We cannot have fields to depends on to invalidate these computed + # fields on vertical.lift.shuttle. But we know that when the state + # of any move line changes, we can invalidate them as the count of + # assigned move lines may change (and we track this in stock.move, + # not stock.move.line, becaus the state of the lines is a related + # to this one). + self.env['vertical.lift.shuttle'].invalidate_cache( + ['number_of_ops', 'number_of_ops_all'] + ) + return result diff --git a/stock_vertical_lift/models/stock_quant.py b/stock_vertical_lift/models/stock_quant.py new file mode 100644 index 000000000000..905484139a7e --- /dev/null +++ b/stock_vertical_lift/models/stock_quant.py @@ -0,0 +1,16 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class StockQuant(models.Model): + _inherit = 'stock.quant' + + def _update_available_quantity(self, *args, **kwargs): + result = super()._update_available_quantity(*args, **kwargs) + # We cannot have fields to depends on to invalidate this computed + # fields on vertical.lift.shuttle. But we know that when the quantity + # of quant changes, we can invalidate the field on the shuttles. + self.env['vertical.lift.shuttle'].invalidate_cache(['tray_qty']) + return result diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py new file mode 100644 index 000000000000..54ec752d269e --- /dev/null +++ b/stock_vertical_lift/models/vertical_lift_shuttle.py @@ -0,0 +1,412 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, exceptions, fields, models +from odoo.addons.base_sparse_field.models.fields import Serialized + + +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 + ) + current_move_line_id = fields.Many2one(comodel_name='stock.move.line') + + number_of_ops = fields.Integer( + compute='_compute_number_of_ops', string='Number of Operations' + ) + number_of_ops_all = fields.Integer( + compute='_compute_number_of_ops_all', + string='Number of Operations in all shuttles', + ) + + operation_descr = fields.Char( + string="Operation", + default="Scan New Destination Location", + readonly=True, + ) + + # tray information (will come from stock.location or a new tray model) + tray_location_id = fields.Many2one( + comodel_name='stock.location', + compute='_compute_tray_matrix', + string='Tray Location', + ) + tray_name = fields.Char(compute='_compute_tray_matrix', string='Tray Name') + tray_type_id = fields.Many2one( + comodel_name='stock.location.tray.type', + compute='_compute_tray_matrix', + string='Tray Type', + ) + tray_type_code = fields.Char( + compute='_compute_tray_matrix', string='Tray Code' + ) + tray_x = fields.Integer(string='X', compute='_compute_tray_matrix') + tray_y = fields.Integer(string='Y', compute='_compute_tray_matrix') + tray_matrix = Serialized(string='Cells', compute='_compute_tray_matrix') + tray_qty = fields.Float( + string='Stock Quantity', compute='_compute_tray_qty' + ) + + # current operation information + picking_id = fields.Many2one( + related='current_move_line_id.picking_id', readonly=True + ) + picking_origin = fields.Char( + related='current_move_line_id.picking_id.origin', readonly=True + ) + picking_partner_id = fields.Many2one( + related='current_move_line_id.picking_id.partner_id', readonly=True + ) + product_id = fields.Many2one( + related='current_move_line_id.product_id', readonly=True + ) + product_uom_id = fields.Many2one( + related='current_move_line_id.product_uom_id', readonly=True + ) + product_uom_qty = fields.Float( + related='current_move_line_id.product_uom_qty', readonly=True + ) + product_packagings = fields.Html( + string='Packaging', compute='_compute_product_packagings' + ) + qty_done = fields.Float( + related='current_move_line_id.qty_done', readonly=True + ) + lot_id = fields.Many2one( + related='current_move_line_id.lot_id', readonly=True + ) + location_dest_id = fields.Many2one( + string="Destination", + related='current_move_line_id.location_dest_id', + readonly=True, + ) + + # TODO add a glue addon with product_expiry to add the field + + _barcode_scanned = fields.Char( + "Barcode Scanned", + help="Value of the last barcode scanned.", + store=False, + ) + + def on_barcode_scanned(self, barcode): + self.ensure_one() + # FIXME notify_info is only for the demo + self.env.user.notify_info('Scanned barcode: {}'.format(barcode)) + method = 'on_barcode_scanned_{}'.format(self.mode) + getattr(self, method)(barcode) + + def on_barcode_scanned_pick(self, barcode): + location = self.env['stock.location'].search( + [('barcode', '=', barcode)] + ) + if location: + self.current_move_line_id.location_dest_id = location + self.operation_descr = _('Save') + else: + self.env.user.notify_warning( + _('No location found for barcode {}').format(barcode) + ) + + def on_barcode_scanned_put(self, barcode): + pass + + def on_barcode_scanned_inventory(self, barcode): + pass + + @api.model + def _selection_hardware(self): + return [('simulation', 'Simulation')] + + @api.depends('current_move_line_id.product_id.packaging_ids') + def _compute_product_packagings(self): + for record in self: + if not record.current_move_line_id: + continue + product = record.current_move_line_id.product_id + values = { + 'packagings': [ + { + 'name': pkg.name, + 'qty': pkg.qty, + 'unit': product.uom_id.name, + } + for pkg in product.packaging_ids + ] + } + content = self.env['ir.qweb'].render( + 'stock_vertical_lift.packagings', values + ) + record.product_packagings = content + + @api.depends() + def _compute_number_of_ops(self): + for record in self: + record.number_of_ops = record.count_move_lines_to_do() + + @api.depends() + def _compute_number_of_ops_all(self): + for record in self: + record.number_of_ops_all = record.count_move_lines_to_do_all() + + @api.depends('tray_location_id', 'current_move_line_id.product_id') + def _compute_tray_qty(self): + for record in self: + if not (record.tray_location_id and record.current_move_line_id): + continue + product = record.current_move_line_id.product_id + quants = self.env['stock.quant'].search( + [ + ('location_id', '=', record.tray_location_id.id), + ('product_id', '=', product.id), + ] + ) + record.tray_qty = sum(quants.mapped('quantity')) + + @api.depends() + def _compute_tray_matrix(self): + for record in self: + modes = { + 'pick': 'location_id', + 'put': 'location_dest_id', + # TODO what to do for inventory? + 'inventory': 'location_id', + } + location = record.current_move_line_id[modes[record.mode]] + tray_type = location.location_id.tray_type_id + selected = [] + cells = [] + if location: + selected = location._tray_cell_coords() + cells = location._tray_cell_matrix() + + # this is the current cell + record.tray_location_id = location.id + # name of the tray where the cell is + record.tray_name = location.location_id.name + record.tray_type_id = tray_type.id + record.tray_type_code = tray_type.code + record.tray_x = location.posx + record.tray_y = location.posy + record.tray_matrix = { + # x, y: position of the selected cell + 'selected': selected, + # 0 is empty, 1 is not + 'cells': cells, + } + + def _domain_move_lines_to_do(self): + domain = [ + # TODO check state + ('state', '=', 'assigned') + ] + domain_extensions = { + 'pick': [('location_id', 'child_of', self.location_id.id)], + # TODO ensure that we cannot have the same ml in 2 shuttles (cannot + # happen with 'pick' as they are in the shuttle's location) + 'put': [('location_dest_id', 'child_of', self.location_id.id)], + # TODO + 'inventory': [('id', '=', 0)], + } + return domain + domain_extensions[self.mode] + + def _domain_move_lines_to_do_all(self): + domain = [ + # TODO check state + ('state', '=', 'assigned') + ] + # TODO search only in the view being a parent of shuttle's location + shuttle_locations = self.env['stock.location'].search( + [('vertical_lift_kind', '=', 'view')] + ) + domain_extensions = { + 'pick': [('location_id', 'child_of', shuttle_locations.ids)], + 'put': [('location_dest_id', 'child_of', shuttle_locations.ids)], + # TODO + 'inventory': [('id', '=', 0)], + } + return domain + domain_extensions[self.mode] + + def count_move_lines_to_do(self): + self.ensure_one() + return self.env['stock.move.line'].search_count( + self._domain_move_lines_to_do() + ) + + def count_move_lines_to_do_all(self): + self.ensure_one() + return self.env['stock.move.line'].search_count( + self._domain_move_lines_to_do_all() + ) + + def button_release(self): + if self.current_move_line_id: + self._hardware_switch_off_laser_pointer() + self._hardware_close_tray() + self.select_next_move_line() + if not self.current_move_line_id: + # sorry not sorry + return { + 'effect': { + 'fadeout': 'slow', + 'message': _('Congrats, you cleared the queue!'), + 'img_url': '/web/static/src/img/smile.svg', + 'type': 'rainbow_man', + } + } + + def process_current_pick(self): + # test code, TODO the smart one + # (scan of barcode increments qty, save calls action_done?) + line = self.current_move_line_id + if line.state != 'done': + line.qty_done = line.product_qty + line.move_id._action_done() + + def process_current_put(self): + raise exceptions.UserError(_('Put workflow not implemented')) + + def process_current_inventory(self): + raise exceptions.UserError(_('Inventory workflow not implemented')) + + def button_save(self): + if not (self and self.current_move_line_id): + return + self.ensure_one() + method = 'process_current_{}'.format(self.mode) + getattr(self, method)() + self.operation_descr = _('Release') + + def select_next_move_line(self): + self.ensure_one() + next_move_line = self.env['stock.move.line'].search( + self._domain_move_lines_to_do(), limit=1 + ) + self.current_move_line_id = next_move_line + # TODO use a state machine to define next steps and + # description? + descr = ( + _('Scan New Destination Location') + if next_move_line + else _('No operations') + ) + self.operation_descr = descr + if next_move_line: + self._hardware_switch_on_laser_pointer() + self._hardware_open_tray() + + def action_open_screen(self): + self.select_next_move_line() + self.ensure_one() + screen_xmlid = ( + 'stock_vertical_lift.vertical_lift_shuttle_view_form_screen' + ) + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'views': [[self.env.ref(screen_xmlid).id, 'form']], + 'res_id': self.id, + 'target': 'fullscreen', + 'flags': { + 'headless': True, + '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_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.select_next_move_line() + + def switch_put(self): + self.mode = 'put' + self.select_next_move_line() + + def switch_inventory(self): + self.mode = 'inventory' + self.select_next_move_line() + + def _hardware_switch_on_laser_pointer(self): + if self.hardware == 'simulation': + self.env.user.notify_info( + message=_('Laser pointer on x{} y{}').format( + self.tray_x, self.tray_y + ), + title=_('Lift Simulation'), + ) + + def _hardware_switch_off_laser_pointer(self): + if self.hardware == 'simulation': + self.env.user.notify_info( + message=_('Switch off laser pointer'), + title=_('Lift Simulation'), + ) + + def _hardware_open_tray(self): + if self.hardware == 'simulation': + self.env.user.notify_info( + message=_('Opening tray {}').format(self.tray_name), + title=_('Lift Simulation'), + ) + + def _hardware_close_tray(self): + if self.hardware == 'simulation': + self.env.user.notify_info( + message=_('Closing tray {}').format(self.tray_name), + title=_('Lift Simulation'), + ) + + +class VerticalLiftShuttleManualBarcode(models.TransientModel): + _name = 'vertical.lift.shuttle.manual.barcode' + _description = 'Action to input a barcode' + + barcode = fields.Char(string="Barcode") + + @api.multi + def button_save(self): + shuttle_id = self.env.context.get('active_id') + shuttle = self.env['vertical.lift.shuttle'].browse(shuttle_id).exists() + if not shuttle: + return + if self.barcode: + shuttle.on_barcode_scanned(self.barcode) diff --git a/stock_vertical_lift/readme/CONFIGURE.rst b/stock_vertical_lift/readme/CONFIGURE.rst new file mode 100644 index 000000000000..1e2e98985d28 --- /dev/null +++ b/stock_vertical_lift/readme/CONFIGURE.rst @@ -0,0 +1,39 @@ +General +~~~~~~~ + +In Inventory Settings, you must have: + + * Storage Locations + * Multi-Warehouses + * Multi-Step Routes + +Locations +~~~~~~~~~ + +Additional configuration parameters are added in Locations: + +* Sub-locations of a location with the "Is a Vertical Lift View Location" + activated are considered as "Shuttles". A shuttle is a vertical lift shelf. +* Sub-locations of shuttles are considered as "Trays", which is a tier of a + shuttle. When a tray is created, a tray type must be selected. When saved, the + tray location will automatically create as many sub-locations - called + "Cells" - as the tray type contains. +* The tray type of a tray can be changed as long as none of its cell contains + products. When changed, it archives the cells and creates new ones as + configured on the new tray type. + +Tray types +~~~~~~~~~~ + +Tray types can be configured in the Inventory settings. +A tray type defines how much cells a tray can hold. It is a square or rectangle +matrix of n cols * m rows. + +Vertical Lift Shuttles +~~~~~~~~~~~~~~~~~~~~~~ + +The Shuttles are the Vertical Lift Trays. One Shuttle entity has to be created +in Odoo for each physical shuttle. Depending of the subsidiary addons installed +(eg. Kardex), different options may be required (host address, ...). The base +addon only includes shuttles of kind "simulation" which will not send orders to +the hardware. diff --git a/stock_vertical_lift/readme/CONTRIBUTORS.rst b/stock_vertical_lift/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..48286263cd35 --- /dev/null +++ b/stock_vertical_lift/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Guewen Baconnier diff --git a/stock_vertical_lift/readme/DESCRIPTION.rst b/stock_vertical_lift/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..f1bc969d4689 --- /dev/null +++ b/stock_vertical_lift/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +Add configuration and dedicated screens to work with Vertical Lift +systems (such as Kardex Remstar, Modula, ...). Drivers for controlling +the lifts physically must be added by additional addons. diff --git a/stock_vertical_lift/readme/ROADMAP.rst b/stock_vertical_lift/readme/ROADMAP.rst new file mode 100644 index 000000000000..c56a030779b4 --- /dev/null +++ b/stock_vertical_lift/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +* Complete Pick screen and workflow (currently enough for a demo, not for production) +* Implement Put-away screen and workflow +* Implement Inventory screen and workflow diff --git a/stock_vertical_lift/security/ir.model.access.csv b/stock_vertical_lift/security/ir.model.access.csv new file mode 100644 index 000000000000..dc7cb4b87a83 --- /dev/null +++ b/stock_vertical_lift/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_vertical_lift_shuttle_stock_user,access_vertical_lift_shuttle stock user,model_vertical_lift_shuttle,stock.group_stock_user,1,0,0,0 +access_vertical_lift_shuttle_manager,access_vertical_lift_shuttle stock manager,model_vertical_lift_shuttle,stock.group_stock_manager,1,1,1,1 diff --git a/stock_vertical_lift/static/description/index.html b/stock_vertical_lift/static/description/index.html new file mode 100644 index 000000000000..b8536e3beed3 --- /dev/null +++ b/stock_vertical_lift/static/description/index.html @@ -0,0 +1,490 @@ + + + + + + +Vertical Lift + + + +
+

Vertical Lift

+ + +

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

+

Add configuration and dedicated screens to work with Vertical Lift +systems (such as Kardex Remstar, Modula, …). Drivers for controlling +the lifts physically must be added by additional addons.

+
+

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

+ +
+

Configuration

+
+

General

+

In Inventory Settings, you must have:

+
+
    +
  • Storage Locations
  • +
  • Multi-Warehouses
  • +
  • Multi-Step Routes
  • +
+
+
+
+

Locations

+

Additional configuration parameters are added in Locations:

+
    +
  • Sub-locations of a location with the “Is a Vertical Lift View Location” +activated are considered as “Shuttles”. A shuttle is a vertical lift shelf.
  • +
  • Sub-locations of shuttles are considered as “Trays”, which is a tier of a +shuttle. When a tray is created, a tray type must be selected. When saved, the +tray location will automatically create as many sub-locations - called +“Cells” - as the tray type contains.
  • +
  • The tray type of a tray can be changed as long as none of its cell contains +products. When changed, it archives the cells and creates new ones as +configured on the new tray type.
  • +
+
+
+

Tray types

+

Tray types can be configured in the Inventory settings. +A tray type defines how much cells a tray can hold. It is a square or rectangle +matrix of n cols * m rows.

+
+
+

Vertical Lift Shuttles

+

The Shuttles are the Vertical Lift Trays. One Shuttle entity has to be created +in Odoo for each physical shuttle. Depending of the subsidiary addons installed +(eg. Kardex), different options may be required (host address, …). The base +addon only includes shuttles of kind “simulation” which will not send orders to +the hardware.

+
+
+
+

Known issues / Roadmap

+
    +
  • Extract the tray types and matrix widget in a module, they can be used +alone without vertical lift
  • +
  • Consider merging the ‘vertical_lift_kind’ with the kind added by +stock_location_zone
  • +
  • Complete Pick screen and workflow (currently enough for a demo, not for production)
  • +
  • Implement Put-away screen and workflow
  • +
  • Implement Inventory screen and workflow
  • +
+
+
+

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.

+
+
+
+ + diff --git a/stock_vertical_lift/static/src/js/vertical_lift.js b/stock_vertical_lift/static/src/js/vertical_lift.js new file mode 100644 index 000000000000..7c0e5d6f4ff1 --- /dev/null +++ b/stock_vertical_lift/static/src/js/vertical_lift.js @@ -0,0 +1,55 @@ +odoo.define('stock_vertical_lift.vertical_lift', function (require) { +"use strict"; + +var core = require('web.core'); +var KanbanRecord = require('web.KanbanRecord'); +var basicFields = require('web.basic_fields'); +var field_registry = require('web.field_registry'); +var FieldInteger = basicFields.FieldInteger; + +KanbanRecord.include({ + + _openRecord: function () { + if (this.modelName === 'vertical.lift.shuttle' + && this.$el.hasClass("open_shuttle_screen")) { + var self = this; + this._rpc({ + method: 'action_open_screen', + model: self.modelName, + args: [self.id], + }).then(function (action) { + self.trigger_up('do_action', {action: action}); + }); + } else { + this._super.apply(this, arguments); + } + }, + +}); + +var ExitButton = FieldInteger.extend({ + tagName: 'button', + className: 'btn btn-danger btn-block btn-lg o_shuttle_exit', + events: { + 'click': '_onClick', + }, + _render: function () { + this.$el.text(this.string); + }, + _onClick: function () { + // the only reason to have this field widget is to be able + // to inject clear_breadcrumbs in the action: + // it will revert back to a normal - non-headless - view + this.do_action('stock_vertical_lift.vertical_lift_shuttle_action', { + clear_breadcrumbs: true, + }); + }, +}); + +field_registry.add('vlift_shuttle_exit_button', ExitButton); + +return { + ExitButton: ExitButton, +}; + +}); diff --git a/stock_vertical_lift/static/src/scss/vertical_lift.scss b/stock_vertical_lift/static/src/scss/vertical_lift.scss new file mode 100644 index 000000000000..11678e055490 --- /dev/null +++ b/stock_vertical_lift/static/src/scss/vertical_lift.scss @@ -0,0 +1,124 @@ +.o_web_client.o_fullscreen { + $o-shuttle-padding: $o-horizontal-padding; + + .o_form_view.o_vlift_shuttle { + display: flex; + flex-flow: column nowrap; + padding: 0; + + font-size: 16px; + + @include media-breakpoint-up(xl) { + font-size: 18px; + } + + .btn { + font-size: 1em; + padding: 1em; + margin: 0 5px; + } + + .o_shuttle_header { + display: flex; + flex-flow: row wrap; + padding: $o-shuttle-padding; + } + + .o_shuttle_header_content { + display: flex; + flex-flow: row nowrap; + font-size: 2.0em; + flex: 1 0 auto; + align-items: center; + width: 33%; + + &.o_shuttle_header_right { + justify-content: flex-end; + } + } + + .o_shuttle_actions { + display: flex; + flex-flow: row nowrap; + font-size: 1.2em; + padding: $o-shuttle-padding * 0.5; + } + + .o_shuttle_operation { + text-align: center; + font-size: 2.5em; + padding: 0.5em; + color: #ffffff; + } + + .o_shuttle_content { + display: flex; + flex-flow: row nowrap; + flex: 1 0 auto; + align-items: center; + + &.o_shuttle_content_right { + justify-content: flex-end; + } + } + + .o_shuttle_data { + display: flex; + flex-flow: row wrap; + padding: $o-shuttle-padding * 0.5; + + .o_shuttle_data_content { + flex-flow: row nowrap; + font-size: 1.2em; + flex: 1 0 auto; + align-items: center; + width: 50%; + + &.o_shuttle_tray { + display: flex; + justify-content: flex-end; + + .o_group { + display: block; + } + } + + .o_field_location_tray_matrix { + width: 450px; + } + } + + .o_shuttle_highlight { + padding: 6px; + border-radius: 10px; + } + } + + } + + .o_vlift_shuttle_menu { + .btn { + margin-bottom: $o-shuttle-padding; + padding: 1em; + font-size: 2em; + text-transform: uppercase; + } + + .o_shuttle_exit { + text-align: center; + } + } + + .o_vlift_shuttle_manual_barcode { + .o_field_char { + padding: 1em; + font-size: 2em; + } + + .btn { + padding: 1em; + font-size: 2em; + text-transform: uppercase; + } + } +} diff --git a/stock_vertical_lift/tests/__init__.py b/stock_vertical_lift/tests/__init__.py new file mode 100644 index 000000000000..c73943e06e0c --- /dev/null +++ b/stock_vertical_lift/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_location +from . import test_vertical_lift_shuttle diff --git a/stock_vertical_lift/tests/common.py b/stock_vertical_lift/tests/common.py new file mode 100644 index 000000000000..811121caef61 --- /dev/null +++ b/stock_vertical_lift/tests/common.py @@ -0,0 +1,49 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.stock_location_tray.tests import common + + +class VerticalLiftCase(common.LocationTrayTypeCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.shuttle = cls.env.ref( + 'stock_vertical_lift.stock_vertical_lift_demo_shuttle_1' + ) + cls.product_socks = cls.env.ref( + 'stock_vertical_lift.product_running_socks' + ) + cls.vertical_lift_loc = cls.env.ref( + 'stock_vertical_lift.stock_location_vertical_lift' + ) + + @classmethod + def _create_simple_picking_out(cls, product, quantity): + stock_loc = cls.env.ref('stock.stock_location_stock') + customer_loc = cls.env.ref('stock.stock_location_customers') + picking_type = cls.env.ref('stock.picking_type_out') + partner = cls.env.ref('base.res_partner_1') + return cls.env['stock.picking'].create( + { + 'picking_type_id': picking_type.id, + 'partner_id': partner.id, + 'location_id': stock_loc.id, + 'location_dest_id': customer_loc.id, + 'move_lines': [ + ( + 0, + 0, + { + 'name': product.name, + 'product_id': product.id, + 'product_uom': product.uom_id.id, + 'product_uom_qty': quantity, + 'picking_type_id': picking_type.id, + 'location_id': stock_loc.id, + 'location_dest_id': customer_loc.id, + }, + ) + ], + } + ) diff --git a/stock_vertical_lift/tests/test_location.py b/stock_vertical_lift/tests/test_location.py new file mode 100644 index 000000000000..710c9cdc0a5c --- /dev/null +++ b/stock_vertical_lift/tests/test_location.py @@ -0,0 +1,40 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from .common import VerticalLiftCase + + +class TestVerticalLiftLocation(VerticalLiftCase): + def test_vertical_lift_kind(self): + # this boolean is what defines a "Vertical Lift View", the upper level + # of the tree (View -> Shuttles -> Trays -> Cells) + self.assertTrue(self.vertical_lift_loc.vertical_lift_location) + self.assertEqual(self.vertical_lift_loc.vertical_lift_kind, 'view') + + # check types accross the hierarchy + shuttles = self.vertical_lift_loc.child_ids + self.assertTrue( + all( + location.vertical_lift_kind == 'shuttle' + for location in shuttles + ) + ) + trays = shuttles.mapped('child_ids') + self.assertTrue( + all(location.vertical_lift_kind == 'tray' for location in trays) + ) + cells = trays.mapped('child_ids') + self.assertTrue( + all(location.vertical_lift_kind == 'cell' for location in cells) + ) + + def test_create_shuttle(self): + # any location created directly under the view is a shuttle + shuttle_loc = self.env['stock.location'].create( + { + 'name': 'Shuttle 42', + 'location_id': self.vertical_lift_loc.id, + 'usage': 'internal', + } + ) + self.assertEqual(shuttle_loc.vertical_lift_kind, 'shuttle') diff --git a/stock_vertical_lift/tests/test_vertical_lift_shuttle.py b/stock_vertical_lift/tests/test_vertical_lift_shuttle.py new file mode 100644 index 000000000000..1575051cbdce --- /dev/null +++ b/stock_vertical_lift/tests/test_vertical_lift_shuttle.py @@ -0,0 +1,252 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import unittest + +from odoo import _, exceptions + +from .common import VerticalLiftCase + + +class TestVerticalLiftTrayType(VerticalLiftCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.picking_out = cls.env.ref( + 'stock_vertical_lift.stock_picking_out_demo_vertical_lift_1' + ) + # we have a move line to pick created by demo picking + # stock_picking_out_demo_vertical_lift_1 + cls.out_move_line = cls.picking_out.move_line_ids + + def test_switch_pick(self): + self.shuttle.switch_pick() + self.assertEqual(self.shuttle.mode, 'pick') + self.assertEqual(self.shuttle.current_move_line_id, self.out_move_line) + + def test_switch_put(self): + self.shuttle.switch_put() + self.assertEqual(self.shuttle.mode, 'put') + # TODO check that we have an incoming move when switching + self.assertEqual( + self.shuttle.current_move_line_id, + self.env['stock.move.line'].browse(), + ) + + def test_switch_inventory(self): + self.shuttle.switch_inventory() + self.assertEqual(self.shuttle.mode, 'inventory') + # TODO check that we have what we should (what?) + self.assertEqual( + self.shuttle.current_move_line_id, + self.env['stock.move.line'].browse(), + ) + + def test_pick_action_open_screen(self): + self.shuttle.switch_pick() + action = self.shuttle.action_open_screen() + self.assertTrue(self.shuttle.current_move_line_id) + self.assertEqual(action['type'], 'ir.actions.act_window') + self.assertEqual(action['res_model'], 'vertical.lift.shuttle') + self.assertEqual(action['res_id'], self.shuttle.id) + + def test_pick_select_next_move_line(self): + self.shuttle.switch_pick() + self.shuttle.select_next_move_line() + self.assertEqual(self.shuttle.current_move_line_id, self.out_move_line) + self.assertEqual( + self.shuttle.operation_descr, + _('Scan New Destination Location') + ) + + def test_pick_save(self): + self.shuttle.switch_pick() + self.shuttle.current_move_line_id = self.out_move_line + self.shuttle.button_save() + self.assertEqual( + self.shuttle.current_move_line_id.state, + 'done' + ) + self.assertEqual(self.shuttle.operation_descr, _('Release')) + + def test_pick_related_fields(self): + self.shuttle.switch_pick() + ml = self.shuttle.current_move_line_id = self.out_move_line + + # Trays related fields + # For pick, this is the source location, which is the cell where the + # product is. + self.assertEqual(self.shuttle.tray_location_id, ml.location_id) + self.assertEqual( + self.shuttle.tray_name, + # parent = tray + ml.location_id.location_id.name, + ) + self.assertEqual( + self.shuttle.tray_type_id, + # the tray type is on the parent of the cell (on the tray) + ml.location_id.location_id.tray_type_id, + ) + self.assertEqual( + self.shuttle.tray_type_code, + ml.location_id.location_id.tray_type_id.code, + ) + self.assertEqual(self.shuttle.tray_x, ml.location_id.posx) + self.assertEqual(self.shuttle.tray_y, ml.location_id.posy) + + # Move line related fields + self.assertEqual(self.shuttle.picking_id, ml.picking_id) + self.assertEqual(self.shuttle.picking_origin, ml.picking_id.origin) + self.assertEqual( + self.shuttle.picking_partner_id, ml.picking_id.partner_id + ) + self.assertEqual(self.shuttle.product_id, ml.product_id) + self.assertEqual(self.shuttle.product_uom_id, ml.product_uom_id) + self.assertEqual(self.shuttle.product_uom_qty, ml.product_uom_qty) + self.assertEqual(self.shuttle.qty_done, ml.qty_done) + self.assertEqual(self.shuttle.lot_id, ml.lot_id) + + def test_pick_count_move_lines(self): + product1 = self.env.ref('stock_vertical_lift.product_running_socks') + product2 = self.env.ref('stock_vertical_lift.product_recovery_socks') + # cancel the picking from demo data to start from a clean state + self.env.ref( + 'stock_vertical_lift.stock_picking_out_demo_vertical_lift_1' + ).action_cancel() + + # ensure that we have stock in some cells, we'll put product1 + # in the first Shuttle and product2 in the second + cell1 = self.env.ref( + 'stock_vertical_lift.' + 'stock_location_vertical_lift_demo_tray_1a_x3y2' + ) + self._update_quantity_in_cell(cell1, product1, 50) + cell2 = self.env.ref( + 'stock_vertical_lift.' + 'stock_location_vertical_lift_demo_tray_2a_x1y1' + ) + self._update_quantity_in_cell(cell2, product2, 50) + + # create pickings (we already have an existing one from demo data) + pickings = self.env['stock.picking'].browse() + pickings |= self._create_simple_picking_out(product1, 1) + pickings |= self._create_simple_picking_out(product1, 1) + pickings |= self._create_simple_picking_out(product1, 1) + pickings |= self._create_simple_picking_out(product1, 1) + pickings |= self._create_simple_picking_out(product2, 20) + pickings |= self._create_simple_picking_out(product2, 30) + # this one should be 'assigned', so should be included in the operation + # count + unassigned = self._create_simple_picking_out(product2, 1) + pickings |= unassigned + pickings.action_confirm() + # product1 will be taken from the shuttle1, product2 from shuttle2 + pickings.action_assign() + + shuttle1 = self.shuttle + shuttle2 = self.env.ref( + 'stock_vertical_lift.stock_vertical_lift_demo_shuttle_2' + ) + + self.assertEqual(shuttle1.number_of_ops, 4) + self.assertEqual(shuttle2.number_of_ops, 2) + self.assertEqual(shuttle1.number_of_ops_all, 6) + self.assertEqual(shuttle2.number_of_ops_all, 6) + + # Process a line, should change the numbers. + shuttle1.select_next_move_line() + shuttle1.process_current_pick() + self.assertEqual(shuttle1.number_of_ops, 3) + self.assertEqual(shuttle2.number_of_ops, 2) + self.assertEqual(shuttle1.number_of_ops_all, 5) + self.assertEqual(shuttle2.number_of_ops_all, 5) + + # add stock and make the last one assigned to check the number is + # updated + self._update_quantity_in_cell(cell2, product2, 10) + unassigned.action_assign() + self.assertEqual(shuttle1.number_of_ops, 3) + self.assertEqual(shuttle2.number_of_ops, 3) + self.assertEqual(shuttle1.number_of_ops_all, 6) + self.assertEqual(shuttle2.number_of_ops_all, 6) + + @unittest.skip('Not implemented') + def test_put_count_move_lines(self): + pass + + @unittest.skip('Not implemented') + def test_inventory_count_move_lines(self): + pass + + @unittest.skip('Not implemented') + def test_on_barcode_scanned(self): + # test to implement when the code is implemented + pass + + def test_button_release(self): + # for the test, we'll consider our last line has been delivered + self.out_move_line.qty_done = self.out_move_line.product_qty + self.out_move_line.move_id._action_done() + # release, no further operation in queue + result = self.shuttle.button_release() + self.assertFalse(self.shuttle.current_move_line_id) + self.assertEqual(self.shuttle.operation_descr, _('No operations')) + expected_result = { + 'effect': { + 'fadeout': 'slow', + 'message': _('Congrats, you cleared the queue!'), + 'img_url': '/web/static/src/img/smile.svg', + 'type': 'rainbow_man', + } + } + self.assertEqual(result, expected_result) + + def test_process_current_pick(self): + self.shuttle.switch_pick() + self.shuttle.current_move_line_id = self.out_move_line + qty_to_process = self.out_move_line.product_qty + self.shuttle.process_current_pick() + self.assertEqual(self.out_move_line.state, 'done') + self.assertEqual(self.out_move_line.qty_done, qty_to_process) + + def test_process_current_put(self): + # test to implement when the code is implemented + with self.assertRaises(exceptions.UserError): + self.shuttle.process_current_put() + + def test_process_current_inventory(self): + # test to implement when the code is implemented + with self.assertRaises(exceptions.UserError): + self.shuttle.process_current_inventory() + + def test_matrix(self): + self.shuttle.switch_pick() + self.shuttle.current_move_line_id = self.out_move_line + location = self.out_move_line.location_id + # offset by -1 because the fields are for humans + expected_x = location.posx - 1 + expected_y = location.posy - 1 + self.assertEqual( + self.shuttle.tray_matrix, + { + 'selected': [expected_x, expected_y], + # fmt: off + 'cells': [ + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + ] + # fmt: on + }, + ) + + def test_tray_qty(self): + cell = self.env.ref( + 'stock_vertical_lift.' + 'stock_location_vertical_lift_demo_tray_1a_x3y2' + ) + self.out_move_line.location_id = cell + self.shuttle.current_move_line_id = self.out_move_line + self._update_quantity_in_cell(cell, self.out_move_line.product_id, 50) + self.assertEqual(self.shuttle.tray_qty, 50) + self._update_quantity_in_cell(cell, self.out_move_line.product_id, -20) + self.assertEqual(self.shuttle.tray_qty, 30) diff --git a/stock_vertical_lift/views/shuttle_screen_templates.xml b/stock_vertical_lift/views/shuttle_screen_templates.xml new file mode 100644 index 000000000000..3301f4ca70df --- /dev/null +++ b/stock_vertical_lift/views/shuttle_screen_templates.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/stock_vertical_lift/views/stock_location_views.xml b/stock_vertical_lift/views/stock_location_views.xml new file mode 100644 index 000000000000..930d7a179008 --- /dev/null +++ b/stock_vertical_lift/views/stock_location_views.xml @@ -0,0 +1,35 @@ + + + + + stock.location.form.vertical.lift + stock.location + + + + + + + + + {'invisible': [('cell_in_tray_type_id', '!=', False)], + 'required': [('vertical_lift_kind', '=', 'tray')]} + + + + + + + stock.location.search.vertical.lift + stock.location + + + + + + + + + diff --git a/stock_vertical_lift/views/stock_vertical_lift_templates.xml b/stock_vertical_lift/views/stock_vertical_lift_templates.xml new file mode 100644 index 000000000000..d1931819c9d2 --- /dev/null +++ b/stock_vertical_lift/views/stock_vertical_lift_templates.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml new file mode 100644 index 000000000000..6d5bb6d53502 --- /dev/null +++ b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml @@ -0,0 +1,287 @@ + + + + + vertical.lift.shuttle.view.form.screen + vertical.lift.shuttle + 99 + +
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ + + +
+
+ +
+ + + + + + + +
+ +
+
+
+
+ + +
+
+ + + vertical.lift.shuttle.view.form.menu + vertical.lift.shuttle + 100 + +
+
+
+
+
+ +
+
+
+
+
+
+ + + vertical.lift.shuttle.manual.barcode.view.form + vertical.lift.shuttle.manual.barcode + +
+
+
+ +
+
+
+
+
+
+
+
+ + + vertical.lift.shuttle.view.form + vertical.lift.shuttle + +
+ + + + + + + + +
+
+
+ + + vertical.lift.shuttle.kanban + vertical.lift.shuttle + + + + + + + + +
+
+ +
+
+
+ + + +
+
+
+ Mode: + +
+
+ Operations: + +
+
+ All Operations: + +
+
+
+
+ + + +
+
+
+
+
+
+
+ + + vertical.lift.shuttle.tree + vertical.lift.shuttle + + + + + + + + + Vertical Lift Shuttles + ir.actions.act_window + vertical.lift.shuttle + form + kanban,tree,form + current + [] + {} + +

+ Open the Shuttle Interface. +

+
+
+ + + +
diff --git a/stock_vertical_lift_kardex/__init__.py b/stock_vertical_lift_kardex/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /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 000000000000..35670055d868 --- /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 000000000000..e99db92f6582 --- /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 000000000000..cfd3c74b9dbf --- /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 000000000000..48286263cd35 --- /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 000000000000..c4b64e9b08ac --- /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 000000000000..3d8aefd2ecb3 --- /dev/null +++ b/stock_vertical_lift_kardex/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Add support of the hardware From e3020a256742e6fc003abef94f48a16481b673d1 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 8 Oct 2019 17:07:20 +0200 Subject: [PATCH 02/41] 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/__manifest__.py | 1 + stock_vertical_lift/models/__init__.py | 1 + stock_vertical_lift/models/stock_location.py | 140 ++++++++++++++++-- stock_vertical_lift/models/stock_move_line.py | 18 +++ .../models/vertical_lift_shuttle.py | 42 +----- .../views/stock_location_views.xml | 3 + .../views/stock_move_line_views.xml | 30 ++++ stock_vertical_lift_kardex/models/__init__.py | 1 + .../models/stock_location.py | 56 +++++++ 9 files changed, 245 insertions(+), 47 deletions(-) create mode 100644 stock_vertical_lift/models/stock_move_line.py create mode 100644 stock_vertical_lift/views/stock_move_line_views.xml create mode 100644 stock_vertical_lift_kardex/models/stock_location.py diff --git a/stock_vertical_lift/__manifest__.py b/stock_vertical_lift/__manifest__.py index e259e71f48db..580fc0706edf 100644 --- a/stock_vertical_lift/__manifest__.py +++ b/stock_vertical_lift/__manifest__.py @@ -24,6 +24,7 @@ ], 'data': [ 'views/stock_location_views.xml', + 'views/stock_move_line_views.xml', 'views/vertical_lift_shuttle_views.xml', 'views/stock_vertical_lift_templates.xml', 'views/shuttle_screen_templates.xml', diff --git a/stock_vertical_lift/models/__init__.py b/stock_vertical_lift/models/__init__.py index 2a0e1d40b651..80b7355db36a 100644 --- a/stock_vertical_lift/models/__init__.py +++ b/stock_vertical_lift/models/__init__.py @@ -1,4 +1,5 @@ from . import vertical_lift_shuttle from . import stock_location from . import stock_move +from . import stock_move_line from . import stock_quant diff --git a/stock_vertical_lift/models/stock_location.py b/stock_vertical_lift/models/stock_location.py index a8d19e0a03b3..93e02cf6513b 100644 --- a/stock_vertical_lift/models/stock_location.py +++ b/stock_vertical_lift/models/stock_location.py @@ -1,40 +1,156 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import _, api, exceptions, fields, models class StockLocation(models.Model): _inherit = "stock.location" vertical_lift_location = fields.Boolean( - 'Is a Vertical Lift View Location?', + "Is a Vertical Lift View Location?", default=False, help="Check this box to use it as the view for Vertical" " Lift Shuttles.", ) vertical_lift_kind = fields.Selection( selection=[ - ('view', 'View'), - ('shuttle', 'Shuttle'), - ('tray', 'Tray'), - ('cell', 'Cell'), + ("view", "View"), + ("shuttle", "Shuttle"), + ("tray", "Tray"), + ("cell", "Cell"), ], - compute='_compute_vertical_lift_kind', + compute="_compute_vertical_lift_kind", + store=True, + ) + + # This is a one2one in practice, but this one is not really interesting. + # It's there only to be in the depends of 'vertical_lift_shuttle_id', which + # give the unique shuttle for any location in the tree (whether it's a + # shuttle, a tray or a cell) + inverse_vertical_lift_shuttle_ids = fields.One2many( + comodel_name="vertical.lift.shuttle", + inverse_name="location_id", + readonly=True, + ) + # compute the unique shuttle for any shuttle, tray or cell location, by + # going through the parents + vertical_lift_shuttle_id = fields.Many2one( + comodel_name="vertical.lift.shuttle", + compute="_compute_vertical_lift_shuttle_id", store=True, ) @api.depends( - 'location_id', - 'location_id.vertical_lift_kind', - 'vertical_lift_location', + "location_id", + "location_id.vertical_lift_kind", + "vertical_lift_location", ) def _compute_vertical_lift_kind(self): - tree = {'view': 'shuttle', 'shuttle': 'tray', 'tray': 'cell'} + tree = {"view": "shuttle", "shuttle": "tray", "tray": "cell"} for location in self: if location.vertical_lift_location: - location.vertical_lift_kind = 'view' + location.vertical_lift_kind = "view" continue kind = tree.get(location.location_id.vertical_lift_kind) if kind: location.vertical_lift_kind = kind + + @api.depends( + "inverse_vertical_lift_shuttle_ids", + "location_id.vertical_lift_shuttle_id", + ) + def _compute_vertical_lift_shuttle_id(self): + for location in self: + if location.inverse_vertical_lift_shuttle_ids: + # we have a unique constraint on the other side + assert len(location.inverse_vertical_lift_shuttle_ids) == 1 + shuttle = location.inverse_vertical_lift_shuttle_ids + else: + shuttle = location.location_id.vertical_lift_shuttle_id + location.vertical_lift_shuttle_id = shuttle + + 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 == "simulation": + message = _("Opening tray {}.").format(self.name) + if cell_location: + from_left, from_bottom = ( + cell_location.tray_cell_center_position() + ) + message += _( + "
Laser pointer on x{} y{} ({}mm, {}mm)" + ).format( + cell_location.posx, + cell_location.posy, + from_left, + from_bottom, + ) + self.env.user.notify_info( + message=message, title=_("Lift Simulation") + ) + + def fetch_vertical_lift_tray(self, cell_location=None): + """Send instructions to the vertical lift hardware + + Public method to use for: + * fetch the vertical lift tray and presenting it to the operator + (physically) + * direct the laser pointer to the cell's location if set + + Depending on the hardware, the laser pointer may not be implemented. + + The actual implementation of the method goes in the private method + ``_hardware_vertical_lift_tray()``. + """ + self.ensure_one() + if self.vertical_lift_kind == "cell": + if cell_location: + raise ValueError( + "cell_location cannot be set when the location is " + "a cell." + ) + tray = self.location_id + tray.fetch_vertical_lift_tray(cell_location=self) + elif self.vertical_lift_kind == "tray": + self._hardware_vertical_lift_tray(cell_location=cell_location) + else: + raise exceptions.UserError( + _("Cannot fetch a vertical lift tray on location %s") + % (self.name,) + ) + return True diff --git a/stock_vertical_lift/models/stock_move_line.py b/stock_vertical_lift/models/stock_move_line.py new file mode 100644 index 000000000000..a26873c0aeb2 --- /dev/null +++ b/stock_vertical_lift/models/stock_move_line.py @@ -0,0 +1,18 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + def fetch_vertical_lift_tray_source(self): + self.ensure_one() + self.location_id.fetch_vertical_lift_tray() + return {"type": "ir.actions.do_nothing"} + + def fetch_vertical_lift_tray_dest(self): + self.ensure_one() + self.location_dest_id.fetch_vertical_lift_tray() + return {"type": "ir.actions.do_nothing"} diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py index 54ec752d269e..fcdb4a3bd98d 100644 --- a/stock_vertical_lift/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift/models/vertical_lift_shuttle.py @@ -107,6 +107,11 @@ class VerticalLiftShuttle(models.Model): store=False, ) + _sql_constraints = [ + ('location_id_unique', 'UNIQUE(location_id)', + 'You cannot have two shuttles using the same location.'), + ] + def on_barcode_scanned(self, barcode): self.ensure_one() # FIXME notify_info is only for the demo @@ -258,9 +263,6 @@ def count_move_lines_to_do_all(self): ) def button_release(self): - if self.current_move_line_id: - self._hardware_switch_off_laser_pointer() - self._hardware_close_tray() self.select_next_move_line() if not self.current_move_line_id: # sorry not sorry @@ -310,8 +312,8 @@ def select_next_move_line(self): ) self.operation_descr = descr if next_move_line: - self._hardware_switch_on_laser_pointer() - self._hardware_open_tray() + # TODO different method (source vs dest) on pick/put scenario + next_move_line.fetch_vertical_lift_tray_source() def action_open_screen(self): self.select_next_move_line() @@ -365,36 +367,6 @@ def switch_inventory(self): self.mode = 'inventory' self.select_next_move_line() - def _hardware_switch_on_laser_pointer(self): - if self.hardware == 'simulation': - self.env.user.notify_info( - message=_('Laser pointer on x{} y{}').format( - self.tray_x, self.tray_y - ), - title=_('Lift Simulation'), - ) - - def _hardware_switch_off_laser_pointer(self): - if self.hardware == 'simulation': - self.env.user.notify_info( - message=_('Switch off laser pointer'), - title=_('Lift Simulation'), - ) - - def _hardware_open_tray(self): - if self.hardware == 'simulation': - self.env.user.notify_info( - message=_('Opening tray {}').format(self.tray_name), - title=_('Lift Simulation'), - ) - - def _hardware_close_tray(self): - if self.hardware == 'simulation': - self.env.user.notify_info( - message=_('Closing tray {}').format(self.tray_name), - title=_('Lift Simulation'), - ) - class VerticalLiftShuttleManualBarcode(models.TransientModel): _name = 'vertical.lift.shuttle.manual.barcode' diff --git a/stock_vertical_lift/views/stock_location_views.xml b/stock_vertical_lift/views/stock_location_views.xml index 930d7a179008..62c6e2996d0b 100644 --- a/stock_vertical_lift/views/stock_location_views.xml +++ b/stock_vertical_lift/views/stock_location_views.xml @@ -11,6 +11,8 @@ attrs="{'invisible': [('vertical_lift_kind', '!=', False), ('vertical_lift_kind', '!=', 'view')]}" /> + @@ -28,6 +30,7 @@ + diff --git a/stock_vertical_lift/views/stock_move_line_views.xml b/stock_vertical_lift/views/stock_move_line_views.xml new file mode 100644 index 000000000000..39249814e392 --- /dev/null +++ b/stock_vertical_lift/views/stock_move_line_views.xml @@ -0,0 +1,30 @@ + + + + + stock.move.line.operations.tree.tray.type + stock.move.line + + + + + + + + diff --git a/stock_vertical_lift_kardex/models/__init__.py b/stock_vertical_lift_kardex/models/__init__.py index e99db92f6582..51a3830f6e9a 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 000000000000..d1098d59a124 --- /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 ab764e13fcbf0cdfcdb83d8692629a071bb3e12d Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 9 Oct 2019 17:09:08 +0200 Subject: [PATCH 03/41] Split the shuttle operations in different models/views Namely, the pick/put/inventory operations are now split in different models. Pick and Put share a model and customize their behavior, which is pretty similar. The inventory operation will have a different view and different workflow. This changes will ease a lot the customization of the different workflows and views. --- stock_vertical_lift/__manifest__.py | 4 + stock_vertical_lift/models/__init__.py | 4 + stock_vertical_lift/models/stock_move.py | 23 +- stock_vertical_lift/models/stock_quant.py | 13 +- .../models/vertical_lift_operation_base.py | 264 ++++++++++++ .../vertical_lift_operation_inventory.py | 10 + .../models/vertical_lift_operation_pick.py | 53 +++ .../models/vertical_lift_operation_put.py | 35 ++ .../models/vertical_lift_shuttle.py | 403 ++++-------------- .../security/ir.model.access.csv | 3 + .../tests/test_vertical_lift_shuttle.py | 187 ++++---- .../vertical_lift_operation_base_views.xml | 147 +++++++ ...ertical_lift_operation_inventory_views.xml | 19 + .../vertical_lift_operation_pick_views.xml | 16 + .../vertical_lift_operation_put_views.xml | 16 + .../views/vertical_lift_shuttle_views.xml | 150 +------ 16 files changed, 781 insertions(+), 566 deletions(-) create mode 100644 stock_vertical_lift/models/vertical_lift_operation_base.py create mode 100644 stock_vertical_lift/models/vertical_lift_operation_inventory.py create mode 100644 stock_vertical_lift/models/vertical_lift_operation_pick.py create mode 100644 stock_vertical_lift/models/vertical_lift_operation_put.py create mode 100644 stock_vertical_lift/views/vertical_lift_operation_base_views.xml create mode 100644 stock_vertical_lift/views/vertical_lift_operation_inventory_views.xml create mode 100644 stock_vertical_lift/views/vertical_lift_operation_pick_views.xml create mode 100644 stock_vertical_lift/views/vertical_lift_operation_put_views.xml diff --git a/stock_vertical_lift/__manifest__.py b/stock_vertical_lift/__manifest__.py index 580fc0706edf..7d97d1741d11 100644 --- a/stock_vertical_lift/__manifest__.py +++ b/stock_vertical_lift/__manifest__.py @@ -26,6 +26,10 @@ 'views/stock_location_views.xml', 'views/stock_move_line_views.xml', 'views/vertical_lift_shuttle_views.xml', + 'views/vertical_lift_operation_base_views.xml', + 'views/vertical_lift_operation_pick_views.xml', + 'views/vertical_lift_operation_put_views.xml', + 'views/vertical_lift_operation_inventory_views.xml', 'views/stock_vertical_lift_templates.xml', 'views/shuttle_screen_templates.xml', 'security/ir.model.access.csv', diff --git a/stock_vertical_lift/models/__init__.py b/stock_vertical_lift/models/__init__.py index 80b7355db36a..839f0488889b 100644 --- a/stock_vertical_lift/models/__init__.py +++ b/stock_vertical_lift/models/__init__.py @@ -1,4 +1,8 @@ from . import vertical_lift_shuttle +from . import vertical_lift_operation_base +from . import vertical_lift_operation_pick +from . import vertical_lift_operation_put +from . import vertical_lift_operation_inventory from . import stock_location from . import stock_move from . import stock_move_line diff --git a/stock_vertical_lift/models/stock_move.py b/stock_vertical_lift/models/stock_move.py index 1210d6b09c1b..42222e716edd 100644 --- a/stock_vertical_lift/models/stock_move.py +++ b/stock_vertical_lift/models/stock_move.py @@ -5,19 +5,24 @@ class StockMove(models.Model): - _inherit = 'stock.move' + _inherit = "stock.move" @api.multi def write(self, vals): result = super().write(vals) - if 'state' in vals: + if "state" in vals: # We cannot have fields to depends on to invalidate these computed - # fields on vertical.lift.shuttle. But we know that when the state - # of any move line changes, we can invalidate them as the count of - # assigned move lines may change (and we track this in stock.move, - # not stock.move.line, becaus the state of the lines is a related - # to this one). - self.env['vertical.lift.shuttle'].invalidate_cache( - ['number_of_ops', 'number_of_ops_all'] + # fields on vertical.lift.operation.*. But we know that when the + # state of any move line changes, we can invalidate them as the + # count of assigned move lines may change (and we track this in + # stock.move, not stock.move.line, because the state of the lines + # is a related to this one). + models = ( + "vertical.lift.operation.pick", + "vertical.lift.operation.put", ) + for model in models: + self.env[model].invalidate_cache( + ["number_of_ops", "number_of_ops_all"] + ) return result diff --git a/stock_vertical_lift/models/stock_quant.py b/stock_vertical_lift/models/stock_quant.py index 905484139a7e..5a5ba4f86aa8 100644 --- a/stock_vertical_lift/models/stock_quant.py +++ b/stock_vertical_lift/models/stock_quant.py @@ -10,7 +10,14 @@ class StockQuant(models.Model): def _update_available_quantity(self, *args, **kwargs): result = super()._update_available_quantity(*args, **kwargs) # We cannot have fields to depends on to invalidate this computed - # fields on vertical.lift.shuttle. But we know that when the quantity - # of quant changes, we can invalidate the field on the shuttles. - self.env['vertical.lift.shuttle'].invalidate_cache(['tray_qty']) + # fields on vertical.lift.operation.* models. But we know that when the + # quantity of quant changes, we can invalidate the field + models = ( + "vertical.lift.operation.pick", + "vertical.lift.operation.put", + ) + for model in models: + self.env[model].invalidate_cache( + ["tray_qty"] + ) return result diff --git a/stock_vertical_lift/models/vertical_lift_operation_base.py b/stock_vertical_lift/models/vertical_lift_operation_base.py new file mode 100644 index 000000000000..a23cdc2ec1f2 --- /dev/null +++ b/stock_vertical_lift/models/vertical_lift_operation_base.py @@ -0,0 +1,264 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.addons.base_sparse_field.models.fields import Serialized + + +class VerticalLiftOperationBase(models.AbstractModel): + """Base model for shuttle operations (pick, put, inventory)""" + + _name = "vertical.lift.operation.base" + _inherit = "barcodes.barcode_events_mixin" + _description = "Vertical Lift Operation - Base" + + name = fields.Char(related="shuttle_id.name", readonly=True) + shuttle_id = fields.Many2one( + comodel_name="vertical.lift.shuttle", required=True, readonly=True + ) + location_id = fields.Many2one( + related="shuttle_id.location_id", readonly=True + ) + mode = fields.Selection(related="shuttle_id.mode", readonly=True) + operation_descr = fields.Char( + string="Operation", default="...", readonly=True + ) + + _sql_constraints = [ + ( + "shuttle_id_unique", + "UNIQUE(shuttle_id)", + "One pick can be run at a time for a shuttle.", + ) + ] + + def on_barcode_scanned(self, barcode): + self.ensure_one() + # to implement in sub-classes + + def on_screen_open(self): + """Called when the screen is open + + To implement in sub-classes. + """ + + def action_open_screen(self): + return self.shuttle_id.action_open_screen() + + def action_menu(self): + return self.shuttle_id.action_menu() + + def action_manual_barcode(self): + return self.shuttle_id.action_manual_barcode() + + +class VerticalLiftOperationTransfer(models.AbstractModel): + """Base model for shuttle pick and put operations""" + + _name = "vertical.lift.operation.transfer" + _inherit = "vertical.lift.operation.base" + _description = "Vertical Lift Operation - Transfer" + + current_move_line_id = fields.Many2one(comodel_name="stock.move.line") + + number_of_ops = fields.Integer( + compute="_compute_number_of_ops", string="Number of Operations" + ) + number_of_ops_all = fields.Integer( + compute="_compute_number_of_ops_all", + string="Number of Operations in all shuttles", + ) + + tray_location_id = fields.Many2one( + comodel_name="stock.location", + compute="_compute_tray_data", + string="Tray Location", + ) + tray_name = fields.Char(compute="_compute_tray_data", string="Tray Name") + tray_type_id = fields.Many2one( + comodel_name="stock.location.tray.type", + compute="_compute_tray_data", + string="Tray Type", + ) + tray_type_code = fields.Char( + compute="_compute_tray_data", string="Tray Code" + ) + tray_x = fields.Integer(string="X", compute="_compute_tray_data") + tray_y = fields.Integer(string="Y", compute="_compute_tray_data") + tray_matrix = Serialized(string="Cells", compute="_compute_tray_data") + tray_qty = fields.Float( + string="Stock Quantity", compute="_compute_tray_qty" + ) + + # current operation information + picking_id = fields.Many2one( + related="current_move_line_id.picking_id", readonly=True + ) + picking_origin = fields.Char( + related="current_move_line_id.picking_id.origin", readonly=True + ) + picking_partner_id = fields.Many2one( + related="current_move_line_id.picking_id.partner_id", readonly=True + ) + product_id = fields.Many2one( + related="current_move_line_id.product_id", readonly=True + ) + product_uom_id = fields.Many2one( + related="current_move_line_id.product_uom_id", readonly=True + ) + product_uom_qty = fields.Float( + related="current_move_line_id.product_uom_qty", readonly=True + ) + product_packagings = fields.Html( + string="Packaging", compute="_compute_product_packagings" + ) + qty_done = fields.Float( + related="current_move_line_id.qty_done", readonly=True + ) + lot_id = fields.Many2one( + related="current_move_line_id.lot_id", readonly=True + ) + location_dest_id = fields.Many2one( + string="Destination", + related="current_move_line_id.location_dest_id", + readonly=True, + ) + # TODO add a glue addon with product_expiry to add the field + + def on_barcode_scanned(self, barcode): + self.ensure_one() + self.env.user.notify_info( + "Scanned barcode: {}. Not implemented.".format(barcode) + ) + + @api.depends("current_move_line_id.product_id.packaging_ids") + def _compute_product_packagings(self): + for record in self: + if not record.current_move_line_id: + continue + product = record.current_move_line_id.product_id + values = { + "packagings": [ + { + "name": pkg.name, + "qty": pkg.qty, + "unit": product.uom_id.name, + } + for pkg in product.packaging_ids + ] + } + content = self.env["ir.qweb"].render( + "stock_vertical_lift.packagings", values + ) + record.product_packagings = content + + @api.depends() + def _compute_number_of_ops(self): + for record in self: + record.number_of_ops = record.count_move_lines_to_do() + + @api.depends() + def _compute_number_of_ops_all(self): + for record in self: + record.number_of_ops_all = record.count_move_lines_to_do_all() + + @api.depends("tray_location_id", "current_move_line_id.product_id") + def _compute_tray_qty(self): + for record in self: + if not (record.tray_location_id and record.current_move_line_id): + continue + product = record.current_move_line_id.product_id + quants = self.env["stock.quant"].search( + [ + ("location_id", "=", record.tray_location_id.id), + ("product_id", "=", product.id), + ] + ) + record.tray_qty = sum(quants.mapped("quantity")) + + # depends of the quantity so we can't have all triggers + @api.depends("current_move_line_id") + def _compute_tray_data(self): + for record in self: + modes = {"pick": "location_id", "put": "location_dest_id"} + location = record.current_move_line_id[modes[record.mode]] + tray_type = location.location_id.tray_type_id + # this is the current cell + record.tray_location_id = location.id + # name of the tray where the cell is + record.tray_name = location.location_id.name + record.tray_type_id = tray_type.id + record.tray_type_code = tray_type.code + record.tray_x = location.posx + record.tray_y = location.posy + record.tray_matrix = location.tray_matrix + + def _domain_move_lines_to_do(self): + # to implement in sub-classes + return [("id", "=", 0)] + + def _domain_move_lines_to_do_all(self): + # to implement in sub-classes + return [("id", "=", 0)] + + def count_move_lines_to_do(self): + """Count move lines to process in current shuttles""" + self.ensure_one() + return self.env["stock.move.line"].search_count( + self._domain_move_lines_to_do() + ) + + def count_move_lines_to_do_all(self): + """Count move lines to process in all shuttles""" + self.ensure_one() + return self.env["stock.move.line"].search_count( + self._domain_move_lines_to_do_all() + ) + + def on_screen_open(self): + """Called when the screen is open""" + self.select_next_move_line() + + def button_release(self): + """Release the operation, go to the next""" + self.select_next_move_line() + if not self.current_move_line_id: + # sorry not sorry + return { + "effect": { + "fadeout": "slow", + "message": _("Congrats, you cleared the queue!"), + "img_url": "/web/static/src/img/smile.svg", + "type": "rainbow_man", + } + } + + def process_current(self): + raise NotImplementedError + + def button_save(self): + if not (self and self.current_move_line_id): + return + self.ensure_one() + self.process_current() + self.operation_descr = _("Release") + + def fetch_tray(self): + return + + def select_next_move_line(self): + self.ensure_one() + next_move_line = self.env["stock.move.line"].search( + self._domain_move_lines_to_do(), limit=1 + ) + self.current_move_line_id = next_move_line + # TODO use a state machine to define next steps and + # description? + descr = ( + _("Scan New Destination Location") + if next_move_line + else _("No operations") + ) + self.operation_descr = descr + if next_move_line: + self.fetch_tray() diff --git a/stock_vertical_lift/models/vertical_lift_operation_inventory.py b/stock_vertical_lift/models/vertical_lift_operation_inventory.py new file mode 100644 index 000000000000..17fd732cda98 --- /dev/null +++ b/stock_vertical_lift/models/vertical_lift_operation_inventory.py @@ -0,0 +1,10 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class VerticalLiftOperationInventory(models.Model): + _name = 'vertical.lift.operation.inventory' + _inherit = 'vertical.lift.operation.base' + _description = 'Vertical Lift Operation Inventory' diff --git a/stock_vertical_lift/models/vertical_lift_operation_pick.py b/stock_vertical_lift/models/vertical_lift_operation_pick.py new file mode 100644 index 000000000000..25bd4017338d --- /dev/null +++ b/stock_vertical_lift/models/vertical_lift_operation_pick.py @@ -0,0 +1,53 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, models + + +class VerticalLiftOperationPick(models.Model): + _name = "vertical.lift.operation.pick" + _inherit = "vertical.lift.operation.transfer" + _description = "Vertical Lift Operation Pick" + + def on_barcode_scanned(self, barcode): + self.ensure_one() + location = self.env["stock.location"].search( + [("barcode", "=", barcode)] + ) + if location: + self.current_move_line_id.location_dest_id = location + self.operation_descr = _("Save") + else: + self.env.user.notify_warning( + _("No location found for barcode {}").format(barcode) + ) + + def _domain_move_lines_to_do(self): + # TODO check domain + domain = [ + ("state", "=", "assigned"), + ("location_id", "child_of", self.location_id.id), + ] + return domain + + def _domain_move_lines_to_do_all(self): + shuttle_locations = self.env["stock.location"].search( + [("vertical_lift_kind", "=", "view")] + ) + # TODO check domain + domain = [ + ("state", "=", "assigned"), + ("location_id", "child_of", shuttle_locations.ids), + ] + return domain + + def fetch_tray(self): + self.current_move_line_id.fetch_vertical_lift_tray_source() + + def process_current(self): + # test code, TODO the smart one + # (scan of barcode increments qty, save calls action_done?) + line = self.current_move_line_id + if line.state != "done": + line.qty_done = line.product_qty + line.move_id._action_done() diff --git a/stock_vertical_lift/models/vertical_lift_operation_put.py b/stock_vertical_lift/models/vertical_lift_operation_put.py new file mode 100644 index 000000000000..92903fb01bc0 --- /dev/null +++ b/stock_vertical_lift/models/vertical_lift_operation_put.py @@ -0,0 +1,35 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, exceptions, models + + +class VerticalLiftOperationPut(models.Model): + _name = "vertical.lift.operation.put" + _inherit = "vertical.lift.operation.transfer" + _description = "Vertical Lift Operation Put" + + def _domain_move_lines_to_do(self): + # TODO check domain + domain = [ + ("state", "=", "assigned"), + ("location_dest_id", "child_of", self.location_id.id), + ] + return domain + + def _domain_move_lines_to_do_all(self): + shuttle_locations = self.env["stock.location"].search( + [("vertical_lift_kind", "=", "view")] + ) + domain = [ + # TODO check state + ("state", "=", "assigned"), + ("location_dest_id", "child_of", shuttle_locations.ids), + ] + return domain + + def fetch_tray(self): + self.current_move_line_id.fetch_vertical_lift_tray_dest() + + def process_current(self): + raise exceptions.UserError(_("Put workflow not implemented")) diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py index fcdb4a3bd98d..d3271acb297c 100644 --- a/stock_vertical_lift/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift/models/vertical_lift_shuttle.py @@ -1,384 +1,141 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import _, api, exceptions, fields, models -from odoo.addons.base_sparse_field.models.fields import Serialized +from odoo import _, api, fields, models class VerticalLiftShuttle(models.Model): - _name = 'vertical.lift.shuttle' - _inherit = 'barcodes.barcode_events_mixin' - _description = 'Vertical Lift Shuttle' + _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', + [("pick", "Pick"), ("put", "Put"), ("inventory", "Inventory")], + default="pick", required=True, ) location_id = fields.Many2one( - comodel_name='stock.location', + comodel_name="stock.location", required=True, domain="[('vertical_lift_kind', '=', 'shuttle')]", - ondelete='restrict', + 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 - ) - current_move_line_id = fields.Many2one(comodel_name='stock.move.line') - - number_of_ops = fields.Integer( - compute='_compute_number_of_ops', string='Number of Operations' - ) - number_of_ops_all = fields.Integer( - compute='_compute_number_of_ops_all', - string='Number of Operations in all shuttles', - ) - - operation_descr = fields.Char( - string="Operation", - default="Scan New Destination Location", - readonly=True, - ) - - # tray information (will come from stock.location or a new tray model) - tray_location_id = fields.Many2one( - comodel_name='stock.location', - compute='_compute_tray_matrix', - string='Tray Location', - ) - tray_name = fields.Char(compute='_compute_tray_matrix', string='Tray Name') - tray_type_id = fields.Many2one( - comodel_name='stock.location.tray.type', - compute='_compute_tray_matrix', - string='Tray Type', - ) - tray_type_code = fields.Char( - compute='_compute_tray_matrix', string='Tray Code' - ) - tray_x = fields.Integer(string='X', compute='_compute_tray_matrix') - tray_y = fields.Integer(string='Y', compute='_compute_tray_matrix') - tray_matrix = Serialized(string='Cells', compute='_compute_tray_matrix') - tray_qty = fields.Float( - string='Stock Quantity', compute='_compute_tray_qty' - ) - - # current operation information - picking_id = fields.Many2one( - related='current_move_line_id.picking_id', readonly=True - ) - picking_origin = fields.Char( - related='current_move_line_id.picking_id.origin', readonly=True - ) - picking_partner_id = fields.Many2one( - related='current_move_line_id.picking_id.partner_id', readonly=True - ) - product_id = fields.Many2one( - related='current_move_line_id.product_id', readonly=True - ) - product_uom_id = fields.Many2one( - related='current_move_line_id.product_uom_id', readonly=True - ) - product_uom_qty = fields.Float( - related='current_move_line_id.product_uom_qty', readonly=True - ) - product_packagings = fields.Html( - string='Packaging', compute='_compute_product_packagings' - ) - qty_done = fields.Float( - related='current_move_line_id.qty_done', readonly=True - ) - lot_id = fields.Many2one( - related='current_move_line_id.lot_id', readonly=True - ) - location_dest_id = fields.Many2one( - string="Destination", - related='current_move_line_id.location_dest_id', - readonly=True, - ) - - # TODO add a glue addon with product_expiry to add the field - - _barcode_scanned = fields.Char( - "Barcode Scanned", - help="Value of the last barcode scanned.", - store=False, + selection="_selection_hardware", default="simulation", required=True ) _sql_constraints = [ - ('location_id_unique', 'UNIQUE(location_id)', - 'You cannot have two shuttles using the same location.'), - ] - - def on_barcode_scanned(self, barcode): - self.ensure_one() - # FIXME notify_info is only for the demo - self.env.user.notify_info('Scanned barcode: {}'.format(barcode)) - method = 'on_barcode_scanned_{}'.format(self.mode) - getattr(self, method)(barcode) - - def on_barcode_scanned_pick(self, barcode): - location = self.env['stock.location'].search( - [('barcode', '=', barcode)] + ( + "location_id_unique", + "UNIQUE(location_id)", + "You cannot have two shuttles using the same location.", ) - if location: - self.current_move_line_id.location_dest_id = location - self.operation_descr = _('Save') - else: - self.env.user.notify_warning( - _('No location found for barcode {}').format(barcode) - ) - - def on_barcode_scanned_put(self, barcode): - pass - - def on_barcode_scanned_inventory(self, barcode): - pass + ] @api.model def _selection_hardware(self): - return [('simulation', 'Simulation')] - - @api.depends('current_move_line_id.product_id.packaging_ids') - def _compute_product_packagings(self): - for record in self: - if not record.current_move_line_id: - continue - product = record.current_move_line_id.product_id - values = { - 'packagings': [ - { - 'name': pkg.name, - 'qty': pkg.qty, - 'unit': product.uom_id.name, - } - for pkg in product.packaging_ids - ] - } - content = self.env['ir.qweb'].render( - 'stock_vertical_lift.packagings', values - ) - record.product_packagings = content - - @api.depends() - def _compute_number_of_ops(self): - for record in self: - record.number_of_ops = record.count_move_lines_to_do() - - @api.depends() - def _compute_number_of_ops_all(self): - for record in self: - record.number_of_ops_all = record.count_move_lines_to_do_all() - - @api.depends('tray_location_id', 'current_move_line_id.product_id') - def _compute_tray_qty(self): - for record in self: - if not (record.tray_location_id and record.current_move_line_id): - continue - product = record.current_move_line_id.product_id - quants = self.env['stock.quant'].search( - [ - ('location_id', '=', record.tray_location_id.id), - ('product_id', '=', product.id), - ] - ) - record.tray_qty = sum(quants.mapped('quantity')) - - @api.depends() - def _compute_tray_matrix(self): - for record in self: - modes = { - 'pick': 'location_id', - 'put': 'location_dest_id', - # TODO what to do for inventory? - 'inventory': 'location_id', - } - location = record.current_move_line_id[modes[record.mode]] - tray_type = location.location_id.tray_type_id - selected = [] - cells = [] - if location: - selected = location._tray_cell_coords() - cells = location._tray_cell_matrix() - - # this is the current cell - record.tray_location_id = location.id - # name of the tray where the cell is - record.tray_name = location.location_id.name - record.tray_type_id = tray_type.id - record.tray_type_code = tray_type.code - record.tray_x = location.posx - record.tray_y = location.posy - record.tray_matrix = { - # x, y: position of the selected cell - 'selected': selected, - # 0 is empty, 1 is not - 'cells': cells, - } + return [("simulation", "Simulation")] - def _domain_move_lines_to_do(self): - domain = [ - # TODO check state - ('state', '=', 'assigned') - ] - domain_extensions = { - 'pick': [('location_id', 'child_of', self.location_id.id)], - # TODO ensure that we cannot have the same ml in 2 shuttles (cannot - # happen with 'pick' as they are in the shuttle's location) - 'put': [('location_dest_id', 'child_of', self.location_id.id)], - # TODO - 'inventory': [('id', '=', 0)], + @property + def _model_for_mode(self): + return { + "pick": "vertical.lift.operation.pick", + "put": "vertical.lift.operation.put", + "inventory": "vertical.lift.operation.inventory", } - return domain + domain_extensions[self.mode] - def _domain_move_lines_to_do_all(self): - domain = [ - # TODO check state - ('state', '=', 'assigned') - ] - # TODO search only in the view being a parent of shuttle's location - shuttle_locations = self.env['stock.location'].search( - [('vertical_lift_kind', '=', 'view')] - ) - domain_extensions = { - 'pick': [('location_id', 'child_of', shuttle_locations.ids)], - 'put': [('location_dest_id', 'child_of', shuttle_locations.ids)], - # TODO - 'inventory': [('id', '=', 0)], + @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" + ), } - return domain + domain_extensions[self.mode] - def count_move_lines_to_do(self): - self.ensure_one() - return self.env['stock.move.line'].search_count( - self._domain_move_lines_to_do() - ) - - def count_move_lines_to_do_all(self): - self.ensure_one() - return self.env['stock.move.line'].search_count( - self._domain_move_lines_to_do_all() - ) - - def button_release(self): - self.select_next_move_line() - if not self.current_move_line_id: - # sorry not sorry - return { - 'effect': { - 'fadeout': 'slow', - 'message': _('Congrats, you cleared the queue!'), - 'img_url': '/web/static/src/img/smile.svg', - 'type': 'rainbow_man', - } - } - - def process_current_pick(self): - # test code, TODO the smart one - # (scan of barcode increments qty, save calls action_done?) - line = self.current_move_line_id - if line.state != 'done': - line.qty_done = line.product_qty - line.move_id._action_done() - - def process_current_put(self): - raise exceptions.UserError(_('Put workflow not implemented')) - - def process_current_inventory(self): - raise exceptions.UserError(_('Inventory workflow not implemented')) - - def button_save(self): - if not (self and self.current_move_line_id): - return - self.ensure_one() - method = 'process_current_{}'.format(self.mode) - getattr(self, method)() - self.operation_descr = _('Release') - - def select_next_move_line(self): - self.ensure_one() - next_move_line = self.env['stock.move.line'].search( - self._domain_move_lines_to_do(), limit=1 - ) - self.current_move_line_id = next_move_line - # TODO use a state machine to define next steps and - # description? - descr = ( - _('Scan New Destination Location') - if next_move_line - else _('No operations') - ) - self.operation_descr = descr - if next_move_line: - # TODO different method (source vs dest) on pick/put scenario - next_move_line.fetch_vertical_lift_tray_source() + 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.select_next_move_line() self.ensure_one() - screen_xmlid = ( - 'stock_vertical_lift.vertical_lift_shuttle_view_form_screen' - ) + 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': self._name, - 'views': [[self.env.ref(screen_xmlid).id, 'form']], - 'res_id': self.id, - 'target': 'fullscreen', - 'flags': { - 'headless': True, - 'form_view_initial_mode': 'edit', - 'no_breadcrumbs': True, + "type": "ir.actions.act_window", + "res_model": operation._name, + "views": [[self.env.ref(screen_xmlid).id, "form"]], + "res_id": operation.id, + "target": "fullscreen", + "flags": { + "headless": True, + "form_view_initial_mode": "edit", + "no_breadcrumbs": True, }, } def action_menu(self): - menu_xmlid = 'stock_vertical_lift.vertical_lift_shuttle_form_menu' + 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, + "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_manual_barcode(self): return { - 'type': 'ir.actions.act_window', - 'res_model': 'vertical.lift.shuttle.manual.barcode', - 'view_mode': 'form', - 'name': _('Barcode'), - 'target': 'new', + "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.select_next_move_line() + self.mode = "pick" + return self.action_open_screen() def switch_put(self): - self.mode = 'put' - self.select_next_move_line() + self.mode = "put" + return self.action_open_screen() def switch_inventory(self): - self.mode = 'inventory' - self.select_next_move_line() + self.mode = "inventory" + return self.action_open_screen() class VerticalLiftShuttleManualBarcode(models.TransientModel): - _name = 'vertical.lift.shuttle.manual.barcode' - _description = 'Action to input a barcode' + _name = "vertical.lift.shuttle.manual.barcode" + _description = "Action to input a barcode" barcode = fields.Char(string="Barcode") @api.multi def button_save(self): - shuttle_id = self.env.context.get('active_id') - shuttle = self.env['vertical.lift.shuttle'].browse(shuttle_id).exists() - if not shuttle: + 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: - shuttle.on_barcode_scanned(self.barcode) + record.on_barcode_scanned(self.barcode) diff --git a/stock_vertical_lift/security/ir.model.access.csv b/stock_vertical_lift/security/ir.model.access.csv index dc7cb4b87a83..2a69468e4540 100644 --- a/stock_vertical_lift/security/ir.model.access.csv +++ b/stock_vertical_lift/security/ir.model.access.csv @@ -1,3 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_vertical_lift_shuttle_stock_user,access_vertical_lift_shuttle stock user,model_vertical_lift_shuttle,stock.group_stock_user,1,0,0,0 access_vertical_lift_shuttle_manager,access_vertical_lift_shuttle stock manager,model_vertical_lift_shuttle,stock.group_stock_manager,1,1,1,1 +access_vertical_lift_operation_pick_stock_user,access_vertical_lift_operation_pick stock user,model_vertical_lift_operation_pick,stock.group_stock_user,1,1,1,1 +access_vertical_lift_operation_put_stock_user,access_vertical_lift_operation_put stock user,model_vertical_lift_operation_put,stock.group_stock_user,1,1,1,1 +access_vertical_lift_operation_inventory_stock_user,access_vertical_lift_operation_inventory stock user,model_vertical_lift_operation_inventory,stock.group_stock_user,1,1,1,1 diff --git a/stock_vertical_lift/tests/test_vertical_lift_shuttle.py b/stock_vertical_lift/tests/test_vertical_lift_shuttle.py index 1575051cbdce..b80e46c1e0b4 100644 --- a/stock_vertical_lift/tests/test_vertical_lift_shuttle.py +++ b/stock_vertical_lift/tests/test_vertical_lift_shuttle.py @@ -13,7 +13,7 @@ class TestVerticalLiftTrayType(VerticalLiftCase): def setUpClass(cls): super().setUpClass() cls.picking_out = cls.env.ref( - 'stock_vertical_lift.stock_picking_out_demo_vertical_lift_1' + "stock_vertical_lift.stock_picking_out_demo_vertical_lift_1" ) # we have a move line to pick created by demo picking # stock_picking_out_demo_vertical_lift_1 @@ -21,114 +21,112 @@ def setUpClass(cls): def test_switch_pick(self): self.shuttle.switch_pick() - self.assertEqual(self.shuttle.mode, 'pick') - self.assertEqual(self.shuttle.current_move_line_id, self.out_move_line) + self.assertEqual(self.shuttle.mode, "pick") + self.assertEqual( + self.shuttle._operation_for_mode().current_move_line_id, + self.out_move_line, + ) def test_switch_put(self): self.shuttle.switch_put() - self.assertEqual(self.shuttle.mode, 'put') + self.assertEqual(self.shuttle.mode, "put") # TODO check that we have an incoming move when switching self.assertEqual( - self.shuttle.current_move_line_id, - self.env['stock.move.line'].browse(), + self.shuttle._operation_for_mode().current_move_line_id, + self.env["stock.move.line"].browse(), ) def test_switch_inventory(self): self.shuttle.switch_inventory() - self.assertEqual(self.shuttle.mode, 'inventory') - # TODO check that we have what we should (what?) - self.assertEqual( - self.shuttle.current_move_line_id, - self.env['stock.move.line'].browse(), - ) + self.assertEqual(self.shuttle.mode, "inventory") def test_pick_action_open_screen(self): self.shuttle.switch_pick() action = self.shuttle.action_open_screen() - self.assertTrue(self.shuttle.current_move_line_id) - self.assertEqual(action['type'], 'ir.actions.act_window') - self.assertEqual(action['res_model'], 'vertical.lift.shuttle') - self.assertEqual(action['res_id'], self.shuttle.id) + operation = self.shuttle._operation_for_mode() + self.assertTrue(operation.current_move_line_id) + self.assertEqual(action["type"], "ir.actions.act_window") + self.assertEqual(action["res_model"], "vertical.lift.operation.pick") + self.assertEqual(action["res_id"], operation.id) def test_pick_select_next_move_line(self): - self.shuttle.switch_pick() - self.shuttle.select_next_move_line() - self.assertEqual(self.shuttle.current_move_line_id, self.out_move_line) + self.shuttle.mode = "pick" + operation = self.shuttle._operation_for_mode() + operation.select_next_move_line() + self.assertEqual(operation.current_move_line_id, self.out_move_line) self.assertEqual( - self.shuttle.operation_descr, - _('Scan New Destination Location') + operation.operation_descr, _("Scan New Destination Location") ) def test_pick_save(self): self.shuttle.switch_pick() - self.shuttle.current_move_line_id = self.out_move_line - self.shuttle.button_save() - self.assertEqual( - self.shuttle.current_move_line_id.state, - 'done' - ) - self.assertEqual(self.shuttle.operation_descr, _('Release')) + operation = self.shuttle._operation_for_mode() + operation.current_move_line_id = self.out_move_line + operation.button_save() + self.assertEqual(operation.current_move_line_id.state, "done") + self.assertEqual(operation.operation_descr, _("Release")) def test_pick_related_fields(self): self.shuttle.switch_pick() - ml = self.shuttle.current_move_line_id = self.out_move_line + operation = self.shuttle._operation_for_mode() + ml = operation.current_move_line_id = self.out_move_line # Trays related fields # For pick, this is the source location, which is the cell where the # product is. - self.assertEqual(self.shuttle.tray_location_id, ml.location_id) + self.assertEqual(operation.tray_location_id, ml.location_id) self.assertEqual( - self.shuttle.tray_name, + operation.tray_name, # parent = tray ml.location_id.location_id.name, ) self.assertEqual( - self.shuttle.tray_type_id, + operation.tray_type_id, # the tray type is on the parent of the cell (on the tray) ml.location_id.location_id.tray_type_id, ) self.assertEqual( - self.shuttle.tray_type_code, + operation.tray_type_code, ml.location_id.location_id.tray_type_id.code, ) - self.assertEqual(self.shuttle.tray_x, ml.location_id.posx) - self.assertEqual(self.shuttle.tray_y, ml.location_id.posy) + self.assertEqual(operation.tray_x, ml.location_id.posx) + self.assertEqual(operation.tray_y, ml.location_id.posy) # Move line related fields - self.assertEqual(self.shuttle.picking_id, ml.picking_id) - self.assertEqual(self.shuttle.picking_origin, ml.picking_id.origin) + self.assertEqual(operation.picking_id, ml.picking_id) + self.assertEqual(operation.picking_origin, ml.picking_id.origin) self.assertEqual( - self.shuttle.picking_partner_id, ml.picking_id.partner_id + operation.picking_partner_id, ml.picking_id.partner_id ) - self.assertEqual(self.shuttle.product_id, ml.product_id) - self.assertEqual(self.shuttle.product_uom_id, ml.product_uom_id) - self.assertEqual(self.shuttle.product_uom_qty, ml.product_uom_qty) - self.assertEqual(self.shuttle.qty_done, ml.qty_done) - self.assertEqual(self.shuttle.lot_id, ml.lot_id) + self.assertEqual(operation.product_id, ml.product_id) + self.assertEqual(operation.product_uom_id, ml.product_uom_id) + self.assertEqual(operation.product_uom_qty, ml.product_uom_qty) + self.assertEqual(operation.qty_done, ml.qty_done) + self.assertEqual(operation.lot_id, ml.lot_id) def test_pick_count_move_lines(self): - product1 = self.env.ref('stock_vertical_lift.product_running_socks') - product2 = self.env.ref('stock_vertical_lift.product_recovery_socks') + product1 = self.env.ref("stock_vertical_lift.product_running_socks") + product2 = self.env.ref("stock_vertical_lift.product_recovery_socks") # cancel the picking from demo data to start from a clean state self.env.ref( - 'stock_vertical_lift.stock_picking_out_demo_vertical_lift_1' + "stock_vertical_lift.stock_picking_out_demo_vertical_lift_1" ).action_cancel() # ensure that we have stock in some cells, we'll put product1 # in the first Shuttle and product2 in the second cell1 = self.env.ref( - 'stock_vertical_lift.' - 'stock_location_vertical_lift_demo_tray_1a_x3y2' + "stock_vertical_lift." + "stock_location_vertical_lift_demo_tray_1a_x3y2" ) self._update_quantity_in_cell(cell1, product1, 50) cell2 = self.env.ref( - 'stock_vertical_lift.' - 'stock_location_vertical_lift_demo_tray_2a_x1y1' + "stock_vertical_lift." + "stock_location_vertical_lift_demo_tray_2a_x1y1" ) self._update_quantity_in_cell(cell2, product2, 50) # create pickings (we already have an existing one from demo data) - pickings = self.env['stock.picking'].browse() + pickings = self.env["stock.picking"].browse() pickings |= self._create_simple_picking_out(product1, 1) pickings |= self._create_simple_picking_out(product1, 1) pickings |= self._create_simple_picking_out(product1, 1) @@ -144,41 +142,43 @@ def test_pick_count_move_lines(self): pickings.action_assign() shuttle1 = self.shuttle + operation1 = shuttle1._operation_for_mode() shuttle2 = self.env.ref( - 'stock_vertical_lift.stock_vertical_lift_demo_shuttle_2' + "stock_vertical_lift.stock_vertical_lift_demo_shuttle_2" ) + operation2 = shuttle2._operation_for_mode() - self.assertEqual(shuttle1.number_of_ops, 4) - self.assertEqual(shuttle2.number_of_ops, 2) - self.assertEqual(shuttle1.number_of_ops_all, 6) - self.assertEqual(shuttle2.number_of_ops_all, 6) + self.assertEqual(operation1.number_of_ops, 4) + self.assertEqual(operation2.number_of_ops, 2) + self.assertEqual(operation1.number_of_ops_all, 6) + self.assertEqual(operation2.number_of_ops_all, 6) # Process a line, should change the numbers. - shuttle1.select_next_move_line() - shuttle1.process_current_pick() - self.assertEqual(shuttle1.number_of_ops, 3) - self.assertEqual(shuttle2.number_of_ops, 2) - self.assertEqual(shuttle1.number_of_ops_all, 5) - self.assertEqual(shuttle2.number_of_ops_all, 5) + operation1.select_next_move_line() + operation1.process_current() + self.assertEqual(operation1.number_of_ops, 3) + self.assertEqual(operation2.number_of_ops, 2) + self.assertEqual(operation1.number_of_ops_all, 5) + self.assertEqual(operation2.number_of_ops_all, 5) # add stock and make the last one assigned to check the number is # updated self._update_quantity_in_cell(cell2, product2, 10) unassigned.action_assign() - self.assertEqual(shuttle1.number_of_ops, 3) - self.assertEqual(shuttle2.number_of_ops, 3) - self.assertEqual(shuttle1.number_of_ops_all, 6) - self.assertEqual(shuttle2.number_of_ops_all, 6) + self.assertEqual(operation1.number_of_ops, 3) + self.assertEqual(operation2.number_of_ops, 3) + self.assertEqual(operation1.number_of_ops_all, 6) + self.assertEqual(operation2.number_of_ops_all, 6) - @unittest.skip('Not implemented') + @unittest.skip("Not implemented") def test_put_count_move_lines(self): pass - @unittest.skip('Not implemented') + @unittest.skip("Not implemented") def test_inventory_count_move_lines(self): pass - @unittest.skip('Not implemented') + @unittest.skip("Not implemented") def test_on_barcode_scanned(self): # test to implement when the code is implemented pass @@ -188,48 +188,52 @@ def test_button_release(self): self.out_move_line.qty_done = self.out_move_line.product_qty self.out_move_line.move_id._action_done() # release, no further operation in queue - result = self.shuttle.button_release() - self.assertFalse(self.shuttle.current_move_line_id) - self.assertEqual(self.shuttle.operation_descr, _('No operations')) + operation = self.shuttle._operation_for_mode() + result = operation.button_release() + self.assertFalse(operation.current_move_line_id) + self.assertEqual(operation.operation_descr, _("No operations")) expected_result = { - 'effect': { - 'fadeout': 'slow', - 'message': _('Congrats, you cleared the queue!'), - 'img_url': '/web/static/src/img/smile.svg', - 'type': 'rainbow_man', + "effect": { + "fadeout": "slow", + "message": _("Congrats, you cleared the queue!"), + "img_url": "/web/static/src/img/smile.svg", + "type": "rainbow_man", } } self.assertEqual(result, expected_result) def test_process_current_pick(self): self.shuttle.switch_pick() - self.shuttle.current_move_line_id = self.out_move_line + operation = self.shuttle._operation_for_mode() + operation.current_move_line_id = self.out_move_line qty_to_process = self.out_move_line.product_qty - self.shuttle.process_current_pick() - self.assertEqual(self.out_move_line.state, 'done') + operation.process_current() + self.assertEqual(self.out_move_line.state, "done") self.assertEqual(self.out_move_line.qty_done, qty_to_process) def test_process_current_put(self): # test to implement when the code is implemented + self.shuttle.switch_put() + operation = self.shuttle._operation_for_mode() with self.assertRaises(exceptions.UserError): - self.shuttle.process_current_put() + operation.process_current() def test_process_current_inventory(self): # test to implement when the code is implemented - with self.assertRaises(exceptions.UserError): - self.shuttle.process_current_inventory() + self.shuttle.switch_inventory() def test_matrix(self): self.shuttle.switch_pick() - self.shuttle.current_move_line_id = self.out_move_line + operation = self.shuttle._operation_for_mode() + operation.current_move_line_id = self.out_move_line location = self.out_move_line.location_id # offset by -1 because the fields are for humans expected_x = location.posx - 1 expected_y = location.posy - 1 self.assertEqual( - self.shuttle.tray_matrix, + operation.tray_matrix, { - 'selected': [expected_x, expected_y], + "selected": [expected_x, expected_y], # fmt: off 'cells': [ [0, 0, 0, 0, 0, 0, 0, 0], @@ -241,12 +245,13 @@ def test_matrix(self): def test_tray_qty(self): cell = self.env.ref( - 'stock_vertical_lift.' - 'stock_location_vertical_lift_demo_tray_1a_x3y2' + "stock_vertical_lift." + "stock_location_vertical_lift_demo_tray_1a_x3y2" ) self.out_move_line.location_id = cell - self.shuttle.current_move_line_id = self.out_move_line + operation = self.shuttle._operation_for_mode() + operation.current_move_line_id = self.out_move_line self._update_quantity_in_cell(cell, self.out_move_line.product_id, 50) - self.assertEqual(self.shuttle.tray_qty, 50) + self.assertEqual(operation.tray_qty, 50) self._update_quantity_in_cell(cell, self.out_move_line.product_id, -20) - self.assertEqual(self.shuttle.tray_qty, 30) + self.assertEqual(operation.tray_qty, 30) diff --git a/stock_vertical_lift/views/vertical_lift_operation_base_views.xml b/stock_vertical_lift/views/vertical_lift_operation_base_views.xml new file mode 100644 index 000000000000..752dd5940411 --- /dev/null +++ b/stock_vertical_lift/views/vertical_lift_operation_base_views.xml @@ -0,0 +1,147 @@ + + + + + vertical.lift.operation.base.screen.view + vertical.lift.operation.base + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ + +
+
+ + + vertical.lift.operation.transfer.screen.view + vertical.lift.operation.transfer + + primary + + + + + {'invisible': [('current_move_line_id', '=', False)]} + + + + +
+
+ + + +
+
+ +
+ + + + + + + +
+ +
+
+
+
+
+
+ +
diff --git a/stock_vertical_lift/views/vertical_lift_operation_inventory_views.xml b/stock_vertical_lift/views/vertical_lift_operation_inventory_views.xml new file mode 100644 index 000000000000..509b471c8e73 --- /dev/null +++ b/stock_vertical_lift/views/vertical_lift_operation_inventory_views.xml @@ -0,0 +1,19 @@ + + + + + vertical.lift.operation.inventory.screen.view + vertical.lift.operation.inventory + + primary + +
+ Inventory Screen +
+ + Not implemented + +
+
+ +
diff --git a/stock_vertical_lift/views/vertical_lift_operation_pick_views.xml b/stock_vertical_lift/views/vertical_lift_operation_pick_views.xml new file mode 100644 index 000000000000..2fc407f9ef7c --- /dev/null +++ b/stock_vertical_lift/views/vertical_lift_operation_pick_views.xml @@ -0,0 +1,16 @@ + + + + + vertical.lift.operation.pick.screen.view + vertical.lift.operation.pick + + primary + +
+ Pick Screen +
+
+
+ +
diff --git a/stock_vertical_lift/views/vertical_lift_operation_put_views.xml b/stock_vertical_lift/views/vertical_lift_operation_put_views.xml new file mode 100644 index 000000000000..4a891d9851cc --- /dev/null +++ b/stock_vertical_lift/views/vertical_lift_operation_put_views.xml @@ -0,0 +1,16 @@ + + + + + vertical.lift.operation.put.screen.view + vertical.lift.operation.put + + primary + +
+ Put Screen +
+
+
+ +
diff --git a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml index 6d5bb6d53502..97169a43cca3 100644 --- a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml +++ b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml @@ -1,133 +1,6 @@ - - vertical.lift.shuttle.view.form.screen - vertical.lift.shuttle - 99 - -
- -
-
- -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
- - - -
-
- -
- - - - - - - -
- -
-
-
-
- - -
-
- vertical.lift.shuttle.view.form.menu vertical.lift.shuttle @@ -190,9 +63,6 @@ - - @@ -205,8 +75,8 @@ remove later --> - - + +
@@ -224,14 +94,14 @@ remove later --> Mode:
-
- Operations: - -
-
- All Operations: - -
+ + + + + + + + From 178b45604f7731828d0d51bdfa2afeeabb1f2751 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 10 Oct 2019 08:59:16 +0200 Subject: [PATCH 04/41] Add default views to re-open the screen views properly When we refresh the page on the browser when we are using the "screen" view, odoo loses the information that we want the view to be headless, fullscreen, etc. so it's displayed pretty badly. This view is a work-around: its priority is lower, so it will be picked up by default on loading, and a button allows to re-open the screen view with the proper options. --- .../vertical_lift_operation_base_views.xml | 22 +++++++++++++++++++ ...ertical_lift_operation_inventory_views.xml | 13 +++++++++++ .../vertical_lift_operation_pick_views.xml | 13 +++++++++++ .../vertical_lift_operation_put_views.xml | 15 ++++++++++++- 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/stock_vertical_lift/views/vertical_lift_operation_base_views.xml b/stock_vertical_lift/views/vertical_lift_operation_base_views.xml index 752dd5940411..876e6012a7a6 100644 --- a/stock_vertical_lift/views/vertical_lift_operation_base_views.xml +++ b/stock_vertical_lift/views/vertical_lift_operation_base_views.xml @@ -1,6 +1,28 @@ + + + vertical.lift.operation.base.button.view + vertical.lift.operation.base + +
+
+ + + {'invisible': [('current_inventory_line_id', '=', False)]} + - Not implemented + +
+
+ + +
+
+ +
+
+
+
+
+ +
+ + + + + + + +
+ +
+
+
From aeb3cfa59fbfab7e9e8a397a23919695a162c840 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 15 Oct 2019 16:24:30 +0200 Subject: [PATCH 07/41] Fix 'fetch' buttons on move lines There is no such action as 'ir.actions.do_nothing', it kinda works, until you look into the js console and stares at the errors. There is a nice OCA module that serves this purpose (more or less, because it reloads the window, this is not an issue). --- stock_vertical_lift/__manifest__.py | 1 + stock_vertical_lift/models/stock_move_line.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/stock_vertical_lift/__manifest__.py b/stock_vertical_lift/__manifest__.py index 7d97d1741d11..11edb3846d23 100644 --- a/stock_vertical_lift/__manifest__.py +++ b/stock_vertical_lift/__manifest__.py @@ -13,6 +13,7 @@ 'base_sparse_field', 'stock_location_tray', # OCA/stock-logistics-warehouse 'web_notify', # OCA/web + 'web_ir_actions_act_view_reload', # OCA/web ], 'website': 'https://github.com/OCA/stock-logistics-warehouse', 'demo': [ diff --git a/stock_vertical_lift/models/stock_move_line.py b/stock_vertical_lift/models/stock_move_line.py index a26873c0aeb2..433d6f33da6d 100644 --- a/stock_vertical_lift/models/stock_move_line.py +++ b/stock_vertical_lift/models/stock_move_line.py @@ -10,9 +10,12 @@ class StockMoveLine(models.Model): def fetch_vertical_lift_tray_source(self): self.ensure_one() self.location_id.fetch_vertical_lift_tray() - return {"type": "ir.actions.do_nothing"} + # We reload mainly because otherwise, it would close + # the popup. This action is provided by the OCA module + # web_ir_actions_act_view_reload + return {"type": "ir.actions.act_view_reload"} def fetch_vertical_lift_tray_dest(self): self.ensure_one() self.location_dest_id.fetch_vertical_lift_tray() - return {"type": "ir.actions.do_nothing"} + return {"type": "ir.actions.act_view_reload"} From fc995a941099689a609335dc9e7fc14d89ae8231 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 15 Oct 2019 16:50:35 +0200 Subject: [PATCH 08/41] Add button on locations to fetch the tray --- stock_vertical_lift/models/stock_location.py | 6 ++++++ stock_vertical_lift/views/stock_location_views.xml | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/stock_vertical_lift/models/stock_location.py b/stock_vertical_lift/models/stock_location.py index 93e02cf6513b..dd457322817f 100644 --- a/stock_vertical_lift/models/stock_location.py +++ b/stock_vertical_lift/models/stock_location.py @@ -154,3 +154,9 @@ def fetch_vertical_lift_tray(self, cell_location=None): % (self.name,) ) return True + + def button_fetch_vertical_lift_tray(self): + self.ensure_one() + if self.vertical_lift_kind in ("cell", "tray"): + self.fetch_vertical_lift_tray() + return True diff --git a/stock_vertical_lift/views/stock_location_views.xml b/stock_vertical_lift/views/stock_location_views.xml index 62c6e2996d0b..c884825ef3bc 100644 --- a/stock_vertical_lift/views/stock_location_views.xml +++ b/stock_vertical_lift/views/stock_location_views.xml @@ -6,6 +6,15 @@ stock.location +
+
Date: Mon, 4 Nov 2019 15:03:03 +0100 Subject: [PATCH 09/41] [IMP] abstract communication with shuttle --- stock_vertical_lift/models/stock_location.py | 13 ++- .../models/vertical_lift_shuttle.py | 91 +++++++++++++++++++ .../views/vertical_lift_shuttle_views.xml | 19 ++-- .../models/stock_location.py | 38 +++++++- .../models/vertical_lift_shuttle.py | 17 ++++ 5 files changed, 164 insertions(+), 14 deletions(-) diff --git a/stock_vertical_lift/models/stock_location.py b/stock_vertical_lift/models/stock_location.py index dd457322817f..14c1977ced88 100644 --- a/stock_vertical_lift/models/stock_location.py +++ b/stock_vertical_lift/models/stock_location.py @@ -71,7 +71,12 @@ def _compute_vertical_lift_shuttle_id(self): location.vertical_lift_shuttle_id = shuttle def _hardware_vertical_lift_tray(self, cell_location=None): - """Send instructions to the vertical lift hardware + payload = self._hardware_vertical_lift_tray_payload(cell_location) + res = self.vertical_lift_shuttle_id._hardware_send_message(payload) + return res + + 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 @@ -120,9 +125,9 @@ def _hardware_vertical_lift_tray(self, cell_location=None): from_left, from_bottom, ) - self.env.user.notify_info( - message=message, title=_("Lift Simulation") - ) + return message + else: + return super()._hardware_vertical_lift_tray_payload(cell_location) def fetch_vertical_lift_tray(self, cell_location=None): """Send instructions to the vertical lift hardware diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py index d3271acb297c..a7e0f35c502e 100644 --- a/stock_vertical_lift/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift/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 +import socket +import ssl from odoo import _, api, fields, models +_logger = logging.getLogger(__name__) + class VerticalLiftShuttle(models.Model): _name = "vertical.lift.shuttle" @@ -26,6 +31,13 @@ class VerticalLiftShuttle(models.Model): 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" + ) _sql_constraints = [ ( @@ -64,6 +76,85 @@ def _screen_view_for_mode(self): ), } + 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, then waiting + for a response and disconnecting. + + :param payload: a bytes object containing the payload + + """ + self.ensure_one() + _logger.info('send %r', payload) + 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 + response = self._hardware_recv_response(conn) + _logger.info('recv %r', response) + return self._check_server_response(payload, response) + finally: + self._hardware_release_server_connection(conn) + + def _hardware_recv_response(self, conn): + """Default implementation expects the remote server to close() + the socket after sending the reponse. + Override to match the protocol implemented by the hardware. + + :param conn: a socket connected to the server + :return: the response sent by the server, as a bytes object + """ + response = b'' + chunk = True + while chunk: + chunk = conn.recv(1024) + response += chunk + return response + + def _check_server_response(self, payload, response): + """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)]) diff --git a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml index c8d7cb639341..0a33ad98d43b 100644 --- a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml +++ b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml @@ -58,12 +58,19 @@ vertical.lift.shuttle
- - - - - - + + + + + + + + + + + + +
diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index d1098d59a124..e307aa21cbd1 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 @@ def _hardware_vertical_lift_tray(self, cell_location=None): 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 cfd3c74b9dbf..67dc1131d5bc 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 @@ def _selection_hardware(self): 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 bbeb506db3dd9962064f6cda3f7546cb119e5d5a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 8 Nov 2019 12:49:25 +0100 Subject: [PATCH 10/41] Add method to refresh a shuttle screen Example of usage in an odoo shell, when a screen is open: >>> self.env['vertical.lift.shuttle'].browse(1)._operation_for_mode().operation_descr = 'foo' >>> self.env['vertical.lift.shuttle'].browse(1)._send_notification_refresh() >>> env.cr.commit() Provided the longpolling is correctly configured with a proxy, the screen should immediately refresh with 'foo' as operation description. --- .../models/vertical_lift_operation_base.py | 22 +++++++++ .../models/vertical_lift_shuttle.py | 14 ++++++ .../static/src/js/vertical_lift.js | 48 +++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/stock_vertical_lift/models/vertical_lift_operation_base.py b/stock_vertical_lift/models/vertical_lift_operation_base.py index 5c1d09e75486..6108f7418ce0 100644 --- a/stock_vertical_lift/models/vertical_lift_operation_base.py +++ b/stock_vertical_lift/models/vertical_lift_operation_base.py @@ -97,6 +97,28 @@ def _get_tray_qty(self, product, location): ) return sum(quants.mapped("quantity")) + def _send_notification_refresh(self): + """Send a refresh notification + + Generally, you want to call the method + _send_notification_refresh() on VerticalLiftShuttle so you + don't need to know the id of the current operation. + + Other notifications can be implemented, they have to be + added in static/src/js/vertical_lift.js and the message + must contain an "action" and "params". + """ + self.ensure_one() + channel = "notify_vertical_lift_screen" + bus_message = { + "action": "refresh", + "params": { + "model": self._name, + "id": self.id, + } + } + self.env["bus.bus"].sendone(channel, bus_message) + class VerticalLiftOperationTransfer(models.AbstractModel): """Base model for shuttle pick and put operations""" diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py index a7e0f35c502e..013586eedc27 100644 --- a/stock_vertical_lift/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift/models/vertical_lift_shuttle.py @@ -214,6 +214,20 @@ def switch_inventory(self): self.mode = "inventory" return self.action_open_screen() + def _send_notification_refresh(self): + """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. + """ + self._operation_for_mode._send_notification_refresh() + class VerticalLiftShuttleManualBarcode(models.TransientModel): _name = "vertical.lift.shuttle.manual.barcode" diff --git a/stock_vertical_lift/static/src/js/vertical_lift.js b/stock_vertical_lift/static/src/js/vertical_lift.js index 7c0e5d6f4ff1..542a06079376 100644 --- a/stock_vertical_lift/static/src/js/vertical_lift.js +++ b/stock_vertical_lift/static/src/js/vertical_lift.js @@ -5,6 +5,7 @@ var core = require('web.core'); var KanbanRecord = require('web.KanbanRecord'); var basicFields = require('web.basic_fields'); var field_registry = require('web.field_registry'); +var FormController = require('web.FormController'); var FieldInteger = basicFields.FieldInteger; KanbanRecord.include({ @@ -44,6 +45,53 @@ var ExitButton = FieldInteger.extend({ clear_breadcrumbs: true, }); }, + +}); + + +FormController.include({ + init: function (parent, model, renderer, params) { + this._super.apply(this, arguments); + if(this.modelName.startsWith('vertical.lift.operation.')) { + this.call('bus_service', 'addChannel', 'notify_vertical_lift_screen'); + this.call( + 'bus_service', 'on', 'notification', + this, this.vlift_bus_notification + ); + this.call('bus_service', 'startPolling'); + } + }, + vlift_bus_notification: function (notifications) { + var self = this; + _.each(notifications, function (notification) { + var channel = notification[0]; + var message = notification[1]; + if(channel === 'notify_vertical_lift_screen') { + switch(message['action']) { + case 'refresh': + self.vlift_bus_action_refresh(message['params']); + break; + } + } + }); + }, + vlift_bus_action_refresh: function(params) { + var selectedIds = this.getSelectedIds(); + if(!selectedIds.length){ + return; + } + var currentId = selectedIds[0]; + if(params['id'] === currentId && params['model'] == this.modelName){ + this.reload(); + } + }, + destroy: function () { + if(this.modelName.startsWith('vertical.lift.operation.')) { + this.call('bus_service', 'deleteChannel', 'notify_vertical_lift_screen'); + } + this._super.apply(this, arguments); + } + }); field_registry.add('vlift_shuttle_exit_button', ExitButton); From 86e9e623060a1e07697d9b31d9bcc0ad0f0eaa86 Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Thu, 14 Nov 2019 10:57:42 +0100 Subject: [PATCH 11/41] [IMP] add a proxy to communicate with the kardex server --- stock_vertical_lift/__init__.py | 1 + stock_vertical_lift/__manifest__.py | 1 + stock_vertical_lift/controllers/__init__.py | 1 + stock_vertical_lift/controllers/main.py | 20 ++ stock_vertical_lift/data/ir_sequence.xml | 12 ++ stock_vertical_lift/models/__init__.py | 1 + stock_vertical_lift/models/stock_location.py | 2 +- .../models/vertical_lift_command.py | 50 +++++ .../models/vertical_lift_shuttle.py | 45 ++--- .../security/ir.model.access.csv | 1 + .../views/vertical_lift_shuttle_views.xml | 12 ++ 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 + 19 files changed, 459 insertions(+), 44 deletions(-) create mode 100644 stock_vertical_lift/controllers/__init__.py create mode 100644 stock_vertical_lift/controllers/main.py create mode 100644 stock_vertical_lift/data/ir_sequence.xml create mode 100644 stock_vertical_lift/models/vertical_lift_command.py 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/__init__.py b/stock_vertical_lift/__init__.py index 0650744f6bc6..f7209b171002 100644 --- a/stock_vertical_lift/__init__.py +++ b/stock_vertical_lift/__init__.py @@ -1 +1,2 @@ from . import models +from . import controllers diff --git a/stock_vertical_lift/__manifest__.py b/stock_vertical_lift/__manifest__.py index 11edb3846d23..04b5c529285f 100644 --- a/stock_vertical_lift/__manifest__.py +++ b/stock_vertical_lift/__manifest__.py @@ -34,6 +34,7 @@ 'views/stock_vertical_lift_templates.xml', 'views/shuttle_screen_templates.xml', 'security/ir.model.access.csv', + 'data/ir_sequence.xml', ], 'installable': True, 'development_status': 'Alpha', diff --git a/stock_vertical_lift/controllers/__init__.py b/stock_vertical_lift/controllers/__init__.py new file mode 100644 index 000000000000..12a7e529b674 --- /dev/null +++ b/stock_vertical_lift/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/stock_vertical_lift/controllers/main.py b/stock_vertical_lift/controllers/main.py new file mode 100644 index 000000000000..a03af98354ef --- /dev/null +++ b/stock_vertical_lift/controllers/main.py @@ -0,0 +1,20 @@ +import logging +import os + +from odoo import http +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class VerticalLiftController(http.Controller): + @http.route(['/vertical-lift'], type='http', auth='public', csrf=False) + def vertical_lift(self, answer, secret): + if secret == os.environ.get('VERTICAL_LIFT_SECRET', ''): + rec = request.env['vertical.lift.command'].sudo().record_answer( + answer + ) + return str(rec.id) + else: + _logger.error('secret mismatch: %r != %r', secret, os.environ.get('VERTICAL_LIFT_SECRET', '')) + raise http.AuthenticationError() diff --git a/stock_vertical_lift/data/ir_sequence.xml b/stock_vertical_lift/data/ir_sequence.xml new file mode 100644 index 000000000000..9b231ed34283 --- /dev/null +++ b/stock_vertical_lift/data/ir_sequence.xml @@ -0,0 +1,12 @@ + + + + + Vertical Lift Commands + vertical.lift.command + L + 6 + + + + diff --git a/stock_vertical_lift/models/__init__.py b/stock_vertical_lift/models/__init__.py index 87a31d40b36e..1db85b28e286 100644 --- a/stock_vertical_lift/models/__init__.py +++ b/stock_vertical_lift/models/__init__.py @@ -8,3 +8,4 @@ from . import stock_move from . import stock_move_line from . import stock_quant +from . import vertical_lift_command diff --git a/stock_vertical_lift/models/stock_location.py b/stock_vertical_lift/models/stock_location.py index 14c1977ced88..052bb6456479 100644 --- a/stock_vertical_lift/models/stock_location.py +++ b/stock_vertical_lift/models/stock_location.py @@ -127,7 +127,7 @@ def _hardware_vertical_lift_tray_payload(self, cell_location=None): ) return message else: - return super()._hardware_vertical_lift_tray_payload(cell_location) + raise NotImplemented() def fetch_vertical_lift_tray(self, cell_location=None): """Send instructions to the vertical lift hardware diff --git a/stock_vertical_lift/models/vertical_lift_command.py b/stock_vertical_lift/models/vertical_lift_command.py new file mode 100644 index 000000000000..7c25b7f1f5ee --- /dev/null +++ b/stock_vertical_lift/models/vertical_lift_command.py @@ -0,0 +1,50 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import api, exceptions, fields, models + +_logger = logging.getLogger(__name__) + + +class VerticalLiftCommand(models.Model): + _name = 'vertical.lift.command' + _order = 'shuttle_id, name desc' + _description = "commands sent to the shuttle" + + @api.model + def _default_name(self): + return self.env['ir.sequence'].next_by_code('vertical.lift.command') + + name = fields.Char( + 'Name', default=_default_name, required=True, index=True + ) + command = fields.Char(required=True) + answer = fields.Char() + error = fields.Char() + shuttle_id = fields.Many2one('vertical.lift.shuttle', required=True) + + @api.model + def record_answer(self, answer): + name = self._get_key(answer) + record = self.search([('name', '=', name)], limit=1) + if not record: + _logger.error('unable to match answer to a command: %r', answer) + raise exceptions.UserError('Unknown record %s' % name) + record.answer = answer + record.shuttle_id._hardware_response_callback(record) + return record + + def _get_key(self, answer): + key = answer.split('|')[1] + return key + + @api.model_create_multi + @api.returns('self', lambda value: value.id) + def create(self, vals_list): + for values in vals_list: + if "name" not in values: + name = self._get_key(values.get('command')) + if name: + values["name"] = name + return super().create(vals_list) diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py index 013586eedc27..d8b584fe153e 100644 --- a/stock_vertical_lift/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift/models/vertical_lift_shuttle.py @@ -38,7 +38,10 @@ class VerticalLiftShuttle(models.Model): 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", @@ -81,14 +84,21 @@ def _hardware_send_message(self, payload): 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, then waiting - for a response and disconnecting. + (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")) @@ -102,28 +112,18 @@ def _hardware_send_message(self, payload): offset += size if offset >= len(payload) or not size: break - response = self._hardware_recv_response(conn) - _logger.info('recv %r', response) - return self._check_server_response(payload, response) finally: self._hardware_release_server_connection(conn) - def _hardware_recv_response(self, conn): - """Default implementation expects the remote server to close() - the socket after sending the reponse. - Override to match the protocol implemented by the hardware. + def _hardware_response_callback(self, command): + """should be called when a response is received from the hardware - :param conn: a socket connected to the server - :return: the response sent by the server, as a bytes object + :param response: a string """ - response = b'' - chunk = True - while chunk: - chunk = conn.recv(1024) - response += chunk - return response - - def _check_server_response(self, payload, response): + 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 @@ -214,7 +214,7 @@ def switch_inventory(self): self.mode = "inventory" return self.action_open_screen() - def _send_notification_refresh(self): + 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 @@ -226,7 +226,8 @@ def _send_notification_refresh(self): The method is private only to prevent xml/rpc calls to interact with the screen. """ - self._operation_for_mode._send_notification_refresh() + # XXX do we want to do something special in the notification? + self._operation_for_mode()._send_notification_refresh() class VerticalLiftShuttleManualBarcode(models.TransientModel): diff --git a/stock_vertical_lift/security/ir.model.access.csv b/stock_vertical_lift/security/ir.model.access.csv index 63741764a132..180158ce4847 100644 --- a/stock_vertical_lift/security/ir.model.access.csv +++ b/stock_vertical_lift/security/ir.model.access.csv @@ -5,3 +5,4 @@ access_vertical_lift_operation_pick_stock_user,access_vertical_lift_operation_pi access_vertical_lift_operation_put_stock_user,access_vertical_lift_operation_put stock user,model_vertical_lift_operation_put,stock.group_stock_user,1,1,1,1 access_vertical_lift_operation_put_line_stock_user,access_vertical_lift_operation_put_line stock user,model_vertical_lift_operation_put_line,stock.group_stock_user,1,1,1,1 access_vertical_lift_operation_inventory_stock_user,access_vertical_lift_operation_inventory stock user,model_vertical_lift_operation_inventory,stock.group_stock_user,1,1,1,1 +access_vertical_lift_command,vertical_lift_command,model_vertical_lift_command,base.group_user,1,0,0,0 diff --git a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml index 0a33ad98d43b..2f81f5d30a1f 100644 --- a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml +++ b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml @@ -71,6 +71,18 @@ + + diff --git a/stock_vertical_lift_kardex/__manifest__.py b/stock_vertical_lift_kardex/__manifest__.py index 35670055d868..8ccd05222861 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 51a3830f6e9a..5a191bac8b4e 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 e307aa21cbd1..39d66fc28e54 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -28,10 +28,10 @@ def _hardware_kardex_prepare_payload(self, cell_location=None): 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 @@ def _hardware_kardex_prepare_payload(self, cell_location=None): '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 @@ def _hardware_vertical_lift_tray_payload(self, cell_location=None): 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 67dc1131d5bc..256439d35a41 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 @@ def _selection_hardware(self): 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 000000000000..f568c5a91af4 --- /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 000000000000..ee4ba4f3d739 --- /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 000000000000..5b6ad518890b --- /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 000000000000..ee4ba4f3d739 --- /dev/null +++ b/stock_vertical_lift_kardex/requirements.txt @@ -0,0 +1 @@ +aiohttp From e6733ddf73c96999dbad0886a0615aa6936d7f43 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 16 Dec 2019 14:22:45 +0100 Subject: [PATCH 12/41] [IMP] stock_vertical_lift: black, isort --- stock_vertical_lift/__manifest__.py | 68 +-- stock_vertical_lift/controllers/main.py | 14 +- stock_vertical_lift/data/ir_sequence.xml | 18 +- stock_vertical_lift/demo/product_demo.xml | 56 +-- .../demo/stock_inventory_demo.xml | 26 +- .../demo/stock_location_demo.xml | 172 +++---- .../demo/stock_picking_demo.xml | 86 ++-- .../demo/vertical_lift_shuttle_demo.xml | 30 +- stock_vertical_lift/images/O-BTN.release.svg | 2 +- stock_vertical_lift/images/O-BTN.save.svg | 2 +- stock_vertical_lift/models/stock_location.py | 35 +- stock_vertical_lift/models/stock_move.py | 9 +- stock_vertical_lift/models/stock_quant.py | 11 +- .../models/vertical_lift_command.py | 26 +- .../models/vertical_lift_operation_base.py | 45 +- .../vertical_lift_operation_inventory.py | 33 +- .../models/vertical_lift_operation_pick.py | 8 +- .../models/vertical_lift_operation_put.py | 33 +- .../models/vertical_lift_shuttle.py | 30 +- .../static/src/js/vertical_lift.js | 174 +++---- stock_vertical_lift/tests/common.py | 135 +++--- stock_vertical_lift/tests/test_inventory.py | 32 +- stock_vertical_lift/tests/test_location.py | 25 +- stock_vertical_lift/tests/test_pick.py | 23 +- stock_vertical_lift/tests/test_put.py | 7 +- .../views/shuttle_screen_templates.xml | 26 +- .../views/stock_vertical_lift_templates.xml | 12 +- .../vertical_lift_operation_pick_views.xml | 24 +- .../views/vertical_lift_shuttle_views.xml | 324 ++++++------- 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 ++++++++++++++++++ 37 files changed, 1297 insertions(+), 913 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/__manifest__.py b/stock_vertical_lift/__manifest__.py index 04b5c529285f..f0860072727f 100644 --- a/stock_vertical_lift/__manifest__.py +++ b/stock_vertical_lift/__manifest__.py @@ -1,41 +1,41 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). { - 'name': 'Vertical Lift', - 'summary': 'Provides the core for integration with Vertical Lifts', - 'version': '12.0.1.0.0', - 'category': 'Stock', - 'author': 'Camptocamp, Odoo Community Association (OCA)', - 'license': 'AGPL-3', - 'depends': [ - 'stock', - 'barcodes', - 'base_sparse_field', - 'stock_location_tray', # OCA/stock-logistics-warehouse - 'web_notify', # OCA/web - 'web_ir_actions_act_view_reload', # OCA/web + "name": "Vertical Lift", + "summary": "Provides the core for integration with Vertical Lifts", + "version": "12.0.1.0.0", + "category": "Stock", + "author": "Camptocamp, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": [ + "stock", + "barcodes", + "base_sparse_field", + "stock_location_tray", # OCA/stock-logistics-warehouse + "web_notify", # OCA/web + "web_ir_actions_act_view_reload", # OCA/web ], - 'website': 'https://github.com/OCA/stock-logistics-warehouse', - 'demo': [ - 'demo/stock_location_demo.xml', - 'demo/vertical_lift_shuttle_demo.xml', - 'demo/product_demo.xml', - 'demo/stock_inventory_demo.xml', - 'demo/stock_picking_demo.xml', + "website": "https://github.com/OCA/stock-logistics-warehouse", + "demo": [ + "demo/stock_location_demo.xml", + "demo/vertical_lift_shuttle_demo.xml", + "demo/product_demo.xml", + "demo/stock_inventory_demo.xml", + "demo/stock_picking_demo.xml", ], - 'data': [ - 'views/stock_location_views.xml', - 'views/stock_move_line_views.xml', - 'views/vertical_lift_shuttle_views.xml', - 'views/vertical_lift_operation_base_views.xml', - 'views/vertical_lift_operation_pick_views.xml', - 'views/vertical_lift_operation_put_views.xml', - 'views/vertical_lift_operation_inventory_views.xml', - 'views/stock_vertical_lift_templates.xml', - 'views/shuttle_screen_templates.xml', - 'security/ir.model.access.csv', - 'data/ir_sequence.xml', + "data": [ + "views/stock_location_views.xml", + "views/stock_move_line_views.xml", + "views/vertical_lift_shuttle_views.xml", + "views/vertical_lift_operation_base_views.xml", + "views/vertical_lift_operation_pick_views.xml", + "views/vertical_lift_operation_put_views.xml", + "views/vertical_lift_operation_inventory_views.xml", + "views/stock_vertical_lift_templates.xml", + "views/shuttle_screen_templates.xml", + "security/ir.model.access.csv", + "data/ir_sequence.xml", ], - 'installable': True, - 'development_status': 'Alpha', + "installable": True, + "development_status": "Alpha", } diff --git a/stock_vertical_lift/controllers/main.py b/stock_vertical_lift/controllers/main.py index a03af98354ef..9a11fb152017 100644 --- a/stock_vertical_lift/controllers/main.py +++ b/stock_vertical_lift/controllers/main.py @@ -8,13 +8,15 @@ class VerticalLiftController(http.Controller): - @http.route(['/vertical-lift'], type='http', auth='public', csrf=False) + @http.route(["/vertical-lift"], type="http", auth="public", csrf=False) def vertical_lift(self, answer, secret): - if secret == os.environ.get('VERTICAL_LIFT_SECRET', ''): - rec = request.env['vertical.lift.command'].sudo().record_answer( - answer - ) + if secret == os.environ.get("VERTICAL_LIFT_SECRET", ""): + rec = request.env["vertical.lift.command"].sudo().record_answer(answer) return str(rec.id) else: - _logger.error('secret mismatch: %r != %r', secret, os.environ.get('VERTICAL_LIFT_SECRET', '')) + _logger.error( + "secret mismatch: %r != %r", + secret, + os.environ.get("VERTICAL_LIFT_SECRET", ""), + ) raise http.AuthenticationError() diff --git a/stock_vertical_lift/data/ir_sequence.xml b/stock_vertical_lift/data/ir_sequence.xml index 9b231ed34283..1345e7e39f76 100644 --- a/stock_vertical_lift/data/ir_sequence.xml +++ b/stock_vertical_lift/data/ir_sequence.xml @@ -1,12 +1,10 @@ - - - - Vertical Lift Commands - vertical.lift.command - L - 6 - - - + + + Vertical Lift Commands + vertical.lift.command + L + 6 + + diff --git a/stock_vertical_lift/demo/product_demo.xml b/stock_vertical_lift/demo/product_demo.xml index b55184196807..0f59a88a315b 100644 --- a/stock_vertical_lift/demo/product_demo.xml +++ b/stock_vertical_lift/demo/product_demo.xml @@ -1,34 +1,34 @@ - - RS200 - 4491673293664 - Running Socks - product - - 30.0 - 20.0 - 1.0 - none - - - - + + RS200 + 4491673293664 + Running Socks + product + + 30.0 + 20.0 + 1.0 + none + + + + - - RS300 - 2779891103531 - Recovery Socks - product - - 30.0 - 20.0 - 1.0 - none - - - - + + RS300 + 2779891103531 + Recovery Socks + product + + 30.0 + 20.0 + 1.0 + none + + + + diff --git a/stock_vertical_lift/demo/stock_inventory_demo.xml b/stock_vertical_lift/demo/stock_inventory_demo.xml index 4b17fcdcd6ec..719b0d5e7d9d 100644 --- a/stock_vertical_lift/demo/stock_inventory_demo.xml +++ b/stock_vertical_lift/demo/stock_inventory_demo.xml @@ -1,20 +1,20 @@ - - Starting Vertical Lift Inventory - + + Starting Vertical Lift Inventory + - - - - - 30.0 - - + + + + + 30.0 + + - - - + + + diff --git a/stock_vertical_lift/demo/stock_location_demo.xml b/stock_vertical_lift/demo/stock_location_demo.xml index 78257a926b40..b66667e8ea38 100644 --- a/stock_vertical_lift/demo/stock_location_demo.xml +++ b/stock_vertical_lift/demo/stock_location_demo.xml @@ -14,103 +14,103 @@ - - Shuttle 1 - - internal - + + Shuttle 1 + + internal + - - Tray 1A - T1A - - - internal - + + Tray 1A + T1A + + + internal + - - Tray 1B - T1B - - - internal - + + Tray 1B + T1B + + + internal + - - Tray 1C - T1C - - - internal - + + Tray 1C + T1C + + + internal + - - Shuttle 2 - - internal - + + Shuttle 2 + + internal + - - Tray 2A - T2A - - - internal - + + Tray 2A + T2A + + + internal + - - Tray 2B - T2B - - - internal - + + Tray 2B + T2B + + + internal + - - Tray 2C - T2C - - - internal - + + Tray 2C + T2C + + + internal + - - Tray 2D - T2D - - - internal - + + Tray 2D + T2D + + + internal + - - Shuttle 3 - - internal - + + Shuttle 3 + + internal + - - Tray 3A - T3A - - - internal - + + Tray 3A + T3A + + + internal + - - Tray 3B - T3B - - - internal - + + Tray 3B + T3B + + + internal + - - - - stock_vertical_lift - + + + + stock_vertical_lift + diff --git a/stock_vertical_lift/demo/stock_picking_demo.xml b/stock_vertical_lift/demo/stock_picking_demo.xml index bff48ee18877..c41583590b34 100644 --- a/stock_vertical_lift/demo/stock_picking_demo.xml +++ b/stock_vertical_lift/demo/stock_picking_demo.xml @@ -1,52 +1,52 @@ - - - Outgoing shipment from Vertical Lift (demo) - - - - - - + + + Outgoing shipment from Vertical Lift (demo) + + + + + + - - - + + + - - - + + + - - - Incoming shipment from Vertical Lift (demo) - - - - - - + + + Incoming shipment from Vertical Lift (demo) + + + + + + - - - + + + diff --git a/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml b/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml index 435c80f3bcf5..13cab7bcda1d 100644 --- a/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml +++ b/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml @@ -1,22 +1,22 @@ - - Shuttle 1 - - pick - + + Shuttle 1 + + pick + - - Shuttle 2 - - pick - + + Shuttle 2 + + pick + - - Shuttle 3 - - pick - + + Shuttle 3 + + pick + diff --git a/stock_vertical_lift/images/O-BTN.release.svg b/stock_vertical_lift/images/O-BTN.release.svg index 42535a126d9a..3755b9da1cad 100644 --- a/stock_vertical_lift/images/O-BTN.release.svg +++ b/stock_vertical_lift/images/O-BTN.release.svg @@ -52,4 +52,4 @@ -O-BTN.release \ No newline at end of file +O-BTN.release diff --git a/stock_vertical_lift/images/O-BTN.save.svg b/stock_vertical_lift/images/O-BTN.save.svg index f32e290a8e53..71a46a893902 100644 --- a/stock_vertical_lift/images/O-BTN.save.svg +++ b/stock_vertical_lift/images/O-BTN.save.svg @@ -43,4 +43,4 @@ -O-BTN.save \ No newline at end of file +O-BTN.save diff --git a/stock_vertical_lift/models/stock_location.py b/stock_vertical_lift/models/stock_location.py index 052bb6456479..3b7a394be1a7 100644 --- a/stock_vertical_lift/models/stock_location.py +++ b/stock_vertical_lift/models/stock_location.py @@ -10,8 +10,7 @@ class StockLocation(models.Model): vertical_lift_location = fields.Boolean( "Is a Vertical Lift View Location?", default=False, - help="Check this box to use it as the view for Vertical" - " Lift Shuttles.", + help="Check this box to use it as the view for Vertical" " Lift Shuttles.", ) vertical_lift_kind = fields.Selection( selection=[ @@ -29,9 +28,7 @@ class StockLocation(models.Model): # give the unique shuttle for any location in the tree (whether it's a # shuttle, a tray or a cell) inverse_vertical_lift_shuttle_ids = fields.One2many( - comodel_name="vertical.lift.shuttle", - inverse_name="location_id", - readonly=True, + comodel_name="vertical.lift.shuttle", inverse_name="location_id", readonly=True ) # compute the unique shuttle for any shuttle, tray or cell location, by # going through the parents @@ -42,9 +39,7 @@ class StockLocation(models.Model): ) @api.depends( - "location_id", - "location_id.vertical_lift_kind", - "vertical_lift_location", + "location_id", "location_id.vertical_lift_kind", "vertical_lift_location" ) def _compute_vertical_lift_kind(self): tree = {"view": "shuttle", "shuttle": "tray", "tray": "cell"} @@ -57,8 +52,7 @@ def _compute_vertical_lift_kind(self): location.vertical_lift_kind = kind @api.depends( - "inverse_vertical_lift_shuttle_ids", - "location_id.vertical_lift_shuttle_id", + "inverse_vertical_lift_shuttle_ids", "location_id.vertical_lift_shuttle_id" ) def _compute_vertical_lift_shuttle_id(self): for location in self: @@ -114,20 +108,13 @@ def _hardware_vertical_lift_tray_payload(self, cell_location=None): if self.vertical_lift_shuttle_id.hardware == "simulation": message = _("Opening tray {}.").format(self.name) if cell_location: - from_left, from_bottom = ( - cell_location.tray_cell_center_position() - ) - message += _( - "
Laser pointer on x{} y{} ({}mm, {}mm)" - ).format( - cell_location.posx, - cell_location.posy, - from_left, - from_bottom, + from_left, from_bottom = cell_location.tray_cell_center_position() + message += _("
Laser pointer on x{} y{} ({}mm, {}mm)").format( + cell_location.posx, cell_location.posy, from_left, from_bottom ) return message else: - raise NotImplemented() + raise NotImplementedError() def fetch_vertical_lift_tray(self, cell_location=None): """Send instructions to the vertical lift hardware @@ -146,8 +133,7 @@ def fetch_vertical_lift_tray(self, cell_location=None): if self.vertical_lift_kind == "cell": if cell_location: raise ValueError( - "cell_location cannot be set when the location is " - "a cell." + "cell_location cannot be set when the location is a cell." ) tray = self.location_id tray.fetch_vertical_lift_tray(cell_location=self) @@ -155,8 +141,7 @@ def fetch_vertical_lift_tray(self, cell_location=None): self._hardware_vertical_lift_tray(cell_location=cell_location) else: raise exceptions.UserError( - _("Cannot fetch a vertical lift tray on location %s") - % (self.name,) + _("Cannot fetch a vertical lift tray on location %s") % (self.name,) ) return True diff --git a/stock_vertical_lift/models/stock_move.py b/stock_vertical_lift/models/stock_move.py index 42222e716edd..fe0ca70e35f2 100644 --- a/stock_vertical_lift/models/stock_move.py +++ b/stock_vertical_lift/models/stock_move.py @@ -17,12 +17,7 @@ def write(self, vals): # count of assigned move lines may change (and we track this in # stock.move, not stock.move.line, because the state of the lines # is a related to this one). - models = ( - "vertical.lift.operation.pick", - "vertical.lift.operation.put", - ) + models = ("vertical.lift.operation.pick", "vertical.lift.operation.put") for model in models: - self.env[model].invalidate_cache( - ["number_of_ops", "number_of_ops_all"] - ) + self.env[model].invalidate_cache(["number_of_ops", "number_of_ops_all"]) return result diff --git a/stock_vertical_lift/models/stock_quant.py b/stock_vertical_lift/models/stock_quant.py index 5a5ba4f86aa8..a31ad9cde114 100644 --- a/stock_vertical_lift/models/stock_quant.py +++ b/stock_vertical_lift/models/stock_quant.py @@ -5,19 +5,14 @@ class StockQuant(models.Model): - _inherit = 'stock.quant' + _inherit = "stock.quant" def _update_available_quantity(self, *args, **kwargs): result = super()._update_available_quantity(*args, **kwargs) # We cannot have fields to depends on to invalidate this computed # fields on vertical.lift.operation.* models. But we know that when the # quantity of quant changes, we can invalidate the field - models = ( - "vertical.lift.operation.pick", - "vertical.lift.operation.put", - ) + models = ("vertical.lift.operation.pick", "vertical.lift.operation.put") for model in models: - self.env[model].invalidate_cache( - ["tray_qty"] - ) + self.env[model].invalidate_cache(["tray_qty"]) return result diff --git a/stock_vertical_lift/models/vertical_lift_command.py b/stock_vertical_lift/models/vertical_lift_command.py index 7c25b7f1f5ee..359ea6c2222a 100644 --- a/stock_vertical_lift/models/vertical_lift_command.py +++ b/stock_vertical_lift/models/vertical_lift_command.py @@ -2,49 +2,47 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from odoo import api, exceptions, fields, models +from odoo import _, api, exceptions, fields, models _logger = logging.getLogger(__name__) class VerticalLiftCommand(models.Model): - _name = 'vertical.lift.command' - _order = 'shuttle_id, name desc' + _name = "vertical.lift.command" + _order = "shuttle_id, name desc" _description = "commands sent to the shuttle" @api.model def _default_name(self): - return self.env['ir.sequence'].next_by_code('vertical.lift.command') + return self.env["ir.sequence"].next_by_code("vertical.lift.command") - name = fields.Char( - 'Name', default=_default_name, required=True, index=True - ) + name = fields.Char("Name", default=_default_name, required=True, index=True) command = fields.Char(required=True) answer = fields.Char() error = fields.Char() - shuttle_id = fields.Many2one('vertical.lift.shuttle', required=True) + shuttle_id = fields.Many2one("vertical.lift.shuttle", required=True) @api.model def record_answer(self, answer): name = self._get_key(answer) - record = self.search([('name', '=', name)], limit=1) + record = self.search([("name", "=", name)], limit=1) if not record: - _logger.error('unable to match answer to a command: %r', answer) - raise exceptions.UserError('Unknown record %s' % name) + _logger.error("unable to match answer to a command: %r", answer) + raise exceptions.UserError(_("Unknown record %s") % name) record.answer = answer record.shuttle_id._hardware_response_callback(record) return record def _get_key(self, answer): - key = answer.split('|')[1] + key = answer.split("|")[1] return key @api.model_create_multi - @api.returns('self', lambda value: value.id) + @api.returns("self", lambda value: value.id) def create(self, vals_list): for values in vals_list: if "name" not in values: - name = self._get_key(values.get('command')) + name = self._get_key(values.get("command")) if name: values["name"] = name return super().create(vals_list) diff --git a/stock_vertical_lift/models/vertical_lift_operation_base.py b/stock_vertical_lift/models/vertical_lift_operation_base.py index 6108f7418ce0..bd2d471be919 100644 --- a/stock_vertical_lift/models/vertical_lift_operation_base.py +++ b/stock_vertical_lift/models/vertical_lift_operation_base.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models + from odoo.addons.base_sparse_field.models.fields import Serialized @@ -16,9 +17,7 @@ class VerticalLiftOperationBase(models.AbstractModel): shuttle_id = fields.Many2one( comodel_name="vertical.lift.shuttle", required=True, readonly=True ) - location_id = fields.Many2one( - related="shuttle_id.location_id", readonly=True - ) + location_id = fields.Many2one(related="shuttle_id.location_id", readonly=True) number_of_ops = fields.Integer( compute="_compute_number_of_ops", string="Number of Operations" ) @@ -27,9 +26,7 @@ class VerticalLiftOperationBase(models.AbstractModel): string="Number of Operations in all shuttles", ) mode = fields.Selection(related="shuttle_id.mode", readonly=True) - operation_descr = fields.Char( - string="Operation", default="...", readonly=True - ) + operation_descr = fields.Char(string="Operation", default="...", readonly=True) _sql_constraints = [ ( @@ -83,17 +80,12 @@ def _render_product_packagings(self, product): for pkg in product.packaging_ids ] } - content = self.env["ir.qweb"].render( - "stock_vertical_lift.packagings", values - ) + content = self.env["ir.qweb"].render("stock_vertical_lift.packagings", values) return content def _get_tray_qty(self, product, location): quants = self.env["stock.quant"].search( - [ - ("location_id", "=", location.id), - ("product_id", "=", product.id), - ] + [("location_id", "=", location.id), ("product_id", "=", product.id)] ) return sum(quants.mapped("quantity")) @@ -112,10 +104,7 @@ def _send_notification_refresh(self): channel = "notify_vertical_lift_screen" bus_message = { "action": "refresh", - "params": { - "model": self._name, - "id": self.id, - } + "params": {"model": self._name, "id": self.id}, } self.env["bus.bus"].sendone(channel, bus_message) @@ -142,15 +131,11 @@ class VerticalLiftOperationTransfer(models.AbstractModel): compute="_compute_tray_data", string="Tray Type", ) - tray_type_code = fields.Char( - compute="_compute_tray_data", string="Tray Code" - ) + tray_type_code = fields.Char(compute="_compute_tray_data", string="Tray Code") tray_x = fields.Integer(string="X", compute="_compute_tray_data") tray_y = fields.Integer(string="Y", compute="_compute_tray_data") tray_matrix = Serialized(string="Cells", compute="_compute_tray_data") - tray_qty = fields.Float( - string="Stock Quantity", compute="_compute_tray_qty" - ) + tray_qty = fields.Float(string="Stock Quantity", compute="_compute_tray_qty") # current operation information picking_id = fields.Many2one( @@ -174,12 +159,8 @@ class VerticalLiftOperationTransfer(models.AbstractModel): product_packagings = fields.Html( string="Packaging", compute="_compute_product_packagings" ) - qty_done = fields.Float( - related="current_move_line_id.qty_done", readonly=True - ) - lot_id = fields.Many2one( - related="current_move_line_id.lot_id", readonly=True - ) + qty_done = fields.Float(related="current_move_line_id.qty_done", readonly=True) + lot_id = fields.Many2one(related="current_move_line_id.lot_id", readonly=True) location_dest_id = fields.Many2one( string="Destination", related="current_move_line_id.location_dest_id", @@ -217,7 +198,7 @@ def _compute_number_of_ops_all(self): def _compute_tray_qty(self): for record in self: if not (record.tray_location_id and record.current_move_line_id): - record.tray_qty = 0. + record.tray_qty = 0.0 continue product = record.current_move_line_id.product_id location = record.tray_location_id @@ -250,9 +231,7 @@ def _domain_move_lines_to_do_all(self): def count_move_lines_to_do(self): """Count move lines to process in current shuttles""" self.ensure_one() - return self.env["stock.move.line"].search_count( - self._domain_move_lines_to_do() - ) + return self.env["stock.move.line"].search_count(self._domain_move_lines_to_do()) def count_move_lines_to_do_all(self): """Count move lines to process in all shuttles""" diff --git a/stock_vertical_lift/models/vertical_lift_operation_inventory.py b/stock_vertical_lift/models/vertical_lift_operation_inventory.py index d1a6088ed071..52040d9747ac 100644 --- a/stock_vertical_lift/models/vertical_lift_operation_inventory.py +++ b/stock_vertical_lift/models/vertical_lift_operation_inventory.py @@ -2,9 +2,10 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models -from odoo.addons.base_sparse_field.models.fields import Serialized from odoo.tools import float_compare +from odoo.addons.base_sparse_field.models.fields import Serialized + # TODO handle autofocus + easy way to validate for the input field @@ -24,10 +25,7 @@ class VerticalLiftOperationInventory(models.Model): state = fields.Selection( selection=[ ("quantity", "Inventory, please enter the amount"), - ( - "confirm_wrong_quantity", - "The quantity does not match, are you sure?", - ), + ("confirm_wrong_quantity", "The quantity does not match, are you sure?"), ("save", "Save"), ], default="quantity", @@ -44,15 +42,11 @@ class VerticalLiftOperationInventory(models.Model): compute="_compute_tray_data", string="Tray Type", ) - tray_type_code = fields.Char( - compute="_compute_tray_data", string="Tray Code" - ) + tray_type_code = fields.Char(compute="_compute_tray_data", string="Tray Code") tray_x = fields.Integer(string="X", compute="_compute_tray_data") tray_y = fields.Integer(string="Y", compute="_compute_tray_data") tray_matrix = Serialized(string="Cells", compute="_compute_tray_data") - tray_qty = fields.Float( - string="Stock Quantity", compute="_compute_tray_qty" - ) + tray_qty = fields.Float(string="Stock Quantity", compute="_compute_tray_qty") # current operation information inventory_id = fields.Many2one( @@ -105,10 +99,8 @@ def _compute_product_packagings(self): @api.depends("tray_location_id", "current_inventory_line_id.product_id") def _compute_tray_qty(self): for record in self: - if not ( - record.tray_location_id and record.current_inventory_line_id - ): - record.tray_qty = 0. + if not (record.tray_location_id and record.current_inventory_line_id): + record.tray_qty = 0.0 continue product = record.current_inventory_line_id.product_id location = record.tray_location_id @@ -151,11 +143,7 @@ def on_screen_open(self): def reset(self): self.write( - { - "quantity_input": 0., - "last_quantity_input": 0., - "state": "quantity", - } + {"quantity_input": 0.0, "last_quantity_input": 0.0, "state": "quantity"} ) self.update_step_description() @@ -216,7 +204,7 @@ def _process_quantity(self): return True else: self.last_quantity_input = self.quantity_input - self.quantity_input = 0. + self.quantity_input = 0.0 self.step_to("confirm_wrong_quantity") return False if self.step() == "confirm_wrong_quantity": @@ -255,7 +243,6 @@ def select_next_inventory_line(self): self.reset() if ( next_line - and previous_line.vertical_lift_tray_id - != next_line.vertical_lift_tray_id + and previous_line.vertical_lift_tray_id != next_line.vertical_lift_tray_id ): self.fetch_tray() diff --git a/stock_vertical_lift/models/vertical_lift_operation_pick.py b/stock_vertical_lift/models/vertical_lift_operation_pick.py index 030541c82d94..d793ab78502d 100644 --- a/stock_vertical_lift/models/vertical_lift_operation_pick.py +++ b/stock_vertical_lift/models/vertical_lift_operation_pick.py @@ -11,9 +11,7 @@ class VerticalLiftOperationPick(models.Model): def on_barcode_scanned(self, barcode): self.ensure_one() - location = self.env["stock.location"].search( - [("barcode", "=", barcode)] - ) + location = self.env["stock.location"].search([("barcode", "=", barcode)]) if location: self.current_move_line_id.location_dest_id = location self.operation_descr = _("Save") @@ -65,9 +63,7 @@ def select_next_move_line(self): # TODO use a state machine to define next steps and # description? descr = ( - _("Scan New Destination Location") - if next_move_line - else _("No operations") + _("Scan New Destination Location") if next_move_line else _("No operations") ) self.operation_descr = descr if next_move_line: diff --git a/stock_vertical_lift/models/vertical_lift_operation_put.py b/stock_vertical_lift/models/vertical_lift_operation_put.py index 694ca30f930a..7309db684989 100644 --- a/stock_vertical_lift/models/vertical_lift_operation_put.py +++ b/stock_vertical_lift/models/vertical_lift_operation_put.py @@ -109,9 +109,7 @@ def process_current(self): self.current_operation_line_id.process() def button_release(self): - self.write( - {"operation_line_ids": [(2, self.current_operation_line_id.id)]} - ) + self.write({"operation_line_ids": [(2, self.current_operation_line_id.id)]}) return super().button_release() def button_save(self): @@ -138,9 +136,7 @@ def select_next_move_line(self): def action_select_operations(self): self.ensure_one() - menu_xmlid = ( - "stock_vertical_lift." "vertical_lift_operation_put_select_view" - ) + menu_xmlid = "stock_vertical_lift." "vertical_lift_operation_put_select_view" select_model = self.env["vertical.lift.operation.put.select"] select = select_model.create( { @@ -165,13 +161,9 @@ class VerticalLiftOperationPutLine(models.Model): _description = "Vertical Lift Operation Put Line" operation_id = fields.Many2one( - comodel_name="vertical.lift.operation.put", - required=True, - readonly=True, - ) - move_line_id = fields.Many2one( - comodel_name="stock.move.line", readonly=True + comodel_name="vertical.lift.operation.put", required=True, readonly=True ) + move_line_id = fields.Many2one(comodel_name="stock.move.line", readonly=True) def process(self): line = self.move_line_id @@ -186,9 +178,7 @@ class VerticalLiftOperationPutSelect(models.TransientModel): _description = "Vertical Lift Operation Put Select" operation_id = fields.Many2one( - comodel_name="vertical.lift.operation.put", - required=True, - readonly=True, + comodel_name="vertical.lift.operation.put", required=True, readonly=True ) move_line_ids = fields.Many2many(comodel_name="stock.move.line") @@ -197,10 +187,7 @@ def _sync_lines(self): operation_line_model = self.env["vertical.lift.operation.put.line"] operation_line_model.create( [ - { - "operation_id": self.operation_id.id, - "move_line_id": move_line.id, - } + {"operation_id": self.operation_id.id, "move_line_id": move_line.id} for move_line in self.move_line_ids ] ) @@ -217,9 +204,7 @@ def _move_line_domain(self): ] def action_add_all(self): - move_lines = self.env["stock.move.line"].search( - self._move_line_domain() - ) + move_lines = self.env["stock.move.line"].search(self._move_line_domain()) self.move_line_ids = move_lines self._sync_lines() return {"type": "ir.actions.act_window_close"} @@ -227,9 +212,7 @@ def action_add_all(self): def on_barcode_scanned(self, barcode): self.ensure_one() domain = self._move_line_domain() - domain = expression.AND( - [domain, [("product_id.barcode", "=", barcode)]] - ) + domain = expression.AND([domain, [("product_id.barcode", "=", barcode)]]) move_lines = self.env["stock.move.line"].search(domain) # note: on_barcode_scanned is called in an onchange, so 'self' # is a NewID, we can't use 'write()' on it. diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py index d8b584fe153e..79c70f7e63e5 100644 --- a/stock_vertical_lift/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift/models/vertical_lift_shuttle.py @@ -39,8 +39,7 @@ class VerticalLiftShuttle(models.Model): help="set this if the server expects TLS wrapped communication" ) command_ids = fields.One2many( - 'vertical.lift.command', 'shuttle_id', - string="Hardware commands" + "vertical.lift.command", "shuttle_id", string="Hardware commands" ) _sql_constraints = [ ( @@ -65,17 +64,10 @@ def _model_for_mode(self): @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" - ), + "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" + "stock_vertical_lift." "vertical_lift_operation_inventory_screen_view" ), } @@ -90,18 +82,12 @@ def _hardware_send_message(self, payload): """ self.ensure_one() - _logger.info('send %r', payload) - command_values = { - 'shuttle_id': self.id, - 'command': payload.decode(), - } + _logger.info("send %r", payload) + command_values = {"shuttle_id": self.id, "command": payload.decode()} - self.env['vertical.lift.command'].sudo().create( - command_values - ) + self.env["vertical.lift.command"].sudo().create(command_values) if self.hardware == "simulation": - self.env.user.notify_info(message=payload, - title=_("Lift Simulation")) + self.env.user.notify_info(message=payload, title=_("Lift Simulation")) return True else: conn = self._hardware_get_server_connection() diff --git a/stock_vertical_lift/static/src/js/vertical_lift.js b/stock_vertical_lift/static/src/js/vertical_lift.js index 542a06079376..1bc89e87c854 100644 --- a/stock_vertical_lift/static/src/js/vertical_lift.js +++ b/stock_vertical_lift/static/src/js/vertical_lift.js @@ -1,103 +1,103 @@ odoo.define('stock_vertical_lift.vertical_lift', function (require) { -"use strict"; + "use strict"; -var core = require('web.core'); -var KanbanRecord = require('web.KanbanRecord'); -var basicFields = require('web.basic_fields'); -var field_registry = require('web.field_registry'); -var FormController = require('web.FormController'); -var FieldInteger = basicFields.FieldInteger; + var KanbanRecord = require('web.KanbanRecord'); + var basicFields = require('web.basic_fields'); + var field_registry = require('web.field_registry'); + var FormController = require('web.FormController'); + var FieldInteger = basicFields.FieldInteger; -KanbanRecord.include({ + KanbanRecord.include({ - _openRecord: function () { - if (this.modelName === 'vertical.lift.shuttle' - && this.$el.hasClass("open_shuttle_screen")) { - var self = this; - this._rpc({ - method: 'action_open_screen', - model: self.modelName, - args: [self.id], - }).then(function (action) { - self.trigger_up('do_action', {action: action}); - }); - } else { - this._super.apply(this, arguments); - } - }, + _openRecord: function () { + if (this.modelName === 'vertical.lift.shuttle' && + this.$el.hasClass("open_shuttle_screen")) { + var self = this; + this._rpc({ + method: 'action_open_screen', + model: self.modelName, + args: [self.id], + }).then(function (action) { + self.trigger_up('do_action', {action: action}); + }); + } else { + this._super.apply(this, arguments); + } + }, -}); + }); -var ExitButton = FieldInteger.extend({ - tagName: 'button', - className: 'btn btn-danger btn-block btn-lg o_shuttle_exit', - events: { - 'click': '_onClick', - }, - _render: function () { - this.$el.text(this.string); - }, - _onClick: function () { - // the only reason to have this field widget is to be able - // to inject clear_breadcrumbs in the action: - // it will revert back to a normal - non-headless - view - this.do_action('stock_vertical_lift.vertical_lift_shuttle_action', { - clear_breadcrumbs: true, - }); - }, + var ExitButton = FieldInteger.extend({ + tagName: 'button', + className: 'btn btn-danger btn-block btn-lg o_shuttle_exit', + events: { + 'click': '_onClick', + }, + _render: function () { + this.$el.text(this.string); + }, + _onClick: function () { + // The only reason to have this field widget is to be able + // to inject clear_breadcrumbs in the action: + // it will revert back to a normal - non-headless - view + this.do_action('stock_vertical_lift.vertical_lift_shuttle_action', { + clear_breadcrumbs: true, + }); + }, -}); + }); -FormController.include({ - init: function (parent, model, renderer, params) { - this._super.apply(this, arguments); - if(this.modelName.startsWith('vertical.lift.operation.')) { - this.call('bus_service', 'addChannel', 'notify_vertical_lift_screen'); - this.call( - 'bus_service', 'on', 'notification', - this, this.vlift_bus_notification - ); - this.call('bus_service', 'startPolling'); - } - }, - vlift_bus_notification: function (notifications) { - var self = this; - _.each(notifications, function (notification) { - var channel = notification[0]; - var message = notification[1]; - if(channel === 'notify_vertical_lift_screen') { - switch(message['action']) { - case 'refresh': - self.vlift_bus_action_refresh(message['params']); - break; + FormController.include({ + init: function () { + this._super.apply(this, arguments); + if (this.modelName.startsWith('vertical.lift.operation.')) { + this.call('bus_service', 'addChannel', 'notify_vertical_lift_screen'); + this.call( + 'bus_service', 'on', 'notification', + this, this.vlift_bus_notification, + ); + this.call('bus_service', 'startPolling'); + } + }, + vlift_bus_notification: function (notifications) { + var self = this; + _.each(notifications, function (notification) { + var channel = notification[0]; + var message = notification[1]; + if (channel === 'notify_vertical_lift_screen') { + switch (message.action) { + case 'refresh': + self.vlift_bus_action_refresh(message.params); + break; + } } + }); + }, + vlift_bus_action_refresh: function (params) { + var selectedIds = this.getSelectedIds(); + if (!selectedIds.length) { + return; + } + var currentId = selectedIds[0]; + if (params.id === currentId && params.model === this.modelName) { + this.reload(); } - }); - }, - vlift_bus_action_refresh: function(params) { - var selectedIds = this.getSelectedIds(); - if(!selectedIds.length){ - return; - } - var currentId = selectedIds[0]; - if(params['id'] === currentId && params['model'] == this.modelName){ - this.reload(); - } - }, - destroy: function () { - if(this.modelName.startsWith('vertical.lift.operation.')) { - this.call('bus_service', 'deleteChannel', 'notify_vertical_lift_screen'); - } - this._super.apply(this, arguments); - } + }, + destroy: function () { + if (this.modelName.startsWith('vertical.lift.operation.')) { + this.call('bus_service', 'deleteChannel', + 'notify_vertical_lift_screen'); + } + this._super.apply(this, arguments); + }, -}); + }); -field_registry.add('vlift_shuttle_exit_button', ExitButton); + field_registry.add('vlift_shuttle_exit_button', ExitButton); -return { - ExitButton: ExitButton, -}; + return { + ExitButton: ExitButton, + }; }); diff --git a/stock_vertical_lift/tests/common.py b/stock_vertical_lift/tests/common.py index 0a6b613bc278..29190936c4dd 100644 --- a/stock_vertical_lift/tests/common.py +++ b/stock_vertical_lift/tests/common.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import _ + from odoo.addons.stock_location_tray.tests import common @@ -10,83 +11,68 @@ class VerticalLiftCase(common.LocationTrayTypeCase): def setUpClass(cls): super().setUpClass() cls.shuttle = cls.env.ref( - 'stock_vertical_lift.stock_vertical_lift_demo_shuttle_1' - ) - cls.product_socks = cls.env.ref( - 'stock_vertical_lift.product_running_socks' - ) - cls.product_recovery = cls.env.ref( - 'stock_vertical_lift.product_recovery_socks' + "stock_vertical_lift.stock_vertical_lift_demo_shuttle_1" ) + cls.product_socks = cls.env.ref("stock_vertical_lift.product_running_socks") + cls.product_recovery = cls.env.ref("stock_vertical_lift.product_recovery_socks") cls.vertical_lift_loc = cls.env.ref( - 'stock_vertical_lift.stock_location_vertical_lift' + "stock_vertical_lift.stock_location_vertical_lift" ) cls.location_1a = cls.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_1a" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a" ) cls.location_1a_x1y1 = cls.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_1a_x1y1" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x1y1" ) cls.location_1a_x2y1 = cls.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_1a_x2y1" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x2y1" ) cls.location_1a_x3y1 = cls.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_1a_x3y1" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x3y1" ) cls.location_1a_x1y2 = cls.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_1a_x1y2" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x1y2" ) cls.location_1b_x1y1 = cls.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_1b_x1y1" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1b_x1y1" ) cls.location_1b_x1y2 = cls.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_1b_x1y2" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1b_x1y2" ) cls.location_2a = cls.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_2a" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_2a" ) cls.location_2a_x1y1 = cls.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_2a_x1y1" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_2a_x1y1" ) def _update_qty_in_location(self, location, product, quantity): - self.env["stock.quant"]._update_available_quantity( - product, location, quantity - ) + self.env["stock.quant"]._update_available_quantity(product, location, quantity) @classmethod def _create_simple_picking_out(cls, product, quantity): - stock_loc = cls.env.ref('stock.stock_location_stock') - customer_loc = cls.env.ref('stock.stock_location_customers') - picking_type = cls.env.ref('stock.picking_type_out') - partner = cls.env.ref('base.res_partner_1') - return cls.env['stock.picking'].create( + stock_loc = cls.env.ref("stock.stock_location_stock") + customer_loc = cls.env.ref("stock.stock_location_customers") + picking_type = cls.env.ref("stock.picking_type_out") + partner = cls.env.ref("base.res_partner_1") + return cls.env["stock.picking"].create( { - 'picking_type_id': picking_type.id, - 'partner_id': partner.id, - 'location_id': stock_loc.id, - 'location_dest_id': customer_loc.id, - 'move_lines': [ + "picking_type_id": picking_type.id, + "partner_id": partner.id, + "location_id": stock_loc.id, + "location_dest_id": customer_loc.id, + "move_lines": [ ( 0, 0, { - 'name': product.name, - 'product_id': product.id, - 'product_uom': product.uom_id.id, - 'product_uom_qty': quantity, - 'picking_type_id': picking_type.id, - 'location_id': stock_loc.id, - 'location_dest_id': customer_loc.id, + "name": product.name, + "product_id": product.id, + "product_uom": product.uom_id.id, + "product_uom_qty": quantity, + "picking_type_id": picking_type.id, + "location_id": stock_loc.id, + "location_dest_id": customer_loc.id, }, ) ], @@ -95,27 +81,27 @@ def _create_simple_picking_out(cls, product, quantity): @classmethod def _create_simple_picking_in(cls, product, quantity, dest_location): - supplier_loc = cls.env.ref('stock.stock_location_suppliers') - picking_type = cls.env.ref('stock.picking_type_in') - partner = cls.env.ref('base.res_partner_1') - return cls.env['stock.picking'].create( + supplier_loc = cls.env.ref("stock.stock_location_suppliers") + picking_type = cls.env.ref("stock.picking_type_in") + partner = cls.env.ref("base.res_partner_1") + return cls.env["stock.picking"].create( { - 'picking_type_id': picking_type.id, - 'partner_id': partner.id, - 'location_id': supplier_loc.id, - 'location_dest_id': dest_location.id, - 'move_lines': [ + "picking_type_id": picking_type.id, + "partner_id": partner.id, + "location_id": supplier_loc.id, + "location_dest_id": dest_location.id, + "move_lines": [ ( 0, 0, { - 'name': product.name, - 'product_id': product.id, - 'product_uom': product.uom_id.id, - 'product_uom_qty': quantity, - 'picking_type_id': picking_type.id, - 'location_id': supplier_loc.id, - 'location_dest_id': dest_location.id, + "name": product.name, + "product_id": product.id, + "product_uom": product.uom_id.id, + "product_uom_qty": quantity, + "picking_type_id": picking_type.id, + "location_id": supplier_loc.id, + "location_dest_id": dest_location.id, }, ) ], @@ -123,21 +109,28 @@ def _create_simple_picking_in(cls, product, quantity, dest_location): ) @classmethod - def _create_inventory(self, products): + def _create_inventory(cls, products): """Create a draft inventory Products is a list of tuples (bin location, product). """ values = { - 'name': 'Test Inventory', - 'filter': 'partial', - 'line_ids': [(0, 0, { - 'product_id': product.id, - 'product_uom_id': product.uom_id.id, - 'location_id': location.id - }) for location, product in products] + "name": "Test Inventory", + "filter": "partial", + "line_ids": [ + ( + 0, + 0, + { + "product_id": product.id, + "product_uom_id": product.uom_id.id, + "location_id": location.id, + }, + ) + for location, product in products + ], } - inventory = self.env['stock.inventory'].create(values) + inventory = cls.env["stock.inventory"].create(values) inventory.action_start() return inventory diff --git a/stock_vertical_lift/tests/test_inventory.py b/stock_vertical_lift/tests/test_inventory.py index 68caaf6096b2..b911d01d2e97 100644 --- a/stock_vertical_lift/tests/test_inventory.py +++ b/stock_vertical_lift/tests/test_inventory.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import _ + from .common import VerticalLiftCase @@ -19,27 +20,19 @@ def test_inventory_action_open_screen(self): action = self.shuttle.action_open_screen() operation = self.shuttle._operation_for_mode() self.assertEqual(action["type"], "ir.actions.act_window") - self.assertEqual( - action["res_model"], "vertical.lift.operation.inventory" - ) + self.assertEqual(action["res_model"], "vertical.lift.operation.inventory") self.assertEqual(action["res_id"], operation.id) def test_inventory_count_ops(self): - self._update_qty_in_location( - self.location_1a_x1y1, self.product_socks, 10 - ) - self._update_qty_in_location( - self.location_1a_x2y1, self.product_recovery, 10 - ) + self._update_qty_in_location(self.location_1a_x1y1, self.product_socks, 10) + self._update_qty_in_location(self.location_1a_x2y1, self.product_recovery, 10) self._create_inventory( [ (self.location_1a_x1y1, self.product_socks), (self.location_1a_x2y1, self.product_recovery), ] ) - self._update_qty_in_location( - self.location_2a_x1y1, self.product_socks, 10 - ) + self._update_qty_in_location(self.location_2a_x1y1, self.product_socks, 10) self._create_inventory([(self.location_2a_x1y1, self.product_socks)]) self.shuttle.switch_inventory() @@ -48,17 +41,13 @@ def test_inventory_count_ops(self): self.assertEqual(operation.number_of_ops_all, 3) def test_process_current_inventory(self): - self._update_qty_in_location( - self.location_1a_x1y1, self.product_socks, 10 - ) + self._update_qty_in_location(self.location_1a_x1y1, self.product_socks, 10) inventory = self._create_inventory( [(self.location_1a_x1y1, self.product_socks)] ) self.shuttle.switch_inventory() operation = self.shuttle._operation_for_mode() - self.assertEqual( - operation.current_inventory_line_id, inventory.line_ids - ) + self.assertEqual(operation.current_inventory_line_id, inventory.line_ids) # test the happy path, quantity is correct operation.quantity_input = 10.0 result = operation.button_save() @@ -79,9 +68,7 @@ def test_process_current_inventory(self): self.assertEqual(result, expected_result) def test_wrong_quantity(self): - self._update_qty_in_location( - self.location_1a_x1y1, self.product_socks, 10 - ) + self._update_qty_in_location(self.location_1a_x1y1, self.product_socks, 10) inventory = self._create_inventory( [(self.location_1a_x1y1, self.product_socks)] ) @@ -97,8 +84,7 @@ def test_wrong_quantity(self): self.assertEqual(operation.state, "confirm_wrong_quantity") self.assertEqual(operation.current_inventory_line_id, line) self.assertEqual( - operation.operation_descr, - _("The quantity does not match, are you sure?"), + operation.operation_descr, _("The quantity does not match, are you sure?") ) # entering the same quantity a second time validates diff --git a/stock_vertical_lift/tests/test_location.py b/stock_vertical_lift/tests/test_location.py index 710c9cdc0a5c..712436d73444 100644 --- a/stock_vertical_lift/tests/test_location.py +++ b/stock_vertical_lift/tests/test_location.py @@ -9,32 +9,29 @@ def test_vertical_lift_kind(self): # this boolean is what defines a "Vertical Lift View", the upper level # of the tree (View -> Shuttles -> Trays -> Cells) self.assertTrue(self.vertical_lift_loc.vertical_lift_location) - self.assertEqual(self.vertical_lift_loc.vertical_lift_kind, 'view') + self.assertEqual(self.vertical_lift_loc.vertical_lift_kind, "view") # check types accross the hierarchy shuttles = self.vertical_lift_loc.child_ids self.assertTrue( - all( - location.vertical_lift_kind == 'shuttle' - for location in shuttles - ) + all(location.vertical_lift_kind == "shuttle" for location in shuttles) ) - trays = shuttles.mapped('child_ids') + trays = shuttles.mapped("child_ids") self.assertTrue( - all(location.vertical_lift_kind == 'tray' for location in trays) + all(location.vertical_lift_kind == "tray" for location in trays) ) - cells = trays.mapped('child_ids') + cells = trays.mapped("child_ids") self.assertTrue( - all(location.vertical_lift_kind == 'cell' for location in cells) + all(location.vertical_lift_kind == "cell" for location in cells) ) def test_create_shuttle(self): # any location created directly under the view is a shuttle - shuttle_loc = self.env['stock.location'].create( + shuttle_loc = self.env["stock.location"].create( { - 'name': 'Shuttle 42', - 'location_id': self.vertical_lift_loc.id, - 'usage': 'internal', + "name": "Shuttle 42", + "location_id": self.vertical_lift_loc.id, + "usage": "internal", } ) - self.assertEqual(shuttle_loc.vertical_lift_kind, 'shuttle') + self.assertEqual(shuttle_loc.vertical_lift_kind, "shuttle") diff --git a/stock_vertical_lift/tests/test_pick.py b/stock_vertical_lift/tests/test_pick.py index 894eb5fa69ed..6d17dd831ec6 100644 --- a/stock_vertical_lift/tests/test_pick.py +++ b/stock_vertical_lift/tests/test_pick.py @@ -21,8 +21,7 @@ def test_switch_pick(self): self.shuttle.switch_pick() self.assertEqual(self.shuttle.mode, "pick") self.assertEqual( - self.shuttle._operation_for_mode().current_move_line_id, - self.out_move_line, + self.shuttle._operation_for_mode().current_move_line_id, self.out_move_line ) def test_pick_action_open_screen(self): @@ -39,9 +38,7 @@ def test_pick_select_next_move_line(self): operation = self.shuttle._operation_for_mode() operation.select_next_move_line() self.assertEqual(operation.current_move_line_id, self.out_move_line) - self.assertEqual( - operation.operation_descr, _("Scan New Destination Location") - ) + self.assertEqual(operation.operation_descr, _("Scan New Destination Location")) def test_pick_save(self): self.shuttle.switch_pick() @@ -71,8 +68,7 @@ def test_pick_related_fields(self): ml.location_id.location_id.tray_type_id, ) self.assertEqual( - operation.tray_type_code, - ml.location_id.location_id.tray_type_id.code, + operation.tray_type_code, ml.location_id.location_id.tray_type_id.code ) self.assertEqual(operation.tray_x, ml.location_id.posx) self.assertEqual(operation.tray_y, ml.location_id.posy) @@ -80,9 +76,7 @@ def test_pick_related_fields(self): # Move line related fields self.assertEqual(operation.picking_id, ml.picking_id) self.assertEqual(operation.picking_origin, ml.picking_id.origin) - self.assertEqual( - operation.picking_partner_id, ml.picking_id.partner_id - ) + self.assertEqual(operation.picking_partner_id, ml.picking_id.partner_id) self.assertEqual(operation.product_id, ml.product_id) self.assertEqual(operation.product_uom_id, ml.product_uom_id) self.assertEqual(operation.product_uom_qty, ml.product_uom_qty) @@ -100,13 +94,11 @@ def test_pick_count_move_lines(self): # ensure that we have stock in some cells, we'll put product1 # in the first Shuttle and product2 in the second cell1 = self.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_1a_x3y2" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x3y2" ) self._update_quantity_in_cell(cell1, product1, 50) cell2 = self.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_2a_x1y1" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_2a_x1y1" ) self._update_quantity_in_cell(cell2, product2, 50) @@ -203,8 +195,7 @@ def test_matrix(self): def test_tray_qty(self): cell = self.env.ref( - "stock_vertical_lift." - "stock_location_vertical_lift_demo_tray_1a_x3y2" + "stock_vertical_lift." "stock_location_vertical_lift_demo_tray_1a_x3y2" ) self.out_move_line.location_id = cell operation = self.shuttle._operation_for_mode() diff --git a/stock_vertical_lift/tests/test_put.py b/stock_vertical_lift/tests/test_put.py index d3f3911007d9..4ddbaf863d50 100644 --- a/stock_vertical_lift/tests/test_put.py +++ b/stock_vertical_lift/tests/test_put.py @@ -56,13 +56,10 @@ def test_select_from_barcode(self): operation = self.shuttle._operation_for_mode() select = select_model.create({"operation_id": operation.id}) select.on_barcode_scanned(self.product_socks.barcode) - self.assertRecordValues( - select, [{"move_line_ids": put1.move_line_ids.ids}] - ) + self.assertRecordValues(select, [{"move_line_ids": put1.move_line_ids.ids}]) select.on_barcode_scanned(self.product_recovery.barcode) self.assertRecordValues( - select, - [{"move_line_ids": (put1.move_line_ids | put2.move_line_ids).ids}], + select, [{"move_line_ids": (put1.move_line_ids | put2.move_line_ids).ids}] ) select.action_validate() self.assertEqual(len(operation.operation_line_ids), 2) diff --git a/stock_vertical_lift/views/shuttle_screen_templates.xml b/stock_vertical_lift/views/shuttle_screen_templates.xml index 3301f4ca70df..5d017220a9f3 100644 --- a/stock_vertical_lift/views/shuttle_screen_templates.xml +++ b/stock_vertical_lift/views/shuttle_screen_templates.xml @@ -1,17 +1,17 @@ - + diff --git a/stock_vertical_lift/views/stock_vertical_lift_templates.xml b/stock_vertical_lift/views/stock_vertical_lift_templates.xml index d1931819c9d2..11f1a3cf5e18 100644 --- a/stock_vertical_lift/views/stock_vertical_lift_templates.xml +++ b/stock_vertical_lift/views/stock_vertical_lift_templates.xml @@ -1,11 +1,11 @@ - + diff --git a/stock_vertical_lift/views/vertical_lift_operation_pick_views.xml b/stock_vertical_lift/views/vertical_lift_operation_pick_views.xml index 6d538fdcba7a..ecc137b51030 100644 --- a/stock_vertical_lift/views/vertical_lift_operation_pick_views.xml +++ b/stock_vertical_lift/views/vertical_lift_operation_pick_views.xml @@ -13,17 +13,17 @@
- - vertical.lift.operation.pick.screen.view - vertical.lift.operation.pick - - 100 - primary - -
- Pick Screen -
-
-
+ + vertical.lift.operation.pick.screen.view + vertical.lift.operation.pick + + 100 + primary + +
+ Pick Screen +
+
+
diff --git a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml index 2f81f5d30a1f..427196b30b44 100644 --- a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml +++ b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml @@ -1,176 +1,176 @@ - - vertical.lift.shuttle.view.form.menu - vertical.lift.shuttle - 100 - -
-
-
-
-
- -
-
-
-
-
-
- - - vertical.lift.shuttle.manual.barcode.view.form - vertical.lift.shuttle.manual.barcode - -
-
-
- -
-
-
-
-
-
-
-
- - - vertical.lift.shuttle.view.form - vertical.lift.shuttle - -
- - - - - - - - - - - - - - - -
-
-
- - - vertical.lift.shuttle.kanban - vertical.lift.shuttle - - - - - - - - -
-
- -
-
-
- - - + + vertical.lift.shuttle.view.form.menu + vertical.lift.shuttle + 100 + +
+
+
+
-
-
- Mode: - -
- - - - - - - - +
+
-
+
+
+
+
- - - - - - +
+ + + + + + vertical.lift.shuttle.view.form + vertical.lift.shuttle + +
+ + + + + + + + + + + + + + + +
+
+
+ + + vertical.lift.shuttle.kanban + vertical.lift.shuttle + + + + + + + + +
+
+ +
+
+
+ + + +
+
+
+ Mode: + +
+ + + + + + + + +
+
+
+ + + +
+
+
+
+
+
+
- - vertical.lift.shuttle.tree - vertical.lift.shuttle - - - - - - + + vertical.lift.shuttle.tree + vertical.lift.shuttle + + + + + + - - Vertical Lift Shuttles - ir.actions.act_window - vertical.lift.shuttle - form - kanban,tree,form - current - [] - {} - -

- Open the Shuttle Interface. -

-
-
+ + Vertical Lift Shuttles + ir.actions.act_window + vertical.lift.shuttle + form + kanban,tree,form + current + [] + {} + +

+ Open the Shuttle Interface. +

+
+
- + diff --git a/stock_vertical_lift_kardex/README.rst b/stock_vertical_lift_kardex/README.rst new file mode 100644 index 000000000000..8d4a3f73021d --- /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 8ccd05222861..e973ec0b5ae2 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 5a191bac8b4e..51a3830f6e9a 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 39d66fc28e54..8b3cd73c25db 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 @@ def _hardware_kardex_prepare_payload(self, cell_location=None): 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 @@ def _hardware_vertical_lift_tray_payload(self, cell_location=None): _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 256439d35a41..54e046dfd997 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 f568c5a91af4..97607999de6a 100644 --- a/stock_vertical_lift_kardex/proxy/kardex-proxy.py +++ b/stock_vertical_lift_kardex/proxy/kardex-proxy.py @@ -87,11 +87,11 @@ def data_received(self, 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') + 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 @@ def connection_lost(self, exc): 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 5b6ad518890b..a7899f21a8ac 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 @@ async def process_queue(self): 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 000000000000..ae2800eba1ff --- /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 b574c829c1846d1a533f91b5a1ed260e16900673 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 16 Dec 2019 15:52:03 +0100 Subject: [PATCH 13/41] [MIG] stock_vertical_lift{,_kardex}: Migration to 13.0 --- stock_vertical_lift/__manifest__.py | 2 +- stock_vertical_lift/demo/product_demo.xml | 2 -- stock_vertical_lift/demo/stock_inventory_demo.xml | 5 ++++- stock_vertical_lift/demo/stock_location_demo.xml | 1 - stock_vertical_lift/models/stock_location.py | 13 +++++++------ stock_vertical_lift/models/stock_move.py | 3 +-- stock_vertical_lift/models/vertical_lift_command.py | 9 ++++----- stock_vertical_lift/models/vertical_lift_shuttle.py | 6 ++---- stock_vertical_lift/tests/common.py | 1 - stock_vertical_lift/views/stock_move_line_views.xml | 2 +- .../views/vertical_lift_shuttle_views.xml | 1 - stock_vertical_lift_kardex/__manifest__.py | 2 +- .../models/vertical_lift_shuttle.py | 3 +-- 13 files changed, 22 insertions(+), 28 deletions(-) diff --git a/stock_vertical_lift/__manifest__.py b/stock_vertical_lift/__manifest__.py index f0860072727f..84b26b75926c 100644 --- a/stock_vertical_lift/__manifest__.py +++ b/stock_vertical_lift/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Vertical Lift", "summary": "Provides the core for integration with 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/demo/product_demo.xml b/stock_vertical_lift/demo/product_demo.xml index 0f59a88a315b..e372bb10050e 100644 --- a/stock_vertical_lift/demo/product_demo.xml +++ b/stock_vertical_lift/demo/product_demo.xml @@ -13,7 +13,6 @@ none - @@ -28,7 +27,6 @@ none - diff --git a/stock_vertical_lift/demo/stock_inventory_demo.xml b/stock_vertical_lift/demo/stock_inventory_demo.xml index 719b0d5e7d9d..18b0664201fb 100644 --- a/stock_vertical_lift/demo/stock_inventory_demo.xml +++ b/stock_vertical_lift/demo/stock_inventory_demo.xml @@ -13,8 +13,11 @@ - + + + + diff --git a/stock_vertical_lift/demo/stock_location_demo.xml b/stock_vertical_lift/demo/stock_location_demo.xml index b66667e8ea38..9b352e76bc11 100644 --- a/stock_vertical_lift/demo/stock_location_demo.xml +++ b/stock_vertical_lift/demo/stock_location_demo.xml @@ -11,7 +11,6 @@ internal - diff --git a/stock_vertical_lift/models/stock_location.py b/stock_vertical_lift/models/stock_location.py index 3b7a394be1a7..fdc9dff898e4 100644 --- a/stock_vertical_lift/models/stock_location.py +++ b/stock_vertical_lift/models/stock_location.py @@ -47,9 +47,8 @@ def _compute_vertical_lift_kind(self): if location.vertical_lift_location: location.vertical_lift_kind = "view" continue - kind = tree.get(location.location_id.vertical_lift_kind) - if kind: - location.vertical_lift_kind = kind + kind = tree.get(location.location_id.vertical_lift_kind, False) + location.vertical_lift_kind = kind @api.depends( "inverse_vertical_lift_shuttle_ids", "location_id.vertical_lift_shuttle_id" @@ -66,8 +65,7 @@ def _compute_vertical_lift_shuttle_id(self): def _hardware_vertical_lift_tray(self, cell_location=None): payload = self._hardware_vertical_lift_tray_payload(cell_location) - res = self.vertical_lift_shuttle_id._hardware_send_message(payload) - return res + return self.vertical_lift_shuttle_id._hardware_send_message(payload) def _hardware_vertical_lift_tray_payload(self, cell_location=None): """Prepare the message to be sent to the vertical lift hardware @@ -104,6 +102,9 @@ def _hardware_vertical_lift_tray_payload(self, cell_location=None): 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. + + Returns a message in bytes, that will be sent through + ``VerticalLiftShuttle._hardware_send_message()``. """ if self.vertical_lift_shuttle_id.hardware == "simulation": message = _("Opening tray {}.").format(self.name) @@ -112,7 +113,7 @@ def _hardware_vertical_lift_tray_payload(self, cell_location=None): message += _("
Laser pointer on x{} y{} ({}mm, {}mm)").format( cell_location.posx, cell_location.posy, from_left, from_bottom ) - return message + return message.encode("utf-8") else: raise NotImplementedError() diff --git a/stock_vertical_lift/models/stock_move.py b/stock_vertical_lift/models/stock_move.py index fe0ca70e35f2..b176b73434d0 100644 --- a/stock_vertical_lift/models/stock_move.py +++ b/stock_vertical_lift/models/stock_move.py @@ -1,13 +1,12 @@ # 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 class StockMove(models.Model): _inherit = "stock.move" - @api.multi def write(self, vals): result = super().write(vals) if "state" in vals: diff --git a/stock_vertical_lift/models/vertical_lift_command.py b/stock_vertical_lift/models/vertical_lift_command.py index 359ea6c2222a..39883e9be5c0 100644 --- a/stock_vertical_lift/models/vertical_lift_command.py +++ b/stock_vertical_lift/models/vertical_lift_command.py @@ -12,17 +12,17 @@ class VerticalLiftCommand(models.Model): _order = "shuttle_id, name desc" _description = "commands sent to the shuttle" - @api.model def _default_name(self): return self.env["ir.sequence"].next_by_code("vertical.lift.command") - name = fields.Char("Name", default=_default_name, required=True, index=True) + name = fields.Char( + "Name", default=lambda s: s._default_name(), required=True, index=True + ) command = fields.Char(required=True) answer = fields.Char() error = fields.Char() shuttle_id = fields.Many2one("vertical.lift.shuttle", required=True) - @api.model def record_answer(self, answer): name = self._get_key(answer) record = self.search([("name", "=", name)], limit=1) @@ -34,11 +34,10 @@ def record_answer(self, answer): return record def _get_key(self, answer): - key = answer.split("|")[1] + key = answer.split("|")[1:2] return key @api.model_create_multi - @api.returns("self", lambda value: value.id) def create(self, vals_list): for values in vals_list: if "name" not in values: diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py index 79c70f7e63e5..1d5670acc999 100644 --- a/stock_vertical_lift/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift/models/vertical_lift_shuttle.py @@ -4,7 +4,7 @@ import socket import ssl -from odoo import _, api, fields, models +from odoo import _, fields, models _logger = logging.getLogger(__name__) @@ -49,7 +49,6 @@ class VerticalLiftShuttle(models.Model): ) ] - @api.model def _selection_hardware(self): return [("simulation", "Simulation")] @@ -161,7 +160,7 @@ def action_open_screen(self): "res_id": operation.id, "target": "fullscreen", "flags": { - "headless": True, + "withControlPanel": False, "form_view_initial_mode": "edit", "no_breadcrumbs": True, }, @@ -222,7 +221,6 @@ class VerticalLiftShuttleManualBarcode(models.TransientModel): barcode = fields.Char(string="Barcode") - @api.multi def button_save(self): active_id = self.env.context.get("active_id") model = self.env.context.get("active_model") diff --git a/stock_vertical_lift/tests/common.py b/stock_vertical_lift/tests/common.py index 29190936c4dd..701503ab5815 100644 --- a/stock_vertical_lift/tests/common.py +++ b/stock_vertical_lift/tests/common.py @@ -116,7 +116,6 @@ def _create_inventory(cls, products): """ values = { "name": "Test Inventory", - "filter": "partial", "line_ids": [ ( 0, diff --git a/stock_vertical_lift/views/stock_move_line_views.xml b/stock_vertical_lift/views/stock_move_line_views.xml index 39249814e392..5dab678aa9fc 100644 --- a/stock_vertical_lift/views/stock_move_line_views.xml +++ b/stock_vertical_lift/views/stock_move_line_views.xml @@ -2,7 +2,7 @@ - stock.move.line.operations.tree.tray.type + stock.move.line.operations.tree.vertical.lift stock.move.line diff --git a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml index 427196b30b44..104b61707d85 100644 --- a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml +++ b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml @@ -155,7 +155,6 @@ Vertical Lift Shuttles ir.actions.act_window vertical.lift.shuttle - form kanban,tree,form current [] diff --git a/stock_vertical_lift_kardex/__manifest__.py b/stock_vertical_lift_kardex/__manifest__.py index e973ec0b5ae2..4a1e76a8d0a2 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 54e046dfd997..7d0d81fd5464 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 @@ class VerticalLiftShuttle(models.Model): _inherit = "vertical.lift.shuttle" - @api.model def _selection_hardware(self): values = super()._selection_hardware() values += [("kardex", "Kardex")] From 141a0e6c7532ccada768ade5a7e35dd6892a0bf2 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 24 Jan 2020 17:21:07 +0100 Subject: [PATCH 14/41] Replace custom js by 'target': 'main' --- .../models/vertical_lift_shuttle.py | 6 ++++ .../static/src/js/vertical_lift.js | 29 +------------------ .../static/src/scss/vertical_lift.scss | 4 --- .../views/vertical_lift_shuttle_views.xml | 6 ++-- 4 files changed, 11 insertions(+), 34 deletions(-) diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py index 1d5670acc999..bb82f17f02b5 100644 --- a/stock_vertical_lift/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift/models/vertical_lift_shuttle.py @@ -177,6 +177,12 @@ def action_menu(self): "res_id": self.id, } + def action_back_to_settings(self): + 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", diff --git a/stock_vertical_lift/static/src/js/vertical_lift.js b/stock_vertical_lift/static/src/js/vertical_lift.js index 1bc89e87c854..28853f2b3058 100644 --- a/stock_vertical_lift/static/src/js/vertical_lift.js +++ b/stock_vertical_lift/static/src/js/vertical_lift.js @@ -2,10 +2,7 @@ odoo.define('stock_vertical_lift.vertical_lift', function (require) { "use strict"; var KanbanRecord = require('web.KanbanRecord'); - var basicFields = require('web.basic_fields'); - var field_registry = require('web.field_registry'); var FormController = require('web.FormController'); - var FieldInteger = basicFields.FieldInteger; KanbanRecord.include({ @@ -27,27 +24,6 @@ odoo.define('stock_vertical_lift.vertical_lift', function (require) { }); - var ExitButton = FieldInteger.extend({ - tagName: 'button', - className: 'btn btn-danger btn-block btn-lg o_shuttle_exit', - events: { - 'click': '_onClick', - }, - _render: function () { - this.$el.text(this.string); - }, - _onClick: function () { - // The only reason to have this field widget is to be able - // to inject clear_breadcrumbs in the action: - // it will revert back to a normal - non-headless - view - this.do_action('stock_vertical_lift.vertical_lift_shuttle_action', { - clear_breadcrumbs: true, - }); - }, - - }); - - FormController.include({ init: function () { this._super.apply(this, arguments); @@ -94,10 +70,7 @@ odoo.define('stock_vertical_lift.vertical_lift', function (require) { }); - field_registry.add('vlift_shuttle_exit_button', ExitButton); - return { - ExitButton: ExitButton, - }; + return {}; }); diff --git a/stock_vertical_lift/static/src/scss/vertical_lift.scss b/stock_vertical_lift/static/src/scss/vertical_lift.scss index 8cd7878aa5a2..5421618db826 100644 --- a/stock_vertical_lift/static/src/scss/vertical_lift.scss +++ b/stock_vertical_lift/static/src/scss/vertical_lift.scss @@ -103,10 +103,6 @@ font-size: 2em; text-transform: uppercase; } - - .o_shuttle_exit { - text-align: center; - } } .o_vlift_shuttle_popup { diff --git a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml index 104b61707d85..5cad32a6da27 100644 --- a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml +++ b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml @@ -23,8 +23,10 @@ class="btn-primary btn-block btn btn-lg"/>
- +
From 44576e69324d0dc715f014625a2856b5c09cd77e Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 3 Mar 2020 13:25:50 +0100 Subject: [PATCH 15/41] 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 6b4c0045feefa4e151f42592fb99a30173a3bd00 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 17 Mar 2020 11:02:38 +0100 Subject: [PATCH 16/41] run pre-commit with new prettiers --- stock_vertical_lift/data/ir_sequence.xml | 4 +- stock_vertical_lift/demo/product_demo.xml | 17 +- .../demo/stock_inventory_demo.xml | 29 +-- .../demo/stock_location_demo.xml | 96 +++++----- .../demo/stock_picking_demo.xml | 59 +++--- .../demo/vertical_lift_shuttle_demo.xml | 12 +- .../static/src/js/vertical_lift.js | 65 +++---- .../static/src/scss/vertical_lift.scss | 5 +- .../views/shuttle_screen_templates.xml | 20 ++- .../views/stock_location_views.xml | 42 ++--- .../views/stock_move_line_views.xml | 37 ++-- .../views/stock_vertical_lift_templates.xml | 21 ++- .../vertical_lift_operation_base_views.xml | 170 +++++++++++------- ...ertical_lift_operation_inventory_views.xml | 75 ++++---- .../vertical_lift_operation_pick_views.xml | 9 +- .../vertical_lift_operation_put_views.xml | 61 ++++--- .../views/vertical_lift_shuttle_views.xml | 119 +++++++----- 17 files changed, 482 insertions(+), 359 deletions(-) diff --git a/stock_vertical_lift/data/ir_sequence.xml b/stock_vertical_lift/data/ir_sequence.xml index 1345e7e39f76..384d6561c2ee 100644 --- a/stock_vertical_lift/data/ir_sequence.xml +++ b/stock_vertical_lift/data/ir_sequence.xml @@ -1,10 +1,10 @@ - + Vertical Lift Commands vertical.lift.command L 6 - + diff --git a/stock_vertical_lift/demo/product_demo.xml b/stock_vertical_lift/demo/product_demo.xml index e372bb10050e..779d09713312 100644 --- a/stock_vertical_lift/demo/product_demo.xml +++ b/stock_vertical_lift/demo/product_demo.xml @@ -1,32 +1,29 @@ - + - RS200 4491673293664 Running Socks product - + 30.0 20.0 1.0 none - - + + - RS300 2779891103531 Recovery Socks product - + 30.0 20.0 1.0 none - - + + - diff --git a/stock_vertical_lift/demo/stock_inventory_demo.xml b/stock_vertical_lift/demo/stock_inventory_demo.xml index 18b0664201fb..9c9e4a4cd107 100644 --- a/stock_vertical_lift/demo/stock_inventory_demo.xml +++ b/stock_vertical_lift/demo/stock_inventory_demo.xml @@ -1,23 +1,30 @@ - + - Starting Vertical Lift Inventory - - - - + + + 30.0 - + - - + - + - diff --git a/stock_vertical_lift/demo/stock_location_demo.xml b/stock_vertical_lift/demo/stock_location_demo.xml index 9b352e76bc11..be6afec8f250 100644 --- a/stock_vertical_lift/demo/stock_location_demo.xml +++ b/stock_vertical_lift/demo/stock_location_demo.xml @@ -1,6 +1,5 @@ - + - Vertical Lift - + internal - + - Shuttle 1 - + internal - Tray 1A T1A - - + + internal - Tray 1B T1B - - + + internal - Tray 1C T1C - - + + internal - Shuttle 2 - + internal - Tray 2A T2A - - + + internal - Tray 2B T2B - - + + internal - Tray 2C T2C - - + + internal - Tray 2D T2D - - + + internal - Shuttle 3 - + internal - Tray 3A T3A - - + + internal - Tray 3B T3B - - + + internal - - + stock_vertical_lift - diff --git a/stock_vertical_lift/demo/stock_picking_demo.xml b/stock_vertical_lift/demo/stock_picking_demo.xml index c41583590b34..206170bf63f8 100644 --- a/stock_vertical_lift/demo/stock_picking_demo.xml +++ b/stock_vertical_lift/demo/stock_picking_demo.xml @@ -1,14 +1,16 @@ - + - - + Outgoing shipment from Vertical Lift (demo) - - - - - + + + + + })]" + /> - - + - - + - - + Incoming shipment from Vertical Lift (demo) - - - - - + + + + + })]" + /> - - + - diff --git a/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml b/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml index 13cab7bcda1d..c4e8ff582de9 100644 --- a/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml +++ b/stock_vertical_lift/demo/vertical_lift_shuttle_demo.xml @@ -1,22 +1,18 @@ - + - Shuttle 1 - + pick - Shuttle 2 - + pick - Shuttle 3 - + pick - diff --git a/stock_vertical_lift/static/src/js/vertical_lift.js b/stock_vertical_lift/static/src/js/vertical_lift.js index 28853f2b3058..8efae2985593 100644 --- a/stock_vertical_lift/static/src/js/vertical_lift.js +++ b/stock_vertical_lift/static/src/js/vertical_lift.js @@ -1,56 +1,59 @@ -odoo.define('stock_vertical_lift.vertical_lift', function (require) { +odoo.define("stock_vertical_lift.vertical_lift", function(require) { "use strict"; - var KanbanRecord = require('web.KanbanRecord'); - var FormController = require('web.FormController'); + var KanbanRecord = require("web.KanbanRecord"); + var FormController = require("web.FormController"); KanbanRecord.include({ - - _openRecord: function () { - if (this.modelName === 'vertical.lift.shuttle' && - this.$el.hasClass("open_shuttle_screen")) { + _openRecord: function() { + if ( + this.modelName === "vertical.lift.shuttle" && + this.$el.hasClass("open_shuttle_screen") + ) { var self = this; this._rpc({ - method: 'action_open_screen', + method: "action_open_screen", model: self.modelName, args: [self.id], - }).then(function (action) { - self.trigger_up('do_action', {action: action}); + }).then(function(action) { + self.trigger_up("do_action", {action: action}); }); } else { this._super.apply(this, arguments); } }, - }); FormController.include({ - init: function () { + init: function() { this._super.apply(this, arguments); - if (this.modelName.startsWith('vertical.lift.operation.')) { - this.call('bus_service', 'addChannel', 'notify_vertical_lift_screen'); + if (this.modelName.startsWith("vertical.lift.operation.")) { + this.call("bus_service", "addChannel", "notify_vertical_lift_screen"); this.call( - 'bus_service', 'on', 'notification', - this, this.vlift_bus_notification, + "bus_service", + "on", + "notification", + this, + this.vlift_bus_notification ); - this.call('bus_service', 'startPolling'); + this.call("bus_service", "startPolling"); } }, - vlift_bus_notification: function (notifications) { + vlift_bus_notification: function(notifications) { var self = this; - _.each(notifications, function (notification) { + _.each(notifications, function(notification) { var channel = notification[0]; var message = notification[1]; - if (channel === 'notify_vertical_lift_screen') { + if (channel === "notify_vertical_lift_screen") { switch (message.action) { - case 'refresh': - self.vlift_bus_action_refresh(message.params); - break; + case "refresh": + self.vlift_bus_action_refresh(message.params); + break; } } }); }, - vlift_bus_action_refresh: function (params) { + vlift_bus_action_refresh: function(params) { var selectedIds = this.getSelectedIds(); if (!selectedIds.length) { return; @@ -60,17 +63,17 @@ odoo.define('stock_vertical_lift.vertical_lift', function (require) { this.reload(); } }, - destroy: function () { - if (this.modelName.startsWith('vertical.lift.operation.')) { - this.call('bus_service', 'deleteChannel', - 'notify_vertical_lift_screen'); + destroy: function() { + if (this.modelName.startsWith("vertical.lift.operation.")) { + this.call( + "bus_service", + "deleteChannel", + "notify_vertical_lift_screen" + ); } this._super.apply(this, arguments); }, - }); - return {}; - }); diff --git a/stock_vertical_lift/static/src/scss/vertical_lift.scss b/stock_vertical_lift/static/src/scss/vertical_lift.scss index 5421618db826..9ace6b616148 100644 --- a/stock_vertical_lift/static/src/scss/vertical_lift.scss +++ b/stock_vertical_lift/static/src/scss/vertical_lift.scss @@ -27,7 +27,7 @@ .o_shuttle_header_content { display: flex; flex-flow: row nowrap; - font-size: 2.0em; + font-size: 2em; flex: 1 0 auto; align-items: center; width: 33%; @@ -93,7 +93,6 @@ border-radius: 10px; } } - } .o_vlift_shuttle_menu { @@ -106,7 +105,6 @@ } .o_vlift_shuttle_popup { - table tr { line-height: 3; font-size: 1.1em; @@ -127,7 +125,6 @@ font-size: 2em; text-transform: uppercase; } - } footer .btn { diff --git a/stock_vertical_lift/views/shuttle_screen_templates.xml b/stock_vertical_lift/views/shuttle_screen_templates.xml index 5d017220a9f3..a34365026f08 100644 --- a/stock_vertical_lift/views/shuttle_screen_templates.xml +++ b/stock_vertical_lift/views/shuttle_screen_templates.xml @@ -1,17 +1,27 @@ - - diff --git a/stock_vertical_lift/views/stock_location_views.xml b/stock_vertical_lift/views/stock_location_views.xml index c884825ef3bc..b3de33086758 100644 --- a/stock_vertical_lift/views/stock_location_views.xml +++ b/stock_vertical_lift/views/stock_location_views.xml @@ -1,27 +1,31 @@ - + - stock.location.form.vertical.lift stock.location - +
-
- + + - - @@ -31,17 +35,15 @@
- stock.location.search.vertical.lift stock.location - + - - + + -
diff --git a/stock_vertical_lift/views/stock_move_line_views.xml b/stock_vertical_lift/views/stock_move_line_views.xml index 5dab678aa9fc..56b8c535f7bf 100644 --- a/stock_vertical_lift/views/stock_move_line_views.xml +++ b/stock_vertical_lift/views/stock_move_line_views.xml @@ -1,30 +1,35 @@ - + - stock.move.line.operations.tree.vertical.lift stock.move.line - + - diff --git a/stock_vertical_lift/views/stock_vertical_lift_templates.xml b/stock_vertical_lift/views/stock_vertical_lift_templates.xml index 11f1a3cf5e18..b312668890ab 100644 --- a/stock_vertical_lift/views/stock_vertical_lift_templates.xml +++ b/stock_vertical_lift/views/stock_vertical_lift_templates.xml @@ -1,11 +1,20 @@ - + - -