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,329 @@
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
+
5
+ from odoo.addons.component.core import Component
6
+ from odoo.addons.shopfloor_base.utils import ensure_model
7
+
8
+
9
+ class DataAction(Component):
10
+ _inherit = "shopfloor.data.action"
11
+
12
+ @ensure_model("stock.location")
13
+ def location(self, record, **kw):
14
+ parser = self._location_parser
15
+ data = self._jsonify(record.with_context(location=record.id), parser, **kw)
16
+ if "with_operation_progress" in kw:
17
+ operation_progress = self._get_location_operations_progress(record)
18
+ data.update({"operation_progress": operation_progress})
19
+ return data
20
+
21
+ def locations(self, record, **kw):
22
+ return self.location(record, multi=True)
23
+
24
+ def _get_location_operations_progress(self, location):
25
+ lines = self.env["stock.move.line"].search(
26
+ [
27
+ ("location_id", "=", location.id),
28
+ ("state", "in", ["partially_available", "assigned"]),
29
+ ("picking_id.state", "=", "assigned"),
30
+ ]
31
+ )
32
+ # operations_to_do = number of total operations that are pending for this location.
33
+ # operations_done = number of operations already done.
34
+ # A line with an assigned package counts as 1 operation.
35
+ operations_to_do = 0
36
+ operations_done = 0
37
+ for line in lines:
38
+ operations_done += line.qty_done if not line.package_id else 1
39
+ operations_to_do += line.reserved_uom_qty if not line.package_id else 1
40
+ return {
41
+ "done": operations_done,
42
+ "to_do": operations_to_do,
43
+ }
44
+
45
+ @property
46
+ def _location_parser(self):
47
+ return [
48
+ "id",
49
+ "name",
50
+ # Fallback to name if barcode is not valued.
51
+ ("barcode", lambda rec, fname: rec[fname] if rec[fname] else rec.name),
52
+ ]
53
+
54
+ @ensure_model("stock.picking")
55
+ def picking(self, record, **kw):
56
+ parser = self._picking_parser
57
+ # progress is a heavy computed field,
58
+ # and it may reduce performance significatively
59
+ # when dealing with a large number of pickings.
60
+ # Thus, we make it optional.
61
+ if "with_progress" in kw:
62
+ parser.append("progress")
63
+ return self._jsonify(record, parser, **kw)
64
+
65
+ def pickings(self, record, **kw):
66
+ return self.picking(record, multi=True)
67
+
68
+ @property
69
+ def _picking_parser(self, **kw):
70
+ return [
71
+ "id",
72
+ "name",
73
+ "origin",
74
+ "note",
75
+ ("partner_id:partner", self._partner_parser),
76
+ ("carrier_id:carrier", self._simple_record_parser()),
77
+ ("ship_carrier_id:ship_carrier", self._simple_record_parser()),
78
+ "move_line_count",
79
+ "package_level_count",
80
+ "bulk_line_count",
81
+ "total_weight:weight",
82
+ "scheduled_date",
83
+ ]
84
+
85
+ @ensure_model("stock.quant.package")
86
+ def package(self, record, picking=None, with_packaging=False, **kw):
87
+ """Return data for a stock.quant.package
88
+
89
+ If a picking is given, it will include the number of lines of the package
90
+ for the picking.
91
+ """
92
+ parser = self._package_parser
93
+ if with_packaging:
94
+ parser += self._package_packaging_parser
95
+ data = self._jsonify(record, parser, **kw)
96
+ # handle special cases
97
+ if data and picking:
98
+ lines = picking.move_line_ids.filtered(
99
+ lambda l: l.result_package_id == record
100
+ and l.state in ["partially_available", "assigned", "done"]
101
+ )
102
+ data.update({"move_line_count": len(lines)})
103
+ return data
104
+
105
+ def packages(self, records, picking=None, **kw):
106
+ return [self.package(rec, picking=picking, **kw) for rec in records]
107
+
108
+ @property
109
+ def _package_parser(self):
110
+ return [
111
+ "id",
112
+ "name",
113
+ "shopfloor_weight:weight",
114
+ ("package_type_id:storage_type", ["id", "name"]),
115
+ ]
116
+
117
+ @property
118
+ def _package_packaging_parser(self):
119
+ return [
120
+ ("product_packaging_id:packaging", self._packaging_parser),
121
+ ]
122
+
123
+ @ensure_model("product.packaging")
124
+ def packaging(self, record, **kw):
125
+ return self._jsonify(record, self._packaging_parser, **kw)
126
+
127
+ def packaging_list(self, record, **kw):
128
+ return self.packaging(record, multi=True)
129
+
130
+ @property
131
+ def _packaging_parser(self):
132
+ return [
133
+ "id",
134
+ ("packaging_level_id:name", lambda rec, fname: rec.packaging_level_id.name),
135
+ ("packaging_level_id:code", lambda rec, fname: rec.packaging_level_id.code),
136
+ "qty",
137
+ ]
138
+
139
+ @ensure_model("stock.package.type")
140
+ def delivery_packaging(self, record, **kw):
141
+ return self._jsonify(record, self._delivery_packaging_parser, **kw)
142
+
143
+ def delivery_packaging_list(self, records, **kw):
144
+ return self.delivery_packaging(records, multi=True)
145
+
146
+ @property
147
+ def _delivery_packaging_parser(self):
148
+ return [
149
+ "id",
150
+ "name",
151
+ "package_carrier_type:packaging_type",
152
+ "barcode",
153
+ ]
154
+
155
+ @ensure_model("stock.lot")
156
+ def lot(self, record, **kw):
157
+ return self._jsonify(record, self._lot_parser, **kw)
158
+
159
+ def lots(self, record, **kw):
160
+ return self.lot(record, multi=True)
161
+
162
+ @property
163
+ def _lot_parser(self):
164
+ return self._simple_record_parser() + ["ref", "expiration_date"]
165
+
166
+ @ensure_model("stock.move.line")
167
+ def move_line(self, record, with_picking=False, **kw):
168
+ record = record.with_context(location=record.location_id.id)
169
+ parser = self._move_line_parser
170
+ if with_picking:
171
+ parser += [("picking_id:picking", self._picking_parser)]
172
+ data = self._jsonify(record, parser)
173
+ if data:
174
+ data.update(
175
+ {
176
+ # cannot use sub-parser here
177
+ # because result might depend on picking
178
+ "package_src": self.package(
179
+ record.package_id, record.picking_id, **kw
180
+ ),
181
+ "package_dest": self.package(
182
+ record.result_package_id.with_context(
183
+ picking_id=record.picking_id.id
184
+ ),
185
+ record.picking_id,
186
+ **kw,
187
+ ),
188
+ }
189
+ )
190
+ return data
191
+
192
+ def move_lines(self, records, **kw):
193
+ return [self.move_line(rec, **kw) for rec in records]
194
+
195
+ @property
196
+ def _move_line_parser(self):
197
+ return [
198
+ "id",
199
+ "qty_done",
200
+ "reserved_uom_qty:quantity",
201
+ ("product_id:product", self._product_parser),
202
+ ("lot_id:lot", self._lot_parser),
203
+ ("location_id:location_src", self._location_parser),
204
+ ("location_dest_id:location_dest", self._location_parser),
205
+ (
206
+ "move_id:priority",
207
+ lambda rec, fname: rec.move_id.priority or "",
208
+ ),
209
+ "progress",
210
+ ]
211
+
212
+ @ensure_model("stock.move")
213
+ def move(self, record, **kw):
214
+ record = record.with_context(location=record.location_id.id)
215
+ parser = self._move_parser
216
+ return self._jsonify(record, parser)
217
+
218
+ def moves(self, records, **kw):
219
+ return [self.move(rec, **kw) for rec in records]
220
+
221
+ @property
222
+ def _move_parser(self):
223
+ return [
224
+ "id",
225
+ "quantity_done",
226
+ "product_uom_qty:quantity",
227
+ ("product_id:product", self._product_parser),
228
+ ("location_id:location_src", self._location_parser),
229
+ ("location_dest_id:location_dest", self._location_parser),
230
+ "priority",
231
+ "progress",
232
+ ]
233
+
234
+ @ensure_model("stock.package_level")
235
+ def package_level(self, record, **kw):
236
+ return self._jsonify(record, self._package_level_parser)
237
+
238
+ def package_levels(self, records, **kw):
239
+ return [self.package_level(rec, **kw) for rec in records]
240
+
241
+ @property
242
+ def _package_level_parser(self):
243
+ return [
244
+ "id",
245
+ "is_done",
246
+ ("picking_id:picking", self._simple_record_parser()),
247
+ ("package_id:package_src", self._package_parser),
248
+ ("location_dest_id:location_dest", self._location_parser),
249
+ (
250
+ "location_id:location_src",
251
+ lambda rec, fname: self.location(
252
+ fields.first(rec.move_line_ids).location_id
253
+ or fields.first(rec.move_lines).location_id
254
+ or rec.picking_id.location_id
255
+ ),
256
+ ),
257
+ # tnx to stock_quant_package_product_packaging
258
+ (
259
+ "package_id:product",
260
+ lambda rec, fname: self.product(rec.package_id.single_product_id),
261
+ ),
262
+ # TODO: allow to pass mapped path to jsonifier
263
+ (
264
+ "package_id:quantity",
265
+ lambda rec, fname: rec.package_id.single_product_qty,
266
+ ),
267
+ ]
268
+
269
+ @ensure_model("product.product")
270
+ def product(self, record, **kw):
271
+ return self._jsonify(record, self._product_parser, **kw)
272
+
273
+ def products(self, record, **kw):
274
+ return self.product(record, multi=True)
275
+
276
+ @property
277
+ def _product_parser(self):
278
+ return [
279
+ "id",
280
+ "name",
281
+ "display_name",
282
+ "default_code",
283
+ "barcode",
284
+ ("packaging_ids:packaging", self._product_packaging),
285
+ ("uom_id:uom", self._simple_record_parser() + ["factor", "rounding"]),
286
+ ("seller_ids:supplier_code", self._product_supplier_code),
287
+ ]
288
+
289
+ def _product_packaging(self, rec, field):
290
+ return self._jsonify(
291
+ rec.packaging_ids.filtered(lambda x: x.qty),
292
+ self._packaging_parser,
293
+ multi=True,
294
+ )
295
+
296
+ def _product_supplier_code(self, rec, field):
297
+ supplier_info = fields.first(
298
+ rec.seller_ids.filtered(lambda x: x.product_id == rec)
299
+ )
300
+ return supplier_info.product_code or ""
301
+
302
+ @ensure_model("stock.picking.batch")
303
+ def picking_batch(self, record, with_pickings=False, **kw):
304
+ parser = self._picking_batch_parser
305
+ if with_pickings:
306
+ parser.append(("picking_ids:pickings", self._picking_parser))
307
+ return self._jsonify(record, parser, **kw)
308
+
309
+ def picking_batches(self, record, with_pickings=False, **kw):
310
+ return self.picking_batch(record, with_pickings=with_pickings, multi=True)
311
+
312
+ @property
313
+ def _picking_batch_parser(self):
314
+ return ["id", "name", "picking_count", "move_line_count", "total_weight:weight"]
315
+
316
+ @ensure_model("stock.picking.type")
317
+ def picking_type(self, record, **kw):
318
+ parser = self._picking_type_parser
319
+ return self._jsonify(record, parser, **kw)
320
+
321
+ def picking_types(self, record, **kw):
322
+ return self.picking_type(record, multi=True)
323
+
324
+ @property
325
+ def _picking_type_parser(self):
326
+ return [
327
+ "id",
328
+ "name",
329
+ ]
@@ -0,0 +1,154 @@
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.tools.float_utils import float_round
4
+
5
+ from odoo.addons.component.core import Component
6
+ from odoo.addons.shopfloor_base.utils import ensure_model
7
+
8
+
9
+ class DataDetailAction(Component):
10
+ _inherit = "shopfloor.data.detail.action"
11
+
12
+ def _select_value_to_label(self, rec, fname):
13
+ return rec._fields[fname].convert_to_export(rec[fname], rec)
14
+
15
+ def location_detail(self, record, **kw):
16
+ return self._jsonify(
17
+ record.with_context(location=record.id), self._location_detail_parser, **kw
18
+ )
19
+
20
+ def locations_detail(self, record, **kw):
21
+ return self.location_detail(record, multi=True)
22
+
23
+ @property
24
+ def _location_detail_parser(self):
25
+ return self._location_parser + [
26
+ "complete_name",
27
+ (
28
+ "reserved_move_line_ids:reserved_move_lines",
29
+ lambda record, fname: self.move_lines(record[fname]),
30
+ ),
31
+ ]
32
+
33
+ @ensure_model("stock.picking")
34
+ def picking_detail(self, record, **kw):
35
+ parser = self._picking_detail_parser
36
+ # progress is a heavy computed field,
37
+ # and it may reduce performance significatively
38
+ # when dealing with a large number of pickings.
39
+ # Thus, we make it optional.
40
+ if "with_progress" in kw:
41
+ parser.append("progress")
42
+ return self._jsonify(record, parser, **kw)
43
+
44
+ def pickings_detail(self, record, **kw):
45
+ return self.picking_detail(record, multi=True)
46
+
47
+ @property
48
+ def _picking_detail_parser(self):
49
+ return self._picking_parser + [
50
+ "picking_type_code",
51
+ ("priority", self._select_value_to_label),
52
+ "scheduled_date",
53
+ ("picking_type_id:operation_type", ["id", "name"]),
54
+ (
55
+ "move_line_ids:move_lines",
56
+ lambda record, fname: self.move_lines(record[fname]),
57
+ ),
58
+ ]
59
+
60
+ def package_detail(self, record, picking=None, **kw):
61
+ # Define a new method to not overload the base one which is used in many places
62
+ data = self.package(record, picking=picking, with_packaging=True, **kw)
63
+ data.update(self._jsonify(record, self._package_detail_parser, **kw))
64
+ return data
65
+
66
+ def packages_detail(self, records, picking=None, **kw):
67
+ return [self.package_detail(rec, picking=picking) for rec in records]
68
+
69
+ @property
70
+ def _package_detail_parser(self):
71
+ return [
72
+ (
73
+ "reserved_move_line_ids:pickings",
74
+ lambda record, fname: self.pickings(record[fname].mapped("picking_id")),
75
+ ),
76
+ (
77
+ "reserved_move_line_ids:move_lines",
78
+ lambda record, fname: self.move_lines(record[fname]),
79
+ ),
80
+ ("location_id:location", ["id", "display_name:name"]),
81
+ ]
82
+
83
+ @ensure_model("stock.lot")
84
+ def lot_detail(self, record, **kw):
85
+ # Define a new method to not overload the base one which is used in many places
86
+ return self._jsonify(record, self._lot_detail_parser, **kw)
87
+
88
+ def lots_detail(self, record, **kw):
89
+ return self.lot_detail(record, multi=True)
90
+
91
+ @property
92
+ def _lot_detail_parser(self):
93
+ return self._lot_parser + [
94
+ "removal_date",
95
+ "expiration_date:expire_date",
96
+ (
97
+ "product_id:product",
98
+ lambda record, fname: self.product_detail(record[fname]),
99
+ ),
100
+ ]
101
+
102
+ @ensure_model("product.product")
103
+ def product_detail(self, record, **kw):
104
+ # Defined new method to not overload the base one used in many places
105
+ data = self._jsonify(record, self._product_detail_parser, **kw)
106
+ suppliers = self.env["product.supplierinfo"].search(
107
+ [("product_id", "=", record.id)]
108
+ )
109
+ data["suppliers"] = self._jsonify(
110
+ suppliers, self._product_supplierinfo_parser, multi=True
111
+ )
112
+ return data
113
+
114
+ def products_detail(self, record, **kw):
115
+ return self.product_detail(record, multi=True)
116
+
117
+ @property
118
+ def _product_parser(self):
119
+ return super()._product_parser + [
120
+ "qty_available",
121
+ ("free_qty:qty_reserved", self._product_reserved_qty_subparser),
122
+ ]
123
+
124
+ def _product_reserved_qty_subparser(self, rec, field_name):
125
+ # free_qty = qty_available - reserved_quantity
126
+ return float_round(
127
+ rec.qty_available - rec[field_name], precision_rounding=rec.uom_id.rounding
128
+ )
129
+
130
+ @property
131
+ def _product_detail_parser(self):
132
+ return self._product_parser + [
133
+ ("image_128:image", self._product_image_url),
134
+ (
135
+ "product_tmpl_id:manufacturer",
136
+ lambda rec, fname: self._jsonify(
137
+ rec.product_tmpl_id.manufacturer_id, ["id", "name"]
138
+ ),
139
+ ),
140
+ ]
141
+
142
+ def _product_image_url(self, record, field_name):
143
+ if not record[field_name]:
144
+ return None
145
+ return "/web/image/product.product/{}/{}".format(record.id, field_name)
146
+
147
+ @property
148
+ def _product_supplierinfo_parser(self):
149
+ return [
150
+ ("id", lambda rec, fname: rec.partner_id.id),
151
+ ("partner_id:partner", lambda rec, fname: rec.partner_id.name),
152
+ "product_name",
153
+ "product_code",
154
+ ]
@@ -0,0 +1,150 @@
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
+
5
+ from odoo.addons.component.core import Component
6
+
7
+
8
+ class InventoryAction(Component):
9
+ """Provide methods to work with inventories
10
+
11
+ Several processes have to create inventories at some point,
12
+ for instance when there is a stock issue.
13
+ """
14
+
15
+ _name = "shopfloor.inventory.action"
16
+ _inherit = "shopfloor.process.action"
17
+ _usage = "inventory"
18
+
19
+ @property
20
+ def inventory_model(self):
21
+ # the _sf_inventory key bypass groups checks,
22
+ # see comment in models/stock_inventory.py
23
+ return self.env["stock.quant"].with_context(_sf_inventory=True)
24
+
25
+ def create_draft_check_empty(self, location, product, ref=None):
26
+ """Create a draft inventory for a product with a zero quantity"""
27
+ return self._create_draft_inventory(location, product)
28
+
29
+ def _inventory_exists(self, location, product, package=None, lot=None):
30
+ """Return if an inventory for location and product exist"""
31
+ domain = [
32
+ ("location_id", "=", location.id),
33
+ ("product_id", "=", product.id),
34
+ ("inventory_quantity_set", "=", True),
35
+ ]
36
+ if package is not None:
37
+ domain.append(("package_id", "=", package.id))
38
+ if lot is not None:
39
+ domain.append(("lot_id", "=", lot.id))
40
+ return self.inventory_model.search_count(domain)
41
+
42
+ def _get_existing_quant(self, location, product, package=None, lot=None, limit=1):
43
+ domain = [("location_id", "=", location.id), ("product_id", "=", product.id)]
44
+ if package is not None:
45
+ domain.append(("package_id", "=", package.id))
46
+ else:
47
+ domain.append(("package_id", "=", False))
48
+ if lot is not None:
49
+ domain.append(("lot_id", "=", lot.id))
50
+ else:
51
+ domain.append(("lot_id", "=", False))
52
+ return self.inventory_model.search(domain, limit=limit)
53
+
54
+ def _create_draft_inventory(self, location, product, package=None, lot=None):
55
+ quants = self._get_existing_quant(
56
+ location, product, package=package, lot=lot, limit=None
57
+ )
58
+ if quants:
59
+ for quant in quants:
60
+ if quant.inventory_quantity_set:
61
+ continue
62
+ quants.write(
63
+ {
64
+ # Set an inventory quantity to prevent the zero quant cleanup
65
+ "inventory_quantity": quant.inventory_quantity + 1,
66
+ "inventory_date": fields.Date.today(),
67
+ }
68
+ )
69
+ return quants
70
+ else:
71
+ return self.inventory_model.sudo().create(
72
+ {
73
+ "location_id": location.id,
74
+ "product_id": product.id,
75
+ "lot_id": lot.id,
76
+ "inventory_quantity": 1,
77
+ "inventory_date": fields.Date.today(),
78
+ "package_id": package.id if package else False,
79
+ }
80
+ )
81
+
82
+ def create_control_stock(
83
+ self, location, product, package=None, lot=None, name=None
84
+ ):
85
+ """Create a draft inventory so a user has to check a location
86
+
87
+ If a draft or in progress inventory already exists for the same
88
+ combination of product/package/lot, no inventory is created.
89
+ """
90
+ if not self._inventory_exists(location, product, package=package, lot=lot):
91
+ self._create_draft_inventory(location, product, package=package, lot=lot)
92
+
93
+ def create_stock_issue(self, move, location, package, lot):
94
+ """Create an inventory for a stock issue
95
+
96
+ It reduces the quantity in a location in a way that:
97
+ * assigned move lines in other batch transfers stay assigned.
98
+ * assigned move lines in same batch but already picked stay assigned.
99
+ """
100
+ other_lines = self._stock_issue_get_related_move_lines(
101
+ move, location, package, lot
102
+ )
103
+ qty_to_keep = sum(other_lines.mapped("reserved_qty"))
104
+ self.create_stock_correction(move, location, package, lot, qty_to_keep)
105
+ move._action_assign()
106
+
107
+ def create_stock_correction(self, move, location, package, lot, quantity):
108
+ """Create an inventory with a forced quantity"""
109
+ quant = self._get_existing_quant(location, move.product_id, package, lot)
110
+ if quant:
111
+ quant.with_context(
112
+ inventory_mode=True
113
+ ).sudo().inventory_quantity_auto_apply = quantity
114
+ else:
115
+ self.inventory_model._update_available_quantity(
116
+ move.product_id, location, quantity, lot_id=lot, package_id=package
117
+ )
118
+ # FIXME
119
+ move.product_id.stock_quant_ids._quant_tasks()
120
+
121
+ def _stock_issue_get_related_move_lines(self, move, location, package, lot):
122
+ """Lookup for all the other moves lines that match given move line"""
123
+ domain = [
124
+ ("location_id", "=", location.id),
125
+ ("product_id", "=", move.product_id.id),
126
+ ("package_id", "=", package.id),
127
+ ("lot_id", "=", lot.id),
128
+ ("state", "in", ("assigned", "partially_available")),
129
+ ]
130
+ return self.env["stock.move.line"].search(domain)
131
+
132
+ def _stock_correction_inventory_values(
133
+ self, move, location, package, lot, line_qty
134
+ ):
135
+ return {
136
+ "location_id": location.id,
137
+ "product_id": move.product_id.id,
138
+ "package_id": package.id,
139
+ "lot_id": lot.id,
140
+ "inventory_quantity": line_qty,
141
+ }
142
+
143
+ def _stock_issue_product_description(self, product, package, lot):
144
+ parts = []
145
+ if package:
146
+ parts.append(package.name)
147
+ parts.append(product.name)
148
+ if lot.name:
149
+ parts.append(_("Lot: ") + lot.name)
150
+ return " - ".join(parts)