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,1763 @@
1
+ # Copyright 2020-2021 Camptocamp SA (http://www.camptocamp.com)
2
+ # Copyright 2020-2021 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
3
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
4
+
5
+ from werkzeug.exceptions import BadRequest
6
+
7
+ from odoo import _, fields
8
+
9
+ from odoo.addons.base_rest.components.service import to_int
10
+ from odoo.addons.component.core import Component
11
+
12
+ from ..utils import to_float
13
+
14
+
15
+ class Checkout(Component):
16
+ """
17
+ Methods for the Checkout Process
18
+
19
+ This scenario runs on existing moves.
20
+ It happens on the "Packing" step of a pick/pack/ship.
21
+
22
+ Use cases:
23
+
24
+ 1) Products are packed (e.g. full pallet shipping) and we keep the packages
25
+ 2) Products are packed (e.g. rollercage bins) and we create a new package
26
+ with same content for shipping
27
+ 3) Products are packed (e.g. half-pallet ) and we merge several into one
28
+ 4) Products are packed (e.g. too high pallet) and we split it on several
29
+ 5) Products are not packed (e.g. raw products) and we create new packages
30
+ 6) Products are not packed (e.g. raw products) and we do not create packages
31
+
32
+ A new flag ``shopfloor_checkout_done`` on move lines allows to track which
33
+ lines have been checked out (can be with or without package).
34
+
35
+ Flow Diagram: https://www.draw.io/#G1qRenBcezk50ggIazDuu2qOfkTsoIAxXP
36
+ """
37
+
38
+ _inherit = "base.shopfloor.process"
39
+ _name = "shopfloor.checkout"
40
+ _usage = "checkout"
41
+ _description = __doc__
42
+
43
+ def _response_for_select_line(
44
+ self, picking, message=None, need_confirm_pack_all=False
45
+ ):
46
+ if all(line.shopfloor_checkout_done for line in picking.move_line_ids):
47
+ return self._response_for_summary(picking, message=message)
48
+ return self._response(
49
+ next_state="select_line",
50
+ data=self._data_for_select_line(
51
+ picking, need_confirm_pack_all=need_confirm_pack_all
52
+ ),
53
+ message=message,
54
+ )
55
+
56
+ def _data_for_select_line(self, picking, need_confirm_pack_all=False):
57
+ return {
58
+ "picking": self._data_for_stock_picking(picking),
59
+ "group_lines_by_location": True,
60
+ "show_oneline_package_content": self.work.menu.show_oneline_package_content,
61
+ "need_confirm_pack_all": need_confirm_pack_all,
62
+ }
63
+
64
+ def _response_for_summary(self, picking, need_confirm=False, message=None):
65
+ return self._response(
66
+ next_state="summary" if not need_confirm else "confirm_done",
67
+ data={
68
+ "picking": self._data_for_stock_picking(picking, done=True),
69
+ "all_processed": not bool(self._lines_to_pack(picking)),
70
+ },
71
+ message=message,
72
+ )
73
+
74
+ def _response_for_select_document(self, message=None):
75
+ return self._response(next_state="select_document", message=message)
76
+
77
+ def _response_for_manual_selection(self, message=None):
78
+ pickings = self.env["stock.picking"].search(
79
+ self._domain_for_list_stock_picking(),
80
+ order=self._order_for_list_stock_picking(),
81
+ )
82
+ data = {"pickings": self.data.pickings(pickings)}
83
+ return self._response(next_state="manual_selection", data=data, message=message)
84
+
85
+ def _response_for_select_package(self, picking, lines, message=None):
86
+ return self._response(
87
+ next_state="select_package",
88
+ data={
89
+ "selected_move_lines": self._data_for_move_lines(lines.sorted()),
90
+ "picking": self.data.picking(picking),
91
+ "packing_info": self._data_for_packing_info(picking),
92
+ "no_package_enabled": not self.options.get(
93
+ "checkout__disable_no_package"
94
+ ),
95
+ },
96
+ message=message,
97
+ )
98
+
99
+ def _data_for_packing_info(self, picking):
100
+ """Return the packing information
101
+
102
+ Intended to be extended.
103
+ """
104
+ # TODO: This could be avoided if included in the picking parser.
105
+ return ""
106
+
107
+ def _response_for_select_dest_package(self, picking, move_lines, message=None):
108
+ packages = picking.mapped("move_line_ids.result_package_id").filtered(
109
+ "package_type_id"
110
+ )
111
+ if not packages:
112
+ # FIXME: do we want to move from 'select_dest_package' to
113
+ # 'select_package' state? Until now (before enforcing the use of
114
+ # delivery package) this part of code was never reached as we
115
+ # always had a package on the picking (source or result)
116
+ # Also the response validator did not support this state...
117
+ return self._response_for_select_package(
118
+ picking,
119
+ move_lines,
120
+ message=self.msg_store.no_valid_package_to_select(),
121
+ )
122
+ picking_data = self.data.picking(picking)
123
+ packages_data = self.data.packages(
124
+ packages.with_context(picking_id=picking.id).sorted(),
125
+ picking=picking,
126
+ with_packaging=True,
127
+ )
128
+ return self._response(
129
+ next_state="select_dest_package",
130
+ data={
131
+ "picking": picking_data,
132
+ "packages": packages_data,
133
+ "selected_move_lines": self._data_for_move_lines(move_lines.sorted()),
134
+ },
135
+ message=message,
136
+ )
137
+
138
+ def _response_for_select_delivery_packaging(self, picking, packaging, message=None):
139
+ return self._response(
140
+ next_state="select_delivery_packaging",
141
+ data={
142
+ # We don't need to send the 'picking' as the mobile frontend
143
+ # already has this info after `select_document` state
144
+ # TODO adapt other endpoints to see if we can get rid of the
145
+ # 'picking' data
146
+ "packaging": self._data_for_delivery_packaging(packaging),
147
+ },
148
+ message=message,
149
+ )
150
+
151
+ def _response_for_change_packaging(self, picking, package, packaging_list):
152
+ if not package:
153
+ return self._response_for_summary(
154
+ picking, message=self.msg_store.record_not_found()
155
+ )
156
+
157
+ return self._response(
158
+ next_state="change_packaging",
159
+ data={
160
+ "picking": self.data.picking(picking),
161
+ "package": self.data.package(
162
+ package, picking=picking, with_packaging=True
163
+ ),
164
+ "packaging": self.data.delivery_packaging_list(
165
+ packaging_list.sorted("sequence")
166
+ ),
167
+ },
168
+ )
169
+
170
+ def scan_document(self, barcode):
171
+ """Scan a package, a product, a transfer or a location
172
+
173
+ When a location is scanned, if all the move lines from this destination
174
+ are for the same stock.picking, the stock.picking is used for the
175
+ next steps.
176
+
177
+ When a package is scanned, if the package has a move line to move it
178
+ from a location/sublocation of the current stock.picking.type, the
179
+ stock.picking for the package is used for the next steps.
180
+
181
+ When a product is scanned, use the first picking (ordered by priority desc,
182
+ scheduled_date asc, id desc) which has an ongoing move line with no source
183
+ package for the given product.
184
+
185
+ When a stock.picking is scanned, it is used for the next steps.
186
+
187
+ In every case above, the stock.picking must be entirely available and
188
+ must match the current picking type.
189
+
190
+ Transitions:
191
+ * select_document: when no stock.picking could be found
192
+ * select_line: a stock.picking is selected
193
+ * summary: stock.picking is selected and all its lines have a
194
+ destination pack set
195
+ """
196
+ search_result = self._scan_document_find(barcode)
197
+ result_handler = getattr(self, "_select_document_from_" + search_result.type)
198
+ return result_handler(search_result.record)
199
+
200
+ def _scan_document_find(self, barcode, search_types=None):
201
+ search = self._actions_for("search")
202
+ search_types = (
203
+ "picking",
204
+ "location",
205
+ "package",
206
+ "product",
207
+ "packaging",
208
+ )
209
+ return search.find(
210
+ barcode,
211
+ types=search_types,
212
+ )
213
+
214
+ def _select_document_from_picking(self, picking, **kw):
215
+ return self._select_picking(picking, "select_document")
216
+
217
+ def _select_document_from_location(self, location, **kw):
218
+ if not self.is_src_location_valid(location):
219
+ return self._response_for_select_document(
220
+ message=self.msg_store.location_not_allowed()
221
+ )
222
+ lines = location.source_move_line_ids
223
+ pickings = lines.mapped("picking_id")
224
+ if len(pickings) > 1:
225
+ return self._response_for_select_document(
226
+ message={
227
+ "message_type": "error",
228
+ "body": _(
229
+ "Several transfers found, please scan a package"
230
+ " or select a transfer manually."
231
+ ),
232
+ }
233
+ )
234
+ return self._select_picking(pickings, "select_document")
235
+
236
+ def _select_document_from_package(self, package, **kw):
237
+ pickings = package.move_line_ids.filtered(
238
+ lambda ml: ml.state not in ("cancel", "done")
239
+ ).mapped("picking_id")
240
+ if len(pickings) > 1:
241
+ # Filter only if we find several pickings to narrow the
242
+ # selection to one of the good type. If we have one picking
243
+ # of the wrong type, it will be caught in _select_picking
244
+ # with the proper error message.
245
+ # Side note: rather unlikely to have several transfers ready
246
+ # and moving the same things
247
+ pickings = pickings.filtered(
248
+ lambda p: p.picking_type_id in self.picking_types
249
+ )
250
+ if len(pickings) == 1:
251
+ picking = pickings
252
+ return self._select_picking(picking, "select_document")
253
+
254
+ def _select_document_from_product(self, product, line_domain=None, **kw):
255
+ line_domain = line_domain or []
256
+ line_domain.extend(
257
+ [
258
+ ("product_id", "=", product.id),
259
+ ("state", "not in", ("cancel", "done")),
260
+ ("package_id", "=", False),
261
+ ]
262
+ )
263
+ lines = self.env["stock.move.line"].search(line_domain)
264
+ picking = self.env["stock.picking"].search(
265
+ [
266
+ ("id", "in", lines.move_id.picking_id.ids),
267
+ ("picking_type_id", "in", self.picking_types.ids),
268
+ ],
269
+ order="priority desc, scheduled_date asc, id desc",
270
+ limit=1,
271
+ )
272
+ return self._select_picking(picking, "select_document")
273
+
274
+ def _select_document_from_packaging(self, packaging, **kw):
275
+ # And retrieve its product
276
+ product = packaging.product_id
277
+ # The picking should have a move line for the product
278
+ # where qty >= packaging.qty, since it doesn't makes sense
279
+ # to select a move line which have less qty than the packaging
280
+ line_domain = [("reserved_uom_qty", ">=", packaging.qty)]
281
+ return self._select_document_from_product(product, line_domain=line_domain)
282
+
283
+ def _select_document_from_none(self, picking, **kw):
284
+ """Handle result when no record is found."""
285
+ return self._select_picking(picking, "select_document")
286
+
287
+ def _select_picking(self, picking, state_for_error):
288
+ if not picking:
289
+ if state_for_error == "manual_selection":
290
+ return self._response_for_manual_selection(
291
+ message=self.msg_store.stock_picking_not_found()
292
+ )
293
+ return self._response_for_select_document(
294
+ message=self.msg_store.barcode_not_found()
295
+ )
296
+ if picking.picking_type_id not in self.picking_types:
297
+ if state_for_error == "manual_selection":
298
+ return self._response_for_manual_selection(
299
+ message=self.msg_store.cannot_move_something_in_picking_type()
300
+ )
301
+ return self._response_for_select_document(
302
+ message=self.msg_store.cannot_move_something_in_picking_type()
303
+ )
304
+ if picking.state != "assigned":
305
+ if state_for_error == "manual_selection":
306
+ return self._response_for_manual_selection(
307
+ message=self.msg_store.stock_picking_not_available(picking)
308
+ )
309
+ return self._response_for_select_document(
310
+ message=self.msg_store.stock_picking_not_available(picking)
311
+ )
312
+ return self._response_for_select_line(picking)
313
+
314
+ def _data_for_move_lines(self, lines, **kw):
315
+ return self.data.move_lines(lines, **kw)
316
+
317
+ def _data_for_delivery_packaging(self, packaging, **kw):
318
+ return self.data.delivery_packaging_list(packaging, **kw)
319
+
320
+ def _data_for_stock_picking(self, picking, done=False):
321
+ data = self.data.picking(picking)
322
+ line_picker = self._lines_checkout_done if done else self._lines_to_pack
323
+ data.update(
324
+ {
325
+ "move_lines": self._data_for_move_lines(
326
+ self._lines_prepare(picking, line_picker(picking)),
327
+ with_packaging=done,
328
+ )
329
+ }
330
+ )
331
+ return data
332
+
333
+ def _lines_checkout_done(self, picking):
334
+ return picking.move_line_ids.filtered(self._filter_lines_checkout_done)
335
+
336
+ def _lines_to_pack(self, picking):
337
+ return picking.move_line_ids.filtered(self._filter_lines_unpacked)
338
+
339
+ def _lines_prepare(self, picking, selected_lines):
340
+ """Hook to manipulate lines' ordering or anything else before sending them back."""
341
+ return selected_lines
342
+
343
+ def _domain_for_list_stock_picking(self):
344
+ return [
345
+ ("state", "=", "assigned"),
346
+ ("picking_type_id", "in", self.picking_types.ids),
347
+ ]
348
+
349
+ def _order_for_list_stock_picking(self):
350
+ return "scheduled_date asc, id asc"
351
+
352
+ def list_stock_picking(self):
353
+ """List stock.picking records available
354
+
355
+ Returns a list of all the available records for the current picking
356
+ type.
357
+
358
+ Transitions:
359
+ * manual_selection: to the selection screen
360
+ """
361
+ return self._response_for_manual_selection()
362
+
363
+ def select(self, picking_id):
364
+ """Select a stock picking for the scenario
365
+
366
+ Used from the list of stock pickings (manual_selection), from there,
367
+ the user can click on a stock.picking record which calls this method.
368
+
369
+ The ``list_stock_picking`` returns only the valid records (same picking
370
+ type, fully available, ...), but this method has to check again in case
371
+ something changed since the list was sent to the client.
372
+
373
+ Transitions:
374
+ * manual_selection: stock.picking could finally not be selected (not
375
+ available, ...)
376
+ * summary: goes straight to this state used to set the moves as done when
377
+ all the move lines with a reserved quantity have a 'quantity done'
378
+ * select_line: the "normal" case, when the user has to put in pack/move
379
+ lines
380
+ """
381
+ picking = self.env["stock.picking"].browse(picking_id)
382
+ message = self._check_picking_status(picking)
383
+ if message:
384
+ return self._response_for_manual_selection(message=message)
385
+ return self._select_picking(picking, "manual_selection")
386
+
387
+ def _select_lines(self, lines, prefill_qty=0, related_lines=None):
388
+ for i, line in enumerate(lines):
389
+ if line.shopfloor_checkout_done:
390
+ continue
391
+ if self.work.menu.no_prefill_qty and i == 0:
392
+ # For prefill quantity we only want to increment one line
393
+ line.qty_done += prefill_qty
394
+ elif not self.work.menu.no_prefill_qty:
395
+ line.qty_done = line.reserved_uom_qty
396
+ line.shopfloor_user_id = self.env.user
397
+
398
+ picking = lines.mapped("picking_id")
399
+ other_lines = picking.move_line_ids - lines
400
+ self._deselect_lines(other_lines)
401
+ if related_lines:
402
+ lines += related_lines
403
+ return lines
404
+
405
+ def _deselect_lines(self, lines):
406
+ lines.filtered(lambda l: not l.shopfloor_checkout_done).write(
407
+ {"qty_done": 0, "shopfloor_user_id": False}
408
+ )
409
+
410
+ def scan_line(self, picking_id, barcode, confirm_pack_all=False):
411
+ """Scan move lines of the stock picking
412
+
413
+ It allows to select move lines of the stock picking for the next
414
+ screen. Lines can be found either by scanning a package, a product or a
415
+ lot.
416
+
417
+ There should be no ambiguity, so for instance if a product is scanned but
418
+ several packs contain it, the endpoint will ask to scan a pack; if the
419
+ product is tracked by lot, to scan a lot.
420
+
421
+ Once move lines are found, their ``qty_done`` is set to their reserved
422
+ quantity.
423
+
424
+ Transitions:
425
+ * select_line: nothing could be found for the barcode
426
+ * select_package: lines are selected, user is redirected to this
427
+ * summary: delivery package is scanned and all lines are done
428
+ screen to change the qty done and destination pack if needed
429
+ """
430
+ picking = self.env["stock.picking"].browse(picking_id)
431
+ message = self._check_picking_status(picking)
432
+ if message:
433
+ return self._response_for_select_document(message=message)
434
+
435
+ selection_lines = self._lines_to_pack(picking)
436
+ if not selection_lines:
437
+ return self._response_for_summary(picking)
438
+
439
+ search_result = self._scan_line_find(picking, barcode)
440
+ result_handler = getattr(self, "_select_lines_from_" + search_result.type)
441
+ kw = {"confirm_pack_all": confirm_pack_all}
442
+ return result_handler(picking, selection_lines, search_result.record, **kw)
443
+
444
+ def _scan_line_find(self, picking, barcode, search_types=None):
445
+ search = self._actions_for("search")
446
+ search_types = (
447
+ "package",
448
+ "product",
449
+ "packaging",
450
+ "lot",
451
+ "serial",
452
+ "delivery_packaging",
453
+ )
454
+ return search.find(
455
+ barcode,
456
+ types=search_types,
457
+ handler_kw=dict(
458
+ lot=dict(products=picking.move_ids.product_id),
459
+ serial=dict(products=picking.move_ids.product_id),
460
+ ),
461
+ )
462
+
463
+ def _select_lines_from_none(self, picking, selection_lines, record, **kw):
464
+ """Handle result when no record is found."""
465
+ return self._response_for_select_line(
466
+ picking, message=self.msg_store.barcode_not_found()
467
+ )
468
+
469
+ def _select_lines_from_package(self, picking, selection_lines, package, **kw):
470
+ lines = selection_lines.filtered(
471
+ lambda l: l.package_id == package and not l.shopfloor_checkout_done
472
+ )
473
+ if not lines:
474
+ return self._response_for_select_line(
475
+ picking,
476
+ message={
477
+ "message_type": "error",
478
+ "body": _("Package {} is not in the current transfer.").format(
479
+ package.name
480
+ ),
481
+ },
482
+ )
483
+ self._select_lines(lines)
484
+ if self.work.menu.no_prefill_qty:
485
+ lines = picking.move_line_ids
486
+ return self._response_for_select_package(picking, lines)
487
+
488
+ def _select_lines_from_product(
489
+ self, picking, selection_lines, product, prefill_qty=1, **kw
490
+ ):
491
+ if product.tracking in ("lot", "serial"):
492
+ return self._response_for_select_line(
493
+ picking, message=self.msg_store.scan_lot_on_product_tracked_by_lot()
494
+ )
495
+
496
+ lines = selection_lines.filtered(lambda l: l.product_id == product)
497
+ if not lines:
498
+ return self._response_for_select_line(
499
+ picking, message=self.msg_store.product_not_found_in_current_picking()
500
+ )
501
+
502
+ # When products are as units outside of packages, we can select them for
503
+ # packing, but if they are in a package, we want the user to scan the packages.
504
+ # If the product is only in one package though, scanning the product selects
505
+ # the package.
506
+ packages = lines.mapped("package_id")
507
+ related_lines = self.env["stock.move.line"].browse()
508
+ # Do not use mapped here: we want to see if we have more than one package,
509
+ # but also if we have one product as a package and the same product as
510
+ # a unit in another line. In both cases, we want the user to scan the
511
+ # package.
512
+ if packages and len({line.package_id for line in lines}) > 1:
513
+ return self._response_for_select_line(
514
+ picking, message=self.msg_store.product_multiple_packages_scan_package()
515
+ )
516
+ elif packages:
517
+ # Select all the lines of the package when we scan a product in a
518
+ # package and we have only one.
519
+ return self._select_lines_from_package(picking, selection_lines, packages)
520
+ else:
521
+ # There is no package on selected lines, so also select all other lines
522
+ # not in a package. But only the quantity on first selected lines
523
+ # are updated.
524
+ related_lines = selection_lines.filtered(
525
+ lambda l: not l.package_id and l.product_id != product
526
+ )
527
+
528
+ lines = self._select_lines(
529
+ lines, prefill_qty=prefill_qty, related_lines=related_lines
530
+ )
531
+ return self._response_for_select_package(picking, lines)
532
+
533
+ def _select_lines_from_packaging(self, picking, selection_lines, packaging, **kw):
534
+ return self._select_lines_from_product(
535
+ picking, selection_lines, packaging.product_id, prefill_qty=packaging.qty
536
+ )
537
+
538
+ def _select_lines_from_lot(self, picking, selection_lines, lot, **kw):
539
+ lines = selection_lines.filtered(lambda l: l.lot_id == lot)
540
+ if not lines:
541
+ return self._response_for_select_line(
542
+ picking,
543
+ message={
544
+ "message_type": "error",
545
+ "body": _("Lot is not in the current transfer."),
546
+ },
547
+ )
548
+
549
+ # When lots are as units outside of packages, we can select them for
550
+ # packing, but if they are in a package, we want the user to scan the packages.
551
+ # If the product is only in one package though, scanning the lot selects
552
+ # the package.
553
+ packages = lines.mapped("package_id")
554
+ # Do not use mapped here: we want to see if we have more than one
555
+ # package, but also if we have one lot as a package and the same lot as
556
+ # a unit in another line. In both cases, we want the user to scan the
557
+ # package.
558
+ if packages and len({line.package_id for line in lines}) > 1:
559
+ return self._response_for_select_line(
560
+ picking, message=self.msg_store.lot_multiple_packages_scan_package()
561
+ )
562
+ elif packages:
563
+ # Select all the lines of the package when we scan a lot in a
564
+ # package and we have only one.
565
+ return self._select_lines_from_package(
566
+ picking, selection_lines, packages, **kw
567
+ )
568
+
569
+ self._select_lines(lines, prefill_qty=1)
570
+ return self._response_for_select_package(picking, lines)
571
+
572
+ def _select_lines_from_serial(self, picking, selection_lines, lot, **kw):
573
+ # Search for serial number is actually the same as searching for lot (as of v14...)
574
+ return self._select_lines_from_lot(picking, selection_lines, lot, **kw)
575
+
576
+ def _select_lines_from_delivery_packaging(
577
+ self, picking, selection_lines, packaging, confirm_pack_all=False, **kw
578
+ ):
579
+ """Handle delivery packaging.
580
+
581
+
582
+ If a delivery pkg has been scanned:
583
+
584
+ 1. validate it
585
+ 2. ask for confirmation to place all lines left into the same package
586
+ 3. if scanned twice for confirmation,
587
+ assign new package and skip `select_package` state
588
+
589
+ """
590
+ carrier = self._get_carrier(picking)
591
+ if carrier:
592
+ # Validate against carrier
593
+ is_valid = self._packaging_good_for_carrier(packaging, carrier)
594
+ else:
595
+ is_valid = True
596
+ if carrier and not is_valid:
597
+ return self._response_for_select_line(
598
+ picking,
599
+ message=self.msg_store.packaging_invalid_for_carrier(
600
+ packaging, carrier
601
+ ),
602
+ )
603
+ if confirm_pack_all:
604
+ # Select all lines and pack them all w/o passing for select_package state
605
+ self._select_lines(selection_lines)
606
+ return self._create_and_assign_new_packaging(
607
+ picking, selection_lines, packaging=packaging
608
+ )
609
+ return self._response_for_select_line(
610
+ picking,
611
+ message=self.msg_store.confirm_put_all_goods_in_delivery_package(packaging),
612
+ need_confirm_pack_all=True,
613
+ )
614
+
615
+ def _select_line_package(self, picking, selection_lines, package):
616
+ if not package:
617
+ return self._response_for_select_line(
618
+ picking, message=self.msg_store.record_not_found()
619
+ )
620
+ return self._select_lines_from_package(picking, selection_lines, package)
621
+
622
+ def _select_line_move_line(self, picking, selection_lines, move_line):
623
+ if not move_line:
624
+ return self._response_for_select_line(
625
+ picking, message=self.msg_store.record_not_found()
626
+ )
627
+ # normally, the client should sent only move lines out of packages, but
628
+ # in case there is a package, handle it as a package
629
+ if move_line.package_id:
630
+ return self._select_lines_from_package(
631
+ picking, selection_lines, move_line.package_id
632
+ )
633
+ self._select_lines(move_line)
634
+ return self._response_for_select_package(picking, move_line)
635
+
636
+ def select_line(self, picking_id, package_id=None, move_line_id=None):
637
+ """Select move lines of the stock picking
638
+
639
+ This is the same as ``scan_line``, except that a package id or a
640
+ move_line_id is given by the client (user clicked on a list).
641
+
642
+ It returns a list of move line ids that will be displayed by the
643
+ screen ``select_package``. This screen will have to send this list to
644
+ the endpoints it calls, so we can select/deselect lines but still
645
+ show them in the list of the client application.
646
+
647
+ Transitions:
648
+ * select_line: nothing could be found for the barcode
649
+ * select_package: lines are selected, user is redirected to this
650
+ screen to change the qty done and destination package if needed
651
+ """
652
+ assert package_id or move_line_id
653
+
654
+ picking = self.env["stock.picking"].browse(picking_id)
655
+ message = self._check_picking_status(picking)
656
+ if message:
657
+ return self._response_for_select_document(message=message)
658
+
659
+ selection_lines = self._lines_to_pack(picking)
660
+ if not selection_lines:
661
+ return self._response_for_summary(picking)
662
+
663
+ if package_id:
664
+ package = self.env["stock.quant.package"].browse(package_id).exists()
665
+ return self._select_line_package(picking, selection_lines, package)
666
+ if move_line_id:
667
+ move_line = self.env["stock.move.line"].browse(move_line_id).exists()
668
+ return self._select_line_move_line(picking, selection_lines, move_line)
669
+
670
+ def _change_line_qty(
671
+ self, picking_id, selected_line_ids, move_line_ids, quantity_func
672
+ ):
673
+ picking = self.env["stock.picking"].browse(picking_id)
674
+ message = self._check_picking_status(picking)
675
+ if message:
676
+ return self._response_for_select_document(message=message)
677
+
678
+ move_lines = self.env["stock.move.line"].browse(move_line_ids).exists()
679
+
680
+ message = None
681
+ if not move_lines:
682
+ message = self.msg_store.record_not_found()
683
+ for move_line in move_lines:
684
+ qty_done = quantity_func(move_line)
685
+ if qty_done < 0:
686
+ message = {
687
+ "body": _("Negative quantity not allowed."),
688
+ "message_type": "error",
689
+ }
690
+ else:
691
+ new_line = self.env["stock.move.line"]
692
+ if qty_done > 0:
693
+ new_line, qty_check = move_line._split_qty_to_be_done(
694
+ qty_done,
695
+ split_partial=False,
696
+ result_package_id=False,
697
+ )
698
+ move_line.qty_done = qty_done
699
+ if new_line:
700
+ selected_line_ids.append(new_line.id)
701
+ if qty_done > move_line.reserved_uom_qty:
702
+ return self._response_for_select_package(
703
+ picking,
704
+ self.env["stock.move.line"].browse(selected_line_ids).exists(),
705
+ message=self.msg_store.line_scanned_qty_done_higher_than_allowed(),
706
+ )
707
+ return self._response_for_select_package(
708
+ picking,
709
+ self.env["stock.move.line"].browse(selected_line_ids).exists(),
710
+ message=message,
711
+ )
712
+
713
+ def reset_line_qty(self, picking_id, selected_line_ids, move_line_id):
714
+ """Reset qty_done of a move line to zero
715
+
716
+ Used to deselect a line in the "select_package" screen.
717
+ The selected_line_ids parameter is used to keep the selection of lines
718
+ stateless.
719
+
720
+ Transitions:
721
+ * select_package: goes back to the same state, the line will appear
722
+ as deselected
723
+ """
724
+ return self._change_line_qty(
725
+ picking_id, selected_line_ids, [move_line_id], lambda __: 0
726
+ )
727
+
728
+ def set_line_qty(self, picking_id, selected_line_ids, move_line_id):
729
+ """Set qty_done of a move line to its reserved quantity
730
+
731
+ Used to select a line in the "select_package" screen.
732
+ The selected_line_ids parameter is used to keep the selection of lines
733
+ stateless.
734
+
735
+ Transitions:
736
+ * select_package: goes back to the same state, the line will appear
737
+ as selected
738
+ """
739
+ return self._change_line_qty(
740
+ picking_id, selected_line_ids, [move_line_id], lambda l: l.reserved_uom_qty
741
+ )
742
+
743
+ def set_custom_qty(self, picking_id, selected_line_ids, move_line_id, qty_done):
744
+ """Change qty_done of a move line with a custom value
745
+
746
+ The selected_line_ids parameter is used to keep the selection of lines
747
+ stateless.
748
+
749
+ Transitions:
750
+ * select_package: goes back to this screen showing all the lines after
751
+ we changed the qty
752
+ """
753
+ return self._change_line_qty(
754
+ picking_id, selected_line_ids, [move_line_id], lambda __: qty_done
755
+ )
756
+
757
+ def _switch_line_qty_done(self, picking, selected_lines, switch_lines):
758
+ """Switch qty_done on lines and return to the 'select_package' state
759
+
760
+ If at least one of the lines to switch has a qty_done, set them all
761
+ to zero. If all the lines to switch have a zero qty_done, switch them
762
+ to their quantity to deliver.
763
+ """
764
+ if any(line.qty_done for line in switch_lines):
765
+ return self._change_line_qty(
766
+ picking.id, selected_lines.ids, switch_lines.ids, lambda __: 0
767
+ )
768
+ else:
769
+ return self._change_line_qty(
770
+ picking.id,
771
+ selected_lines.ids,
772
+ switch_lines.ids,
773
+ lambda l: l.reserved_uom_qty,
774
+ )
775
+
776
+ def _increment_custom_qty(
777
+ self, picking, selected_lines, increment_lines, qty_increment
778
+ ):
779
+ """Increment the qty_done of a move line with a custom value
780
+
781
+ The selected_line parameter is used to keep the selection of lines
782
+ stateless.
783
+
784
+ Transitions:
785
+ * select_package: goes back to this screen showing all the lines after
786
+ we changed the qty
787
+ """
788
+ return self._change_line_qty(
789
+ picking.id,
790
+ selected_lines.ids,
791
+ increment_lines.ids,
792
+ lambda line: line.qty_done + qty_increment,
793
+ )
794
+
795
+ @staticmethod
796
+ def _filter_lines_unpacked(move_line):
797
+ return (
798
+ move_line.qty_done == 0 or move_line.shopfloor_user_id
799
+ ) and not move_line.shopfloor_checkout_done
800
+
801
+ @staticmethod
802
+ def _filter_lines_to_pack(move_line):
803
+ return move_line.qty_done > 0 and not move_line.shopfloor_checkout_done
804
+
805
+ @staticmethod
806
+ def _filter_lines_checkout_done(move_line):
807
+ return move_line.qty_done > 0 and move_line.shopfloor_checkout_done
808
+
809
+ def _is_package_allowed(self, picking, package):
810
+ """Check if a package is allowed as a destination/delivery package.
811
+
812
+ A package is allowed as a destination one if it is present among
813
+ `picking` lines and qualified as a "delivery package" (having a
814
+ delivery packaging set on it).
815
+ """
816
+ existing_packages = picking.mapped("move_line_ids.result_package_id").filtered(
817
+ "package_type_id"
818
+ )
819
+ return package in existing_packages
820
+
821
+ def _put_lines_in_package(self, picking, selected_lines, package):
822
+ """Put the current selected lines with a qty_done in a package
823
+
824
+ Note: only packages which are already a delivery package for another
825
+ line of the stock picking can be selected. Packages which are the
826
+ source packages are allowed too only if it is a delivery package (we
827
+ keep the current package).
828
+ """
829
+ if not self._is_package_allowed(picking, package):
830
+ return self._response_for_select_package(
831
+ picking,
832
+ selected_lines,
833
+ message=self.msg_store.dest_package_not_valid(package),
834
+ )
835
+ return self._pack_lines(picking, selected_lines, package)
836
+
837
+ def _put_lines_in_allowed_package(self, picking, lines_to_pack, package):
838
+ for line in lines_to_pack:
839
+ if line.qty_done < line.reserved_uom_qty:
840
+ line._split_partial_quantity_to_be_done(line.qty_done, {})
841
+ lines_to_pack.write(
842
+ {"result_package_id": package.id, "shopfloor_checkout_done": True}
843
+ )
844
+ self._post_put_lines_in_package(lines_to_pack)
845
+ # Hook to this method to override the response
846
+ # if anything else has to be handled
847
+ # before auto posting the lines.
848
+ return {}
849
+
850
+ def _post_put_lines_in_package(self, lines_packaged):
851
+ """Hook to override."""
852
+
853
+ def _create_and_assign_new_packaging(self, picking, selected_lines, packaging=None):
854
+ actions = self._actions_for("packaging")
855
+ package = actions.create_package_from_packaging(packaging=packaging)
856
+ return self._pack_lines(picking, selected_lines, package)
857
+
858
+ def _pack_lines(self, picking, selected_lines, package):
859
+ lines_to_pack = selected_lines.filtered(self._filter_lines_to_pack)
860
+ if not lines_to_pack:
861
+ message = self.msg_store.no_line_to_pack()
862
+ return self._response_for_select_line(
863
+ picking,
864
+ message=message,
865
+ )
866
+ response = self._put_lines_in_allowed_package(picking, lines_to_pack, package)
867
+ if response:
868
+ return response
869
+ if self.work.menu.auto_post_line:
870
+ # If option auto_post_line is active in the shopfloor menu,
871
+ # create a split order with these packed lines.
872
+ self._auto_post_lines(lines_to_pack)
873
+ message = self.msg_store.goods_packed_in(package)
874
+ # go back to the screen to select the next lines to pack
875
+ return self._response_for_select_line(
876
+ picking,
877
+ message=message,
878
+ )
879
+
880
+ def scan_package_action(self, picking_id, selected_line_ids, barcode):
881
+ """Scan a package, a lot, a product or a package to handle a line
882
+
883
+ When a package is scanned (only delivery ones), if the package is known
884
+ as the destination package of one of the lines or is the source package
885
+ of a selected line, the package is set to be the destination package of
886
+ all the lines to pack.
887
+
888
+ When a product is scanned, it selects (set qty_done = reserved qty) or
889
+ deselects (set qty_done = 0) the move lines for this product. Only
890
+ products not tracked by lot can use this.
891
+
892
+ When a lot is scanned, it does the same as for the products but based
893
+ on the lot.
894
+
895
+ When a packaging type (one without related product) is scanned, a new
896
+ package is created and set as destination of the lines to pack.
897
+
898
+ Lines to pack are move lines in the list of ``selected_line_ids``
899
+ where ``qty_done`` > 0 and have not been packed yet
900
+ (``shopfloor_checkout_done is False``).
901
+
902
+ Transitions:
903
+ * select_package: when a product or lot is scanned to select/deselect,
904
+ the client app has to show the same screen with the updated selection
905
+ * select_line: when a package or packaging type is scanned, move lines
906
+ have been put in package and we can return back to this state to handle
907
+ the other lines
908
+ * summary: if there is no other lines, go to the summary screen to be able
909
+ to close the stock picking
910
+ """
911
+ picking = self.env["stock.picking"].browse(picking_id)
912
+ message = self._check_picking_status(picking)
913
+ if message:
914
+ return self._response_for_select_document(message=message)
915
+
916
+ selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists()
917
+ search_result = self._scan_package_find(picking, barcode)
918
+ result_handler = getattr(
919
+ self, "_scan_package_action_from_" + search_result.type
920
+ )
921
+ return result_handler(picking, selected_lines, search_result.record)
922
+
923
+ def _scan_package_find(self, picking, barcode, search_types=None):
924
+ search = self._actions_for("search")
925
+ search_types = (
926
+ "package",
927
+ "product",
928
+ "packaging",
929
+ "lot",
930
+ "serial",
931
+ "delivery_packaging",
932
+ )
933
+ return search.find(
934
+ barcode,
935
+ types=search_types,
936
+ handler_kw=dict(
937
+ lot=dict(products=picking.move_ids.product_id),
938
+ serial=dict(products=picking.move_ids.product_id),
939
+ ),
940
+ )
941
+
942
+ def _scan_package_action_from_product(
943
+ self, picking, selected_lines, product, packaging=None, **kw
944
+ ):
945
+ if product.tracking in ("lot", "serial"):
946
+ return self._response_for_select_package(
947
+ picking,
948
+ selected_lines,
949
+ message=self.msg_store.scan_lot_on_product_tracked_by_lot(),
950
+ )
951
+ product_lines = selected_lines.filtered(lambda l: l.product_id == product)
952
+ if self.work.menu.no_prefill_qty:
953
+ quantity_increment = packaging.qty if packaging else 1
954
+ return self._increment_custom_qty(
955
+ picking,
956
+ selected_lines,
957
+ fields.first(product_lines),
958
+ quantity_increment,
959
+ )
960
+ return self._switch_line_qty_done(picking, selected_lines, product_lines)
961
+
962
+ def _scan_package_action_from_packaging(
963
+ self, picking, selected_lines, packaging, **kw
964
+ ):
965
+ return self._scan_package_action_from_product(
966
+ picking, selected_lines, packaging.product_id, packaging=packaging
967
+ )
968
+
969
+ def _scan_package_action_from_lot(self, picking, selected_lines, lot, **kw):
970
+ lot_lines = selected_lines.filtered(lambda l: l.lot_id == lot)
971
+ if self.work.menu.no_prefill_qty:
972
+ return self._increment_custom_qty(
973
+ picking, selected_lines, fields.first(lot_lines), 1
974
+ )
975
+ return self._switch_line_qty_done(picking, selected_lines, lot_lines)
976
+
977
+ def _scan_package_action_from_serial(self, picking, selection_lines, lot, **kw):
978
+ # Search for serial number is actually the same as searching for lot (as of v14...)
979
+ return self._scan_package_action_from_lot(picking, selection_lines, lot, **kw)
980
+
981
+ def _scan_package_action_from_package(self, picking, selected_lines, package, **kw):
982
+ if not package.package_type_id:
983
+ return self._response_for_select_package(
984
+ picking,
985
+ selected_lines,
986
+ message=self.msg_store.dest_package_not_valid(package),
987
+ )
988
+ return self._put_lines_in_package(picking, selected_lines, package)
989
+
990
+ def _scan_package_action_from_delivery_packaging(
991
+ self, picking, selected_lines, packaging, **kw
992
+ ):
993
+ carrier = self._get_carrier(picking)
994
+ if carrier:
995
+ # Validate against carrier
996
+ is_valid = self._packaging_type_good_for_carrier(packaging, carrier)
997
+ else:
998
+ is_valid = True
999
+ if carrier and not is_valid:
1000
+ return self._response_for_select_package(
1001
+ picking,
1002
+ selected_lines,
1003
+ message=self.msg_store.packaging_invalid_for_carrier(
1004
+ packaging, carrier
1005
+ ),
1006
+ )
1007
+ return self._create_and_assign_new_packaging(picking, selected_lines, packaging)
1008
+
1009
+ def _scan_package_action_from_none(self, picking, selected_lines, record, **kw):
1010
+ return self._response_for_select_package(
1011
+ picking, selected_lines, message=self.msg_store.barcode_not_found()
1012
+ )
1013
+
1014
+ def _get_carrier(self, picking):
1015
+ return picking.ship_carrier_id or picking.carrier_id
1016
+
1017
+ def _packaging_type_good_for_carrier(self, packaging, carrier):
1018
+ actions = self._actions_for("packaging")
1019
+ return actions.packaging_type_valid_for_carrier(packaging, carrier)
1020
+
1021
+ def _packaging_good_for_carrier(self, packaging, carrier):
1022
+ actions = self._actions_for("packaging")
1023
+ return actions.packaging_valid_for_carrier(packaging, carrier)
1024
+
1025
+ def _get_available_delivery_packaging(self, picking):
1026
+ model = self.env["stock.package.type"]
1027
+ carrier = picking.ship_carrier_id or picking.carrier_id
1028
+ if not carrier:
1029
+ return model.browse()
1030
+ return model.search(
1031
+ [("package_carrier_type", "=", carrier.delivery_type or "none")],
1032
+ order="name",
1033
+ )
1034
+
1035
+ def list_delivery_packaging(self, picking_id, selected_line_ids):
1036
+ """List available delivery packaging for given picking.
1037
+
1038
+ Transitions:
1039
+ * select_delivery_packaging: list available delivery packaging, the
1040
+ user has to choose one to create the new package
1041
+ * select_package: when no delivery packaging is available
1042
+ """
1043
+ picking = self.env["stock.picking"].browse(picking_id)
1044
+ message = self._check_picking_status(picking)
1045
+ if message:
1046
+ return self._response_for_select_document(message=message)
1047
+ selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists()
1048
+ delivery_packaging = self._get_available_delivery_packaging(picking)
1049
+ if not delivery_packaging:
1050
+ return self._response_for_select_package(
1051
+ picking,
1052
+ selected_lines,
1053
+ message=self.msg_store.no_delivery_packaging_available(),
1054
+ )
1055
+ response = self._check_allowed_qty_done(picking, selected_lines)
1056
+ if response:
1057
+ return response
1058
+ return self._response_for_select_delivery_packaging(picking, delivery_packaging)
1059
+
1060
+ def new_package(self, picking_id, selected_line_ids, package_type_id=None):
1061
+ """Add all selected lines in a new package
1062
+
1063
+ It creates a new package and set it as the destination package of all
1064
+ the selected lines.
1065
+
1066
+ Selected lines are move lines in the list of ``move_line_ids`` where
1067
+ ``qty_done`` > 0 and have no destination package
1068
+ (shopfloor_checkout_done is False).
1069
+
1070
+ Transitions:
1071
+ * select_line: goes back to selection of lines to work on next lines
1072
+ """
1073
+ picking = self.env["stock.picking"].browse(picking_id)
1074
+ message = self._check_picking_status(picking)
1075
+ if message:
1076
+ return self._response_for_select_document(message=message)
1077
+ packaging = None
1078
+ if package_type_id:
1079
+ packaging = self.env["stock.package.type"].browse(package_type_id).exists()
1080
+ selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists()
1081
+ return self._create_and_assign_new_packaging(picking, selected_lines, packaging)
1082
+
1083
+ def no_package(self, picking_id, selected_line_ids):
1084
+ """Process all selected lines without any package.
1085
+
1086
+ Selected lines are move lines in the list of ``move_line_ids`` where
1087
+ ``qty_done`` > 0 and have no destination package
1088
+ (shopfloor_checkout_done is False).
1089
+
1090
+ Transitions:
1091
+ * select_line: goes back to selection of lines to work on next lines
1092
+ """
1093
+ if self.options.get("checkout__disable_no_package"):
1094
+ raise BadRequest("`checkout.no_package` endpoint is not enabled")
1095
+ picking = self.env["stock.picking"].browse(picking_id)
1096
+ message = self._check_picking_status(picking)
1097
+ if message:
1098
+ return self._response_for_select_document(message=message)
1099
+ selected_lines = self.env["stock.move.line"].browse(selected_line_ids).exists()
1100
+ selected_lines.write(
1101
+ {"shopfloor_checkout_done": True, "result_package_id": False}
1102
+ )
1103
+ response = self._check_allowed_qty_done(picking, selected_lines)
1104
+ if response:
1105
+ return response
1106
+ return self._response_for_select_line(
1107
+ picking,
1108
+ message={
1109
+ "message_type": "success",
1110
+ "body": _("Product(s) processed as raw product(s)"),
1111
+ },
1112
+ )
1113
+
1114
+ def list_dest_package(self, picking_id, selected_line_ids):
1115
+ """Return a list of packages the user can select for the lines
1116
+
1117
+ Only valid packages must be proposed. Look at ``scan_dest_package``
1118
+ for the conditions to be valid.
1119
+
1120
+ Transitions:
1121
+ * select_dest_package: selection screen
1122
+ * select_package: when no package is available
1123
+ """
1124
+ picking = self.env["stock.picking"].browse(picking_id)
1125
+ message = self._check_picking_status(picking)
1126
+ if message:
1127
+ return self._response_for_select_document(message=message)
1128
+ lines = self.env["stock.move.line"].browse(selected_line_ids).exists()
1129
+ response = self._check_allowed_qty_done(picking, lines)
1130
+ if response:
1131
+ return response
1132
+ return self._response_for_select_dest_package(picking, lines)
1133
+
1134
+ def _check_allowed_qty_done(self, picking, lines):
1135
+ for line in lines:
1136
+ # Do not allow to proceed if the qty_done of
1137
+ # any of the selected lines
1138
+ # is higher than the quantity to do.
1139
+ if line.qty_done > line.reserved_uom_qty:
1140
+ return self._response_for_select_package(
1141
+ picking,
1142
+ lines,
1143
+ message=self.msg_store.selected_lines_qty_done_higher_than_allowed(),
1144
+ )
1145
+
1146
+ def _set_dest_package_from_selection(self, picking, selected_lines, package):
1147
+ if not self._is_package_allowed(picking, package):
1148
+ return self._response_for_select_dest_package(
1149
+ picking,
1150
+ selected_lines,
1151
+ message=self.msg_store.dest_package_not_valid(package),
1152
+ )
1153
+ return self._pack_lines(picking, selected_lines, package)
1154
+
1155
+ def scan_dest_package(self, picking_id, selected_line_ids, barcode):
1156
+ """Scan destination package for lines
1157
+
1158
+ Set the destination package on the selected lines with a `qty_done` if
1159
+ the package is valid. It is valid when one of:
1160
+
1161
+ * it is already the destination package of another line of the stock.picking
1162
+ * it is the source package of the selected lines
1163
+
1164
+ Note: by default, Odoo puts the same destination package as the source
1165
+ package on lines.
1166
+
1167
+ Transitions:
1168
+ * select_dest_package: error when scanning package
1169
+ * select_line: lines to package remain
1170
+ * summary: all lines are put in packages
1171
+ """
1172
+ picking = self.env["stock.picking"].browse(picking_id)
1173
+ message = self._check_picking_status(picking)
1174
+ if message:
1175
+ return self._response_for_select_document(message=message)
1176
+ lines = self.env["stock.move.line"].browse(selected_line_ids).exists()
1177
+ search = self._actions_for("search")
1178
+ package = search.package_from_scan(barcode)
1179
+ if not package:
1180
+ return self._response_for_select_dest_package(
1181
+ picking,
1182
+ lines,
1183
+ message=self.msg_store.package_not_found_for_barcode(barcode),
1184
+ )
1185
+ return self._set_dest_package_from_selection(picking, lines, package)
1186
+
1187
+ def set_dest_package(self, picking_id, selected_line_ids, package_id):
1188
+ """Set destination package for lines from a package id
1189
+
1190
+ Used by the list obtained from ``list_dest_package``.
1191
+
1192
+ The validity is the same as ``scan_dest_package``.
1193
+
1194
+ Transitions:
1195
+ * select_dest_package: error when selecting package
1196
+ * select_line: lines to package remain
1197
+ * summary: all lines are put in packages
1198
+ """
1199
+ picking = self.env["stock.picking"].browse(picking_id)
1200
+ message = self._check_picking_status(picking)
1201
+ if message:
1202
+ return self._response_for_select_document(message=message)
1203
+ lines = self.env["stock.move.line"].browse(selected_line_ids).exists()
1204
+ package = self.env["stock.quant.package"].browse(package_id).exists()
1205
+ if not package:
1206
+ return self._response_for_select_dest_package(
1207
+ picking,
1208
+ lines,
1209
+ message=self.msg_store.record_not_found(),
1210
+ )
1211
+ return self._set_dest_package_from_selection(picking, lines, package)
1212
+
1213
+ def _auto_post_lines(self, selected_lines):
1214
+ moves = self.env["stock.move"]
1215
+ for line in selected_lines:
1216
+ move = line.move_id.split_other_move_lines(line, intersection=True)
1217
+ moves = moves | move
1218
+ moves.extract_and_action_done()
1219
+
1220
+ def summary(self, picking_id):
1221
+ """Return information for the summary screen
1222
+
1223
+ Transitions:
1224
+ * summary
1225
+ """
1226
+ picking = self.env["stock.picking"].browse(picking_id)
1227
+ message = self._check_picking_status(picking)
1228
+ if message:
1229
+ return self._response_for_select_document(message=message)
1230
+ return self._response_for_summary(picking)
1231
+
1232
+ def _get_allowed_packaging(self):
1233
+ return self.env["stock.package.type"].search([])
1234
+
1235
+ def list_packaging(self, picking_id, package_id):
1236
+ """List the available package types for a package
1237
+
1238
+ For a package, we can change the packaging. The available
1239
+ packaging are the ones with no product.
1240
+
1241
+ Transitions:
1242
+ * change_packaging
1243
+ * summary: if the package_id no longer exists
1244
+ """
1245
+ picking = self.env["stock.picking"].browse(picking_id)
1246
+ message = self._check_picking_status(picking)
1247
+ if message:
1248
+ return self._response_for_select_document(message=message)
1249
+ package = self.env["stock.quant.package"].browse(package_id).exists()
1250
+ packaging_list = self._get_allowed_packaging()
1251
+ return self._response_for_change_packaging(picking, package, packaging_list)
1252
+
1253
+ def set_packaging(self, picking_id, package_id, package_type_id):
1254
+ """Set a package type on a package
1255
+
1256
+ Transitions:
1257
+ * change_packaging: in case of error
1258
+ * summary
1259
+ """
1260
+ picking = self.env["stock.picking"].browse(picking_id)
1261
+ message = self._check_picking_status(picking)
1262
+ if message:
1263
+ return self._response_for_select_document(message=message)
1264
+
1265
+ package = self.env["stock.quant.package"].browse(package_id).exists()
1266
+ packaging = self.env["stock.package.type"].browse(package_type_id).exists()
1267
+ if not (package and packaging):
1268
+ return self._response_for_summary(
1269
+ picking, message=self.msg_store.record_not_found()
1270
+ )
1271
+ package.package_type_id = packaging
1272
+ return self._response_for_summary(
1273
+ picking,
1274
+ message={
1275
+ "message_type": "success",
1276
+ "body": _("Packaging changed on package {}").format(package.name),
1277
+ },
1278
+ )
1279
+
1280
+ def cancel_line(self, picking_id, package_id=None, line_id=None):
1281
+ """Cancel work done on given line or package.
1282
+
1283
+ If package, remove destination package from lines and set qty done to 0.
1284
+ If line is a raw product, set qty done to 0.
1285
+
1286
+ All the move lines with the package as ``result_package_id`` have their
1287
+ ``result_package_id`` reset to the source package (default odoo behavior)
1288
+ and their ``qty_done`` set to 0.
1289
+
1290
+ It flags ``shopfloor_checkout_done`` to False
1291
+ so they have to be processed again.
1292
+
1293
+ Transitions:
1294
+ * summary: if package or line are not found
1295
+ * select_line: when package or line has been canceled
1296
+ """
1297
+ picking = self.env["stock.picking"].browse(picking_id)
1298
+ message = self._check_picking_status(picking)
1299
+ if message:
1300
+ return self._response_for_select_document(message=message)
1301
+
1302
+ package = self.env["stock.quant.package"].browse(package_id).exists()
1303
+ line = self.env["stock.move.line"].browse(line_id).exists()
1304
+ if not package and not line:
1305
+ return self._response_for_summary(
1306
+ picking, message=self.msg_store.record_not_found()
1307
+ )
1308
+
1309
+ if package:
1310
+ move_lines = picking.move_line_ids.filtered(
1311
+ lambda l: self._filter_lines_checkout_done(l)
1312
+ and l.result_package_id == package
1313
+ )
1314
+ for move_line in move_lines:
1315
+ move_line.write(
1316
+ {
1317
+ "qty_done": 0,
1318
+ "result_package_id": move_line.package_id,
1319
+ "shopfloor_checkout_done": False,
1320
+ }
1321
+ )
1322
+ msg = _("Package cancelled")
1323
+ if line:
1324
+ line.write({"qty_done": 0, "shopfloor_checkout_done": False})
1325
+ msg = _("Line cancelled")
1326
+ return self._response_for_select_line(
1327
+ picking, message={"message_type": "success", "body": msg}
1328
+ )
1329
+
1330
+ def done(self, picking_id, confirmation=False):
1331
+ """Set the moves as done
1332
+
1333
+ If some lines have not the full ``qty_done`` or no destination package set,
1334
+ a confirmation is asked to the user.
1335
+
1336
+ Transitions:
1337
+ * summary: in case of error
1338
+ * select_document: after done, goes back to start
1339
+ * confirm_done: confirm a partial
1340
+ """
1341
+ picking = self.env["stock.picking"].browse(picking_id)
1342
+ message = self._check_picking_status(picking)
1343
+ if message:
1344
+ return self._response_for_select_document(message=message)
1345
+ lines = picking.move_line_ids
1346
+ if not confirmation:
1347
+ if not all(line.qty_done == line.reserved_uom_qty for line in lines):
1348
+ return self._response_for_summary(
1349
+ picking,
1350
+ need_confirm=True,
1351
+ message=self.msg_store.transfer_confirm_done(),
1352
+ )
1353
+ elif not all(line.shopfloor_checkout_done for line in lines):
1354
+ return self._response_for_summary(
1355
+ picking,
1356
+ need_confirm=True,
1357
+ message={
1358
+ "message_type": "warning",
1359
+ "body": _("Remaining raw product not packed, proceed anyway?"),
1360
+ },
1361
+ )
1362
+ stock = self._actions_for("stock")
1363
+ lines_done = self._lines_checkout_done(picking)
1364
+ stock.validate_moves(lines_done.move_id)
1365
+ return self._response_for_select_document(
1366
+ message=self.msg_store.transfer_done_success(lines_done.picking_id)
1367
+ )
1368
+
1369
+
1370
+ class ShopfloorCheckoutValidator(Component):
1371
+ """Validators for the Checkout endpoints"""
1372
+
1373
+ _inherit = "base.shopfloor.validator"
1374
+ _name = "shopfloor.checkout.validator"
1375
+ _usage = "checkout.validator"
1376
+
1377
+ def scan_document(self):
1378
+ return {"barcode": {"required": True, "type": "string"}}
1379
+
1380
+ def list_stock_picking(self):
1381
+ return {}
1382
+
1383
+ def select(self):
1384
+ return {"picking_id": {"coerce": to_int, "required": True, "type": "integer"}}
1385
+
1386
+ def scan_line(self):
1387
+ return {
1388
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1389
+ "barcode": {"required": True, "type": "string"},
1390
+ "confirm_pack_all": {
1391
+ "type": "boolean",
1392
+ "nullable": True,
1393
+ "required": False,
1394
+ },
1395
+ }
1396
+
1397
+ def select_line(self):
1398
+ return {
1399
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1400
+ "package_id": {"coerce": to_int, "required": False, "type": "integer"},
1401
+ "move_line_id": {"coerce": to_int, "required": False, "type": "integer"},
1402
+ }
1403
+
1404
+ def reset_line_qty(self):
1405
+ return {
1406
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1407
+ "selected_line_ids": {
1408
+ "type": "list",
1409
+ "required": True,
1410
+ "schema": {"coerce": to_int, "required": True, "type": "integer"},
1411
+ },
1412
+ "move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
1413
+ }
1414
+
1415
+ def set_line_qty(self):
1416
+ return {
1417
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1418
+ "selected_line_ids": {
1419
+ "type": "list",
1420
+ "required": True,
1421
+ "schema": {"coerce": to_int, "required": True, "type": "integer"},
1422
+ },
1423
+ "move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
1424
+ }
1425
+
1426
+ def set_custom_qty(self):
1427
+ return {
1428
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1429
+ "selected_line_ids": {
1430
+ "type": "list",
1431
+ "required": True,
1432
+ "schema": {"coerce": to_int, "required": True, "type": "integer"},
1433
+ },
1434
+ "move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
1435
+ "qty_done": {"coerce": to_float, "required": True, "type": "float"},
1436
+ }
1437
+
1438
+ def scan_package_action(self):
1439
+ return {
1440
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1441
+ "selected_line_ids": {
1442
+ "type": "list",
1443
+ "required": True,
1444
+ "schema": {"coerce": to_int, "required": True, "type": "integer"},
1445
+ },
1446
+ "barcode": {"required": True, "type": "string"},
1447
+ }
1448
+
1449
+ def list_delivery_packaging(self):
1450
+ return {
1451
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1452
+ "selected_line_ids": {
1453
+ "type": "list",
1454
+ "required": True,
1455
+ "schema": {"coerce": to_int, "required": True, "type": "integer"},
1456
+ },
1457
+ }
1458
+
1459
+ def new_package(self):
1460
+ return {
1461
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1462
+ "selected_line_ids": {
1463
+ "type": "list",
1464
+ "required": True,
1465
+ "schema": {"coerce": to_int, "required": True, "type": "integer"},
1466
+ },
1467
+ "package_type_id": {
1468
+ "coerce": to_int,
1469
+ "required": False,
1470
+ "type": "integer",
1471
+ },
1472
+ }
1473
+
1474
+ def no_package(self):
1475
+ return self.new_package()
1476
+
1477
+ def list_dest_package(self):
1478
+ return {
1479
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1480
+ "selected_line_ids": {
1481
+ "type": "list",
1482
+ "required": True,
1483
+ "schema": {"coerce": to_int, "required": True, "type": "integer"},
1484
+ },
1485
+ }
1486
+
1487
+ def scan_dest_package(self):
1488
+ return {
1489
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1490
+ "selected_line_ids": {
1491
+ "type": "list",
1492
+ "required": True,
1493
+ "schema": {"coerce": to_int, "required": True, "type": "integer"},
1494
+ },
1495
+ "barcode": {"required": True, "type": "string"},
1496
+ }
1497
+
1498
+ def set_dest_package(self):
1499
+ return {
1500
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1501
+ "selected_line_ids": {
1502
+ "type": "list",
1503
+ "required": True,
1504
+ "schema": {"coerce": to_int, "required": True, "type": "integer"},
1505
+ },
1506
+ "package_id": {"coerce": to_int, "required": True, "type": "integer"},
1507
+ }
1508
+
1509
+ def summary(self):
1510
+ return {"picking_id": {"coerce": to_int, "required": True, "type": "integer"}}
1511
+
1512
+ def list_packaging(self):
1513
+ return {
1514
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1515
+ "package_id": {"coerce": to_int, "required": True, "type": "integer"},
1516
+ }
1517
+
1518
+ def set_packaging(self):
1519
+ return {
1520
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1521
+ "package_id": {"coerce": to_int, "required": True, "type": "integer"},
1522
+ "package_type_id": {
1523
+ "coerce": to_int,
1524
+ "required": True,
1525
+ "type": "integer",
1526
+ },
1527
+ }
1528
+
1529
+ def cancel_line(self):
1530
+ return {
1531
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1532
+ "package_id": {
1533
+ "coerce": to_int,
1534
+ "required": False,
1535
+ "type": "integer",
1536
+ # excludes does not set the other as not required??? :/
1537
+ "excludes": "line_id",
1538
+ },
1539
+ "line_id": {
1540
+ "coerce": to_int,
1541
+ "required": False,
1542
+ "type": "integer",
1543
+ "excludes": "package_id",
1544
+ },
1545
+ }
1546
+
1547
+ def done(self):
1548
+ return {
1549
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
1550
+ "confirmation": {"type": "boolean", "nullable": True, "required": False},
1551
+ }
1552
+
1553
+
1554
+ class ShopfloorCheckoutValidatorResponse(Component):
1555
+ """Validators for the Checkout endpoints responses"""
1556
+
1557
+ _inherit = "base.shopfloor.validator.response"
1558
+ _name = "shopfloor.checkout.validator.response"
1559
+ _usage = "checkout.validator.response"
1560
+
1561
+ _start_state = "select_document"
1562
+
1563
+ def _states(self):
1564
+ """List of possible next states
1565
+
1566
+ With the schema of the data send to the client to transition
1567
+ to the next state.
1568
+ """
1569
+ return {
1570
+ "select_document": {},
1571
+ "manual_selection": self._schema_selection_list,
1572
+ "select_line": self._schema_stock_picking_details,
1573
+ "select_package": dict(
1574
+ self._schema_selected_lines,
1575
+ packing_info={"type": "string", "nullable": True},
1576
+ no_package_enabled={
1577
+ "type": "boolean",
1578
+ "nullable": True,
1579
+ "required": False,
1580
+ },
1581
+ ),
1582
+ "change_quantity": self._schema_selected_lines,
1583
+ "select_dest_package": self._schema_select_package,
1584
+ "select_delivery_packaging": self._schema_select_delivery_packaging,
1585
+ "summary": self._schema_summary,
1586
+ "change_packaging": self._schema_select_packaging,
1587
+ "confirm_done": self._schema_confirm_done,
1588
+ }
1589
+
1590
+ def _schema_stock_picking(self, lines_with_packaging=False):
1591
+ schema = self.schemas.picking()
1592
+ schema.update(
1593
+ {
1594
+ "move_lines": self.schemas._schema_list_of(
1595
+ self.schemas.move_line(with_packaging=lines_with_packaging)
1596
+ )
1597
+ }
1598
+ )
1599
+ return {"picking": self.schemas._schema_dict_of(schema, required=True)}
1600
+
1601
+ @property
1602
+ def _schema_stock_picking_details(self):
1603
+ return dict(
1604
+ self._schema_stock_picking(),
1605
+ group_lines_by_location={"type": "boolean"},
1606
+ show_oneline_package_content={"type": "boolean"},
1607
+ need_confirm_pack_all={"type": "boolean"},
1608
+ )
1609
+
1610
+ @property
1611
+ def _schema_summary(self):
1612
+ return dict(
1613
+ self._schema_stock_picking(lines_with_packaging=True),
1614
+ all_processed={"type": "boolean"},
1615
+ )
1616
+
1617
+ @property
1618
+ def _schema_confirm_done(self):
1619
+ return self._schema_stock_picking(lines_with_packaging=True)
1620
+
1621
+ @property
1622
+ def _schema_selection_list(self):
1623
+ return {
1624
+ "pickings": {
1625
+ "type": "list",
1626
+ "schema": {"type": "dict", "schema": self.schemas.picking()},
1627
+ }
1628
+ }
1629
+
1630
+ @property
1631
+ def _schema_select_package(self):
1632
+ return {
1633
+ "selected_move_lines": {
1634
+ "type": "list",
1635
+ "schema": {"type": "dict", "schema": self.schemas.move_line()},
1636
+ },
1637
+ "packages": {
1638
+ "type": "list",
1639
+ "schema": {
1640
+ "type": "dict",
1641
+ "schema": self.schemas.package(with_packaging=True),
1642
+ },
1643
+ },
1644
+ "picking": {"type": "dict", "schema": self.schemas.picking()},
1645
+ }
1646
+
1647
+ @property
1648
+ def _schema_select_delivery_packaging(self):
1649
+ return {
1650
+ "packaging": self.schemas._schema_list_of(
1651
+ self.schemas.delivery_packaging()
1652
+ ),
1653
+ }
1654
+
1655
+ @property
1656
+ def _schema_select_packaging(self):
1657
+ return {
1658
+ "picking": {"type": "dict", "schema": self.schemas.picking()},
1659
+ "package": {
1660
+ "type": "dict",
1661
+ "schema": self.schemas.package(with_packaging=True),
1662
+ },
1663
+ "packaging": {
1664
+ "type": "list",
1665
+ "schema": {"type": "dict", "schema": self.schemas.delivery_packaging()},
1666
+ },
1667
+ }
1668
+
1669
+ @property
1670
+ def _schema_selected_lines(self):
1671
+ return {
1672
+ "selected_move_lines": {
1673
+ "type": "list",
1674
+ "schema": {"type": "dict", "schema": self.schemas.move_line()},
1675
+ },
1676
+ "picking": {"type": "dict", "schema": self.schemas.picking()},
1677
+ }
1678
+
1679
+ def scan_document(self):
1680
+ return self._response_schema(
1681
+ next_states={"select_document", "select_line", "summary"}
1682
+ )
1683
+
1684
+ def list_stock_picking(self):
1685
+ return self._response_schema(next_states={"manual_selection"})
1686
+
1687
+ def select(self):
1688
+ return self._response_schema(
1689
+ next_states={"manual_selection", "summary", "select_line"}
1690
+ )
1691
+
1692
+ def scan_line(self):
1693
+ return self._response_schema(
1694
+ next_states={"select_line", "select_package", "summary"}
1695
+ )
1696
+
1697
+ def select_line(self):
1698
+ return self.scan_line()
1699
+
1700
+ def reset_line_qty(self):
1701
+ return self._response_schema(next_states={"select_package"})
1702
+
1703
+ def set_line_qty(self):
1704
+ return self._response_schema(next_states={"select_package"})
1705
+
1706
+ def set_custom_qty(self):
1707
+ return self._response_schema(next_states={"select_package"})
1708
+
1709
+ def scan_package_action(self):
1710
+ return self._response_schema(
1711
+ next_states={"select_package", "select_line", "summary"}
1712
+ )
1713
+
1714
+ def list_delivery_packaging(self):
1715
+ return self._response_schema(
1716
+ next_states={"select_delivery_packaging", "select_package"}
1717
+ )
1718
+
1719
+ def new_package(self):
1720
+ return self._response_schema(next_states={"select_line", "summary"})
1721
+
1722
+ def no_package(self):
1723
+ return self.new_package()
1724
+
1725
+ def list_dest_package(self):
1726
+ return self._response_schema(
1727
+ next_states={"select_dest_package", "select_package"}
1728
+ )
1729
+
1730
+ def scan_dest_package(self):
1731
+ return self._response_schema(
1732
+ next_states={
1733
+ "select_dest_package",
1734
+ "select_package",
1735
+ "select_line",
1736
+ "summary",
1737
+ }
1738
+ )
1739
+
1740
+ def set_dest_package(self):
1741
+ return self._response_schema(
1742
+ next_states={
1743
+ "select_dest_package",
1744
+ "select_package",
1745
+ "select_line",
1746
+ "summary",
1747
+ }
1748
+ )
1749
+
1750
+ def summary(self):
1751
+ return self._response_schema(next_states={"summary"})
1752
+
1753
+ def list_packaging(self):
1754
+ return self._response_schema(next_states={"change_packaging", "summary"})
1755
+
1756
+ def set_packaging(self):
1757
+ return self._response_schema(next_states={"change_packaging", "summary"})
1758
+
1759
+ def cancel_line(self):
1760
+ return self._response_schema(next_states={"summary", "select_line"})
1761
+
1762
+ def done(self):
1763
+ return self._response_schema(next_states={"summary", "confirm_done"})