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
This commit is contained in:
Guewen Baconnier
2019-10-08 17:07:20 +02:00
parent 311fb24447
commit e3020a2567
9 changed files with 245 additions and 47 deletions

View File

@@ -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',

View File

@@ -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

View File

@@ -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 += _(
"<br/>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

View File

@@ -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"}

View File

@@ -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'

View File

@@ -11,6 +11,8 @@
attrs="{'invisible': [('vertical_lift_kind', '!=', False), ('vertical_lift_kind', '!=', 'view')]}"
/>
<field name="vertical_lift_kind"/>
<field name="vertical_lift_shuttle_id"
attrs="{'invisible': [('vertical_lift_kind', 'not in', ('shuttle', 'tray', 'cell'))]}" />
</field>
<field name="tray_type_id" position="attributes">
<attribute name="attrs">
@@ -28,6 +30,7 @@
<field name="arch" type="xml">
<field name="tray_type_id" position="after">
<field name="vertical_lift_kind"/>
<field name="vertical_lift_shuttle_id"/>
</field>
</field>
</record>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_stock_move_line_operation_tree" model="ir.ui.view">
<field name="name">stock.move.line.operations.tree.tray.type</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock_location_tray.view_stock_move_line_operation_tree" />
<field name="arch" type="xml">
<button name="action_show_source_tray" position="before">
<button name="fetch_vertical_lift_tray_source"
string="Fetch Source Tray"
type="object" icon="fa-hand-paper-o"
attrs="{'invisible': [('tray_source_matrix', '=', {})]}"
invisible="not context.get('show_source_location')"
groups="stock.group_stock_multi_locations"
/>
</button>
<button name="action_show_dest_tray" position="before">
<button name="fetch_vertical_lift_tray_dest"
string="Fetch Destination Tray"
type="object" icon="fa-hand-paper-o"
attrs="{'invisible': [('tray_dest_matrix', '=', {})]}"
invisible="not context.get('show_destination_location')"
groups="stock.group_stock_multi_locations"
/>
</button>
</field>
</record>
</odoo>

View File

@@ -1 +1,2 @@
from . import stock_location
from . import vertical_lift_shuttle

View File

@@ -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)