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,187 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+
4
+ from odoo.osv.expression import AND
5
+
6
+ from odoo.addons.component.core import Component
7
+
8
+
9
+ class SearchResult:
10
+
11
+ __slots__ = ("record", "type", "code")
12
+
13
+ def __init__(self, **kw) -> None:
14
+ for k in self.__slots__:
15
+ setattr(self, k, kw.get(k))
16
+
17
+ def __repr__(self) -> str:
18
+ return f"<{self.__class__.__name__}: type={self.type} code={self.code}>"
19
+
20
+ def __bool__(self):
21
+ return self.type != "none" or bool(self.record)
22
+
23
+ def __eq__(self, other):
24
+ for k in self.__slots__:
25
+ if not hasattr(other, k):
26
+ return False
27
+ if getattr(other, k) != getattr(self, k):
28
+ return False
29
+ return True
30
+
31
+ @property
32
+ def records(self):
33
+ """In some cases we expect more than one records (eg: location limit > 1) or lots"""
34
+ return self.record if len(self.record) > 1 else None
35
+
36
+
37
+ class SearchAction(Component):
38
+ """Provide methods to search records from scanner
39
+
40
+ The methods should be used in Service Components, so a search will always
41
+ have the same result in all scenarios.
42
+ """
43
+
44
+ _inherit = "shopfloor.search.action"
45
+
46
+ @property
47
+ def _barcode_type_handler(self):
48
+ return {
49
+ "product": self.product_from_scan,
50
+ "package": self.package_from_scan,
51
+ "picking": self.picking_from_scan,
52
+ "location": self.location_from_scan,
53
+ "location_dest": self.location_from_scan,
54
+ "lot": self.lot_from_scan,
55
+ "serial": self.lot_from_scan,
56
+ "packaging": self.packaging_from_scan,
57
+ "delivery_packaging": self.delivery_packaging_from_scan,
58
+ "origin_move": self.origin_move_from_scan,
59
+ }
60
+
61
+ def _make_search_result(self, **kwargs):
62
+ """Build a 'SearchResult' object describing the record found.
63
+
64
+ If no record has been found, the SearchResult object will have
65
+ its 'type' defined to "none".
66
+ """
67
+ return SearchResult(**kwargs)
68
+
69
+ def find(self, barcode, types=None, handler_kw=None):
70
+ """Find Odoo record matching given `barcode`.
71
+
72
+ Plain barcodes
73
+ """
74
+ barcode = barcode or ""
75
+ return self.generic_find(barcode, types=types, handler_kw=handler_kw)
76
+
77
+ def generic_find(self, barcode, types=None, handler_kw=None):
78
+ handler_kw = handler_kw or {}
79
+ _types = types or self._barcode_type_handler.keys()
80
+ # TODO: decide the best default order in case we don't pass `types`
81
+ for btype in _types:
82
+ handler = self._barcode_type_handler.get(btype)
83
+ if not handler:
84
+ continue
85
+ record = handler(barcode, **handler_kw.get(btype, {}))
86
+ if record:
87
+ return self._make_search_result(record=record, code=barcode, type=btype)
88
+
89
+ return self._make_search_result(type="none")
90
+
91
+ def location_from_scan(self, barcode, limit=1):
92
+ model = self.env["stock.location"]
93
+ if not barcode:
94
+ return model.browse()
95
+ # First search location by barcode
96
+ res = model.search([("barcode", "=", barcode)], limit=limit)
97
+ # And only if we have not found through barcode search on the location name
98
+ if len(res) < limit:
99
+ res |= model.search([("name", "=", barcode)], limit=(limit - len(res)))
100
+ return res
101
+
102
+ def package_from_scan(self, barcode):
103
+ model = self.env["stock.quant.package"]
104
+ if not barcode:
105
+ return model.browse()
106
+ return model.search([("name", "=", barcode)], limit=1)
107
+
108
+ def picking_from_scan(self, barcode, use_origin=False):
109
+ model = self.env["stock.picking"]
110
+ if not barcode:
111
+ return model.browse()
112
+ picking = model.search([("name", "=", barcode)], limit=1)
113
+ # We need to split the domain in two different searches
114
+ # as there might be a case where
115
+ # the name of a picking is the same as the origin of another picking
116
+ # (e.g. in a backorder) and we need to make sure
117
+ # the name search takes priority.
118
+ if picking:
119
+ return picking
120
+ if use_origin:
121
+ source_document_domain = [
122
+ # We could have the same origin for multiple transfers
123
+ # but we're interested only in the "assigned" ones.
124
+ ("origin", "=", barcode),
125
+ ("state", "=", "assigned"),
126
+ ]
127
+ return model.search(source_document_domain)
128
+ return model.browse()
129
+
130
+ def product_from_scan(self, barcode):
131
+ model = self.env["product.product"]
132
+ if not barcode:
133
+ return model.browse()
134
+ return model.search(
135
+ [
136
+ "|",
137
+ ("barcode", "=", barcode),
138
+ ("default_code", "=", barcode),
139
+ ],
140
+ limit=1,
141
+ )
142
+
143
+ def lot_from_scan(self, barcode, products=None, limit=1):
144
+ model = self.env["stock.lot"]
145
+ if not barcode:
146
+ return model.browse()
147
+ domain = [
148
+ ("company_id", "=", self.env.company.id),
149
+ ("name", "=", barcode),
150
+ ]
151
+ if products:
152
+ domain.append(("product_id", "in", products.ids))
153
+ return model.search(domain, limit=limit)
154
+
155
+ def packaging_from_scan(self, barcode):
156
+ model = self.env["product.packaging"]
157
+ if not barcode:
158
+ return model.browse()
159
+ return model.search(
160
+ [("barcode", "=", barcode), ("product_id", "!=", False)], limit=1
161
+ )
162
+
163
+ def generic_packaging_from_scan(self, barcode):
164
+ model = self.env["product.packaging"]
165
+ if not barcode:
166
+ return model.browse()
167
+ return model.search(
168
+ [("barcode", "=", barcode), ("product_id", "=", False)], limit=1
169
+ )
170
+
171
+ def delivery_packaging_from_scan(self, barcode):
172
+ model = self.env["stock.package.type"]
173
+ if not barcode:
174
+ return model.browse()
175
+ return model.search([("barcode", "=", barcode)], limit=1)
176
+
177
+ def origin_move_from_scan(self, barcode, extra_domain=None):
178
+ model = self.env["stock.move"]
179
+ outgoing_move_domain = [
180
+ # We could have the same origin for multiple transfers
181
+ # but we're interested only in the "done" ones.
182
+ ("origin", "=", barcode),
183
+ ("state", "=", "done"),
184
+ ]
185
+ if extra_domain:
186
+ outgoing_move_domain = AND([outgoing_move_domain, extra_domain])
187
+ return model.search(outgoing_move_domain)
@@ -0,0 +1,239 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+ from odoo import _, fields
4
+ from odoo.tools.float_utils import float_round
5
+
6
+ from odoo.addons.component.core import Component
7
+
8
+ from ..exceptions import ConcurentWorkOnTransfer
9
+
10
+
11
+ class StockAction(Component):
12
+ """Provide methods to work with stock operations."""
13
+
14
+ _name = "shopfloor.stock.action"
15
+ _inherit = "shopfloor.process.action"
16
+ _usage = "stock"
17
+
18
+ def _create_return_move__get_max_qty(self, origin_move):
19
+ """Returns the max returneable qty."""
20
+ # The max returnable qty is the sent qty minus the already returned qties
21
+ quantity = origin_move.reserved_qty
22
+ for move in origin_move.move_dest_ids:
23
+ if (
24
+ move.origin_returned_move_id
25
+ and move.origin_returned_move_id != origin_move
26
+ ):
27
+ continue
28
+ if move.state in ("partially_available", "assigned"):
29
+ quantity -= sum(move.move_line_ids.mapped("reserved_qty"))
30
+ elif move.state in ("done"):
31
+ quantity -= move.reserved_qty
32
+ return float_round(
33
+ quantity, precision_rounding=origin_move.product_id.uom_id.rounding
34
+ )
35
+
36
+ def _create_return_move__get_vals(self, return_picking, origin_move):
37
+ product = origin_move.product_id
38
+ return_type = return_picking.picking_type_id
39
+ return {
40
+ "product_id": product.id,
41
+ "product_uom": product.uom_id.id,
42
+ "picking_id": return_picking.id,
43
+ "state": "draft",
44
+ "date": fields.Datetime.now(),
45
+ "location_id": return_picking.location_id.id,
46
+ "location_dest_id": return_picking.location_dest_id.id,
47
+ "picking_type_id": return_type.id,
48
+ "warehouse_id": return_type.warehouse_id.id,
49
+ "origin_returned_move_id": origin_move.id,
50
+ "procure_method": "make_to_stock",
51
+ }
52
+
53
+ def _create_return_move__link_to_origin(self, return_move, origin_move):
54
+ move_orig_to_link = origin_move.move_dest_ids.mapped("returned_move_ids")
55
+ move_orig_to_link |= origin_move
56
+ origin_move_dest = origin_move.move_dest_ids.filtered(
57
+ lambda m: m.state not in ("cancel")
58
+ )
59
+ move_orig_to_link |= origin_move_dest.move_orig_ids.filtered(
60
+ lambda m: m.state not in ("cancel")
61
+ )
62
+ move_dest_to_link = origin_move.move_orig_ids.mapped("returned_move_ids")
63
+ move_dest_orig = origin_move.returned_move_ids.move_orig_ids.filtered(
64
+ lambda m: m.state not in ("cancel")
65
+ )
66
+ move_dest_to_link |= move_dest_orig.move_dest_ids.filtered(
67
+ lambda m: m.state not in ("cancel")
68
+ )
69
+ write_vals = {
70
+ "move_orig_ids": [(4, m.id) for m in move_orig_to_link],
71
+ "move_dest_ids": [(4, m.id) for m in move_dest_to_link],
72
+ }
73
+ return_move.write(write_vals)
74
+
75
+ def create_return_move(self, return_picking, origin_moves):
76
+ """Creates a return move for a given return picking / move"""
77
+ # Logic has been copied from
78
+ # odoo_src/addons/stock/wizard/stock_picking_return.py
79
+ for origin_move in origin_moves:
80
+ # If max qty <= 0, it means that everything has been returned already.
81
+ # Try with the next one from the recordset.
82
+ max_qty = self._create_return_move__get_max_qty(origin_move)
83
+ if max_qty > 0:
84
+ return_move_vals = self._create_return_move__get_vals(
85
+ return_picking, origin_move
86
+ )
87
+ return_move_vals.update(product_uom_qty=max_qty)
88
+ return_move = origin_move.copy(return_move_vals)
89
+ self._create_return_move__link_to_origin(return_move, origin_move)
90
+ return return_move
91
+
92
+ def _create_return_picking__get_vals(self, return_types, origin):
93
+ return_type = fields.first(return_types)
94
+ return {
95
+ "move_lines": [],
96
+ "picking_type_id": return_type.id,
97
+ "state": "draft",
98
+ "origin": origin,
99
+ "location_id": return_type.default_location_src_id.id,
100
+ "location_dest_id": return_type.default_location_dest_id.id,
101
+ "is_shopfloor_created": True,
102
+ }
103
+
104
+ def create_return_picking(self, picking, return_types, origin):
105
+ # Logic has been copied from
106
+ # odoo_src/addons/stock/wizard/stock_picking_return.py
107
+ return_values = self._create_return_picking__get_vals(return_types, origin)
108
+ return picking.copy(return_values)
109
+
110
+ def mark_move_line_as_picked(
111
+ self, move_lines, quantity=None, package=None, user=None, check_user=False
112
+ ):
113
+ """Set the qty_done and extract lines in new order"""
114
+ user = user or self.env.user
115
+ if check_user:
116
+ picking_users = move_lines.picking_id.user_id
117
+ if not all(pick_user == user for pick_user in picking_users):
118
+ raise ConcurentWorkOnTransfer(
119
+ _("Someone is already working on these transfers")
120
+ )
121
+ for line in move_lines:
122
+ qty_done = quantity if quantity is not None else line.reserved_uom_qty
123
+ line.qty_done = qty_done
124
+ line._split_partial_quantity()
125
+ data = {
126
+ "shopfloor_user_id": user.id,
127
+ }
128
+ if package:
129
+ # destination package is set to the scanned one
130
+ data["result_package_id"] = package.id
131
+ line.write(data)
132
+ # Extract the picked quantity in a split order and set current user
133
+ move_lines._extract_in_split_order(
134
+ {
135
+ "user_id": user.id,
136
+ "printed": True,
137
+ }
138
+ )
139
+ move_lines.picking_id.filtered(lambda p: p.user_id != user).user_id = user.id
140
+
141
+ def unmark_move_line_as_picked(self, move_lines):
142
+ """Reverse the change from `mark_move_line_as_picked`."""
143
+ move_lines.write(
144
+ {
145
+ "shopfloor_user_id": False,
146
+ "qty_done": 0,
147
+ "result_package_id": False,
148
+ }
149
+ )
150
+ pickings = move_lines.picking_id
151
+ for picking in pickings:
152
+ lines_still_assigned = picking.move_line_ids.filtered(
153
+ lambda l: l.shopfloor_user_id
154
+ )
155
+ if lines_still_assigned:
156
+ # Because there is other lines in the picking still assigned
157
+ # The picking has to be split
158
+ unmark_lines = picking.move_line_ids & move_lines
159
+ unmark_lines._extract_in_split_order(default={"user_id": False})
160
+ else:
161
+ pickings.write(
162
+ {
163
+ "user_id": False,
164
+ "printed": False,
165
+ }
166
+ )
167
+
168
+ def validate_moves(self, moves):
169
+ """Validate moves in different ways depending on several criterias:
170
+
171
+ - moves to process are all the moves of the related transfer:
172
+ the current transfer is validated
173
+ - moves to process are a subset of available moves in the picking:
174
+ the moves are put in a new transfer which is validated, the current
175
+ transfer still have the remaining moves
176
+ - moves to process are exactly the assigned moves of the related transfer:
177
+ the transfer is validated as usual, creating a backorder.
178
+ """
179
+ moves.split_unavailable_qty()
180
+ backorders = self.env["stock.picking"]
181
+ for picking in moves.picking_id:
182
+ # the backorder strategy is checked in the 'button_validate' method
183
+ # on odoo standard. Since we call the sub-method '_action_done' here,
184
+ # we have to set the context key 'cancel_backorder' as it is done
185
+ # in the 'button_validate' method according to the backorder strategy.
186
+ not_to_backorder = picking.picking_type_id.create_backorder == "never"
187
+ picking = picking.with_context(cancel_backorder=not_to_backorder)
188
+ moves_todo = picking.move_ids & moves
189
+ if self._check_backorder(picking, moves_todo):
190
+ existing_backorders = picking.backorder_ids
191
+ picking._action_done()
192
+ new_backorders = picking.backorder_ids - existing_backorders
193
+ if new_backorders:
194
+ new_backorders.write({"user_id": False})
195
+ backorders |= new_backorders
196
+ else:
197
+ backorders |= moves_todo.extract_and_action_done()
198
+ return backorders
199
+
200
+ def _check_backorder(self, picking, moves):
201
+ """Check if the `picking` has to be validated as usual to create a backorder.
202
+
203
+ We want to create a normal backorder if:
204
+
205
+ - the moves are equal to all available moves of the current picking
206
+ but there are still unavailable moves to process
207
+ - the moves are not linked to unprocessed ancestor moves
208
+ """
209
+ assigned_moves = picking.move_ids.filtered(lambda m: m.state == "assigned")
210
+ has_ancestors = bool(
211
+ moves.move_orig_ids.filtered(lambda m: m.state not in ("cancel", "done"))
212
+ )
213
+ return moves == assigned_moves and not has_ancestors
214
+
215
+ def put_package_level_in_move(self, package_level):
216
+ """Ensure to put the package level in its own move.
217
+
218
+ In standard the moves linked to a package level could also be linked to
219
+ other unrelated move lines. This method ensures that the package level
220
+ will be attached to a move with only the relevant lines.
221
+ This is useful to process a single package, having its own move makes
222
+ this process easy.
223
+ """
224
+ package_move_lines = package_level.move_line_ids
225
+ package_moves = package_move_lines.move_id
226
+ for package_move in package_moves:
227
+ # Check if there is no other lines linked to the move others than
228
+ # the lines related to the package itself. In such case we have to
229
+ # split the move to process only the lines related to the package.
230
+ package_move.split_other_move_lines(package_move_lines)
231
+
232
+ def no_putaway_available(self, picking_types, move_lines):
233
+ """Returns `True` if no putaway destination has been computed for one
234
+ of the given move lines.
235
+ """
236
+ base_locations = picking_types.default_location_dest_id
237
+ # when no putaway is found, the move line destination stays the
238
+ # default's of the picking type
239
+ return any(line.location_dest_id in base_locations for line in move_lines)
@@ -0,0 +1,66 @@
1
+ # Copyright 2022 Camptocamp SA
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
3
+ from odoo.addons.component.core import Component
4
+
5
+
6
+ class StockUnreserve(Component):
7
+ """Provide methods to unreserve goods of a location."""
8
+
9
+ _name = "shopfloor.stock.unreserve.action"
10
+ _inherit = "shopfloor.process.action"
11
+ _usage = "stock.unreserve"
12
+
13
+ def check_unreserve(self, location, move_lines, product=None, lot=None):
14
+ """Return a message if there is an ongoing operation in the location.
15
+
16
+ It could be a move line with some qty already processed or another
17
+ Shopfloor user working there.
18
+
19
+ :param location: stock location from which moves are unreserved
20
+ :param move_lines: move lines to unreserve
21
+ :param product: optional product to limit the scope in the location
22
+ """
23
+ location_move_lines = self._find_location_all_move_lines(location, product, lot)
24
+ extra_move_lines = location_move_lines - move_lines
25
+ if extra_move_lines:
26
+ return self.msg_store.picking_already_started_in_location(
27
+ extra_move_lines.picking_id
28
+ )
29
+
30
+ def unreserve_moves(self, move_lines, picking_types):
31
+ """Unreserve moves from `move_lines'.
32
+
33
+ Returns a tuple of (
34
+ move lines that stays in the location to process,
35
+ moves to reserve again
36
+ )
37
+ """
38
+ moves_to_unreserve = move_lines.move_id
39
+ # If there is no other moves to unreserve of a different picking type, leave
40
+ lines_other_picking_types = move_lines.filtered(
41
+ lambda line: line.picking_id.picking_type_id not in picking_types
42
+ )
43
+ if not lines_other_picking_types:
44
+ return (move_lines, self.env["stock.move"].browse())
45
+ # if we leave the package level around, it will try to reserve
46
+ # the same package as before
47
+ package_levels = move_lines.package_level_id
48
+ package_levels.explode_package()
49
+ moves_to_unreserve._do_unreserve()
50
+ return (move_lines - lines_other_picking_types, moves_to_unreserve)
51
+
52
+ def _find_location_all_move_lines_domain(self, location, product=None, lot=None):
53
+ domain = [
54
+ ("location_id", "=", location.id),
55
+ ("state", "in", ("assigned", "partially_available")),
56
+ ]
57
+ if product:
58
+ domain.append(("product_id", "=", product.id))
59
+ if lot:
60
+ domain.append(("lot_id", "=", lot.id))
61
+ return domain
62
+
63
+ def _find_location_all_move_lines(self, location, product=None, lot=None):
64
+ return self.env["stock.move.line"].search(
65
+ self._find_location_all_move_lines_domain(location, product, lot)
66
+ )
@@ -0,0 +1,5 @@
1
+ from . import scan_handler_location
2
+ from . import scan_handler_package
3
+ from . import scan_handler_product
4
+ from . import scan_handler_lot
5
+ from . import scan_handler_transfer
@@ -0,0 +1,26 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu)
3
+ # @author Simone Orsi <simahawk@gmail.com>
4
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
5
+
6
+ from odoo.addons.component.core import Component
7
+
8
+
9
+ class LocationHandler(Component):
10
+ """Scan anything handler for stock.location."""
11
+
12
+ _name = "shopfloor.scan.location.handler"
13
+ _inherit = "shopfloor.scan.anything.handler"
14
+
15
+ record_type = "location"
16
+
17
+ def search(self, identifier):
18
+ res = self._search.find(identifier, types=("location",))
19
+ return res.record if res.record else self.env["stock.location"]
20
+
21
+ @property
22
+ def converter(self):
23
+ return self._data_detail.location_detail
24
+
25
+ def schema(self):
26
+ return self._schema_detail.location_detail()
@@ -0,0 +1,26 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu)
3
+ # @author Simone Orsi <simahawk@gmail.com>
4
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
5
+
6
+ from odoo.addons.component.core import Component
7
+
8
+
9
+ class LotHandler(Component):
10
+ """Scan anything handler for stock.lot."""
11
+
12
+ _name = "shopfloor.scan.lot.handler"
13
+ _inherit = "shopfloor.scan.anything.handler"
14
+
15
+ record_type = "lot"
16
+
17
+ def search(self, identifier):
18
+ res = self._search.find(identifier, types=("lot",))
19
+ return res.record if res.record else self.env["stock.lot"]
20
+
21
+ @property
22
+ def converter(self):
23
+ return self._data_detail.lot_detail
24
+
25
+ def schema(self):
26
+ return self._schema_detail.lot_detail()
@@ -0,0 +1,26 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu)
3
+ # @author Simone Orsi <simahawk@gmail.com>
4
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
5
+
6
+ from odoo.addons.component.core import Component
7
+
8
+
9
+ class PackageHandler(Component):
10
+ """Scan anything handler for stock.quant.package."""
11
+
12
+ _name = "shopfloor.scan.package.handler"
13
+ _inherit = "shopfloor.scan.anything.handler"
14
+
15
+ record_type = "package"
16
+
17
+ def search(self, identifier):
18
+ res = self._search.find(identifier, types=("package",))
19
+ return res.record if res.record else self.env["stock.quant.package"]
20
+
21
+ @property
22
+ def converter(self):
23
+ return self._data_detail.package_detail
24
+
25
+ def schema(self):
26
+ return self._schema_detail.package_detail()
@@ -0,0 +1,26 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu)
3
+ # @author Simone Orsi <simahawk@gmail.com>
4
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
5
+
6
+ from odoo.addons.component.core import Component
7
+
8
+
9
+ class ProductHandler(Component):
10
+ """Scan anything handler for product.product."""
11
+
12
+ _name = "shopfloor.scan.product.handler"
13
+ _inherit = "shopfloor.scan.anything.handler"
14
+
15
+ record_type = "product"
16
+
17
+ def search(self, identifier):
18
+ res = self._search.find(identifier, types=("product", "packaging"))
19
+ return res.record if res.record else self.env["product.product"]
20
+
21
+ @property
22
+ def converter(self):
23
+ return self._data_detail.product_detail
24
+
25
+ def schema(self):
26
+ return self._schema_detail.product_detail()
@@ -0,0 +1,26 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # Copyright 2021 ACSONE SA/NV (http://www.acsone.eu)
3
+ # @author Simone Orsi <simahawk@gmail.com>
4
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
5
+
6
+ from odoo.addons.component.core import Component
7
+
8
+
9
+ class TransferHandler(Component):
10
+ """Scan anything handler for stock.picking."""
11
+
12
+ _name = "shopfloor.scan.transfer.handler"
13
+ _inherit = "shopfloor.scan.anything.handler"
14
+
15
+ record_type = "transfer"
16
+
17
+ def search(self, identifier):
18
+ res = self._search.find(identifier, types=("picking",))
19
+ return res.record if res.record else self.env["stock.picking"]
20
+
21
+ @property
22
+ def converter(self):
23
+ return self._data_detail.picking_detail
24
+
25
+ def schema(self):
26
+ return self._schema_detail.picking_detail()