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