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,1175 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+ from odoo.tests.common import Form
4
+
5
+ from .common import CommonCase
6
+
7
+
8
+ # pylint: disable=missing-return
9
+ class TestActionsChangePackageLot(CommonCase):
10
+ """Tests covering changing a package on a move line"""
11
+
12
+ @classmethod
13
+ def setUpClass(cls):
14
+ super().setUpClass()
15
+ with cls.work_on_actions(cls) as work:
16
+ cls.change_package_lot = work.component(usage="change.package.lot")
17
+
18
+ @classmethod
19
+ def setUpClassVars(cls):
20
+ super().setUpClassVars()
21
+ cls.wh = cls.env.ref("stock.warehouse0")
22
+ cls.picking_type = cls.wh.out_type_id
23
+ cls.picking_type.sudo().show_entire_packs = True
24
+
25
+ def _create_picking_with_package_level(self, packages):
26
+ picking_form = Form(
27
+ self.env["stock.picking"].with_context(force_detailed_view=True)
28
+ )
29
+ picking_form.partner_id = self.customer
30
+ picking_form.origin = "test"
31
+ picking_form.picking_type_id = self.picking_type
32
+ picking_form.location_id = self.stock_location
33
+ for package in packages:
34
+ with picking_form.package_level_ids_details.new() as move:
35
+ move.package_id = package
36
+ picking = picking_form.save()
37
+ picking.action_confirm()
38
+ picking.action_assign()
39
+ return picking
40
+
41
+ def assert_quant_reserved_qty(self, move_line, qty_func, package=None, lot=None):
42
+ domain = [
43
+ ("location_id", "=", move_line.location_id.id),
44
+ ("product_id", "=", move_line.product_id.id),
45
+ ]
46
+ if package:
47
+ domain.append(("package_id", "=", package.id))
48
+ if lot:
49
+ domain.append(("lot_id", "=", lot.id))
50
+ quant = self.env["stock.quant"].search(domain)
51
+ self.assertEqual(quant.reserved_quantity, qty_func())
52
+
53
+ def assert_quant_package_qty(self, location, package, qty_func):
54
+ quant = self.env["stock.quant"].search(
55
+ [("location_id", "=", location.id), ("package_id", "=", package.id)]
56
+ )
57
+ self.assertEqual(quant.quantity, qty_func())
58
+
59
+ @staticmethod
60
+ def unreachable_func(move_line, message=None):
61
+ raise AssertionError("should not reach this function")
62
+
63
+ def test_change_lot_ok(self):
64
+ initial_lot = self._create_lot(self.product_a)
65
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot)
66
+ picking = self._create_picking(lines=[(self.product_a, 10)])
67
+ picking.action_assign()
68
+ line = picking.move_line_ids
69
+ source_location = line.location_id
70
+ new_lot = self._create_lot(self.product_a)
71
+ # ensure we have our new package in the same location
72
+ self._update_qty_in_location(source_location, line.product_id, 10, lot=new_lot)
73
+ self.change_package_lot.change_lot(
74
+ line,
75
+ new_lot,
76
+ # success callback
77
+ lambda move_line, message=None: self.assertEqual(
78
+ message, self.msg_store.lot_replaced_by_lot(initial_lot, new_lot)
79
+ ),
80
+ # failure callback
81
+ self.unreachable_func,
82
+ )
83
+ self.assertRecordValues(line, [{"lot_id": new_lot.id}])
84
+ # check that reservations have been updated
85
+ self.assert_quant_reserved_qty(line, lambda: 0, lot=initial_lot)
86
+ self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot)
87
+
88
+ def test_change_lot_less_quantity_ok(self):
89
+ initial_lot = self._create_lot(self.product_a)
90
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot)
91
+ picking = self._create_picking(lines=[(self.product_a, 10)])
92
+ picking.action_assign()
93
+ line = picking.move_line_ids
94
+ source_location = line.location_id
95
+ new_lot = self._create_lot(self.product_a)
96
+ # ensure we have our new package in the same location
97
+ self._update_qty_in_location(source_location, line.product_id, 8, lot=new_lot)
98
+ expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot)
99
+ expected_message["body"] += " The quantity to do has changed!"
100
+ self.change_package_lot.change_lot(
101
+ line,
102
+ new_lot,
103
+ # success callback
104
+ lambda move_line, message=None: self.assertEqual(message, expected_message),
105
+ # failure callback
106
+ self.unreachable_func,
107
+ )
108
+ self.assertRecordValues(line, [{"lot_id": new_lot.id, "reserved_qty": 8}])
109
+ other_line = line.move_id.move_line_ids - line
110
+ self.assertRecordValues(
111
+ other_line, [{"lot_id": initial_lot.id, "reserved_qty": 2}]
112
+ )
113
+ # check that reservations have been updated
114
+ self.assert_quant_reserved_qty(line, lambda: 2, lot=initial_lot)
115
+ self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot)
116
+
117
+ def test_change_lot_zero_quant_error(self):
118
+ """No quant in the location for the scanned lot
119
+
120
+ As the user scanned it, it's an inventory error.
121
+ """
122
+ initial_lot = self._create_lot(self.product_a)
123
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot)
124
+ picking = self._create_picking(lines=[(self.product_a, 10)])
125
+ picking.action_assign()
126
+ line = picking.move_line_ids
127
+ new_lot = self._create_lot(self.product_a)
128
+ expected_message = self.msg_store.cannot_change_lot_already_picked(new_lot)
129
+ self.change_package_lot.change_lot(
130
+ line,
131
+ new_lot,
132
+ # success callback
133
+ self.unreachable_func,
134
+ # failure callback
135
+ lambda move_line, message=None: self.assertEqual(message, expected_message),
136
+ )
137
+
138
+ self.assertRecordValues(line, [{"lot_id": initial_lot.id, "reserved_qty": 10}])
139
+ # check that reservations have not been updated
140
+ self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=initial_lot)
141
+ self.assert_quant_reserved_qty(line, lambda: 0, lot=new_lot)
142
+
143
+ def test_change_lot_package_explode_ok(self):
144
+ """Scan a lot on units replacing a package"""
145
+ initial_lot = self._create_lot(self.product_a)
146
+ package = self._create_package_in_location(
147
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=initial_lot)]
148
+ )
149
+ picking = self._create_picking(lines=[(self.product_a, 10)])
150
+ picking.action_assign()
151
+ line = picking.move_line_ids
152
+ self.assertEqual(line.lot_id, initial_lot)
153
+ self.assertEqual(line.package_id, package)
154
+
155
+ new_lot = self._create_lot(self.product_a)
156
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=new_lot)
157
+ expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot)
158
+ self.change_package_lot.change_lot(
159
+ line,
160
+ new_lot,
161
+ # success callback
162
+ lambda move_line, message=None: self.assertEqual(message, expected_message),
163
+ # failure callback
164
+ self.unreachable_func,
165
+ )
166
+
167
+ self.assertRecordValues(
168
+ line,
169
+ [
170
+ {
171
+ "lot_id": new_lot.id,
172
+ "reserved_qty": 10,
173
+ "package_id": False,
174
+ "package_level_id": False,
175
+ }
176
+ ],
177
+ )
178
+
179
+ # check that reservations have been updated
180
+ self.assert_quant_reserved_qty(line, lambda: 0, lot=initial_lot)
181
+ self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot)
182
+
183
+ def test_change_lot_reserved_qty_ok(self):
184
+ """Scan a lot already reserved by other lines
185
+
186
+ It should unreserve the other line, use the lot for the current line,
187
+ and re-reserve the other move.
188
+ """
189
+ initial_lot = self._create_lot(self.product_a)
190
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot)
191
+ picking = self._create_picking(lines=[(self.product_a, 10)])
192
+ picking.action_assign()
193
+ line = picking.move_line_ids
194
+ self.assertEqual(line.lot_id, initial_lot)
195
+
196
+ new_lot = self._create_lot(self.product_a)
197
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=new_lot)
198
+ picking2 = self._create_picking(lines=[(self.product_a, 10)])
199
+ picking2.action_assign()
200
+ line2 = picking2.move_line_ids
201
+ self.assertEqual(line2.lot_id, new_lot)
202
+
203
+ expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot)
204
+ self.change_package_lot.change_lot(
205
+ line,
206
+ new_lot,
207
+ # success callback
208
+ lambda move_line, message=None: self.assertEqual(message, expected_message),
209
+ # failure callback
210
+ self.unreachable_func,
211
+ )
212
+
213
+ self.assertRecordValues(line, [{"lot_id": new_lot.id, "reserved_qty": 10}])
214
+ # line has been re-created
215
+ line2 = picking2.move_line_ids
216
+ self.assertRecordValues(line2, [{"lot_id": initial_lot.id, "reserved_qty": 10}])
217
+
218
+ # check that reservations have been updated
219
+ self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot)
220
+ self.assert_quant_reserved_qty(
221
+ line2, lambda: line2.reserved_qty, lot=initial_lot
222
+ )
223
+
224
+ def test_change_lot_reserved_partial_qty_ok(self):
225
+ """Scan a lot already reserved by other lines and can only be reserved
226
+ partially
227
+
228
+ It should unreserve the other line, use the lot for the current line,
229
+ and re-reserve the other move. The quantity for the current line must
230
+ be adapted to the available
231
+ """
232
+ initial_lot = self._create_lot(self.product_a)
233
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot)
234
+ picking = self._create_picking(lines=[(self.product_a, 10)])
235
+ picking.action_assign()
236
+ line = picking.move_line_ids
237
+ self.assertEqual(line.lot_id, initial_lot)
238
+
239
+ new_lot = self._create_lot(self.product_a)
240
+ self._update_qty_in_location(self.shelf1, self.product_a, 8, lot=new_lot)
241
+ picking2 = self._create_picking(lines=[(self.product_a, 8)])
242
+ picking2.action_assign()
243
+ line2 = picking2.move_line_ids
244
+ self.assertEqual(line2.lot_id, new_lot)
245
+
246
+ expected_message = self.msg_store.lot_replaced_by_lot(initial_lot, new_lot)
247
+ expected_message["body"] += " The quantity to do has changed!"
248
+ self.change_package_lot.change_lot(
249
+ line,
250
+ new_lot,
251
+ # success callback
252
+ lambda move_line, message=None: self.assertEqual(message, expected_message),
253
+ # failure callback
254
+ self.unreachable_func,
255
+ )
256
+
257
+ self.assertRecordValues(line, [{"lot_id": new_lot.id, "reserved_qty": 8}])
258
+ other_line = picking.move_line_ids - line
259
+ self.assertRecordValues(
260
+ other_line, [{"lot_id": initial_lot.id, "reserved_qty": 2}]
261
+ )
262
+ # line has been re-created
263
+ line2 = picking2.move_line_ids
264
+ self.assertRecordValues(line2, [{"lot_id": initial_lot.id, "reserved_qty": 8}])
265
+
266
+ # check that reservations have been updated
267
+ self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=new_lot)
268
+ # both line2 and the line for the 2 remaining will re-reserve the initial lot
269
+ self.assert_quant_reserved_qty(
270
+ other_line,
271
+ lambda: line2.reserved_qty + other_line.reserved_qty,
272
+ lot=initial_lot,
273
+ )
274
+
275
+ def test_change_lot_reserved_qty_done_error(self):
276
+ """Scan a lot already reserved by other *picked* lines
277
+
278
+ Cannot "steal" lot from picked lines
279
+ """
280
+ initial_lot = self._create_lot(self.product_a)
281
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot)
282
+ picking = self._create_picking(lines=[(self.product_a, 10)])
283
+ picking.action_assign()
284
+ line = picking.move_line_ids
285
+ self.assertEqual(line.lot_id, initial_lot)
286
+
287
+ new_lot = self._create_lot(self.product_a)
288
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=new_lot)
289
+ picking2 = self._create_picking(lines=[(self.product_a, 10)])
290
+ picking2.action_assign()
291
+ line2 = picking2.move_line_ids
292
+ self.assertEqual(line2.lot_id, new_lot)
293
+ line2.qty_done = 10.0
294
+
295
+ expected_message = self.msg_store.cannot_change_lot_already_picked(new_lot)
296
+ self.change_package_lot.change_lot(
297
+ line,
298
+ new_lot,
299
+ # success callback
300
+ self.unreachable_func,
301
+ # failure callback
302
+ lambda move_line, message=None: self.assertEqual(message, expected_message),
303
+ )
304
+
305
+ # no changes
306
+ self.assertRecordValues(line, [{"lot_id": initial_lot.id, "reserved_qty": 10}])
307
+ self.assertRecordValues(
308
+ line2, [{"lot_id": new_lot.id, "reserved_qty": 10, "qty_done": 10.0}]
309
+ )
310
+ self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=initial_lot)
311
+ self.assert_quant_reserved_qty(line2, lambda: line2.reserved_qty, lot=new_lot)
312
+
313
+ def test_change_lot_different_location_error(self):
314
+ "If the scanned lot is in a different location, we cannot process it"
315
+ self.product_a.tracking = "lot"
316
+ initial_lot = self._create_lot(self.product_a)
317
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot)
318
+ picking = self._create_picking(lines=[(self.product_a, 10)])
319
+ picking.action_assign()
320
+ line = picking.move_line_ids
321
+ new_lot = self._create_lot(self.product_a)
322
+ # ensure we have our new lot in a different location
323
+ self._update_qty_in_location(self.shelf2, line.product_id, 10, lot=new_lot)
324
+ expected_message = self.msg_store.cannot_change_lot_already_picked(new_lot)
325
+ self.change_package_lot.change_lot(
326
+ line,
327
+ new_lot,
328
+ # success callback
329
+ self.unreachable_func,
330
+ # failure callback
331
+ lambda move_line, message=None: self.assertEqual(message, expected_message),
332
+ )
333
+
334
+ self.assertRecordValues(line, [{"lot_id": initial_lot.id}])
335
+ # check that reservations have not been updated
336
+ self.assert_quant_reserved_qty(line, lambda: line.reserved_qty, lot=initial_lot)
337
+ self.assert_quant_reserved_qty(line, lambda: 0, lot=new_lot)
338
+
339
+ def test_change_lot_in_several_packages_error(self):
340
+ self.product_a.tracking = "lot"
341
+ initial_lot = self._create_lot(self.product_a)
342
+ self._create_package_in_location(
343
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=initial_lot)]
344
+ )
345
+ picking = self._create_picking(lines=[(self.product_a, 10)])
346
+ picking.action_assign()
347
+ line = picking.move_line_ids
348
+ # create 2 packages for the same new lot in the same location
349
+ new_lot = self._create_lot(self.product_a)
350
+ self._create_package_in_location(
351
+ self.shelf1, [self.PackageContent(self.product_a, 10, new_lot)]
352
+ )
353
+ self._create_package_in_location(
354
+ self.shelf1, [self.PackageContent(self.product_a, 10, new_lot)]
355
+ )
356
+ self.change_package_lot.change_lot(
357
+ line,
358
+ new_lot,
359
+ # success callback
360
+ self.unreachable_func,
361
+ # failure callback
362
+ lambda move_line, message=None: self.assertEqual(
363
+ message, self.msg_store.several_packs_in_location(self.shelf1)
364
+ ),
365
+ )
366
+
367
+ def test_change_lot_in_package_ok(self):
368
+ self.product_a.tracking = "lot"
369
+ initial_lot = self._create_lot(self.product_a)
370
+ initial_package = self._create_package_in_location(
371
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=initial_lot)]
372
+ )
373
+ # ensure we have our new package in the same location
374
+ new_lot = self._create_lot(self.product_a)
375
+ new_package = self._create_package_in_location(
376
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=new_lot)]
377
+ )
378
+ picking = self._create_picking(lines=[(self.product_a, 10)])
379
+ picking.action_assign()
380
+ line = picking.move_line_ids
381
+ self.change_package_lot.change_lot(
382
+ line,
383
+ new_lot,
384
+ # success callback
385
+ lambda move_line, message=None: self.assertEqual(
386
+ message,
387
+ self.msg_store.package_replaced_by_package(
388
+ initial_package, new_package
389
+ ),
390
+ ),
391
+ # failure callback
392
+ self.unreachable_func,
393
+ )
394
+ self.assertRecordValues(
395
+ line,
396
+ [
397
+ {
398
+ "package_id": new_package.id,
399
+ "result_package_id": new_package.id,
400
+ "lot_id": new_lot.id,
401
+ "reserved_qty": 10.0,
402
+ }
403
+ ],
404
+ )
405
+ self.assertRecordValues(line.package_level_id, [{"package_id": new_package.id}])
406
+ # check that reservations have been updated
407
+ self.assert_quant_reserved_qty(line, lambda: 0, package=initial_package)
408
+ self.assert_quant_reserved_qty(
409
+ line, lambda: line.reserved_qty, package=new_package
410
+ )
411
+
412
+ def test_change_lot_in_package_no_initial_package_ok(self):
413
+ self.product_a.tracking = "lot"
414
+ initial_lot = self._create_lot(self.product_a)
415
+ self._update_qty_in_location(self.shelf1, self.product_a, 10, lot=initial_lot)
416
+ # ensure we have our new package in the same location
417
+ new_lot = self._create_lot(self.product_a)
418
+ new_package = self._create_package_in_location(
419
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=new_lot)]
420
+ )
421
+ picking = self._create_picking(lines=[(self.product_a, 10)])
422
+ picking.action_assign()
423
+ line = picking.move_line_ids
424
+ self.change_package_lot.change_lot(
425
+ line,
426
+ new_lot,
427
+ # success callback
428
+ lambda move_line, message=None: self.assertEqual(
429
+ message, self.msg_store.units_replaced_by_package(new_package)
430
+ ),
431
+ # failure callback
432
+ self.unreachable_func,
433
+ )
434
+ self.assertRecordValues(
435
+ line,
436
+ [
437
+ {
438
+ "package_id": new_package.id,
439
+ "result_package_id": new_package.id,
440
+ "lot_id": new_lot.id,
441
+ "reserved_qty": 10.0,
442
+ }
443
+ ],
444
+ )
445
+ self.assertRecordValues(line.package_level_id, [{"package_id": new_package.id}])
446
+ # check that reservations have been updated
447
+ self.assert_quant_reserved_qty(line, lambda: 0, lot=initial_lot)
448
+ self.assert_quant_reserved_qty(
449
+ line, lambda: line.reserved_qty, package=new_package
450
+ )
451
+
452
+ def test_change_pack_different_content_error(self):
453
+ # create the initial package, that will be reserved first
454
+ initial_package = self._create_package_in_location(
455
+ self.shelf1,
456
+ [
457
+ self.PackageContent(self.product_a, 10, lot=None),
458
+ self.PackageContent(self.product_b, 10, lot=None),
459
+ ],
460
+ )
461
+ picking = self._create_picking_with_package_level(initial_package)
462
+ # create a new package in the same location
463
+ # with a different content
464
+ new_package = self._create_package_in_location(
465
+ self.shelf1, [self.PackageContent(self.product_b, 8, lot=None)]
466
+ )
467
+
468
+ lines = picking.move_line_ids
469
+ # try to use the new package, which doesn't contain our product,
470
+ # cannot be changed
471
+ self.change_package_lot.change_package(
472
+ lines[0],
473
+ new_package,
474
+ # success callback
475
+ self.unreachable_func,
476
+ # failure callback
477
+ lambda move_line, message=None: self.assertEqual(
478
+ message, self.msg_store.package_different_content(new_package)
479
+ ),
480
+ )
481
+
482
+ def test_change_pack_multi_content_with_lot(self):
483
+ """Switch package for a line which was part of a multi-products package
484
+
485
+ We have a move line which is part of a package with more than one
486
+ product and the other product is moved by another move line.
487
+
488
+ We want to pick the goods for product A in a different package. What
489
+ should happen is:
490
+
491
+ * the package level is exploded, as we will no longer move the entire
492
+ package
493
+ * the move line for product A should now use the new package, and be
494
+ updated with the lot of the package
495
+ * the move line for the other product should keep the other package, if
496
+ the user want to change the package for the other product too, they
497
+ can do it when they pick it
498
+ """
499
+ (self.product_a + self.product_b).tracking = "lot"
500
+ # create a package with 2 products tracked by lot, stored in shelf1
501
+ # this package is reserved first on the move line
502
+ initial_lot_a = self._create_lot(self.product_a)
503
+ initial_lot_b = self._create_lot(self.product_b)
504
+ initial_package = self._create_package_in_location(
505
+ self.shelf1,
506
+ [
507
+ self.PackageContent(self.product_a, 10, initial_lot_a),
508
+ self.PackageContent(self.product_b, 10, initial_lot_b),
509
+ ],
510
+ )
511
+
512
+ # create and reserve our transfer using the initial package
513
+ picking = self._create_picking_with_package_level(initial_package)
514
+
515
+ lines = picking.move_line_ids
516
+
517
+ # create a second package with the same content, which will be used
518
+ # as replacement
519
+ new_lot_a = self._create_lot(self.product_a)
520
+ new_lot_b = self._create_lot(self.product_b)
521
+ new_package = self._create_package_in_location(
522
+ self.shelf1,
523
+ [
524
+ self.PackageContent(self.product_a, 10, new_lot_a),
525
+ self.PackageContent(self.product_b, 10, new_lot_b),
526
+ ],
527
+ )
528
+ line1, line2 = lines
529
+ self.change_package_lot.change_package(
530
+ line1,
531
+ new_package,
532
+ # success callback
533
+ lambda move_line, message=None: self.assertEqual(
534
+ message,
535
+ self.msg_store.package_replaced_by_package(
536
+ initial_package, new_package
537
+ ),
538
+ ),
539
+ # failure callback
540
+ self.unreachable_func,
541
+ )
542
+ self.assertRecordValues(
543
+ line1,
544
+ [
545
+ {
546
+ "package_id": new_package.id,
547
+ # we are no longer moving an entire package
548
+ "result_package_id": False,
549
+ "lot_id": new_lot_a.id,
550
+ "reserved_qty": 10.0,
551
+ }
552
+ ],
553
+ )
554
+ self.assertRecordValues(
555
+ line2,
556
+ [
557
+ {
558
+ "package_id": initial_package.id,
559
+ # we are no longer moving an entire package
560
+ "result_package_id": False,
561
+ "lot_id": initial_lot_b.id,
562
+ "reserved_qty": 10.0,
563
+ }
564
+ ],
565
+ )
566
+ # check that reservations have been updated
567
+ self.assert_quant_reserved_qty(line1, lambda: 0, package=initial_package)
568
+ self.assert_quant_reserved_qty(
569
+ line2, lambda: line2.reserved_qty, package=initial_package
570
+ )
571
+ self.assert_quant_reserved_qty(
572
+ line1, lambda: line1.reserved_qty, package=new_package
573
+ )
574
+ self.assert_quant_reserved_qty(line2, lambda: 0, package=new_package)
575
+
576
+ def test_change_pack_different_location(self):
577
+ initial_package = self._create_package_in_location(
578
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=None)]
579
+ )
580
+ # put a package in shelf2 in the system, but we assume that in real,
581
+ # the operator put it in shelf1
582
+ new_package = self._create_package_in_location(
583
+ self.shelf2, [self.PackageContent(self.product_a, 10, lot=None)]
584
+ )
585
+
586
+ picking = self._create_picking(lines=[(self.product_a, 10)])
587
+ picking.action_assign()
588
+ line = picking.move_line_ids
589
+ self.assertEqual(line.package_id, initial_package)
590
+ # when the operator wants to pick the initial package, in shelf1, the new
591
+ # package is in front of the other so they want to change the package
592
+ self.change_package_lot.change_package(
593
+ line,
594
+ new_package,
595
+ # success callback
596
+ lambda move_line, message=None: self.assertEqual(
597
+ message,
598
+ self.msg_store.package_replaced_by_package(
599
+ initial_package, new_package
600
+ ),
601
+ ),
602
+ # failure callback
603
+ self.unreachable_func,
604
+ )
605
+
606
+ self.assertRecordValues(
607
+ line, [{"package_id": new_package.id, "result_package_id": new_package.id}]
608
+ )
609
+ self.assertRecordValues(line.package_level_id, [{"package_id": new_package.id}])
610
+ # check that reservations have been updated, the new package is not
611
+ # supposed to be in shelf2 anymore, and we should have no reserved qty
612
+ # for the initial package anymore
613
+ self.assert_quant_package_qty(self.shelf2, new_package, lambda: 0)
614
+ self.assert_quant_reserved_qty(line, lambda: 0, package=initial_package)
615
+ self.assert_quant_reserved_qty(
616
+ line, lambda: line.reserved_qty, package=new_package
617
+ )
618
+
619
+ def test_change_pack_different_location_reserved_package(self):
620
+ initial_package = self._create_package_in_location(
621
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=None)]
622
+ )
623
+
624
+ picking = self._create_picking(lines=[(self.product_a, 10)])
625
+ picking.action_assign()
626
+ line = picking.move_line_ids
627
+ self.assertEqual(line.package_id, initial_package)
628
+
629
+ # put a package in shelf2 in the system, but we assume that in real,
630
+ # the operator put it in shelf1
631
+ new_package = self._create_package_in_location(
632
+ self.shelf2, [self.PackageContent(self.product_a, 10, lot=None)]
633
+ )
634
+ picking2 = self._create_picking(lines=[(self.product_a, 10)])
635
+ picking2.action_assign()
636
+ line2 = picking2.move_line_ids
637
+ self.assertEqual(line2.package_id, new_package)
638
+
639
+ # When the operator wants to pick the initial package, in shelf1, the new
640
+ # package is in front of the other so they want to change the package.
641
+ # The new package was supposed to be in shelf2 but is in fact in
642
+ # shelf1.
643
+ # An inventory must move it in shelf1 before we change the package on the line.
644
+ # Line2 must be unreserved and reserved again.
645
+ self.change_package_lot.change_package(
646
+ line,
647
+ new_package,
648
+ # success callback
649
+ lambda move_line, message=None: self.assertEqual(
650
+ message,
651
+ self.msg_store.package_replaced_by_package(
652
+ initial_package, new_package
653
+ ),
654
+ ),
655
+ # failure callback
656
+ self.unreachable_func,
657
+ )
658
+
659
+ # line2 has been re-created
660
+ line2 = picking2.move_line_ids
661
+ self.assertRecordValues(
662
+ line + line2,
663
+ [
664
+ {
665
+ "package_id": new_package.id,
666
+ "result_package_id": new_package.id,
667
+ "location_id": self.shelf1.id,
668
+ "reserved_qty": 10.0,
669
+ },
670
+ {
671
+ "package_id": initial_package.id,
672
+ "result_package_id": initial_package.id,
673
+ "location_id": self.shelf1.id,
674
+ "reserved_qty": 10.0,
675
+ },
676
+ ],
677
+ )
678
+ self.assertRecordValues(line.package_level_id, [{"package_id": new_package.id}])
679
+ self.assertRecordValues(
680
+ line2.package_level_id, [{"package_id": initial_package.id}]
681
+ )
682
+ # check that reservations have been updated, the new package is not
683
+ # supposed to be in shelf2 anymore, and we should have no reserved qty
684
+ # for the initial package anymore
685
+ self.assert_quant_package_qty(self.shelf2, new_package, lambda: 0)
686
+ self.assert_quant_reserved_qty(
687
+ line, lambda: line.reserved_qty, package=new_package
688
+ )
689
+ self.assert_quant_reserved_qty(
690
+ line2, lambda: line2.reserved_qty, package=initial_package
691
+ )
692
+
693
+ def test_change_pack_different_location_reserved_package_qty_done(self):
694
+ initial_package = self._create_package_in_location(
695
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=None)]
696
+ )
697
+
698
+ picking = self._create_picking(lines=[(self.product_a, 10)])
699
+ picking.action_assign()
700
+ line = picking.move_line_ids
701
+ self.assertEqual(line.package_id, initial_package)
702
+
703
+ # put a package in shelf2 in the system, but we assume that in real,
704
+ # the operator put it in shelf1
705
+ new_package = self._create_package_in_location(
706
+ self.shelf2, [self.PackageContent(self.product_a, 10, lot=None)]
707
+ )
708
+ picking2 = self._create_picking(lines=[(self.product_a, 10)])
709
+ picking2.action_assign()
710
+ line2 = picking2.move_line_ids
711
+ self.assertEqual(line2.package_id, new_package)
712
+ line2.qty_done = 10.0
713
+
714
+ # The new package was supposed to be in shelf2 but is in fact in shelf1.
715
+ # The package has already been picked in shelf2 (unlikely to happen...
716
+ # still we have to handle it). Forbid to pick.
717
+ expected_message = self.msg_store.package_change_error(
718
+ new_package,
719
+ "Package {} has been partially picked in another location".format(
720
+ new_package.display_name
721
+ ),
722
+ )
723
+ self.change_package_lot.change_package(
724
+ line,
725
+ new_package,
726
+ # success callback
727
+ self.unreachable_func,
728
+ # failure callback
729
+ lambda move_line, message=None: self.assertEqual(message, expected_message),
730
+ )
731
+
732
+ # line2 has been re-created
733
+ line2 = picking2.move_line_ids
734
+ self.assertRecordValues(
735
+ line + line2,
736
+ [
737
+ {
738
+ "package_id": initial_package.id,
739
+ "result_package_id": initial_package.id,
740
+ "location_id": self.shelf1.id,
741
+ "reserved_qty": 10.0,
742
+ },
743
+ {
744
+ "package_id": new_package.id,
745
+ "result_package_id": new_package.id,
746
+ "location_id": self.shelf2.id,
747
+ "reserved_qty": 10.0,
748
+ },
749
+ ],
750
+ )
751
+ # no change
752
+ self.assertRecordValues(
753
+ line.package_level_id, [{"package_id": initial_package.id}]
754
+ )
755
+ self.assertRecordValues(
756
+ line2.package_level_id, [{"package_id": new_package.id}]
757
+ )
758
+ self.assert_quant_package_qty(self.shelf2, new_package, lambda: 10.0)
759
+ self.assert_quant_reserved_qty(
760
+ line, lambda: line.reserved_qty, package=initial_package
761
+ )
762
+ self.assert_quant_reserved_qty(
763
+ line2, lambda: line2.reserved_qty, package=new_package
764
+ )
765
+
766
+ def test_change_pack_lot_change_pack_less_qty_ok(self):
767
+ initial_package = self._create_package_in_location(
768
+ self.shelf1, [self.PackageContent(self.product_a, 100, lot=None)]
769
+ )
770
+
771
+ picking = self._create_picking(lines=[(self.product_a, 10)])
772
+ picking.action_assign()
773
+ line = picking.move_line_ids
774
+
775
+ self.assertRecordValues(
776
+ line,
777
+ [
778
+ {
779
+ "package_id": initial_package.id,
780
+ # since we don't move the entire package (10 out of 100), no
781
+ # result package
782
+ "result_package_id": False,
783
+ "reserved_qty": 10.0,
784
+ }
785
+ ],
786
+ )
787
+ self.assertFalse(line.package_level_id)
788
+
789
+ # ensure we have our new package in the same location
790
+ new_package = self._create_package_in_location(
791
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=None)]
792
+ )
793
+ self.change_package_lot.change_package(
794
+ line,
795
+ new_package,
796
+ # success callback
797
+ lambda move_line, message=None: self.assertEqual(
798
+ message,
799
+ self.msg_store.package_replaced_by_package(
800
+ initial_package, new_package
801
+ ),
802
+ ),
803
+ # failure callback
804
+ self.unreachable_func,
805
+ )
806
+ self.assertRecordValues(
807
+ line,
808
+ [
809
+ {
810
+ "package_id": new_package.id,
811
+ "result_package_id": new_package.id,
812
+ "reserved_qty": 10.0,
813
+ }
814
+ ],
815
+ )
816
+ self.assertRecordValues(line.package_level_id, [{"package_id": new_package.id}])
817
+
818
+ # check that reservations have been updated
819
+ self.assert_quant_reserved_qty(line, lambda: 0, package=initial_package)
820
+ self.assert_quant_reserved_qty(
821
+ line, lambda: line.reserved_qty, package=new_package
822
+ )
823
+
824
+ def test_change_pack_steal_from_other_move_line(self):
825
+ """Exchange pack with another line
826
+
827
+ When we scan the package used on another line not picked yet (qty_done
828
+ == 0), we unreserve the other line and use its package. The other line
829
+ is reserved again and should reserve the package used initially on our
830
+ move line.
831
+ """
832
+ # create 2 picking, each with its own package
833
+ package1 = self._create_package_in_location(
834
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=None)]
835
+ )
836
+ picking1 = self._create_picking_with_package_level(package1)
837
+ self.assertEqual(picking1.move_line_ids.package_id, package1)
838
+
839
+ package2 = self._create_package_in_location(
840
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=None)]
841
+ )
842
+ picking2 = self._create_picking_with_package_level(package2)
843
+ self.assertEqual(picking2.move_line_ids.package_id, package2)
844
+
845
+ line = picking1.move_line_ids
846
+
847
+ # We "steal" package2 for the picking1
848
+ self.change_package_lot.change_package(
849
+ line,
850
+ package2,
851
+ # success callback
852
+ lambda move_line, message=None: self.assertEqual(
853
+ message, self.msg_store.package_replaced_by_package(package1, package2)
854
+ ),
855
+ # failure callback
856
+ self.unreachable_func,
857
+ )
858
+
859
+ self.assertRecordValues(
860
+ picking1.move_line_ids,
861
+ [
862
+ {
863
+ "package_id": package2.id,
864
+ "result_package_id": package2.id,
865
+ "state": "assigned",
866
+ "reserved_qty": 10.0,
867
+ }
868
+ ],
869
+ )
870
+ self.assertRecordValues(
871
+ picking2.move_line_ids,
872
+ [
873
+ {
874
+ "package_id": package1.id,
875
+ "result_package_id": package1.id,
876
+ "state": "assigned",
877
+ "reserved_qty": 10.0,
878
+ }
879
+ ],
880
+ )
881
+ self.assertRecordValues(
882
+ picking1.package_level_ids,
883
+ [{"package_id": package2.id, "state": "assigned"}],
884
+ )
885
+ self.assertRecordValues(
886
+ picking2.package_level_ids,
887
+ [{"package_id": package1.id, "state": "assigned"}],
888
+ )
889
+ # check that reservations have been updated
890
+ self.assert_quant_reserved_qty(
891
+ picking1.move_line_ids,
892
+ lambda: picking1.move_line_ids.reserved_qty,
893
+ package=package2,
894
+ )
895
+ self.assert_quant_reserved_qty(
896
+ picking2.move_line_ids,
897
+ lambda: picking2.move_line_ids.reserved_qty,
898
+ package=package1,
899
+ )
900
+
901
+ def test_other_line_with_qty_done(self):
902
+ """Try to exchange pack with other line with qty_done
903
+
904
+ When we scan the package used on another line which has been picked
905
+ (qty_done > 0), do not unreserve the other line.
906
+ """
907
+ # create 2 picking, each with its own package
908
+ package1 = self._create_package_in_location(
909
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=None)]
910
+ )
911
+ picking1 = self._create_picking_with_package_level(package1)
912
+ self.assertEqual(picking1.move_line_ids.package_id, package1)
913
+
914
+ package2 = self._create_package_in_location(
915
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=None)]
916
+ )
917
+ picking2 = self._create_picking_with_package_level(package2)
918
+ self.assertEqual(picking2.move_line_ids.package_id, package2)
919
+
920
+ line1 = picking1.move_line_ids
921
+ line2 = picking2.move_line_ids
922
+ line2.qty_done = 10
923
+
924
+ self.change_package_lot.change_package(
925
+ line1,
926
+ package2,
927
+ # success callback
928
+ self.unreachable_func,
929
+ # failure callback
930
+ lambda move_line, message=None: self.assertEqual(
931
+ message,
932
+ self.msg_store.package_change_error(
933
+ package2,
934
+ "Package {} does not contain available product {},"
935
+ " cannot replace package.".format(
936
+ package2.display_name, line1.product_id.display_name
937
+ ),
938
+ ),
939
+ ),
940
+ )
941
+
942
+ # did not change
943
+ self.assertRecordValues(
944
+ picking1.move_line_ids,
945
+ [
946
+ {
947
+ "package_id": package1.id,
948
+ "result_package_id": package1.id,
949
+ "state": "assigned",
950
+ }
951
+ ],
952
+ )
953
+ self.assertRecordValues(
954
+ picking2.move_line_ids,
955
+ [
956
+ {
957
+ "package_id": package2.id,
958
+ "result_package_id": package2.id,
959
+ "state": "assigned",
960
+ }
961
+ ],
962
+ )
963
+ self.assertRecordValues(
964
+ picking1.package_level_ids,
965
+ [{"package_id": package1.id, "state": "assigned"}],
966
+ )
967
+ self.assertRecordValues(
968
+ picking2.package_level_ids,
969
+ [{"package_id": package2.id, "state": "assigned"}],
970
+ )
971
+ # check that reservations have been updated
972
+ self.assert_quant_reserved_qty(
973
+ picking1.move_line_ids,
974
+ lambda: picking1.move_line_ids.reserved_qty,
975
+ package=package1,
976
+ )
977
+ self.assert_quant_reserved_qty(
978
+ picking2.move_line_ids,
979
+ lambda: picking2.move_line_ids.reserved_qty,
980
+ package=package2,
981
+ )
982
+
983
+ def test_package_partial(self):
984
+ """Try to exchange pack with a package partially picked
985
+
986
+ When we scan the package used on another line which has been picked
987
+ (qty_done > 0), but the new package still has unreserved quantity:
988
+
989
+ * the current line is updated for the remaining unreserved quantity
990
+ * a new line is created for the remaining
991
+ * the other already picked line is untouched
992
+ """
993
+ # create 2 picking, each with its own package
994
+ package1 = self._create_package_in_location(
995
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=None)]
996
+ )
997
+ picking1 = self._create_picking_with_package_level(package1)
998
+ line1 = picking1.move_line_ids
999
+ self.assertEqual(line1.package_id, package1)
1000
+
1001
+ package2 = self._create_package_in_location(
1002
+ self.shelf1, [self.PackageContent(self.product_a, 10, lot=None)]
1003
+ )
1004
+
1005
+ # take partially in package2 (no package level as moving partial
1006
+ # package)
1007
+ picking2 = self._create_picking(lines=[(self.product_a, 8)])
1008
+ picking2.action_assign()
1009
+ line2 = picking2.move_line_ids
1010
+ self.assertEqual(line2.package_id, package2)
1011
+
1012
+ # this line is picked, should not be changed, but we still have
1013
+ # 2 units in package2
1014
+ line2.qty_done = line2.reserved_qty
1015
+
1016
+ self.change_package_lot.change_package(
1017
+ line1,
1018
+ package2,
1019
+ # success callback
1020
+ lambda move_line, message=None: self.assertEqual(
1021
+ message, self.msg_store.package_replaced_by_package(package1, package2)
1022
+ ),
1023
+ # failure callback
1024
+ self.unreachable_func,
1025
+ )
1026
+
1027
+ self.assertRecordValues(
1028
+ line1,
1029
+ [
1030
+ {
1031
+ "package_id": package2.id,
1032
+ # not moved entirely by this transfer
1033
+ "result_package_id": False,
1034
+ "state": "assigned",
1035
+ # as the remaining was 2 units, the line is
1036
+ # changed to take only 2
1037
+ "reserved_qty": 2.0,
1038
+ }
1039
+ ],
1040
+ )
1041
+ self.assertRecordValues(
1042
+ # this line should be unchanged
1043
+ line2,
1044
+ [
1045
+ {
1046
+ "package_id": package2.id,
1047
+ # not moved entirely by this transfer
1048
+ "result_package_id": False,
1049
+ "state": "assigned",
1050
+ "reserved_qty": 8.0,
1051
+ }
1052
+ ],
1053
+ )
1054
+
1055
+ # A new line has been created for the quantity the line1
1056
+ # couldn't take in package2. It will take the first goods
1057
+ # available, which happen to be package1 (which was unreserved
1058
+ # when we changed the package of line1).
1059
+ remaining_line = picking1.move_line_ids - line1
1060
+ self.assertRecordValues(
1061
+ remaining_line,
1062
+ [
1063
+ {
1064
+ "package_id": package1.id,
1065
+ # not moved entirely by this transfer
1066
+ "result_package_id": False,
1067
+ "state": "assigned",
1068
+ # remaining qty for the 1st move
1069
+ "reserved_qty": 8.0,
1070
+ }
1071
+ ],
1072
+ )
1073
+
1074
+ # the package1 must have only 8 reserved, for the remaining
1075
+ # of the line
1076
+ self.assertEqual(package1.quant_ids.reserved_quantity, 8)
1077
+ self.assertEqual(package2.quant_ids.reserved_quantity, 10)
1078
+
1079
+ # no package is moved entirely at once
1080
+ self.assertFalse(picking1.package_level_ids)
1081
+ self.assertFalse(picking2.package_level_ids)
1082
+
1083
+ def test_package_2_lines_1_move(self):
1084
+ """Keep picked move line if we have 2 lines on a move
1085
+
1086
+ Create a situation where we have 2 move lines on a move, with different
1087
+ packages, 1 one of them is already picked (qty_done > 0), we change the
1088
+ package on the second one: the first one must not be changed.
1089
+ """
1090
+ package1 = self._create_package_in_location(
1091
+ self.shelf1, [self.PackageContent(self.product_a, 4, lot=None)]
1092
+ )
1093
+ package2 = self._create_package_in_location(
1094
+ self.shelf1, [self.PackageContent(self.product_a, 8, lot=None)]
1095
+ )
1096
+
1097
+ # take partially in package2 (no package level as moving partial
1098
+ # package)
1099
+ picking = self._create_picking(lines=[(self.product_a, 10)])
1100
+ picking.action_assign()
1101
+ move = picking.move_ids
1102
+ line1, line2 = move.move_line_ids
1103
+ self.assertEqual(line1.package_id, package1)
1104
+ self.assertEqual(line2.package_id, package2)
1105
+
1106
+ # package to switch to
1107
+ package3 = self._create_package_in_location(
1108
+ self.shelf1, [self.PackageContent(self.product_a, 8, lot=None)]
1109
+ )
1110
+
1111
+ # this line is picked and must not be changed
1112
+ line1.qty_done = line1.reserved_qty
1113
+
1114
+ # as we change for package2, the line should get only the remaining
1115
+ # part of the package
1116
+
1117
+ self.change_package_lot.change_package(
1118
+ line2,
1119
+ package3,
1120
+ # success callback
1121
+ lambda move_line, message=None: self.assertEqual(
1122
+ message, self.msg_store.package_replaced_by_package(package2, package3)
1123
+ ),
1124
+ # failure callback
1125
+ self.unreachable_func,
1126
+ )
1127
+
1128
+ self.assertRecordValues(
1129
+ line1 | line2,
1130
+ [
1131
+ {
1132
+ "package_id": package1.id,
1133
+ "state": "assigned",
1134
+ "reserved_qty": 4.0,
1135
+ "qty_done": 4.0,
1136
+ },
1137
+ {
1138
+ "package_id": package3.id,
1139
+ "state": "assigned",
1140
+ "reserved_qty": 6.0,
1141
+ "qty_done": 0.0,
1142
+ },
1143
+ ],
1144
+ )
1145
+
1146
+ # package1 is moved entirely
1147
+ self.assertTrue(line1.package_level_id)
1148
+ # package2 is not moved entirely
1149
+ self.assertFalse(line2.package_level_id)
1150
+
1151
+ # the package1 must have only 8 reserved, for the remaining
1152
+ # of the line
1153
+ self.assertEqual(package1.quant_ids.reserved_quantity, 4)
1154
+ self.assertEqual(package2.quant_ids.reserved_quantity, 0)
1155
+ self.assertEqual(package3.quant_ids.reserved_quantity, 6)
1156
+
1157
+ def test_change_pack_same(self):
1158
+ initial_package = self._create_package_in_location(
1159
+ self.shelf1, [self.PackageContent(self.product_a, 100, lot=None)]
1160
+ )
1161
+ picking = self._create_picking(lines=[(self.product_a, 10)])
1162
+ picking.action_assign()
1163
+ line = picking.move_line_ids
1164
+ self.assertEqual(line.package_id, initial_package)
1165
+ self.change_package_lot.change_package(
1166
+ line,
1167
+ initial_package,
1168
+ # success callback
1169
+ self.unreachable_func,
1170
+ # failure callback
1171
+ lambda move_line, message=None: self.assertEqual(
1172
+ message,
1173
+ self.msg_store.package_change_error_same_package(initial_package),
1174
+ ),
1175
+ )