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,509 @@
1
+ # Copyright 2020 Camptocamp SA
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3
+
4
+ from .test_location_content_transfer_base import LocationContentTransferCommonCase
5
+
6
+ # pylint: disable=missing-return
7
+
8
+
9
+ class LocationContentTransferMixCase(LocationContentTransferCommonCase):
10
+ """Tests where we mix location content transfer with other scenarios."""
11
+
12
+ @classmethod
13
+ def setUpClassUsers(cls):
14
+ super().setUpClassUsers()
15
+ Users = (
16
+ cls.env["res.users"]
17
+ .sudo()
18
+ .with_context(
19
+ **{"no_reset_password": True, "mail_create_nosubscribe": True}
20
+ )
21
+ )
22
+ cls.stock_user2 = Users.create(
23
+ {
24
+ "name": "Paul Posichon",
25
+ "login": "paulposichon",
26
+ "email": "paul.posichon@example.com",
27
+ "notification_type": "inbox",
28
+ "groups_id": [(6, 0, [cls.env.ref("stock.group_stock_user").id])],
29
+ }
30
+ )
31
+
32
+ @classmethod
33
+ def setUpClassVars(cls, *args, **kwargs):
34
+ super().setUpClassVars(*args, **kwargs)
35
+ cls.zp_menu = cls.env.ref("shopfloor.shopfloor_menu_demo_zone_picking")
36
+ cls.wh.sudo().delivery_steps = "pick_pack_ship"
37
+ cls.pack_location = cls.wh.wh_pack_stock_loc_id
38
+ cls.ship_location = cls.wh.wh_output_stock_loc_id
39
+ # Allows zone picking to process PICK picking type
40
+ cls.zp_menu.sudo().picking_type_ids += cls.wh.pick_type_id
41
+ # Allows location content transfer to process PACK picking type
42
+ cls.menu.sudo().picking_type_ids = cls.wh.pack_type_id
43
+ cls.wh.pack_type_id.sudo().default_location_dest_id = cls.env.ref(
44
+ "stock.stock_location_output"
45
+ )
46
+
47
+ @classmethod
48
+ def setUpClassBaseData(cls):
49
+ super().setUpClassBaseData()
50
+ cls.packing_location.sudo().active = True
51
+ products = cls.product_a
52
+ for product in products:
53
+ cls.env["stock.putaway.rule"].sudo().create(
54
+ {
55
+ "product_id": product.id,
56
+ "location_in_id": cls.stock_location.id,
57
+ "location_out_id": cls.shelf1.id,
58
+ }
59
+ )
60
+
61
+ # Put product_a quantities in different packages to get
62
+ # two stock move lines (6 and 4 to satisfy 10 qties)
63
+ cls.package_1 = cls.env["stock.quant.package"].create({"name": "PACKAGE_1"})
64
+ cls.package_2 = cls.env["stock.quant.package"].create({"name": "PACKAGE_2"})
65
+ cls._update_qty_in_location(
66
+ cls.stock_location, cls.product_a, 6, package=cls.package_1
67
+ )
68
+ cls._update_qty_in_location(
69
+ cls.stock_location, cls.product_a, 4, package=cls.package_2
70
+ )
71
+ # Create the pick/pack/ship transfers
72
+ cls.ship_move_a = cls.env["stock.move"].create(
73
+ {
74
+ "name": cls.product_a.display_name,
75
+ "product_id": cls.product_a.id,
76
+ "product_uom_qty": 10.0,
77
+ "product_uom": cls.product_a.uom_id.id,
78
+ "location_id": cls.ship_location.id,
79
+ "location_dest_id": cls.customer_location.id,
80
+ "warehouse_id": cls.wh.id,
81
+ "picking_type_id": cls.wh.out_type_id.id,
82
+ "procure_method": "make_to_order",
83
+ "state": "draft",
84
+ }
85
+ )
86
+ cls.ship_move_a._assign_picking()
87
+ cls.ship_move_a._action_confirm()
88
+ cls.pack_move_a = cls.ship_move_a.move_orig_ids[0]
89
+ cls.pick_move_a = cls.pack_move_a.move_orig_ids[0]
90
+ cls.picking1 = cls.pick_move_a.picking_id
91
+ cls.packing1 = cls.pack_move_a.picking_id
92
+ cls.picking1.action_assign()
93
+
94
+ def setUp(self):
95
+ super().setUp()
96
+ with self.work_on_services(menu=self.zp_menu, profile=self.profile) as work:
97
+ self.zp_service = work.component(usage="zone_picking")
98
+
99
+ def _zone_picking_process_line(self, move_line, dest_location=None):
100
+ picking = move_line.picking_id
101
+ zone_location = picking.location_id
102
+ picking_type = picking.picking_type_id
103
+ self.zp_service.work.current_zone_location = zone_location
104
+ self.zp_service.work.current_picking_type = picking_type
105
+ move_lines = picking.move_line_ids.filtered(
106
+ lambda m: m.state not in ("cancel", "done")
107
+ )
108
+ # Select the picking type
109
+ response = self.zp_service.scan_location(barcode=zone_location.barcode)
110
+ available_picking_type_ids = [
111
+ r["id"] for r in response["data"]["select_picking_type"]["picking_types"]
112
+ ]
113
+ assert picking_type.id in available_picking_type_ids
114
+ assert "message" not in response
115
+ # Check the move lines related to the picking type
116
+ response = self.zp_service.list_move_lines()
117
+ available_move_line_ids = [
118
+ r["id"] for r in response["data"]["select_line"]["move_lines"]
119
+ ]
120
+ assert not set(move_lines.ids) - set(available_move_line_ids)
121
+ assert "message" not in response
122
+ # Set the destination on the move line
123
+ if not dest_location:
124
+ dest_location = move_line.location_dest_id
125
+ qty = move_line.reserved_uom_qty
126
+ response = self.zp_service.set_destination(
127
+ move_line.id,
128
+ dest_location.barcode,
129
+ qty,
130
+ confirmation=True,
131
+ )
132
+ assert response["message"]["message_type"] == "success"
133
+ self.assertEqual(move_line.state, "done")
134
+ self.assertEqual(move_line.move_id.product_uom_qty, qty)
135
+
136
+ def _location_content_transfer_process_line(
137
+ self, move_line, set_destination=False, user=None
138
+ ):
139
+ service = self.service
140
+ if user:
141
+ env = self.env(user=user)
142
+ service = self.get_service(
143
+ "location_content_transfer",
144
+ env=env,
145
+ menu=self.menu,
146
+ profile=self.profile,
147
+ )
148
+
149
+ pack_location = move_line.location_id
150
+ out_location = move_line.location_dest_id
151
+ # Scan the location
152
+ response = service.scan_location(pack_location.barcode)
153
+ # Set the destination
154
+ if set_destination:
155
+ assert response["next_state"] in ("scan_destination_all", "start_single")
156
+ qty = move_line.reserved_uom_qty
157
+ if response["next_state"] == "scan_destination_all":
158
+ response = service.set_destination_all(
159
+ pack_location.id, out_location.barcode
160
+ )
161
+ self.assert_response_start(
162
+ response,
163
+ message=service.msg_store.location_content_transfer_complete(
164
+ pack_location,
165
+ out_location,
166
+ ),
167
+ )
168
+ self.assertEqual(move_line.state, "done")
169
+ self.assertEqual(move_line.move_id.product_uom_qty, qty)
170
+ elif response["next_state"] == "start_single":
171
+ response = service.scan_line(
172
+ pack_location.id, move_line.id, move_line.product_id.barcode
173
+ )
174
+ assert response["message"]["message_type"] == "success"
175
+ response = service.set_destination_line(
176
+ pack_location.id, move_line.id, qty, out_location.barcode
177
+ )
178
+ assert response["message"]["message_type"] == "success"
179
+ assert move_line.state == "done"
180
+ assert move_line.qty_done == qty
181
+ return response
182
+
183
+ def test_with_zone_picking1(self):
184
+ """Test the following scenario:
185
+
186
+ 1) Operator-1 processes the first pallet with the "zone picking" scenario:
187
+
188
+ move1 PICK -> PACK 'done'
189
+
190
+ 2) Operator-2 with the "location content transfer" scenario scan
191
+ the location where this first pallet is (so the move line is still not
192
+ done, the operator is currently moving the goods to the destination location):
193
+
194
+ move1 PACK -> SHIP 'assigned' while the operator is moving it
195
+
196
+ 3) Operator-1 process the second pallet with the "zone picking" scenario:
197
+
198
+ move2 PICK -> PACK 'done'
199
+
200
+ 4) Operator-3 with the "location content transfer" scenario scan
201
+ the location where this second pallet is, Odoo should return only this
202
+ second pallet as the first one, even if not fully processed (done)
203
+ is not physically available in the scanned location.
204
+
205
+ move2 PACK -> SHIP 'assigned' is proposed to the operator
206
+ move1 PACK -> SHIP while still 'assigned' is not proposed to the operator
207
+ """
208
+ picking = self.picking1
209
+ move_lines = picking.move_line_ids
210
+ pick_move_line1 = move_lines.filtered(
211
+ lambda ml: ml.result_package_id == self.package_1
212
+ )
213
+ pick_move_line2 = move_lines.filtered(
214
+ lambda ml: ml.result_package_id == self.package_2
215
+ )
216
+ # Operator-1 process the first pallet with the "zone picking" scenario
217
+ self._zone_picking_process_line(pick_move_line1)
218
+ # Operator-2 with the "location content transfer" scenario scan
219
+ # the location where this first pallet is (so the move line is still not
220
+ # done, the operator is currently moving the goods to the destination location)
221
+ pack_move_line1 = pick_move_line1.move_id.move_dest_ids.filtered(
222
+ lambda m: m.state not in ("cancel", "done")
223
+ ).move_line_ids.filtered(lambda l: not l.shopfloor_user_id)
224
+ self._location_content_transfer_process_line(pack_move_line1)
225
+ # Operator-1 process the second pallet with the "zone picking" scenario
226
+ self._zone_picking_process_line(pick_move_line2)
227
+ # Operator-3 with the "location content transfer" scenario scan
228
+ # the location where this second pallet is
229
+ pack_move_line2 = pick_move_line2.move_id.move_dest_ids.filtered(
230
+ lambda m: m.state not in ("cancel", "done")
231
+ ).move_line_ids.filtered(lambda l: not l.shopfloor_user_id)
232
+ assert (
233
+ len(pack_move_line2) == 1
234
+ ), "Operator-3 should end up with one move line taken from {}".format(
235
+ pack_move_line2.picking_id.name
236
+ )
237
+ self._location_content_transfer_process_line(pack_move_line2)
238
+
239
+ def test_with_zone_picking2(self):
240
+ """Test the following scenario:
241
+
242
+ 1) Operator-1 processes the first pallet with the "zone picking" scenario
243
+ to move the goods to PACK-1 and unload in destination location1:
244
+
245
+ move1 PICK -> PACK-1 'done'
246
+
247
+ 2) Operator-1 processes the second pallet with the "zone picking" scenario
248
+ to move the goods to PACK-2 and unload in destination location2:
249
+
250
+ move1 PICK -> PACK-2 'done'
251
+
252
+ 3) Operator-2 with the "location content transfer" scenario scan
253
+ the location where the first pallet is (PACK-1):
254
+ - the app should found one move line
255
+ - this move line will be put in its own transfer as its sibling lines
256
+ are in another source location
257
+ - as such the app should ask the destination location (as there is
258
+ only one line)
259
+
260
+ move1 PACK-2 -> SHIP (still handled by the operator so not 'done')
261
+
262
+ 4) Operator-3 with the "location content transfer" scenario scan
263
+ the location where the first pallet is (PACK-1):
264
+ - nothing is found as the pallet is currently handled by Operator-2
265
+
266
+ 5) If Operator-2 is unable to finish the flow with the first pallet
267
+ (barcode device out of battery... etc), he should be able to recover
268
+ what he started.
269
+
270
+ 6) Operator-2 then finishes its operation regarding the first pallet, and
271
+ scan the location where the second pallet is (PACK-2). He should find
272
+ only this pallet available.
273
+ """
274
+ move_lines = self.picking1.move_line_ids
275
+ pick_move_line1 = move_lines.filtered(
276
+ lambda ml: ml.result_package_id == self.package_1
277
+ )
278
+ pick_move_line2 = move_lines.filtered(
279
+ lambda ml: ml.result_package_id == self.package_2
280
+ )
281
+ # Operator-1 process the first pallet with the "zone picking" scenario
282
+ orig_dest_location = pick_move_line1.location_dest_id
283
+ dest_location1 = pick_move_line1.location_dest_id.sudo().copy(
284
+ {
285
+ "name": orig_dest_location.name + "_1",
286
+ "barcode": orig_dest_location.barcode + "_1",
287
+ "location_id": orig_dest_location.id,
288
+ }
289
+ )
290
+ self._zone_picking_process_line(pick_move_line1, dest_location=dest_location1)
291
+ # Operator-1 process the second pallet with the "zone picking" scenario
292
+ dest_location2 = orig_dest_location.sudo().copy(
293
+ {
294
+ "name": orig_dest_location.name + "_2",
295
+ "barcode": orig_dest_location.barcode + "_2",
296
+ "location_id": orig_dest_location.id,
297
+ }
298
+ )
299
+ self._zone_picking_process_line(pick_move_line2, dest_location=dest_location2)
300
+ pack_move_a = pick_move_line1.move_id.move_dest_ids.filtered(
301
+ lambda m: m.state not in ("cancel", "done")
302
+ )
303
+ self.assertEqual(pack_move_a, self.pack_move_a)
304
+ pack_first_pallet = pack_move_a.move_line_ids.filtered(
305
+ lambda l: not l.shopfloor_user_id and l.location_id == dest_location1
306
+ )
307
+ self.assertEqual(pack_first_pallet.reserved_uom_qty, 6)
308
+ self.assertEqual(pack_first_pallet.qty_done, 0)
309
+ pack_second_pallet = pack_move_a.move_line_ids.filtered(
310
+ lambda l: not l.shopfloor_user_id and l.location_id == dest_location2
311
+ )
312
+ self.assertEqual(pack_second_pallet.reserved_uom_qty, 4)
313
+ self.assertEqual(pack_second_pallet.qty_done, 0)
314
+ # Operator-2 with the "location content transfer" scenario scan
315
+ # the location where the first pallet is.
316
+ # This pallet/move line will be put in its own transfer as its sibling
317
+ # lines are in another source location.
318
+ previous_picking = pack_first_pallet.picking_id
319
+ response = self._location_content_transfer_process_line(pack_first_pallet)
320
+ new_picking = pack_first_pallet.picking_id
321
+ self.assertTrue(previous_picking != new_picking)
322
+ self.assert_response_scan_destination_all(response, new_picking)
323
+ response_packages = response["data"]["scan_destination_all"]["package_levels"]
324
+ self.assertEqual(len(response_packages), 1)
325
+ self.assertEqual(
326
+ response_packages[0]["package_src"]["id"], pack_first_pallet.package_id.id
327
+ )
328
+ # Ensure that the second pallet is untouched
329
+ self.assertEqual(pack_second_pallet.qty_done, 0)
330
+ # Operator-3 with the "location content transfer" scenario scan
331
+ # the location where the first pallet is: he should found nothing
332
+ response = self._location_content_transfer_process_line(
333
+ pack_first_pallet, user=self.stock_user2
334
+ )
335
+ self.assert_response_start(
336
+ response, message=self.service.msg_store.new_move_lines_not_assigned()
337
+ )
338
+ # Check if Operator-2 is able to recover its session
339
+ expected_picking = pack_first_pallet.picking_id
340
+ response = self.service.start_or_recover()
341
+ self.assert_response_scan_destination_all(
342
+ response,
343
+ expected_picking,
344
+ message=self.service.msg_store.recovered_previous_session(),
345
+ )
346
+ # Operator-2 finishes its operation regarding the first pallet
347
+ qty = pack_first_pallet.reserved_uom_qty
348
+ response = self.service.set_destination_all(
349
+ pack_first_pallet.location_id.id, pack_first_pallet.location_dest_id.barcode
350
+ )
351
+ self.assert_response_start(
352
+ response,
353
+ message=self.service.msg_store.location_content_transfer_complete(
354
+ pack_first_pallet.location_id,
355
+ pack_first_pallet.location_dest_id,
356
+ ),
357
+ )
358
+ self.assertEqual(pack_first_pallet.qty_done, 6)
359
+ self.assertEqual(pack_first_pallet.state, "done")
360
+ self.assertEqual(pack_first_pallet.move_id.product_uom_qty, qty)
361
+ # Ensure that the second pallet is untouched
362
+ self.assertEqual(pack_second_pallet.qty_done, 0)
363
+ # Operator-2 (still with the "location content transfer" scenario) scan
364
+ # the location where the second pallet is
365
+ pack_move_a = pick_move_line2.move_id.move_dest_ids.filtered(
366
+ lambda m: m.state not in ("cancel", "done")
367
+ )
368
+ self.assertEqual(pack_move_a, self.pack_move_a)
369
+ pack_second_pallet = pack_move_a.move_line_ids.filtered(
370
+ lambda l: not l.shopfloor_user_id and l.location_id == dest_location2
371
+ )
372
+ picking_before = pack_second_pallet.picking_id
373
+ move_lines = self.service._find_location_move_lines(
374
+ pack_second_pallet.location_id
375
+ )
376
+ response = self._location_content_transfer_process_line(pack_second_pallet)
377
+ response_packages = response["data"]["scan_destination_all"]["package_levels"]
378
+ self.assertEqual(len(response_packages), 1)
379
+ self.assertEqual(
380
+ response_packages[0]["package_src"]["id"], pack_second_pallet.package_id.id
381
+ )
382
+ picking_after = pack_second_pallet.picking_id
383
+ self.assertEqual(picking_before, picking_after)
384
+ self.assert_response_scan_destination_all(response, picking_after)
385
+
386
+ def test_with_zone_picking3(self):
387
+ """Test the following scenario:
388
+
389
+ 1) Operator-1 processes the first pallet with the "zone picking" scenario
390
+ to move the goods to PACK-1:
391
+
392
+ move1 PICK -> PACK-1 'done'
393
+
394
+ 2) Operator-2 with the "location content transfer" scenario scan
395
+ the location where the first pallet is (PACK-1):
396
+ - the app should found one move line
397
+ - this move line will be put in its own transfer in any case
398
+ - as such the app should ask the destination location (as there is
399
+ only one line)
400
+
401
+ move1 PACK-1 -> SHIP (still handled by the operator so not 'done')
402
+
403
+ 3) Operator-1 processes the second pallet with the "zone picking" scenario
404
+ to move the goods to PACK-2:
405
+
406
+ move1 PICK -> PACK-2 'done'
407
+
408
+ - this will automatically update the reservation (new move line) in
409
+ the transfer previously processed by Operator-2.
410
+
411
+ 4) Operator-2 then finishes its operation regarding the first pallet
412
+ without any trouble.
413
+
414
+ 5) Operator-2 with the "location content transfer" scenario scan
415
+ the location where the second pallet is (PACK-2), etc
416
+ """
417
+ move_lines = self.picking1.move_line_ids
418
+ pick_move_line1 = move_lines.filtered(
419
+ lambda ml: ml.result_package_id == self.package_1
420
+ )
421
+ pick_move_line2 = move_lines.filtered(
422
+ lambda ml: ml.result_package_id == self.package_2
423
+ )
424
+ orig_dest_location = pick_move_line1.location_dest_id
425
+ dest_location1 = pick_move_line1.location_dest_id.sudo().copy(
426
+ {
427
+ "name": orig_dest_location.name + "_1",
428
+ "barcode": orig_dest_location.barcode + "_1",
429
+ "location_id": orig_dest_location.id,
430
+ }
431
+ )
432
+ dest_location2 = orig_dest_location.sudo().copy(
433
+ {
434
+ "name": orig_dest_location.name + "_2",
435
+ "barcode": orig_dest_location.barcode + "_2",
436
+ "location_id": orig_dest_location.id,
437
+ }
438
+ )
439
+ # Operator-1 process the first pallet with the "zone picking" scenario
440
+ self._zone_picking_process_line(pick_move_line1, dest_location=dest_location1)
441
+ pack_move_a1 = pick_move_line1.move_id.move_dest_ids.filtered(
442
+ lambda m: m.move_line_ids.package_id == self.package_1
443
+ )
444
+ self.assertEqual(pack_move_a1, self.pack_move_a)
445
+ pack_first_pallet = pack_move_a1.move_line_ids.filtered(
446
+ lambda l: not l.shopfloor_user_id and l.location_id == dest_location1
447
+ )
448
+ self.assertEqual(pack_first_pallet.reserved_uom_qty, 6)
449
+ self.assertEqual(pack_first_pallet.qty_done, 0)
450
+ # Operator-2 with the "location content transfer" scenario scan
451
+ # the location where the first pallet is.
452
+ # This pallet/move line will be put in its own move and transfer by convenience
453
+ original_pack_transfer = pack_first_pallet.picking_id
454
+ response = self._location_content_transfer_process_line(pack_first_pallet)
455
+ new_pack_transfer = pack_first_pallet.picking_id
456
+ self.assertNotEqual(original_pack_transfer, new_pack_transfer)
457
+ self.assert_response_scan_destination_all(response, new_pack_transfer)
458
+ response_packages = response["data"]["scan_destination_all"]["package_levels"]
459
+ self.assertEqual(len(response_packages), 1)
460
+ self.assertEqual(
461
+ response_packages[0]["package_src"]["id"], pack_first_pallet.package_id.id
462
+ )
463
+ # All pack lines have been processed until now, so the existing pack
464
+ # operation is now waiting goods from pick operation
465
+ self.assertEqual(original_pack_transfer.state, "waiting")
466
+ # Operator-1 process the second pallet with the "zone picking" scenario
467
+ self._zone_picking_process_line(pick_move_line2, dest_location=dest_location2)
468
+ pack_move_a2 = pick_move_line2.move_id.move_dest_ids.filtered(
469
+ lambda m: m.move_line_ids.package_id == self.package_2
470
+ )
471
+ pack_second_pallet = pack_move_a2.move_line_ids.filtered(
472
+ lambda l: not l.shopfloor_user_id and l.location_id == dest_location2
473
+ )
474
+ self.assertEqual(pack_second_pallet.reserved_uom_qty, 4)
475
+ self.assertEqual(pack_second_pallet.qty_done, 0)
476
+ # The last action has updated the pack operation (new move line) in the
477
+ # transfer previously processed by Operator-2.
478
+ self.assertEqual(original_pack_transfer.state, "assigned")
479
+ self.assertIn(self.package_2, original_pack_transfer.move_line_ids.package_id)
480
+ # Operator-2 finishes its operation regarding the first pallet without
481
+ # any trouble as the processed move line has been put in its own
482
+ # move+transfer
483
+ qty = pack_first_pallet.reserved_uom_qty
484
+ response = self.service.set_destination_all(
485
+ pack_first_pallet.location_id.id, pack_first_pallet.location_dest_id.barcode
486
+ )
487
+ self.assert_response_start(
488
+ response,
489
+ message=self.service.msg_store.location_content_transfer_complete(
490
+ pack_first_pallet.location_id,
491
+ pack_first_pallet.location_dest_id,
492
+ ),
493
+ )
494
+ self.assertEqual(pack_first_pallet.qty_done, 6)
495
+ self.assertEqual(pack_first_pallet.state, "done")
496
+ self.assertEqual(pack_first_pallet.move_id.product_uom_qty, qty)
497
+ # Operator-2 with the "location content transfer" scenario scan
498
+ # the location where the second pallet is.
499
+ original_pack_transfer = pack_second_pallet.picking_id
500
+ response = self._location_content_transfer_process_line(pack_second_pallet)
501
+ new_pack_transfer = pack_second_pallet.picking_id
502
+ # Transfer hasn't been split as we were processing the last line/pallet
503
+ self.assertEqual(original_pack_transfer, new_pack_transfer)
504
+ self.assert_response_scan_destination_all(response, new_pack_transfer)
505
+ response_packages = response["data"]["scan_destination_all"]["package_levels"]
506
+ self.assertEqual(len(response_packages), 1)
507
+ self.assertEqual(
508
+ response_packages[0]["package_src"]["id"], pack_second_pallet.package_id.id
509
+ )
@@ -0,0 +1,143 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+
4
+ from .test_location_content_transfer_base import LocationContentTransferCommonCase
5
+
6
+ # pylint: disable=missing-return
7
+
8
+
9
+ class TestLocationContentTransferPutaway(LocationContentTransferCommonCase):
10
+ """Tests with putaway when using option to ignore unavailable putaway locations"""
11
+
12
+ @classmethod
13
+ def setUpClassVars(cls, *args, **kwargs):
14
+ super().setUpClassVars(*args, **kwargs)
15
+ cls.pallets_storage_type = cls.env.ref(
16
+ "stock_storage_type.package_storage_type_pallets"
17
+ )
18
+ cls.main_pallets_location = cls.env.ref(
19
+ "stock_storage_type.stock_location_pallets"
20
+ )
21
+ cls.reserve_pallets_locations = cls.env.ref(
22
+ "stock_storage_type.stock_location_pallets_reserve"
23
+ )
24
+ cls.all_pallets_locations = (
25
+ cls.main_pallets_location.leaf_location_ids
26
+ | cls.reserve_pallets_locations.leaf_location_ids
27
+ )
28
+
29
+ @classmethod
30
+ def setUpClassBaseData(cls, *args, **kwargs):
31
+ super().setUpClassBaseData(*args, **kwargs)
32
+ cls.package = cls.env["stock.quant.package"].create(
33
+ {
34
+ # this will parameterize the putaway to use pallet locations,
35
+ # and if not, it will stay on the picking type's default dest.
36
+ "package_type_id": cls.pallets_storage_type.id,
37
+ }
38
+ )
39
+ cls.package2 = cls.env["stock.quant.package"].create(
40
+ {
41
+ # this will parameterize the putaway to use pallet locations,
42
+ # and if not, it will stay on the picking type's default dest.
43
+ "package_type_id": cls.pallets_storage_type.id,
44
+ }
45
+ )
46
+ # create a location to be sure it's empty
47
+ cls.test_loc = (
48
+ cls.env["stock.location"]
49
+ .sudo()
50
+ .create(
51
+ {
52
+ "location_id": cls.stock_location.id,
53
+ "name": "test",
54
+ "barcode": "test_loc",
55
+ }
56
+ )
57
+ )
58
+ cls._update_qty_in_location(
59
+ cls.test_loc, cls.product_a, 10, package=cls.package
60
+ )
61
+ cls._update_qty_in_location(
62
+ cls.test_loc, cls.product_a, 10, package=cls.package2
63
+ )
64
+ cls.menu.sudo().allow_move_create = True
65
+ cls.menu.sudo().ignore_no_putaway_available = True
66
+ cls.menu.sudo().allow_unreserve_other_moves = True
67
+
68
+ def test_normal_putaway(self):
69
+ """Ensure putaway is applied on moves"""
70
+ response = self.service.dispatch(
71
+ "scan_location", params={"barcode": self.test_loc.barcode}
72
+ )
73
+ self.assert_response(
74
+ response,
75
+ next_state="start_single",
76
+ data=self.ANY,
77
+ )
78
+ package_level_id = response["data"]["start_single"]["package_level"]["id"]
79
+ package_level = self.env["stock.package_level"].browse(package_level_id)
80
+ self.assertIn(package_level.location_dest_id, self.all_pallets_locations)
81
+
82
+ def test_ignore_no_putaway_available(self):
83
+ """Ignore no putaway available is activated on the menu
84
+
85
+ In this case, when no putaway is possible, the changes
86
+ are rollbacked and an error is returned.
87
+ """
88
+ for location in self.all_pallets_locations:
89
+ package = self.env["stock.quant.package"].create(
90
+ {"package_type_id": self.pallets_storage_type.id}
91
+ )
92
+ self._update_qty_in_location(location, self.product_a, 10, package=package)
93
+
94
+ response = self.service.dispatch(
95
+ "scan_location", params={"barcode": self.test_loc.barcode}
96
+ )
97
+ self.assert_response(
98
+ response,
99
+ next_state="scan_location",
100
+ message=self.service.msg_store.no_putaway_destination_available(),
101
+ )
102
+
103
+ package_levels = self.env["stock.package_level"].search(
104
+ [("package_id", "in", (self.package.id, self.package2.id))]
105
+ )
106
+ # no package level created to move the package
107
+ self.assertFalse(package_levels)
108
+
109
+ def test_putaway_move_dest_not_child_of_picking_type_dest(self):
110
+ """Putaway is applied on move but the destination location is not a
111
+ child of the default picking type destination location.
112
+ """
113
+ # Change the default destination location of the picking type
114
+ # to get it outside of the putaway destination
115
+ self.picking_type.sudo().default_location_dest_id = self.main_pallets_location
116
+ # Create a standard putaway to move the package from pallet storage
117
+ # to a unrelated one (outside of the pallet storage tree)
118
+ self.env["stock.putaway.rule"].sudo().create(
119
+ {
120
+ "product_id": self.product_a.id,
121
+ "location_in_id": self.picking_type.default_location_dest_id.id,
122
+ "location_out_id": self.env.ref("stock.location_refrigerator_small").id,
123
+ }
124
+ )
125
+ # Check the result
126
+ existing_moves = self.env["stock.move"].search(
127
+ [("location_id", "=", self.test_loc.id), ("state", "=", "assigned")]
128
+ )
129
+ response = self.service.dispatch(
130
+ "scan_location", params={"barcode": self.test_loc.barcode}
131
+ )
132
+ self.assert_response(
133
+ response,
134
+ next_state="scan_location",
135
+ data=self.ANY,
136
+ message=self.service.msg_store.location_content_unable_to_transfer(
137
+ self.test_loc
138
+ ),
139
+ )
140
+ current_moves = self.env["stock.move"].search(
141
+ [("location_id", "=", self.test_loc.id), ("state", "=", "assigned")]
142
+ )
143
+ self.assertEqual(existing_moves, current_moves)
@@ -0,0 +1,34 @@
1
+ # Copyright 2023 Camptocamp SA
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
3
+
4
+ from .test_location_content_transfer_base import LocationContentTransferCommonCase
5
+
6
+ # pylint: disable=missing-return
7
+
8
+
9
+ class TestLocationContentTransferScanLocation(LocationContentTransferCommonCase):
10
+ @classmethod
11
+ def setUpClassBaseData(cls):
12
+ super().setUpClassBaseData()
13
+ # One picking with shipping policy set on "When all products are ready"
14
+ # With only one of the move available in the stock
15
+ cls.picking1 = cls._create_picking(
16
+ lines=[(cls.product_a, 10), (cls.product_b, 10)]
17
+ )
18
+ cls.picking1.move_type = "one"
19
+ cls.move1 = cls.picking1.move_ids[0]
20
+ cls._fill_stock_for_moves(cls.move1, in_package=False, location=cls.content_loc)
21
+ cls.picking1.action_assign()
22
+ # Another picking available
23
+ picking2 = cls._create_picking(lines=[(cls.product_c, 5)])
24
+ cls._fill_stock_for_moves(picking2.move_ids, location=cls.content_loc)
25
+ picking2.action_assign()
26
+
27
+ def test_lines_returned_by_scan_location(self):
28
+ """Check that lines from not ready pickings are not offered to work on."""
29
+ response = self.service.dispatch(
30
+ "scan_location", params={"barcode": self.content_loc.barcode}
31
+ )
32
+ lines = response["data"]["scan_destination_all"]["move_lines"]
33
+ line_ids = [line["id"] for line in lines]
34
+ self.assertTrue(self.move1.move_line_ids.id not in line_ids)