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.
Files changed (182) hide show
  1. odoo/addons/shopfloor/README.rst +160 -0
  2. odoo/addons/shopfloor/__init__.py +4 -0
  3. odoo/addons/shopfloor/__manifest__.py +65 -0
  4. odoo/addons/shopfloor/actions/__init__.py +15 -0
  5. odoo/addons/shopfloor/actions/change_package_lot.py +164 -0
  6. odoo/addons/shopfloor/actions/completion_info.py +42 -0
  7. odoo/addons/shopfloor/actions/data.py +329 -0
  8. odoo/addons/shopfloor/actions/data_detail.py +154 -0
  9. odoo/addons/shopfloor/actions/inventory.py +150 -0
  10. odoo/addons/shopfloor/actions/location_content_transfer_sorter.py +89 -0
  11. odoo/addons/shopfloor/actions/message.py +846 -0
  12. odoo/addons/shopfloor/actions/move_line_search.py +119 -0
  13. odoo/addons/shopfloor/actions/packaging.py +59 -0
  14. odoo/addons/shopfloor/actions/savepoint.py +44 -0
  15. odoo/addons/shopfloor/actions/schema.py +182 -0
  16. odoo/addons/shopfloor/actions/schema_detail.py +98 -0
  17. odoo/addons/shopfloor/actions/search.py +187 -0
  18. odoo/addons/shopfloor/actions/stock.py +239 -0
  19. odoo/addons/shopfloor/actions/stock_unreserve.py +66 -0
  20. odoo/addons/shopfloor/components/__init__.py +5 -0
  21. odoo/addons/shopfloor/components/scan_handler_location.py +26 -0
  22. odoo/addons/shopfloor/components/scan_handler_lot.py +26 -0
  23. odoo/addons/shopfloor/components/scan_handler_package.py +26 -0
  24. odoo/addons/shopfloor/components/scan_handler_product.py +26 -0
  25. odoo/addons/shopfloor/components/scan_handler_transfer.py +26 -0
  26. odoo/addons/shopfloor/data/shopfloor_scenario_data.xml +73 -0
  27. odoo/addons/shopfloor/demo/shopfloor_app_demo.xml +12 -0
  28. odoo/addons/shopfloor/demo/shopfloor_menu_demo.xml +64 -0
  29. odoo/addons/shopfloor/demo/shopfloor_profile_demo.xml +8 -0
  30. odoo/addons/shopfloor/demo/stock_picking_type_demo.xml +93 -0
  31. odoo/addons/shopfloor/docs/checkout_diag_seq.plantuml +61 -0
  32. odoo/addons/shopfloor/docs/checkout_diag_seq.png +0 -0
  33. odoo/addons/shopfloor/docs/cluster_picking_diag_seq.plantuml +112 -0
  34. odoo/addons/shopfloor/docs/cluster_picking_diag_seq.png +0 -0
  35. odoo/addons/shopfloor/docs/delivery_diag_seq.plantuml +56 -0
  36. odoo/addons/shopfloor/docs/delivery_diag_seq.png +0 -0
  37. odoo/addons/shopfloor/docs/location_content_transfer_diag_seq.plantuml +66 -0
  38. odoo/addons/shopfloor/docs/location_content_transfer_diag_seq.png +0 -0
  39. odoo/addons/shopfloor/docs/oca_logo.png +0 -0
  40. odoo/addons/shopfloor/docs/single_pack_transfer_diag_seq.plantuml +36 -0
  41. odoo/addons/shopfloor/docs/single_pack_transfer_diag_seq.png +0 -0
  42. odoo/addons/shopfloor/docs/zone_picking_diag_seq.plantuml +85 -0
  43. odoo/addons/shopfloor/docs/zone_picking_diag_seq.png +0 -0
  44. odoo/addons/shopfloor/exceptions.py +6 -0
  45. odoo/addons/shopfloor/i18n/ca.po +1802 -0
  46. odoo/addons/shopfloor/i18n/de.po +1791 -0
  47. odoo/addons/shopfloor/i18n/es_AR.po +2147 -0
  48. odoo/addons/shopfloor/i18n/pt_BR.po +1791 -0
  49. odoo/addons/shopfloor/i18n/shopfloor.pot +1877 -0
  50. odoo/addons/shopfloor/models/__init__.py +12 -0
  51. odoo/addons/shopfloor/models/priority_postpone_mixin.py +41 -0
  52. odoo/addons/shopfloor/models/shopfloor_app.py +9 -0
  53. odoo/addons/shopfloor/models/shopfloor_menu.py +436 -0
  54. odoo/addons/shopfloor/models/stock_location.py +76 -0
  55. odoo/addons/shopfloor/models/stock_move.py +119 -0
  56. odoo/addons/shopfloor/models/stock_move_line.py +307 -0
  57. odoo/addons/shopfloor/models/stock_package_level.py +50 -0
  58. odoo/addons/shopfloor/models/stock_picking.py +118 -0
  59. odoo/addons/shopfloor/models/stock_picking_batch.py +41 -0
  60. odoo/addons/shopfloor/models/stock_picking_type.py +26 -0
  61. odoo/addons/shopfloor/models/stock_quant.py +31 -0
  62. odoo/addons/shopfloor/models/stock_quant_package.py +101 -0
  63. odoo/addons/shopfloor/readme/CONTRIBUTORS.rst +18 -0
  64. odoo/addons/shopfloor/readme/CREDITS.rst +5 -0
  65. odoo/addons/shopfloor/readme/DESCRIPTION.rst +17 -0
  66. odoo/addons/shopfloor/readme/HISTORY.rst +4 -0
  67. odoo/addons/shopfloor/readme/ROADMAP.rst +4 -0
  68. odoo/addons/shopfloor/readme/USAGE.rst +6 -0
  69. odoo/addons/shopfloor/security/groups.xml +17 -0
  70. odoo/addons/shopfloor/services/__init__.py +16 -0
  71. odoo/addons/shopfloor/services/checkout.py +1763 -0
  72. odoo/addons/shopfloor/services/cluster_picking.py +1628 -0
  73. odoo/addons/shopfloor/services/delivery.py +828 -0
  74. odoo/addons/shopfloor/services/forms/__init__.py +1 -0
  75. odoo/addons/shopfloor/services/forms/picking_form.py +78 -0
  76. odoo/addons/shopfloor/services/location_content_transfer.py +1194 -0
  77. odoo/addons/shopfloor/services/menu.py +60 -0
  78. odoo/addons/shopfloor/services/picking_batch.py +126 -0
  79. odoo/addons/shopfloor/services/service.py +101 -0
  80. odoo/addons/shopfloor/services/single_pack_transfer.py +366 -0
  81. odoo/addons/shopfloor/services/zone_picking.py +1938 -0
  82. odoo/addons/shopfloor/static/description/icon.png +0 -0
  83. odoo/addons/shopfloor/static/description/index.html +500 -0
  84. odoo/addons/shopfloor/tests/__init__.py +83 -0
  85. odoo/addons/shopfloor/tests/common.py +324 -0
  86. odoo/addons/shopfloor/tests/models.py +29 -0
  87. odoo/addons/shopfloor/tests/test_actions_change_package_lot.py +1175 -0
  88. odoo/addons/shopfloor/tests/test_actions_data.py +376 -0
  89. odoo/addons/shopfloor/tests/test_actions_data_base.py +244 -0
  90. odoo/addons/shopfloor/tests/test_actions_data_detail.py +322 -0
  91. odoo/addons/shopfloor/tests/test_actions_search.py +248 -0
  92. odoo/addons/shopfloor/tests/test_actions_stock.py +48 -0
  93. odoo/addons/shopfloor/tests/test_checkout_auto_post.py +67 -0
  94. odoo/addons/shopfloor/tests/test_checkout_base.py +81 -0
  95. odoo/addons/shopfloor/tests/test_checkout_cancel_line.py +154 -0
  96. odoo/addons/shopfloor/tests/test_checkout_change_packaging.py +184 -0
  97. odoo/addons/shopfloor/tests/test_checkout_done.py +133 -0
  98. odoo/addons/shopfloor/tests/test_checkout_list_delivery_packaging.py +131 -0
  99. odoo/addons/shopfloor/tests/test_checkout_list_package.py +327 -0
  100. odoo/addons/shopfloor/tests/test_checkout_new_package.py +88 -0
  101. odoo/addons/shopfloor/tests/test_checkout_no_package.py +95 -0
  102. odoo/addons/shopfloor/tests/test_checkout_scan.py +174 -0
  103. odoo/addons/shopfloor/tests/test_checkout_scan_line.py +377 -0
  104. odoo/addons/shopfloor/tests/test_checkout_scan_line_base.py +25 -0
  105. odoo/addons/shopfloor/tests/test_checkout_scan_line_no_prefill_qty.py +91 -0
  106. odoo/addons/shopfloor/tests/test_checkout_scan_package_action.py +451 -0
  107. odoo/addons/shopfloor/tests/test_checkout_scan_package_action_no_prefill_qty.py +107 -0
  108. odoo/addons/shopfloor/tests/test_checkout_select.py +74 -0
  109. odoo/addons/shopfloor/tests/test_checkout_select_line.py +130 -0
  110. odoo/addons/shopfloor/tests/test_checkout_select_package_base.py +64 -0
  111. odoo/addons/shopfloor/tests/test_checkout_set_qty.py +257 -0
  112. odoo/addons/shopfloor/tests/test_checkout_summary.py +69 -0
  113. odoo/addons/shopfloor/tests/test_cluster_picking_base.py +83 -0
  114. odoo/addons/shopfloor/tests/test_cluster_picking_batch.py +109 -0
  115. odoo/addons/shopfloor/tests/test_cluster_picking_change_pack_lot.py +111 -0
  116. odoo/addons/shopfloor/tests/test_cluster_picking_is_zero.py +98 -0
  117. odoo/addons/shopfloor/tests/test_cluster_picking_scan_destination.py +376 -0
  118. odoo/addons/shopfloor/tests/test_cluster_picking_scan_destination_no_prefill_qty.py +115 -0
  119. odoo/addons/shopfloor/tests/test_cluster_picking_scan_line.py +402 -0
  120. odoo/addons/shopfloor/tests/test_cluster_picking_scan_line_location_or_pack_first.py +114 -0
  121. odoo/addons/shopfloor/tests/test_cluster_picking_scan_line_no_prefill_qty.py +70 -0
  122. odoo/addons/shopfloor/tests/test_cluster_picking_select.py +387 -0
  123. odoo/addons/shopfloor/tests/test_cluster_picking_skip.py +90 -0
  124. odoo/addons/shopfloor/tests/test_cluster_picking_stock_issue.py +364 -0
  125. odoo/addons/shopfloor/tests/test_cluster_picking_unload.py +911 -0
  126. odoo/addons/shopfloor/tests/test_delivery_base.py +155 -0
  127. odoo/addons/shopfloor/tests/test_delivery_done.py +108 -0
  128. odoo/addons/shopfloor/tests/test_delivery_list_stock_picking.py +49 -0
  129. odoo/addons/shopfloor/tests/test_delivery_reset_qty_done_line.py +119 -0
  130. odoo/addons/shopfloor/tests/test_delivery_reset_qty_done_pack.py +107 -0
  131. odoo/addons/shopfloor/tests/test_delivery_scan_deliver.py +557 -0
  132. odoo/addons/shopfloor/tests/test_delivery_select.py +38 -0
  133. odoo/addons/shopfloor/tests/test_delivery_set_qty_done_line.py +91 -0
  134. odoo/addons/shopfloor/tests/test_delivery_set_qty_done_pack.py +135 -0
  135. odoo/addons/shopfloor/tests/test_delivery_sublocation.py +180 -0
  136. odoo/addons/shopfloor/tests/test_location_content_transfer_base.py +136 -0
  137. odoo/addons/shopfloor/tests/test_location_content_transfer_get_work.py +125 -0
  138. odoo/addons/shopfloor/tests/test_location_content_transfer_mix.py +509 -0
  139. odoo/addons/shopfloor/tests/test_location_content_transfer_putaway.py +143 -0
  140. odoo/addons/shopfloor/tests/test_location_content_transfer_scan_location.py +34 -0
  141. odoo/addons/shopfloor/tests/test_location_content_transfer_set_destination_all.py +343 -0
  142. odoo/addons/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py +1074 -0
  143. odoo/addons/shopfloor/tests/test_location_content_transfer_single.py +748 -0
  144. odoo/addons/shopfloor/tests/test_location_content_transfer_start.py +359 -0
  145. odoo/addons/shopfloor/tests/test_menu_base.py +261 -0
  146. odoo/addons/shopfloor/tests/test_menu_counters.py +61 -0
  147. odoo/addons/shopfloor/tests/test_misc.py +25 -0
  148. odoo/addons/shopfloor/tests/test_move_action_assign.py +87 -0
  149. odoo/addons/shopfloor/tests/test_openapi.py +21 -0
  150. odoo/addons/shopfloor/tests/test_picking_form.py +62 -0
  151. odoo/addons/shopfloor/tests/test_scan_anything.py +49 -0
  152. odoo/addons/shopfloor/tests/test_single_pack_transfer.py +1121 -0
  153. odoo/addons/shopfloor/tests/test_single_pack_transfer_base.py +32 -0
  154. odoo/addons/shopfloor/tests/test_single_pack_transfer_putaway.py +104 -0
  155. odoo/addons/shopfloor/tests/test_stock_split.py +204 -0
  156. odoo/addons/shopfloor/tests/test_user.py +42 -0
  157. odoo/addons/shopfloor/tests/test_zone_picking_base.py +608 -0
  158. odoo/addons/shopfloor/tests/test_zone_picking_change_pack_lot.py +140 -0
  159. odoo/addons/shopfloor/tests/test_zone_picking_select_line.py +723 -0
  160. odoo/addons/shopfloor/tests/test_zone_picking_select_line_first_scan_location.py +207 -0
  161. odoo/addons/shopfloor/tests/test_zone_picking_select_line_first_scan_location.py.bak +202 -0
  162. odoo/addons/shopfloor/tests/test_zone_picking_select_line_no_prefill_qty.py +107 -0
  163. odoo/addons/shopfloor/tests/test_zone_picking_select_picking_type.py +26 -0
  164. odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination.py +643 -0
  165. odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination_no_prefill_qty.py +146 -0
  166. odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py +241 -0
  167. odoo/addons/shopfloor/tests/test_zone_picking_start.py +206 -0
  168. odoo/addons/shopfloor/tests/test_zone_picking_stock_issue.py +121 -0
  169. odoo/addons/shopfloor/tests/test_zone_picking_unload_all.py +353 -0
  170. odoo/addons/shopfloor/tests/test_zone_picking_unload_buffer_lines.py +113 -0
  171. odoo/addons/shopfloor/tests/test_zone_picking_unload_set_destination.py +374 -0
  172. odoo/addons/shopfloor/tests/test_zone_picking_unload_single.py +123 -0
  173. odoo/addons/shopfloor/tests/test_zone_picking_zero_check.py +43 -0
  174. odoo/addons/shopfloor/utils.py +13 -0
  175. odoo/addons/shopfloor/views/shopfloor_menu.xml +167 -0
  176. odoo/addons/shopfloor/views/stock_location.xml +20 -0
  177. odoo/addons/shopfloor/views/stock_move_line.xml +52 -0
  178. odoo/addons/shopfloor/views/stock_picking_type.xml +19 -0
  179. odoo_addon_shopfloor-16.0.1.0.0.24.dist-info/METADATA +192 -0
  180. odoo_addon_shopfloor-16.0.1.0.0.24.dist-info/RECORD +182 -0
  181. odoo_addon_shopfloor-16.0.1.0.0.24.dist-info/WHEEL +5 -0
  182. 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
+ )