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,60 @@
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.addons.base_rest.components.service import to_int
4
+ from odoo.addons.component.core import Component
5
+
6
+
7
+ class ShopfloorMenu(Component):
8
+ _inherit = "shopfloor.service.menu"
9
+
10
+ def _convert_one_record(self, record):
11
+ values = super()._convert_one_record(record)
12
+ if record.picking_type_ids:
13
+ counters = self._get_move_line_counters(record)
14
+ values.update(counters)
15
+ return values
16
+
17
+ def _get_move_line_counters(self, record):
18
+ """Lookup for all lines per menu item and compute counters."""
19
+ # TODO: maybe to be improved w/ raw SQL as this run for each menu item
20
+ # and it's called every time the menu is opened/gets refreshed
21
+ move_line_search = self._actions_for(
22
+ "search_move_line", picking_types=record.picking_type_ids
23
+ )
24
+ locations = record.picking_type_ids.mapped("default_location_src_id")
25
+ lines_per_menu = move_line_search.search_move_lines_by_location(locations)
26
+ return move_line_search.counters_for_lines(lines_per_menu)
27
+
28
+ def _one_record_parser(self, record):
29
+ parser = super()._one_record_parser(record)
30
+ if not record.picking_type_ids:
31
+ return parser
32
+ return parser + [
33
+ ("picking_type_ids:picking_types", ["id", "name"]),
34
+ ]
35
+
36
+
37
+ class ShopfloorMenuValidatorResponse(Component):
38
+ """Validators for the Menu endpoints responses"""
39
+
40
+ _inherit = "shopfloor.service.menu.validator.response"
41
+
42
+ @property
43
+ def _record_schema(self):
44
+ schema = super()._record_schema
45
+ schema.update(
46
+ {
47
+ "picking_types": self.schemas._schema_list_of(
48
+ self._picking_type_schema, required=False, nullable=True
49
+ )
50
+ }
51
+ )
52
+ schema.update(self.schemas.move_lines_counters())
53
+ return schema
54
+
55
+ @property
56
+ def _picking_type_schema(self):
57
+ return {
58
+ "id": {"coerce": to_int, "required": True, "type": "integer"},
59
+ "name": {"type": "string", "nullable": False, "required": True},
60
+ }
@@ -0,0 +1,126 @@
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.osv import expression
4
+
5
+ from odoo.addons.component.core import Component
6
+
7
+
8
+ class PickingBatch(Component):
9
+ """Picking Batch services for the client application."""
10
+
11
+ _inherit = "base.shopfloor.service"
12
+ _name = "shopfloor.picking.batch"
13
+ _usage = "picking_batch"
14
+ _expose_model = "stock.picking.batch"
15
+ _description = __doc__
16
+
17
+ def _get_base_search_domain(self):
18
+ base_domain = super()._get_base_search_domain()
19
+ user = self.env.user
20
+ return expression.AND(
21
+ [
22
+ base_domain,
23
+ [
24
+ "|",
25
+ "&",
26
+ ("user_id", "=", False),
27
+ ("state", "=", "draft"),
28
+ "&",
29
+ ("user_id", "=", user.id),
30
+ ("state", "in", ("draft", "in_progress")),
31
+ ],
32
+ ]
33
+ )
34
+
35
+ def _search(self, name_fragment=None, batch_ids=None):
36
+ domain = self._get_base_search_domain()
37
+ if name_fragment:
38
+ domain = expression.AND([domain, [("name", "ilike", name_fragment)]])
39
+ if batch_ids:
40
+ domain = expression.AND([domain, [("id", "in", batch_ids)]])
41
+ records = self.env[self._expose_model].search(domain, order="id asc")
42
+ records = records.filtered(
43
+ # Include done/cancel because we want to be able to work on the
44
+ # batch even if some pickings are done/canceled. They'll should be
45
+ # ignored later.
46
+ lambda batch: all(
47
+ (
48
+ # When the batch is already in progress, we do not care
49
+ # about state of the pickings, because we want to be able
50
+ # to recover it in any case, even if, for instance, a stock
51
+ # error changed a picking to unavailable after the user
52
+ # started to work on the batch.
53
+ batch.state == "in_progress"
54
+ or picking.state in ("assigned", "done", "cancel")
55
+ )
56
+ and picking.picking_type_id in self.picking_types
57
+ for picking in batch.picking_ids
58
+ )
59
+ )
60
+ return records
61
+
62
+ def search(self, name_fragment=None):
63
+ """List available stock picking batches for current user
64
+
65
+ Show only picking batches where all the pickings are available and
66
+ where all pickings are in the picking type of the current scenario.
67
+ """
68
+ records = self._search(name_fragment=name_fragment)
69
+ return self._response(
70
+ data={"size": len(records), "records": self._to_json(records)}
71
+ )
72
+
73
+ def _convert_one_record(self, record):
74
+ assigned_pickings = record.picking_ids.filtered(
75
+ lambda picking: picking.state == "assigned"
76
+ )
77
+ return {
78
+ "id": record.id,
79
+ "name": record.name,
80
+ "picking_count": len(assigned_pickings),
81
+ "move_line_count": len(assigned_pickings.mapped("move_line_ids")),
82
+ "weight": record.total_weight(),
83
+ }
84
+
85
+
86
+ class ShopfloorPickingBatchValidator(Component):
87
+ """Validators for the Picking_Batch endpoints"""
88
+
89
+ _inherit = "base.shopfloor.validator"
90
+ _name = "shopfloor.picking.batch.validator"
91
+ _usage = "picking_batch.validator"
92
+
93
+ def search(self):
94
+ return {
95
+ "name_fragment": {"type": "string", "nullable": True, "required": False}
96
+ }
97
+
98
+
99
+ class ShopfloorPickingBatchValidatorResponse(Component):
100
+ """Validators for the Picking_Batch endpoints responses"""
101
+
102
+ _inherit = "base.shopfloor.validator.response"
103
+ _name = "shopfloor.picking.batch.validator.response"
104
+ _usage = "picking_batch.validator.response"
105
+
106
+ def search(self):
107
+ return self._response_schema(
108
+ {
109
+ "size": {"required": True, "type": "integer"},
110
+ "records": {
111
+ "type": "list",
112
+ "required": True,
113
+ "schema": {"type": "dict", "schema": self._record_schema},
114
+ },
115
+ }
116
+ )
117
+
118
+ @property
119
+ def _record_schema(self):
120
+ return {
121
+ "id": {"required": True, "type": "integer"},
122
+ "name": {"type": "string", "nullable": False, "required": True},
123
+ "picking_count": {"required": True, "type": "integer"},
124
+ "move_line_count": {"required": True, "type": "integer"},
125
+ "weight": {"required": True, "nullable": True, "type": "float"},
126
+ }
@@ -0,0 +1,101 @@
1
+ # Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
2
+ # Copyright 2020 Akretion (http://www.akretion.com)
3
+ # Copyright 2020-2021 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
4
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
5
+ from odoo import _, exceptions
6
+
7
+ from odoo.addons.component.core import AbstractComponent
8
+
9
+
10
+ class BaseShopfloorService(AbstractComponent):
11
+ """Base class for REST services"""
12
+
13
+ _inherit = "base.shopfloor.service"
14
+
15
+ @property
16
+ def search_move_line(self):
17
+ # TODO: propagating `picking_types` should probably be default
18
+ return self._actions_for("search_move_line", propagate_kwargs=["picking_types"])
19
+
20
+
21
+ class BaseShopfloorProcess(AbstractComponent):
22
+
23
+ _inherit = "base.shopfloor.process"
24
+
25
+ def _get_process_picking_types(self):
26
+ """Return picking types for the menu"""
27
+ return self.work.menu.picking_type_ids
28
+
29
+ @property
30
+ def picking_types(self):
31
+ if not hasattr(self.work, "picking_types"):
32
+ self.work.picking_types = self._get_process_picking_types()
33
+ if not self.work.picking_types:
34
+ raise exceptions.UserError(
35
+ _("No operation types configured on menu {}.").format(
36
+ self.work.menu.name
37
+ )
38
+ )
39
+ return self.work.picking_types
40
+
41
+ @property
42
+ def search_move_line(self):
43
+ # TODO: picking types should be set somehow straight in the work context
44
+ # by `_validate_headers_update_work_context` in this way
45
+ # we can remove this override and the need to call `_get_process_picking_types`
46
+ # every time.
47
+ return self._actions_for("search_move_line", picking_types=self.picking_types)
48
+
49
+ def _check_picking_status(self, pickings, states=("assigned",)):
50
+ """Check if given pickings can be processed.
51
+
52
+ If the picking is already done, canceled or didn't belong to the
53
+ expected picking type, a message is returned.
54
+ """
55
+ for picking in pickings:
56
+ if not picking.exists():
57
+ return self.msg_store.stock_picking_not_found()
58
+ if picking.state == "done":
59
+ return self.msg_store.already_done()
60
+ if picking.state not in states: # the picking must be ready
61
+ return self.msg_store.stock_picking_not_available(picking)
62
+ if picking.picking_type_id not in self.picking_types:
63
+ return self.msg_store.cannot_move_something_in_picking_type()
64
+
65
+ def is_src_location_valid(self, location):
66
+ """Check the source location is valid for given process.
67
+
68
+ We ensure the source is valid regarding one of the picking types of the
69
+ process.
70
+ """
71
+ return location.is_sublocation_of(self.picking_types.default_location_src_id)
72
+
73
+ def is_dest_location_valid(self, moves, location):
74
+ """Check the destination location is valid for given moves.
75
+
76
+ We ensure the destination is either valid regarding the picking
77
+ destination location or the move destination location. With the push
78
+ rules in the module stock_dynamic_routing in OCA/wms, it is possible
79
+ that the move destination is not anymore a child of the picking default
80
+ destination (as it is the last pushed move that now respects this
81
+ condition and not anymore this one that has a destination to an
82
+ intermediate location)
83
+ """
84
+ return location.is_sublocation_of(
85
+ moves.picking_id.location_dest_id, func=all
86
+ ) or location.is_sublocation_of(moves.location_dest_id, func=all)
87
+
88
+ def is_dest_location_to_confirm(self, location_dest_id, location):
89
+ """Check the destination location requires confirmation
90
+
91
+ The location is valid but not the expected one: ask for confirmation
92
+ """
93
+ return not location.is_sublocation_of(location_dest_id)
94
+
95
+ def is_allow_move_create(self):
96
+ """Check a new operation can be created
97
+
98
+ The menu is configured to allow the creation of moves
99
+ The menu is bind to one picking type
100
+ """
101
+ return self.work.menu.allow_move_create and len(self.picking_types) == 1
@@ -0,0 +1,366 @@
1
+ # Copyright 2020-2021 Camptocamp SA (http://www.camptocamp.com)
2
+ # Copyright 2020-2021 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
3
+ # Copyright 2020 Akretion (http://www.akretion.com)
4
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
5
+ from odoo import fields
6
+
7
+ from odoo.addons.base_rest.components.service import to_int
8
+ from odoo.addons.component.core import Component
9
+
10
+
11
+ class SinglePackTransfer(Component):
12
+ """Methods for the Single Pack Transfer Process
13
+
14
+ You will find a sequence diagram describing states and endpoints
15
+ relationships [here](../docs/single_pack_transfer_diag_seq.png).
16
+ Keep [the sequence diagram](../docs/single_pack_transfer_diag_seq.plantuml)
17
+ up-to-date if you change endpoints.
18
+ """
19
+
20
+ _inherit = "base.shopfloor.process"
21
+ _name = "shopfloor.single.pack.transfer"
22
+ _usage = "single_pack_transfer"
23
+ _description = __doc__
24
+
25
+ def _data_after_package_scanned(self, package_level):
26
+ move_lines = package_level.move_line_ids
27
+ package = package_level.package_id
28
+ # TODO use data.package_level (but the "name" moves in "package.name")
29
+ return {
30
+ "id": package_level.id,
31
+ "name": package.name,
32
+ "location_src": self.data.location(package.location_id),
33
+ "location_dest": self.data.location(package_level.location_dest_id),
34
+ "products": self.data.products(move_lines.product_id),
35
+ "picking": self.data.picking(move_lines.picking_id),
36
+ }
37
+
38
+ def _response_for_start(self, message=None, popup=None):
39
+ return self._response(next_state="start", message=message, popup=popup)
40
+
41
+ def _response_for_confirm_start(self, package_level, message=None):
42
+ data = self._data_after_package_scanned(package_level)
43
+ data["confirmation_required"] = True
44
+ return self._response(
45
+ next_state="start",
46
+ data=data,
47
+ message=message,
48
+ )
49
+
50
+ def _response_for_scan_location(
51
+ self, package_level, message=None, confirmation_required=False
52
+ ):
53
+ data = self._data_after_package_scanned(package_level)
54
+ data["confirmation_required"] = confirmation_required
55
+ return self._response(
56
+ next_state="scan_location",
57
+ data=data,
58
+ message=message,
59
+ )
60
+
61
+ def _scan_source(self, barcode, confirmation=False):
62
+ """Search a package"""
63
+ search = self._actions_for("search")
64
+ location = search.location_from_scan(barcode)
65
+
66
+ package = self.env["stock.quant.package"]
67
+ if location:
68
+ package = self.env["stock.quant.package"].search(
69
+ [("location_id", "=", location.id)]
70
+ )
71
+ if not package:
72
+ return (self.msg_store.no_pack_in_location(location), None)
73
+ if len(package) > 1:
74
+ return (self.msg_store.several_packs_in_location(location), None)
75
+
76
+ if not package:
77
+ package = search.package_from_scan(barcode)
78
+
79
+ if not package:
80
+ return (self.msg_store.package_not_found_for_barcode(barcode), None)
81
+ if not package.location_id:
82
+ return (self.msg_store.package_has_no_product_to_take(barcode), None)
83
+ if not self.is_src_location_valid(package.location_id):
84
+ return (
85
+ self.msg_store.package_not_allowed_in_src_location(
86
+ barcode, self.picking_types
87
+ ),
88
+ None,
89
+ )
90
+
91
+ return (None, package)
92
+
93
+ def start(self, barcode, confirmation=False):
94
+ picking_types = self.picking_types
95
+ message, package = self._scan_source(barcode, confirmation)
96
+ if message:
97
+ return self._response_for_start(message=message)
98
+ package_level = self.env["stock.package_level"].search(
99
+ [
100
+ ("package_id", "=", package.id),
101
+ ("picking_id.picking_type_id", "in", picking_types.ids),
102
+ ]
103
+ )
104
+
105
+ # Start a savepoint because we are may unreserve moves of other
106
+ # picking types. If we do and we can't create a package level after,
107
+ # we rollback to the initial state
108
+ savepoint = self._actions_for("savepoint").new()
109
+ unreserved_moves = self.env["stock.move"].browse()
110
+ if not package_level:
111
+ other_move_lines = self.env["stock.move.line"].search(
112
+ [
113
+ ("package_id", "=", package.id),
114
+ # to exclude canceled and done
115
+ ("state", "in", ("assigned", "partially_available")),
116
+ ]
117
+ )
118
+ if any(line.qty_done > 0 for line in other_move_lines) or (
119
+ other_move_lines and not self.work.menu.allow_unreserve_other_moves
120
+ ):
121
+ picking = fields.first(other_move_lines).picking_id
122
+ return self._response_for_start(
123
+ message=self.msg_store.package_already_picked_by(package, picking)
124
+ )
125
+ elif other_move_lines and self.work.menu.allow_unreserve_other_moves:
126
+
127
+ unreserved_moves = other_move_lines.move_id
128
+ other_package_levels = other_move_lines.package_level_id
129
+ other_package_levels.explode_package()
130
+ unreserved_moves._do_unreserve()
131
+
132
+ # State is computed, can't use it in the domain. And it's probably faster
133
+ # to filter here rather than using a domain on "picking_id.state" that would
134
+ # use a sub-search on stock.picking: we shouldn't have dozens of package levels
135
+ # for a package.
136
+ package_level = package_level.filtered(
137
+ lambda pl: pl.state not in ("cancel", "done", "draft")
138
+ )
139
+ message = self.msg_store.no_pending_operation_for_pack(package)
140
+ if not package_level and self.is_allow_move_create():
141
+ package_level = self._create_package_level(package)
142
+ if not self.is_dest_location_valid(
143
+ package_level.move_line_ids.move_id, package_level.location_dest_id
144
+ ):
145
+ package_level = None
146
+ savepoint.rollback()
147
+ message = self.msg_store.package_unable_to_transfer(package)
148
+
149
+ if not package_level:
150
+ # restore any unreserved move/package level
151
+ savepoint.rollback()
152
+ return self._response_for_start(message=message)
153
+ stock = self._actions_for("stock")
154
+ if self.work.menu.ignore_no_putaway_available and stock.no_putaway_available(
155
+ self.picking_types, package_level.move_line_ids
156
+ ):
157
+ # the putaway created a move line but no putaway was possible, so revert
158
+ # to the initial state
159
+ savepoint.rollback()
160
+ return self._response_for_start(
161
+ message=self.msg_store.no_putaway_destination_available()
162
+ )
163
+
164
+ if package_level.is_done and not confirmation:
165
+ return self._response_for_confirm_start(
166
+ package_level, message=self.msg_store.already_running_ask_confirmation()
167
+ )
168
+ if not package_level.is_done:
169
+ package_level.is_done = True
170
+
171
+ unreserved_moves._action_assign()
172
+
173
+ savepoint.release()
174
+
175
+ return self._response_for_scan_location(package_level)
176
+
177
+ def _create_package_level(self, package):
178
+ # this method can be called only if we have one picking type
179
+ # (allow_move_create==True on menu)
180
+ assert self.picking_types.ensure_one()
181
+ StockPicking = self.env["stock.picking"].with_context(
182
+ default_picking_type_id=self.picking_types.id
183
+ )
184
+ picking = StockPicking.create({})
185
+ package_level = self.env["stock.package_level"].create(
186
+ {
187
+ "picking_id": picking.id,
188
+ "package_id": package.id,
189
+ "location_dest_id": picking.location_dest_id.id,
190
+ "company_id": self.env.company.id,
191
+ }
192
+ )
193
+ picking.action_confirm()
194
+ picking.action_assign()
195
+ # For packages that contain several products (so linked to several
196
+ # moves), the putaway destination computation of the strategy
197
+ # triggered by `action_assign()` above won't work, so we trigger
198
+ # the computation manually here at the package level.
199
+ package_level.recompute_pack_putaway()
200
+ return package_level
201
+
202
+ def _is_move_state_valid(self, moves):
203
+ return all(move.state != "cancel" for move in moves)
204
+
205
+ def validate(self, package_level_id, location_barcode, confirmation=False):
206
+ """Validate the transfer"""
207
+ search = self._actions_for("search")
208
+
209
+ package_level = self.env["stock.package_level"].browse(package_level_id)
210
+ if not package_level.exists():
211
+ return self._response_for_start(
212
+ message=self.msg_store.operation_not_found()
213
+ )
214
+
215
+ # Do not use package_level.move_lines, this is only filled in when the
216
+ # moves have been created from a manually encoded package level, not
217
+ # when a package has been reserved for existing moves
218
+ moves = package_level.move_line_ids.move_id
219
+ if not self._is_move_state_valid(moves):
220
+ return self._response_for_start(
221
+ message=self.msg_store.operation_has_been_canceled_elsewhere()
222
+ )
223
+
224
+ scanned_location = search.location_from_scan(location_barcode)
225
+ if not scanned_location:
226
+ return self._response_for_scan_location(
227
+ package_level, message=self.msg_store.no_location_found()
228
+ )
229
+
230
+ if not self.is_dest_location_valid(moves, scanned_location):
231
+ return self._response_for_scan_location(
232
+ package_level, message=self.msg_store.dest_location_not_allowed()
233
+ )
234
+
235
+ if not confirmation and self.is_dest_location_to_confirm(
236
+ package_level.location_dest_id, scanned_location
237
+ ):
238
+ return self._response_for_scan_location(
239
+ package_level,
240
+ confirmation_required=True,
241
+ message=self.msg_store.confirm_location_changed(
242
+ package_level.location_dest_id, scanned_location
243
+ ),
244
+ )
245
+
246
+ self._set_destination_and_done(package_level, scanned_location)
247
+ return self._router_validate_success(package_level)
248
+
249
+ def _is_last_move(self, move):
250
+ return move.picking_id.completion_info == "next_picking_ready"
251
+
252
+ def _router_validate_success(self, package_level):
253
+ move = package_level.move_line_ids.move_id
254
+
255
+ message = self.msg_store.confirm_pack_moved()
256
+
257
+ completion_info_popup = None
258
+ if self._is_last_move(move):
259
+ completion_info = self._actions_for("completion.info")
260
+ completion_info_popup = completion_info.popup(package_level.move_line_ids)
261
+ return self._response_for_start(message=message, popup=completion_info_popup)
262
+
263
+ def _set_destination_and_done(self, package_level, scanned_location):
264
+ # when writing the destination on the package level, it writes
265
+ # on the move lines
266
+ package_level.location_dest_id = scanned_location
267
+ stock = self._actions_for("stock")
268
+ stock.put_package_level_in_move(package_level)
269
+ stock.validate_moves(package_level.move_line_ids.move_id)
270
+
271
+ def cancel(self, package_level_id):
272
+ package_level = self.env["stock.package_level"].browse(package_level_id)
273
+ if not package_level.exists():
274
+ return self._response_for_start(
275
+ message=self.msg_store.operation_not_found()
276
+ )
277
+ # package.move_lines may be empty, it seems
278
+ moves = package_level.move_ids | package_level.move_line_ids.move_id
279
+ if "done" in moves.mapped("state"):
280
+ return self._response_for_start(message=self.msg_store.already_done())
281
+
282
+ package_level.is_done = False
283
+ return self._response_for_start(
284
+ message=self.msg_store.confirm_canceled_scan_next_pack()
285
+ )
286
+
287
+
288
+ class SinglePackTransferValidator(Component):
289
+ """Validators for Single Pack Transfer methods"""
290
+
291
+ _inherit = "base.shopfloor.validator"
292
+ _name = "shopfloor.single.pack.transfer.validator"
293
+ _usage = "single_pack_transfer.validator"
294
+
295
+ def start(self):
296
+ return {
297
+ "barcode": {"type": "string", "nullable": False, "required": True},
298
+ "confirmation": {"type": "boolean", "required": False},
299
+ }
300
+
301
+ def cancel(self):
302
+ return {
303
+ "package_level_id": {"coerce": to_int, "required": True, "type": "integer"}
304
+ }
305
+
306
+ def validate(self):
307
+ return {
308
+ "package_level_id": {"coerce": to_int, "required": True, "type": "integer"},
309
+ "location_barcode": {"type": "string", "nullable": False, "required": True},
310
+ "confirmation": {"type": "boolean", "required": False},
311
+ }
312
+
313
+
314
+ class SinglePackTransferValidatorResponse(Component):
315
+ """Validators for Single Pack Transfer methods responses"""
316
+
317
+ _inherit = "base.shopfloor.validator.response"
318
+ _name = "shopfloor.single.pack.transfer.validator.response"
319
+ _usage = "single_pack_transfer.validator.response"
320
+
321
+ def _states(self):
322
+ """List of possible next states
323
+
324
+ With the schema of the data send to the client to transition
325
+ to the next state.
326
+ """
327
+ schema_for_start = self._schema_for_package_level_details()
328
+ schema_for_start.update(self._schema_confirmation_required())
329
+ schema_for_scan_location = self._schema_for_package_level_details(required=True)
330
+ schema_for_scan_location.update(self._schema_confirmation_required())
331
+ return {
332
+ "start": schema_for_start,
333
+ "scan_location": schema_for_scan_location,
334
+ }
335
+
336
+ def start(self):
337
+ return self._response_schema(next_states={"start", "scan_location"})
338
+
339
+ def cancel(self):
340
+ return self._response_schema(next_states={"start"})
341
+
342
+ def validate(self):
343
+ return self._response_schema(next_states={"scan_location", "start"})
344
+
345
+ def _schema_for_package_level_details(self, required=False):
346
+ # TODO use schemas.package_level (but the "name" moves in "package.name")
347
+ return {
348
+ "id": {"required": required, "type": "integer"},
349
+ "name": {"type": "string", "nullable": False, "required": required},
350
+ "location_src": {"type": "dict", "schema": self.schemas.location()},
351
+ "location_dest": {"type": "dict", "schema": self.schemas.location()},
352
+ "products": {
353
+ "type": "list",
354
+ "schema": {"type": "dict", "schema": self.schemas.product()},
355
+ },
356
+ "picking": {"type": "dict", "schema": self.schemas.picking()},
357
+ }
358
+
359
+ def _schema_confirmation_required(self):
360
+ return {
361
+ "confirmation_required": {
362
+ "type": "boolean",
363
+ "nullable": True,
364
+ "required": False,
365
+ },
366
+ }