odoo-addon-shopfloor 16.0.1.0.0.24__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/shopfloor/README.rst +160 -0
- odoo/addons/shopfloor/__init__.py +4 -0
- odoo/addons/shopfloor/__manifest__.py +65 -0
- odoo/addons/shopfloor/actions/__init__.py +15 -0
- odoo/addons/shopfloor/actions/change_package_lot.py +164 -0
- odoo/addons/shopfloor/actions/completion_info.py +42 -0
- odoo/addons/shopfloor/actions/data.py +329 -0
- odoo/addons/shopfloor/actions/data_detail.py +154 -0
- odoo/addons/shopfloor/actions/inventory.py +150 -0
- odoo/addons/shopfloor/actions/location_content_transfer_sorter.py +89 -0
- odoo/addons/shopfloor/actions/message.py +846 -0
- odoo/addons/shopfloor/actions/move_line_search.py +119 -0
- odoo/addons/shopfloor/actions/packaging.py +59 -0
- odoo/addons/shopfloor/actions/savepoint.py +44 -0
- odoo/addons/shopfloor/actions/schema.py +182 -0
- odoo/addons/shopfloor/actions/schema_detail.py +98 -0
- odoo/addons/shopfloor/actions/search.py +187 -0
- odoo/addons/shopfloor/actions/stock.py +239 -0
- odoo/addons/shopfloor/actions/stock_unreserve.py +66 -0
- odoo/addons/shopfloor/components/__init__.py +5 -0
- odoo/addons/shopfloor/components/scan_handler_location.py +26 -0
- odoo/addons/shopfloor/components/scan_handler_lot.py +26 -0
- odoo/addons/shopfloor/components/scan_handler_package.py +26 -0
- odoo/addons/shopfloor/components/scan_handler_product.py +26 -0
- odoo/addons/shopfloor/components/scan_handler_transfer.py +26 -0
- odoo/addons/shopfloor/data/shopfloor_scenario_data.xml +73 -0
- odoo/addons/shopfloor/demo/shopfloor_app_demo.xml +12 -0
- odoo/addons/shopfloor/demo/shopfloor_menu_demo.xml +64 -0
- odoo/addons/shopfloor/demo/shopfloor_profile_demo.xml +8 -0
- odoo/addons/shopfloor/demo/stock_picking_type_demo.xml +93 -0
- odoo/addons/shopfloor/docs/checkout_diag_seq.plantuml +61 -0
- odoo/addons/shopfloor/docs/checkout_diag_seq.png +0 -0
- odoo/addons/shopfloor/docs/cluster_picking_diag_seq.plantuml +112 -0
- odoo/addons/shopfloor/docs/cluster_picking_diag_seq.png +0 -0
- odoo/addons/shopfloor/docs/delivery_diag_seq.plantuml +56 -0
- odoo/addons/shopfloor/docs/delivery_diag_seq.png +0 -0
- odoo/addons/shopfloor/docs/location_content_transfer_diag_seq.plantuml +66 -0
- odoo/addons/shopfloor/docs/location_content_transfer_diag_seq.png +0 -0
- odoo/addons/shopfloor/docs/oca_logo.png +0 -0
- odoo/addons/shopfloor/docs/single_pack_transfer_diag_seq.plantuml +36 -0
- odoo/addons/shopfloor/docs/single_pack_transfer_diag_seq.png +0 -0
- odoo/addons/shopfloor/docs/zone_picking_diag_seq.plantuml +85 -0
- odoo/addons/shopfloor/docs/zone_picking_diag_seq.png +0 -0
- odoo/addons/shopfloor/exceptions.py +6 -0
- odoo/addons/shopfloor/i18n/ca.po +1802 -0
- odoo/addons/shopfloor/i18n/de.po +1791 -0
- odoo/addons/shopfloor/i18n/es_AR.po +2147 -0
- odoo/addons/shopfloor/i18n/pt_BR.po +1791 -0
- odoo/addons/shopfloor/i18n/shopfloor.pot +1877 -0
- odoo/addons/shopfloor/models/__init__.py +12 -0
- odoo/addons/shopfloor/models/priority_postpone_mixin.py +41 -0
- odoo/addons/shopfloor/models/shopfloor_app.py +9 -0
- odoo/addons/shopfloor/models/shopfloor_menu.py +436 -0
- odoo/addons/shopfloor/models/stock_location.py +76 -0
- odoo/addons/shopfloor/models/stock_move.py +119 -0
- odoo/addons/shopfloor/models/stock_move_line.py +307 -0
- odoo/addons/shopfloor/models/stock_package_level.py +50 -0
- odoo/addons/shopfloor/models/stock_picking.py +118 -0
- odoo/addons/shopfloor/models/stock_picking_batch.py +41 -0
- odoo/addons/shopfloor/models/stock_picking_type.py +26 -0
- odoo/addons/shopfloor/models/stock_quant.py +31 -0
- odoo/addons/shopfloor/models/stock_quant_package.py +101 -0
- odoo/addons/shopfloor/readme/CONTRIBUTORS.rst +18 -0
- odoo/addons/shopfloor/readme/CREDITS.rst +5 -0
- odoo/addons/shopfloor/readme/DESCRIPTION.rst +17 -0
- odoo/addons/shopfloor/readme/HISTORY.rst +4 -0
- odoo/addons/shopfloor/readme/ROADMAP.rst +4 -0
- odoo/addons/shopfloor/readme/USAGE.rst +6 -0
- odoo/addons/shopfloor/security/groups.xml +17 -0
- odoo/addons/shopfloor/services/__init__.py +16 -0
- odoo/addons/shopfloor/services/checkout.py +1763 -0
- odoo/addons/shopfloor/services/cluster_picking.py +1628 -0
- odoo/addons/shopfloor/services/delivery.py +828 -0
- odoo/addons/shopfloor/services/forms/__init__.py +1 -0
- odoo/addons/shopfloor/services/forms/picking_form.py +78 -0
- odoo/addons/shopfloor/services/location_content_transfer.py +1194 -0
- odoo/addons/shopfloor/services/menu.py +60 -0
- odoo/addons/shopfloor/services/picking_batch.py +126 -0
- odoo/addons/shopfloor/services/service.py +101 -0
- odoo/addons/shopfloor/services/single_pack_transfer.py +366 -0
- odoo/addons/shopfloor/services/zone_picking.py +1938 -0
- odoo/addons/shopfloor/static/description/icon.png +0 -0
- odoo/addons/shopfloor/static/description/index.html +500 -0
- odoo/addons/shopfloor/tests/__init__.py +83 -0
- odoo/addons/shopfloor/tests/common.py +324 -0
- odoo/addons/shopfloor/tests/models.py +29 -0
- odoo/addons/shopfloor/tests/test_actions_change_package_lot.py +1175 -0
- odoo/addons/shopfloor/tests/test_actions_data.py +376 -0
- odoo/addons/shopfloor/tests/test_actions_data_base.py +244 -0
- odoo/addons/shopfloor/tests/test_actions_data_detail.py +322 -0
- odoo/addons/shopfloor/tests/test_actions_search.py +248 -0
- odoo/addons/shopfloor/tests/test_actions_stock.py +48 -0
- odoo/addons/shopfloor/tests/test_checkout_auto_post.py +67 -0
- odoo/addons/shopfloor/tests/test_checkout_base.py +81 -0
- odoo/addons/shopfloor/tests/test_checkout_cancel_line.py +154 -0
- odoo/addons/shopfloor/tests/test_checkout_change_packaging.py +184 -0
- odoo/addons/shopfloor/tests/test_checkout_done.py +133 -0
- odoo/addons/shopfloor/tests/test_checkout_list_delivery_packaging.py +131 -0
- odoo/addons/shopfloor/tests/test_checkout_list_package.py +327 -0
- odoo/addons/shopfloor/tests/test_checkout_new_package.py +88 -0
- odoo/addons/shopfloor/tests/test_checkout_no_package.py +95 -0
- odoo/addons/shopfloor/tests/test_checkout_scan.py +174 -0
- odoo/addons/shopfloor/tests/test_checkout_scan_line.py +377 -0
- odoo/addons/shopfloor/tests/test_checkout_scan_line_base.py +25 -0
- odoo/addons/shopfloor/tests/test_checkout_scan_line_no_prefill_qty.py +91 -0
- odoo/addons/shopfloor/tests/test_checkout_scan_package_action.py +451 -0
- odoo/addons/shopfloor/tests/test_checkout_scan_package_action_no_prefill_qty.py +107 -0
- odoo/addons/shopfloor/tests/test_checkout_select.py +74 -0
- odoo/addons/shopfloor/tests/test_checkout_select_line.py +130 -0
- odoo/addons/shopfloor/tests/test_checkout_select_package_base.py +64 -0
- odoo/addons/shopfloor/tests/test_checkout_set_qty.py +257 -0
- odoo/addons/shopfloor/tests/test_checkout_summary.py +69 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_base.py +83 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_batch.py +109 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_change_pack_lot.py +111 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_is_zero.py +98 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_scan_destination.py +376 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_scan_destination_no_prefill_qty.py +115 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_scan_line.py +402 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_scan_line_location_or_pack_first.py +114 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_scan_line_no_prefill_qty.py +70 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_select.py +387 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_skip.py +90 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_stock_issue.py +364 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_unload.py +911 -0
- odoo/addons/shopfloor/tests/test_delivery_base.py +155 -0
- odoo/addons/shopfloor/tests/test_delivery_done.py +108 -0
- odoo/addons/shopfloor/tests/test_delivery_list_stock_picking.py +49 -0
- odoo/addons/shopfloor/tests/test_delivery_reset_qty_done_line.py +119 -0
- odoo/addons/shopfloor/tests/test_delivery_reset_qty_done_pack.py +107 -0
- odoo/addons/shopfloor/tests/test_delivery_scan_deliver.py +557 -0
- odoo/addons/shopfloor/tests/test_delivery_select.py +38 -0
- odoo/addons/shopfloor/tests/test_delivery_set_qty_done_line.py +91 -0
- odoo/addons/shopfloor/tests/test_delivery_set_qty_done_pack.py +135 -0
- odoo/addons/shopfloor/tests/test_delivery_sublocation.py +180 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_base.py +136 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_get_work.py +125 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_mix.py +509 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_putaway.py +143 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_scan_location.py +34 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_set_destination_all.py +343 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py +1074 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_single.py +748 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_start.py +359 -0
- odoo/addons/shopfloor/tests/test_menu_base.py +261 -0
- odoo/addons/shopfloor/tests/test_menu_counters.py +61 -0
- odoo/addons/shopfloor/tests/test_misc.py +25 -0
- odoo/addons/shopfloor/tests/test_move_action_assign.py +87 -0
- odoo/addons/shopfloor/tests/test_openapi.py +21 -0
- odoo/addons/shopfloor/tests/test_picking_form.py +62 -0
- odoo/addons/shopfloor/tests/test_scan_anything.py +49 -0
- odoo/addons/shopfloor/tests/test_single_pack_transfer.py +1121 -0
- odoo/addons/shopfloor/tests/test_single_pack_transfer_base.py +32 -0
- odoo/addons/shopfloor/tests/test_single_pack_transfer_putaway.py +104 -0
- odoo/addons/shopfloor/tests/test_stock_split.py +204 -0
- odoo/addons/shopfloor/tests/test_user.py +42 -0
- odoo/addons/shopfloor/tests/test_zone_picking_base.py +608 -0
- odoo/addons/shopfloor/tests/test_zone_picking_change_pack_lot.py +140 -0
- odoo/addons/shopfloor/tests/test_zone_picking_select_line.py +723 -0
- odoo/addons/shopfloor/tests/test_zone_picking_select_line_first_scan_location.py +207 -0
- odoo/addons/shopfloor/tests/test_zone_picking_select_line_first_scan_location.py.bak +202 -0
- odoo/addons/shopfloor/tests/test_zone_picking_select_line_no_prefill_qty.py +107 -0
- odoo/addons/shopfloor/tests/test_zone_picking_select_picking_type.py +26 -0
- odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination.py +643 -0
- odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination_no_prefill_qty.py +146 -0
- odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py +241 -0
- odoo/addons/shopfloor/tests/test_zone_picking_start.py +206 -0
- odoo/addons/shopfloor/tests/test_zone_picking_stock_issue.py +121 -0
- odoo/addons/shopfloor/tests/test_zone_picking_unload_all.py +353 -0
- odoo/addons/shopfloor/tests/test_zone_picking_unload_buffer_lines.py +113 -0
- odoo/addons/shopfloor/tests/test_zone_picking_unload_set_destination.py +374 -0
- odoo/addons/shopfloor/tests/test_zone_picking_unload_single.py +123 -0
- odoo/addons/shopfloor/tests/test_zone_picking_zero_check.py +43 -0
- odoo/addons/shopfloor/utils.py +13 -0
- odoo/addons/shopfloor/views/shopfloor_menu.xml +167 -0
- odoo/addons/shopfloor/views/stock_location.xml +20 -0
- odoo/addons/shopfloor/views/stock_move_line.xml +52 -0
- odoo/addons/shopfloor/views/stock_picking_type.xml +19 -0
- odoo_addon_shopfloor-16.0.1.0.0.24.dist-info/METADATA +192 -0
- odoo_addon_shopfloor-16.0.1.0.0.24.dist-info/RECORD +182 -0
- odoo_addon_shopfloor-16.0.1.0.0.24.dist-info/WHEEL +5 -0
- odoo_addon_shopfloor-16.0.1.0.0.24.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1938 @@
|
|
1
|
+
# Copyright 2020-2021 Camptocamp SA (http://www.camptocamp.com)
|
2
|
+
# Copyright 2020-2021 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
3
|
+
# Copyright 2023 Michael Tietz (MT Software) <mtietz@mt-software.de>
|
4
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
5
|
+
import functools
|
6
|
+
from collections import defaultdict
|
7
|
+
|
8
|
+
from odoo.fields import first
|
9
|
+
from odoo.tools.float_utils import float_compare, float_is_zero
|
10
|
+
|
11
|
+
from odoo.addons.base_rest.components.service import to_bool, to_int
|
12
|
+
from odoo.addons.component.core import Component
|
13
|
+
|
14
|
+
from ..exceptions import ConcurentWorkOnTransfer
|
15
|
+
from ..utils import to_float
|
16
|
+
|
17
|
+
|
18
|
+
class ZonePicking(Component):
|
19
|
+
"""
|
20
|
+
Methods for the Zone Picking Process
|
21
|
+
|
22
|
+
Zone picking of move lines.
|
23
|
+
|
24
|
+
You will find a sequence diagram describing states and endpoints
|
25
|
+
relationships [here](../docs/zone_picking_diag_seq.png).
|
26
|
+
Keep [the sequence diagram](../docs/zone_picking_diag_seq.plantuml)
|
27
|
+
up-to-date if you change endpoints.
|
28
|
+
|
29
|
+
Note:
|
30
|
+
|
31
|
+
* Several operation types could be linked to a single menu item
|
32
|
+
* If several operator work in a same zone, they’ll see the same move lines but
|
33
|
+
will only posts theirs when unloading their goods, which means that when they
|
34
|
+
scan lines, the backend has to store the user id on the move lines
|
35
|
+
|
36
|
+
Workflow:
|
37
|
+
|
38
|
+
1. The operator scans the zone location with goods to pick (zone location
|
39
|
+
meaning a parent location, not a leaf)
|
40
|
+
2. If the zone contains lines from different picking types, the operator
|
41
|
+
chooses the type to work with
|
42
|
+
3. The client application shows the list of move lines, with an option
|
43
|
+
to choose the sorting of the lines
|
44
|
+
4. The operator selects a line to pick, by scanning one of:
|
45
|
+
|
46
|
+
* location, if only a single move line there; if a location is scanned
|
47
|
+
and it contains several move lines, the view is updated to show only
|
48
|
+
them. The next scan (e.g. a product) will be based on the previous
|
49
|
+
scanned location.
|
50
|
+
* package, if it is linked to a move line. If the package is not linked
|
51
|
+
to an existing move line but can be a replacement for one, the view is
|
52
|
+
updated to show only the fitting move lines. And the user can confirm
|
53
|
+
the change of package by scanning it a second time.
|
54
|
+
* product, if only a single move line matches. Otherwise the view is updated
|
55
|
+
to show only the matching move lines, The next scan (e.g. a location) will
|
56
|
+
be based on the previous product scanned.
|
57
|
+
* lot
|
58
|
+
|
59
|
+
5. The operator scans the destination for the line they scanned, this is where
|
60
|
+
the path splits:
|
61
|
+
|
62
|
+
* they scan a location, in which case the move line's destination is
|
63
|
+
updated with it and the move is done
|
64
|
+
* they scan a package, which becomes the destination package of the move
|
65
|
+
line, the move line is not set to done, its ``qty_done`` is updated
|
66
|
+
and a field ``shopfloor_user_id`` is set to the user; consider the
|
67
|
+
move line is set in a buffer
|
68
|
+
|
69
|
+
6. At any point, from the list of moves, the operator can reach the
|
70
|
+
"unload" screens to unload what they had put into the buffer (scanned a
|
71
|
+
destination package during step 5.). This is optional as they can directly
|
72
|
+
move whole pallets by scanning the destination in step 5.
|
73
|
+
7. The unload screens (similar to those of the Cluster Picking workflow) are
|
74
|
+
used to move what has been put in the buffer:
|
75
|
+
|
76
|
+
* if the original destination of all the lines is unique, screen allows
|
77
|
+
to scan a single destination; they can use a "split" button to go to
|
78
|
+
the line by line screen
|
79
|
+
* if the lines have different destinations, they have to scan the destination
|
80
|
+
package, then scan the destination location, scan the next package and its
|
81
|
+
destination and so on.
|
82
|
+
|
83
|
+
The list of move lines (point 4.) has support functions:
|
84
|
+
|
85
|
+
* Change a lot or pack: if the expected lot is at the very bottom of the
|
86
|
+
location or a stock error forces a user to change lot or pack, user can
|
87
|
+
do it during the picking.
|
88
|
+
* Declare stock out: if a good is in fact not in stock or only partially.
|
89
|
+
Note the move lines may become unavailable or partially unavailable and
|
90
|
+
generate a back-order.
|
91
|
+
|
92
|
+
"""
|
93
|
+
|
94
|
+
_inherit = "base.shopfloor.process"
|
95
|
+
_name = "shopfloor.zone.picking"
|
96
|
+
_usage = "zone_picking"
|
97
|
+
_description = __doc__
|
98
|
+
|
99
|
+
@property
|
100
|
+
def _validation_rules(self):
|
101
|
+
return super()._validation_rules + (
|
102
|
+
# rule to apply, active flag handler
|
103
|
+
(self.ZONE_LOCATION_ID_HEADER_RULE, self._requires_header_zone_picking),
|
104
|
+
(self.PICKING_TYPE_ID_HEADER_RULE, self._requires_header_zone_picking),
|
105
|
+
(self.LINES_ORDER_HEADER_RULE, self._requires_header_zone_picking),
|
106
|
+
)
|
107
|
+
|
108
|
+
def _requires_header_zone_picking(self, request, method):
|
109
|
+
# TODO: maybe we should have a decorator?
|
110
|
+
return method not in ("select_zone", "scan_location")
|
111
|
+
|
112
|
+
ZONE_LOCATION_ID_HEADER_RULE = (
|
113
|
+
# header name, coerce func, ctx handler, mandatory
|
114
|
+
"HTTP_SERVICE_CTX_ZONE_LOCATION_ID",
|
115
|
+
int,
|
116
|
+
"_work_ctx_get_zone_location_id",
|
117
|
+
True,
|
118
|
+
)
|
119
|
+
PICKING_TYPE_ID_HEADER_RULE = (
|
120
|
+
# header name, coerce func, ctx handler, mandatory
|
121
|
+
"HTTP_SERVICE_CTX_PICKING_TYPE_ID",
|
122
|
+
int,
|
123
|
+
"_work_ctx_get_picking_type_id",
|
124
|
+
True,
|
125
|
+
)
|
126
|
+
LINES_ORDER_HEADER_RULE = (
|
127
|
+
# header name, coerce func, ctx handler, mandatory
|
128
|
+
"HTTP_SERVICE_CTX_LINES_ORDER",
|
129
|
+
str,
|
130
|
+
"_work_ctx_get_lines_order",
|
131
|
+
True,
|
132
|
+
)
|
133
|
+
|
134
|
+
def _work_ctx_get_zone_location_id(self, rec_id):
|
135
|
+
return (
|
136
|
+
"current_zone_location",
|
137
|
+
self.env["stock.location"].browse(rec_id).exists(),
|
138
|
+
)
|
139
|
+
|
140
|
+
def _work_ctx_get_picking_type_id(self, rec_id):
|
141
|
+
return (
|
142
|
+
"current_picking_type",
|
143
|
+
self.env["stock.picking.type"].browse(rec_id).exists(),
|
144
|
+
)
|
145
|
+
|
146
|
+
def _work_ctx_get_lines_order(self, order):
|
147
|
+
return "current_lines_order", order
|
148
|
+
|
149
|
+
@property
|
150
|
+
def zone_location(self):
|
151
|
+
return self.work.current_zone_location
|
152
|
+
|
153
|
+
@property
|
154
|
+
def picking_type(self):
|
155
|
+
return getattr(self.work, "current_picking_type", None)
|
156
|
+
|
157
|
+
@property
|
158
|
+
def lines_order(self):
|
159
|
+
return getattr(self.work, "current_lines_order", "priority")
|
160
|
+
|
161
|
+
def _pick_pack_same_time(self):
|
162
|
+
return self.work.menu.pick_pack_same_time
|
163
|
+
|
164
|
+
def _response_for_start(self, message=None):
|
165
|
+
zones = self.work.menu.picking_type_ids.mapped(
|
166
|
+
"default_location_src_id.child_ids"
|
167
|
+
)
|
168
|
+
data = {"zones": self._data_for_select_zone(zones)}
|
169
|
+
buffer = self._find_buffer_move_lines()
|
170
|
+
if buffer:
|
171
|
+
# Some lines can be unloaded, let the user know
|
172
|
+
# The call to the endpoint will need the location and picking id
|
173
|
+
line = first(buffer)
|
174
|
+
picking_type = line.picking_id.picking_type_id
|
175
|
+
zone = line.move_id.location_id
|
176
|
+
data["buffer"] = {
|
177
|
+
"zone_location": self.data.location(zone),
|
178
|
+
"picking_type": self.data.picking_type(picking_type),
|
179
|
+
}
|
180
|
+
return self._response(
|
181
|
+
next_state="start",
|
182
|
+
data=data,
|
183
|
+
message=message,
|
184
|
+
)
|
185
|
+
|
186
|
+
def _response_for_select_picking_type(
|
187
|
+
self, zone_location, picking_types, message=None
|
188
|
+
):
|
189
|
+
return self._response(
|
190
|
+
next_state="select_picking_type",
|
191
|
+
data=self._data_for_select_picking_type(zone_location, picking_types),
|
192
|
+
message=message,
|
193
|
+
)
|
194
|
+
|
195
|
+
def _response_for_select_line(
|
196
|
+
self,
|
197
|
+
move_lines,
|
198
|
+
message=None,
|
199
|
+
popup=None,
|
200
|
+
confirmation_required=False,
|
201
|
+
product=False,
|
202
|
+
sublocation=False,
|
203
|
+
package=False,
|
204
|
+
):
|
205
|
+
if confirmation_required and not message:
|
206
|
+
message = self.msg_store.need_confirmation()
|
207
|
+
data = self._data_for_move_lines(
|
208
|
+
move_lines, product=product, sublocation=sublocation, package=package
|
209
|
+
)
|
210
|
+
data["confirmation_required"] = confirmation_required
|
211
|
+
data["scan_location_or_pack_first"] = self.work.menu.scan_location_or_pack_first
|
212
|
+
return self._response(
|
213
|
+
next_state="select_line",
|
214
|
+
data=data,
|
215
|
+
message=message,
|
216
|
+
popup=popup,
|
217
|
+
)
|
218
|
+
|
219
|
+
def _response_for_set_line_destination(
|
220
|
+
self,
|
221
|
+
move_line,
|
222
|
+
message=None,
|
223
|
+
confirmation_required=False,
|
224
|
+
**kw,
|
225
|
+
):
|
226
|
+
if confirmation_required and not message:
|
227
|
+
message = self.msg_store.need_confirmation()
|
228
|
+
data = self._data_for_move_line(move_line)
|
229
|
+
data["move_line"].update(kw)
|
230
|
+
data["confirmation_required"] = confirmation_required
|
231
|
+
return self._response(
|
232
|
+
next_state="set_line_destination", data=data, message=message
|
233
|
+
)
|
234
|
+
|
235
|
+
def _response_for_zero_check(self, move_line, message=None):
|
236
|
+
data = self._data_for_location(move_line.location_id)
|
237
|
+
data["move_line"] = self.data.move_line(move_line)
|
238
|
+
return self._response(
|
239
|
+
next_state="zero_check",
|
240
|
+
data=data,
|
241
|
+
message=message,
|
242
|
+
)
|
243
|
+
|
244
|
+
def _response_for_change_pack_lot(self, move_line, message=None):
|
245
|
+
return self._response(
|
246
|
+
next_state="change_pack_lot",
|
247
|
+
data=self._data_for_move_line(move_line),
|
248
|
+
message=message,
|
249
|
+
)
|
250
|
+
|
251
|
+
def _response_for_unload_all(
|
252
|
+
self,
|
253
|
+
move_lines,
|
254
|
+
message=None,
|
255
|
+
confirmation_required=False,
|
256
|
+
):
|
257
|
+
if confirmation_required and not message:
|
258
|
+
message = self.msg_store.need_confirmation()
|
259
|
+
data = self._data_for_move_lines(move_lines)
|
260
|
+
data["confirmation_required"] = confirmation_required
|
261
|
+
return self._response(next_state="unload_all", data=data, message=message)
|
262
|
+
|
263
|
+
def _response_for_unload_single(self, move_line, message=None, popup=None):
|
264
|
+
buffer_lines = self._find_buffer_move_lines()
|
265
|
+
completion_info = self._actions_for("completion.info")
|
266
|
+
completion_info_popup = completion_info.popup(buffer_lines)
|
267
|
+
return self._response(
|
268
|
+
next_state="unload_single",
|
269
|
+
data=self._data_for_move_line(move_line),
|
270
|
+
message=message,
|
271
|
+
popup=popup or completion_info_popup,
|
272
|
+
)
|
273
|
+
|
274
|
+
def _response_for_unload_set_destination(
|
275
|
+
self,
|
276
|
+
move_line,
|
277
|
+
message=None,
|
278
|
+
confirmation_required=False,
|
279
|
+
):
|
280
|
+
if confirmation_required and not message:
|
281
|
+
message = self.msg_store.need_confirmation()
|
282
|
+
data = self._data_for_move_line(move_line)
|
283
|
+
data["confirmation_required"] = confirmation_required
|
284
|
+
return self._response(
|
285
|
+
next_state="unload_set_destination", data=data, message=message
|
286
|
+
)
|
287
|
+
|
288
|
+
def _data_for_select_picking_type(self, zone_location, picking_types):
|
289
|
+
data = {
|
290
|
+
"zone_location": self.data.location(zone_location),
|
291
|
+
# available picking types to choose from
|
292
|
+
"picking_types": self.data.picking_types(picking_types),
|
293
|
+
}
|
294
|
+
for datum in data["picking_types"]:
|
295
|
+
picking_type = self.env["stock.picking.type"].browse(datum["id"])
|
296
|
+
zone_lines = self._picking_type_zone_lines(zone_location, picking_type)
|
297
|
+
counters = self._counters_for_zone_lines(zone_lines)
|
298
|
+
datum.update(counters)
|
299
|
+
return data
|
300
|
+
|
301
|
+
def _counters_for_zone_lines(self, zone_lines):
|
302
|
+
return self.search_move_line.counters_for_lines(zone_lines)
|
303
|
+
|
304
|
+
def _picking_type_zone_lines(self, zone_location, picking_type):
|
305
|
+
return self.search_move_line.search_move_lines_by_location(
|
306
|
+
zone_location, picking_type=picking_type
|
307
|
+
)
|
308
|
+
|
309
|
+
def _data_for_move_line(
|
310
|
+
self, move_line, zone_location=None, picking_type=None, **kw
|
311
|
+
):
|
312
|
+
zone_location = zone_location or self.zone_location
|
313
|
+
picking_type = picking_type or self.picking_type
|
314
|
+
line_data = self.data.move_line(move_line, with_picking=True)
|
315
|
+
line_data.update(kw)
|
316
|
+
return {
|
317
|
+
"zone_location": self.data.location(zone_location),
|
318
|
+
"picking_type": self.data.picking_type(picking_type),
|
319
|
+
"move_line": line_data,
|
320
|
+
}
|
321
|
+
|
322
|
+
def _data_for_move_lines(
|
323
|
+
self,
|
324
|
+
move_lines,
|
325
|
+
zone_location=None,
|
326
|
+
picking_type=None,
|
327
|
+
product=None,
|
328
|
+
sublocation=None,
|
329
|
+
package=None,
|
330
|
+
):
|
331
|
+
zone_location = zone_location or self.zone_location
|
332
|
+
picking_type = picking_type or self.picking_type
|
333
|
+
data = {
|
334
|
+
"zone_location": self.data.location(zone_location),
|
335
|
+
"picking_type": self.data.picking_type(picking_type),
|
336
|
+
"move_lines": self.data.move_lines(move_lines, with_picking=True),
|
337
|
+
}
|
338
|
+
if product:
|
339
|
+
data["product"] = self.data.product(product)
|
340
|
+
if sublocation and sublocation != zone_location:
|
341
|
+
data["sublocation"] = self.data.location(sublocation)
|
342
|
+
if package:
|
343
|
+
data["package"] = self.data.package(package)
|
344
|
+
for data_move_line in data["move_lines"]:
|
345
|
+
# TODO: this could be expensive, think about a better way
|
346
|
+
# to retrieve if location will be empty.
|
347
|
+
# Maybe group lines by location and compute only once.
|
348
|
+
move_line = self.env["stock.move.line"].browse(data_move_line["id"])
|
349
|
+
# `location_will_be_empty` flag states if, by processing this move line
|
350
|
+
# and picking the product, the location will be emptied.
|
351
|
+
data_move_line[
|
352
|
+
"location_will_be_empty"
|
353
|
+
] = move_line.location_id.planned_qty_in_location_is_empty(move_line)
|
354
|
+
return data
|
355
|
+
|
356
|
+
def _data_for_location(self, location, zone_location=None, picking_type=None):
|
357
|
+
zone_location = zone_location or self.zone_location
|
358
|
+
picking_type = picking_type or self.picking_type
|
359
|
+
return {
|
360
|
+
"zone_location": self.data.location(zone_location),
|
361
|
+
"picking_type": self.data.picking_type(picking_type),
|
362
|
+
"location": self.data.location(location),
|
363
|
+
}
|
364
|
+
|
365
|
+
def _zone_lines(self, zones):
|
366
|
+
return self._find_location_move_lines(zones)
|
367
|
+
|
368
|
+
def _data_for_select_zone(self, zones):
|
369
|
+
"""Retrieve detailed info for each zone.
|
370
|
+
|
371
|
+
Zone without lines are skipped.
|
372
|
+
Zone with lines will have line counters by operation type.
|
373
|
+
|
374
|
+
:param zones: zone location recordset
|
375
|
+
:return: see _schema_for_select_zone
|
376
|
+
"""
|
377
|
+
res = []
|
378
|
+
for zone in zones:
|
379
|
+
zone_data = self.data.location(zone)
|
380
|
+
zone_lines = self._zone_lines(zone)
|
381
|
+
if not zone_lines:
|
382
|
+
continue
|
383
|
+
lines_by_op_type = defaultdict(list)
|
384
|
+
for line in zone_lines:
|
385
|
+
lines_by_op_type[line.picking_id.picking_type_id].append(line)
|
386
|
+
|
387
|
+
zone_data["operation_types"] = []
|
388
|
+
zone_counters = defaultdict(int)
|
389
|
+
for picking_type, lines in lines_by_op_type.items():
|
390
|
+
op_type_data = self.data.picking_type(picking_type)
|
391
|
+
counters = self._counters_for_zone_lines(lines)
|
392
|
+
op_type_data.update(counters)
|
393
|
+
zone_data["operation_types"].append(op_type_data)
|
394
|
+
for k, v in counters.items():
|
395
|
+
zone_counters[k] += v
|
396
|
+
zone_data.update(zone_counters)
|
397
|
+
res.append(zone_data)
|
398
|
+
return res
|
399
|
+
|
400
|
+
def _find_location_move_lines(
|
401
|
+
self,
|
402
|
+
locations=None,
|
403
|
+
picking_type=None,
|
404
|
+
package=None,
|
405
|
+
product=None,
|
406
|
+
lot=None,
|
407
|
+
match_user=False,
|
408
|
+
enforce_empty_package=False,
|
409
|
+
):
|
410
|
+
"""Find lines that potentially need work in given locations."""
|
411
|
+
return self.search_move_line.search_move_lines_by_location(
|
412
|
+
locations or self.zone_location,
|
413
|
+
picking_type=picking_type or self.picking_type,
|
414
|
+
order=self.lines_order,
|
415
|
+
package=package,
|
416
|
+
product=product,
|
417
|
+
lot=lot,
|
418
|
+
match_user=match_user,
|
419
|
+
enforce_empty_package=enforce_empty_package,
|
420
|
+
)
|
421
|
+
|
422
|
+
def _find_buffer_move_lines_domain(self, dest_package=None):
|
423
|
+
domain = [
|
424
|
+
("picking_id.picking_type_id", "in", self.picking_types.ids),
|
425
|
+
("qty_done", ">", 0),
|
426
|
+
("state", "not in", ("cancel", "done")),
|
427
|
+
("result_package_id", "!=", False),
|
428
|
+
("shopfloor_user_id", "=", self.env.user.id),
|
429
|
+
]
|
430
|
+
if dest_package:
|
431
|
+
domain.append(("result_package_id", "=", dest_package.id))
|
432
|
+
return domain
|
433
|
+
|
434
|
+
def _find_buffer_move_lines(self, dest_package=None):
|
435
|
+
"""Find lines that belongs to the operator's buffer and return them
|
436
|
+
grouped by destination package.
|
437
|
+
"""
|
438
|
+
domain = self._find_buffer_move_lines_domain(dest_package)
|
439
|
+
return (
|
440
|
+
self.env["stock.move.line"]
|
441
|
+
.search(domain)
|
442
|
+
.sorted(self.search_move_line._sort_key_move_lines(self.lines_order))
|
443
|
+
)
|
444
|
+
|
445
|
+
def _group_buffer_move_lines_by_package(self, move_lines):
|
446
|
+
data = {}
|
447
|
+
for move_line in move_lines:
|
448
|
+
data.setdefault(move_line.result_package_id, move_line.browse())
|
449
|
+
data[move_line.result_package_id] |= move_line
|
450
|
+
return data
|
451
|
+
|
452
|
+
def select_zone(self):
|
453
|
+
"""Retrieve all available zones to work with.
|
454
|
+
|
455
|
+
A zone is defined by the first level location below the source location
|
456
|
+
of the operation types linked to the menu.
|
457
|
+
|
458
|
+
The count of lines to process by available operations is computed per each zone.
|
459
|
+
"""
|
460
|
+
return self._response_for_start()
|
461
|
+
|
462
|
+
def scan_location(self, barcode):
|
463
|
+
"""Scan the zone location where the picking should occur
|
464
|
+
|
465
|
+
The location must be a sub-location of one of the picking types'
|
466
|
+
default source locations of the menu.
|
467
|
+
|
468
|
+
Transitions:
|
469
|
+
* start: invalid barcode
|
470
|
+
* select_picking_type: the location is valid, user has to choose a picking type
|
471
|
+
"""
|
472
|
+
search = self._actions_for("search")
|
473
|
+
zone_location = search.location_from_scan(barcode)
|
474
|
+
if not zone_location:
|
475
|
+
return self._response_for_start(message=self.msg_store.no_location_found())
|
476
|
+
if not self.is_src_location_valid(zone_location):
|
477
|
+
return self._response_for_start(
|
478
|
+
message=self.msg_store.location_not_allowed()
|
479
|
+
)
|
480
|
+
move_lines = self._find_location_move_lines(zone_location)
|
481
|
+
if not move_lines:
|
482
|
+
return self._response_for_start(
|
483
|
+
message=self.msg_store.no_lines_to_process()
|
484
|
+
)
|
485
|
+
picking_types = move_lines.picking_id.picking_type_id
|
486
|
+
return self._response_for_select_picking_type(zone_location, picking_types)
|
487
|
+
|
488
|
+
def list_move_lines(self):
|
489
|
+
"""List all move lines to pick, sorted
|
490
|
+
|
491
|
+
Transitions:
|
492
|
+
* select_line: show the list of move lines
|
493
|
+
"""
|
494
|
+
return self._list_move_lines(self.zone_location)
|
495
|
+
|
496
|
+
def _list_move_lines(
|
497
|
+
self, location, product=False, sublocation=False, package=False
|
498
|
+
):
|
499
|
+
move_lines = self._find_location_move_lines(
|
500
|
+
sublocation or location, product=product, package=package, match_user=True
|
501
|
+
)
|
502
|
+
return self._response_for_select_line(
|
503
|
+
move_lines, product=product, sublocation=sublocation, package=package
|
504
|
+
)
|
505
|
+
|
506
|
+
def _scan_source_location(
|
507
|
+
self,
|
508
|
+
barcode,
|
509
|
+
confirmation=False,
|
510
|
+
product_id=False,
|
511
|
+
sublocation=False,
|
512
|
+
package=False,
|
513
|
+
):
|
514
|
+
"""Search a location and find available lines into it."""
|
515
|
+
response = None
|
516
|
+
message = None
|
517
|
+
search = self._actions_for("search")
|
518
|
+
location = search.location_from_scan(barcode)
|
519
|
+
if not location:
|
520
|
+
return response, message
|
521
|
+
|
522
|
+
if not location.is_sublocation_of(self.zone_location):
|
523
|
+
response = self._list_move_lines(self.zone_location)
|
524
|
+
message = self.msg_store.location_not_allowed()
|
525
|
+
return response, message
|
526
|
+
|
527
|
+
if package and package.location_id != location:
|
528
|
+
# Do not search based on a package from a previous location
|
529
|
+
package = False
|
530
|
+
product, lot, packages = self._find_product_in_location(
|
531
|
+
location, product_id, package
|
532
|
+
)
|
533
|
+
if len(packages) > 1:
|
534
|
+
message = self.msg_store.several_packs_in_location(location)
|
535
|
+
elif len(packages) == 1 and self.work.menu.scan_location_or_pack_first:
|
536
|
+
message = self.msg_store.scan_the_package()
|
537
|
+
elif len(product) > 1 and not message:
|
538
|
+
message = self.msg_store.several_products_in_location(location)
|
539
|
+
elif len(lot) > 1 and not message:
|
540
|
+
message = self.msg_store.several_lots_in_location(location)
|
541
|
+
if message:
|
542
|
+
response = self._list_move_lines(
|
543
|
+
location, sublocation=location, package=package
|
544
|
+
)
|
545
|
+
return response, message
|
546
|
+
move_lines = self._find_location_move_lines(
|
547
|
+
location,
|
548
|
+
product=product,
|
549
|
+
lot=lot,
|
550
|
+
package=package,
|
551
|
+
match_user=True,
|
552
|
+
)
|
553
|
+
if move_lines:
|
554
|
+
move_line = first(move_lines)
|
555
|
+
response = self._response_for_set_line_destination(
|
556
|
+
move_line, qty_done=self._get_prefill_qty(move_line)
|
557
|
+
)
|
558
|
+
else:
|
559
|
+
response = self._list_move_lines(self.zone_location)
|
560
|
+
message = self.msg_store.wrong_record(location)
|
561
|
+
return response, message
|
562
|
+
|
563
|
+
def _find_product_in_location(self, location, product_id, package=False):
|
564
|
+
"""Find the prooducts in stock in given location move line in the location."""
|
565
|
+
domain = [("location_id", "=", location.id)]
|
566
|
+
if product_id:
|
567
|
+
domain.append(("product_id", "=", product_id))
|
568
|
+
if package:
|
569
|
+
domain.append(("package_id", "=", package.id))
|
570
|
+
quants = self.env["stock.quant"].search(domain)
|
571
|
+
product = quants.product_id
|
572
|
+
lot = quants.lot_id
|
573
|
+
package = quants.package_id
|
574
|
+
return product, lot, package
|
575
|
+
|
576
|
+
def _scan_source_package(
|
577
|
+
self,
|
578
|
+
barcode,
|
579
|
+
confirmation=False,
|
580
|
+
product_id=False,
|
581
|
+
sublocation=False,
|
582
|
+
package=False,
|
583
|
+
):
|
584
|
+
"""Search a package and find available lines for it.
|
585
|
+
|
586
|
+
First search for lines that have the specific package.
|
587
|
+
If none are found search for lines whose package could be replaced
|
588
|
+
by the one selected and in that case ask for confirmation.
|
589
|
+
"""
|
590
|
+
message = None
|
591
|
+
response = None
|
592
|
+
search = self._actions_for("search")
|
593
|
+
packaging = self._actions_for("packaging")
|
594
|
+
package = search.package_from_scan(barcode)
|
595
|
+
if not package:
|
596
|
+
return response, message
|
597
|
+
if not package.location_id.is_sublocation_of(self.zone_location):
|
598
|
+
# Package is not in an allowed location
|
599
|
+
response = self._list_move_lines(self.zone_location)
|
600
|
+
message = self.msg_store.location_not_allowed()
|
601
|
+
return response, message
|
602
|
+
|
603
|
+
move_lines = self._find_location_move_lines(
|
604
|
+
locations=sublocation, package=package
|
605
|
+
)
|
606
|
+
if move_lines:
|
607
|
+
if packaging.package_has_several_products(package):
|
608
|
+
message = self.msg_store.several_products_in_package(package)
|
609
|
+
if packaging.package_has_several_lots(package):
|
610
|
+
message = self.msg_store.several_lots_in_package(package)
|
611
|
+
if message:
|
612
|
+
return (
|
613
|
+
self._list_move_lines(
|
614
|
+
self.zone_location,
|
615
|
+
sublocation=sublocation,
|
616
|
+
package=package,
|
617
|
+
),
|
618
|
+
message,
|
619
|
+
)
|
620
|
+
move_line = first(move_lines)
|
621
|
+
# Fix me for a package prefill qty is zero ?
|
622
|
+
qty_done = self._get_prefill_qty(move_line)
|
623
|
+
response = self._response_for_set_line_destination(
|
624
|
+
move_line, qty_done=qty_done
|
625
|
+
)
|
626
|
+
return response, message
|
627
|
+
# Check if the package selected can be a substitute on a move line
|
628
|
+
products = package.quant_ids.filtered(lambda q: q.quantity > 0).product_id
|
629
|
+
for product in products:
|
630
|
+
move_lines |= self._find_location_move_lines(
|
631
|
+
locations=package.location_id,
|
632
|
+
product=product,
|
633
|
+
)
|
634
|
+
if move_lines:
|
635
|
+
if not confirmation:
|
636
|
+
message = self.msg_store.package_different_change()
|
637
|
+
response = self._response_for_select_line(
|
638
|
+
move_lines, message, confirmation_required=True
|
639
|
+
)
|
640
|
+
else:
|
641
|
+
change_package_lot = self._actions_for("change.package.lot")
|
642
|
+
response = change_package_lot.change_package(
|
643
|
+
first(move_lines),
|
644
|
+
package,
|
645
|
+
# FIXME we may need to pass the quantity being done
|
646
|
+
self._response_for_set_line_destination,
|
647
|
+
self._response_for_change_pack_lot,
|
648
|
+
)
|
649
|
+
else:
|
650
|
+
response = self._list_move_lines(sublocation or self.zone_location)
|
651
|
+
message = self.msg_store.package_has_no_product_to_take(barcode)
|
652
|
+
return response, message
|
653
|
+
|
654
|
+
def _get_prefill_qty(self, move_line, qty=0):
|
655
|
+
"""Returns the done quantity to use on the selection of a move line.
|
656
|
+
|
657
|
+
Before the introduction of the no prefill quantity parameter on scenarios,
|
658
|
+
when a move line was selected the done quantity was equal to the quantity
|
659
|
+
on the line. This is still the default behaviour.
|
660
|
+
But when the no prefill quantity is set. The quantity done will be set
|
661
|
+
according to the scanned barcode.
|
662
|
+
|
663
|
+
"""
|
664
|
+
if self.work.menu.no_prefill_qty:
|
665
|
+
return qty
|
666
|
+
return move_line.reserved_uom_qty
|
667
|
+
|
668
|
+
def _scan_source_product(
|
669
|
+
self,
|
670
|
+
barcode,
|
671
|
+
confirmation=False,
|
672
|
+
product_id=False,
|
673
|
+
sublocation=False,
|
674
|
+
package=False,
|
675
|
+
):
|
676
|
+
"""Search a product and find available lines for it."""
|
677
|
+
message = None
|
678
|
+
response = None
|
679
|
+
search = self._actions_for("search")
|
680
|
+
product = search.product_from_scan(barcode)
|
681
|
+
packaging = self.env["product.packaging"].browse()
|
682
|
+
if not product:
|
683
|
+
packaging = search.packaging_from_scan(barcode)
|
684
|
+
product = packaging.product_id
|
685
|
+
if not product:
|
686
|
+
return response, message
|
687
|
+
move_lines = self._find_location_move_lines(
|
688
|
+
locations=sublocation,
|
689
|
+
product=product,
|
690
|
+
package=package,
|
691
|
+
enforce_empty_package=self.work.menu.scan_location_or_pack_first,
|
692
|
+
)
|
693
|
+
|
694
|
+
move_lines_with_package_ids = []
|
695
|
+
move_lines_without_package_ids = []
|
696
|
+
if not package and self.work.menu.scan_location_or_pack_first:
|
697
|
+
for move_line in move_lines:
|
698
|
+
if move_line.package_id:
|
699
|
+
move_lines_with_package_ids.append(move_line.id)
|
700
|
+
else:
|
701
|
+
move_lines_without_package_ids.append(move_line.id)
|
702
|
+
move_lines = move_lines.browse(move_lines_without_package_ids)
|
703
|
+
|
704
|
+
if len(move_lines.location_id) > 1:
|
705
|
+
message = self.msg_store.several_move_in_different_location()
|
706
|
+
elif len(move_lines.lot_id) > 1:
|
707
|
+
message = self.msg_store.several_move_with_different_lot()
|
708
|
+
if message:
|
709
|
+
response = self._list_move_lines(
|
710
|
+
self.zone_location, product, sublocation=sublocation, package=package
|
711
|
+
)
|
712
|
+
elif move_lines:
|
713
|
+
move_line = first(move_lines)
|
714
|
+
qty_done = self._get_prefill_qty(move_line, qty=(packaging.qty or 1.0))
|
715
|
+
response = self._response_for_set_line_destination(
|
716
|
+
move_line, qty_done=qty_done
|
717
|
+
)
|
718
|
+
else:
|
719
|
+
response = self._list_move_lines(
|
720
|
+
sublocation or self.zone_location,
|
721
|
+
sublocation=sublocation,
|
722
|
+
package=package,
|
723
|
+
)
|
724
|
+
if move_lines_with_package_ids:
|
725
|
+
message = self.msg_store.product_not_unitary_in_package_scan_package()
|
726
|
+
else:
|
727
|
+
message = self.msg_store.product_not_found_in_pickings()
|
728
|
+
return response, message
|
729
|
+
|
730
|
+
def _scan_source_lot(
|
731
|
+
self,
|
732
|
+
barcode,
|
733
|
+
confirmation=False,
|
734
|
+
product_id=False,
|
735
|
+
sublocation=False,
|
736
|
+
package=False,
|
737
|
+
):
|
738
|
+
"""Search a lot and find available lines for it."""
|
739
|
+
message = None
|
740
|
+
response = None
|
741
|
+
search = self._actions_for("search")
|
742
|
+
products = self.env["product.product"].browse(product_id)
|
743
|
+
# Could get several lots from different products, check each of them
|
744
|
+
lots = search.lot_from_scan(barcode, products=products, limit=None)
|
745
|
+
if not lots:
|
746
|
+
return response, message
|
747
|
+
move_lines_with_package_ids = []
|
748
|
+
move_lines_without_package_ids = []
|
749
|
+
for lot in lots:
|
750
|
+
move_lines = self._find_location_move_lines(
|
751
|
+
locations=sublocation, lot=lot, package=package
|
752
|
+
)
|
753
|
+
if not move_lines:
|
754
|
+
continue
|
755
|
+
if not package and self.work.menu.scan_location_or_pack_first:
|
756
|
+
for move_line in move_lines:
|
757
|
+
if move_line.package_id:
|
758
|
+
move_lines_with_package_ids.append(move_line.id)
|
759
|
+
else:
|
760
|
+
move_lines_without_package_ids.append(move_line.id)
|
761
|
+
move_lines = move_lines.browse(move_lines_without_package_ids)
|
762
|
+
|
763
|
+
if len(move_lines.location_id) > 1:
|
764
|
+
message = self.msg_store.several_move_in_different_location()
|
765
|
+
response = self.list_move_lines()
|
766
|
+
else:
|
767
|
+
move_line = first(move_lines)
|
768
|
+
qty_done = self._get_prefill_qty(move_line, qty=1.0)
|
769
|
+
response = self._response_for_set_line_destination(
|
770
|
+
move_line, qty_done=qty_done
|
771
|
+
)
|
772
|
+
return response, message
|
773
|
+
message = self.msg_store.lot_not_found_in_pickings()
|
774
|
+
response = self._list_move_lines(
|
775
|
+
sublocation or self.zone_location, package=package
|
776
|
+
)
|
777
|
+
if move_lines_with_package_ids:
|
778
|
+
message = self.msg_store.lot_mixed_package_scan_package()
|
779
|
+
else:
|
780
|
+
message = self.msg_store.lot_not_found_in_pickings()
|
781
|
+
return response, message
|
782
|
+
|
783
|
+
def scan_source(
|
784
|
+
self,
|
785
|
+
barcode,
|
786
|
+
confirmation=False,
|
787
|
+
product_id=None,
|
788
|
+
sublocation_id=None,
|
789
|
+
package_id=None,
|
790
|
+
):
|
791
|
+
"""Select a move line or narrow the list of move lines
|
792
|
+
|
793
|
+
When the barcode is a location and we can unambiguously know which move
|
794
|
+
line is picked (the quants in the location has one product/lot/package,
|
795
|
+
matching a single move line), then the move line is selected.
|
796
|
+
Otherwise, the list of move lines is refreshed with a filter on the
|
797
|
+
scanned location, showing the move lines that have this location as
|
798
|
+
source.
|
799
|
+
|
800
|
+
When the barcode is a package, a product or a lot, the first matching
|
801
|
+
line is selected.
|
802
|
+
|
803
|
+
A selected line goes to the next screen to select the destination
|
804
|
+
location or package.
|
805
|
+
|
806
|
+
If a product is passed to the function the search on move line will
|
807
|
+
be filtered based on it as well.
|
808
|
+
|
809
|
+
And if a sublocation_id is passed the search on move line will be restriced
|
810
|
+
to it.
|
811
|
+
|
812
|
+
Transitions:
|
813
|
+
* select_line: barcode not found or narrow the list on a location
|
814
|
+
* set_line_destination: a line has been selected for picking
|
815
|
+
"""
|
816
|
+
# select corresponding move line from barcode (location, package, product, lot)
|
817
|
+
sublocation = (
|
818
|
+
self.env["stock.location"].browse(sublocation_id).exists()
|
819
|
+
if sublocation_id
|
820
|
+
else self.env["stock.location"]
|
821
|
+
)
|
822
|
+
selected_package = (
|
823
|
+
self.env["stock.quant.package"].browse(package_id).exists()
|
824
|
+
if package_id
|
825
|
+
else self.env["stock.quant.package"]
|
826
|
+
)
|
827
|
+
handlers = (
|
828
|
+
# search by location 1st
|
829
|
+
self._scan_source_location,
|
830
|
+
# then by package
|
831
|
+
self._scan_source_package,
|
832
|
+
) + (
|
833
|
+
# if first scan location or pack option is not set
|
834
|
+
# or the sublocation has already been scanned
|
835
|
+
(
|
836
|
+
# by product
|
837
|
+
self._scan_source_product,
|
838
|
+
# then by lot
|
839
|
+
self._scan_source_lot,
|
840
|
+
)
|
841
|
+
if not self.work.menu.scan_location_or_pack_first
|
842
|
+
or sublocation_id
|
843
|
+
or selected_package
|
844
|
+
else ()
|
845
|
+
)
|
846
|
+
for handler in handlers:
|
847
|
+
response, message = handler(
|
848
|
+
barcode,
|
849
|
+
confirmation=confirmation,
|
850
|
+
product_id=product_id,
|
851
|
+
sublocation=sublocation,
|
852
|
+
package=selected_package,
|
853
|
+
)
|
854
|
+
if response:
|
855
|
+
return self._response(base_response=response, message=message)
|
856
|
+
response = self._list_move_lines(
|
857
|
+
self.zone_location, sublocation=sublocation, package=selected_package
|
858
|
+
)
|
859
|
+
return self._response(
|
860
|
+
base_response=response, message=self.msg_store.barcode_not_found()
|
861
|
+
)
|
862
|
+
|
863
|
+
def _set_destination_location(self, move_line, quantity, confirmation, location):
|
864
|
+
location_changed = False
|
865
|
+
response = None
|
866
|
+
|
867
|
+
# A valid location is a sub-location of the original destination, or a
|
868
|
+
# any sub-location of the picking type's default destination location
|
869
|
+
# if `confirmation is True
|
870
|
+
# Ask confirmation to the user if the scanned location is not in the
|
871
|
+
# expected ones but is valid (in picking type's default destination)
|
872
|
+
if not self.is_dest_location_valid(move_line.move_id, location):
|
873
|
+
response = self._response_for_set_line_destination(
|
874
|
+
move_line,
|
875
|
+
message=self.msg_store.dest_location_not_allowed(),
|
876
|
+
qty_done=quantity,
|
877
|
+
)
|
878
|
+
return (location_changed, response)
|
879
|
+
|
880
|
+
if not confirmation and self.is_dest_location_to_confirm(
|
881
|
+
move_line.location_dest_id, location
|
882
|
+
):
|
883
|
+
response = self._response_for_set_line_destination(
|
884
|
+
move_line,
|
885
|
+
message=self.msg_store.confirm_location_changed(
|
886
|
+
move_line.location_dest_id, location
|
887
|
+
),
|
888
|
+
confirmation_required=True,
|
889
|
+
qty_done=quantity,
|
890
|
+
)
|
891
|
+
return (location_changed, response)
|
892
|
+
|
893
|
+
# If no destination package
|
894
|
+
if not move_line.result_package_id:
|
895
|
+
response = self._response_for_set_line_destination(
|
896
|
+
move_line,
|
897
|
+
message=self.msg_store.dest_package_required(),
|
898
|
+
qty_done=quantity,
|
899
|
+
)
|
900
|
+
return (location_changed, response)
|
901
|
+
# destination location set to the scanned one
|
902
|
+
self._write_destination_on_lines(move_line, location)
|
903
|
+
stock = self._actions_for("stock")
|
904
|
+
try:
|
905
|
+
stock.mark_move_line_as_picked(move_line, quantity, check_user=True)
|
906
|
+
except ConcurentWorkOnTransfer as error:
|
907
|
+
response = self._response_for_set_line_destination(
|
908
|
+
move_line,
|
909
|
+
message={
|
910
|
+
"message_type": "error",
|
911
|
+
"body": str(error),
|
912
|
+
},
|
913
|
+
qty_done=quantity,
|
914
|
+
)
|
915
|
+
return (location_changed, response)
|
916
|
+
stock.validate_moves(move_line.move_id)
|
917
|
+
location_changed = True
|
918
|
+
# Zero check
|
919
|
+
zero_check = self.picking_type.shopfloor_zero_check
|
920
|
+
if zero_check and move_line.location_id.planned_qty_in_location_is_empty():
|
921
|
+
response = self._response_for_zero_check(move_line)
|
922
|
+
return (location_changed, response)
|
923
|
+
|
924
|
+
def _is_package_empty(self, package):
|
925
|
+
return not bool(package.quant_ids)
|
926
|
+
|
927
|
+
def _is_package_already_used(self, package):
|
928
|
+
# Deprecated, use planned_move_line_ids instead
|
929
|
+
return bool(package.planned_move_line_ids)
|
930
|
+
|
931
|
+
def _move_line_compare_qty(self, move_line, qty):
|
932
|
+
rounding = move_line.product_uom_id.rounding
|
933
|
+
return float_compare(
|
934
|
+
qty, move_line.reserved_uom_qty, precision_rounding=rounding
|
935
|
+
)
|
936
|
+
|
937
|
+
def _move_line_full_qty(self, move_line, qty):
|
938
|
+
rounding = move_line.product_uom_id.rounding
|
939
|
+
return float_is_zero(
|
940
|
+
move_line.reserved_uom_qty - qty, precision_rounding=rounding
|
941
|
+
)
|
942
|
+
|
943
|
+
def _set_destination_package(self, move_line, quantity, package):
|
944
|
+
package_changed = False
|
945
|
+
response = None
|
946
|
+
# A valid package is:
|
947
|
+
# * an empty package
|
948
|
+
# * not used as destination for another move line
|
949
|
+
if not self._is_package_empty(package):
|
950
|
+
response = self._response_for_set_line_destination(
|
951
|
+
move_line,
|
952
|
+
message=self.msg_store.package_not_empty(package),
|
953
|
+
qty_done=quantity,
|
954
|
+
)
|
955
|
+
return (package_changed, response)
|
956
|
+
multiple_move_allowed = self.work.menu.multiple_move_single_pack
|
957
|
+
if package.planned_move_line_ids and not multiple_move_allowed:
|
958
|
+
response = self._response_for_set_line_destination(
|
959
|
+
move_line,
|
960
|
+
message=self.msg_store.package_already_used(package),
|
961
|
+
qty_done=quantity,
|
962
|
+
)
|
963
|
+
return (package_changed, response)
|
964
|
+
# the quantity done is set to the passed quantity
|
965
|
+
# but if we move a partial qty, we need to split the move line
|
966
|
+
compare = self._move_line_compare_qty(move_line, quantity)
|
967
|
+
qty_greater = compare == 1
|
968
|
+
if qty_greater:
|
969
|
+
response = self._response_for_set_line_destination(
|
970
|
+
move_line,
|
971
|
+
message=self.msg_store.unable_to_pick_more(move_line.reserved_uom_qty),
|
972
|
+
qty_done=quantity,
|
973
|
+
)
|
974
|
+
return (package_changed, response)
|
975
|
+
stock = self._actions_for("stock")
|
976
|
+
try:
|
977
|
+
stock.mark_move_line_as_picked(
|
978
|
+
move_line, quantity, package, check_user=True
|
979
|
+
)
|
980
|
+
except ConcurentWorkOnTransfer as error:
|
981
|
+
response = self._response_for_set_line_destination(
|
982
|
+
move_line,
|
983
|
+
message={
|
984
|
+
"message_type": "error",
|
985
|
+
"body": str(error),
|
986
|
+
},
|
987
|
+
qty_done=quantity,
|
988
|
+
)
|
989
|
+
return (package_changed, response)
|
990
|
+
package_changed = True
|
991
|
+
# Zero check
|
992
|
+
zero_check = self.picking_type.shopfloor_zero_check
|
993
|
+
if zero_check and move_line.location_id.planned_qty_in_location_is_empty():
|
994
|
+
response = self._response_for_zero_check(move_line)
|
995
|
+
return (package_changed, response)
|
996
|
+
|
997
|
+
def _set_destination_update_quantity(self, move_line, quantity, barcode):
|
998
|
+
"""Handle the done quantity increment on set_destination end point."""
|
999
|
+
response = None
|
1000
|
+
if not self.work.menu.no_prefill_qty:
|
1001
|
+
return response
|
1002
|
+
search = self._actions_for("search")
|
1003
|
+
# Handle barcode of product or packaging
|
1004
|
+
product = search.product_from_scan(barcode)
|
1005
|
+
packaging = self.env["product.packaging"].browse()
|
1006
|
+
if not product:
|
1007
|
+
packaging = search.packaging_from_scan(barcode)
|
1008
|
+
product = packaging.product_id
|
1009
|
+
if product and move_line.product_id == product:
|
1010
|
+
quantity += packaging.qty or 1.0
|
1011
|
+
response = self._response_for_set_line_destination(
|
1012
|
+
move_line, qty_done=quantity
|
1013
|
+
)
|
1014
|
+
return response
|
1015
|
+
# Handle barcode of a lot
|
1016
|
+
lot = search.lot_from_scan(barcode)
|
1017
|
+
if lot and move_line.lot_id == lot:
|
1018
|
+
quantity += 1.0
|
1019
|
+
response = self._response_for_set_line_destination(
|
1020
|
+
move_line, qty_done=quantity
|
1021
|
+
)
|
1022
|
+
return response
|
1023
|
+
return response
|
1024
|
+
|
1025
|
+
# flake8: noqa: C901
|
1026
|
+
def set_destination(
|
1027
|
+
self,
|
1028
|
+
move_line_id,
|
1029
|
+
barcode,
|
1030
|
+
quantity,
|
1031
|
+
confirmation=False,
|
1032
|
+
):
|
1033
|
+
"""Set a destination location (and done) or a destination package (in buffer)
|
1034
|
+
|
1035
|
+
When a line is picked, it can either:
|
1036
|
+
|
1037
|
+
* be moved directly to a destination location, typically a pallet
|
1038
|
+
* be moved to a destination package, that we'll call buffer in the docstrings
|
1039
|
+
|
1040
|
+
When the barcode is a valid location, actions on the move line:
|
1041
|
+
|
1042
|
+
* destination location set to the scanned one
|
1043
|
+
* the quantity done is set to the passed quantity
|
1044
|
+
* if the move has other move lines, it is split to have only this move line
|
1045
|
+
* set to done (without backorder)
|
1046
|
+
|
1047
|
+
A valid location is a sub-location of the original destination, or a
|
1048
|
+
sub-location of the picking type's default destination location if
|
1049
|
+
``confirmation`` is True.
|
1050
|
+
|
1051
|
+
When the barcode is a valid package, actions on the move line:
|
1052
|
+
|
1053
|
+
* destination package is set to the scanned one
|
1054
|
+
* the quantity done is set to the passed quantity
|
1055
|
+
* the field ``shopfloor_user_id`` is updated with the current user
|
1056
|
+
|
1057
|
+
Those fields will be used to identify which move lines are in the buffer.
|
1058
|
+
|
1059
|
+
A valid package is:
|
1060
|
+
|
1061
|
+
* an empty package
|
1062
|
+
* not used as destination for another move line
|
1063
|
+
|
1064
|
+
With the addition of the no prefill quantity parameter this endpoint can also
|
1065
|
+
be used to change the done quantity on the move line before setting a
|
1066
|
+
destination.
|
1067
|
+
|
1068
|
+
When the barcode is the product (or its packaging) or the lot on the line:
|
1069
|
+
* The done quantity is incremented by one or the packaging quantity.
|
1070
|
+
|
1071
|
+
Transitions:
|
1072
|
+
* select_line: destination has been set, showing the next lines to pick
|
1073
|
+
* zero_check: if the option is active and if the quantity of product
|
1074
|
+
moved is 0 in the source location after the move (beware: at this
|
1075
|
+
point the product we put in the buffer is still considered to be in
|
1076
|
+
the source location, so we have to compute the source location's
|
1077
|
+
quantity - qty_done).
|
1078
|
+
* set_line_destination: the scanned location is invalid, user has to
|
1079
|
+
scan another one
|
1080
|
+
* set_line_destination+confirmation_required: the scanned location is not
|
1081
|
+
in the expected one but is valid (in picking type's default destination)
|
1082
|
+
"""
|
1083
|
+
move_line = self.env["stock.move.line"].browse(move_line_id)
|
1084
|
+
if not move_line.exists():
|
1085
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
1086
|
+
|
1087
|
+
pkg_moved = False
|
1088
|
+
search = self._actions_for("search")
|
1089
|
+
accept_only_package = not self._move_line_full_qty(move_line, quantity)
|
1090
|
+
|
1091
|
+
response = self._set_destination_update_quantity(move_line, quantity, barcode)
|
1092
|
+
if response:
|
1093
|
+
return response
|
1094
|
+
|
1095
|
+
if quantity <= 0:
|
1096
|
+
message = self.msg_store.picking_zero_quantity()
|
1097
|
+
return self._response_for_set_line_destination(
|
1098
|
+
move_line,
|
1099
|
+
message=message,
|
1100
|
+
qty_done=self._get_prefill_qty(move_line, qty=0),
|
1101
|
+
)
|
1102
|
+
|
1103
|
+
extra_message = ""
|
1104
|
+
if not accept_only_package:
|
1105
|
+
# When the barcode is a location
|
1106
|
+
location = search.location_from_scan(barcode)
|
1107
|
+
if location:
|
1108
|
+
if self._pick_pack_same_time():
|
1109
|
+
(
|
1110
|
+
good_for_packing,
|
1111
|
+
message,
|
1112
|
+
) = self._handle_pick_pack_same_time_for_location(move_line)
|
1113
|
+
# TODO: we should append the msg instead.
|
1114
|
+
# To achieve this, we should refactor `response.message` to a list
|
1115
|
+
# or, to no break backward compat, we could add `extra_messages`
|
1116
|
+
# to allow backend to send a main message and N additional messages.
|
1117
|
+
extra_message = message
|
1118
|
+
if not good_for_packing:
|
1119
|
+
return self._response_for_set_line_destination(
|
1120
|
+
move_line, message=message, qty_done=quantity
|
1121
|
+
)
|
1122
|
+
pkg_moved, response = self._set_destination_location(
|
1123
|
+
move_line,
|
1124
|
+
quantity,
|
1125
|
+
confirmation,
|
1126
|
+
location,
|
1127
|
+
)
|
1128
|
+
if response:
|
1129
|
+
if extra_message:
|
1130
|
+
if response.get("message"):
|
1131
|
+
response["message"]["body"] += "\n" + extra_message["body"]
|
1132
|
+
else:
|
1133
|
+
response["message"] = extra_message
|
1134
|
+
return response
|
1135
|
+
|
1136
|
+
# When the barcode is a package
|
1137
|
+
package = search.package_from_scan(barcode)
|
1138
|
+
if package:
|
1139
|
+
if self._pick_pack_same_time():
|
1140
|
+
(
|
1141
|
+
good_for_packing,
|
1142
|
+
message,
|
1143
|
+
) = self._handle_pick_pack_same_time_for_package(move_line, package)
|
1144
|
+
if not good_for_packing:
|
1145
|
+
return self._response_for_set_line_destination(
|
1146
|
+
move_line, message=message, qty_done=quantity
|
1147
|
+
)
|
1148
|
+
location = move_line.location_dest_id
|
1149
|
+
pkg_moved, response = self._set_destination_package(
|
1150
|
+
move_line, quantity, package
|
1151
|
+
)
|
1152
|
+
if response:
|
1153
|
+
return response
|
1154
|
+
|
1155
|
+
message = None
|
1156
|
+
|
1157
|
+
if not pkg_moved and not package:
|
1158
|
+
if accept_only_package:
|
1159
|
+
message = self.msg_store.package_not_found_for_barcode(barcode)
|
1160
|
+
else:
|
1161
|
+
# we don't know if user wanted to scan a location or a package
|
1162
|
+
message = self.msg_store.barcode_not_found()
|
1163
|
+
return self._response_for_set_line_destination(
|
1164
|
+
move_line, message=message, qty_done=quantity
|
1165
|
+
)
|
1166
|
+
|
1167
|
+
if pkg_moved:
|
1168
|
+
message = self.msg_store.confirm_pack_moved()
|
1169
|
+
if extra_message:
|
1170
|
+
message["body"] += "\n" + extra_message["body"]
|
1171
|
+
|
1172
|
+
# Process the next line
|
1173
|
+
response = self.list_move_lines()
|
1174
|
+
return self._response(base_response=response, message=message)
|
1175
|
+
|
1176
|
+
def _handle_pick_pack_same_time_for_location(self, move_line):
|
1177
|
+
"""Automatically put product in carrier-specific package.
|
1178
|
+
|
1179
|
+
:param move_line: current move line to process
|
1180
|
+
:return: tuple like ($succes_flag, $success_or_failure_message)
|
1181
|
+
"""
|
1182
|
+
good_for_packing = False
|
1183
|
+
message = ""
|
1184
|
+
picking = move_line.picking_id
|
1185
|
+
carrier = picking.ship_carrier_id or picking.carrier_id
|
1186
|
+
if carrier:
|
1187
|
+
actions = self._actions_for("packaging")
|
1188
|
+
pkg = actions.create_delivery_package(carrier)
|
1189
|
+
move_line.write({"result_package_id": pkg.id})
|
1190
|
+
message = self.msg_store.goods_packed_in(pkg)
|
1191
|
+
good_for_packing = True
|
1192
|
+
else:
|
1193
|
+
message = self.msg_store.picking_without_carrier_cannot_pack(picking)
|
1194
|
+
return good_for_packing, message
|
1195
|
+
|
1196
|
+
def _handle_pick_pack_same_time_for_package(self, move_line, package):
|
1197
|
+
"""Validate package for packing at the same time.
|
1198
|
+
|
1199
|
+
:param move_line: current move line to process
|
1200
|
+
:param package: package to validate
|
1201
|
+
:return: tuple like ($succes_flag, $success_or_failure_message)
|
1202
|
+
"""
|
1203
|
+
good_for_packing = False
|
1204
|
+
message = None
|
1205
|
+
picking = move_line.picking_id
|
1206
|
+
carrier = picking.ship_carrier_id or picking.carrier_id
|
1207
|
+
if carrier:
|
1208
|
+
actions = self._actions_for("packaging")
|
1209
|
+
if actions.packaging_valid_for_carrier(
|
1210
|
+
package.product_packaging_id, carrier
|
1211
|
+
):
|
1212
|
+
good_for_packing = True
|
1213
|
+
else:
|
1214
|
+
message = self.msg_store.packaging_invalid_for_carrier(
|
1215
|
+
package.product_packaging_id, carrier
|
1216
|
+
)
|
1217
|
+
else:
|
1218
|
+
message = self.msg_store.picking_without_carrier_cannot_pack(picking)
|
1219
|
+
return good_for_packing, message
|
1220
|
+
|
1221
|
+
def is_zero(self, move_line_id, zero):
|
1222
|
+
"""Confirm or not if the source location of a move has zero qty
|
1223
|
+
|
1224
|
+
If the user confirms there is zero quantity, it means the stock was
|
1225
|
+
correct and there is nothing to do. If the user says "no", a draft
|
1226
|
+
empty inventory is created for the product (with lot if tracked).
|
1227
|
+
|
1228
|
+
Transitions:
|
1229
|
+
* select_line: whether the user confirms or not the location is empty,
|
1230
|
+
go back to the picking of lines
|
1231
|
+
"""
|
1232
|
+
move_line = self.env["stock.move.line"].browse(move_line_id)
|
1233
|
+
if not move_line.exists():
|
1234
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
1235
|
+
return self.list_move_lines()
|
1236
|
+
|
1237
|
+
def _domain_stock_issue_unlink_lines(self, move_line):
|
1238
|
+
# Since we have not enough stock, delete the move lines, which will
|
1239
|
+
# in turn unreserve the moves. The moves lines we delete are those
|
1240
|
+
# in the same location, and not yet started.
|
1241
|
+
# The goal is to prevent the same operator to declare twice the same
|
1242
|
+
# stock issue for the same product/lot/package.
|
1243
|
+
move = move_line.move_id
|
1244
|
+
lot = move_line.lot_id
|
1245
|
+
package = move_line.package_id
|
1246
|
+
location = move_line.location_id
|
1247
|
+
domain = [
|
1248
|
+
("location_id", "=", location.id),
|
1249
|
+
("product_id", "=", move.product_id.id),
|
1250
|
+
("package_id", "=", package.id),
|
1251
|
+
("lot_id", "=", lot.id),
|
1252
|
+
("state", "not in", ("cancel", "done")),
|
1253
|
+
("qty_done", "=", 0),
|
1254
|
+
]
|
1255
|
+
return domain
|
1256
|
+
|
1257
|
+
def stock_issue(self, move_line_id):
|
1258
|
+
"""Declare a stock issue for a line
|
1259
|
+
|
1260
|
+
After errors in the stock, the user cannot take all the products
|
1261
|
+
because there is physically not enough goods. The move line is deleted
|
1262
|
+
(unreserve), and an inventory is created to reduce the quantity in the
|
1263
|
+
source location to prevent future errors until a correction. Beware:
|
1264
|
+
the quantity already reserved and having a qty_done set on other lines
|
1265
|
+
in the same location should remain reserved so the inventory's quantity
|
1266
|
+
must be set to the total of qty_done of other lines.
|
1267
|
+
|
1268
|
+
The other lines not yet picked (no qty_done) in the same location for
|
1269
|
+
the same product, lot, package are unreserved as well (moves lines
|
1270
|
+
deleted, which unreserve their quantity on the move).
|
1271
|
+
|
1272
|
+
A second inventory is created in draft to have someone do an inventory
|
1273
|
+
check.
|
1274
|
+
|
1275
|
+
At the end, it tries to reserve the goods again, and if the current
|
1276
|
+
line could be reserved in the current zone location, it transitions
|
1277
|
+
directly to the screen to set the destination.
|
1278
|
+
|
1279
|
+
Transitions:
|
1280
|
+
* select_line: go back to the picking of lines for the next ones (nothing
|
1281
|
+
could be reserved as replacement)
|
1282
|
+
* set_line_destination: something could be reserved instead of the original
|
1283
|
+
move line
|
1284
|
+
"""
|
1285
|
+
move_line = self.env["stock.move.line"].browse(move_line_id)
|
1286
|
+
if not move_line.exists():
|
1287
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
1288
|
+
inventory = self._actions_for("inventory")
|
1289
|
+
# create a draft inventory for a user to check
|
1290
|
+
inventory.create_control_stock(
|
1291
|
+
move_line.location_id,
|
1292
|
+
move_line.product_id,
|
1293
|
+
move_line.package_id,
|
1294
|
+
move_line.lot_id,
|
1295
|
+
)
|
1296
|
+
move = move_line.move_id
|
1297
|
+
lot = move_line.lot_id
|
1298
|
+
package = move_line.package_id
|
1299
|
+
location = move_line.location_id
|
1300
|
+
|
1301
|
+
# unreserve every lines for the same product/lot in the same location and
|
1302
|
+
# not done yet, so the same user doesn't have to declare 2 times the
|
1303
|
+
# stock issue for the same thing!
|
1304
|
+
domain = self._domain_stock_issue_unlink_lines(move_line)
|
1305
|
+
unreserve_move_lines = move_line | self.env["stock.move.line"].search(domain)
|
1306
|
+
unreserve_moves = unreserve_move_lines.mapped("move_id").sorted()
|
1307
|
+
unreserve_move_lines.unlink()
|
1308
|
+
|
1309
|
+
# Then, create an inventory with just enough qty so the other assigned
|
1310
|
+
# move lines for the same product in other batches and the other move lines
|
1311
|
+
# already picked stay assigned.
|
1312
|
+
inventory.create_stock_issue(move, location, package, lot)
|
1313
|
+
|
1314
|
+
# try to reassign the moves in case we have stock in another location
|
1315
|
+
unreserve_moves._action_assign()
|
1316
|
+
|
1317
|
+
if move.move_line_ids:
|
1318
|
+
return self._response_for_set_line_destination(move.move_line_ids[0])
|
1319
|
+
return self.list_move_lines()
|
1320
|
+
|
1321
|
+
def change_pack_lot(self, move_line_id, barcode):
|
1322
|
+
"""Change the source package or the lot of a move line
|
1323
|
+
|
1324
|
+
If the expected lot or package is at the very bottom of the location or
|
1325
|
+
a stock error forces a user to change lot or package, user can change the
|
1326
|
+
package or lot of the current move line.
|
1327
|
+
|
1328
|
+
If the pack or lot was not supposed to be in the source location,
|
1329
|
+
a draft inventory is created to have this checked.
|
1330
|
+
|
1331
|
+
Transitions:
|
1332
|
+
* change_pack_lot: the barcode scanned is invalid or change could not be done
|
1333
|
+
* set_line_destination: the package / lot has been changed, can be
|
1334
|
+
moved to destination now
|
1335
|
+
* select_line: if the move line does not exist anymore
|
1336
|
+
"""
|
1337
|
+
move_line = self.env["stock.move.line"].browse(move_line_id)
|
1338
|
+
if not move_line.exists():
|
1339
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
1340
|
+
search = self._actions_for("search")
|
1341
|
+
# pre-configured callable used to generate the response as the
|
1342
|
+
# change.package.lot component is not aware of the needed response type
|
1343
|
+
# and related parameters for zone picking scenario
|
1344
|
+
response_ok_func = functools.partial(self._response_for_set_line_destination)
|
1345
|
+
response_error_func = functools.partial(self._response_for_change_pack_lot)
|
1346
|
+
response = None
|
1347
|
+
change_package_lot = self._actions_for("change.package.lot")
|
1348
|
+
# handle lot
|
1349
|
+
lot = search.lot_from_scan(barcode, products=move_line.product_id)
|
1350
|
+
if lot:
|
1351
|
+
response = change_package_lot.change_lot(
|
1352
|
+
move_line, lot, response_ok_func, response_error_func
|
1353
|
+
)
|
1354
|
+
# handle package
|
1355
|
+
package = search.package_from_scan(barcode)
|
1356
|
+
if package:
|
1357
|
+
return change_package_lot.change_package(
|
1358
|
+
move_line, package, response_ok_func, response_error_func
|
1359
|
+
)
|
1360
|
+
# if the response is not an error, we check the move_line status
|
1361
|
+
# to adapt the response ('set_line_destination' or 'select_line')
|
1362
|
+
# TODO not sure to understand how 'move_line' could not exist here?
|
1363
|
+
if response and response["message"]["message_type"] == "success":
|
1364
|
+
# TODO adapt the response based on the move_line.exists()
|
1365
|
+
if move_line.exists():
|
1366
|
+
return response
|
1367
|
+
return response
|
1368
|
+
|
1369
|
+
return self._response_for_change_pack_lot(
|
1370
|
+
move_line,
|
1371
|
+
message=self.msg_store.no_package_or_lot_for_barcode(barcode),
|
1372
|
+
)
|
1373
|
+
|
1374
|
+
def prepare_unload(self):
|
1375
|
+
"""Initiate the unloading of the buffer
|
1376
|
+
|
1377
|
+
The buffer is composed of move lines:
|
1378
|
+
|
1379
|
+
* in the current zone location and picking type
|
1380
|
+
* not done or canceled
|
1381
|
+
* with a qty_done > 0
|
1382
|
+
* have a destination package
|
1383
|
+
* with ``shopfloor_user_id`` equal to the current user
|
1384
|
+
|
1385
|
+
The lines are grouped by their destination package. The destination
|
1386
|
+
package is what is shown on the screen (with their content, which is
|
1387
|
+
the move lines with the package as destination), and this is what is
|
1388
|
+
passed along in the ``package_id`` parameters in the unload methods.
|
1389
|
+
|
1390
|
+
It goes to different screens depending if there is only one move line,
|
1391
|
+
or if all the move lines have the same destination or not.
|
1392
|
+
|
1393
|
+
Transitions:
|
1394
|
+
* unload_single: move lines have different destinations, return data
|
1395
|
+
for the next destination package
|
1396
|
+
* unload_set_destination: there is only one move line in the buffer
|
1397
|
+
* unload_all: the move lines in the buffer all have the same
|
1398
|
+
destination location
|
1399
|
+
* select_line: no remaining move lines in buffer
|
1400
|
+
"""
|
1401
|
+
move_lines = self._find_buffer_move_lines()
|
1402
|
+
location_dest = move_lines.mapped("location_dest_id")
|
1403
|
+
if len(move_lines) == 1:
|
1404
|
+
return self._response_for_unload_set_destination(move_lines)
|
1405
|
+
elif len(move_lines) > 1 and len(location_dest) == 1:
|
1406
|
+
return self._response_for_unload_all(move_lines)
|
1407
|
+
elif len(move_lines) > 1 and len(location_dest) > 1:
|
1408
|
+
return self._response_for_unload_single(first(move_lines))
|
1409
|
+
return self.list_move_lines()
|
1410
|
+
|
1411
|
+
def _set_destination_all_response(self, buffer_lines, message=None):
|
1412
|
+
if buffer_lines:
|
1413
|
+
return self._response_for_unload_all(buffer_lines, message=message)
|
1414
|
+
move_lines = self._find_location_move_lines()
|
1415
|
+
if move_lines:
|
1416
|
+
return self._response_for_select_line(move_lines, message=message)
|
1417
|
+
return self._response_for_start(message=message)
|
1418
|
+
|
1419
|
+
def set_destination_all(self, barcode, confirmation=False):
|
1420
|
+
"""Set the destination for all the lines in the buffer
|
1421
|
+
|
1422
|
+
Look in ``prepare_unload`` for the definition of the buffer.
|
1423
|
+
|
1424
|
+
This method must be used only if all the buffer's move lines which have
|
1425
|
+
a destination package, qty done > 0, and have the same destination
|
1426
|
+
location.
|
1427
|
+
|
1428
|
+
A scanned location outside of the destination location of the operation
|
1429
|
+
type is invalid.
|
1430
|
+
|
1431
|
+
The move lines are then set to done, without backorders.
|
1432
|
+
|
1433
|
+
Transitions:
|
1434
|
+
* unload_all: the scanned destination is invalid, user has to
|
1435
|
+
scan another one
|
1436
|
+
* unload_all + confirmation: the scanned location is not in the
|
1437
|
+
expected one but is valid (in picking type's default destination)
|
1438
|
+
* select_line: no remaining move lines in buffer
|
1439
|
+
"""
|
1440
|
+
search = self._actions_for("search")
|
1441
|
+
location = search.location_from_scan(barcode)
|
1442
|
+
message = None
|
1443
|
+
buffer_lines = self._find_buffer_move_lines()
|
1444
|
+
if location:
|
1445
|
+
error = None
|
1446
|
+
location_dest = buffer_lines.mapped("location_dest_id")
|
1447
|
+
# check if move lines share the same destination
|
1448
|
+
if len(location_dest) != 1:
|
1449
|
+
error = self.msg_store.lines_different_dest_location()
|
1450
|
+
# check if the scanned location is allowed
|
1451
|
+
moves = buffer_lines.mapped("move_id")
|
1452
|
+
if not self.is_dest_location_valid(moves, location):
|
1453
|
+
error = self.msg_store.location_not_allowed()
|
1454
|
+
if error:
|
1455
|
+
return self._set_destination_all_response(buffer_lines, message=error)
|
1456
|
+
# check if the destination location is not the expected one
|
1457
|
+
# - OK if the scanned destination is a child of the current
|
1458
|
+
# destination set on buffer lines
|
1459
|
+
# - To confirm if the scanned destination is not a child of the
|
1460
|
+
# current destination set on buffer lines
|
1461
|
+
if not confirmation and self.is_dest_location_to_confirm(
|
1462
|
+
buffer_lines.location_dest_id, location
|
1463
|
+
):
|
1464
|
+
return self._response_for_unload_all(
|
1465
|
+
buffer_lines,
|
1466
|
+
message=self.msg_store.confirm_location_changed(
|
1467
|
+
first(buffer_lines.location_dest_id), location
|
1468
|
+
),
|
1469
|
+
confirmation_required=True,
|
1470
|
+
)
|
1471
|
+
# the scanned location is still valid, use it
|
1472
|
+
self._write_destination_on_lines(buffer_lines, location)
|
1473
|
+
stock = self._actions_for("stock")
|
1474
|
+
stock.validate_moves(moves)
|
1475
|
+
message = self.msg_store.buffer_complete()
|
1476
|
+
buffer_lines = self._find_buffer_move_lines()
|
1477
|
+
else:
|
1478
|
+
message = self.msg_store.no_location_found()
|
1479
|
+
return self._set_destination_all_response(buffer_lines, message=message)
|
1480
|
+
|
1481
|
+
def _write_destination_on_lines(self, lines, location):
|
1482
|
+
self._lock_lines(lines)
|
1483
|
+
lines.location_dest_id = location
|
1484
|
+
lines.package_level_id.location_dest_id = location
|
1485
|
+
if self.work.menu.unload_package_at_destination:
|
1486
|
+
lines.result_package_id = False
|
1487
|
+
|
1488
|
+
def unload_split(self):
|
1489
|
+
"""Indicates that now the buffer must be treated line per line
|
1490
|
+
|
1491
|
+
Called from a button, users decides to handle destinations one by one.
|
1492
|
+
Even if the move lines to unload all have the same destination.
|
1493
|
+
|
1494
|
+
Look in ``prepare_unload`` for the definition of the buffer.
|
1495
|
+
|
1496
|
+
Transitions:
|
1497
|
+
* unload_single: more than one remaining line in the buffer
|
1498
|
+
* unload_set_destination: there is only one remaining line in the buffer
|
1499
|
+
* select_line: no remaining move lines in buffer
|
1500
|
+
"""
|
1501
|
+
buffer_lines = self._find_buffer_move_lines()
|
1502
|
+
# more than one remaining move line in the buffer
|
1503
|
+
if len(buffer_lines) > 1:
|
1504
|
+
return self._response_for_unload_single(first(buffer_lines))
|
1505
|
+
# only one move line to process in the buffer
|
1506
|
+
elif len(buffer_lines) == 1:
|
1507
|
+
return self._response_for_unload_set_destination(first(buffer_lines))
|
1508
|
+
# no remaining move lines in buffer
|
1509
|
+
move_lines = self._find_location_move_lines()
|
1510
|
+
return self._response_for_select_line(
|
1511
|
+
move_lines,
|
1512
|
+
message=self.msg_store.buffer_complete(),
|
1513
|
+
)
|
1514
|
+
|
1515
|
+
def _unload_response(self, unload_single_message=None):
|
1516
|
+
"""Prepare the right response depending on the move lines to process."""
|
1517
|
+
# if there are still move lines to process from the buffer
|
1518
|
+
move_lines = self._find_buffer_move_lines()
|
1519
|
+
if move_lines:
|
1520
|
+
return self._response_for_unload_single(
|
1521
|
+
first(move_lines),
|
1522
|
+
message=unload_single_message,
|
1523
|
+
)
|
1524
|
+
# if there are still move lines to process from the picking type
|
1525
|
+
# => buffer complete!
|
1526
|
+
move_lines = self._find_location_move_lines()
|
1527
|
+
if move_lines:
|
1528
|
+
return self._response_for_select_line(
|
1529
|
+
move_lines,
|
1530
|
+
message=self.msg_store.buffer_complete(),
|
1531
|
+
)
|
1532
|
+
# no more move lines to process from the current picking type
|
1533
|
+
# => picking type complete!
|
1534
|
+
return self._response_for_start(
|
1535
|
+
message=self.msg_store.picking_type_complete(self.picking_type)
|
1536
|
+
)
|
1537
|
+
|
1538
|
+
def unload_scan_pack(self, package_id, barcode):
|
1539
|
+
"""Scan the destination package to check user moves the good one
|
1540
|
+
|
1541
|
+
The "unload_single" screen proposes a package (which has been
|
1542
|
+
previously been set as destination package of lines of the buffer).
|
1543
|
+
The user has to scan the package to validate they took the good one.
|
1544
|
+
|
1545
|
+
Transitions:
|
1546
|
+
* unload_single: the scanned barcode does not match the package
|
1547
|
+
* unload_set_destination: the scanned barcode matches the package
|
1548
|
+
* select_line: no remaining move lines in buffer
|
1549
|
+
* start: no remaining move lines in picking type
|
1550
|
+
"""
|
1551
|
+
package = self.env["stock.quant.package"].browse(package_id)
|
1552
|
+
if not package.exists():
|
1553
|
+
return self._unload_response(
|
1554
|
+
unload_single_message=self.msg_store.record_not_found(),
|
1555
|
+
)
|
1556
|
+
search = self._actions_for("search")
|
1557
|
+
scanned_package = search.package_from_scan(barcode)
|
1558
|
+
# the scanned barcode matches the package
|
1559
|
+
if scanned_package == package:
|
1560
|
+
move_lines = self._find_buffer_move_lines(dest_package=scanned_package)
|
1561
|
+
if move_lines:
|
1562
|
+
return self._response_for_unload_set_destination(first(move_lines))
|
1563
|
+
return self._unload_response(
|
1564
|
+
unload_single_message=self.msg_store.barcode_no_match(package.name),
|
1565
|
+
)
|
1566
|
+
|
1567
|
+
def _lock_lines(self, lines):
|
1568
|
+
"""Lock move lines"""
|
1569
|
+
self._actions_for("lock").for_update(lines)
|
1570
|
+
|
1571
|
+
def unload_set_destination(self, package_id, barcode, confirmation=False):
|
1572
|
+
"""Scan the final destination for move lines in the buffer with the
|
1573
|
+
destination package
|
1574
|
+
|
1575
|
+
All the move lines in the buffer with the package_id as destination
|
1576
|
+
package are updated with the scanned location.
|
1577
|
+
|
1578
|
+
The move lines are then set to done, without backorders.
|
1579
|
+
|
1580
|
+
Look in ``prepare_unload`` for the definition of the buffer.
|
1581
|
+
|
1582
|
+
Transitions:
|
1583
|
+
* unload_single: buffer still contains move lines, unload the next package
|
1584
|
+
* unload_set_destination: the scanned location is invalid, user has to
|
1585
|
+
scan another one
|
1586
|
+
* unload_set_destination+confirmation_required: the scanned location is not
|
1587
|
+
in the expected one but is valid (in picking type's default destination)
|
1588
|
+
* select_line: no remaining move lines in buffer
|
1589
|
+
* start: no remaining move lines to process in the picking type
|
1590
|
+
"""
|
1591
|
+
package = self.env["stock.quant.package"].browse(package_id)
|
1592
|
+
buffer_lines = self._find_buffer_move_lines(dest_package=package)
|
1593
|
+
if not package.exists() or not buffer_lines:
|
1594
|
+
move_lines = self._find_location_move_lines()
|
1595
|
+
return self._response_for_select_line(
|
1596
|
+
move_lines,
|
1597
|
+
message=self.msg_store.record_not_found(),
|
1598
|
+
)
|
1599
|
+
search = self._actions_for("search")
|
1600
|
+
location = search.location_from_scan(barcode)
|
1601
|
+
if location:
|
1602
|
+
moves = buffer_lines.mapped("move_id")
|
1603
|
+
if not self.is_dest_location_valid(moves, location):
|
1604
|
+
return self._response_for_unload_set_destination(
|
1605
|
+
first(buffer_lines),
|
1606
|
+
message=self.msg_store.dest_location_not_allowed(),
|
1607
|
+
)
|
1608
|
+
# check if the destination location is not the expected one
|
1609
|
+
# - OK if the scanned destination is a child of the current
|
1610
|
+
# destination set on buffer lines
|
1611
|
+
# - To confirm if the scanned destination is not a child of the
|
1612
|
+
# current destination set on buffer lines
|
1613
|
+
if not confirmation and self.is_dest_location_to_confirm(
|
1614
|
+
buffer_lines.location_dest_id, location
|
1615
|
+
):
|
1616
|
+
return self._response_for_unload_set_destination(
|
1617
|
+
first(buffer_lines),
|
1618
|
+
message=self.msg_store.confirm_location_changed(
|
1619
|
+
first(buffer_lines.location_dest_id), location
|
1620
|
+
),
|
1621
|
+
confirmation_required=True,
|
1622
|
+
)
|
1623
|
+
# the scanned location is valid, use it
|
1624
|
+
self._write_destination_on_lines(buffer_lines, location)
|
1625
|
+
# set lines to done + refresh buffer lines (should be empty)
|
1626
|
+
# split move lines to a backorder move
|
1627
|
+
# if quantity is not fully satisfied
|
1628
|
+
for move in moves:
|
1629
|
+
move.split_other_move_lines(buffer_lines & move.move_line_ids)
|
1630
|
+
|
1631
|
+
stock = self._actions_for("stock")
|
1632
|
+
stock.validate_moves(moves)
|
1633
|
+
buffer_lines = self._find_buffer_move_lines()
|
1634
|
+
|
1635
|
+
if buffer_lines:
|
1636
|
+
# TODO: return success message if line has been processed
|
1637
|
+
return self._response_for_unload_single(first(buffer_lines))
|
1638
|
+
move_lines = self._find_location_move_lines()
|
1639
|
+
if move_lines:
|
1640
|
+
return self._response_for_select_line(
|
1641
|
+
move_lines,
|
1642
|
+
message=self.msg_store.buffer_complete(),
|
1643
|
+
)
|
1644
|
+
return self._response_for_start(
|
1645
|
+
message=self.msg_store.picking_type_complete(self.picking_type)
|
1646
|
+
)
|
1647
|
+
# TODO: when we have no lines here
|
1648
|
+
# we should not redirect to `unload_set_destination`
|
1649
|
+
# because we'll have nothing to display (currently the UI is broken).
|
1650
|
+
return self._response_for_unload_set_destination(
|
1651
|
+
first(buffer_lines),
|
1652
|
+
message=self.msg_store.no_location_found(),
|
1653
|
+
)
|
1654
|
+
|
1655
|
+
|
1656
|
+
class ShopfloorZonePickingValidator(Component):
|
1657
|
+
"""Validators for the Zone Picking endpoints"""
|
1658
|
+
|
1659
|
+
_inherit = "base.shopfloor.validator"
|
1660
|
+
_name = "shopfloor.zone_picking.validator"
|
1661
|
+
_usage = "zone_picking.validator"
|
1662
|
+
|
1663
|
+
def select_zone(self):
|
1664
|
+
return {}
|
1665
|
+
|
1666
|
+
def scan_location(self):
|
1667
|
+
return {"barcode": {"required": True, "type": "string"}}
|
1668
|
+
|
1669
|
+
def list_move_lines(self):
|
1670
|
+
return {
|
1671
|
+
"barcode": {"required": False, "nullable": True, "type": "string"},
|
1672
|
+
}
|
1673
|
+
|
1674
|
+
def scan_source(self):
|
1675
|
+
return {
|
1676
|
+
"barcode": {"required": False, "nullable": True, "type": "string"},
|
1677
|
+
"confirmation": {"type": "boolean", "nullable": True, "required": False},
|
1678
|
+
"product_id": {"required": False, "nullable": True, "type": "integer"},
|
1679
|
+
"sublocation_id": {"required": False, "nullable": True, "type": "integer"},
|
1680
|
+
"package_id": {"required": False, "nullable": True, "type": "integer"},
|
1681
|
+
}
|
1682
|
+
|
1683
|
+
def set_destination(self):
|
1684
|
+
return {
|
1685
|
+
"move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1686
|
+
"barcode": {"required": False, "nullable": True, "type": "string"},
|
1687
|
+
"quantity": {
|
1688
|
+
"coerce": to_float,
|
1689
|
+
"required": True,
|
1690
|
+
"type": "float",
|
1691
|
+
},
|
1692
|
+
"confirmation": {"type": "boolean", "nullable": True, "required": False},
|
1693
|
+
}
|
1694
|
+
|
1695
|
+
def is_zero(self):
|
1696
|
+
return {
|
1697
|
+
"move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1698
|
+
"zero": {"coerce": to_bool, "required": True, "type": "boolean"},
|
1699
|
+
}
|
1700
|
+
|
1701
|
+
def stock_issue(self):
|
1702
|
+
return {
|
1703
|
+
"move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1704
|
+
}
|
1705
|
+
|
1706
|
+
def change_pack_lot(self):
|
1707
|
+
return {
|
1708
|
+
"move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1709
|
+
"barcode": {"required": False, "nullable": True, "type": "string"},
|
1710
|
+
}
|
1711
|
+
|
1712
|
+
def prepare_unload(self):
|
1713
|
+
return {}
|
1714
|
+
|
1715
|
+
def set_destination_all(self):
|
1716
|
+
return {
|
1717
|
+
"barcode": {"required": False, "nullable": True, "type": "string"},
|
1718
|
+
"confirmation": {"type": "boolean", "nullable": True, "required": False},
|
1719
|
+
}
|
1720
|
+
|
1721
|
+
def unload_split(self):
|
1722
|
+
return {}
|
1723
|
+
|
1724
|
+
def unload_scan_pack(self):
|
1725
|
+
return {
|
1726
|
+
"package_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1727
|
+
"barcode": {"required": False, "nullable": True, "type": "string"},
|
1728
|
+
}
|
1729
|
+
|
1730
|
+
def unload_set_destination(self):
|
1731
|
+
return {
|
1732
|
+
"package_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1733
|
+
"barcode": {"required": False, "nullable": True, "type": "string"},
|
1734
|
+
"confirmation": {"type": "boolean", "nullable": True, "required": False},
|
1735
|
+
}
|
1736
|
+
|
1737
|
+
|
1738
|
+
class ShopfloorZonePickingValidatorResponse(Component):
|
1739
|
+
"""Validators for the Zone Picking endpoints responses"""
|
1740
|
+
|
1741
|
+
_inherit = "base.shopfloor.validator.response"
|
1742
|
+
_name = "shopfloor.zone_picking.validator.response"
|
1743
|
+
_usage = "zone_picking.validator.response"
|
1744
|
+
|
1745
|
+
def _states(self):
|
1746
|
+
"""List of possible next states
|
1747
|
+
|
1748
|
+
With the schema of the data send to the client to transition
|
1749
|
+
to the next state.
|
1750
|
+
"""
|
1751
|
+
return {
|
1752
|
+
"start": self._schema_for_select_zone,
|
1753
|
+
"select_picking_type": self._schema_for_select_picking_type,
|
1754
|
+
"select_line": self._schema_for_move_lines_empty_location,
|
1755
|
+
"set_line_destination": self._schema_for_move_line,
|
1756
|
+
"zero_check": self._schema_for_zero_check,
|
1757
|
+
"change_pack_lot": self._schema_for_move_line,
|
1758
|
+
"unload_all": self._schema_for_move_lines,
|
1759
|
+
"unload_single": self._schema_for_move_line,
|
1760
|
+
"unload_set_destination": self._schema_for_move_line,
|
1761
|
+
}
|
1762
|
+
|
1763
|
+
def select_zone(self):
|
1764
|
+
return self._response_schema(next_states={"start"})
|
1765
|
+
|
1766
|
+
def scan_location(self):
|
1767
|
+
return self._response_schema(next_states={"start", "select_picking_type"})
|
1768
|
+
|
1769
|
+
def list_move_lines(self):
|
1770
|
+
return self._response_schema(next_states={"select_line"})
|
1771
|
+
|
1772
|
+
def scan_source(self):
|
1773
|
+
return self._response_schema(
|
1774
|
+
next_states={"select_line", "set_line_destination"}
|
1775
|
+
)
|
1776
|
+
|
1777
|
+
def set_destination(self):
|
1778
|
+
return self._response_schema(
|
1779
|
+
next_states={"select_line", "set_line_destination", "zero_check"}
|
1780
|
+
)
|
1781
|
+
|
1782
|
+
def is_zero(self):
|
1783
|
+
return self._response_schema(next_states={"select_line"})
|
1784
|
+
|
1785
|
+
def stock_issue(self):
|
1786
|
+
return self._response_schema(
|
1787
|
+
next_states={"select_line", "set_line_destination"}
|
1788
|
+
)
|
1789
|
+
|
1790
|
+
def change_pack_lot(self):
|
1791
|
+
return self._response_schema(
|
1792
|
+
next_states={"change_pack_lot", "set_line_destination", "select_line"}
|
1793
|
+
)
|
1794
|
+
|
1795
|
+
def prepare_unload(self):
|
1796
|
+
return self._response_schema(
|
1797
|
+
next_states={
|
1798
|
+
"unload_all",
|
1799
|
+
"unload_single",
|
1800
|
+
"unload_set_destination",
|
1801
|
+
"select_line",
|
1802
|
+
}
|
1803
|
+
)
|
1804
|
+
|
1805
|
+
def set_destination_all(self):
|
1806
|
+
return self._response_schema(next_states={"unload_all", "select_line"})
|
1807
|
+
|
1808
|
+
def unload_split(self):
|
1809
|
+
return self._response_schema(
|
1810
|
+
next_states={"unload_single", "unload_set_destination", "select_line"}
|
1811
|
+
)
|
1812
|
+
|
1813
|
+
def unload_scan_pack(self):
|
1814
|
+
return self._response_schema(
|
1815
|
+
next_states={
|
1816
|
+
"unload_single",
|
1817
|
+
"unload_set_destination",
|
1818
|
+
"select_line",
|
1819
|
+
"start",
|
1820
|
+
}
|
1821
|
+
)
|
1822
|
+
|
1823
|
+
def unload_set_destination(self):
|
1824
|
+
return self._response_schema(
|
1825
|
+
next_states={"unload_single", "unload_set_destination", "select_line"}
|
1826
|
+
)
|
1827
|
+
|
1828
|
+
@property
|
1829
|
+
def _schema_for_select_zone(self):
|
1830
|
+
zone_schema = self.schemas.location()
|
1831
|
+
picking_type_schema = self.schemas.picking_type()
|
1832
|
+
picking_type_schema.update(self._schema_for_zone_line_counters)
|
1833
|
+
zone_schema["operation_types"] = self.schemas._schema_list_of(
|
1834
|
+
picking_type_schema
|
1835
|
+
)
|
1836
|
+
zone_schema.update(self._schema_for_zone_line_counters)
|
1837
|
+
zone_schema = {
|
1838
|
+
"zones": self.schemas._schema_list_of(zone_schema),
|
1839
|
+
"buffer": {
|
1840
|
+
"type": "dict",
|
1841
|
+
"nullable": False,
|
1842
|
+
"required": False,
|
1843
|
+
"schema": {
|
1844
|
+
"zone_location": self.schemas._schema_dict_of(
|
1845
|
+
self.schemas.location(), nullable=False, required=False
|
1846
|
+
),
|
1847
|
+
"picking_type": self.schemas._schema_dict_of(
|
1848
|
+
self.schemas.picking_type(), nullable=False, required=False
|
1849
|
+
),
|
1850
|
+
},
|
1851
|
+
},
|
1852
|
+
}
|
1853
|
+
return zone_schema
|
1854
|
+
|
1855
|
+
@property
|
1856
|
+
def _schema_for_zone_line_counters(self):
|
1857
|
+
return self.schemas.move_lines_counters()
|
1858
|
+
|
1859
|
+
@property
|
1860
|
+
def _schema_for_select_picking_type(self):
|
1861
|
+
picking_type = self.schemas.picking_type()
|
1862
|
+
picking_type.update(self._schema_for_zone_line_counters)
|
1863
|
+
schema = {
|
1864
|
+
"zone_location": self.schemas._schema_dict_of(self.schemas.location()),
|
1865
|
+
"picking_types": self.schemas._schema_list_of(picking_type),
|
1866
|
+
}
|
1867
|
+
return schema
|
1868
|
+
|
1869
|
+
@property
|
1870
|
+
def _schema_for_move_line(self):
|
1871
|
+
schema = {
|
1872
|
+
"zone_location": self.schemas._schema_dict_of(self.schemas.location()),
|
1873
|
+
"picking_type": self.schemas._schema_dict_of(self.schemas.picking_type()),
|
1874
|
+
"move_line": self.schemas._schema_dict_of(
|
1875
|
+
self.schemas.move_line(with_picking=True)
|
1876
|
+
),
|
1877
|
+
"confirmation_required": {
|
1878
|
+
"type": "boolean",
|
1879
|
+
"nullable": True,
|
1880
|
+
"required": False,
|
1881
|
+
},
|
1882
|
+
"product_id": {
|
1883
|
+
"type": "integer",
|
1884
|
+
"nullable": True,
|
1885
|
+
"required": False,
|
1886
|
+
},
|
1887
|
+
}
|
1888
|
+
return schema
|
1889
|
+
|
1890
|
+
@property
|
1891
|
+
def _schema_for_move_lines(self):
|
1892
|
+
schema = {
|
1893
|
+
"zone_location": self.schemas._schema_dict_of(self.schemas.location()),
|
1894
|
+
"picking_type": self.schemas._schema_dict_of(self.schemas.picking_type()),
|
1895
|
+
"move_lines": self.schemas._schema_list_of(
|
1896
|
+
self.schemas.move_line(with_picking=True)
|
1897
|
+
),
|
1898
|
+
"confirmation_required": {
|
1899
|
+
"type": "boolean",
|
1900
|
+
"nullable": True,
|
1901
|
+
"required": False,
|
1902
|
+
},
|
1903
|
+
"product": self.schemas._schema_dict_of(
|
1904
|
+
self.schemas.product(), required=False
|
1905
|
+
),
|
1906
|
+
"sublocation": self.schemas._schema_dict_of(
|
1907
|
+
self.schemas.location(), required=False
|
1908
|
+
),
|
1909
|
+
"package": self.schemas._schema_dict_of(
|
1910
|
+
self.schemas.package(), required=False
|
1911
|
+
),
|
1912
|
+
}
|
1913
|
+
return schema
|
1914
|
+
|
1915
|
+
@property
|
1916
|
+
def _schema_for_move_lines_empty_location(self):
|
1917
|
+
schema = self._schema_for_move_lines
|
1918
|
+
schema["move_lines"]["schema"]["schema"]["location_will_be_empty"] = {
|
1919
|
+
"type": "boolean",
|
1920
|
+
"nullable": False,
|
1921
|
+
"required": True,
|
1922
|
+
}
|
1923
|
+
schema["scan_location_or_pack_first"] = {
|
1924
|
+
"type": "boolean",
|
1925
|
+
"nullable": False,
|
1926
|
+
"required": True,
|
1927
|
+
}
|
1928
|
+
return schema
|
1929
|
+
|
1930
|
+
@property
|
1931
|
+
def _schema_for_zero_check(self):
|
1932
|
+
schema = {
|
1933
|
+
"zone_location": self.schemas._schema_dict_of(self.schemas.location()),
|
1934
|
+
"picking_type": self.schemas._schema_dict_of(self.schemas.picking_type()),
|
1935
|
+
"location": self.schemas._schema_dict_of(self.schemas.location()),
|
1936
|
+
"move_line": self.schemas._schema_dict_of(self.schemas.move_line()),
|
1937
|
+
}
|
1938
|
+
return schema
|