mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
Rework workflows using a small state machine
The documentation of the state machine is in VerticalLiftOperationBase._transitions.
This commit is contained in:
committed by
Hai Lang
parent
d1b7a6f47c
commit
7d78e8b540
@@ -38,7 +38,7 @@ class VerticalLiftCommand(models.Model):
|
|||||||
if key:
|
if key:
|
||||||
return key[0]
|
return key[0]
|
||||||
else:
|
else:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
# Copyright 2019 Camptocamp SA
|
# Copyright 2019 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
import logging
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
from odoo.addons.base_sparse_field.models.fields import Serialized
|
from odoo.addons.base_sparse_field.models.fields import Serialized
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class VerticalLiftOperationBase(models.AbstractModel):
|
class VerticalLiftOperationBase(models.AbstractModel):
|
||||||
"""Base model for shuttle operations (pick, put, inventory)"""
|
"""Base model for shuttle operations (pick, put, inventory)"""
|
||||||
@@ -26,7 +31,18 @@ class VerticalLiftOperationBase(models.AbstractModel):
|
|||||||
string="Number of Operations in all shuttles",
|
string="Number of Operations in all shuttles",
|
||||||
)
|
)
|
||||||
mode = fields.Selection(related="shuttle_id.mode", readonly=True)
|
mode = fields.Selection(related="shuttle_id.mode", readonly=True)
|
||||||
operation_descr = fields.Char(string="Operation", default="...", readonly=True)
|
|
||||||
|
state = fields.Selection(
|
||||||
|
selection=lambda self: self._selection_states(),
|
||||||
|
default=lambda self: self._initial_state,
|
||||||
|
)
|
||||||
|
_initial_state = None # to define in sub-classes
|
||||||
|
|
||||||
|
# if there is an action and it's returning True, the transition is done,
|
||||||
|
# otherwise not
|
||||||
|
Transition = namedtuple("Transition", "current_state next_state action direct_eval")
|
||||||
|
# default values to None
|
||||||
|
Transition.__new__.__defaults__ = (None,) * len(Transition._fields)
|
||||||
|
|
||||||
_sql_constraints = [
|
_sql_constraints = [
|
||||||
(
|
(
|
||||||
@@ -36,6 +52,128 @@ class VerticalLiftOperationBase(models.AbstractModel):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _selection_states(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _transitions(self):
|
||||||
|
"""Define the transitions between the states
|
||||||
|
|
||||||
|
To set in sub-classes.
|
||||||
|
It is a tuple of a ``Transition`` instances, evaluated in order.
|
||||||
|
A transition has a source step, a destination step, a function and a
|
||||||
|
flag ``direct_eval``.
|
||||||
|
When the function returns True, the transition is applied, otherwise,
|
||||||
|
the next transition matching the current step is evaluated.
|
||||||
|
When a transition has no function, it is always applied.
|
||||||
|
The flag ``direct_eval`` indicates that the workflow should directly
|
||||||
|
evaluates again the transitions to reach the next step. It allows to
|
||||||
|
use "virtual" steps that will never be kept for users but be used as
|
||||||
|
router.
|
||||||
|
|
||||||
|
The initial state must be defined in the attribute ``_initial_state``.
|
||||||
|
|
||||||
|
The transition from a step to another are triggered by a call to
|
||||||
|
``next_step()``. This method is called in several places:
|
||||||
|
|
||||||
|
* ``reset_steps()`` (called when the screen opens)
|
||||||
|
* ``button_save()``, generally used to post the move
|
||||||
|
* ``button_release()``, generally used to go to the next line
|
||||||
|
* ``on_barcode_scanned()``, the calls to ``next_step()`` are to
|
||||||
|
implement in sub-classed if the scanned barcode leads to the next
|
||||||
|
step
|
||||||
|
|
||||||
|
Example of workflow described below:
|
||||||
|
|
||||||
|
::
|
||||||
|
_initial_state = "noop"
|
||||||
|
|
||||||
|
def _selection_states(self):
|
||||||
|
return [
|
||||||
|
("noop", "No operations"),
|
||||||
|
("scan_destination", "Scan New Destination Location"),
|
||||||
|
("save", "Put goods in tray and save"),
|
||||||
|
("release", "Release"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _transitions(self):
|
||||||
|
return (
|
||||||
|
self.Transition(
|
||||||
|
"noop",
|
||||||
|
"scan_destination",
|
||||||
|
lambda self: self.select_next_move_line()
|
||||||
|
),
|
||||||
|
self.Transition("scan_destination", "save"),
|
||||||
|
self.Transition("save", "release"),
|
||||||
|
self.Transition(
|
||||||
|
"release",
|
||||||
|
"scan_destination",
|
||||||
|
lambda self: self.select_next_move_line()
|
||||||
|
),
|
||||||
|
self.Transition("release", "noop"),
|
||||||
|
)
|
||||||
|
|
||||||
|
When we arrive on the screen, the ``on_screen_open`` methods resets the
|
||||||
|
steps (``reset_steps()``). It ensures the current step is ``noop`` and
|
||||||
|
directly tries to reach the next step (call to ``next_step()``).
|
||||||
|
|
||||||
|
It tries to go from ``noop`` to ``scan_destination``, calling
|
||||||
|
``self.select_next_move_line()``. If the method finds a line, it
|
||||||
|
returns True and the transition is applied, otherwise, the step stays
|
||||||
|
``noop``.
|
||||||
|
|
||||||
|
The transitions from ``scan_destination`` and ``save`` and from
|
||||||
|
``save`` and ``release`` are always applied when ``next_step()`` is
|
||||||
|
called (``scan_destination`` → ``save`` from ``on_barcode_scanned``
|
||||||
|
when a destination was found, ``save`` → ``release`` from the save
|
||||||
|
button).
|
||||||
|
|
||||||
|
When ``button_release()`` is called, it calls ``next_step()`` which
|
||||||
|
first evaluates ``self.select_next_move_line()``: if a move line remains, it
|
||||||
|
goes to ``scan_destination``, otherwise to ``noop``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return ()
|
||||||
|
|
||||||
|
def step(self):
|
||||||
|
return self.state
|
||||||
|
|
||||||
|
def next_step(self, direct_eval=False):
|
||||||
|
current_state = self.state
|
||||||
|
for transition in self._transitions():
|
||||||
|
if direct_eval and not transition.direct_eval:
|
||||||
|
continue
|
||||||
|
if transition.current_state != current_state:
|
||||||
|
continue
|
||||||
|
if not transition.action or transition.action(self):
|
||||||
|
_logger.debug(
|
||||||
|
"Transition %s → %s",
|
||||||
|
transition.current_state,
|
||||||
|
transition.next_state,
|
||||||
|
)
|
||||||
|
self.state = transition.next_state
|
||||||
|
break
|
||||||
|
# reevaluate the transitions if we have a new state with direct_eval transitions
|
||||||
|
if self.state != current_state and any(
|
||||||
|
transition.direct_eval
|
||||||
|
for transition in self._transitions()
|
||||||
|
if transition.current_state == self.state
|
||||||
|
):
|
||||||
|
self.next_step(direct_eval=True)
|
||||||
|
|
||||||
|
def reset_steps(self):
|
||||||
|
if not self._initial_state:
|
||||||
|
raise NotImplementedError("_initial_state must be defined")
|
||||||
|
self.state = self._initial_state
|
||||||
|
self.next_step()
|
||||||
|
|
||||||
|
def on_barcode_scanned(self, barcode):
|
||||||
|
self.ensure_one()
|
||||||
|
# to implement in sub-classes
|
||||||
|
|
||||||
|
def on_screen_open(self):
|
||||||
|
"""Called when the screen is opened"""
|
||||||
|
self.reset_steps()
|
||||||
|
|
||||||
def onchange(self, values, field_name, field_onchange):
|
def onchange(self, values, field_name, field_onchange):
|
||||||
if field_name == "_barcode_scanned":
|
if field_name == "_barcode_scanned":
|
||||||
# _barcode_scanner is implemented (in the barcodes module) as an
|
# _barcode_scanner is implemented (in the barcodes module) as an
|
||||||
@@ -43,7 +181,7 @@ class VerticalLiftOperationBase(models.AbstractModel):
|
|||||||
# normal button and actually have side effect in the database
|
# normal button and actually have side effect in the database
|
||||||
# (update line, go to the next step, ...). This override shorts the
|
# (update line, go to the next step, ...). This override shorts the
|
||||||
# onchange call and calls the scanner method as a normal method.
|
# onchange call and calls the scanner method as a normal method.
|
||||||
self.on_barcode_scanned(values['_barcode_scanned'])
|
self.on_barcode_scanned(values["_barcode_scanned"])
|
||||||
# We can't know which fields on_barcode_scanned changed, refresh
|
# We can't know which fields on_barcode_scanned changed, refresh
|
||||||
# everything.
|
# everything.
|
||||||
return {"value": self.read()[0]}
|
return {"value": self.read()[0]}
|
||||||
@@ -60,16 +198,6 @@ class VerticalLiftOperationBase(models.AbstractModel):
|
|||||||
for record in self:
|
for record in self:
|
||||||
record.number_of_ops_all = 0
|
record.number_of_ops_all = 0
|
||||||
|
|
||||||
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):
|
def action_open_screen(self):
|
||||||
return self.shuttle_id.action_open_screen()
|
return self.shuttle_id.action_open_screen()
|
||||||
|
|
||||||
@@ -79,13 +207,26 @@ class VerticalLiftOperationBase(models.AbstractModel):
|
|||||||
def action_manual_barcode(self):
|
def action_manual_barcode(self):
|
||||||
return self.shuttle_id.action_manual_barcode()
|
return self.shuttle_id.action_manual_barcode()
|
||||||
|
|
||||||
def button_save(self):
|
def process_current(self):
|
||||||
"""Process the action (pick, put, ...)"""
|
"""Process the action (pick, put, ...)
|
||||||
|
|
||||||
|
To implement in sub-classes
|
||||||
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def button_save(self):
|
||||||
|
"""Confirm the operation (set move to done, ...)"""
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.step() == "save":
|
||||||
|
return
|
||||||
|
self.next_step()
|
||||||
|
|
||||||
def button_release(self):
|
def button_release(self):
|
||||||
"""Release the operation, go to the next"""
|
"""Release the operation, go to the next"""
|
||||||
raise NotImplementedError
|
self.ensure_one()
|
||||||
|
if not self.step() == "release":
|
||||||
|
return
|
||||||
|
self.next_step()
|
||||||
|
|
||||||
def _render_product_packagings(self, product):
|
def _render_product_packagings(self, product):
|
||||||
values = {
|
values = {
|
||||||
@@ -254,32 +395,20 @@ class VerticalLiftOperationTransfer(models.AbstractModel):
|
|||||||
self._domain_move_lines_to_do_all()
|
self._domain_move_lines_to_do_all()
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_screen_open(self):
|
|
||||||
"""Called when the screen is open"""
|
|
||||||
|
|
||||||
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):
|
def process_current(self):
|
||||||
raise NotImplementedError
|
line = self.current_move_line_id
|
||||||
|
if line.state in ("assigned", "partially_available"):
|
||||||
def button_save(self):
|
line.qty_done = line.product_qty
|
||||||
if not (self and self.current_move_line_id):
|
line.move_id._action_done()
|
||||||
return
|
return True
|
||||||
self.ensure_one()
|
|
||||||
self.process_current()
|
|
||||||
self.operation_descr = _("Release")
|
|
||||||
|
|
||||||
def fetch_tray(self):
|
def fetch_tray(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def reset_steps(self):
|
||||||
|
self.clear_current_move_line()
|
||||||
|
super().reset_steps()
|
||||||
|
|
||||||
|
def clear_current_move_line(self):
|
||||||
|
self.current_move_line_id = False
|
||||||
|
return True
|
||||||
|
|||||||
@@ -14,6 +14,68 @@ class VerticalLiftOperationInventory(models.Model):
|
|||||||
_inherit = "vertical.lift.operation.base"
|
_inherit = "vertical.lift.operation.base"
|
||||||
_description = "Vertical Lift Operation Inventory"
|
_description = "Vertical Lift Operation Inventory"
|
||||||
|
|
||||||
|
_initial_state = "noop"
|
||||||
|
|
||||||
|
def _selection_states(self):
|
||||||
|
return [
|
||||||
|
("noop", "No inventory in progress"),
|
||||||
|
("quantity", "Inventory, please enter the amount"),
|
||||||
|
("confirm_wrong_quantity", "The quantity does not match, are you sure?"),
|
||||||
|
# save is never visible, but save and go to the next or noop directly
|
||||||
|
("save", "Save"),
|
||||||
|
# no need for release and save button here?
|
||||||
|
# ("release", "Release"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _transitions(self):
|
||||||
|
return (
|
||||||
|
self.Transition(
|
||||||
|
"noop",
|
||||||
|
"quantity",
|
||||||
|
# transition only if inventory lines are found
|
||||||
|
lambda self: self.select_next_inventory_line(),
|
||||||
|
),
|
||||||
|
self.Transition(
|
||||||
|
"quantity", "save", lambda self: self._has_identical_quantity(),
|
||||||
|
),
|
||||||
|
self.Transition(
|
||||||
|
"quantity",
|
||||||
|
"confirm_wrong_quantity",
|
||||||
|
lambda self: self._start_confirm_wrong_quantity(),
|
||||||
|
),
|
||||||
|
self.Transition(
|
||||||
|
"confirm_wrong_quantity",
|
||||||
|
"save",
|
||||||
|
lambda self: self.quantity_input == self.last_quantity_input,
|
||||||
|
),
|
||||||
|
# if the confirmation of the quantity is different, cycle back to
|
||||||
|
# the 'quantity' step
|
||||||
|
self.Transition(
|
||||||
|
"confirm_wrong_quantity",
|
||||||
|
"quantity",
|
||||||
|
lambda self: self._go_back_to_quantity_input(),
|
||||||
|
),
|
||||||
|
# go to quantity if we have lines in queue, otherwise, go to noop
|
||||||
|
self.Transition(
|
||||||
|
"save",
|
||||||
|
"quantity",
|
||||||
|
lambda self: self.process_current()
|
||||||
|
and self.select_next_inventory_line(),
|
||||||
|
# when we reach 'save', this transition is directly
|
||||||
|
# evaluated
|
||||||
|
direct_eval=True,
|
||||||
|
),
|
||||||
|
self.Transition(
|
||||||
|
"save",
|
||||||
|
"noop",
|
||||||
|
lambda self: self.process_current()
|
||||||
|
and self.clear_current_inventory_line(),
|
||||||
|
# when we reach 'save', this transition is directly
|
||||||
|
# evaluated
|
||||||
|
direct_eval=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
current_inventory_line_id = fields.Many2one(
|
current_inventory_line_id = fields.Many2one(
|
||||||
comodel_name="stock.inventory.line", readonly=True
|
comodel_name="stock.inventory.line", readonly=True
|
||||||
)
|
)
|
||||||
@@ -22,14 +84,6 @@ class VerticalLiftOperationInventory(models.Model):
|
|||||||
# if the quantity is wrong, user has to write 2 times
|
# if the quantity is wrong, user has to write 2 times
|
||||||
# the same quantity to really confirm it's correct
|
# the same quantity to really confirm it's correct
|
||||||
last_quantity_input = fields.Float()
|
last_quantity_input = fields.Float()
|
||||||
state = fields.Selection(
|
|
||||||
selection=[
|
|
||||||
("quantity", "Inventory, please enter the amount"),
|
|
||||||
("confirm_wrong_quantity", "The quantity does not match, are you sure?"),
|
|
||||||
("save", "Save"),
|
|
||||||
],
|
|
||||||
default="quantity",
|
|
||||||
)
|
|
||||||
|
|
||||||
tray_location_id = fields.Many2one(
|
tray_location_id = fields.Many2one(
|
||||||
comodel_name="stock.location",
|
comodel_name="stock.location",
|
||||||
@@ -137,54 +191,9 @@ class VerticalLiftOperationInventory(models.Model):
|
|||||||
("vertical_lift_done", "=", False),
|
("vertical_lift_done", "=", False),
|
||||||
]
|
]
|
||||||
|
|
||||||
def on_screen_open(self):
|
def reset_steps(self):
|
||||||
"""Called when the screen is open"""
|
self.clear_current_inventory_line()
|
||||||
self.select_next_inventory_line()
|
super().reset_steps()
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.write(
|
|
||||||
{"quantity_input": 0.0, "last_quantity_input": 0.0, "state": "quantity"}
|
|
||||||
)
|
|
||||||
self.update_step_description()
|
|
||||||
|
|
||||||
def step(self):
|
|
||||||
return self.state
|
|
||||||
|
|
||||||
def step_to(self, state):
|
|
||||||
self.state = state
|
|
||||||
self.update_step_description()
|
|
||||||
|
|
||||||
def step_description(self):
|
|
||||||
state_field = self._fields["state"]
|
|
||||||
return state_field.convert_to_export(self.state, self)
|
|
||||||
|
|
||||||
def update_step_description(self):
|
|
||||||
if self.current_inventory_line_id:
|
|
||||||
descr = self.step_description()
|
|
||||||
else:
|
|
||||||
descr = _("No operations")
|
|
||||||
self.operation_descr = descr
|
|
||||||
|
|
||||||
def button_save(self):
|
|
||||||
if not self.current_inventory_line_id:
|
|
||||||
return
|
|
||||||
self.ensure_one()
|
|
||||||
self.process_current()
|
|
||||||
if self.step() == "save":
|
|
||||||
self.select_next_inventory_line()
|
|
||||||
if not self.current_inventory_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 button_release(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _has_identical_quantity(self):
|
def _has_identical_quantity(self):
|
||||||
line = self.current_inventory_line_id
|
line = self.current_inventory_line_id
|
||||||
@@ -197,35 +206,25 @@ class VerticalLiftOperationInventory(models.Model):
|
|||||||
== 0
|
== 0
|
||||||
)
|
)
|
||||||
|
|
||||||
def _process_quantity(self):
|
def _start_confirm_wrong_quantity(self):
|
||||||
if self.step() == "quantity":
|
self.last_quantity_input = self.quantity_input
|
||||||
if self._has_identical_quantity():
|
self.quantity_input = 0.0
|
||||||
self.step_to("save")
|
return True
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.last_quantity_input = self.quantity_input
|
|
||||||
self.quantity_input = 0.0
|
|
||||||
self.step_to("confirm_wrong_quantity")
|
|
||||||
return False
|
|
||||||
if self.step() == "confirm_wrong_quantity":
|
|
||||||
if self.quantity_input == self.last_quantity_input:
|
|
||||||
# confirms the previous input
|
|
||||||
self.step_to("save")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
# cycle back to the first quantity check
|
|
||||||
self.step_to("quantity")
|
|
||||||
return self._process_quantity()
|
|
||||||
|
|
||||||
def process_current(self):
|
def _go_back_to_quantity_input(self):
|
||||||
line = self.current_inventory_line_id
|
self.last_quantity_input = self.quantity_input
|
||||||
if self._process_quantity() and not line.vertical_lift_done:
|
self.quantity_input = 0.0
|
||||||
line.vertical_lift_done = True
|
return True
|
||||||
if self.quantity_input != line.product_qty:
|
|
||||||
line.product_qty = self.quantity_input
|
def clear_current_inventory_line(self):
|
||||||
inventory = line.inventory_id
|
self.write(
|
||||||
if all(line.vertical_lift_done for line in inventory.line_ids):
|
{
|
||||||
inventory.action_validate()
|
"quantity_input": 0.0,
|
||||||
|
"last_quantity_input": 0.0,
|
||||||
|
"current_inventory_line_id": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
def fetch_tray(self):
|
def fetch_tray(self):
|
||||||
location = self.current_inventory_line_id.location_id
|
location = self.current_inventory_line_id.location_id
|
||||||
@@ -233,16 +232,44 @@ class VerticalLiftOperationInventory(models.Model):
|
|||||||
|
|
||||||
def select_next_inventory_line(self):
|
def select_next_inventory_line(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
previous_line = self.current_inventory_line_id
|
||||||
next_line = self.env["stock.inventory.line"].search(
|
next_line = self.env["stock.inventory.line"].search(
|
||||||
self._domain_inventory_lines_to_do(),
|
self._domain_inventory_lines_to_do(),
|
||||||
limit=1,
|
limit=1,
|
||||||
order="vertical_lift_tray_id, location_id, id",
|
order="vertical_lift_tray_id, location_id, id",
|
||||||
)
|
)
|
||||||
previous_line = self.current_inventory_line_id
|
|
||||||
self.current_inventory_line_id = next_line
|
self.current_inventory_line_id = next_line
|
||||||
self.reset()
|
|
||||||
if (
|
if (
|
||||||
next_line
|
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()
|
self.fetch_tray()
|
||||||
|
return bool(next_line)
|
||||||
|
|
||||||
|
def process_current(self):
|
||||||
|
line = self.current_inventory_line_id
|
||||||
|
if not line.vertical_lift_done:
|
||||||
|
line.vertical_lift_done = True
|
||||||
|
if self.quantity_input != line.product_qty:
|
||||||
|
line.product_qty = self.quantity_input
|
||||||
|
inventory = line.inventory_id
|
||||||
|
if all(line.vertical_lift_done for line in inventory.line_ids):
|
||||||
|
inventory.action_validate()
|
||||||
|
self.quantity_input = self.last_quantity_input = 0.0
|
||||||
|
return True
|
||||||
|
|
||||||
|
def button_save(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.step() in ("quantity", "confirm_wrong_quantity"):
|
||||||
|
return
|
||||||
|
self.next_step()
|
||||||
|
if self.step() == "noop":
|
||||||
|
# sorry not sorry
|
||||||
|
return {
|
||||||
|
"effect": {
|
||||||
|
"fadeout": "slow",
|
||||||
|
"message": _("Congrats, you cleared the queue!"),
|
||||||
|
"img_url": "/web/static/src/img/smile.svg",
|
||||||
|
"type": "rainbow_man",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,18 +9,43 @@ class VerticalLiftOperationPick(models.Model):
|
|||||||
_inherit = "vertical.lift.operation.transfer"
|
_inherit = "vertical.lift.operation.transfer"
|
||||||
_description = "Vertical Lift Operation Pick"
|
_description = "Vertical Lift Operation Pick"
|
||||||
|
|
||||||
|
_initial_state = "noop"
|
||||||
|
|
||||||
|
def _selection_states(self):
|
||||||
|
return [
|
||||||
|
("noop", "No operations"),
|
||||||
|
("scan_destination", "Scan New Destination Location"),
|
||||||
|
("save", "Pick goods and save"),
|
||||||
|
("release", "Release"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _transitions(self):
|
||||||
|
return (
|
||||||
|
self.Transition(
|
||||||
|
"noop", "scan_destination", lambda self: self.select_next_move_line()
|
||||||
|
),
|
||||||
|
self.Transition("scan_destination", "save"),
|
||||||
|
self.Transition("save", "release", lambda self: self.process_current()),
|
||||||
|
# go to scan_destination if we have lines in queue, otherwise, go to noop
|
||||||
|
self.Transition(
|
||||||
|
"release", "scan_destination", lambda self: self.select_next_move_line()
|
||||||
|
),
|
||||||
|
self.Transition("release", "noop"),
|
||||||
|
)
|
||||||
|
|
||||||
def on_barcode_scanned(self, barcode):
|
def on_barcode_scanned(self, barcode):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
if not self.current_move_line_id or self.current_move_line_id.state == "done":
|
if not self.current_move_line_id or self.current_move_line_id.state == "done":
|
||||||
return
|
return
|
||||||
location = self.env["stock.location"].search([("barcode", "=", barcode)])
|
if self.step() == "scan_destination":
|
||||||
if location:
|
location = self.env["stock.location"].search([("barcode", "=", barcode)])
|
||||||
self.location_dest_id = location
|
if location:
|
||||||
self.operation_descr = _("Save")
|
self.location_dest_id = location
|
||||||
else:
|
self.next_step()
|
||||||
self.env.user.notify_warning(
|
else:
|
||||||
_("No location found for barcode {}").format(barcode)
|
self.env.user.notify_warning(
|
||||||
)
|
_("No location found for barcode {}").format(barcode)
|
||||||
|
)
|
||||||
|
|
||||||
def _domain_move_lines_to_do(self):
|
def _domain_move_lines_to_do(self):
|
||||||
domain = [
|
domain = [
|
||||||
@@ -42,27 +67,27 @@ class VerticalLiftOperationPick(models.Model):
|
|||||||
def fetch_tray(self):
|
def fetch_tray(self):
|
||||||
self.current_move_line_id.fetch_vertical_lift_tray_source()
|
self.current_move_line_id.fetch_vertical_lift_tray_source()
|
||||||
|
|
||||||
def process_current(self):
|
|
||||||
line = self.current_move_line_id
|
|
||||||
if line.state in ("assigned", "partially_available"):
|
|
||||||
line.qty_done = line.product_qty
|
|
||||||
line.move_id._action_done()
|
|
||||||
|
|
||||||
def on_screen_open(self):
|
|
||||||
"""Called when the screen is open"""
|
|
||||||
self.select_next_move_line()
|
|
||||||
|
|
||||||
def select_next_move_line(self):
|
def select_next_move_line(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
next_move_line = self.env["stock.move.line"].search(
|
next_move_line = self.env["stock.move.line"].search(
|
||||||
self._domain_move_lines_to_do(), limit=1
|
self._domain_move_lines_to_do(), limit=1
|
||||||
)
|
)
|
||||||
self.current_move_line_id = next_move_line
|
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:
|
if next_move_line:
|
||||||
self.fetch_tray()
|
self.fetch_tray()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def button_release(self):
|
||||||
|
"""Release the operation, go to the next"""
|
||||||
|
super().button_release()
|
||||||
|
if self.step() == "noop":
|
||||||
|
# sorry not sorry
|
||||||
|
return {
|
||||||
|
"effect": {
|
||||||
|
"fadeout": "slow",
|
||||||
|
"message": _("Congrats, you cleared the queue!"),
|
||||||
|
"img_url": "/web/static/src/img/smile.svg",
|
||||||
|
"type": "rainbow_man",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo import _, fields, models
|
from odoo import _, fields, models
|
||||||
from odoo.osv import expression
|
from odoo.osv.expression import AND
|
||||||
|
|
||||||
|
|
||||||
class VerticalLiftOperationPut(models.Model):
|
class VerticalLiftOperationPut(models.Model):
|
||||||
@@ -10,210 +10,153 @@ class VerticalLiftOperationPut(models.Model):
|
|||||||
_inherit = "vertical.lift.operation.transfer"
|
_inherit = "vertical.lift.operation.transfer"
|
||||||
_description = "Vertical Lift Operation Put"
|
_description = "Vertical Lift Operation Put"
|
||||||
|
|
||||||
operation_line_ids = fields.One2many(
|
_initial_state = "scan_source"
|
||||||
comodel_name="vertical.lift.operation.put.line",
|
|
||||||
inverse_name="operation_id",
|
|
||||||
readonly=True,
|
|
||||||
)
|
|
||||||
current_operation_line_id = fields.Many2one(
|
|
||||||
comodel_name="vertical.lift.operation.put.line", readonly=True
|
|
||||||
)
|
|
||||||
current_move_line_id = fields.Many2one(
|
|
||||||
related="current_operation_line_id.move_line_id", readonly=True
|
|
||||||
)
|
|
||||||
# TODO think about moving the "steps" to the base model,
|
|
||||||
# integrate 'save' and 'release' in 'next_step()', use states
|
|
||||||
# in 'pick' as well
|
|
||||||
state = fields.Selection(
|
|
||||||
selection=[
|
|
||||||
("scan_product", "Scan Product"),
|
|
||||||
("scan_tray_type", "Scan Tray Type"),
|
|
||||||
("save", "Save"),
|
|
||||||
("release", "Release"),
|
|
||||||
],
|
|
||||||
default="scan_product",
|
|
||||||
)
|
|
||||||
|
|
||||||
next_operation = object()
|
def _selection_states(self):
|
||||||
|
return [
|
||||||
|
("scan_source", "Scan a package, product or lot to put-away"),
|
||||||
|
("scan_tray_type", "Scan Tray Type"),
|
||||||
|
("save", "Put goods in tray and save"),
|
||||||
|
("release", "Release"),
|
||||||
|
]
|
||||||
|
|
||||||
def _transitions(self):
|
def _transitions(self):
|
||||||
return {
|
return (
|
||||||
"scan_product": "scan_tray_type",
|
self.Transition(
|
||||||
"scan_tray_type": "save",
|
"scan_source",
|
||||||
"save": "release",
|
"scan_tray_type",
|
||||||
"release": self.next_operation,
|
# transition only if a move line has been selected
|
||||||
}
|
# (by on_barcode_scanner)
|
||||||
|
lambda self: self.current_move_line_id,
|
||||||
# The steps cannot be in 'vertical.lift.operation.put.line' because the
|
),
|
||||||
# state has to be modified by on_barcode_scanned. As this method is an
|
self.Transition("scan_tray_type", "save"),
|
||||||
# onchange underneath, it has to be on the same model.
|
self.Transition("save", "release", lambda self: self.process_current()),
|
||||||
def step(self):
|
self.Transition(
|
||||||
return self.state
|
"release", "scan_source", lambda self: self.clear_current_move_line()
|
||||||
|
),
|
||||||
def next_step(self):
|
|
||||||
next_state = self._transitions().get(self.state)
|
|
||||||
if next_state is not self.next_operation:
|
|
||||||
self.state = next_state
|
|
||||||
self.update_step_description()
|
|
||||||
|
|
||||||
def step_description(self):
|
|
||||||
state_field = self._fields["state"]
|
|
||||||
return state_field.convert_to_export(self.state, self)
|
|
||||||
|
|
||||||
def reset_steps(self):
|
|
||||||
self.state = "scan_product"
|
|
||||||
self.update_step_description()
|
|
||||||
|
|
||||||
def count_move_lines_to_do(self):
|
|
||||||
"""Count move lines to process in current shuttle"""
|
|
||||||
self.ensure_one()
|
|
||||||
return self.env["vertical.lift.operation.put.line"].search_count(
|
|
||||||
[("operation_id", "=", self.id)]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def count_move_lines_to_do_all(self):
|
def _domain_move_lines_to_do(self):
|
||||||
"""Count move lines to process in all shuttles"""
|
domain = [
|
||||||
self.ensure_one()
|
("state", "in", ("assigned", "partially_available")),
|
||||||
return self.env["vertical.lift.operation.put.line"].search_count([])
|
("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 = [
|
||||||
|
("state", "in", ("assigned", "partially_available")),
|
||||||
|
("location_dest_id", "child_of", shuttle_locations.ids),
|
||||||
|
]
|
||||||
|
return domain
|
||||||
|
|
||||||
def on_barcode_scanned(self, barcode):
|
def on_barcode_scanned(self, barcode):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
operation_line = self.current_operation_line_id
|
if self.step() == "scan_source":
|
||||||
if operation_line:
|
line = self._find_move_line(barcode)
|
||||||
if self.step() == "scan_product":
|
if line:
|
||||||
if self._check_product(barcode):
|
self.current_move_line_id = line
|
||||||
self.next_step()
|
self.next_step()
|
||||||
if self.step() == "scan_tray_type":
|
else:
|
||||||
if self._check_tray_type(barcode):
|
self.env.user.notify_warning(
|
||||||
self.next_step()
|
_("No move line found for barcode {}").format(barcode)
|
||||||
|
)
|
||||||
|
|
||||||
def _check_product(self, barcode):
|
# TODO if the move line already has a storage type, assign directly a
|
||||||
return barcode == self.current_move_line_id.product_id.barcode
|
# destination and save
|
||||||
|
|
||||||
|
elif self.step() == "scan_tray_type":
|
||||||
|
tray_type = self._find_tray_type(barcode)
|
||||||
|
if tray_type:
|
||||||
|
if self._assign_available_cell(tray_type):
|
||||||
|
self.fetch_tray()
|
||||||
|
self.next_step()
|
||||||
|
else:
|
||||||
|
self.env.user.notify_warning(
|
||||||
|
_("No free space for this tray type in this shuttle.")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.env.user.notify_warning(
|
||||||
|
_("No tray type found for barcode {}").format(barcode)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _find_tray_type(self, barcode):
|
||||||
|
return self.env["stock.location.tray.type"].search(
|
||||||
|
[("code", "=", barcode)], limit=1
|
||||||
|
)
|
||||||
|
|
||||||
|
def _find_move_line(self, barcode):
|
||||||
|
package = self.env["stock.quant.package"].search([("name", "=", barcode)])
|
||||||
|
if package:
|
||||||
|
return self._find_move_line_for_package(package)
|
||||||
|
|
||||||
|
lot = self.env["stock.production.lot"].search([("name", "=", barcode)])
|
||||||
|
if lot:
|
||||||
|
return self._find_move_line_for_lot(package)
|
||||||
|
|
||||||
|
product = self.env["product.product"].search([("barcode", "=", barcode)])
|
||||||
|
if not product:
|
||||||
|
packaging = self.env["product.packaging"].search(
|
||||||
|
[("product_id", "!=", False), ("barcode", "=", barcode)]
|
||||||
|
)
|
||||||
|
product = packaging.product_id
|
||||||
|
if product:
|
||||||
|
return self._find_move_line_for_product(product)
|
||||||
|
|
||||||
|
def _find_move_line_for_package(self, package):
|
||||||
|
domain = AND(
|
||||||
|
[self._domain_move_lines_to_do_all(), [("package_id", "=", package.id)]]
|
||||||
|
)
|
||||||
|
return self.env["stock.move.line"].search(domain, limit=1)
|
||||||
|
|
||||||
|
def _find_move_line_for_lot(self, lot):
|
||||||
|
domain = AND(
|
||||||
|
[
|
||||||
|
self._domain_move_lines_to_do_all(),
|
||||||
|
[
|
||||||
|
("lot_id", "=", lot.id),
|
||||||
|
# if the lot is in a package, the package must be scanned
|
||||||
|
("package_id", "=", False),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return self.env["stock.move.line"].search(domain, limit=1)
|
||||||
|
|
||||||
|
def _find_move_line_for_product(self, product):
|
||||||
|
domain = AND(
|
||||||
|
[
|
||||||
|
self._domain_move_lines_to_do_all(),
|
||||||
|
[
|
||||||
|
("product_id", "=", product.id),
|
||||||
|
# if the lot is in a package, the package must be scanned
|
||||||
|
("package_id", "=", False),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return self.env["stock.move.line"].search(domain, limit=1)
|
||||||
|
|
||||||
def _check_tray_type(self, barcode):
|
def _check_tray_type(self, barcode):
|
||||||
location = self.current_move_line_id.location_dest_id
|
location = self.current_move_line_id.location_dest_id
|
||||||
tray_type = location.cell_in_tray_type_id
|
tray_type = location.cell_in_tray_type_id
|
||||||
return barcode == tray_type.code
|
return barcode == tray_type.code
|
||||||
|
|
||||||
def update_step_description(self):
|
def _assign_available_cell(self, tray_type):
|
||||||
if self.current_operation_line_id:
|
locations = self.env["stock.location"].search(
|
||||||
descr = self.step_description()
|
[
|
||||||
else:
|
("id", "child_of", self.location_id.id),
|
||||||
descr = _("No operations")
|
("cell_in_tray_type_id", "=", tray_type.id),
|
||||||
self.operation_descr = descr
|
]
|
||||||
|
)
|
||||||
|
location = fields.first(
|
||||||
|
locations.filtered(lambda loc: not loc.tray_cell_contains_stock)
|
||||||
|
)
|
||||||
|
if location:
|
||||||
|
self.current_move_line_id.location_dest_id = location
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def fetch_tray(self):
|
def fetch_tray(self):
|
||||||
self.current_move_line_id.fetch_vertical_lift_tray_dest()
|
self.current_move_line_id.fetch_vertical_lift_tray_dest()
|
||||||
|
|
||||||
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)]})
|
|
||||||
return super().button_release()
|
|
||||||
|
|
||||||
def button_save(self):
|
|
||||||
if not (self and self.current_operation_line_id):
|
|
||||||
return
|
|
||||||
self.ensure_one()
|
|
||||||
self.process_current()
|
|
||||||
self.next_step()
|
|
||||||
|
|
||||||
def on_screen_open(self):
|
|
||||||
"""Called when the screen is open"""
|
|
||||||
if self.operation_line_ids:
|
|
||||||
self.select_next_move_line()
|
|
||||||
else:
|
|
||||||
return self.action_select_operations()
|
|
||||||
|
|
||||||
def select_next_move_line(self):
|
|
||||||
self.ensure_one()
|
|
||||||
next_operation = fields.first(self.operation_line_ids)
|
|
||||||
self.current_operation_line_id = next_operation
|
|
||||||
self.reset_steps()
|
|
||||||
if next_operation:
|
|
||||||
self.fetch_tray()
|
|
||||||
|
|
||||||
def action_select_operations(self):
|
|
||||||
self.ensure_one()
|
|
||||||
menu_xmlid = "stock_vertical_lift." "vertical_lift_operation_put_select_view"
|
|
||||||
select_model = self.env["vertical.lift.operation.put.select"]
|
|
||||||
select = select_model.create(
|
|
||||||
{
|
|
||||||
"operation_id": self.id,
|
|
||||||
"move_line_ids": [
|
|
||||||
(6, 0, self.mapped("operation_line_ids.move_line_id.id"))
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"type": "ir.actions.act_window",
|
|
||||||
"res_model": "vertical.lift.operation.put.select",
|
|
||||||
"views": [[self.env.ref(menu_xmlid).id, "form"]],
|
|
||||||
"name": _("Scan Operations"),
|
|
||||||
"target": "new",
|
|
||||||
"res_id": select.id,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class VerticalLiftOperationPutLine(models.Model):
|
|
||||||
_name = "vertical.lift.operation.put.line"
|
|
||||||
_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)
|
|
||||||
|
|
||||||
def process(self):
|
|
||||||
line = self.move_line_id
|
|
||||||
if line.state != "done":
|
|
||||||
line.qty_done = line.product_qty
|
|
||||||
line.move_id._action_done()
|
|
||||||
|
|
||||||
|
|
||||||
class VerticalLiftOperationPutSelect(models.TransientModel):
|
|
||||||
_name = "vertical.lift.operation.put.select"
|
|
||||||
_inherit = "barcodes.barcode_events_mixin"
|
|
||||||
_description = "Vertical Lift Operation Put Select"
|
|
||||||
|
|
||||||
operation_id = fields.Many2one(
|
|
||||||
comodel_name="vertical.lift.operation.put", required=True, readonly=True
|
|
||||||
)
|
|
||||||
move_line_ids = fields.Many2many(comodel_name="stock.move.line")
|
|
||||||
|
|
||||||
def _sync_lines(self):
|
|
||||||
self.operation_id.operation_line_ids.unlink()
|
|
||||||
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}
|
|
||||||
for move_line in self.move_line_ids
|
|
||||||
]
|
|
||||||
)
|
|
||||||
self.operation_id.select_next_move_line()
|
|
||||||
|
|
||||||
def action_validate(self):
|
|
||||||
self._sync_lines()
|
|
||||||
return {"type": "ir.actions.act_window_close"}
|
|
||||||
|
|
||||||
def _move_line_domain(self):
|
|
||||||
return [
|
|
||||||
("state", "=", "assigned"),
|
|
||||||
("location_dest_id", "child_of", self.operation_id.location_id.id),
|
|
||||||
]
|
|
||||||
|
|
||||||
def action_add_all(self):
|
|
||||||
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"}
|
|
||||||
|
|
||||||
def on_barcode_scanned(self, barcode):
|
|
||||||
self.ensure_one()
|
|
||||||
domain = self._move_line_domain()
|
|
||||||
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.
|
|
||||||
self.move_line_ids |= move_lines
|
|
||||||
|
|||||||
@@ -37,3 +37,12 @@ in Odoo for each physical shuttle. Depending of the subsidiary addons installed
|
|||||||
(eg. Kardex), different options may be required (host address, ...). The base
|
(eg. Kardex), different options may be required (host address, ...). The base
|
||||||
addon only includes shuttles of kind "simulation" which will not send orders to
|
addon only includes shuttles of kind "simulation" which will not send orders to
|
||||||
the hardware.
|
the hardware.
|
||||||
|
|
||||||
|
Put-away configuration
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If you want to use put-away in the vertical lift, the Receipts must have the
|
||||||
|
vertical lift view as destination. E.g. create put-away rules on the products
|
||||||
|
so when they arrive in WH/Stock, they are stored in WH/Stock/Vertical Lift. On
|
||||||
|
the put-away screen, when scanning the tray type to store, the destination will
|
||||||
|
be updated with an available cell of the same tray type in the current shuttle.
|
||||||
|
|||||||
@@ -3,6 +3,5 @@ access_vertical_lift_shuttle_stock_user,access_vertical_lift_shuttle stock user,
|
|||||||
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_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_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_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_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
|
access_vertical_lift_command,vertical_lift_command,model_vertical_lift_command,base.group_user,1,0,0,0
|
||||||
|
|||||||
|
@@ -18,6 +18,8 @@ class VerticalLiftCase(common.LocationTrayTypeCase):
|
|||||||
cls.vertical_lift_loc = cls.env.ref(
|
cls.vertical_lift_loc = cls.env.ref(
|
||||||
"stock_vertical_lift.stock_location_vertical_lift"
|
"stock_vertical_lift.stock_location_vertical_lift"
|
||||||
)
|
)
|
||||||
|
cls.stock_location = cls.env.ref("stock.stock_location_stock")
|
||||||
|
cls.customers_location = cls.env.ref("stock.stock_location_customers")
|
||||||
cls.location_1a = cls.env.ref(
|
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"
|
||||||
)
|
)
|
||||||
@@ -49,6 +51,12 @@ class VerticalLiftCase(common.LocationTrayTypeCase):
|
|||||||
def _update_qty_in_location(self, location, product, quantity):
|
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)
|
||||||
|
|
||||||
|
def _open_screen(self, mode, shuttle=None):
|
||||||
|
getattr(shuttle or self.shuttle, "switch_{}".format(mode))()
|
||||||
|
# opening the screen can do some initialization for the steps
|
||||||
|
action = (shuttle or self.shuttle).action_open_screen()
|
||||||
|
return self.env[action["res_model"]].browse(action["res_id"])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_simple_picking_out(cls, product, quantity):
|
def _create_simple_picking_out(cls, product, quantity):
|
||||||
stock_loc = cls.env.ref("stock.stock_location_stock")
|
stock_loc = cls.env.ref("stock.stock_location_stock")
|
||||||
@@ -133,15 +141,17 @@ class VerticalLiftCase(common.LocationTrayTypeCase):
|
|||||||
inventory.action_start()
|
inventory.action_start()
|
||||||
return inventory
|
return inventory
|
||||||
|
|
||||||
def _test_button_release(self, move_line):
|
def _test_button_release(self, move_line, expected_state):
|
||||||
# for the test, we'll consider our last line has been delivered
|
# for the test, we'll consider our last line has been delivered
|
||||||
move_line.qty_done = move_line.product_qty
|
move_line.qty_done = move_line.product_qty
|
||||||
move_line.move_id._action_done()
|
move_line.move_id._action_done()
|
||||||
# release, no further operation in queue
|
# release, no further operation in queue
|
||||||
operation = self.shuttle._operation_for_mode()
|
operation = self.shuttle._operation_for_mode()
|
||||||
|
# the release button can be used only in the state... release
|
||||||
|
operation.state = "release"
|
||||||
result = operation.button_release()
|
result = operation.button_release()
|
||||||
|
self.assertEqual(operation.state, expected_state)
|
||||||
self.assertFalse(operation.current_move_line_id)
|
self.assertFalse(operation.current_move_line_id)
|
||||||
self.assertEqual(operation.operation_descr, _("No operations"))
|
|
||||||
expected_result = {
|
expected_result = {
|
||||||
"effect": {
|
"effect": {
|
||||||
"fadeout": "slow",
|
"fadeout": "slow",
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ class TestInventory(VerticalLiftCase):
|
|||||||
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._create_inventory([(self.location_2a_x1y1, self.product_socks)])
|
||||||
|
|
||||||
self.shuttle.switch_inventory()
|
operation = self._open_screen("inventory")
|
||||||
operation = self.shuttle._operation_for_mode()
|
|
||||||
self.assertEqual(operation.number_of_ops, 2)
|
self.assertEqual(operation.number_of_ops, 2)
|
||||||
self.assertEqual(operation.number_of_ops_all, 3)
|
self.assertEqual(operation.number_of_ops_all, 3)
|
||||||
|
|
||||||
@@ -45,16 +44,17 @@ class TestInventory(VerticalLiftCase):
|
|||||||
inventory = self._create_inventory(
|
inventory = self._create_inventory(
|
||||||
[(self.location_1a_x1y1, self.product_socks)]
|
[(self.location_1a_x1y1, self.product_socks)]
|
||||||
)
|
)
|
||||||
self.shuttle.switch_inventory()
|
operation = self._open_screen("inventory")
|
||||||
operation = self.shuttle._operation_for_mode()
|
self.assertEqual(operation.state, "quantity")
|
||||||
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
|
# test the happy path, quantity is correct
|
||||||
operation.quantity_input = 10.0
|
operation.quantity_input = 10.0
|
||||||
result = operation.button_save()
|
result = operation.button_save()
|
||||||
|
|
||||||
# state is reset
|
# state is reset
|
||||||
self.assertEqual(operation.state, "quantity")
|
# noop because we have no further lines
|
||||||
|
self.assertEqual(operation.state, "noop")
|
||||||
self.assertFalse(operation.current_inventory_line_id)
|
self.assertFalse(operation.current_inventory_line_id)
|
||||||
self.assertEqual(operation.operation_descr, _("No operations"))
|
|
||||||
self.assertTrue(inventory.line_ids.vertical_lift_done)
|
self.assertTrue(inventory.line_ids.vertical_lift_done)
|
||||||
self.assertEqual(inventory.state, "done")
|
self.assertEqual(inventory.state, "done")
|
||||||
expected_result = {
|
expected_result = {
|
||||||
@@ -72,8 +72,7 @@ class TestInventory(VerticalLiftCase):
|
|||||||
inventory = self._create_inventory(
|
inventory = self._create_inventory(
|
||||||
[(self.location_1a_x1y1, self.product_socks)]
|
[(self.location_1a_x1y1, self.product_socks)]
|
||||||
)
|
)
|
||||||
self.shuttle.switch_inventory()
|
operation = self._open_screen("inventory")
|
||||||
operation = self.shuttle._operation_for_mode()
|
|
||||||
line = operation.current_inventory_line_id
|
line = operation.current_inventory_line_id
|
||||||
self.assertEqual(line, inventory.line_ids)
|
self.assertEqual(line, inventory.line_ids)
|
||||||
|
|
||||||
@@ -83,9 +82,6 @@ class TestInventory(VerticalLiftCase):
|
|||||||
self.assertEqual(operation.quantity_input, 0.0)
|
self.assertEqual(operation.quantity_input, 0.0)
|
||||||
self.assertEqual(operation.state, "confirm_wrong_quantity")
|
self.assertEqual(operation.state, "confirm_wrong_quantity")
|
||||||
self.assertEqual(operation.current_inventory_line_id, line)
|
self.assertEqual(operation.current_inventory_line_id, line)
|
||||||
self.assertEqual(
|
|
||||||
operation.operation_descr, _("The quantity does not match, are you sure?")
|
|
||||||
)
|
|
||||||
|
|
||||||
# entering the same quantity a second time validates
|
# entering the same quantity a second time validates
|
||||||
operation.quantity_input = 12.0
|
operation.quantity_input = 12.0
|
||||||
@@ -94,3 +90,26 @@ class TestInventory(VerticalLiftCase):
|
|||||||
|
|
||||||
self.assertTrue(inventory.line_ids.vertical_lift_done)
|
self.assertTrue(inventory.line_ids.vertical_lift_done)
|
||||||
self.assertEqual(inventory.state, "done")
|
self.assertEqual(inventory.state, "done")
|
||||||
|
|
||||||
|
def test_inventory_next_line(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)
|
||||||
|
inventory = self._create_inventory(
|
||||||
|
[
|
||||||
|
(self.location_1a_x1y1, self.product_socks),
|
||||||
|
(self.location_1a_x2y1, self.product_recovery),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
inventory_lines = inventory.line_ids
|
||||||
|
operation = self._open_screen("inventory")
|
||||||
|
operation.quantity_input = 10.0
|
||||||
|
line1 = operation.current_inventory_line_id
|
||||||
|
result = operation.button_save()
|
||||||
|
self.assertFalse(result) # no rainbow man
|
||||||
|
|
||||||
|
# go to next line
|
||||||
|
remaining_line = inventory_lines - line1
|
||||||
|
self.assertEqual(operation.state, "quantity")
|
||||||
|
self.assertEqual(operation.current_inventory_line_id, remaining_line)
|
||||||
|
self.assertEqual(operation.last_quantity_input, 0.0)
|
||||||
|
self.assertEqual(operation.quantity_input, 0.0)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# Copyright 2019 Camptocamp SA
|
# Copyright 2019 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
from odoo import _
|
|
||||||
|
|
||||||
from .common import VerticalLiftCase
|
from .common import VerticalLiftCase
|
||||||
|
|
||||||
|
|
||||||
@@ -34,23 +32,22 @@ class TestPick(VerticalLiftCase):
|
|||||||
self.assertEqual(action["res_id"], operation.id)
|
self.assertEqual(action["res_id"], operation.id)
|
||||||
|
|
||||||
def test_pick_select_next_move_line(self):
|
def test_pick_select_next_move_line(self):
|
||||||
self.shuttle.mode = "pick"
|
operation = self._open_screen("pick")
|
||||||
operation = self.shuttle._operation_for_mode()
|
|
||||||
operation.select_next_move_line()
|
operation.select_next_move_line()
|
||||||
self.assertEqual(operation.current_move_line_id, self.out_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.state, "scan_destination")
|
||||||
|
|
||||||
def test_pick_save(self):
|
def test_pick_save(self):
|
||||||
self.shuttle.switch_pick()
|
operation = self._open_screen("pick")
|
||||||
operation = self.shuttle._operation_for_mode()
|
# assume we already scanned the destination, current state is save
|
||||||
|
operation.state = "save"
|
||||||
operation.current_move_line_id = self.out_move_line
|
operation.current_move_line_id = self.out_move_line
|
||||||
operation.button_save()
|
operation.button_save()
|
||||||
self.assertEqual(operation.current_move_line_id.state, "done")
|
self.assertEqual(operation.current_move_line_id.state, "done")
|
||||||
self.assertEqual(operation.operation_descr, _("Release"))
|
self.assertEqual(operation.state, "release")
|
||||||
|
|
||||||
def test_pick_related_fields(self):
|
def test_pick_related_fields(self):
|
||||||
self.shuttle.switch_pick()
|
operation = self._open_screen("pick")
|
||||||
operation = self.shuttle._operation_for_mode()
|
|
||||||
ml = operation.current_move_line_id = self.out_move_line
|
ml = operation.current_move_line_id = self.out_move_line
|
||||||
|
|
||||||
# Trays related fields
|
# Trays related fields
|
||||||
@@ -148,8 +145,8 @@ class TestPick(VerticalLiftCase):
|
|||||||
self.assertEqual(operation2.number_of_ops_all, 6)
|
self.assertEqual(operation2.number_of_ops_all, 6)
|
||||||
|
|
||||||
def test_on_barcode_scanned(self):
|
def test_on_barcode_scanned(self):
|
||||||
self.shuttle.switch_pick()
|
operation = self._open_screen("pick")
|
||||||
operation = self.shuttle._operation_for_mode()
|
self.assertEqual(operation.state, "scan_destination")
|
||||||
move_line = operation.current_move_line_id
|
move_line = operation.current_move_line_id
|
||||||
current_destination = move_line.location_dest_id
|
current_destination = move_line.location_dest_id
|
||||||
stock_location = self.env.ref("stock.stock_location_stock")
|
stock_location = self.env.ref("stock.stock_location_stock")
|
||||||
@@ -158,14 +155,14 @@ class TestPick(VerticalLiftCase):
|
|||||||
)
|
)
|
||||||
operation.on_barcode_scanned(stock_location.barcode)
|
operation.on_barcode_scanned(stock_location.barcode)
|
||||||
self.assertEqual(move_line.location_dest_id, stock_location)
|
self.assertEqual(move_line.location_dest_id, stock_location)
|
||||||
|
self.assertEqual(operation.state, "save")
|
||||||
|
|
||||||
def test_button_release(self):
|
def test_button_release(self):
|
||||||
self.shuttle.switch_pick()
|
self._open_screen("pick")
|
||||||
self._test_button_release(self.out_move_line)
|
self._test_button_release(self.out_move_line, "noop")
|
||||||
|
|
||||||
def test_process_current_pick(self):
|
def test_process_current_pick(self):
|
||||||
self.shuttle.switch_pick()
|
operation = self._open_screen("pick")
|
||||||
operation = self.shuttle._operation_for_mode()
|
|
||||||
operation.current_move_line_id = self.out_move_line
|
operation.current_move_line_id = self.out_move_line
|
||||||
qty_to_process = self.out_move_line.product_qty
|
qty_to_process = self.out_move_line.product_qty
|
||||||
operation.process_current()
|
operation.process_current()
|
||||||
@@ -173,8 +170,7 @@ class TestPick(VerticalLiftCase):
|
|||||||
self.assertEqual(self.out_move_line.qty_done, qty_to_process)
|
self.assertEqual(self.out_move_line.qty_done, qty_to_process)
|
||||||
|
|
||||||
def test_matrix(self):
|
def test_matrix(self):
|
||||||
self.shuttle.switch_pick()
|
operation = self._open_screen("pick")
|
||||||
operation = self.shuttle._operation_for_mode()
|
|
||||||
operation.current_move_line_id = self.out_move_line
|
operation.current_move_line_id = self.out_move_line
|
||||||
location = self.out_move_line.location_id
|
location = self.out_move_line.location_id
|
||||||
# offset by -1 because the fields are for humans
|
# offset by -1 because the fields are for humans
|
||||||
|
|||||||
@@ -13,17 +13,7 @@ class TestPut(VerticalLiftCase):
|
|||||||
)
|
)
|
||||||
cls.picking_in.action_confirm()
|
cls.picking_in.action_confirm()
|
||||||
cls.in_move_line = cls.picking_in.move_line_ids
|
cls.in_move_line = cls.picking_in.move_line_ids
|
||||||
cls.in_move_line.location_dest_id = cls.location_1a_x3y1
|
cls.in_move_line.location_dest_id = cls.shuttle.location_id
|
||||||
|
|
||||||
def _select_move_lines(self, shuttle, move_lines=None):
|
|
||||||
select_model = self.env["vertical.lift.operation.put.select"]
|
|
||||||
operation = shuttle._operation_for_mode()
|
|
||||||
select = select_model.create({"operation_id": operation.id})
|
|
||||||
if move_lines:
|
|
||||||
select.move_line_ids = [(6, 0, move_lines.ids)]
|
|
||||||
else:
|
|
||||||
select.action_add_all()
|
|
||||||
select._sync_lines()
|
|
||||||
|
|
||||||
def test_put_action_open_screen(self):
|
def test_put_action_open_screen(self):
|
||||||
self.shuttle.switch_put()
|
self.shuttle.switch_put()
|
||||||
@@ -41,127 +31,105 @@ class TestPut(VerticalLiftCase):
|
|||||||
self.env["stock.move.line"].browse(),
|
self.env["stock.move.line"].browse(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_select_from_barcode(self):
|
|
||||||
self.shuttle.switch_put()
|
|
||||||
self.picking_in.action_cancel()
|
|
||||||
put1 = self._create_simple_picking_in(
|
|
||||||
self.product_socks, 10, self.location_1a_x1y1
|
|
||||||
)
|
|
||||||
put1.action_confirm()
|
|
||||||
put2 = self._create_simple_picking_in(
|
|
||||||
self.product_recovery, 10, self.location_1a_x2y1
|
|
||||||
)
|
|
||||||
put2.action_confirm()
|
|
||||||
select_model = self.env["vertical.lift.operation.put.select"]
|
|
||||||
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}])
|
|
||||||
select.on_barcode_scanned(self.product_recovery.barcode)
|
|
||||||
self.assertRecordValues(
|
|
||||||
select, [{"move_line_ids": (put1.move_line_ids | put2.move_line_ids).ids}]
|
|
||||||
)
|
|
||||||
select.action_validate()
|
|
||||||
self.assertEqual(len(operation.operation_line_ids), 2)
|
|
||||||
self.assertRecordValues(
|
|
||||||
operation.mapped("operation_line_ids"),
|
|
||||||
[
|
|
||||||
{"move_line_id": put1.move_line_ids.id},
|
|
||||||
{"move_line_id": put2.move_line_ids.id},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_no_select_from_barcode_outside_location(self):
|
|
||||||
self.shuttle.switch_put()
|
|
||||||
self.picking_in.action_cancel()
|
|
||||||
location = self.env.ref("stock.location_refrigerator_small")
|
|
||||||
put1 = self._create_simple_picking_in(self.product_socks, 10, location)
|
|
||||||
put1.action_confirm()
|
|
||||||
select_model = self.env["vertical.lift.operation.put.select"]
|
|
||||||
operation = self.shuttle._operation_for_mode()
|
|
||||||
select = select_model.create({"operation_id": operation.id})
|
|
||||||
select.on_barcode_scanned(self.product_socks.barcode)
|
|
||||||
# the move line is outside of the vertical lift, should not be
|
|
||||||
# selected
|
|
||||||
self.assertRecordValues(select, [{"move_line_ids": []}])
|
|
||||||
|
|
||||||
def test_put_count_move_lines(self):
|
def test_put_count_move_lines(self):
|
||||||
self.shuttle.switch_put()
|
|
||||||
self.picking_in.action_cancel()
|
self.picking_in.action_cancel()
|
||||||
put1 = self._create_simple_picking_in(
|
put1 = self._create_simple_picking_in(
|
||||||
self.product_socks, 10, self.location_1a_x1y1
|
self.product_socks, 10, self.location_1a_x1y1
|
||||||
)
|
)
|
||||||
put1.action_confirm()
|
put1.action_confirm()
|
||||||
put2 = self._create_simple_picking_in(
|
put2 = self._create_simple_picking_in(
|
||||||
self.product_recovery, 10, self.location_1a_x2y1
|
self.product_recovery, 10, self.vertical_lift_loc
|
||||||
)
|
)
|
||||||
put2.action_confirm()
|
put2.action_confirm()
|
||||||
put3 = self._create_simple_picking_in(
|
put3 = self._create_simple_picking_in(
|
||||||
self.product_recovery, 10, self.location_2a_x1y1
|
self.product_recovery, 10, self.vertical_lift_loc
|
||||||
)
|
)
|
||||||
put3.action_confirm()
|
put3.action_confirm()
|
||||||
operation = self.shuttle._operation_for_mode()
|
operation = self._open_screen("put")
|
||||||
self._select_move_lines(self.shuttle)
|
|
||||||
shuttle2 = self.env.ref(
|
shuttle2 = self.env.ref(
|
||||||
"stock_vertical_lift.stock_vertical_lift_demo_shuttle_2"
|
"stock_vertical_lift.stock_vertical_lift_demo_shuttle_2"
|
||||||
)
|
)
|
||||||
shuttle2.switch_put()
|
operation2 = self._open_screen("put", shuttle=shuttle2)
|
||||||
operation2 = shuttle2._operation_for_mode()
|
|
||||||
self._select_move_lines(shuttle2)
|
|
||||||
|
|
||||||
self.assertEqual(operation.number_of_ops, 2)
|
# we don't really care about the "number_of_ops" for the
|
||||||
|
# put-away, as the move lines are supposed to have the whole
|
||||||
|
# whole shuttle view as destination
|
||||||
|
self.assertEqual(operation.number_of_ops, 1)
|
||||||
self.assertEqual(operation.number_of_ops_all, 3)
|
self.assertEqual(operation.number_of_ops_all, 3)
|
||||||
self.assertEqual(operation2.number_of_ops, 1)
|
self.assertEqual(operation2.number_of_ops, 0)
|
||||||
self.assertEqual(operation2.number_of_ops_all, 3)
|
self.assertEqual(operation2.number_of_ops_all, 3)
|
||||||
|
|
||||||
def test_process_current_put(self):
|
def test_transition_start(self):
|
||||||
self.shuttle.switch_put()
|
operation = self._open_screen("put")
|
||||||
operation = self.shuttle._operation_for_mode()
|
# we begin with an empty screen, user has to scan a package, product,
|
||||||
self._select_move_lines(self.shuttle, self.in_move_line)
|
# or lot
|
||||||
self.assertEqual(operation.current_move_line_id, self.in_move_line)
|
self.assertEqual(operation.state, "scan_source")
|
||||||
qty_to_process = self.in_move_line.product_qty
|
|
||||||
operation.process_current()
|
|
||||||
self.assertEqual(self.in_move_line.state, "done")
|
|
||||||
self.assertEqual(self.in_move_line.qty_done, qty_to_process)
|
|
||||||
|
|
||||||
def test_transition_reset(self):
|
def test_transition_scan_source_to_scan_tray_type(self):
|
||||||
self.shuttle.switch_put()
|
operation = self._open_screen("put")
|
||||||
operation = self.shuttle._operation_for_mode()
|
self.assertEqual(operation.state, "scan_source")
|
||||||
self._select_move_lines(self.shuttle, self.in_move_line)
|
|
||||||
operation.state = "scan_tray_type"
|
|
||||||
operation.reset_steps()
|
|
||||||
self.assertEqual(operation.step(), "scan_product")
|
|
||||||
|
|
||||||
def test_transition_scan_product(self):
|
|
||||||
self.shuttle.switch_put()
|
|
||||||
operation = self.shuttle._operation_for_mode()
|
|
||||||
self._select_move_lines(self.shuttle, self.in_move_line)
|
|
||||||
operation.state = "scan_product"
|
|
||||||
# wrong barcode, nothing happens
|
# wrong barcode, nothing happens
|
||||||
operation.on_barcode_scanned("foo")
|
operation.on_barcode_scanned("foo")
|
||||||
|
self.assertEqual(operation.state, "scan_source")
|
||||||
# product scanned, move to next step
|
# product scanned, move to next step
|
||||||
operation.on_barcode_scanned(self.product_socks.barcode)
|
operation.on_barcode_scanned(self.product_socks.barcode)
|
||||||
self.assertEqual(operation.step(), "scan_tray_type")
|
self.assertEqual(operation.state, "scan_tray_type")
|
||||||
|
self.assertEqual(operation.current_move_line_id, self.in_move_line)
|
||||||
|
|
||||||
def test_transition_scan_tray_type(self):
|
def test_transition_scan_tray_type_to_save(self):
|
||||||
self.shuttle.switch_put()
|
operation = self._open_screen("put")
|
||||||
operation = self.shuttle._operation_for_mode()
|
# assume we already scanned the product
|
||||||
self._select_move_lines(self.shuttle, self.in_move_line)
|
|
||||||
operation.state = "scan_tray_type"
|
operation.state = "scan_tray_type"
|
||||||
|
operation.current_move_line_id = self.in_move_line
|
||||||
# wrong barcode, nothing happens
|
# wrong barcode, nothing happens
|
||||||
operation.on_barcode_scanned("foo")
|
operation.on_barcode_scanned("foo")
|
||||||
# tray type scanned, move to next step
|
# tray type scanned, move to next step
|
||||||
operation.on_barcode_scanned(operation.tray_type_id.code)
|
operation.on_barcode_scanned(self.location_1a.tray_type_id.code)
|
||||||
self.assertEqual(operation.step(), "save")
|
self.assertEqual(operation.state, "save")
|
||||||
|
# a cell has been set
|
||||||
|
self.assertTrue(
|
||||||
|
self.in_move_line.location_dest_id
|
||||||
|
in self.shuttle.location_id.child_ids.child_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_transition_scan_tray_type_no_empty_cell(self):
|
||||||
|
operation = self._open_screen("put")
|
||||||
|
# assume we already scanned the product
|
||||||
|
operation.state = "scan_tray_type"
|
||||||
|
operation.current_move_line_id = self.in_move_line
|
||||||
|
# create a tray type without location, which is the same as if all the
|
||||||
|
# locations of a tray type were full
|
||||||
|
new_tray_type = self.env["stock.location.tray.type"].create(
|
||||||
|
{"name": "new tray type", "code": "test", "rows": 1, "cols": 1}
|
||||||
|
)
|
||||||
|
operation.on_barcode_scanned(new_tray_type.code)
|
||||||
|
# should stay the same state
|
||||||
|
self.assertEqual(operation.state, "scan_tray_type")
|
||||||
|
# destination not changed
|
||||||
|
self.assertEqual(self.in_move_line.location_dest_id, self.shuttle.location_id)
|
||||||
|
|
||||||
def test_transition_save(self):
|
def test_transition_save(self):
|
||||||
self.shuttle.switch_put()
|
operation = self._open_screen("put")
|
||||||
operation = self.shuttle._operation_for_mode()
|
# first steps of the workflow are done
|
||||||
self._select_move_lines(self.shuttle, self.in_move_line)
|
operation.current_move_line_id = self.in_move_line
|
||||||
|
operation.current_move_line_id.location_dest_id = self.location_1a_x1y1
|
||||||
operation.state = "save"
|
operation.state = "save"
|
||||||
|
qty_to_process = self.in_move_line.product_qty
|
||||||
operation.button_save()
|
operation.button_save()
|
||||||
self.assertEqual(operation.step(), "release")
|
self.assertEqual(self.in_move_line.state, "done")
|
||||||
|
self.assertEqual(self.in_move_line.qty_done, qty_to_process)
|
||||||
|
|
||||||
def test_transition_button_release(self):
|
def test_transition_button_release(self):
|
||||||
self.shuttle.switch_put()
|
operation = self._open_screen("put")
|
||||||
self._test_button_release(self.in_move_line)
|
move_line = self.in_move_line
|
||||||
|
# first steps of the workflow are done
|
||||||
|
operation.current_move_line_id = move_line
|
||||||
|
operation.current_move_line_id.location_dest_id = self.location_1a_x1y1
|
||||||
|
# for the test, we'll consider our last line has been delivered
|
||||||
|
move_line.qty_done = move_line.product_qty
|
||||||
|
move_line.move_id._action_done()
|
||||||
|
|
||||||
|
operation = self._open_screen("put")
|
||||||
|
operation.button_release()
|
||||||
|
self.assertEqual(operation.state, "scan_source")
|
||||||
|
self.assertFalse(operation.current_move_line_id)
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="o_shuttle_header_right o_shuttle_header_content">
|
<div class="o_shuttle_header_right o_shuttle_header_content">
|
||||||
<label for="number_of_ops" />
|
<label for="number_of_ops" />
|
||||||
<field name="number_of_ops" readonly="1" /> /
|
<field name="number_of_ops" readonly="1" />
|
||||||
|
<span>/</span>
|
||||||
<field name="number_of_ops_all" readonly="1" />
|
<field name="number_of_ops_all" readonly="1" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
icon="fa-check"
|
icon="fa-check"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
barcode_trigger="save"
|
barcode_trigger="save"
|
||||||
|
attrs="{'invisible': [('state', '!=', 'save')]}"
|
||||||
/>
|
/>
|
||||||
<!-- will react on barcode 'O-BTN.release -->
|
<!-- will react on barcode 'O-BTN.release -->
|
||||||
<button
|
<button
|
||||||
@@ -80,13 +82,14 @@
|
|||||||
string="Release"
|
string="Release"
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
barcode_trigger="release"
|
barcode_trigger="release"
|
||||||
|
attrs="{'invisible': [('state', '!=', 'release')]}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_shuttle_operation bg-primary jumbotron jumbotron-fluid">
|
<div class="o_shuttle_operation bg-primary jumbotron jumbotron-fluid">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<field name="operation_descr" />
|
<field name="state" invisible="0" readonly="True" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_shuttle_data">
|
<div class="o_shuttle_data">
|
||||||
@@ -134,7 +137,6 @@
|
|||||||
options="{'no_open': True}"
|
options="{'no_open': True}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO change for pick and put? -->
|
|
||||||
<label for="location_dest_id" />
|
<label for="location_dest_id" />
|
||||||
<div>
|
<div>
|
||||||
<field
|
<field
|
||||||
@@ -179,8 +181,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- on the right of the screen -->
|
<!-- on the right of the screen -->
|
||||||
<div class="o_shuttle_data_content o_shuttle_tray">
|
<div
|
||||||
|
class="o_shuttle_data_content o_shuttle_tray"
|
||||||
|
attrs="{'invisible': [('tray_type_id', '=', False)]}"
|
||||||
|
>
|
||||||
<group col="1">
|
<group col="1">
|
||||||
|
<field name="tray_type_id" invisible="1" />
|
||||||
<field name="tray_name" />
|
<field name="tray_name" />
|
||||||
<field name="tray_type_code" />
|
<field name="tray_type_code" />
|
||||||
<field name="tray_y" />
|
<field name="tray_y" />
|
||||||
|
|||||||
@@ -24,6 +24,11 @@
|
|||||||
<form position="inside">
|
<form position="inside">
|
||||||
<field name="state" invisible="1" />
|
<field name="state" invisible="1" />
|
||||||
</form>
|
</form>
|
||||||
|
<button name="button_save" position="attributes">
|
||||||
|
<attribute
|
||||||
|
name="attrs"
|
||||||
|
>{'invisible': [('state', 'not in', ('quantity', 'confirm_wrong_quantity'))]}</attribute>
|
||||||
|
</button>
|
||||||
<button name="button_release" position="attributes">
|
<button name="button_release" position="attributes">
|
||||||
<attribute name="invisible">1</attribute>
|
<attribute name="invisible">1</attribute>
|
||||||
</button>
|
</button>
|
||||||
@@ -72,6 +77,7 @@
|
|||||||
name="quantity_input"
|
name="quantity_input"
|
||||||
default_focus="1"
|
default_focus="1"
|
||||||
class="oe_inline"
|
class="oe_inline"
|
||||||
|
attrs="{'readonly': [('state', 'not in', ('quantity', 'confirm_wrong_quantity'))]}"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="product_uom_id"
|
name="product_uom_id"
|
||||||
|
|||||||
@@ -22,68 +22,16 @@
|
|||||||
<attribute name="string">Put-Away Screen</attribute>
|
<attribute name="string">Put-Away Screen</attribute>
|
||||||
</form>
|
</form>
|
||||||
<xpath
|
<xpath
|
||||||
expr="//div[hasclass('o_shuttle_actions')]/div[hasclass('o_shuttle_content_left')]"
|
expr="//div[hasclass('o_shuttle_header_right')]/field[@name='number_of_ops']"
|
||||||
position="inside"
|
position="attributes"
|
||||||
|
>
|
||||||
|
<attribute name="invisible">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath
|
||||||
|
expr="//div[hasclass('o_shuttle_header_right')]/field[@name='number_of_ops']/following-sibling::span"
|
||||||
|
position="replace"
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
name="action_select_operations"
|
|
||||||
type="object"
|
|
||||||
class="btn-primary"
|
|
||||||
string="Select Operations"
|
|
||||||
aria-label="Select Operations"
|
|
||||||
title="Select Operations"
|
|
||||||
/>
|
|
||||||
</xpath>
|
</xpath>
|
||||||
<field name="_barcode_scanned" position="before">
|
|
||||||
<!-- these fields have to be in the view otherwise they
|
|
||||||
would be empty in the record sent to the _barcode_scanned
|
|
||||||
onchange method
|
|
||||||
-->
|
|
||||||
<field name="state" invisible="1" />
|
|
||||||
<field name="current_operation_line_id" invisible="1" />
|
|
||||||
</field>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
<record id="vertical_lift_operation_put_select_view" model="ir.ui.view">
|
|
||||||
<field name="name">vertical.lift.operation.put.select.view</field>
|
|
||||||
<field name="model">vertical.lift.operation.put.select</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form string="Select" class="o_vlift_shuttle_popup">
|
|
||||||
<field name="move_line_ids">
|
|
||||||
<tree create="0" edit="0">
|
|
||||||
<field name="product_id" />
|
|
||||||
</tree>
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="product_id" readonly="1" />
|
|
||||||
<field name="product_uom_qty" readonly="1" />
|
|
||||||
<field name="qty_done" readonly="1" />
|
|
||||||
</group>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
<!-- this field has to be in the view otherwise it
|
|
||||||
would be empty in the record sent to the _barcode_scanned
|
|
||||||
onchange method
|
|
||||||
-->
|
|
||||||
<field name="operation_id" invisible="1" />
|
|
||||||
<field name="_barcode_scanned" widget="barcode_handler" />
|
|
||||||
<footer>
|
|
||||||
<button
|
|
||||||
name="action_validate"
|
|
||||||
string="Validate"
|
|
||||||
type="object"
|
|
||||||
class="btn-primary"
|
|
||||||
/>
|
|
||||||
<button string="Cancel" class="btn-secondary" special="cancel" />
|
|
||||||
<button
|
|
||||||
name="action_add_all"
|
|
||||||
string="Add all"
|
|
||||||
type="object"
|
|
||||||
groups="base.group_no_one"
|
|
||||||
class="btn-secondary"
|
|
||||||
/>
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Reference in New Issue
Block a user