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,1074 @@
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 LocationContentTransferSetDestinationXCase(LocationContentTransferCommonCase):
10
+ """Tests for endpoint used from scan_destination
11
+
12
+ * /set_destination_package
13
+ * /set_destination_line
14
+
15
+ """
16
+
17
+ # TODO see what can be common
18
+ @classmethod
19
+ def setUpClassBaseData(cls):
20
+ super().setUpClassBaseData()
21
+ products = cls.product_a + cls.product_b + cls.product_c + cls.product_d
22
+ for product in products:
23
+ cls.env["stock.putaway.rule"].sudo().create(
24
+ {
25
+ "product_id": product.id,
26
+ "location_in_id": cls.stock_location.id,
27
+ "location_out_id": cls.shelf1.id,
28
+ }
29
+ )
30
+
31
+ cls.picking1 = picking1 = cls._create_picking(
32
+ lines=[(cls.product_a, 10), (cls.product_b, 10)]
33
+ )
34
+ cls.picking2 = picking2 = cls._create_picking(
35
+ lines=[(cls.product_c, 10), (cls.product_d, 10)]
36
+ )
37
+ cls.pickings = picking1 | picking2
38
+ cls._fill_stock_for_moves(
39
+ picking1.move_ids, in_package=True, location=cls.content_loc
40
+ )
41
+ cls._fill_stock_for_moves(picking2.move_ids, location=cls.content_loc)
42
+ cls.pickings.action_assign()
43
+ cls._simulate_pickings_selected(cls.pickings)
44
+ cls.dest_location = (
45
+ cls.env["stock.location"]
46
+ .sudo()
47
+ .create(
48
+ {
49
+ "name": "Sub Shelf 1",
50
+ "barcode": "subshelf1",
51
+ "location_id": cls.shelf1.id,
52
+ }
53
+ )
54
+ )
55
+ cls.warehouse = cls.env.ref("stock.warehouse0")
56
+
57
+ def test_set_destination_package_wrong_parameters(self):
58
+ """Wrong 'location' and 'package_level_id' parameters, redirect the
59
+ user to the 'start' screen.
60
+ """
61
+ package_level = self.picking1.package_level_ids[0]
62
+ self._simulate_selected_move_line(package_level.move_line_ids)
63
+ response = self.service.dispatch(
64
+ "set_destination_package",
65
+ params={
66
+ "location_id": 1234567890, # Doesn't exist
67
+ "package_level_id": package_level.id,
68
+ "barcode": "TEST",
69
+ },
70
+ )
71
+ self.assert_response_start(
72
+ response, message=self.service.msg_store.record_not_found()
73
+ )
74
+ response = self.service.dispatch(
75
+ "set_destination_package",
76
+ params={
77
+ "location_id": self.content_loc.id,
78
+ "package_level_id": 1234567890, # Doesn't exist
79
+ "barcode": "TEST",
80
+ },
81
+ )
82
+ move_lines = self.service._find_transfer_move_lines(self.content_loc)
83
+ self.assert_response_start_single(
84
+ response,
85
+ move_lines.mapped("picking_id"),
86
+ )
87
+
88
+ def test_set_destination_package_dest_location_nok(self):
89
+ """Scanned destination location not valid, redirect to 'scan_destination'."""
90
+ package_level = self.picking1.package_level_ids[0]
91
+ self._simulate_selected_move_line(package_level.move_line_ids)
92
+ # Unknown destination location
93
+ response = self.service.dispatch(
94
+ "set_destination_package",
95
+ params={
96
+ "location_id": self.content_loc.id,
97
+ "package_level_id": package_level.id,
98
+ "barcode": "UNKNOWN_LOCATION",
99
+ },
100
+ )
101
+ self.assert_response_scan_destination(
102
+ response,
103
+ package_level,
104
+ message=self.service.msg_store.no_location_found(),
105
+ )
106
+ # Destination location not allowed
107
+ customer_location = self.env.ref("stock.stock_location_customers")
108
+ customer_location.sudo().barcode = "CUSTOMER"
109
+ response = self.service.dispatch(
110
+ "set_destination_package",
111
+ params={
112
+ "location_id": self.content_loc.id,
113
+ "package_level_id": package_level.id,
114
+ "barcode": customer_location.barcode,
115
+ },
116
+ )
117
+ self.assert_response_scan_destination(
118
+ response,
119
+ package_level,
120
+ message=self.service.msg_store.dest_location_not_allowed(),
121
+ )
122
+
123
+ def test_set_destination_package_dest_location_move_nok(self):
124
+ """Scanned destination location not valid (different as move and picking)"""
125
+ package_level = self.picking1.package_level_ids[0]
126
+ # if the move related to the package level has a destination
127
+ # location not a parent or equal to the scanned location,
128
+ # refuse the action
129
+ move = package_level.move_line_ids.move_id
130
+ move.location_dest_id = self.shelf1
131
+ move.picking_id.location_dest_id = self.shelf1
132
+ self._simulate_selected_move_line(package_level.move_line_ids)
133
+ response = self.service.dispatch(
134
+ "set_destination_package",
135
+ params={
136
+ "location_id": self.content_loc.id,
137
+ "package_level_id": package_level.id,
138
+ "barcode": self.shelf2.barcode,
139
+ },
140
+ )
141
+ self.assert_response_scan_destination(
142
+ response,
143
+ package_level,
144
+ message=self.service.msg_store.dest_location_not_allowed(),
145
+ )
146
+
147
+ def test_set_destination_package_dest_location_to_confirm(self):
148
+ """Scanned destination location valid, but need a confirmation."""
149
+ package_level = self.picking1.package_level_ids[0]
150
+ self._simulate_selected_move_line(package_level.move_line_ids)
151
+ response = self.service.dispatch(
152
+ "set_destination_package",
153
+ params={
154
+ "location_id": self.content_loc.id,
155
+ "package_level_id": package_level.id,
156
+ "barcode": self.env.ref("stock.stock_location_14").barcode,
157
+ },
158
+ )
159
+ self.assert_response_scan_destination(
160
+ response,
161
+ package_level,
162
+ message=self.service.msg_store.need_confirmation(),
163
+ confirmation_required=True,
164
+ )
165
+
166
+ def test_set_destination_package_dest_location_ok(self):
167
+ """Scanned destination location valid, moves set to done."""
168
+ original_picking = self.picking1
169
+ package_level = original_picking.package_level_ids[0]
170
+ self._simulate_selected_move_line(package_level.move_line_ids)
171
+ response = self.service.dispatch(
172
+ "set_destination_package",
173
+ params={
174
+ "location_id": self.content_loc.id,
175
+ "package_level_id": package_level.id,
176
+ "barcode": self.dest_location.barcode,
177
+ },
178
+ )
179
+ # Check the data (the whole transfer has been validated here w/o backorder)
180
+ self.assertFalse(original_picking.backorder_ids)
181
+ self.assertEqual(original_picking.state, "done")
182
+ self.assertEqual(package_level.state, "done")
183
+ # Check the response
184
+ move_lines = self.service._find_transfer_move_lines(self.content_loc)
185
+ self.assert_response_start_single(
186
+ response,
187
+ move_lines.mapped("picking_id"),
188
+ message=self.service.msg_store.location_content_transfer_item_complete(
189
+ self.dest_location
190
+ ),
191
+ )
192
+ for move in package_level.move_line_ids.mapped("move_id"):
193
+ self.assertEqual(move.state, "done")
194
+
195
+ def test_set_destination_package_dest_location_ok_with_completion_info(self):
196
+ """Scanned destination location valid, moves set to done
197
+ and completion info is returned as the next transfer is ready.
198
+ """
199
+ original_picking = self.picking1
200
+ package_level = original_picking.package_level_ids[0]
201
+ move = package_level.move_line_ids.move_id[0]
202
+ next_move = move.copy(
203
+ {
204
+ "picking_id": False,
205
+ "picking_type_id": self.warehouse.out_type_id.id,
206
+ "location_id": move.location_dest_id.id,
207
+ "location_dest_id": self.customer_location.id,
208
+ "move_orig_ids": [(6, 0, move.ids)],
209
+ }
210
+ )
211
+ next_move._action_confirm(merge=False)
212
+ next_move._assign_picking()
213
+ self.assertEqual(next_move.state, "waiting")
214
+ self.assertTrue(next_move.picking_id)
215
+ self._simulate_selected_move_line(package_level.move_line_ids)
216
+ response = self.service.dispatch(
217
+ "set_destination_package",
218
+ params={
219
+ "location_id": self.content_loc.id,
220
+ "package_level_id": package_level.id,
221
+ "barcode": self.dest_location.barcode,
222
+ },
223
+ )
224
+ # Check the data (the whole transfer has been validated here w/o backorder)
225
+ self.assertFalse(original_picking.backorder_ids)
226
+ self.assertEqual(original_picking.state, "done")
227
+ self.assertEqual(package_level.state, "done")
228
+ self.assertEqual(next_move.state, "assigned")
229
+ # Check the response
230
+ move_lines = self.service._find_transfer_move_lines(self.content_loc)
231
+ completion_info = self.service._actions_for("completion.info")
232
+ completion_info_popup = completion_info.popup(package_level.move_line_ids)
233
+ self.assert_response_start_single(
234
+ response,
235
+ move_lines.mapped("picking_id"),
236
+ message=self.service.msg_store.location_content_transfer_item_complete(
237
+ self.dest_location
238
+ ),
239
+ popup=completion_info_popup,
240
+ )
241
+ for move in package_level.move_line_ids.mapped("move_id"):
242
+ self.assertEqual(move.state, "done")
243
+
244
+ def test_set_destination_line_wrong_parameters(self):
245
+ """Wrong 'location' and 'move_line_id' parameters, redirect the
246
+ user to the 'start' screen.
247
+ """
248
+ move_line = self.picking2.move_line_ids[0]
249
+ self._simulate_selected_move_line(move_line)
250
+ response = self.service.dispatch(
251
+ "set_destination_line",
252
+ params={
253
+ "location_id": 1234567890, # Doesn't exist
254
+ "move_line_id": move_line.id,
255
+ "quantity": move_line.reserved_uom_qty,
256
+ "barcode": "TEST",
257
+ },
258
+ )
259
+ self.assert_response_start(
260
+ response, message=self.service.msg_store.record_not_found()
261
+ )
262
+ response = self.service.dispatch(
263
+ "set_destination_line",
264
+ params={
265
+ "location_id": self.content_loc.id,
266
+ "move_line_id": 1234567890, # Doesn't exist
267
+ "quantity": move_line.reserved_uom_qty,
268
+ "barcode": "TEST",
269
+ },
270
+ )
271
+ move_lines = self.service._find_transfer_move_lines(self.content_loc)
272
+ self.assert_response_start_single(
273
+ response,
274
+ move_lines.mapped("picking_id"),
275
+ )
276
+
277
+ def test_set_destination_line_dest_location_nok(self):
278
+ """Scanned destination location not valid, redirect to 'scan_destination'."""
279
+ move_line = self.picking2.move_line_ids[0]
280
+ self._simulate_selected_move_line(move_line)
281
+ # Unknown destination location
282
+ response = self.service.dispatch(
283
+ "set_destination_line",
284
+ params={
285
+ "location_id": self.content_loc.id,
286
+ "move_line_id": move_line.id,
287
+ "quantity": move_line.reserved_uom_qty,
288
+ "barcode": "UNKNOWN_LOCATION",
289
+ },
290
+ )
291
+ self.assert_response_scan_destination(
292
+ response,
293
+ move_line,
294
+ message=self.service.msg_store.no_location_found(),
295
+ )
296
+ # Destination location not allowed
297
+ customer_location = self.env.ref("stock.stock_location_customers")
298
+ customer_location.sudo().barcode = "CUSTOMER"
299
+ response = self.service.dispatch(
300
+ "set_destination_line",
301
+ params={
302
+ "location_id": self.content_loc.id,
303
+ "move_line_id": move_line.id,
304
+ "quantity": move_line.reserved_uom_qty,
305
+ "barcode": customer_location.barcode,
306
+ },
307
+ )
308
+ self.assert_response_scan_destination(
309
+ response,
310
+ move_line,
311
+ message=self.service.msg_store.dest_location_not_allowed(),
312
+ )
313
+
314
+ def test_set_destination_line_dest_location_move_nok(self):
315
+ """Scanned destination location not valid (different as picking and move)"""
316
+ move_line = self.picking2.move_line_ids[0]
317
+ # if the move related to the move line has a destination
318
+ # location not a parent or equal to the scanned location,
319
+ # refuse the action
320
+ move_line.move_id.location_dest_id = self.shelf1
321
+ move_line.picking_id.location_dest_id = self.shelf1
322
+ self._simulate_selected_move_line(move_line)
323
+ response = self.service.dispatch(
324
+ "set_destination_line",
325
+ params={
326
+ "location_id": self.content_loc.id,
327
+ "move_line_id": move_line.id,
328
+ "quantity": move_line.reserved_uom_qty,
329
+ "barcode": self.shelf2.barcode,
330
+ },
331
+ )
332
+ self.assert_response_scan_destination(
333
+ response,
334
+ move_line,
335
+ message=self.service.msg_store.dest_location_not_allowed(),
336
+ )
337
+
338
+ def test_set_destination_line_dest_location_to_confirm(self):
339
+ """Scanned destination location valid, but need a confirmation."""
340
+ move_line = self.picking2.move_line_ids[0]
341
+ self._simulate_selected_move_line(move_line)
342
+ response = self.service.dispatch(
343
+ "set_destination_line",
344
+ params={
345
+ "location_id": self.content_loc.id,
346
+ "move_line_id": move_line.id,
347
+ "quantity": move_line.reserved_uom_qty,
348
+ "barcode": self.env.ref("stock.stock_location_14").barcode,
349
+ },
350
+ )
351
+ self.assert_response_scan_destination(
352
+ response,
353
+ move_line,
354
+ message=self.service.msg_store.need_confirmation(),
355
+ confirmation_required=True,
356
+ )
357
+
358
+ def test_set_destination_line_dest_location_ok(self):
359
+ """Scanned destination location valid, moves set to done."""
360
+ original_picking = self.picking2
361
+ move_line = original_picking.move_line_ids[0]
362
+ self._simulate_selected_move_line(move_line)
363
+ response = self.service.dispatch(
364
+ "set_destination_line",
365
+ params={
366
+ "location_id": self.content_loc.id,
367
+ "move_line_id": move_line.id,
368
+ "quantity": move_line.reserved_uom_qty,
369
+ "barcode": self.dest_location.barcode,
370
+ },
371
+ )
372
+ # Check the resulting data
373
+ # We got a new picking as the original one had two moves (and we
374
+ # validated only one)
375
+ new_picking = move_line.picking_id
376
+ self.assertTrue(new_picking != original_picking)
377
+ self.assertEqual(move_line.move_id.state, "done")
378
+ self.assertEqual(move_line.picking_id.state, "done")
379
+ self.assertEqual(original_picking.state, "assigned")
380
+ # Check the response
381
+ move_lines = self.service._find_transfer_move_lines(self.content_loc)
382
+ self.assert_response_start_single(
383
+ response,
384
+ move_lines.mapped("picking_id"),
385
+ message=self.service.msg_store.location_content_transfer_item_complete(
386
+ self.dest_location
387
+ ),
388
+ )
389
+
390
+ def test_set_destination_line_dest_location_ok_with_completion_info(self):
391
+ """Scanned destination location valid, moves set to done
392
+ and completion info is returned as the next transfer is ready.
393
+ """
394
+ original_picking = self.picking2
395
+ move_line = original_picking.move_line_ids[0]
396
+ move = move_line.move_id
397
+ next_move = move.copy(
398
+ {
399
+ "picking_id": False,
400
+ "picking_type_id": self.warehouse.out_type_id.id,
401
+ "location_id": move.location_dest_id.id,
402
+ "location_dest_id": self.customer_location.id,
403
+ "move_orig_ids": [(6, 0, move.ids)],
404
+ }
405
+ )
406
+ next_move._action_confirm(merge=False)
407
+ next_move._assign_picking()
408
+ self.assertEqual(next_move.state, "waiting")
409
+ self.assertTrue(next_move.picking_id)
410
+ self._simulate_selected_move_line(move_line)
411
+ response = self.service.dispatch(
412
+ "set_destination_line",
413
+ params={
414
+ "location_id": self.content_loc.id,
415
+ "move_line_id": move_line.id,
416
+ "quantity": move_line.reserved_uom_qty,
417
+ "barcode": self.dest_location.barcode,
418
+ },
419
+ )
420
+ # Check the resulting data
421
+ # We got a new picking as the original one had two moves (and we
422
+ # validated only one)
423
+ new_picking = move_line.picking_id
424
+ self.assertTrue(new_picking != original_picking)
425
+ self.assertEqual(move_line.move_id.state, "done")
426
+ self.assertEqual(move_line.picking_id.state, "done")
427
+ self.assertEqual(original_picking.state, "assigned")
428
+ self.assertEqual(next_move.state, "assigned")
429
+ # Check the response
430
+ move_lines = self.service._find_transfer_move_lines(self.content_loc)
431
+ completion_info = self.service._actions_for("completion.info")
432
+ completion_info_popup = completion_info.popup(move_line)
433
+ self.assert_response_start_single(
434
+ response,
435
+ move_lines.mapped("picking_id"),
436
+ message=self.service.msg_store.location_content_transfer_item_complete(
437
+ self.dest_location
438
+ ),
439
+ popup=completion_info_popup,
440
+ )
441
+
442
+ def test_set_destination_line_partial_qty(self):
443
+ """Scanned destination location with partial qty, but related moves
444
+ has to be splitted.
445
+ """
446
+ original_picking = self.picking2
447
+ move_line_c = original_picking.move_line_ids.filtered(
448
+ lambda m: m.product_id == self.product_c
449
+ )
450
+ self.assertEqual(move_line_c.reserved_uom_qty, 10)
451
+ self.assertEqual(move_line_c.qty_done, 10)
452
+ self._simulate_selected_move_line(move_line_c)
453
+ # Scan partial qty (6/10)
454
+ response = self.service.dispatch(
455
+ "set_destination_line",
456
+ params={
457
+ "location_id": self.content_loc.id,
458
+ "move_line_id": move_line_c.id,
459
+ "quantity": move_line_c.reserved_uom_qty - 4, # Scan 6 qty
460
+ "barcode": self.dest_location.barcode,
461
+ },
462
+ )
463
+ done_picking = original_picking.backorder_ids
464
+ # Check move line data
465
+ self.assertEqual(move_line_c.move_id.product_uom_qty, 6)
466
+ self.assertEqual(move_line_c.reserved_uom_qty, 0)
467
+ self.assertEqual(move_line_c.qty_done, 6)
468
+ self.assertEqual(move_line_c.state, "done")
469
+ self.assertEqual(original_picking.backorder_ids, done_picking)
470
+ self.assertEqual(done_picking.state, "done")
471
+
472
+ # the remaining move is put in a backorder
473
+ move = done_picking.backorder_ids.move_ids
474
+ self.assertEqual(move.picking_id.state, "assigned")
475
+
476
+ self.assertEqual(move.state, "assigned")
477
+ self.assertEqual(move.product_id, self.product_c)
478
+ self.assertEqual(move.product_uom_qty, 4)
479
+ self.assertEqual(move.move_line_ids.reserved_uom_qty, 4)
480
+ self.assertEqual(move.move_line_ids.qty_done, 4)
481
+ # Check the response -> we must first process the backorder
482
+ self.assert_response_start_single(
483
+ response,
484
+ done_picking.backorder_ids,
485
+ message=self.service.msg_store.location_content_transfer_item_complete(
486
+ self.dest_location
487
+ ),
488
+ )
489
+ self.assertEqual(move_line_c.move_id.state, "done")
490
+ # Scan remaining qty (4/10)
491
+ remaining_move_line_c = move.move_line_ids
492
+ self._simulate_selected_move_line(remaining_move_line_c)
493
+ self.service.dispatch(
494
+ "set_destination_line",
495
+ params={
496
+ "location_id": self.content_loc.id,
497
+ "move_line_id": remaining_move_line_c.id,
498
+ "quantity": remaining_move_line_c.reserved_uom_qty,
499
+ "barcode": self.dest_location.barcode,
500
+ },
501
+ )
502
+ done_picking2 = remaining_move_line_c.picking_id
503
+ # Check move line data
504
+ self.assertEqual(remaining_move_line_c.move_id.product_uom_qty, 4)
505
+ self.assertEqual(remaining_move_line_c.reserved_uom_qty, 0)
506
+ self.assertEqual(remaining_move_line_c.qty_done, 4)
507
+ self.assertEqual(remaining_move_line_c.state, "done")
508
+ self.assertTrue(done_picking2 != original_picking)
509
+ self.assertEqual(done_picking2.state, "done")
510
+ # All move lines related to product_c are now done and extracted from
511
+ # the initial transfer
512
+ all_pickings = original_picking | done_picking | done_picking2
513
+ moves_product_c = all_pickings.move_ids.filtered(
514
+ lambda m: m.product_id == self.product_c
515
+ )
516
+ moves_product_c_done = all(move.state == "done" for move in moves_product_c)
517
+ self.assertTrue(moves_product_c_done)
518
+ moves_product_c_qty_done = sum([move.quantity_done for move in moves_product_c])
519
+ self.assertEqual(moves_product_c_qty_done, 10)
520
+ # The picking is still not done as product_d hasn't been processed
521
+ self.assertEqual(original_picking.state, "assigned")
522
+ # Let scan product_d quantity and check picking state
523
+ move_line_d = original_picking.move_line_ids.filtered(
524
+ lambda m: m.product_id == self.product_d
525
+ )
526
+ self._simulate_selected_move_line(move_line_d)
527
+ self.service.dispatch(
528
+ "set_destination_line",
529
+ params={
530
+ "location_id": self.content_loc.id,
531
+ "move_line_id": move_line_d.id,
532
+ "quantity": move_line_d.reserved_uom_qty,
533
+ "barcode": self.dest_location.barcode,
534
+ },
535
+ )
536
+ self.assertEqual(move_line_d.move_id.product_uom_qty, 10)
537
+ self.assertEqual(move_line_d.reserved_uom_qty, 0)
538
+ self.assertEqual(move_line_d.qty_done, 10)
539
+ self.assertEqual(move_line_d.state, "done")
540
+ self.assertEqual(original_picking.state, "done")
541
+
542
+ def test_set_destination_line_partial_qty_with_backorder_policy(self):
543
+ """Scanned destination location with partial qty, but related moves
544
+ has to be splitted. Since the backorder policy is 'never', the
545
+ remaining move line should be removed.
546
+ """
547
+ # set the backorder policy to 'never'
548
+
549
+ picking = self._create_picking(lines=[(self.product_a, 10)])
550
+ picking.picking_type_id.sudo().create_backorder = "never"
551
+ self._update_qty_in_location(picking.location_id, self.product_a, 20)
552
+ # Reserve quantities
553
+ picking.action_assign()
554
+ self._simulate_pickings_selected(picking)
555
+ move_line = picking.move_line_ids[0]
556
+ self._simulate_selected_move_line(move_line)
557
+ # Scan partial qty (6/10)
558
+ self.service.dispatch(
559
+ "set_destination_line",
560
+ params={
561
+ "location_id": self.content_loc.id,
562
+ "move_line_id": move_line.id,
563
+ "quantity": move_line.reserved_uom_qty - 4, # Scan 6 qty
564
+ "barcode": self.dest_location.barcode,
565
+ },
566
+ )
567
+ done_picking = picking
568
+ # Check move line data
569
+ self.assertEqual(move_line.move_id.product_uom_qty, 6)
570
+ self.assertEqual(move_line.reserved_uom_qty, 0)
571
+ self.assertEqual(move_line.qty_done, 6)
572
+ self.assertEqual(move_line.state, "done")
573
+ self.assertEqual(done_picking.state, "done")
574
+
575
+ # no remaining move should exist
576
+ self.assertFalse(done_picking.backorder_ids.move_ids)
577
+
578
+ def test_set_destination_lines_partial_qty_with_backorder_policy(self):
579
+ """Scanned destination location with partial qty, but related moves
580
+ has to be splitted. Since the backorder policy is 'never', the
581
+ remaining move line should be removed.
582
+
583
+ # multi lines mode
584
+ """
585
+ # set the backorder policy to 'never'
586
+
587
+ picking = self._create_picking(
588
+ lines=[(self.product_a, 10), (self.product_b, 10)]
589
+ )
590
+ picking.picking_type_id.sudo().create_backorder = "never"
591
+ self._update_qty_in_location(picking.location_id, self.product_a, 20)
592
+ self._update_qty_in_location(picking.location_id, self.product_b, 20)
593
+ # Reserve quantities
594
+ picking.action_assign()
595
+ self._simulate_pickings_selected(picking)
596
+ move_line = picking.move_line_ids.filtered(
597
+ lambda ml: ml.product_id == self.product_a
598
+ )
599
+ self._simulate_selected_move_line(move_line)
600
+ # Scan partial qty (6/10)
601
+ self.service.dispatch(
602
+ "set_destination_line",
603
+ params={
604
+ "location_id": self.content_loc.id,
605
+ "move_line_id": move_line.id,
606
+ "quantity": move_line.reserved_uom_qty - 4, # Scan 6 qty
607
+ "barcode": self.dest_location.barcode,
608
+ },
609
+ )
610
+ # 2 operations then the done operation is set into a specific picking
611
+ first_done_picking = picking.backorder_ids
612
+ # Check move line data
613
+ self.assertEqual(move_line.move_id.product_uom_qty, 6)
614
+ self.assertEqual(move_line.reserved_uom_qty, 0)
615
+ self.assertEqual(move_line.qty_done, 6)
616
+ self.assertEqual(move_line.state, "done")
617
+ self.assertEqual(first_done_picking.state, "done")
618
+
619
+ # no remaining move should exist
620
+ self.assertFalse(first_done_picking.backorder_ids.move_ids)
621
+
622
+ # process the second line
623
+ move_line = picking.move_line_ids.filtered(
624
+ lambda ml: ml.product_id == self.product_b
625
+ )
626
+ self._simulate_selected_move_line(move_line)
627
+ # Scan partial qty (6/10)
628
+ self.service.dispatch(
629
+ "set_destination_line",
630
+ params={
631
+ "location_id": self.content_loc.id,
632
+ "move_line_id": move_line.id,
633
+ "quantity": move_line.reserved_uom_qty - 4, # Scan 6 qty
634
+ "barcode": self.dest_location.barcode,
635
+ },
636
+ )
637
+
638
+ # the initial picking should be done
639
+ # Check move line data
640
+ self.assertEqual(move_line.move_id.product_uom_qty, 6)
641
+ self.assertEqual(move_line.reserved_uom_qty, 0)
642
+ self.assertEqual(move_line.qty_done, 6)
643
+ self.assertEqual(move_line.state, "done")
644
+ self.assertEqual(picking.state, "done")
645
+
646
+ # no remaining move should exist
647
+ self.assertEqual(picking.backorder_ids, first_done_picking)
648
+
649
+
650
+ class LocationContentTransferSetDestinationXSpecialCase(
651
+ LocationContentTransferCommonCase
652
+ ):
653
+ """Tests for endpoint used from scan_destination (special cases)
654
+
655
+ * /set_destination_package
656
+ * /set_destination_line
657
+
658
+ """
659
+
660
+ @classmethod
661
+ def setUpClassBaseData(cls):
662
+ super().setUpClassBaseData()
663
+ products = cls.product_a
664
+ for product in products:
665
+ cls.env["stock.putaway.rule"].sudo().create(
666
+ {
667
+ "product_id": product.id,
668
+ "location_in_id": cls.stock_location.id,
669
+ "location_out_id": cls.shelf1.id,
670
+ }
671
+ )
672
+
673
+ cls.picking = cls._create_picking(
674
+ lines=[(cls.product_a, 10), (cls.product_b, 10)]
675
+ )
676
+ cls.move_product_a = cls.picking.move_ids.filtered(
677
+ lambda m: m.product_id == cls.product_a
678
+ )
679
+ cls.move_product_b = cls.picking.move_ids.filtered(
680
+ lambda m: m.product_id == cls.product_b
681
+ )
682
+ # Change the initial demand of product_a to get two move lines for
683
+ # reserved qties:
684
+ # - 10 from the package
685
+ # - 5 from the qty without package
686
+ cls._fill_stock_for_moves(
687
+ cls.move_product_a, in_package=True, location=cls.content_loc
688
+ )
689
+ cls.move_product_a.product_uom_qty = 15
690
+ cls._update_qty_in_location(
691
+ cls.picking.location_id,
692
+ cls.product_a,
693
+ 5,
694
+ )
695
+ # Put product_b quantities in two different source locations to get
696
+ # two stock move lines (6 and 4 to satisfy 10 qties)
697
+ cls._update_qty_in_location(cls.picking.location_id, cls.product_b, 6)
698
+ cls._update_qty_in_location(cls.content_loc, cls.product_b, 4)
699
+ # Reserve quantities
700
+ cls.picking.action_assign()
701
+ cls._simulate_pickings_selected(cls.picking)
702
+ cls.dest_location = (
703
+ cls.env["stock.location"]
704
+ .sudo()
705
+ .create(
706
+ {
707
+ "name": "Sub Shelf 1",
708
+ "barcode": "subshelf1",
709
+ "location_id": cls.shelf1.id,
710
+ }
711
+ )
712
+ )
713
+
714
+ def test_set_destination_package_split_move(self):
715
+ """Scanned destination location valid for a package, but related moves
716
+ has to be splitted because it is linked to additional move lines.
717
+ """
718
+ original_picking = self.picking
719
+ self.assertEqual(len(original_picking.move_ids), 2)
720
+ self.assertEqual(len(self.move_product_a.move_line_ids), 2)
721
+ package_level = original_picking.package_level_ids[0]
722
+ self._simulate_selected_move_line(package_level.move_line_ids)
723
+ response = self.service.dispatch(
724
+ "set_destination_package",
725
+ params={
726
+ "location_id": self.content_loc.id,
727
+ "package_level_id": package_level.id,
728
+ "barcode": self.dest_location.barcode,
729
+ },
730
+ )
731
+ done_picking = package_level.picking_id
732
+ # Check the picking data
733
+ self.assertEqual(original_picking.backorder_ids, done_picking)
734
+ self.assertEqual(package_level.location_dest_id, self.dest_location)
735
+ for move_line in package_level.move_line_ids:
736
+ self.assertEqual(move_line.location_dest_id, self.dest_location)
737
+ moves_product_a = original_picking.move_ids.filtered(
738
+ lambda m: m.product_id == self.product_a
739
+ )
740
+ self.assertEqual(len(original_picking.move_ids), 2)
741
+ self.assertEqual(len(moves_product_a), 1)
742
+ for move in moves_product_a:
743
+ self.assertEqual(len(move.move_line_ids), 1)
744
+ move_lines_wo_pkg = original_picking.move_line_ids_without_package
745
+ move_lines_wo_pkg_states = set(move_lines_wo_pkg.mapped("state"))
746
+ self.assertEqual(len(move_lines_wo_pkg_states), 1)
747
+ self.assertEqual(move_lines_wo_pkg_states.pop(), "assigned")
748
+ self.assertEqual(done_picking.package_level_ids.state, "done")
749
+ # Check the response
750
+ move_lines = self.service._find_transfer_move_lines(self.content_loc)
751
+ self.assert_response_start_single(
752
+ response,
753
+ move_lines.mapped("picking_id"),
754
+ message=self.service.msg_store.location_content_transfer_item_complete(
755
+ self.dest_location
756
+ ),
757
+ )
758
+
759
+ def test_set_destination_line_split_move(self):
760
+ """Scanned destination location valid for a move line, but related moves
761
+ has to be splitted because it is linked to additional move lines.
762
+ """
763
+ original_picking = self.picking
764
+ self.assertEqual(len(original_picking.move_ids), 2)
765
+ self.assertEqual(len(self.move_product_b.move_line_ids), 2)
766
+ move_line = self.move_product_b.move_line_ids.filtered(
767
+ lambda ml: ml.reserved_uom_qty == 6
768
+ )
769
+ self._simulate_selected_move_line(move_line)
770
+ response = self.service.dispatch(
771
+ "set_destination_line",
772
+ params={
773
+ "location_id": self.content_loc.id,
774
+ "move_line_id": move_line.id,
775
+ "quantity": move_line.reserved_uom_qty,
776
+ "barcode": self.dest_location.barcode,
777
+ },
778
+ )
779
+ done_picking = move_line.picking_id
780
+ # Check the picking data
781
+ self.assertEqual(original_picking.backorder_ids, done_picking)
782
+ self.assertEqual(done_picking.state, "done")
783
+ self.assertEqual(original_picking.state, "assigned")
784
+ self.assertEqual(move_line.move_id.product_uom_qty, 6)
785
+ self.assertEqual(move_line.reserved_uom_qty, 0)
786
+ self.assertEqual(move_line.qty_done, 6)
787
+ self.assertEqual(move_line.location_dest_id, self.dest_location)
788
+ self.assertEqual(len(original_picking.move_ids), 2)
789
+ moves_product_b = original_picking.move_ids.filtered(
790
+ lambda m: m.product_id == self.product_b
791
+ )
792
+ self.assertEqual(len(moves_product_b), 1)
793
+ for move in moves_product_b:
794
+ self.assertEqual(len(move.move_line_ids), 1)
795
+ move_lines_wo_pkg = original_picking.move_line_ids_without_package
796
+ move_lines_wo_pkg_states = set(move_lines_wo_pkg.mapped("state"))
797
+ self.assertEqual(len(move_lines_wo_pkg_states), 1)
798
+ self.assertTrue(all(state == "assigned" for state in move_lines_wo_pkg_states))
799
+ self.assertEqual(move_line.state, "done")
800
+ remaining_move = original_picking.move_ids.filtered(
801
+ lambda m: move_line.move_id != m and m.product_id == self.product_b
802
+ )
803
+ self.assertEqual(remaining_move.state, "assigned")
804
+ self.assertEqual(remaining_move.product_uom_qty, 4)
805
+ self.assertEqual(remaining_move.move_line_ids.reserved_uom_qty, 4)
806
+ self.assertEqual(remaining_move.move_line_ids.qty_done, 4)
807
+ # Check the response
808
+ move_lines = self.service._find_transfer_move_lines(self.content_loc)
809
+ self.assert_response_start_single(
810
+ response,
811
+ move_lines.mapped("picking_id"),
812
+ message=self.service.msg_store.location_content_transfer_item_complete(
813
+ self.dest_location
814
+ ),
815
+ )
816
+ # Process the other move lines (lines w/o package + package levels)
817
+ # to check the picking state
818
+ remaining_move_lines = original_picking.move_line_ids_without_package.filtered(
819
+ lambda ml: ml.state == "assigned"
820
+ )
821
+ for ml in remaining_move_lines:
822
+ self._simulate_selected_move_line(ml)
823
+ self.service.dispatch(
824
+ "set_destination_line",
825
+ params={
826
+ "location_id": self.content_loc.id,
827
+ "move_line_id": ml.id,
828
+ "quantity": ml.reserved_uom_qty,
829
+ "barcode": self.dest_location.barcode,
830
+ },
831
+ )
832
+ self.assertEqual(original_picking.state, "assigned")
833
+ package_level = original_picking.package_level_ids[0]
834
+ self._simulate_selected_move_line(package_level.move_line_ids)
835
+ self.service.dispatch(
836
+ "set_destination_package",
837
+ params={
838
+ "location_id": self.content_loc.id,
839
+ "package_level_id": package_level.id,
840
+ "barcode": self.dest_location.barcode,
841
+ },
842
+ )
843
+ self.assertEqual(original_picking.state, "done")
844
+
845
+
846
+ class LocationContentTransferSetDestinationChainSpecialCase(
847
+ LocationContentTransferCommonCase
848
+ ):
849
+ """Tests for endpoint used from scan_destination (special cases with
850
+ chained pickings)
851
+
852
+ * /set_destination_package
853
+ * /set_destination_line
854
+
855
+ """
856
+
857
+ @classmethod
858
+ def setUpClassBaseData(cls):
859
+ super().setUpClassBaseData()
860
+ # Test split of partial qty when the moves have "move_orig_ids".
861
+ # We create a chain of pickings to ensure the proper state is computed
862
+ # for the split move.
863
+ cls.picking_a = picking_a = cls._create_picking(lines=[(cls.product_c, 10)])
864
+ cls.picking_b = picking_b = cls._create_picking(lines=[(cls.product_c, 10)])
865
+ # connect a and b in a chain of moves
866
+ for move_a in picking_a.move_ids:
867
+ for move_b in picking_b.move_ids:
868
+ if move_a.product_id == move_b.product_id:
869
+ move_a.move_dest_ids = move_b
870
+ move_b.procure_method = "make_to_order"
871
+
872
+ cls.pickings = picking_a | picking_b
873
+ cls._fill_stock_for_moves(picking_a.move_ids, location=cls.content_loc)
874
+ cls.pickings.action_assign()
875
+
876
+ cls.dest_location = (
877
+ cls.env["stock.location"]
878
+ .sudo()
879
+ .create(
880
+ {
881
+ "name": "Sub Shelf 1",
882
+ "barcode": "subshelf1",
883
+ "location_id": cls.shelf1.id,
884
+ }
885
+ )
886
+ )
887
+
888
+ def test_set_destination_line_partial_qty_with_move_orig_ids(self):
889
+ """Scanned destination location with partial qty, but related moves
890
+ has to be split and the move has origin moves (with origin moves)
891
+ """
892
+ picking_a = self.picking_a
893
+ picking_b = self.picking_b
894
+ picking_a.move_line_ids.qty_done = 10
895
+ picking_a._action_done()
896
+ self.assertEqual(picking_a.state, "done")
897
+ self.assertEqual(picking_b.state, "assigned")
898
+ self._simulate_pickings_selected(picking_b)
899
+
900
+ move_line_c = picking_b.move_line_ids.filtered(
901
+ lambda m: m.product_id == self.product_c
902
+ )
903
+
904
+ self.assertEqual(move_line_c.reserved_uom_qty, 10)
905
+ self.assertEqual(move_line_c.qty_done, 10)
906
+ # Scan partial qty (6/10)
907
+ self.service.dispatch(
908
+ "set_destination_line",
909
+ params={
910
+ "location_id": self.content_loc.id,
911
+ "move_line_id": move_line_c.id,
912
+ "quantity": move_line_c.reserved_uom_qty - 4, # Scan 6 qty
913
+ "barcode": self.dest_location.barcode,
914
+ },
915
+ )
916
+ # Check move line data
917
+ self.assertEqual(move_line_c.move_id.product_uom_qty, 6)
918
+ self.assertEqual(move_line_c.reserved_uom_qty, 0)
919
+ self.assertEqual(move_line_c.qty_done, 6)
920
+ self.assertEqual(move_line_c.state, "done")
921
+ # the move has been split
922
+ move = move_line_c.picking_id.backorder_ids.move_ids
923
+ self.assertNotEqual(move_line_c.move_id, move)
924
+
925
+ # Check the move handling the remaining qty
926
+ self.assertEqual(move.state, "assigned")
927
+ move_line = move.move_line_ids
928
+ self.assertEqual(move_line.move_id.product_uom_qty, 4)
929
+ self.assertEqual(move_line.reserved_uom_qty, 4)
930
+ self.assertEqual(move_line.qty_done, 4)
931
+
932
+ def test_set_destination_package_partial_qty_with_move_orig_ids(self):
933
+ """Scanned destination location with partial qty, but related moves
934
+ has to be split and the move has origin moves
935
+ (with package and origin moves)
936
+ """
937
+ picking_a = self.picking_a
938
+ picking_b = self.picking_b
939
+
940
+ # we put 6 in a new package and 4 in another new package
941
+ package1 = self.env["stock.quant.package"].create({})
942
+ package2 = self.env["stock.quant.package"].create({})
943
+ line1 = picking_a.move_line_ids
944
+ line2 = line1.copy({"reserved_uom_qty": 4, "qty_done": 4})
945
+ line1.with_context(bypass_reservation_update=True).reserved_uom_qty = 6
946
+ line1.qty_done = 6
947
+ line1.result_package_id = package1
948
+ line2.result_package_id = package2
949
+ picking_a._action_done()
950
+ self.assertEqual(picking_a.state, "done")
951
+ self.assertEqual(picking_b.state, "assigned")
952
+ # we have 1 move line per package
953
+ self.assertEqual(len(picking_b.move_line_ids), 2)
954
+ self._simulate_pickings_selected(picking_b)
955
+
956
+ move_line = picking_b.move_line_ids.filtered(lambda m: m.package_id == package1)
957
+ move = move_line.move_id
958
+
959
+ self.assertEqual(move_line.reserved_uom_qty, 6.0)
960
+ self.assertEqual(move_line.qty_done, 6.0)
961
+ self._simulate_selected_move_line(move_line)
962
+ # Scan partial qty (6/10)
963
+ self.service.dispatch(
964
+ "set_destination_line",
965
+ params={
966
+ "location_id": self.content_loc.id,
967
+ "move_line_id": move_line.id,
968
+ "quantity": 6.0, # Scan 6 qty
969
+ "barcode": self.dest_location.barcode,
970
+ },
971
+ )
972
+ # Check move line data
973
+ self.assertEqual(move_line.move_id.product_uom_qty, 6)
974
+ self.assertEqual(move_line.reserved_uom_qty, 0)
975
+ self.assertEqual(move_line.qty_done, 6)
976
+ self.assertEqual(move_line.state, "done")
977
+ # the move has been split
978
+ self.assertNotEqual(move_line.move_id, move)
979
+
980
+ # Check the move handling the remaining qty
981
+ self.assertEqual(move.state, "assigned")
982
+ move_line = move.move_line_ids
983
+ self.assertEqual(move_line.move_id.product_uom_qty, 4)
984
+ self.assertEqual(move_line.reserved_uom_qty, 4)
985
+ self.assertEqual(move_line.qty_done, 4)
986
+
987
+
988
+ class LocationContentTransferSetDestinationNextOperationSpecialCase(
989
+ LocationContentTransferCommonCase
990
+ ):
991
+ """Tests for endpoint used from scan_destination to ensure that in
992
+ case of partial qty, the next operation is the one for the remaining
993
+ qty.
994
+
995
+ * /set_destination_line
996
+
997
+ """
998
+
999
+ @classmethod
1000
+ def setUpClassBaseData(cls):
1001
+ super().setUpClassBaseData()
1002
+ cls.picking = cls._create_picking(
1003
+ lines=[(cls.product_a, 10), (cls.product_b, 10)]
1004
+ )
1005
+ cls._update_qty_in_location(cls.picking.location_id, cls.product_a, 20)
1006
+ cls._update_qty_in_location(cls.picking.location_id, cls.product_b, 20)
1007
+ # Reserve quantities
1008
+ cls.picking.action_assign()
1009
+ cls._simulate_pickings_selected(cls.picking)
1010
+ cls.dest_location = (
1011
+ cls.env["stock.location"]
1012
+ .sudo()
1013
+ .create(
1014
+ {
1015
+ "name": "Sub Shelf 1",
1016
+ "barcode": "subshelf1",
1017
+ "location_id": cls.shelf1.id,
1018
+ }
1019
+ )
1020
+ )
1021
+
1022
+ def test_set_destination_lines_partial_qty_next_line(self):
1023
+ """Scanned destination location with partial qty, the next line to process
1024
+ should be the one for the remaining qty.
1025
+ """
1026
+
1027
+ move_line = self.picking.move_line_ids[0]
1028
+ self._simulate_selected_move_line(move_line)
1029
+ # Scan partial qty (6/10)
1030
+ response = self.service.dispatch(
1031
+ "set_destination_line",
1032
+ params={
1033
+ "location_id": self.content_loc.id,
1034
+ "move_line_id": move_line.id,
1035
+ "quantity": move_line.reserved_uom_qty - 4, # Scan 6 qty
1036
+ "barcode": self.dest_location.barcode,
1037
+ },
1038
+ )
1039
+ # the new qty is in a backorder of the backorder where the done qty has bee
1040
+ # processed
1041
+ backorder = self.picking.backorder_ids.backorder_ids
1042
+ self.assertTrue(backorder)
1043
+
1044
+ self.assert_response_start_single(
1045
+ response,
1046
+ backorder,
1047
+ message=self.service.msg_store.location_content_transfer_item_complete(
1048
+ self.dest_location
1049
+ ),
1050
+ )
1051
+ # check that the next operation has the appropriate attributes
1052
+ move_line = backorder.move_line_ids
1053
+ self.assertEqual(move_line.reserved_uom_qty, 4)
1054
+ self.assertEqual(move_line.qty_done, 4)
1055
+ self.assertEqual(move_line.picking_id.user_id, self.env.user)
1056
+ # if we process the quantity of the backorder, the next operation should
1057
+ # be the remaining one of the initial picking
1058
+ self._simulate_selected_move_line(move_line)
1059
+ response = self.service.dispatch(
1060
+ "set_destination_line",
1061
+ params={
1062
+ "location_id": self.content_loc.id,
1063
+ "move_line_id": move_line.id,
1064
+ "quantity": move_line.reserved_uom_qty, # Scan 6 qty
1065
+ "barcode": self.dest_location.barcode,
1066
+ },
1067
+ )
1068
+ self.assert_response_start_single(
1069
+ response,
1070
+ self.picking,
1071
+ message=self.service.msg_store.location_content_transfer_item_complete(
1072
+ self.dest_location
1073
+ ),
1074
+ )