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,1194 @@
|
|
1
|
+
# Copyright 2020-2021 Camptocamp SA (http://www.camptocamp.com)
|
2
|
+
# Copyright 2020-2022 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
|
+
from odoo import _, fields
|
6
|
+
|
7
|
+
from odoo.addons.base_rest.components.service import to_int
|
8
|
+
from odoo.addons.component.core import Component
|
9
|
+
|
10
|
+
from ..utils import to_float
|
11
|
+
|
12
|
+
# NOTE for the implementation: share several similarities with the "cluster
|
13
|
+
# picking" scenario
|
14
|
+
|
15
|
+
|
16
|
+
# TODO add picking and package content in package level?
|
17
|
+
|
18
|
+
|
19
|
+
class LocationContentTransfer(Component):
|
20
|
+
"""
|
21
|
+
Methods for the Location Content Transfer Process
|
22
|
+
|
23
|
+
Move the full content of a location to one or more locations.
|
24
|
+
|
25
|
+
Generally used to move a pallet with multiple boxes to either:
|
26
|
+
|
27
|
+
* 1 destination location, unloading the full pallet
|
28
|
+
* To multiple destination locations, unloading one product/lot per
|
29
|
+
locations
|
30
|
+
* To multiple destination locations, unloading one product/lot per
|
31
|
+
locations and then unloading all remaining product/lot to a single final
|
32
|
+
destination
|
33
|
+
|
34
|
+
The move lines must exist beforehand, the workflow only moves them.
|
35
|
+
|
36
|
+
Expected:
|
37
|
+
|
38
|
+
* All move lines and package level have a destination set, and are done.
|
39
|
+
|
40
|
+
2 complementary actions are possible on the screens allowing to move a line:
|
41
|
+
|
42
|
+
* Declare a stock out for a product or package (nothing found in the
|
43
|
+
location)
|
44
|
+
* Skip to the next line (will be asked again at the end)
|
45
|
+
|
46
|
+
Flow Diagram: https://www.draw.io/#G1qRenBcezk50ggIazDuu2qOfkTsoIAxXP
|
47
|
+
"""
|
48
|
+
|
49
|
+
_inherit = "base.shopfloor.process"
|
50
|
+
_name = "shopfloor.location.content.transfer"
|
51
|
+
_usage = "location_content_transfer"
|
52
|
+
_description = __doc__
|
53
|
+
|
54
|
+
_advisory_lock_find_work = "location_content_transfer_find_work"
|
55
|
+
|
56
|
+
def _response_for_start(self, message=None, popup=None):
|
57
|
+
"""Transition to the 'start' or 'get_work' state
|
58
|
+
|
59
|
+
The switch to 'get_work' is done if the option is enabled on the scenario
|
60
|
+
"""
|
61
|
+
if self.work.menu.allow_get_work:
|
62
|
+
return self._response(
|
63
|
+
next_state="get_work", data={}, message=message, popup=popup
|
64
|
+
)
|
65
|
+
return self._response(next_state="scan_location", message=message, popup=popup)
|
66
|
+
|
67
|
+
def _response_for_scan_location(self, location=None, message=None):
|
68
|
+
"""Transition to the 'scan_location' state
|
69
|
+
|
70
|
+
If location is set, the client will display information on that location
|
71
|
+
and only accept this specific location to be scanned.
|
72
|
+
"""
|
73
|
+
data = {}
|
74
|
+
if location:
|
75
|
+
data["location"] = self.data.location(location)
|
76
|
+
return self._response(
|
77
|
+
next_state="scan_location",
|
78
|
+
data=data,
|
79
|
+
message=message,
|
80
|
+
)
|
81
|
+
|
82
|
+
def _response_for_scan_destination_all(
|
83
|
+
self, pickings, message=None, confirmation_required=False
|
84
|
+
):
|
85
|
+
"""Transition to the 'scan_destination_all' state
|
86
|
+
|
87
|
+
The client screen shows a summary of all the lines and packages
|
88
|
+
to move to a single destination.
|
89
|
+
|
90
|
+
If `confirmation_required` is set,
|
91
|
+
the client will ask to scan again the destination
|
92
|
+
"""
|
93
|
+
data = self._data_content_all_for_location(pickings=pickings)
|
94
|
+
data["confirmation_required"] = confirmation_required
|
95
|
+
if confirmation_required and not message:
|
96
|
+
message = self.msg_store.need_confirmation()
|
97
|
+
return self._response(
|
98
|
+
next_state="scan_destination_all", data=data, message=message
|
99
|
+
)
|
100
|
+
|
101
|
+
def _response_for_start_single(self, pickings, message=None, popup=None):
|
102
|
+
"""Transition to the 'start_single' state
|
103
|
+
|
104
|
+
The client screen shows details of the package level or move line to move.
|
105
|
+
"""
|
106
|
+
location = pickings.mapped("location_id")
|
107
|
+
next_content = self._next_content(pickings)
|
108
|
+
if not next_content:
|
109
|
+
# TODO test (no more lines)
|
110
|
+
return self._response_for_start(message=message, popup=popup)
|
111
|
+
return self._response(
|
112
|
+
next_state="start_single",
|
113
|
+
data=self._data_content_line_for_location(location, next_content),
|
114
|
+
message=message,
|
115
|
+
popup=popup,
|
116
|
+
)
|
117
|
+
|
118
|
+
def _response_for_scan_destination(
|
119
|
+
self, location, next_content, message=None, confirmation_required=False
|
120
|
+
):
|
121
|
+
"""Transition to the 'scan_destination' state
|
122
|
+
|
123
|
+
The client screen shows details of the package level or move line to move.
|
124
|
+
"""
|
125
|
+
data = self._data_content_line_for_location(location, next_content)
|
126
|
+
data["confirmation_required"] = confirmation_required
|
127
|
+
if confirmation_required and not message:
|
128
|
+
message = self.msg_store.need_confirmation()
|
129
|
+
return self._response(next_state="scan_destination", data=data, message=message)
|
130
|
+
|
131
|
+
def _data_content_all_for_location(self, pickings):
|
132
|
+
sorter = self._actions_for("location_content_transfer.sorter")
|
133
|
+
sorter.feed_pickings(pickings)
|
134
|
+
lines = sorter.move_lines()
|
135
|
+
package_levels = sorter.package_levels()
|
136
|
+
location = pickings.mapped("move_line_ids.location_id")
|
137
|
+
assert len(location) == 1, "There should be only one src location at this stage"
|
138
|
+
return {
|
139
|
+
"location": self.data.location(location),
|
140
|
+
"move_lines": self.data.move_lines(lines),
|
141
|
+
"package_levels": self.data.package_levels(package_levels),
|
142
|
+
}
|
143
|
+
|
144
|
+
def _data_content_line_for_location(self, location, next_content):
|
145
|
+
assert next_content._name in ("stock.move.line", "stock.package_level")
|
146
|
+
line_data = (
|
147
|
+
self.data.move_line(next_content)
|
148
|
+
if next_content._name == "stock.move.line"
|
149
|
+
else None
|
150
|
+
)
|
151
|
+
level_data = (
|
152
|
+
self.data.package_level(next_content)
|
153
|
+
if next_content._name == "stock.package_level"
|
154
|
+
else None
|
155
|
+
)
|
156
|
+
return {"move_line": line_data, "package_level": level_data}
|
157
|
+
|
158
|
+
def _next_content(self, pickings):
|
159
|
+
sorter = self._actions_for("location_content_transfer.sorter")
|
160
|
+
sorter.feed_pickings(pickings)
|
161
|
+
try:
|
162
|
+
next_content = next(sorter)
|
163
|
+
except StopIteration:
|
164
|
+
# TODO set picking to done
|
165
|
+
return None
|
166
|
+
return next_content
|
167
|
+
|
168
|
+
def _router_single_or_all_destination(self, pickings, message=None):
|
169
|
+
location_dest = pickings.mapped("move_line_ids.location_dest_id")
|
170
|
+
location_src = pickings.mapped("move_line_ids.location_id")
|
171
|
+
if len(location_dest) == len(location_src) == 1:
|
172
|
+
return self._response_for_scan_destination_all(pickings, message=message)
|
173
|
+
else:
|
174
|
+
return self._response_for_start_single(pickings, message=message)
|
175
|
+
|
176
|
+
def _domain_recover_pickings(self):
|
177
|
+
return [
|
178
|
+
("user_id", "=", self.env.uid),
|
179
|
+
("state", "=", "assigned"),
|
180
|
+
("picking_type_id", "in", self.picking_types.ids),
|
181
|
+
]
|
182
|
+
|
183
|
+
def _search_recover_pickings(self):
|
184
|
+
candidate_pickings = self.env["stock.picking"].search(
|
185
|
+
self._domain_recover_pickings()
|
186
|
+
)
|
187
|
+
started_pickings = candidate_pickings.filtered(
|
188
|
+
lambda picking: any(line.qty_done for line in picking.move_line_ids)
|
189
|
+
)
|
190
|
+
return started_pickings
|
191
|
+
|
192
|
+
def _recover_started_picking(self):
|
193
|
+
"""Get the next response if the user has work in progress."""
|
194
|
+
started_pickings = self._search_recover_pickings()
|
195
|
+
if not started_pickings:
|
196
|
+
return False
|
197
|
+
return self._router_single_or_all_destination(
|
198
|
+
started_pickings, message=self.msg_store.recovered_previous_session()
|
199
|
+
)
|
200
|
+
|
201
|
+
def start_or_recover(self):
|
202
|
+
"""Start a new session or recover an existing one
|
203
|
+
|
204
|
+
If the current user had transfers in progress in this scenario
|
205
|
+
and reopen the menu, we want to directly reopen the screens to choose
|
206
|
+
destinations. Otherwise, we go to the "start" state.
|
207
|
+
"""
|
208
|
+
response = self._recover_started_picking()
|
209
|
+
return response or self._response_for_start()
|
210
|
+
|
211
|
+
def _find_location_move_lines_domain(self, location):
|
212
|
+
return [
|
213
|
+
("location_id", "=", location.id),
|
214
|
+
("qty_done", "=", 0),
|
215
|
+
("state", "in", ("assigned", "partially_available")),
|
216
|
+
("picking_id.user_id", "in", (False, self.env.uid)),
|
217
|
+
("picking_id.state", "=", "assigned"),
|
218
|
+
]
|
219
|
+
|
220
|
+
def _find_location_move_lines_from_scan_location(self, *args, **kwargs):
|
221
|
+
return self._find_location_move_lines(*args, **kwargs)
|
222
|
+
|
223
|
+
def _find_location_move_lines(self, location):
|
224
|
+
"""Find lines that potentially are to move in the location"""
|
225
|
+
return self.env["stock.move.line"].search(
|
226
|
+
self._find_location_move_lines_domain(location)
|
227
|
+
)
|
228
|
+
|
229
|
+
def _create_moves_from_location(self, location):
|
230
|
+
# get all quants from the scanned location
|
231
|
+
quants = self.env["stock.quant"].search(
|
232
|
+
[("location_id", "=", location.id), ("quantity", ">", 0)]
|
233
|
+
)
|
234
|
+
# create moves for each quant
|
235
|
+
picking_type = self.picking_types
|
236
|
+
move_vals_list = []
|
237
|
+
for quant in quants:
|
238
|
+
move_vals_list.append(
|
239
|
+
{
|
240
|
+
"name": quant.product_id.name,
|
241
|
+
"company_id": picking_type.company_id.id,
|
242
|
+
"product_id": quant.product_id.id,
|
243
|
+
"product_uom": quant.product_uom_id.id,
|
244
|
+
"product_uom_qty": quant.quantity,
|
245
|
+
"location_id": location.id,
|
246
|
+
"location_dest_id": picking_type.default_location_dest_id.id,
|
247
|
+
"origin": self.work.menu.name,
|
248
|
+
"picking_type_id": picking_type.id,
|
249
|
+
}
|
250
|
+
)
|
251
|
+
return self.env["stock.move"].create(move_vals_list)
|
252
|
+
|
253
|
+
def _find_location_to_work_from(self):
|
254
|
+
location = self.env["stock.location"]
|
255
|
+
pickings = self.env["stock.picking"].search(
|
256
|
+
[
|
257
|
+
("picking_type_id", "in", self.picking_types.ids),
|
258
|
+
("state", "=", "assigned"),
|
259
|
+
("user_id", "in", (False, self.env.user.id)),
|
260
|
+
],
|
261
|
+
order="user_id, priority desc, scheduled_date asc, id desc",
|
262
|
+
)
|
263
|
+
|
264
|
+
for next_picking in pickings:
|
265
|
+
move_lines = next_picking.move_line_ids.filtered(
|
266
|
+
lambda line: line.qty_done < line.reserved_uom_qty
|
267
|
+
)
|
268
|
+
location = fields.first(move_lines).location_id
|
269
|
+
if location:
|
270
|
+
break
|
271
|
+
return location
|
272
|
+
|
273
|
+
def find_work(self):
|
274
|
+
"""Find the next location to work from, for a user.
|
275
|
+
|
276
|
+
First recover any started pickings.
|
277
|
+
The find the first move line from the oldest transfer that can be worked on.
|
278
|
+
Mark all move lines on that location as picked.
|
279
|
+
And ask the user to confirm.
|
280
|
+
|
281
|
+
Transitions:
|
282
|
+
* start: no work found
|
283
|
+
* scan_location: with the location to work form for confirmation
|
284
|
+
"""
|
285
|
+
response = self._recover_started_picking()
|
286
|
+
if response:
|
287
|
+
return response
|
288
|
+
self._actions_for("lock").advisory(self._advisory_lock_find_work)
|
289
|
+
location = self._find_location_to_work_from()
|
290
|
+
if not location:
|
291
|
+
return self._response_for_start(message=self.msg_store.no_work_found())
|
292
|
+
move_lines = self._find_location_move_lines(location)
|
293
|
+
stock = self._actions_for("stock")
|
294
|
+
stock.mark_move_line_as_picked(move_lines, quantity=0)
|
295
|
+
return self._response_for_scan_location(location=location)
|
296
|
+
|
297
|
+
def _find_move_lines_to_cancel_work(self, location):
|
298
|
+
unreserve = self._actions_for("stock.unreserve")
|
299
|
+
return self.env["stock.move.line"].search(
|
300
|
+
unreserve._find_location_all_move_lines_domain(location)
|
301
|
+
)
|
302
|
+
|
303
|
+
def _move_lines_cancel_work(self, move_lines):
|
304
|
+
move_lines.write({"shopfloor_user_id": False})
|
305
|
+
move_lines.mapped("picking_id").write({"user_id": False})
|
306
|
+
stock = self._actions_for("stock")
|
307
|
+
stock.unmark_move_line_as_picked(move_lines)
|
308
|
+
|
309
|
+
def cancel_work(self, location_id):
|
310
|
+
"""Cancel work marked as picked by the user.
|
311
|
+
|
312
|
+
Transitions:
|
313
|
+
* start:
|
314
|
+
"""
|
315
|
+
location = self.env["stock.location"].browse(location_id)
|
316
|
+
if not location:
|
317
|
+
return self._response_for_start(message=self.msg_store.location_not_found())
|
318
|
+
|
319
|
+
move_lines = self._find_move_lines_to_cancel_work(location)
|
320
|
+
self._move_lines_cancel_work(move_lines)
|
321
|
+
return self._response_for_start()
|
322
|
+
|
323
|
+
def scan_location(self, barcode): # noqa: C901
|
324
|
+
"""Scan start location
|
325
|
+
|
326
|
+
Called at the beginning at the workflow to select the location from which
|
327
|
+
we want to move the content.
|
328
|
+
|
329
|
+
All the move lines and package levels must have the same picking type.
|
330
|
+
|
331
|
+
If the scanned location has no move lines, new move lines to move the
|
332
|
+
whole content of the location are created if:
|
333
|
+
|
334
|
+
* the menu has the option "Allow to create move(s)"
|
335
|
+
* the menu is linked to only one picking type.
|
336
|
+
|
337
|
+
When move lines and package levels have different destinations, the
|
338
|
+
first line without package level or package level is sent to the client.
|
339
|
+
|
340
|
+
The selected move lines to process are bound to the current operator,
|
341
|
+
this will allow another operator to find unprocessed lines in parallel
|
342
|
+
and not overlap with current ones.
|
343
|
+
|
344
|
+
Transitions:
|
345
|
+
* start: location not found, ...
|
346
|
+
* scan_destination_all: if the destination of all the lines and package
|
347
|
+
levels have the same destination
|
348
|
+
* start_single: if any line or package level has a different destination
|
349
|
+
"""
|
350
|
+
location = self._actions_for("search").location_from_scan(barcode)
|
351
|
+
if not location:
|
352
|
+
return self._response_for_start(message=self.msg_store.barcode_not_found())
|
353
|
+
|
354
|
+
if not self.is_src_location_valid(location):
|
355
|
+
return self._response_for_start(
|
356
|
+
message=self.msg_store.cannot_move_something_in_picking_type()
|
357
|
+
)
|
358
|
+
|
359
|
+
move_lines = self._find_location_move_lines_from_scan_location(location)
|
360
|
+
|
361
|
+
savepoint = self._actions_for("savepoint").new()
|
362
|
+
unreserve = self._actions_for("stock.unreserve")
|
363
|
+
|
364
|
+
unreserved_moves = self.env["stock.move"].browse()
|
365
|
+
if self.work.menu.allow_unreserve_other_moves:
|
366
|
+
message = unreserve.check_unreserve(location, move_lines)
|
367
|
+
if message:
|
368
|
+
return self._response_for_start(message=message)
|
369
|
+
move_lines, unreserved_moves = unreserve.unreserve_moves(
|
370
|
+
move_lines, self.picking_types
|
371
|
+
)
|
372
|
+
else:
|
373
|
+
picking_types = move_lines.picking_id.picking_type_id
|
374
|
+
if len(picking_types) > 1:
|
375
|
+
return self._response_for_start(
|
376
|
+
message={
|
377
|
+
"message_type": "error",
|
378
|
+
"body": _("This location content can't be moved at once."),
|
379
|
+
}
|
380
|
+
)
|
381
|
+
if picking_types - self.picking_types:
|
382
|
+
return self._response_for_start(
|
383
|
+
message=self.msg_store.cannot_move_something_in_picking_type()
|
384
|
+
)
|
385
|
+
|
386
|
+
if not move_lines:
|
387
|
+
if not self.is_allow_move_create():
|
388
|
+
savepoint.rollback()
|
389
|
+
return self._response_for_start(
|
390
|
+
message=self.msg_store.location_empty(location)
|
391
|
+
)
|
392
|
+
new_moves = self._create_moves_from_location(location)
|
393
|
+
if not new_moves:
|
394
|
+
savepoint.rollback()
|
395
|
+
return self._response_for_start(
|
396
|
+
message=self.msg_store.location_empty(location)
|
397
|
+
)
|
398
|
+
new_moves._action_confirm(merge=False)
|
399
|
+
new_moves._action_assign()
|
400
|
+
if not all([x.state == "assigned" for x in new_moves]):
|
401
|
+
savepoint.rollback()
|
402
|
+
return self._response_for_start(
|
403
|
+
message=self.msg_store.new_move_lines_not_assigned()
|
404
|
+
)
|
405
|
+
move_lines = new_moves.move_line_ids
|
406
|
+
for line in move_lines:
|
407
|
+
if not self.is_dest_location_valid(line.move_id, line.location_dest_id):
|
408
|
+
savepoint.rollback()
|
409
|
+
return self._response_for_start(
|
410
|
+
message=self.msg_store.location_content_unable_to_transfer(
|
411
|
+
location
|
412
|
+
)
|
413
|
+
)
|
414
|
+
|
415
|
+
stock = self._actions_for("stock")
|
416
|
+
if self.work.menu.ignore_no_putaway_available and stock.no_putaway_available(
|
417
|
+
self.picking_types, move_lines
|
418
|
+
):
|
419
|
+
# the putaway created a move line but no putaway was possible, so revert
|
420
|
+
# to the initial state
|
421
|
+
savepoint.rollback()
|
422
|
+
return self._response_for_start(
|
423
|
+
message=self.msg_store.no_putaway_destination_available()
|
424
|
+
)
|
425
|
+
|
426
|
+
stock.mark_move_line_as_picked(move_lines)
|
427
|
+
|
428
|
+
unreserved_moves._action_assign()
|
429
|
+
|
430
|
+
savepoint.release()
|
431
|
+
|
432
|
+
return self._router_single_or_all_destination(move_lines.picking_id)
|
433
|
+
|
434
|
+
def _find_transfer_move_lines_domain(self, location):
|
435
|
+
return [
|
436
|
+
("location_id", "=", location.id),
|
437
|
+
("state", "in", ("assigned", "partially_available")),
|
438
|
+
("qty_done", ">", 0),
|
439
|
+
# TODO check generated SQL
|
440
|
+
("picking_id.user_id", "=", self.env.uid),
|
441
|
+
]
|
442
|
+
|
443
|
+
def _find_transfer_move_lines(self, location):
|
444
|
+
"""Find move lines currently being moved by the user"""
|
445
|
+
lines = self.env["stock.move.line"].search(
|
446
|
+
self._find_transfer_move_lines_domain(location)
|
447
|
+
)
|
448
|
+
return lines
|
449
|
+
|
450
|
+
# hook used in module shopfloor_checkout_sync
|
451
|
+
def _write_destination_on_lines(self, lines, location):
|
452
|
+
lines.location_dest_id = location
|
453
|
+
lines.package_level_id.picking_id.location_dest_id = location
|
454
|
+
|
455
|
+
def _set_all_destination_lines_and_done(self, pickings, move_lines, dest_location):
|
456
|
+
self._write_destination_on_lines(move_lines, dest_location)
|
457
|
+
stock = self._actions_for("stock")
|
458
|
+
stock.validate_moves(move_lines.move_id)
|
459
|
+
|
460
|
+
def _lock_lines(self, lines):
|
461
|
+
"""Lock move lines"""
|
462
|
+
self._actions_for("lock").for_update(lines)
|
463
|
+
|
464
|
+
def set_destination_all(self, location_id, barcode, confirmation=False):
|
465
|
+
"""Scan destination location for all the moves of the location
|
466
|
+
|
467
|
+
barcode is a stock.location for the destination
|
468
|
+
|
469
|
+
Transitions:
|
470
|
+
* scan_destination_all: invalid destination or could not set moves to done
|
471
|
+
* start: moves are done
|
472
|
+
"""
|
473
|
+
location = self.env["stock.location"].browse(location_id)
|
474
|
+
if not location.exists():
|
475
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
476
|
+
move_lines = self._find_transfer_move_lines(location)
|
477
|
+
pickings = move_lines.mapped("picking_id")
|
478
|
+
if not pickings:
|
479
|
+
# if we can't find the lines anymore, they likely have been done
|
480
|
+
# by someone else
|
481
|
+
return self._response_for_start(message=self.msg_store.already_done())
|
482
|
+
scanned_location = self._actions_for("search").location_from_scan(barcode)
|
483
|
+
if not scanned_location:
|
484
|
+
return self._response_for_scan_destination_all(
|
485
|
+
pickings, message=self.msg_store.barcode_not_found()
|
486
|
+
)
|
487
|
+
|
488
|
+
if not self.is_dest_location_valid(move_lines.move_id, scanned_location):
|
489
|
+
return self._response_for_scan_destination_all(
|
490
|
+
pickings, message=self.msg_store.dest_location_not_allowed()
|
491
|
+
)
|
492
|
+
if not confirmation and self.is_dest_location_to_confirm(
|
493
|
+
move_lines.location_dest_id, scanned_location
|
494
|
+
):
|
495
|
+
return self._response_for_scan_destination_all(
|
496
|
+
pickings, confirmation_required=True
|
497
|
+
)
|
498
|
+
self._lock_lines(move_lines)
|
499
|
+
|
500
|
+
self._set_all_destination_lines_and_done(pickings, move_lines, scanned_location)
|
501
|
+
|
502
|
+
completion_info = self._actions_for("completion.info")
|
503
|
+
completion_info_popup = completion_info.popup(move_lines)
|
504
|
+
return self._response_for_start(
|
505
|
+
message=self.msg_store.location_content_transfer_complete(
|
506
|
+
location, scanned_location
|
507
|
+
),
|
508
|
+
popup=completion_info_popup,
|
509
|
+
)
|
510
|
+
|
511
|
+
def go_to_single(self, location_id):
|
512
|
+
"""Ask the first move line or package level
|
513
|
+
|
514
|
+
If the user was brought to the screen allowing to move everything to
|
515
|
+
the same location, but they want to move them to different locations,
|
516
|
+
this method will return the first move line or package level.
|
517
|
+
|
518
|
+
Transitions:
|
519
|
+
* start: no remaining lines in the location
|
520
|
+
* start_single: if any line or package level has a different destination
|
521
|
+
"""
|
522
|
+
location = self.env["stock.location"].browse(location_id)
|
523
|
+
if not location.exists():
|
524
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
525
|
+
move_lines = self._find_transfer_move_lines(location)
|
526
|
+
if not move_lines:
|
527
|
+
return self._response_for_start(
|
528
|
+
message=self.msg_store.no_lines_to_process()
|
529
|
+
)
|
530
|
+
return self._response_for_start_single(move_lines.mapped("picking_id"))
|
531
|
+
|
532
|
+
def scan_package(self, location_id, package_level_id, barcode):
|
533
|
+
"""Scan a package level to move
|
534
|
+
|
535
|
+
It validates that the user scanned the correct package, lot or product.
|
536
|
+
|
537
|
+
Transitions:
|
538
|
+
* start: no remaining lines in the location
|
539
|
+
* start_single: barcode not found, ...
|
540
|
+
* scan_destination: the barcode matches
|
541
|
+
"""
|
542
|
+
location = self.env["stock.location"].browse(location_id)
|
543
|
+
if not location.exists():
|
544
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
545
|
+
package_level = self.env["stock.package_level"].browse(package_level_id)
|
546
|
+
if not package_level.exists():
|
547
|
+
move_lines = self._find_transfer_move_lines(location)
|
548
|
+
return self._response_for_start_single(
|
549
|
+
move_lines.mapped("picking_id"),
|
550
|
+
message=self.msg_store.record_not_found(),
|
551
|
+
)
|
552
|
+
|
553
|
+
search = self._actions_for("search")
|
554
|
+
package = search.package_from_scan(barcode)
|
555
|
+
if package and package_level.package_id == package:
|
556
|
+
return self._response_for_scan_destination(location, package_level)
|
557
|
+
|
558
|
+
move_lines = self._find_transfer_move_lines(location)
|
559
|
+
package_move_lines = package_level.move_line_ids
|
560
|
+
other_move_lines = move_lines - package_move_lines
|
561
|
+
|
562
|
+
product = search.product_from_scan(barcode)
|
563
|
+
if not product:
|
564
|
+
packaging = search.packaging_from_scan(barcode)
|
565
|
+
product = packaging.product_id
|
566
|
+
# Normally the user scan the barcode of the package. But if they scan the
|
567
|
+
# product and we can be sure it's the correct package, it's tolerated.
|
568
|
+
if product and product in package_move_lines.mapped("product_id"):
|
569
|
+
if product in other_move_lines.mapped("product_id") or product.tracking in (
|
570
|
+
"lot",
|
571
|
+
"serial",
|
572
|
+
):
|
573
|
+
# When the product exists in other move lines as raw products
|
574
|
+
# or part of another package, we can't be sure they scanned
|
575
|
+
# the correct package, so ask to scan the package.
|
576
|
+
return self._response_for_start_single(
|
577
|
+
move_lines.mapped("picking_id"),
|
578
|
+
message={"message_type": "error", "body": _("Scan the package")},
|
579
|
+
)
|
580
|
+
else:
|
581
|
+
return self._response_for_scan_destination(location, package_level)
|
582
|
+
|
583
|
+
lot = search.lot_from_scan(barcode, products=package_move_lines.product_id)
|
584
|
+
if lot and lot in package_move_lines.mapped("lot_id"):
|
585
|
+
if lot in other_move_lines.mapped("lot_id"):
|
586
|
+
return self._response_for_start_single(
|
587
|
+
move_lines.mapped("picking_id"),
|
588
|
+
message={"message_type": "error", "body": _("Scan the package")},
|
589
|
+
)
|
590
|
+
else:
|
591
|
+
return self._response_for_scan_destination(location, package_level)
|
592
|
+
|
593
|
+
# Nothing matches what is expected from the move line.
|
594
|
+
for rec in (package, product, lot):
|
595
|
+
if rec:
|
596
|
+
return self._response_for_start_single(
|
597
|
+
move_lines.mapped("picking_id"),
|
598
|
+
message=self.msg_store.wrong_record(rec),
|
599
|
+
)
|
600
|
+
return self._response_for_start_single(
|
601
|
+
move_lines.mapped("picking_id"), message=self.msg_store.barcode_not_found()
|
602
|
+
)
|
603
|
+
|
604
|
+
def scan_line(self, location_id, move_line_id, barcode):
|
605
|
+
"""Scan a move line to move
|
606
|
+
|
607
|
+
It validates that the user scanned the correct package, lot or product.
|
608
|
+
|
609
|
+
Transitions:
|
610
|
+
* start: no remaining lines in the location
|
611
|
+
* start_single: barcode not found, ...
|
612
|
+
* scan_destination: the barcode matches
|
613
|
+
"""
|
614
|
+
location = self.env["stock.location"].browse(location_id)
|
615
|
+
if not location.exists():
|
616
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
617
|
+
move_line = self.env["stock.move.line"].browse(move_line_id)
|
618
|
+
if not move_line.exists():
|
619
|
+
move_lines = self._find_transfer_move_lines(location)
|
620
|
+
return self._response_for_start_single(
|
621
|
+
move_lines.mapped("picking_id"),
|
622
|
+
message=self.msg_store.record_not_found(),
|
623
|
+
)
|
624
|
+
|
625
|
+
search = self._actions_for("search")
|
626
|
+
|
627
|
+
package = search.package_from_scan(barcode)
|
628
|
+
if package and move_line.package_id == package:
|
629
|
+
# In case we have a source package but no package level because if
|
630
|
+
# we have a package level, we would use "scan_package".
|
631
|
+
return self._response_for_scan_destination(location, move_line)
|
632
|
+
|
633
|
+
product = search.product_from_scan(barcode)
|
634
|
+
if not product:
|
635
|
+
packaging = search.packaging_from_scan(barcode)
|
636
|
+
if packaging:
|
637
|
+
product = packaging.product_id
|
638
|
+
|
639
|
+
if product and product == move_line.product_id:
|
640
|
+
if product.tracking in ("lot", "serial"):
|
641
|
+
move_lines = self._find_transfer_move_lines(location)
|
642
|
+
return self._response_for_start_single(
|
643
|
+
move_lines.mapped("picking_id"),
|
644
|
+
message=self.msg_store.scan_lot_on_product_tracked_by_lot(),
|
645
|
+
)
|
646
|
+
else:
|
647
|
+
return self._response_for_scan_destination(location, move_line)
|
648
|
+
|
649
|
+
lot = search.lot_from_scan(barcode, products=move_line.product_id)
|
650
|
+
if lot and lot == move_line.lot_id:
|
651
|
+
return self._response_for_scan_destination(location, move_line)
|
652
|
+
|
653
|
+
# Nothing matches what is expected from the move line.
|
654
|
+
move_lines = self._find_transfer_move_lines(location)
|
655
|
+
for rec in (package, product, lot):
|
656
|
+
if rec:
|
657
|
+
return self._response_for_start_single(
|
658
|
+
move_lines.mapped("picking_id"),
|
659
|
+
message=self.msg_store.wrong_record(rec),
|
660
|
+
)
|
661
|
+
return self._response_for_start_single(
|
662
|
+
move_lines.mapped("picking_id"), message=self.msg_store.barcode_not_found()
|
663
|
+
)
|
664
|
+
|
665
|
+
def set_destination_package(
|
666
|
+
self, location_id, package_level_id, barcode, confirmation=False
|
667
|
+
):
|
668
|
+
"""Scan destination location for package level
|
669
|
+
|
670
|
+
If the move has other move lines / package levels it has to be split
|
671
|
+
so we can post only this part.
|
672
|
+
|
673
|
+
After the destination is set, the move is set to done.
|
674
|
+
|
675
|
+
Transitions:
|
676
|
+
* scan_destination: invalid destination or could not
|
677
|
+
* start_single: continue with the next package level / line
|
678
|
+
* start: if there is no more package level / line to process
|
679
|
+
"""
|
680
|
+
location = self.env["stock.location"].browse(location_id)
|
681
|
+
if not location.exists():
|
682
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
683
|
+
package_level = self.env["stock.package_level"].browse(package_level_id)
|
684
|
+
if not package_level.exists():
|
685
|
+
move_lines = self._find_transfer_move_lines(location)
|
686
|
+
return self._response_for_start_single(move_lines.mapped("picking_id"))
|
687
|
+
search = self._actions_for("search")
|
688
|
+
scanned_location = search.location_from_scan(barcode)
|
689
|
+
if not scanned_location:
|
690
|
+
return self._response_for_scan_destination(
|
691
|
+
location, package_level, message=self.msg_store.no_location_found()
|
692
|
+
)
|
693
|
+
package_moves = package_level.move_line_ids.move_id
|
694
|
+
if not self.is_dest_location_valid(package_moves, scanned_location):
|
695
|
+
return self._response_for_scan_destination(
|
696
|
+
location,
|
697
|
+
package_level,
|
698
|
+
message=self.msg_store.dest_location_not_allowed(),
|
699
|
+
)
|
700
|
+
if not confirmation and self.is_dest_location_to_confirm(
|
701
|
+
package_level.location_dest_id, scanned_location
|
702
|
+
):
|
703
|
+
return self._response_for_scan_destination(
|
704
|
+
location, package_level, confirmation_required=True
|
705
|
+
)
|
706
|
+
package_move_lines = package_level.move_line_ids
|
707
|
+
self._lock_lines(package_move_lines)
|
708
|
+
stock = self._actions_for("stock")
|
709
|
+
stock.put_package_level_in_move(package_level)
|
710
|
+
self._write_destination_on_lines(package_level.move_line_ids, scanned_location)
|
711
|
+
stock.validate_moves(package_moves)
|
712
|
+
move_lines = self._find_transfer_move_lines(location)
|
713
|
+
message = self.msg_store.location_content_transfer_item_complete(
|
714
|
+
scanned_location
|
715
|
+
)
|
716
|
+
completion_info = self._actions_for("completion.info")
|
717
|
+
completion_info_popup = completion_info.popup(package_moves.move_line_ids)
|
718
|
+
return self._response_for_start_single(
|
719
|
+
move_lines.mapped("picking_id"),
|
720
|
+
message=message,
|
721
|
+
popup=completion_info_popup,
|
722
|
+
)
|
723
|
+
|
724
|
+
def set_destination_line(
|
725
|
+
self, location_id, move_line_id, quantity, barcode, confirmation=False
|
726
|
+
):
|
727
|
+
"""Scan destination location for move line
|
728
|
+
|
729
|
+
If the quantity < qty of the line, split the move and reserve it.
|
730
|
+
If the move has other move lines / package levels it has to be split
|
731
|
+
so we can post only this part.
|
732
|
+
|
733
|
+
After the destination and quantity are set, the move is set to done.
|
734
|
+
|
735
|
+
Transitions:
|
736
|
+
* scan_destination: invalid destination or could not
|
737
|
+
* start_single: continue with the next package level / line
|
738
|
+
* start: if there is no more package level / line to process
|
739
|
+
"""
|
740
|
+
location = self.env["stock.location"].browse(location_id)
|
741
|
+
if not location.exists():
|
742
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
743
|
+
move_line = self.env["stock.move.line"].browse(move_line_id)
|
744
|
+
if not move_line.exists():
|
745
|
+
move_lines = self._find_transfer_move_lines(location)
|
746
|
+
return self._response_for_start_single(move_lines.mapped("picking_id"))
|
747
|
+
search = self._actions_for("search")
|
748
|
+
scanned_location = search.location_from_scan(barcode)
|
749
|
+
if not scanned_location:
|
750
|
+
return self._response_for_scan_destination(
|
751
|
+
location, move_line, message=self.msg_store.no_location_found()
|
752
|
+
)
|
753
|
+
if not self.is_dest_location_valid(move_line.move_id, scanned_location):
|
754
|
+
return self._response_for_scan_destination(
|
755
|
+
location, move_line, message=self.msg_store.dest_location_not_allowed()
|
756
|
+
)
|
757
|
+
if not confirmation and self.is_dest_location_to_confirm(
|
758
|
+
move_line.location_dest_id, scanned_location
|
759
|
+
):
|
760
|
+
return self._response_for_scan_destination(
|
761
|
+
location, move_line, confirmation_required=True
|
762
|
+
)
|
763
|
+
|
764
|
+
self._lock_lines(move_line)
|
765
|
+
|
766
|
+
move_line.qty_done = quantity
|
767
|
+
self._write_destination_on_lines(move_line, scanned_location)
|
768
|
+
|
769
|
+
stock = self._actions_for("stock")
|
770
|
+
|
771
|
+
backorders = stock.validate_moves(move_line.move_id)
|
772
|
+
if backorders:
|
773
|
+
for move_line in backorders.mapped("move_line_ids"):
|
774
|
+
move_line.qty_done = move_line.reserved_uom_qty
|
775
|
+
backorders.user_id = self.env.user
|
776
|
+
# process first backorder of current line
|
777
|
+
move_lines = backorders.move_line_ids
|
778
|
+
else:
|
779
|
+
move_lines = self._find_transfer_move_lines(move_line.location_id)
|
780
|
+
message = self.msg_store.location_content_transfer_item_complete(
|
781
|
+
scanned_location
|
782
|
+
)
|
783
|
+
completion_info = self._actions_for("completion.info")
|
784
|
+
completion_info_popup = completion_info.popup(move_line)
|
785
|
+
return self._response_for_start_single(
|
786
|
+
move_lines.mapped("picking_id"),
|
787
|
+
message=message,
|
788
|
+
popup=completion_info_popup,
|
789
|
+
)
|
790
|
+
|
791
|
+
def postpone_package(self, location_id, package_level_id):
|
792
|
+
"""Mark a package level as postponed and return the next level/line
|
793
|
+
|
794
|
+
Transitions:
|
795
|
+
* start_single: continue with the next package level / line
|
796
|
+
"""
|
797
|
+
location = self.env["stock.location"].browse(location_id)
|
798
|
+
package_level = self.env["stock.package_level"].browse(package_level_id)
|
799
|
+
if not location.exists():
|
800
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
801
|
+
move_lines = self._find_transfer_move_lines(location)
|
802
|
+
if package_level.exists():
|
803
|
+
pickings = move_lines.mapped("picking_id")
|
804
|
+
sorter = self._actions_for("location_content_transfer.sorter")
|
805
|
+
sorter.feed_pickings(pickings)
|
806
|
+
package_levels = sorter.package_levels()
|
807
|
+
package_level.shopfloor_postpone(move_lines, package_levels)
|
808
|
+
return self._response_for_start_single(move_lines.mapped("picking_id"))
|
809
|
+
|
810
|
+
def postpone_line(self, location_id, move_line_id):
|
811
|
+
"""Mark a move line as postponed and return the next level/line
|
812
|
+
|
813
|
+
Transitions:
|
814
|
+
* start_single: continue with the next package level / line
|
815
|
+
"""
|
816
|
+
location = self.env["stock.location"].browse(location_id)
|
817
|
+
if not location.exists():
|
818
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
819
|
+
move_line = self.env["stock.move.line"].browse(move_line_id)
|
820
|
+
move_lines = self._find_transfer_move_lines(location)
|
821
|
+
if move_line.exists():
|
822
|
+
pickings = move_lines.mapped("picking_id")
|
823
|
+
sorter = self._actions_for("location_content_transfer.sorter")
|
824
|
+
sorter.feed_pickings(pickings)
|
825
|
+
package_levels = sorter.package_levels()
|
826
|
+
move_line.shopfloor_postpone(move_lines, package_levels)
|
827
|
+
return self._response_for_start_single(move_lines.mapped("picking_id"))
|
828
|
+
|
829
|
+
def stock_out_package(self, location_id, package_level_id):
|
830
|
+
"""Declare a stock out on a package level
|
831
|
+
|
832
|
+
It first ensures the stock.move only has this package level. If not, it
|
833
|
+
splits the move to have no side-effect on the other package levels/move
|
834
|
+
lines.
|
835
|
+
|
836
|
+
It unreserves the move, create an inventory at 0 in the move's source
|
837
|
+
location, create a second draft inventory (if none exists) to check later.
|
838
|
+
Finally, it cancels the move.
|
839
|
+
|
840
|
+
Transitions:
|
841
|
+
* start: no more content to move
|
842
|
+
* start_single: continue with the next package level / line
|
843
|
+
"""
|
844
|
+
location = self.env["stock.location"].browse(location_id)
|
845
|
+
if not location.exists():
|
846
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
847
|
+
package_level = self.env["stock.package_level"].browse(package_level_id)
|
848
|
+
if not package_level.exists():
|
849
|
+
move_lines = self._find_transfer_move_lines(location)
|
850
|
+
return self._response_for_start_single(move_lines.mapped("picking_id"))
|
851
|
+
inventory = self._actions_for("inventory")
|
852
|
+
package_move_lines = package_level.move_line_ids
|
853
|
+
package_moves = package_move_lines.mapped("move_id")
|
854
|
+
package = package_level.package_id
|
855
|
+
for package_move in package_moves:
|
856
|
+
# Check if there is no other lines linked to the move others than
|
857
|
+
# the lines related to the package itself. In such case we have to
|
858
|
+
# split the move to process only the lines related to the package.
|
859
|
+
package_move.split_other_move_lines(package_move_lines)
|
860
|
+
lot = package_move.move_line_ids.lot_id
|
861
|
+
# We need to set qty_done at 0 because otherwise
|
862
|
+
# the move_line will not be deleted
|
863
|
+
package_move.move_line_ids.write({"qty_done": 0})
|
864
|
+
package_move._do_unreserve()
|
865
|
+
package_move._recompute_state()
|
866
|
+
# Create an inventory at 0 in the move's source location
|
867
|
+
inventory.create_stock_issue(package_move, location, package, lot)
|
868
|
+
# Create a draft inventory to control stock
|
869
|
+
inventory.create_control_stock(
|
870
|
+
location, package_move.product_id, package, lot
|
871
|
+
)
|
872
|
+
package_move._action_cancel()
|
873
|
+
# remove the package level (this is what does the `picking.do_unreserve()`
|
874
|
+
# method, but here we want to unreserve+unlink this package alone)
|
875
|
+
move_lines = self._find_transfer_move_lines(location)
|
876
|
+
return self._response_for_start_single(move_lines.mapped("picking_id"))
|
877
|
+
|
878
|
+
def stock_out_line(self, location_id, move_line_id):
|
879
|
+
"""Declare a stock out on a move line
|
880
|
+
|
881
|
+
It first ensures the stock.move only has this move line. If not, it
|
882
|
+
splits the move to have no side-effect on the other package levels/move
|
883
|
+
lines.
|
884
|
+
|
885
|
+
It unreserves the move, create an inventory at 0 in the move's source
|
886
|
+
location, create a second draft inventory (if none exists) to check later.
|
887
|
+
Finally, it cancels the move.
|
888
|
+
|
889
|
+
Transitions:
|
890
|
+
* start: no more content to move
|
891
|
+
* start_single: continue with the next package level / line
|
892
|
+
"""
|
893
|
+
location = self.env["stock.location"].browse(location_id)
|
894
|
+
if not location.exists():
|
895
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
896
|
+
move_line = self.env["stock.move.line"].browse(move_line_id)
|
897
|
+
if not move_line.exists():
|
898
|
+
move_lines = self._find_transfer_move_lines(location)
|
899
|
+
return self._response_for_start_single(move_lines.mapped("picking_id"))
|
900
|
+
inventory = self._actions_for("inventory")
|
901
|
+
move_line.move_id.split_other_move_lines(move_line)
|
902
|
+
move_line_src_location = move_line.location_id
|
903
|
+
move = move_line.move_id
|
904
|
+
package = move_line.package_id
|
905
|
+
lot = move_line.lot_id
|
906
|
+
# We need to set qty_done at 0 because otherwise
|
907
|
+
# the move_line will not be deleted
|
908
|
+
move_line.qty_done = 0
|
909
|
+
move._do_unreserve()
|
910
|
+
move._recompute_state()
|
911
|
+
# Create an inventory at 0 in the move's source location
|
912
|
+
inventory.create_stock_issue(move, move_line_src_location, package, lot)
|
913
|
+
# Create a draft inventory to control stock
|
914
|
+
inventory.create_control_stock(
|
915
|
+
move_line_src_location, move.product_id, package, lot
|
916
|
+
)
|
917
|
+
move._action_cancel()
|
918
|
+
move_lines = self._find_transfer_move_lines(location)
|
919
|
+
return self._response_for_start_single(move_lines.mapped("picking_id"))
|
920
|
+
|
921
|
+
def dismiss_package_level(self, location_id, package_level_id):
|
922
|
+
"""Dismiss the package level.
|
923
|
+
|
924
|
+
The result package of the related move lines is unset, then the package
|
925
|
+
level itself is removed from the picking. This allows to move parts
|
926
|
+
of the package to different locations.
|
927
|
+
|
928
|
+
The user is then redirected to process the next line of the related picking.
|
929
|
+
|
930
|
+
Transitions:
|
931
|
+
* start_single: continue with the next line
|
932
|
+
"""
|
933
|
+
location = self.env["stock.location"].browse(location_id)
|
934
|
+
if not location.exists():
|
935
|
+
return self._response_for_start(message=self.msg_store.record_not_found())
|
936
|
+
package_level = self.env["stock.package_level"].browse(package_level_id)
|
937
|
+
if not package_level.exists():
|
938
|
+
move_lines = self._find_transfer_move_lines(location)
|
939
|
+
return self._response_for_start_single(
|
940
|
+
move_lines.mapped("picking_id"),
|
941
|
+
message=self.msg_store.record_not_found(),
|
942
|
+
)
|
943
|
+
move_lines = package_level.move_line_ids
|
944
|
+
package_level.explode_package()
|
945
|
+
move_lines.write(
|
946
|
+
{
|
947
|
+
# ensure all the lines in the package are the next ones to be processed
|
948
|
+
"shopfloor_priority": 1,
|
949
|
+
}
|
950
|
+
)
|
951
|
+
return self._response_for_start_single(
|
952
|
+
move_lines.mapped("picking_id"), message=self.msg_store.package_open()
|
953
|
+
)
|
954
|
+
|
955
|
+
|
956
|
+
class ShopfloorLocationContentTransferValidator(Component):
|
957
|
+
"""Validators for the Location Content Transfer endpoints"""
|
958
|
+
|
959
|
+
_inherit = "base.shopfloor.validator"
|
960
|
+
_name = "shopfloor.location.content.transfer.validator"
|
961
|
+
_usage = "location_content_transfer.validator"
|
962
|
+
|
963
|
+
def start_or_recover(self):
|
964
|
+
return {}
|
965
|
+
|
966
|
+
def get_work(self):
|
967
|
+
return {}
|
968
|
+
|
969
|
+
def cancel_work(self):
|
970
|
+
return {"location_id": {"required": True, "type": "integer"}}
|
971
|
+
|
972
|
+
def scan_location(self):
|
973
|
+
return {"barcode": {"required": True, "type": "string"}}
|
974
|
+
|
975
|
+
def set_destination_all(self):
|
976
|
+
return {
|
977
|
+
"location_id": {"coerce": to_int, "required": True, "type": "integer"},
|
978
|
+
"barcode": {"required": True, "type": "string"},
|
979
|
+
"confirmation": {"type": "boolean", "nullable": True, "required": False},
|
980
|
+
}
|
981
|
+
|
982
|
+
def go_to_single(self):
|
983
|
+
return {"location_id": {"coerce": to_int, "required": True, "type": "integer"}}
|
984
|
+
|
985
|
+
def scan_package(self):
|
986
|
+
return {
|
987
|
+
"location_id": {"coerce": to_int, "required": True, "type": "integer"},
|
988
|
+
"package_level_id": {"coerce": to_int, "required": True, "type": "integer"},
|
989
|
+
"barcode": {"required": True, "type": "string"},
|
990
|
+
}
|
991
|
+
|
992
|
+
def scan_line(self):
|
993
|
+
return {
|
994
|
+
"location_id": {"coerce": to_int, "required": True, "type": "integer"},
|
995
|
+
"move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
|
996
|
+
"barcode": {"required": True, "type": "string"},
|
997
|
+
}
|
998
|
+
|
999
|
+
def set_destination_package(self):
|
1000
|
+
return {
|
1001
|
+
"location_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1002
|
+
"package_level_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1003
|
+
"barcode": {"required": True, "type": "string"},
|
1004
|
+
"confirmation": {"type": "boolean", "nullable": True, "required": False},
|
1005
|
+
}
|
1006
|
+
|
1007
|
+
def set_destination_line(self):
|
1008
|
+
return {
|
1009
|
+
"location_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1010
|
+
"move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1011
|
+
"quantity": {"coerce": to_float, "required": True, "type": "float"},
|
1012
|
+
"barcode": {"required": True, "type": "string"},
|
1013
|
+
"confirmation": {"type": "boolean", "nullable": True, "required": False},
|
1014
|
+
}
|
1015
|
+
|
1016
|
+
def postpone_package(self):
|
1017
|
+
return {
|
1018
|
+
"location_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1019
|
+
"package_level_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1020
|
+
}
|
1021
|
+
|
1022
|
+
def postpone_line(self):
|
1023
|
+
return {
|
1024
|
+
"location_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1025
|
+
"move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1026
|
+
}
|
1027
|
+
|
1028
|
+
def stock_out_package(self):
|
1029
|
+
return {
|
1030
|
+
"location_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1031
|
+
"package_level_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1032
|
+
}
|
1033
|
+
|
1034
|
+
def stock_out_line(self):
|
1035
|
+
return {
|
1036
|
+
"location_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1037
|
+
"move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1038
|
+
}
|
1039
|
+
|
1040
|
+
def dismiss_package_level(self):
|
1041
|
+
return {
|
1042
|
+
"location_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1043
|
+
"package_level_id": {"coerce": to_int, "required": True, "type": "integer"},
|
1044
|
+
}
|
1045
|
+
|
1046
|
+
|
1047
|
+
class ShopfloorLocationContentTransferValidatorResponse(Component):
|
1048
|
+
"""Validators for the Location Content Transfer endpoints responses"""
|
1049
|
+
|
1050
|
+
_inherit = "base.shopfloor.validator.response"
|
1051
|
+
_name = "shopfloor.location.content.transfer.validator.response"
|
1052
|
+
_usage = "location_content_transfer.validator.response"
|
1053
|
+
|
1054
|
+
def _states(self):
|
1055
|
+
"""List of possible next states
|
1056
|
+
|
1057
|
+
With the schema of the data send to the client to transition
|
1058
|
+
to the next state.
|
1059
|
+
"""
|
1060
|
+
return {
|
1061
|
+
"start": {},
|
1062
|
+
"scan_location": {},
|
1063
|
+
"get_work": {},
|
1064
|
+
"scan_destination_all": self._schema_all,
|
1065
|
+
"start_single": self._schema_single,
|
1066
|
+
"scan_destination": self._schema_single,
|
1067
|
+
}
|
1068
|
+
|
1069
|
+
@property
|
1070
|
+
def _schema_all(self):
|
1071
|
+
package_level_schema = self.schemas.package_level()
|
1072
|
+
move_line_schema = self.schemas.move_line()
|
1073
|
+
return {
|
1074
|
+
"location": self.schemas._schema_dict_of(self.schemas.location()),
|
1075
|
+
# we'll display all the packages and move lines *without package
|
1076
|
+
# levels*
|
1077
|
+
"package_levels": self.schemas._schema_list_of(package_level_schema),
|
1078
|
+
"move_lines": self.schemas._schema_list_of(move_line_schema),
|
1079
|
+
"confirmation_required": {
|
1080
|
+
"type": "boolean",
|
1081
|
+
"nullable": True,
|
1082
|
+
"required": False,
|
1083
|
+
},
|
1084
|
+
}
|
1085
|
+
|
1086
|
+
@property
|
1087
|
+
def _schema_single(self):
|
1088
|
+
schema_package_level = self.schemas.package_level()
|
1089
|
+
schema_move_line = self.schemas.move_line()
|
1090
|
+
return {
|
1091
|
+
# we'll have one or the other...
|
1092
|
+
"package_level": self.schemas._schema_dict_of(schema_package_level),
|
1093
|
+
"move_line": self.schemas._schema_dict_of(schema_move_line),
|
1094
|
+
"confirmation_required": {
|
1095
|
+
"type": "boolean",
|
1096
|
+
"nullable": True,
|
1097
|
+
"required": False,
|
1098
|
+
},
|
1099
|
+
}
|
1100
|
+
|
1101
|
+
def start_or_recover(self):
|
1102
|
+
return self._response_schema(
|
1103
|
+
next_states={
|
1104
|
+
"scan_location",
|
1105
|
+
"scan_destination_all",
|
1106
|
+
"start_single",
|
1107
|
+
"get_work",
|
1108
|
+
}
|
1109
|
+
)
|
1110
|
+
|
1111
|
+
def scan_location(self):
|
1112
|
+
return self._response_schema(
|
1113
|
+
next_states={
|
1114
|
+
"scan_location",
|
1115
|
+
"get_work",
|
1116
|
+
"scan_destination_all",
|
1117
|
+
"start_single",
|
1118
|
+
}
|
1119
|
+
)
|
1120
|
+
|
1121
|
+
def set_destination_all(self):
|
1122
|
+
return self._response_schema(
|
1123
|
+
next_states={"scan_location", "get_work", "scan_destination_all"}
|
1124
|
+
)
|
1125
|
+
|
1126
|
+
def go_to_single(self):
|
1127
|
+
return self._response_schema(
|
1128
|
+
next_states={"scan_location", "get_work", "start_single"}
|
1129
|
+
)
|
1130
|
+
|
1131
|
+
def scan_package(self):
|
1132
|
+
return self._response_schema(
|
1133
|
+
next_states={
|
1134
|
+
"scan_location",
|
1135
|
+
"get_work",
|
1136
|
+
"start_single",
|
1137
|
+
"scan_destination",
|
1138
|
+
}
|
1139
|
+
)
|
1140
|
+
|
1141
|
+
def scan_line(self):
|
1142
|
+
return self._response_schema(
|
1143
|
+
next_states={
|
1144
|
+
"scan_location",
|
1145
|
+
"get_work",
|
1146
|
+
"start_single",
|
1147
|
+
"scan_destination",
|
1148
|
+
}
|
1149
|
+
)
|
1150
|
+
|
1151
|
+
def set_destination_package(self):
|
1152
|
+
return self._response_schema(
|
1153
|
+
next_states={
|
1154
|
+
"scan_location",
|
1155
|
+
"get_work",
|
1156
|
+
"start_single",
|
1157
|
+
"scan_destination",
|
1158
|
+
}
|
1159
|
+
)
|
1160
|
+
|
1161
|
+
def set_destination_line(self):
|
1162
|
+
return self._response_schema(
|
1163
|
+
next_states={
|
1164
|
+
"scan_location",
|
1165
|
+
"get_work",
|
1166
|
+
"start_single",
|
1167
|
+
"scan_destination",
|
1168
|
+
}
|
1169
|
+
)
|
1170
|
+
|
1171
|
+
def postpone_package(self):
|
1172
|
+
return self._response_schema(
|
1173
|
+
next_states={"scan_location", "get_work", "start_single"}
|
1174
|
+
)
|
1175
|
+
|
1176
|
+
def postpone_line(self):
|
1177
|
+
return self._response_schema(
|
1178
|
+
next_states={"scan_location", "get_work", "start_single"}
|
1179
|
+
)
|
1180
|
+
|
1181
|
+
def stock_out_package(self):
|
1182
|
+
return self._response_schema(
|
1183
|
+
next_states={"scan_location", "get_work", "start_single"}
|
1184
|
+
)
|
1185
|
+
|
1186
|
+
def stock_out_line(self):
|
1187
|
+
return self._response_schema(
|
1188
|
+
next_states={"scan_location", "get_work", "start_single"}
|
1189
|
+
)
|
1190
|
+
|
1191
|
+
def dismiss_package_level(self):
|
1192
|
+
return self._response_schema(
|
1193
|
+
next_states={"scan_location", "get_work", "start_single"}
|
1194
|
+
)
|