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,828 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+ from odoo import _, fields
4
+ from odoo.osv import expression
5
+ from odoo.tools.float_utils import float_is_zero
6
+
7
+ from odoo.addons.base_rest.components.service import to_bool, to_int
8
+ from odoo.addons.component.core import Component
9
+
10
+
11
+ class Delivery(Component):
12
+ """
13
+ Methods for the Delivery Process
14
+
15
+ Deliver the goods by processing the PACK and raw products by delivery order.
16
+ Last step in the pick/pack/ship steps. (Cluster Picking → Checkout → Delivery)
17
+
18
+ Multiple operators could be processing a same delivery order.
19
+
20
+ You will find a sequence diagram describing states and endpoints
21
+ relationships [here](../docs/delivery_diag_seq.png).
22
+ Keep [the sequence diagram](../docs/delivery_diag_seq.plantuml)
23
+ up-to-date if you change endpoints.
24
+
25
+ Expected:
26
+
27
+ * Existing packages are moved to customer location
28
+ * Products are moved to customer location as raw products
29
+ * Bin packed products are placed in new shipping package and shipped to customer
30
+
31
+ Every time a package, product or lot is scanned, the package level and move line
32
+ are set to done. When the last line is scanned, the transfer is set to done.
33
+ Data for the last transfer for which we have been scanning a line if it is not done.
34
+ When a transfer is scanned, it returns its data to be shown on the screen.
35
+
36
+ Flow Diagram: https://www.draw.io/#G1qRenBcezk50ggIazDuu2qOfkTsoIAxXP
37
+ """
38
+
39
+ _inherit = "base.shopfloor.process"
40
+ _name = "shopfloor.delivery"
41
+ _usage = "delivery"
42
+ _description = __doc__
43
+
44
+ def _response_for_deliver(self, picking=None, location=None, message=None):
45
+ """Transition to the 'deliver' state
46
+
47
+ If no picking is passed, the screen shows an empty screen
48
+ """
49
+ return self._response(
50
+ next_state="deliver",
51
+ data={
52
+ "picking": self.data_detail.picking_detail(picking)
53
+ if picking
54
+ else None,
55
+ "sublocation": self.data.location(
56
+ location, with_operation_progress=True
57
+ )
58
+ if location
59
+ else None,
60
+ },
61
+ message=message,
62
+ )
63
+
64
+ def _response_for_manual_selection(self, pickings, message=None):
65
+ """Transition to the 'manual_selection' state
66
+
67
+ If no picking is passed, the screen shows an empty screen
68
+ """
69
+ return self._response(
70
+ next_state="manual_selection",
71
+ data={
72
+ "pickings": [
73
+ self.data_detail.picking_detail(picking) for picking in pickings
74
+ ],
75
+ },
76
+ message=message,
77
+ )
78
+
79
+ def _response_for_confirm_done(self, picking, message=None):
80
+ """Transition to the 'confirm_done' state."""
81
+ return self._response(
82
+ next_state="confirm_done",
83
+ data={
84
+ "picking": self.data_detail.picking_detail(picking)
85
+ if picking
86
+ else None,
87
+ },
88
+ message=message,
89
+ )
90
+
91
+ def scan_deliver(self, barcode, picking_id=None, location_id=None):
92
+ """Scan a stock picking, a package/product/lot or a stock location
93
+
94
+ When a stock picking is scanned and is partially or fully available, it
95
+ is returned to show its lines.
96
+
97
+ When a package is scanned, and has an available move line part of the
98
+ expected picking type, the package level is directly set to "done" and
99
+ the stock picking of the line is returned to work on its other lines.
100
+
101
+ When a stock location is scanned and it is a sub-location of an operation
102
+ type allowed on the current shopfloor menu, the next delivery
103
+ operations will be put into that location.
104
+
105
+ If the barcode is a product or a product's packaging, the move lines
106
+ for this product are set to done. However, if the product is in more
107
+ than one package, a package barcode is requested, and if the product is
108
+ tracked by lot/serial, a lot is asked.
109
+
110
+ If the option 'Process as pre-packaged' is enabled on the menu, then
111
+ when a product's packaging is scanned, the first move line without
112
+ a source package (bulk line) corresponding to the quantity of the
113
+ packaging will be set to done.
114
+
115
+ If the barcode is a lot, the lines for this lot are set to
116
+ done. However, if the lot is in more than one package, a package
117
+ barcode is requested.
118
+
119
+ NOTE: see scan_line in the Checkout service.
120
+
121
+ When all the available move lines of the stock picking are done, the
122
+ stock picking is set to done.
123
+
124
+ The ``picking_id`` parameter is used to be stateless: if the client
125
+ sends a wrong barcode, it allows to stay on the last picking with
126
+ updated data (and we really want to refresh data because several
127
+ users may work on the same transfer).
128
+
129
+ Transitions:
130
+ * deliver: always return here with the data for the last touched
131
+ picking or no picking if the picking has been set to done
132
+ """
133
+ location = (
134
+ self.env["stock.location"].browse(location_id) if location_id else None
135
+ )
136
+ if not barcode:
137
+ return self._response_for_deliver(location=location)
138
+ search = self._actions_for("search")
139
+ picking = search.picking_from_scan(barcode)
140
+ barcode_valid = bool(picking)
141
+
142
+ if picking:
143
+ message = self._check_picking_status(picking)
144
+ if message:
145
+ return self._response_for_deliver(location=location, message=message)
146
+
147
+ if picking_id:
148
+ picking = self.env["stock.picking"].browse(picking_id)
149
+
150
+ # Validate picking anyway
151
+ if not barcode_valid:
152
+ package = search.package_from_scan(barcode)
153
+ if package:
154
+ return self._deliver_package(picking, package, location)
155
+
156
+ if not barcode_valid:
157
+ product = search.product_from_scan(barcode)
158
+ if product:
159
+ return self._deliver_product(
160
+ picking, product, product_qty=1, location=location
161
+ )
162
+
163
+ if not barcode_valid:
164
+ packaging = search.packaging_from_scan(barcode)
165
+ if packaging:
166
+ # By scanning a packaging, we want to process
167
+ # the full quantity of the packaging
168
+ packaging_qty = packaging.product_uom_id._compute_quantity(
169
+ packaging.qty, packaging.product_id.uom_id
170
+ )
171
+ return self._deliver_product(
172
+ picking,
173
+ packaging.product_id,
174
+ product_qty=packaging_qty,
175
+ location=location,
176
+ )
177
+
178
+ if not barcode_valid:
179
+ lot = search.lot_from_scan(barcode)
180
+ if lot:
181
+ return self._deliver_lot(picking, lot, product_qty=1, location=location)
182
+
183
+ if not barcode_valid:
184
+ sublocation = search.location_from_scan(barcode)
185
+ if sublocation and sublocation.is_sublocation_of(
186
+ self.picking_types.mapped("default_location_src_id")
187
+ ):
188
+ message = self.msg_store.location_src_set_to_sublocation(sublocation)
189
+ return self._response_for_deliver(location=sublocation, message=message)
190
+
191
+ message = self.msg_store.barcode_not_found() if not barcode_valid else None
192
+ return self._response_for_deliver(
193
+ picking=picking, location=location, message=message
194
+ )
195
+
196
+ def _set_lines_done(self, lines, product_qty=None):
197
+ """Set done quantities on `lines`.
198
+
199
+ Once all lines of a picking have been processed, the picking will be
200
+ validated automatically.
201
+ Return `True` if the related picking has been validated.
202
+ """
203
+ allow_prepackaged_product = self.work.menu.allow_prepackaged_product
204
+ if product_qty: # defined with lot/product/packaging scan
205
+ # With a product_qty we process only one move line,
206
+ # so one move to deal with regarding the qty
207
+ qty_done = lines.move_id.product_id.uom_id._compute_quantity(
208
+ product_qty, lines.move_id.product_uom
209
+ )
210
+ lines.qty_done += qty_done
211
+ return self._action_picking_done(
212
+ lines.picking_id, force=allow_prepackaged_product
213
+ )
214
+ for line in lines:
215
+ # note: the package level is automatically set to "is_done" when
216
+ # the qty_done is full
217
+ line.qty_done = line.reserved_uom_qty
218
+ picking = fields.first(lines.mapped("picking_id"))
219
+ return self._action_picking_done(picking, force=allow_prepackaged_product)
220
+
221
+ def _reset_lines(self, lines):
222
+ for line in lines:
223
+ # note: the package level "is_done" field is automatically unset
224
+ # when the qty_done is not full
225
+ line.qty_done = 0
226
+
227
+ def _deliver_package(self, picking, package, location):
228
+ lines = package.move_line_ids.filtered(
229
+ lambda l: l.state in ("assigned", "partially_available")
230
+ )
231
+ # State of the picking might change while we reach this point: check again!
232
+ message = self._check_picking_status(lines.mapped("picking_id"))
233
+ if message:
234
+ message["body"] = "\n".join(
235
+ [
236
+ _("Package {} belongs to a picking without a valid state.").format(
237
+ package.name
238
+ ),
239
+ message["body"],
240
+ ]
241
+ )
242
+ return self._response_for_deliver(location=location, message=message)
243
+ if not lines:
244
+ return self._response_for_deliver(
245
+ picking=picking,
246
+ location=location,
247
+ message=self.msg_store.cannot_move_something_in_picking_type(),
248
+ )
249
+ # TODO add a message if any of the lines already had a qty_done > 0
250
+ new_picking = fields.first(lines.mapped("picking_id"))
251
+ if self._set_lines_done(lines):
252
+ return self._response_for_deliver(
253
+ location=location, message=self.msg_store.transfer_complete(new_picking)
254
+ )
255
+ return self._response_for_deliver(picking=new_picking, location=location)
256
+
257
+ def _lines_base_domain(self, no_qty_done=True):
258
+ domain = [
259
+ # we added auto_join for this, otherwise, the ORM would search all pickings
260
+ # in the picking type, and then use IN (ids)
261
+ ("picking_id.picking_type_id", "in", self.picking_types.ids),
262
+ ]
263
+ if no_qty_done:
264
+ domain.append(("qty_done", "=", 0))
265
+ return domain
266
+
267
+ def _lines_from_lot_domain(
268
+ self, lot, no_qty_done=True, product_qty=None, location=None
269
+ ):
270
+ location_domain = (
271
+ [("picking_id.location_id", "=", location.id)] if location else []
272
+ )
273
+ domain = expression.AND(
274
+ [
275
+ self._lines_base_domain(no_qty_done),
276
+ [("lot_id", "=", lot.id)],
277
+ location_domain,
278
+ ]
279
+ )
280
+ if product_qty:
281
+ domain.extend(
282
+ [
283
+ ("reserved_qty", ">=", product_qty),
284
+ ]
285
+ )
286
+ return domain
287
+
288
+ def _lines_from_product_domain(
289
+ self, product, no_qty_done=True, product_qty=None, location=None
290
+ ):
291
+ # TODO: searching lines is common to other scenario, to refactor
292
+ domain = expression.AND(
293
+ [self._lines_base_domain(no_qty_done), [("product_id", "=", product.id)]]
294
+ )
295
+ if location:
296
+ domain.extend([("location_id", "=", location.id)])
297
+ if product_qty:
298
+ domain.extend(
299
+ [
300
+ ("reserved_qty", ">=", product_qty),
301
+ ]
302
+ )
303
+ return domain
304
+
305
+ def _lines_from_package_domain(self, package, no_qty_done=True):
306
+ return expression.AND(
307
+ [self._lines_base_domain(no_qty_done), [("package_id", "=", package.id)]]
308
+ )
309
+
310
+ def _deliver_product(self, picking, product, product_qty=None, location=None):
311
+ """Handle the scan_deliver end point for a product."""
312
+ if product.tracking in ("lot", "serial"):
313
+ return self._response_for_deliver(
314
+ picking,
315
+ location=location,
316
+ message=self.msg_store.scan_lot_on_product_tracked_by_lot(),
317
+ )
318
+
319
+ lines = self.env["stock.move.line"].search(
320
+ self._lines_from_product_domain(
321
+ product, no_qty_done=False, product_qty=product_qty, location=location
322
+ ),
323
+ order="date_planned",
324
+ )
325
+ if not lines:
326
+ return self._response_for_deliver(
327
+ picking,
328
+ location=location,
329
+ message=self.msg_store.product_not_found_in_pickings(),
330
+ )
331
+
332
+ multiple_location = (
333
+ not location and len(lines.mapped("picking_id.location_id")) > 1
334
+ )
335
+ if multiple_location:
336
+ return self._response_for_deliver(
337
+ picking,
338
+ location=location,
339
+ message=self.msg_store.product_in_multiple_sublocation(product),
340
+ )
341
+
342
+ # State of the picking might change while we reach this point: check again!
343
+ message = self._check_picking_status(lines.mapped("picking_id"))
344
+ if message:
345
+ message["body"] = "\n".join(
346
+ [
347
+ _("Product {} belongs to a picking without a valid state.").format(
348
+ product.name
349
+ ),
350
+ message["body"],
351
+ ]
352
+ )
353
+ return self._response_for_deliver(location=location, message=message)
354
+
355
+ new_picking = fields.first(lines.mapped("picking_id"))
356
+ # When products are as units outside of packages, we can select them for
357
+ # packing, but if they are in a package, we want the user to scan the packages.
358
+ # If the product is only in one package though, scanning the product selects
359
+ # the package.
360
+ packages = lines.mapped("package_id")
361
+ # Do not use mapped here: we want to see if we have more than one package,
362
+ # but also if we have one product as a package and the same product as
363
+ # a unit in another line. In both cases, we want the user to scan the
364
+ # package.
365
+ if packages and len({m.package_id for m in lines}) > 1:
366
+ return self._response_for_deliver(
367
+ new_picking,
368
+ location=location,
369
+ message=self.msg_store.product_multiple_packages_scan_package(),
370
+ )
371
+ elif packages:
372
+ # we have 1 package
373
+ # abort the operation if the package contain more than one product
374
+ if len(packages.mapped("quant_ids.product_id")) > 1:
375
+ return self._response_for_deliver(
376
+ new_picking,
377
+ location=location,
378
+ message=self.msg_store.product_mixed_package_scan_package(),
379
+ )
380
+ # abort if the quantity is bigger than one
381
+ if sum(packages.quant_ids.mapped("reserved_quantity")) > 1:
382
+ return self._response_for_deliver(
383
+ new_picking,
384
+ location=location,
385
+ message=self.msg_store.product_not_unitary_in_package_scan_package(),
386
+ )
387
+ # We focus only on lines on which we can increase the 'qty_done'
388
+ lines = lines.filtered(
389
+ lambda l: (l.qty_done + product_qty) <= l.reserved_uom_qty
390
+ )
391
+ # Filter lines to keep only ones from one delivery operation
392
+ # (we do not want to process lines of another delivery operation)
393
+ lines = lines._filter_on_picking(picking)
394
+ # We want to process 1 qty of one line
395
+ lines = fields.first(lines)
396
+ # Validate lines (this will validate the delivery if all lines are processed)
397
+ if self._set_lines_done(lines, product_qty):
398
+ return self._response_for_deliver(
399
+ location=location, message=self.msg_store.transfer_complete(new_picking)
400
+ )
401
+ return self._response_for_deliver(new_picking, location=location)
402
+
403
+ def _deliver_lot(self, picking, lot, product_qty=None, location=None):
404
+ lines = self.env["stock.move.line"].search(
405
+ self._lines_from_lot_domain(
406
+ lot, no_qty_done=False, product_qty=product_qty, location=location
407
+ )
408
+ )
409
+ if not lines:
410
+ return self._response_for_deliver(
411
+ picking,
412
+ location=location,
413
+ message=self.msg_store.lot_not_found_in_pickings(),
414
+ )
415
+
416
+ multiple_location = (
417
+ not location and len(lines.mapped("picking_id.location_id")) > 1
418
+ )
419
+ if multiple_location:
420
+ return self._response_for_deliver(
421
+ picking,
422
+ location=location,
423
+ message=self.msg_store.lot_in_multiple_sublocation(lot),
424
+ )
425
+
426
+ # State of the picking might change while we reach this point: check again!
427
+ message = self._check_picking_status(lines.mapped("picking_id"))
428
+ if message:
429
+ message["body"] = "\n".join(
430
+ [
431
+ _("Lot {} belongs to a picking without a valid state.").format(
432
+ lot.name
433
+ ),
434
+ message["body"],
435
+ ]
436
+ )
437
+ return self._response_for_deliver(location=location, message=message)
438
+
439
+ new_picking = fields.first(lines.mapped("picking_id"))
440
+
441
+ # When lots are as units outside of packages, we can select them for
442
+ # packing, but if they are in a package, we want the user to scan the packages.
443
+ # If the product is only in one package though, scanning the lot selects
444
+ # the package.
445
+ packages = lines.mapped("package_id")
446
+ # Do not use mapped here: we want to see if we have more than one
447
+ # package, but also if we have one lot as a package and the same lot as
448
+ # a unit in another line. In both cases, we want the user to scan the
449
+ # package.
450
+ if packages and len({m.package_id for m in lines}) > 1:
451
+ return self._response_for_deliver(
452
+ new_picking,
453
+ location=location,
454
+ message=self.msg_store.lot_multiple_packages_scan_package(),
455
+ )
456
+ elif packages:
457
+ # we have 1 package
458
+ # abort the operation if the package contain more than one product
459
+ if len(packages.quant_ids) > 1:
460
+ return self._response_for_deliver(
461
+ new_picking,
462
+ location=location,
463
+ message=self.msg_store.lot_mixed_package_scan_package(),
464
+ )
465
+
466
+ # Filter lines to keep only ones from one delivery operation
467
+ # (we do not want to process lines of another delivery operation)
468
+ lines = lines._filter_on_picking(picking)
469
+ # We want to process 1 qty of one line
470
+ lines = fields.first(lines)
471
+ if self._set_lines_done(lines, product_qty=product_qty):
472
+ return self._response_for_deliver(
473
+ location=location, message=self.msg_store.transfer_complete(new_picking)
474
+ )
475
+ return self._response_for_deliver(new_picking, location)
476
+
477
+ def _action_picking_done(self, picking, force=False):
478
+ """Try to validate the stock picking if all quantities are satisfied.
479
+
480
+ Return `True` if the picking has been validated successfully.
481
+
482
+ :param picking: stock.picking recordset
483
+ :param force: bypass check and set picking as done no matter if satisfied.
484
+ You will likely get a backorder for not processed lines.
485
+ """
486
+ if picking.state == "done":
487
+ return True
488
+ if force:
489
+ picking._action_done()
490
+ return True
491
+ all_done = False
492
+ for move in picking.move_ids:
493
+ if move.state in ("done", "cancel"):
494
+ continue
495
+ all_done = move._qty_is_satisfied()
496
+ if not all_done:
497
+ # At least one move not satisfied, cannot mark as done automatically
498
+ break
499
+ if all_done:
500
+ picking._action_done()
501
+ return True
502
+ return False
503
+
504
+ def list_stock_picking(self, message=None, location_id=None):
505
+ """Return the list of stock pickings for the picking types
506
+
507
+ It returns only stock picking available or partially available.
508
+
509
+ Transitions:
510
+ * manual_selection: next state to show the list of stock pickings
511
+ """
512
+ pickings = self.env["stock.picking"].search(
513
+ self._pickings_domain(location_id), order="id"
514
+ )
515
+ return self._response_for_manual_selection(pickings, message=message)
516
+
517
+ def _pickings_domain(self, location_id=None):
518
+ domain = [
519
+ ("picking_type_id", "in", self.picking_types.ids),
520
+ ("state", "=", "assigned"),
521
+ ]
522
+ if location_id:
523
+ domain.append(("location_id", "=", location_id))
524
+ return domain
525
+
526
+ def select(self, picking_id):
527
+ """Select a stock picking from its ID (found using /list_stock_picking)
528
+
529
+ It returns only stock picking available or partially available.
530
+
531
+ Transitions:
532
+ * manual_selection: the selected stock picking is no longer valid
533
+ * deliver: with information about the stock.picking
534
+ """
535
+ picking = self.env["stock.picking"].browse(picking_id)
536
+ message = self._check_picking_status(picking)
537
+ if message:
538
+ return self.list_stock_picking(message=message)
539
+ if picking:
540
+ return self._response_for_deliver(picking)
541
+ return self.list_stock_picking(message=self.msg_store.stock_picking_not_found())
542
+
543
+ def set_qty_done_pack(self, picking_id, package_id, location_id=None):
544
+ """Set a package to "Done"
545
+
546
+ When all the available move lines of the stock picking are done, the
547
+ stock picking is set to done.
548
+
549
+ Transitions:
550
+ * deliver: always return here with updated data
551
+ """
552
+ picking = self.env["stock.picking"].browse(picking_id)
553
+ message = self._check_picking_status(picking)
554
+ if message:
555
+ return self._response_for_deliver(message=message)
556
+ package = self.env["stock.quant.package"].browse(package_id).exists()
557
+ if package:
558
+ response = self._deliver_package(picking, package, location_id)
559
+ self._action_picking_done(picking)
560
+ return response
561
+ return self._response_for_deliver(
562
+ picking=picking, message=self.msg_store.package_not_found()
563
+ )
564
+
565
+ def set_qty_done_line(self, picking_id, move_line_id):
566
+ """Set a move line to "Done"
567
+
568
+ Should be called only for lines of raw products, /set_qty_done_pack
569
+ must be used for lines that move a package.
570
+
571
+ When all the available move lines of the stock picking are done, the
572
+ stock picking is set to done.
573
+
574
+ Transitions:
575
+ * deliver: always return here with updated data
576
+ """
577
+ picking = self.env["stock.picking"].browse(picking_id)
578
+ message = self._check_picking_status(picking)
579
+ if message:
580
+ return self._response_for_deliver(message=message)
581
+ line = self.env["stock.move.line"].browse(move_line_id).exists()
582
+ if line:
583
+ if line.package_id:
584
+ return self._response_for_deliver(
585
+ picking=picking,
586
+ message=self.msg_store.line_has_package_scan_package(),
587
+ )
588
+ if self._set_lines_done(line):
589
+ return self._response_for_deliver(
590
+ message=self.msg_store.transfer_complete(picking)
591
+ )
592
+ return self._response_for_deliver(picking)
593
+ return self._response_for_deliver(
594
+ picking=picking,
595
+ message=self.msg_store.record_not_found(),
596
+ )
597
+
598
+ def reset_qty_done_pack(self, picking_id, package_id):
599
+ """Remove "Done" on a package
600
+
601
+ Transitions:
602
+ * deliver: always return here with updated data
603
+ """
604
+ picking = self.env["stock.picking"].browse(picking_id)
605
+ message = self._check_picking_status(picking)
606
+ if message:
607
+ return self._response_for_deliver(message=message)
608
+ package = self.env["stock.quant.package"].browse(package_id).exists()
609
+ if package:
610
+ lines = self.env["stock.move.line"].search(
611
+ self._lines_from_package_domain(package, no_qty_done=False)
612
+ )
613
+ if not lines:
614
+ return self._response_for_deliver(
615
+ picking,
616
+ message=self.msg_store.package_not_available_in_picking(
617
+ package, picking
618
+ ),
619
+ )
620
+ self._reset_lines(lines)
621
+ return self._response_for_deliver(picking)
622
+ return self._response_for_deliver(
623
+ picking=picking, message=self.msg_store.package_not_found()
624
+ )
625
+
626
+ def reset_qty_done_line(self, picking_id, move_line_id):
627
+ """Remove "Done" on a move line
628
+
629
+ Should be called only for lines of raw products, /set_qty_done_pack
630
+ must be used for lines that move a package.
631
+
632
+ Transitions:
633
+ * deliver: always return here with updated data
634
+ """
635
+ picking = self.env["stock.picking"].browse(picking_id)
636
+ message = self._check_picking_status(picking)
637
+ if message:
638
+ return self._response_for_deliver(message=message)
639
+ line = self.env["stock.move.line"].browse(move_line_id).exists()
640
+ if line:
641
+ if line.picking_id != picking:
642
+ return self._response_for_deliver(
643
+ picking=picking,
644
+ message=self.msg_store.line_not_available_in_picking(picking),
645
+ )
646
+ if line.package_id:
647
+ return self._response_for_deliver(
648
+ picking=picking,
649
+ message=self.msg_store.line_has_package_scan_package(),
650
+ )
651
+ self._reset_lines(line)
652
+ return self._response_for_deliver(picking)
653
+ return self._response_for_deliver(
654
+ picking=picking,
655
+ message=self.msg_store.record_not_found(),
656
+ )
657
+
658
+ def done(self, picking_id, confirm=False):
659
+ """Set the stock picking to done
660
+
661
+ Transitions:
662
+ * deliver: error during action
663
+ * confirm_done: when not all lines of the stock.picking are done
664
+ """
665
+ picking = self.env["stock.picking"].browse(picking_id)
666
+ message = self._check_picking_status(picking)
667
+ if message:
668
+ return self._response_for_deliver(message=message)
669
+ if self._action_picking_done(picking):
670
+ return self._response_for_deliver(
671
+ message=self.msg_store.transfer_complete(picking)
672
+ )
673
+ if confirm:
674
+ precision_digits = self.env["decimal.precision"].precision_get(
675
+ "Product Unit of Measure"
676
+ )
677
+ no_quantities_done = all(
678
+ float_is_zero(move_line.qty_done, precision_digits=precision_digits)
679
+ for move_line in picking.move_line_ids.filtered(
680
+ lambda m: m.state not in ("done", "cancel")
681
+ )
682
+ )
683
+ if no_quantities_done:
684
+ return self._response_for_deliver(
685
+ message=self.msg_store.transfer_no_qty_done()
686
+ )
687
+ self._action_picking_done(picking, force=True)
688
+ return self._response_for_deliver(
689
+ message=self.msg_store.transfer_complete(picking)
690
+ )
691
+ return self._response_for_confirm_done(
692
+ picking,
693
+ message=self.msg_store.transfer_confirm_done(),
694
+ )
695
+
696
+
697
+ class ShopfloorDeliveryValidator(Component):
698
+ """Validators for the Delivery endpoints"""
699
+
700
+ _inherit = "base.shopfloor.validator"
701
+ _name = "shopfloor.delivery.validator"
702
+ _usage = "delivery.validator"
703
+
704
+ def scan_deliver(self):
705
+ return {
706
+ "barcode": {"required": True, "type": "string"},
707
+ "picking_id": {
708
+ "coerce": to_int,
709
+ "required": False,
710
+ "nullable": True,
711
+ "type": "integer",
712
+ },
713
+ "location_id": {
714
+ "coerce": to_int,
715
+ "required": False,
716
+ "nullable": True,
717
+ "type": "integer",
718
+ },
719
+ }
720
+
721
+ def list_stock_picking(self):
722
+ return {
723
+ "location_id": {
724
+ "coerce": to_int,
725
+ "required": False,
726
+ "nullable": True,
727
+ "type": "integer",
728
+ },
729
+ }
730
+
731
+ def select(self):
732
+ return {"picking_id": {"coerce": to_int, "required": True, "type": "integer"}}
733
+
734
+ def set_qty_done_pack(self):
735
+ return {
736
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
737
+ "package_id": {"coerce": to_int, "required": True, "type": "integer"},
738
+ }
739
+
740
+ def set_qty_done_line(self):
741
+ return {
742
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
743
+ "move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
744
+ }
745
+
746
+ def reset_qty_done_pack(self):
747
+ return {
748
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
749
+ "package_id": {"coerce": to_int, "required": True, "type": "integer"},
750
+ }
751
+
752
+ def reset_qty_done_line(self):
753
+ return {
754
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
755
+ "move_line_id": {"coerce": to_int, "required": True, "type": "integer"},
756
+ }
757
+
758
+ def done(self):
759
+ return {
760
+ "picking_id": {"coerce": to_int, "required": True, "type": "integer"},
761
+ "confirm": {"coerce": to_bool, "required": False, "type": "boolean"},
762
+ }
763
+
764
+
765
+ class ShopfloorDeliveryValidatorResponse(Component):
766
+ """Validators for the Delivery endpoints responses"""
767
+
768
+ _inherit = "base.shopfloor.validator.response"
769
+ _name = "shopfloor.delivery.validator.response"
770
+ _usage = "delivery.validator.response"
771
+
772
+ _start_state = "deliver"
773
+
774
+ def _states(self):
775
+ """List of possible next states
776
+
777
+ With the schema of the data send to the client to transition
778
+ to the next state.
779
+ """
780
+ return {
781
+ "deliver": self._schema_deliver,
782
+ "manual_selection": self._schema_selection_list,
783
+ "confirm_done": self._schema_deliver,
784
+ }
785
+
786
+ @property
787
+ def _schema_deliver(self):
788
+ schema_picking = self.schemas_detail.picking_detail()
789
+ schema_location = self.schemas.location()
790
+ return {
791
+ "picking": {"type": "dict", "nullable": True, "schema": schema_picking},
792
+ "sublocation": {
793
+ "type": "dict",
794
+ "nullable": True,
795
+ "schema": schema_location,
796
+ },
797
+ }
798
+
799
+ @property
800
+ def _schema_selection_list(self):
801
+ schema = self.schemas_detail.picking_detail()
802
+ return {
803
+ "pickings": {"type": "list", "schema": {"type": "dict", "schema": schema}}
804
+ }
805
+
806
+ def scan_deliver(self):
807
+ return self._response_schema(next_states={"deliver"})
808
+
809
+ def list_stock_picking(self):
810
+ return self._response_schema(next_states={"manual_selection"})
811
+
812
+ def select(self):
813
+ return self._response_schema(next_states={"deliver", "manual_selection"})
814
+
815
+ def set_qty_done_pack(self):
816
+ return self._response_schema(next_states={"deliver"})
817
+
818
+ def set_qty_done_line(self):
819
+ return self._response_schema(next_states={"deliver"})
820
+
821
+ def reset_qty_done_pack(self):
822
+ return self._response_schema(next_states={"deliver"})
823
+
824
+ def reset_qty_done_line(self):
825
+ return self._response_schema(next_states={"deliver"})
826
+
827
+ def done(self):
828
+ return self._response_schema(next_states={"deliver", "confirm_done"})