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. +

+
+
+ + + +