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,119 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # Copyright 2022 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
3
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
4
+ from odoo import _, models
5
+ from odoo.tools.float_utils import float_compare
6
+
7
+
8
+ class StockMove(models.Model):
9
+ _inherit = "stock.move"
10
+
11
+ def _qty_is_satisfied(self):
12
+ compare = float_compare(
13
+ self.quantity_done,
14
+ self.product_uom_qty,
15
+ precision_rounding=self.product_uom.rounding,
16
+ )
17
+ # greater or equal
18
+ return compare in (0, 1)
19
+
20
+ def split_other_move_lines(self, move_lines, intersection=False):
21
+ """Substract `move_lines` from `move.move_line_ids`, put the result
22
+ in a new move and returns it.
23
+
24
+ If `intersection` is set to `True`, this is the common lines between
25
+ `move_lines` and `move.move_line_ids` which will be put in a new move.
26
+ """
27
+ self.ensure_one()
28
+ other_move_lines = self.move_line_ids - move_lines
29
+ if intersection:
30
+ to_move = self.move_line_ids & move_lines
31
+ else:
32
+ to_move = other_move_lines
33
+ if other_move_lines or self.state == "partially_available":
34
+ if intersection:
35
+ qty_to_split = sum(to_move.mapped("reserved_uom_qty"))
36
+ else:
37
+ qty_to_split = self.product_uom_qty - sum(
38
+ move_lines.mapped("reserved_uom_qty")
39
+ )
40
+ split_move_vals = self._split(qty_to_split)
41
+ split_move = self.create(split_move_vals)
42
+ split_move.move_line_ids = to_move
43
+ split_move._action_confirm(merge=False)
44
+ split_move._recompute_state()
45
+ split_move._action_assign()
46
+ self._recompute_state()
47
+ return split_move
48
+ return self.browse()
49
+
50
+ def split_unavailable_qty(self):
51
+ """Put unavailable qty of a partially available move in their own
52
+ move (which will be 'confirmed').
53
+ """
54
+ partial_moves = self.filtered(lambda m: m.state == "partially_available")
55
+ for partial_move in partial_moves:
56
+ partial_move.split_other_move_lines(partial_move.move_line_ids)
57
+ return partial_moves
58
+
59
+ def _extract_in_split_order(self, default=None, backorder=False):
60
+ """Extract moves in a new picking
61
+
62
+ :param default: dictionary of field values to override in the original
63
+ values of the copied record
64
+ :param backorder: indicate if the original picking can be seen as a
65
+ backorder after the split. You could apply a specific backorder
66
+ strategy (e.g. cancel it).
67
+ :return: the new order
68
+ """
69
+ picking = self.picking_id
70
+ picking.ensure_one()
71
+ data = {
72
+ "name": "/",
73
+ "move_ids": [],
74
+ "move_line_ids": [],
75
+ "backorder_id": picking.id,
76
+ }
77
+ data.update(dict(default or []))
78
+ new_picking = picking.copy(data)
79
+ link = '<a href="#" data-oe-model="stock.picking" data-oe-id="%d">%s</a>' % (
80
+ new_picking.id,
81
+ new_picking.name,
82
+ )
83
+ message = (_("The split order {} has been created.")).format(link)
84
+ picking.message_post(body=message)
85
+ self.picking_id = new_picking.id
86
+ self.package_level_id.picking_id = new_picking.id
87
+ self.move_line_ids.picking_id = new_picking.id
88
+ self.move_line_ids.package_level_id.picking_id = new_picking.id
89
+ self._action_assign()
90
+ return new_picking
91
+
92
+ def extract_and_action_done(self):
93
+ """Extract the moves in a separate transfer and validate them.
94
+
95
+ You can combine this method with `split_other_move_lines` method
96
+ to first extract some move lines in a separate move, then validate it
97
+ with this method.
98
+ """
99
+ # Process assigned moves
100
+ moves = self.filtered(lambda m: m.state == "assigned")
101
+ if not moves:
102
+ return False
103
+ new_backorders = self.env["stock.picking"]
104
+ for picking in moves.picking_id:
105
+ existing_backorders = picking.backorder_ids
106
+ moves_todo = picking.move_ids & moves
107
+ # No need to create a new transfer if we are processing all moves
108
+ if moves_todo == picking.move_ids:
109
+ new_picking = picking
110
+ # We process some available moves of the picking, but there are still
111
+ # some other moves to process, then we put the moves to process in
112
+ # a new transfer to validate. All remaining moves stay in the
113
+ # current transfer.
114
+ else:
115
+ new_picking = moves_todo._extract_in_split_order()
116
+ assert new_picking.state == "assigned"
117
+ new_picking._action_done()
118
+ new_backorders |= new_picking.backorder_ids - existing_backorders
119
+ return new_backorders
@@ -0,0 +1,307 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # Copyright 2022 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
3
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
4
+ import logging
5
+
6
+ from odoo import _, exceptions, fields, models
7
+ from odoo.exceptions import UserError
8
+ from odoo.tools.float_utils import float_compare, float_is_zero
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+
13
+ class StockMoveLine(models.Model):
14
+ _name = "stock.move.line"
15
+ _inherit = ["stock.move.line", "shopfloor.priority.postpone.mixin"]
16
+
17
+ # TODO use a serialized field
18
+ shopfloor_unloaded = fields.Boolean(default=False)
19
+ shopfloor_checkout_done = fields.Boolean(default=False)
20
+ shopfloor_user_id = fields.Many2one(comodel_name="res.users", index=True)
21
+
22
+ date_planned = fields.Datetime(related="move_id.date", store=True, index=True)
23
+
24
+ # we search lines based on their location in some workflows
25
+ location_id = fields.Many2one(index=True)
26
+ package_id = fields.Many2one(index=True)
27
+
28
+ # allow domain on picking_id.xxx without too much perf penalty
29
+ picking_id = fields.Many2one(auto_join=True)
30
+
31
+ def _split_partial_quantity(self):
32
+ """Create new move line for the quantity remaining to do
33
+
34
+ :return: the new move line if created else empty recordset
35
+ """
36
+ self.ensure_one()
37
+ rounding = self.product_uom_id.rounding
38
+ if float_is_zero(self.qty_done, precision_rounding=rounding):
39
+ return self.browse()
40
+ compare = float_compare(
41
+ self.qty_done, self.reserved_uom_qty, precision_rounding=rounding
42
+ )
43
+ qty_lesser = compare == -1
44
+ qty_greater = compare == 1
45
+ assert not qty_greater, "Quantity done cannot exceed quantity to do"
46
+ if qty_lesser:
47
+ remaining = self.reserved_uom_qty - self.qty_done
48
+ new_line = self.copy({"reserved_uom_qty": remaining, "qty_done": 0})
49
+ # if we didn't bypass reservation update, the quant reservation
50
+ # would be reduced as much as the deduced quantity, which is wrong
51
+ # as we only moved the quantity to a new move line
52
+ self.with_context(
53
+ bypass_reservation_update=True
54
+ ).reserved_uom_qty = self.qty_done
55
+ return new_line
56
+ return self.browse()
57
+
58
+ def _extract_in_split_order(self, default=None):
59
+ """Have pickings fully reserved with only those move lines.
60
+
61
+ If the condition is not met, extract the move lines in a new picking.
62
+ :param default: dictionary of field values to override in the original
63
+ values of the copied record
64
+ """
65
+ for picking in self.picking_id:
66
+ moves_to_extract = new_move = picking.move_ids.browse()
67
+ need_backorder = need_split = False
68
+ for move in picking.move_ids:
69
+ if move.state in ("cancel", "done"):
70
+ continue
71
+ if move.state == "confirmed":
72
+ # The move has no ancestor and is not available
73
+ need_backorder = True
74
+ continue
75
+ move_lines = move.move_line_ids & self
76
+ if not move_lines:
77
+ # The picking contains moves not related to given move lines
78
+ need_split = True
79
+ continue
80
+ new_move = move.split_other_move_lines(move_lines, intersection=True)
81
+ if new_move:
82
+ if move.state == "confirmed":
83
+ # The move has no ancestor and is not available
84
+ need_backorder = True
85
+ else:
86
+ # The move contains other move lines
87
+ need_split = True
88
+ moves_to_extract += new_move or move
89
+ if need_split:
90
+ moves_to_extract._extract_in_split_order(default=default)
91
+ elif need_backorder:
92
+ # All the lines are processed but some moves are partially available
93
+ moves_to_extract._extract_in_split_order(
94
+ default=default, backorder=True
95
+ )
96
+
97
+ def _split_pickings_from_source_location(self):
98
+ """Ensure that the related pickings will have the same source location.
99
+
100
+ Some pickings related could have other unrelated move lines, as such we
101
+ have to split them to contain only the move lines related to the expected
102
+ source location.
103
+
104
+ Example:
105
+
106
+ Initial data:
107
+
108
+ PICK1:
109
+ - move line with source location LOC1
110
+ - move line with source location LOC2
111
+ PICK2:
112
+ - move line with source location LOC2
113
+ - move line with source location LOC3
114
+
115
+ Then we process move lines related to LOC2 with this method, we get:
116
+
117
+ PICK1:
118
+ - move line with source location LOC1
119
+ PICK2:
120
+ - move line with source location LOC3
121
+ PICK3:
122
+ - move line with source location LOC2
123
+ - move line with source location LOC2
124
+
125
+ Return the pickings containing the given move lines.
126
+ """
127
+ _logger.warning(
128
+ "`_split_pickings_from_source_location` is deprecated "
129
+ "and replaced by `_extract_in_split_order`"
130
+ )
131
+ location_src_to_process = self.location_id
132
+ if location_src_to_process and len(location_src_to_process) != 1:
133
+ raise UserError(
134
+ _("Move lines processed have to share the same source location.")
135
+ )
136
+ pickings = self.picking_id
137
+ move_lines_to_process_ids = []
138
+ for picking in pickings:
139
+ location_src = picking.move_line_ids.location_id
140
+ if len(location_src) == 1:
141
+ continue
142
+ (picking.move_line_ids & self)._extract_in_split_order()
143
+ # Get the related move lines among the picking and split them
144
+ move_lines_to_process_ids.extend(
145
+ set(picking.move_line_ids.ids) & set(self.ids)
146
+ )
147
+ return self.picking_id
148
+
149
+ def _split_qty_to_be_done(self, qty_done, split_partial=True, **split_default_vals):
150
+ """Check qty to be done for current move line. Split it if needed.
151
+
152
+ :param qty_done: qty expected to be done
153
+ :param split_partial: split if qty is less than expected
154
+ otherwise rely on a backorder.
155
+ """
156
+ # store a new line if we have split our line (not enough qty)
157
+ new_line = self.env["stock.move.line"]
158
+ rounding = self.product_uom_id.rounding
159
+ compare = float_compare(
160
+ qty_done, self.reserved_uom_qty, precision_rounding=rounding
161
+ )
162
+ qty_lesser = compare == -1
163
+ qty_greater = compare == 1
164
+ if qty_greater:
165
+ return (new_line, "greater")
166
+ elif qty_lesser:
167
+ if not split_partial:
168
+ return (new_line, "lesser")
169
+ new_line = self._split_partial_quantity_to_be_done(
170
+ qty_done, split_default_vals
171
+ )
172
+ return (new_line, "lesser")
173
+ return (new_line, "full")
174
+
175
+ def _split_partial_quantity_to_be_done(self, quantity_done, split_default_vals):
176
+ """Create a new move line with the remaining quantity to process."""
177
+ # split the move line which will be processed later (maybe the user
178
+ # has to pick some goods from another place because the location
179
+ # contained less items than expected)
180
+ remaining = self.reserved_uom_qty - quantity_done
181
+ vals = {"reserved_uom_qty": remaining, "qty_done": 0}
182
+ vals.update(split_default_vals)
183
+ new_line = self.copy(vals)
184
+ # if we didn't bypass reservation update, the quant reservation
185
+ # would be reduced as much as the deduced quantity, which is wrong
186
+ # as we only moved the quantity to a new move line
187
+ self.with_context(
188
+ bypass_reservation_update=True
189
+ ).reserved_uom_qty = quantity_done
190
+ return new_line
191
+
192
+ def replace_package(self, new_package):
193
+ """Replace a package on an assigned move line"""
194
+ self.ensure_one()
195
+
196
+ # search other move lines which should already pick the scanned package
197
+ other_reserved_lines = self.env["stock.move.line"].search(
198
+ [
199
+ ("package_id", "=", new_package.id),
200
+ ("state", "in", ("partially_available", "assigned")),
201
+ ]
202
+ )
203
+
204
+ # we can't change already picked lines
205
+ unreservable_lines = other_reserved_lines.filtered(
206
+ lambda line: line.qty_done == 0
207
+ )
208
+ to_assign_moves = unreservable_lines.move_id
209
+
210
+ # if we leave the package level, it will try to reserve the same
211
+ # one again
212
+ unreservable_lines.package_level_id.explode_package()
213
+ # unreserve qties of other lines
214
+ unreservable_lines.unlink()
215
+
216
+ if new_package.location_id != self.location_id:
217
+ if new_package.quant_ids.reserved_quantity:
218
+ # this is a unexpected condition: if we started picking a package
219
+ # in another location, user should never be able to scan it in
220
+ # another location, block the operation
221
+ raise exceptions.UserError(
222
+ _(
223
+ "Package {} has been partially picked in another location"
224
+ ).format(new_package.display_name)
225
+ )
226
+ # the package has been scanned in the current location so we know its
227
+ # a mistake in the data... fix the quant to move the package here
228
+ new_package.move_package_to_location(self.location_id)
229
+
230
+ # several move lines can be moved by the package level, if we change
231
+ # the package for the current one, we destroy the package level because
232
+ # we are no longer moving the entire package
233
+ self.package_level_id.explode_package()
234
+
235
+ def is_greater(value, other, rounding):
236
+ return float_compare(value, other, precision_rounding=rounding) == 1
237
+
238
+ def is_lesser(value, other, rounding):
239
+ return float_compare(value, other, precision_rounding=rounding) == -1
240
+
241
+ quant = fields.first(
242
+ new_package.quant_ids.filtered(
243
+ lambda quant: quant.product_id == self.product_id
244
+ and is_greater(
245
+ quant.quantity,
246
+ quant.reserved_quantity,
247
+ quant.product_uom_id.rounding,
248
+ )
249
+ )
250
+ )
251
+ if not quant:
252
+ raise exceptions.UserError(
253
+ _(
254
+ "Package %(package_name)s does not contain available product "
255
+ "%(product_name)s, cannot replace package.",
256
+ package_name=new_package.display_name,
257
+ product_name=self.product_id.display_name,
258
+ )
259
+ )
260
+
261
+ values = {
262
+ "package_id": new_package.id,
263
+ "lot_id": quant.lot_id.id,
264
+ "owner_id": quant.owner_id.id,
265
+ "result_package_id": False,
266
+ }
267
+
268
+ available_quantity = quant.quantity - quant.reserved_quantity
269
+ if is_lesser(
270
+ available_quantity, self.reserved_qty, quant.product_uom_id.rounding
271
+ ):
272
+ new_uom_qty = self.product_id.uom_id._compute_quantity(
273
+ available_quantity, self.product_uom_id, rounding_method="HALF-UP"
274
+ )
275
+ values["reserved_uom_qty"] = new_uom_qty
276
+
277
+ self.write(values)
278
+
279
+ # try reassign the move in case we had a partial qty, also, it will
280
+ # recreate a package level if it applies
281
+ if "reserved_uom_qty" in values:
282
+ # when we change the quantity of the move, the state
283
+ # will still be "assigned" and be skipped by "_action_assign",
284
+ # recompute the state to be "partially_available"
285
+ self.move_id._recompute_state()
286
+
287
+ # if the new package has less quantities, assign will create new move
288
+ # lines
289
+ self.move_id._action_confirm()
290
+ self.move_id._action_assign()
291
+
292
+ # Find other available goods for the lines which were using the
293
+ # package before...
294
+ to_assign_moves._action_assign()
295
+
296
+ # computation of the 'state' of the package levels is not
297
+ # triggered, force it
298
+ to_assign_moves.move_line_ids.package_level_id.modified(["move_line_ids"])
299
+ self.package_level_id.modified(["move_line_ids"])
300
+
301
+ def _filter_on_picking(self, picking=False):
302
+ """Filter a bunch of lines on a picking.
303
+
304
+ If no picking is provided the first one is taken.
305
+ """
306
+ picking = picking or fields.first(self.picking_id)
307
+ return self.filtered_domain([("picking_id", "=", picking.id)])
@@ -0,0 +1,50 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+ from odoo import fields, models
4
+
5
+
6
+ class StockPackageLevel(models.Model):
7
+ _name = "stock.package_level"
8
+ _inherit = ["stock.package_level", "shopfloor.priority.postpone.mixin"]
9
+
10
+ # we search package levels based on their package in some workflows
11
+ package_id = fields.Many2one(index=True)
12
+ # allow domain on picking_id.xxx without too much perf penalty
13
+ picking_id = fields.Many2one(auto_join=True)
14
+
15
+ def explode_package(self):
16
+ """Unlink but keep the moves.
17
+
18
+ Original motivation:
19
+
20
+ A package level has a relation to "move_lines" only when the
21
+ package level was created first from the UI and it created
22
+ its move.
23
+ When we unlink a package level, it deletes the move it created.
24
+ But in some cases, we want to keep the move, e.g.:
25
+
26
+ * create a package level from the UI to move a package
27
+ * it generates a move for the matching product quantity
28
+ * we use a barcode scenario such as cluster or zone picking
29
+ * we use the "replace package" button
30
+ * when replacing the package, we have to delete the package level,
31
+ but we still have the same need in term of "I want X products",
32
+ so we have to keep the move
33
+ * another case is when we "dismiss" the package level in the location
34
+ content transfer scenario, we want to keep the "need" in moves, but
35
+ we are no longer moving the entire package level
36
+
37
+ Commit
38
+
39
+ https://github.com/odoo/odoo/commit/b33e72d0bf027fb2c789b1b9476f7edf1a40b0a6
40
+
41
+ introduced the handling of pkg level deletion
42
+ which is doing what was done by this method.
43
+
44
+ Moreover it has been fixed here https://github.com/odoo/odoo/pull/66517.
45
+
46
+ Hence, we keep this method to unify the action of "exploding a package"
47
+ especially to avoid to refactor many places every time the core changes.
48
+ """
49
+ # This will trigger the deletion of the pkg level
50
+ self.move_line_ids.result_package_id = False
@@ -0,0 +1,118 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+ from odoo import _, api, fields, models
4
+
5
+
6
+ class StockPicking(models.Model):
7
+ _inherit = "stock.picking"
8
+
9
+ total_weight = fields.Float(
10
+ compute="_compute_picking_info",
11
+ help="Technical field. Indicates total weight of transfers included.",
12
+ )
13
+ move_line_count = fields.Integer(
14
+ compute="_compute_picking_info",
15
+ help="Technical field. Indicates number of move lines included.",
16
+ )
17
+ package_level_count = fields.Integer(
18
+ compute="_compute_picking_info",
19
+ help="Technical field. Indicates number of package_level included.",
20
+ )
21
+ bulk_line_count = fields.Integer(
22
+ compute="_compute_picking_info",
23
+ help="Technical field. Indicates number of move lines without package included.",
24
+ )
25
+
26
+ @api.depends(
27
+ "move_line_ids", "move_line_ids.reserved_qty", "move_line_ids.product_id.weight"
28
+ )
29
+ def _compute_picking_info(self):
30
+ for item in self:
31
+ item.update(
32
+ {
33
+ "total_weight": item._calc_weight(),
34
+ "move_line_count": len(item.move_line_ids),
35
+ "package_level_count": len(item.package_level_ids),
36
+ # NOTE: not based on 'move_line_ids_without_package' field
37
+ # on purpose as it also takes into account the
38
+ # 'Move entire packs' option from the picking type.
39
+ "bulk_line_count": len(
40
+ item.move_line_ids.filtered(lambda ml: not ml.package_level_id)
41
+ ),
42
+ }
43
+ )
44
+
45
+ def _calc_weight(self):
46
+ weight = 0.0
47
+ for move_line in self.mapped("move_line_ids"):
48
+ weight += move_line.reserved_qty * move_line.product_id.weight
49
+ return weight
50
+
51
+ def _check_move_lines_map_quant_package(self, package):
52
+ # see tests/test_move_action_assign.py for details
53
+ pack_move_lines = self.move_line_ids.filtered(
54
+ lambda ml: ml.package_id == package
55
+ )
56
+ # if we set a qty_done on any line, it's picked, we don't want
57
+ # to change it in any case, so we ignore the package level
58
+ if any(pack_move_lines.mapped("qty_done")):
59
+ return False
60
+ # if we already changed the destination package, do not create
61
+ # a new package level
62
+ if any(
63
+ line.result_package_id != package
64
+ for line in pack_move_lines
65
+ if line.result_package_id
66
+ ):
67
+ return False
68
+ return super()._check_move_lines_map_quant_package(package)
69
+
70
+ def split_assigned_move_lines(self, move_lines=None):
71
+ """Put all reserved quantities (move lines) in their own moves and transfer.
72
+
73
+ As a result, the current transfer will contain only confirmed moves.
74
+ """
75
+ self.ensure_one()
76
+ # Check in the picking all the moves which are partially available or confirmed
77
+ moves = self.move_lines.filtered(
78
+ lambda m: m.state in ("partially_available", "confirmed")
79
+ )
80
+ # If one of these moves has an ancestor, split the moves
81
+ # then extract all the assigned moves in a new transfer.
82
+ # Indeed, a move without ancestor won't see its reserved qty changed
83
+ # automatically over time.
84
+ has_ancestors = bool(
85
+ moves.move_orig_ids.filtered(lambda m: m.state not in ("cancel", "done"))
86
+ )
87
+ if not has_ancestors:
88
+ return self.id
89
+ # Get only transfers composed of moves assigned or confirmed
90
+ moves.split_other_move_lines(moves.move_line_ids)
91
+ # Put assigned moves related to processed move lines into a separate transfer
92
+ if move_lines:
93
+ assigned_moves = self.move_lines & move_lines.move_id
94
+ else:
95
+ assigned_moves = self.move_lines.filtered(lambda m: m.state == "assigned")
96
+ if assigned_moves == self.move_lines:
97
+ return self.id
98
+ new_picking = self.copy(
99
+ {
100
+ "name": "/",
101
+ "move_lines": [],
102
+ "move_line_ids": [],
103
+ "backorder_id": self.id,
104
+ }
105
+ )
106
+ message = _(
107
+ 'The backorder <a href="#" '
108
+ 'data-oe-model="stock.picking" '
109
+ 'data-oe-id="%(new_picking_id)d">%(new_picking_name)s</a> has been created.'
110
+ ) % dict(new_picking_id=new_picking.id, new_picking_name=new_picking.name)
111
+ self.message_post(body=message)
112
+ assigned_moves.write({"picking_id": new_picking.id})
113
+ assigned_moves.mapped("move_line_ids").write({"picking_id": new_picking.id})
114
+ assigned_moves.move_line_ids.package_level_id.write(
115
+ {"picking_id": new_picking.id}
116
+ )
117
+ assigned_moves._action_assign()
118
+ return new_picking.id
@@ -0,0 +1,41 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+ from odoo import api, fields, models
4
+
5
+
6
+ class StockPickingBatch(models.Model):
7
+ _inherit = "stock.picking.batch"
8
+
9
+ picking_count = fields.Integer(
10
+ compute="_compute_picking_info",
11
+ help="Technical field. Indicates number of transfers included.",
12
+ )
13
+ move_line_count = fields.Integer(
14
+ compute="_compute_picking_info",
15
+ help="Technical field. Indicates number of move lines included.",
16
+ )
17
+ total_weight = fields.Float(
18
+ compute="_compute_picking_info",
19
+ help="Technical field. Indicates total weight of transfers included.",
20
+ )
21
+
22
+ @api.depends(
23
+ "picking_ids.state", "picking_ids.total_weight", "picking_ids.move_line_ids"
24
+ )
25
+ def _compute_picking_info(self):
26
+ for item in self:
27
+ assigned_pickings = item.picking_ids.filtered(
28
+ lambda picking: picking.state == "assigned"
29
+ )
30
+ item.update(
31
+ {
32
+ "picking_count": len(assigned_pickings.ids),
33
+ "move_line_count": len(
34
+ assigned_pickings.mapped("move_line_ids").ids
35
+ ),
36
+ "total_weight": item._calc_weight(assigned_pickings),
37
+ }
38
+ )
39
+
40
+ def _calc_weight(self, pickings):
41
+ return sum(pickings.mapped("total_weight"))
@@ -0,0 +1,26 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+ from odoo import api, fields, models
4
+
5
+
6
+ class StockPickingType(models.Model):
7
+ _inherit = "stock.picking.type"
8
+
9
+ shopfloor_menu_ids = fields.Many2many(
10
+ comodel_name="shopfloor.menu",
11
+ string="Shopfloor Menus",
12
+ readonly=True,
13
+ )
14
+ shopfloor_zero_check = fields.Boolean(
15
+ string="Activate Zero Check",
16
+ help="For Shopfloor scenarios using it (Cluster Picking, Zone Picking,"
17
+ " Discrete order Picking), the zero check step will be activated when"
18
+ " a location becomes empty after a move.",
19
+ )
20
+
21
+ @api.constrains("show_entire_packs")
22
+ def _check_move_entire_packages(self):
23
+ menu_items = self.env["shopfloor.menu"].search(
24
+ [("picking_type_ids", "in", self.ids)]
25
+ )
26
+ menu_items._check_move_entire_packages()