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.
- odoo/addons/shopfloor/README.rst +160 -0
- odoo/addons/shopfloor/__init__.py +4 -0
- odoo/addons/shopfloor/__manifest__.py +65 -0
- odoo/addons/shopfloor/actions/__init__.py +15 -0
- odoo/addons/shopfloor/actions/change_package_lot.py +164 -0
- odoo/addons/shopfloor/actions/completion_info.py +42 -0
- odoo/addons/shopfloor/actions/data.py +329 -0
- odoo/addons/shopfloor/actions/data_detail.py +154 -0
- odoo/addons/shopfloor/actions/inventory.py +150 -0
- odoo/addons/shopfloor/actions/location_content_transfer_sorter.py +89 -0
- odoo/addons/shopfloor/actions/message.py +846 -0
- odoo/addons/shopfloor/actions/move_line_search.py +119 -0
- odoo/addons/shopfloor/actions/packaging.py +59 -0
- odoo/addons/shopfloor/actions/savepoint.py +44 -0
- odoo/addons/shopfloor/actions/schema.py +182 -0
- odoo/addons/shopfloor/actions/schema_detail.py +98 -0
- odoo/addons/shopfloor/actions/search.py +187 -0
- odoo/addons/shopfloor/actions/stock.py +239 -0
- odoo/addons/shopfloor/actions/stock_unreserve.py +66 -0
- odoo/addons/shopfloor/components/__init__.py +5 -0
- odoo/addons/shopfloor/components/scan_handler_location.py +26 -0
- odoo/addons/shopfloor/components/scan_handler_lot.py +26 -0
- odoo/addons/shopfloor/components/scan_handler_package.py +26 -0
- odoo/addons/shopfloor/components/scan_handler_product.py +26 -0
- odoo/addons/shopfloor/components/scan_handler_transfer.py +26 -0
- odoo/addons/shopfloor/data/shopfloor_scenario_data.xml +73 -0
- odoo/addons/shopfloor/demo/shopfloor_app_demo.xml +12 -0
- odoo/addons/shopfloor/demo/shopfloor_menu_demo.xml +64 -0
- odoo/addons/shopfloor/demo/shopfloor_profile_demo.xml +8 -0
- odoo/addons/shopfloor/demo/stock_picking_type_demo.xml +93 -0
- odoo/addons/shopfloor/docs/checkout_diag_seq.plantuml +61 -0
- odoo/addons/shopfloor/docs/checkout_diag_seq.png +0 -0
- odoo/addons/shopfloor/docs/cluster_picking_diag_seq.plantuml +112 -0
- odoo/addons/shopfloor/docs/cluster_picking_diag_seq.png +0 -0
- odoo/addons/shopfloor/docs/delivery_diag_seq.plantuml +56 -0
- odoo/addons/shopfloor/docs/delivery_diag_seq.png +0 -0
- odoo/addons/shopfloor/docs/location_content_transfer_diag_seq.plantuml +66 -0
- odoo/addons/shopfloor/docs/location_content_transfer_diag_seq.png +0 -0
- odoo/addons/shopfloor/docs/oca_logo.png +0 -0
- odoo/addons/shopfloor/docs/single_pack_transfer_diag_seq.plantuml +36 -0
- odoo/addons/shopfloor/docs/single_pack_transfer_diag_seq.png +0 -0
- odoo/addons/shopfloor/docs/zone_picking_diag_seq.plantuml +85 -0
- odoo/addons/shopfloor/docs/zone_picking_diag_seq.png +0 -0
- odoo/addons/shopfloor/exceptions.py +6 -0
- odoo/addons/shopfloor/i18n/ca.po +1802 -0
- odoo/addons/shopfloor/i18n/de.po +1791 -0
- odoo/addons/shopfloor/i18n/es_AR.po +2147 -0
- odoo/addons/shopfloor/i18n/pt_BR.po +1791 -0
- odoo/addons/shopfloor/i18n/shopfloor.pot +1877 -0
- odoo/addons/shopfloor/models/__init__.py +12 -0
- odoo/addons/shopfloor/models/priority_postpone_mixin.py +41 -0
- odoo/addons/shopfloor/models/shopfloor_app.py +9 -0
- odoo/addons/shopfloor/models/shopfloor_menu.py +436 -0
- odoo/addons/shopfloor/models/stock_location.py +76 -0
- odoo/addons/shopfloor/models/stock_move.py +119 -0
- odoo/addons/shopfloor/models/stock_move_line.py +307 -0
- odoo/addons/shopfloor/models/stock_package_level.py +50 -0
- odoo/addons/shopfloor/models/stock_picking.py +118 -0
- odoo/addons/shopfloor/models/stock_picking_batch.py +41 -0
- odoo/addons/shopfloor/models/stock_picking_type.py +26 -0
- odoo/addons/shopfloor/models/stock_quant.py +31 -0
- odoo/addons/shopfloor/models/stock_quant_package.py +101 -0
- odoo/addons/shopfloor/readme/CONTRIBUTORS.rst +18 -0
- odoo/addons/shopfloor/readme/CREDITS.rst +5 -0
- odoo/addons/shopfloor/readme/DESCRIPTION.rst +17 -0
- odoo/addons/shopfloor/readme/HISTORY.rst +4 -0
- odoo/addons/shopfloor/readme/ROADMAP.rst +4 -0
- odoo/addons/shopfloor/readme/USAGE.rst +6 -0
- odoo/addons/shopfloor/security/groups.xml +17 -0
- odoo/addons/shopfloor/services/__init__.py +16 -0
- odoo/addons/shopfloor/services/checkout.py +1763 -0
- odoo/addons/shopfloor/services/cluster_picking.py +1628 -0
- odoo/addons/shopfloor/services/delivery.py +828 -0
- odoo/addons/shopfloor/services/forms/__init__.py +1 -0
- odoo/addons/shopfloor/services/forms/picking_form.py +78 -0
- odoo/addons/shopfloor/services/location_content_transfer.py +1194 -0
- odoo/addons/shopfloor/services/menu.py +60 -0
- odoo/addons/shopfloor/services/picking_batch.py +126 -0
- odoo/addons/shopfloor/services/service.py +101 -0
- odoo/addons/shopfloor/services/single_pack_transfer.py +366 -0
- odoo/addons/shopfloor/services/zone_picking.py +1938 -0
- odoo/addons/shopfloor/static/description/icon.png +0 -0
- odoo/addons/shopfloor/static/description/index.html +500 -0
- odoo/addons/shopfloor/tests/__init__.py +83 -0
- odoo/addons/shopfloor/tests/common.py +324 -0
- odoo/addons/shopfloor/tests/models.py +29 -0
- odoo/addons/shopfloor/tests/test_actions_change_package_lot.py +1175 -0
- odoo/addons/shopfloor/tests/test_actions_data.py +376 -0
- odoo/addons/shopfloor/tests/test_actions_data_base.py +244 -0
- odoo/addons/shopfloor/tests/test_actions_data_detail.py +322 -0
- odoo/addons/shopfloor/tests/test_actions_search.py +248 -0
- odoo/addons/shopfloor/tests/test_actions_stock.py +48 -0
- odoo/addons/shopfloor/tests/test_checkout_auto_post.py +67 -0
- odoo/addons/shopfloor/tests/test_checkout_base.py +81 -0
- odoo/addons/shopfloor/tests/test_checkout_cancel_line.py +154 -0
- odoo/addons/shopfloor/tests/test_checkout_change_packaging.py +184 -0
- odoo/addons/shopfloor/tests/test_checkout_done.py +133 -0
- odoo/addons/shopfloor/tests/test_checkout_list_delivery_packaging.py +131 -0
- odoo/addons/shopfloor/tests/test_checkout_list_package.py +327 -0
- odoo/addons/shopfloor/tests/test_checkout_new_package.py +88 -0
- odoo/addons/shopfloor/tests/test_checkout_no_package.py +95 -0
- odoo/addons/shopfloor/tests/test_checkout_scan.py +174 -0
- odoo/addons/shopfloor/tests/test_checkout_scan_line.py +377 -0
- odoo/addons/shopfloor/tests/test_checkout_scan_line_base.py +25 -0
- odoo/addons/shopfloor/tests/test_checkout_scan_line_no_prefill_qty.py +91 -0
- odoo/addons/shopfloor/tests/test_checkout_scan_package_action.py +451 -0
- odoo/addons/shopfloor/tests/test_checkout_scan_package_action_no_prefill_qty.py +107 -0
- odoo/addons/shopfloor/tests/test_checkout_select.py +74 -0
- odoo/addons/shopfloor/tests/test_checkout_select_line.py +130 -0
- odoo/addons/shopfloor/tests/test_checkout_select_package_base.py +64 -0
- odoo/addons/shopfloor/tests/test_checkout_set_qty.py +257 -0
- odoo/addons/shopfloor/tests/test_checkout_summary.py +69 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_base.py +83 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_batch.py +109 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_change_pack_lot.py +111 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_is_zero.py +98 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_scan_destination.py +376 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_scan_destination_no_prefill_qty.py +115 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_scan_line.py +402 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_scan_line_location_or_pack_first.py +114 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_scan_line_no_prefill_qty.py +70 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_select.py +387 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_skip.py +90 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_stock_issue.py +364 -0
- odoo/addons/shopfloor/tests/test_cluster_picking_unload.py +911 -0
- odoo/addons/shopfloor/tests/test_delivery_base.py +155 -0
- odoo/addons/shopfloor/tests/test_delivery_done.py +108 -0
- odoo/addons/shopfloor/tests/test_delivery_list_stock_picking.py +49 -0
- odoo/addons/shopfloor/tests/test_delivery_reset_qty_done_line.py +119 -0
- odoo/addons/shopfloor/tests/test_delivery_reset_qty_done_pack.py +107 -0
- odoo/addons/shopfloor/tests/test_delivery_scan_deliver.py +557 -0
- odoo/addons/shopfloor/tests/test_delivery_select.py +38 -0
- odoo/addons/shopfloor/tests/test_delivery_set_qty_done_line.py +91 -0
- odoo/addons/shopfloor/tests/test_delivery_set_qty_done_pack.py +135 -0
- odoo/addons/shopfloor/tests/test_delivery_sublocation.py +180 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_base.py +136 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_get_work.py +125 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_mix.py +509 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_putaway.py +143 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_scan_location.py +34 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_set_destination_all.py +343 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_set_destination_package_or_line.py +1074 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_single.py +748 -0
- odoo/addons/shopfloor/tests/test_location_content_transfer_start.py +359 -0
- odoo/addons/shopfloor/tests/test_menu_base.py +261 -0
- odoo/addons/shopfloor/tests/test_menu_counters.py +61 -0
- odoo/addons/shopfloor/tests/test_misc.py +25 -0
- odoo/addons/shopfloor/tests/test_move_action_assign.py +87 -0
- odoo/addons/shopfloor/tests/test_openapi.py +21 -0
- odoo/addons/shopfloor/tests/test_picking_form.py +62 -0
- odoo/addons/shopfloor/tests/test_scan_anything.py +49 -0
- odoo/addons/shopfloor/tests/test_single_pack_transfer.py +1121 -0
- odoo/addons/shopfloor/tests/test_single_pack_transfer_base.py +32 -0
- odoo/addons/shopfloor/tests/test_single_pack_transfer_putaway.py +104 -0
- odoo/addons/shopfloor/tests/test_stock_split.py +204 -0
- odoo/addons/shopfloor/tests/test_user.py +42 -0
- odoo/addons/shopfloor/tests/test_zone_picking_base.py +608 -0
- odoo/addons/shopfloor/tests/test_zone_picking_change_pack_lot.py +140 -0
- odoo/addons/shopfloor/tests/test_zone_picking_select_line.py +723 -0
- odoo/addons/shopfloor/tests/test_zone_picking_select_line_first_scan_location.py +207 -0
- odoo/addons/shopfloor/tests/test_zone_picking_select_line_first_scan_location.py.bak +202 -0
- odoo/addons/shopfloor/tests/test_zone_picking_select_line_no_prefill_qty.py +107 -0
- odoo/addons/shopfloor/tests/test_zone_picking_select_picking_type.py +26 -0
- odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination.py +643 -0
- odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination_no_prefill_qty.py +146 -0
- odoo/addons/shopfloor/tests/test_zone_picking_set_line_destination_pick_pack.py +241 -0
- odoo/addons/shopfloor/tests/test_zone_picking_start.py +206 -0
- odoo/addons/shopfloor/tests/test_zone_picking_stock_issue.py +121 -0
- odoo/addons/shopfloor/tests/test_zone_picking_unload_all.py +353 -0
- odoo/addons/shopfloor/tests/test_zone_picking_unload_buffer_lines.py +113 -0
- odoo/addons/shopfloor/tests/test_zone_picking_unload_set_destination.py +374 -0
- odoo/addons/shopfloor/tests/test_zone_picking_unload_single.py +123 -0
- odoo/addons/shopfloor/tests/test_zone_picking_zero_check.py +43 -0
- odoo/addons/shopfloor/utils.py +13 -0
- odoo/addons/shopfloor/views/shopfloor_menu.xml +167 -0
- odoo/addons/shopfloor/views/stock_location.xml +20 -0
- odoo/addons/shopfloor/views/stock_move_line.xml +52 -0
- odoo/addons/shopfloor/views/stock_picking_type.xml +19 -0
- odoo_addon_shopfloor-16.0.1.0.0.24.dist-info/METADATA +192 -0
- odoo_addon_shopfloor-16.0.1.0.0.24.dist-info/RECORD +182 -0
- odoo_addon_shopfloor-16.0.1.0.0.24.dist-info/WHEEL +5 -0
- 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
|
+
}
|