From e3020a256742e6fc003abef94f48a16481b673d1 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 8 Oct 2019 17:07:20 +0200 Subject: [PATCH] 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 e259e71f4..580fc0706 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 2a0e1d40b..80b7355db 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 a8d19e0a0..93e02cf65 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 000000000..a26873c0a --- /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 54ec752d2..fcdb4a3bd 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 @@ class VerticalLiftShuttle(models.Model): ) 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 @@ class VerticalLiftShuttle(models.Model): ) 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 @@ class VerticalLiftShuttle(models.Model): 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 930d7a179..62c6e2996 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 000000000..39249814e --- /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 e99db92f6..51a3830f6 100644 --- a/stock_vertical_lift_kardex/models/__init__.py +++ b/stock_vertical_lift_kardex/models/__init__.py @@ -1 +1,2 @@ +from . import stock_location from . import vertical_lift_shuttle diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py new file mode 100644 index 000000000..d1098d59a --- /dev/null +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -0,0 +1,56 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo import models + +_logger = logging.getLogger(__name__) + + +class StockLocation(models.Model): + _inherit = 'stock.location' + + def _hardware_kardex_prepare_payload(self, cell_location=None): + return "" + + def _hardware_vertical_lift_tray(self, cell_location=None): + """Send instructions to the vertical lift hardware + + Private method, this is where the implementation actually happens. + Addons can add their instructions based on the hardware used for + this location. + + The hardware used for a location can be found in: + + ``self.vertical_lift_shuttle_id.hardware`` + + Each addon can implement its own mechanism depending of this value + and must call ``super``. + + The method must send the command to the vertical lift to fetch / open + the tray. If a ``cell_location`` is passed and if the hardware supports + a way to show a cell (such as a laser pointer), it should send this + command as well. + + Useful information that could be needed for the drivers: + + * Any field of `self` (name, barcode, ...) which is the current tray. + * Any field of `cell_location` (name, barcode, ...) which is the cell + in the tray. + * ``self.vertical_lift_shuttle_id`` is the current Shuttle, where we + find details about the hardware, the current mode (pick, put, ...). + * ``self.tray_type_id`` is the kind of tray. + * ``self.tray_type_id.width_per_cell`` and + ``self.tray_type_id.depth_per_cell`` return the size of a cell in mm. + * ``cell_location.posx`` and ``posy`` are the coordinate from the + bottom-left of the tray. + * ``cell_location.tray_cell_center_position()`` returns the central + position of the cell in mm from the bottom-left of a tray. (distance + from left, distance from bottom). Can be used for instance for + highlighting the cell using a laser pointer. + """ + if self.vertical_lift_shuttle_id.hardware == "kardex": + payload = self._hardware_kardex_prepare_payload() + _logger.debug("Sending to kardex: {}", payload) + # TODO implement the communication with kardex + super()._hardware_vertical_lift_tray(cell_location=cell_location)