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,119 @@
|
|
1
|
+
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
|
2
|
+
# Copyright 2022 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
3
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
4
|
+
from odoo import _, models
|
5
|
+
from odoo.tools.float_utils import float_compare
|
6
|
+
|
7
|
+
|
8
|
+
class StockMove(models.Model):
|
9
|
+
_inherit = "stock.move"
|
10
|
+
|
11
|
+
def _qty_is_satisfied(self):
|
12
|
+
compare = float_compare(
|
13
|
+
self.quantity_done,
|
14
|
+
self.product_uom_qty,
|
15
|
+
precision_rounding=self.product_uom.rounding,
|
16
|
+
)
|
17
|
+
# greater or equal
|
18
|
+
return compare in (0, 1)
|
19
|
+
|
20
|
+
def split_other_move_lines(self, move_lines, intersection=False):
|
21
|
+
"""Substract `move_lines` from `move.move_line_ids`, put the result
|
22
|
+
in a new move and returns it.
|
23
|
+
|
24
|
+
If `intersection` is set to `True`, this is the common lines between
|
25
|
+
`move_lines` and `move.move_line_ids` which will be put in a new move.
|
26
|
+
"""
|
27
|
+
self.ensure_one()
|
28
|
+
other_move_lines = self.move_line_ids - move_lines
|
29
|
+
if intersection:
|
30
|
+
to_move = self.move_line_ids & move_lines
|
31
|
+
else:
|
32
|
+
to_move = other_move_lines
|
33
|
+
if other_move_lines or self.state == "partially_available":
|
34
|
+
if intersection:
|
35
|
+
qty_to_split = sum(to_move.mapped("reserved_uom_qty"))
|
36
|
+
else:
|
37
|
+
qty_to_split = self.product_uom_qty - sum(
|
38
|
+
move_lines.mapped("reserved_uom_qty")
|
39
|
+
)
|
40
|
+
split_move_vals = self._split(qty_to_split)
|
41
|
+
split_move = self.create(split_move_vals)
|
42
|
+
split_move.move_line_ids = to_move
|
43
|
+
split_move._action_confirm(merge=False)
|
44
|
+
split_move._recompute_state()
|
45
|
+
split_move._action_assign()
|
46
|
+
self._recompute_state()
|
47
|
+
return split_move
|
48
|
+
return self.browse()
|
49
|
+
|
50
|
+
def split_unavailable_qty(self):
|
51
|
+
"""Put unavailable qty of a partially available move in their own
|
52
|
+
move (which will be 'confirmed').
|
53
|
+
"""
|
54
|
+
partial_moves = self.filtered(lambda m: m.state == "partially_available")
|
55
|
+
for partial_move in partial_moves:
|
56
|
+
partial_move.split_other_move_lines(partial_move.move_line_ids)
|
57
|
+
return partial_moves
|
58
|
+
|
59
|
+
def _extract_in_split_order(self, default=None, backorder=False):
|
60
|
+
"""Extract moves in a new picking
|
61
|
+
|
62
|
+
:param default: dictionary of field values to override in the original
|
63
|
+
values of the copied record
|
64
|
+
:param backorder: indicate if the original picking can be seen as a
|
65
|
+
backorder after the split. You could apply a specific backorder
|
66
|
+
strategy (e.g. cancel it).
|
67
|
+
:return: the new order
|
68
|
+
"""
|
69
|
+
picking = self.picking_id
|
70
|
+
picking.ensure_one()
|
71
|
+
data = {
|
72
|
+
"name": "/",
|
73
|
+
"move_ids": [],
|
74
|
+
"move_line_ids": [],
|
75
|
+
"backorder_id": picking.id,
|
76
|
+
}
|
77
|
+
data.update(dict(default or []))
|
78
|
+
new_picking = picking.copy(data)
|
79
|
+
link = '<a href="#" data-oe-model="stock.picking" data-oe-id="%d">%s</a>' % (
|
80
|
+
new_picking.id,
|
81
|
+
new_picking.name,
|
82
|
+
)
|
83
|
+
message = (_("The split order {} has been created.")).format(link)
|
84
|
+
picking.message_post(body=message)
|
85
|
+
self.picking_id = new_picking.id
|
86
|
+
self.package_level_id.picking_id = new_picking.id
|
87
|
+
self.move_line_ids.picking_id = new_picking.id
|
88
|
+
self.move_line_ids.package_level_id.picking_id = new_picking.id
|
89
|
+
self._action_assign()
|
90
|
+
return new_picking
|
91
|
+
|
92
|
+
def extract_and_action_done(self):
|
93
|
+
"""Extract the moves in a separate transfer and validate them.
|
94
|
+
|
95
|
+
You can combine this method with `split_other_move_lines` method
|
96
|
+
to first extract some move lines in a separate move, then validate it
|
97
|
+
with this method.
|
98
|
+
"""
|
99
|
+
# Process assigned moves
|
100
|
+
moves = self.filtered(lambda m: m.state == "assigned")
|
101
|
+
if not moves:
|
102
|
+
return False
|
103
|
+
new_backorders = self.env["stock.picking"]
|
104
|
+
for picking in moves.picking_id:
|
105
|
+
existing_backorders = picking.backorder_ids
|
106
|
+
moves_todo = picking.move_ids & moves
|
107
|
+
# No need to create a new transfer if we are processing all moves
|
108
|
+
if moves_todo == picking.move_ids:
|
109
|
+
new_picking = picking
|
110
|
+
# We process some available moves of the picking, but there are still
|
111
|
+
# some other moves to process, then we put the moves to process in
|
112
|
+
# a new transfer to validate. All remaining moves stay in the
|
113
|
+
# current transfer.
|
114
|
+
else:
|
115
|
+
new_picking = moves_todo._extract_in_split_order()
|
116
|
+
assert new_picking.state == "assigned"
|
117
|
+
new_picking._action_done()
|
118
|
+
new_backorders |= new_picking.backorder_ids - existing_backorders
|
119
|
+
return new_backorders
|
@@ -0,0 +1,307 @@
|
|
1
|
+
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
|
2
|
+
# Copyright 2022 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
3
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
4
|
+
import logging
|
5
|
+
|
6
|
+
from odoo import _, exceptions, fields, models
|
7
|
+
from odoo.exceptions import UserError
|
8
|
+
from odoo.tools.float_utils import float_compare, float_is_zero
|
9
|
+
|
10
|
+
_logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class StockMoveLine(models.Model):
|
14
|
+
_name = "stock.move.line"
|
15
|
+
_inherit = ["stock.move.line", "shopfloor.priority.postpone.mixin"]
|
16
|
+
|
17
|
+
# TODO use a serialized field
|
18
|
+
shopfloor_unloaded = fields.Boolean(default=False)
|
19
|
+
shopfloor_checkout_done = fields.Boolean(default=False)
|
20
|
+
shopfloor_user_id = fields.Many2one(comodel_name="res.users", index=True)
|
21
|
+
|
22
|
+
date_planned = fields.Datetime(related="move_id.date", store=True, index=True)
|
23
|
+
|
24
|
+
# we search lines based on their location in some workflows
|
25
|
+
location_id = fields.Many2one(index=True)
|
26
|
+
package_id = fields.Many2one(index=True)
|
27
|
+
|
28
|
+
# allow domain on picking_id.xxx without too much perf penalty
|
29
|
+
picking_id = fields.Many2one(auto_join=True)
|
30
|
+
|
31
|
+
def _split_partial_quantity(self):
|
32
|
+
"""Create new move line for the quantity remaining to do
|
33
|
+
|
34
|
+
:return: the new move line if created else empty recordset
|
35
|
+
"""
|
36
|
+
self.ensure_one()
|
37
|
+
rounding = self.product_uom_id.rounding
|
38
|
+
if float_is_zero(self.qty_done, precision_rounding=rounding):
|
39
|
+
return self.browse()
|
40
|
+
compare = float_compare(
|
41
|
+
self.qty_done, self.reserved_uom_qty, precision_rounding=rounding
|
42
|
+
)
|
43
|
+
qty_lesser = compare == -1
|
44
|
+
qty_greater = compare == 1
|
45
|
+
assert not qty_greater, "Quantity done cannot exceed quantity to do"
|
46
|
+
if qty_lesser:
|
47
|
+
remaining = self.reserved_uom_qty - self.qty_done
|
48
|
+
new_line = self.copy({"reserved_uom_qty": remaining, "qty_done": 0})
|
49
|
+
# if we didn't bypass reservation update, the quant reservation
|
50
|
+
# would be reduced as much as the deduced quantity, which is wrong
|
51
|
+
# as we only moved the quantity to a new move line
|
52
|
+
self.with_context(
|
53
|
+
bypass_reservation_update=True
|
54
|
+
).reserved_uom_qty = self.qty_done
|
55
|
+
return new_line
|
56
|
+
return self.browse()
|
57
|
+
|
58
|
+
def _extract_in_split_order(self, default=None):
|
59
|
+
"""Have pickings fully reserved with only those move lines.
|
60
|
+
|
61
|
+
If the condition is not met, extract the move lines in a new picking.
|
62
|
+
:param default: dictionary of field values to override in the original
|
63
|
+
values of the copied record
|
64
|
+
"""
|
65
|
+
for picking in self.picking_id:
|
66
|
+
moves_to_extract = new_move = picking.move_ids.browse()
|
67
|
+
need_backorder = need_split = False
|
68
|
+
for move in picking.move_ids:
|
69
|
+
if move.state in ("cancel", "done"):
|
70
|
+
continue
|
71
|
+
if move.state == "confirmed":
|
72
|
+
# The move has no ancestor and is not available
|
73
|
+
need_backorder = True
|
74
|
+
continue
|
75
|
+
move_lines = move.move_line_ids & self
|
76
|
+
if not move_lines:
|
77
|
+
# The picking contains moves not related to given move lines
|
78
|
+
need_split = True
|
79
|
+
continue
|
80
|
+
new_move = move.split_other_move_lines(move_lines, intersection=True)
|
81
|
+
if new_move:
|
82
|
+
if move.state == "confirmed":
|
83
|
+
# The move has no ancestor and is not available
|
84
|
+
need_backorder = True
|
85
|
+
else:
|
86
|
+
# The move contains other move lines
|
87
|
+
need_split = True
|
88
|
+
moves_to_extract += new_move or move
|
89
|
+
if need_split:
|
90
|
+
moves_to_extract._extract_in_split_order(default=default)
|
91
|
+
elif need_backorder:
|
92
|
+
# All the lines are processed but some moves are partially available
|
93
|
+
moves_to_extract._extract_in_split_order(
|
94
|
+
default=default, backorder=True
|
95
|
+
)
|
96
|
+
|
97
|
+
def _split_pickings_from_source_location(self):
|
98
|
+
"""Ensure that the related pickings will have the same source location.
|
99
|
+
|
100
|
+
Some pickings related could have other unrelated move lines, as such we
|
101
|
+
have to split them to contain only the move lines related to the expected
|
102
|
+
source location.
|
103
|
+
|
104
|
+
Example:
|
105
|
+
|
106
|
+
Initial data:
|
107
|
+
|
108
|
+
PICK1:
|
109
|
+
- move line with source location LOC1
|
110
|
+
- move line with source location LOC2
|
111
|
+
PICK2:
|
112
|
+
- move line with source location LOC2
|
113
|
+
- move line with source location LOC3
|
114
|
+
|
115
|
+
Then we process move lines related to LOC2 with this method, we get:
|
116
|
+
|
117
|
+
PICK1:
|
118
|
+
- move line with source location LOC1
|
119
|
+
PICK2:
|
120
|
+
- move line with source location LOC3
|
121
|
+
PICK3:
|
122
|
+
- move line with source location LOC2
|
123
|
+
- move line with source location LOC2
|
124
|
+
|
125
|
+
Return the pickings containing the given move lines.
|
126
|
+
"""
|
127
|
+
_logger.warning(
|
128
|
+
"`_split_pickings_from_source_location` is deprecated "
|
129
|
+
"and replaced by `_extract_in_split_order`"
|
130
|
+
)
|
131
|
+
location_src_to_process = self.location_id
|
132
|
+
if location_src_to_process and len(location_src_to_process) != 1:
|
133
|
+
raise UserError(
|
134
|
+
_("Move lines processed have to share the same source location.")
|
135
|
+
)
|
136
|
+
pickings = self.picking_id
|
137
|
+
move_lines_to_process_ids = []
|
138
|
+
for picking in pickings:
|
139
|
+
location_src = picking.move_line_ids.location_id
|
140
|
+
if len(location_src) == 1:
|
141
|
+
continue
|
142
|
+
(picking.move_line_ids & self)._extract_in_split_order()
|
143
|
+
# Get the related move lines among the picking and split them
|
144
|
+
move_lines_to_process_ids.extend(
|
145
|
+
set(picking.move_line_ids.ids) & set(self.ids)
|
146
|
+
)
|
147
|
+
return self.picking_id
|
148
|
+
|
149
|
+
def _split_qty_to_be_done(self, qty_done, split_partial=True, **split_default_vals):
|
150
|
+
"""Check qty to be done for current move line. Split it if needed.
|
151
|
+
|
152
|
+
:param qty_done: qty expected to be done
|
153
|
+
:param split_partial: split if qty is less than expected
|
154
|
+
otherwise rely on a backorder.
|
155
|
+
"""
|
156
|
+
# store a new line if we have split our line (not enough qty)
|
157
|
+
new_line = self.env["stock.move.line"]
|
158
|
+
rounding = self.product_uom_id.rounding
|
159
|
+
compare = float_compare(
|
160
|
+
qty_done, self.reserved_uom_qty, precision_rounding=rounding
|
161
|
+
)
|
162
|
+
qty_lesser = compare == -1
|
163
|
+
qty_greater = compare == 1
|
164
|
+
if qty_greater:
|
165
|
+
return (new_line, "greater")
|
166
|
+
elif qty_lesser:
|
167
|
+
if not split_partial:
|
168
|
+
return (new_line, "lesser")
|
169
|
+
new_line = self._split_partial_quantity_to_be_done(
|
170
|
+
qty_done, split_default_vals
|
171
|
+
)
|
172
|
+
return (new_line, "lesser")
|
173
|
+
return (new_line, "full")
|
174
|
+
|
175
|
+
def _split_partial_quantity_to_be_done(self, quantity_done, split_default_vals):
|
176
|
+
"""Create a new move line with the remaining quantity to process."""
|
177
|
+
# split the move line which will be processed later (maybe the user
|
178
|
+
# has to pick some goods from another place because the location
|
179
|
+
# contained less items than expected)
|
180
|
+
remaining = self.reserved_uom_qty - quantity_done
|
181
|
+
vals = {"reserved_uom_qty": remaining, "qty_done": 0}
|
182
|
+
vals.update(split_default_vals)
|
183
|
+
new_line = self.copy(vals)
|
184
|
+
# if we didn't bypass reservation update, the quant reservation
|
185
|
+
# would be reduced as much as the deduced quantity, which is wrong
|
186
|
+
# as we only moved the quantity to a new move line
|
187
|
+
self.with_context(
|
188
|
+
bypass_reservation_update=True
|
189
|
+
).reserved_uom_qty = quantity_done
|
190
|
+
return new_line
|
191
|
+
|
192
|
+
def replace_package(self, new_package):
|
193
|
+
"""Replace a package on an assigned move line"""
|
194
|
+
self.ensure_one()
|
195
|
+
|
196
|
+
# search other move lines which should already pick the scanned package
|
197
|
+
other_reserved_lines = self.env["stock.move.line"].search(
|
198
|
+
[
|
199
|
+
("package_id", "=", new_package.id),
|
200
|
+
("state", "in", ("partially_available", "assigned")),
|
201
|
+
]
|
202
|
+
)
|
203
|
+
|
204
|
+
# we can't change already picked lines
|
205
|
+
unreservable_lines = other_reserved_lines.filtered(
|
206
|
+
lambda line: line.qty_done == 0
|
207
|
+
)
|
208
|
+
to_assign_moves = unreservable_lines.move_id
|
209
|
+
|
210
|
+
# if we leave the package level, it will try to reserve the same
|
211
|
+
# one again
|
212
|
+
unreservable_lines.package_level_id.explode_package()
|
213
|
+
# unreserve qties of other lines
|
214
|
+
unreservable_lines.unlink()
|
215
|
+
|
216
|
+
if new_package.location_id != self.location_id:
|
217
|
+
if new_package.quant_ids.reserved_quantity:
|
218
|
+
# this is a unexpected condition: if we started picking a package
|
219
|
+
# in another location, user should never be able to scan it in
|
220
|
+
# another location, block the operation
|
221
|
+
raise exceptions.UserError(
|
222
|
+
_(
|
223
|
+
"Package {} has been partially picked in another location"
|
224
|
+
).format(new_package.display_name)
|
225
|
+
)
|
226
|
+
# the package has been scanned in the current location so we know its
|
227
|
+
# a mistake in the data... fix the quant to move the package here
|
228
|
+
new_package.move_package_to_location(self.location_id)
|
229
|
+
|
230
|
+
# several move lines can be moved by the package level, if we change
|
231
|
+
# the package for the current one, we destroy the package level because
|
232
|
+
# we are no longer moving the entire package
|
233
|
+
self.package_level_id.explode_package()
|
234
|
+
|
235
|
+
def is_greater(value, other, rounding):
|
236
|
+
return float_compare(value, other, precision_rounding=rounding) == 1
|
237
|
+
|
238
|
+
def is_lesser(value, other, rounding):
|
239
|
+
return float_compare(value, other, precision_rounding=rounding) == -1
|
240
|
+
|
241
|
+
quant = fields.first(
|
242
|
+
new_package.quant_ids.filtered(
|
243
|
+
lambda quant: quant.product_id == self.product_id
|
244
|
+
and is_greater(
|
245
|
+
quant.quantity,
|
246
|
+
quant.reserved_quantity,
|
247
|
+
quant.product_uom_id.rounding,
|
248
|
+
)
|
249
|
+
)
|
250
|
+
)
|
251
|
+
if not quant:
|
252
|
+
raise exceptions.UserError(
|
253
|
+
_(
|
254
|
+
"Package %(package_name)s does not contain available product "
|
255
|
+
"%(product_name)s, cannot replace package.",
|
256
|
+
package_name=new_package.display_name,
|
257
|
+
product_name=self.product_id.display_name,
|
258
|
+
)
|
259
|
+
)
|
260
|
+
|
261
|
+
values = {
|
262
|
+
"package_id": new_package.id,
|
263
|
+
"lot_id": quant.lot_id.id,
|
264
|
+
"owner_id": quant.owner_id.id,
|
265
|
+
"result_package_id": False,
|
266
|
+
}
|
267
|
+
|
268
|
+
available_quantity = quant.quantity - quant.reserved_quantity
|
269
|
+
if is_lesser(
|
270
|
+
available_quantity, self.reserved_qty, quant.product_uom_id.rounding
|
271
|
+
):
|
272
|
+
new_uom_qty = self.product_id.uom_id._compute_quantity(
|
273
|
+
available_quantity, self.product_uom_id, rounding_method="HALF-UP"
|
274
|
+
)
|
275
|
+
values["reserved_uom_qty"] = new_uom_qty
|
276
|
+
|
277
|
+
self.write(values)
|
278
|
+
|
279
|
+
# try reassign the move in case we had a partial qty, also, it will
|
280
|
+
# recreate a package level if it applies
|
281
|
+
if "reserved_uom_qty" in values:
|
282
|
+
# when we change the quantity of the move, the state
|
283
|
+
# will still be "assigned" and be skipped by "_action_assign",
|
284
|
+
# recompute the state to be "partially_available"
|
285
|
+
self.move_id._recompute_state()
|
286
|
+
|
287
|
+
# if the new package has less quantities, assign will create new move
|
288
|
+
# lines
|
289
|
+
self.move_id._action_confirm()
|
290
|
+
self.move_id._action_assign()
|
291
|
+
|
292
|
+
# Find other available goods for the lines which were using the
|
293
|
+
# package before...
|
294
|
+
to_assign_moves._action_assign()
|
295
|
+
|
296
|
+
# computation of the 'state' of the package levels is not
|
297
|
+
# triggered, force it
|
298
|
+
to_assign_moves.move_line_ids.package_level_id.modified(["move_line_ids"])
|
299
|
+
self.package_level_id.modified(["move_line_ids"])
|
300
|
+
|
301
|
+
def _filter_on_picking(self, picking=False):
|
302
|
+
"""Filter a bunch of lines on a picking.
|
303
|
+
|
304
|
+
If no picking is provided the first one is taken.
|
305
|
+
"""
|
306
|
+
picking = picking or fields.first(self.picking_id)
|
307
|
+
return self.filtered_domain([("picking_id", "=", picking.id)])
|
@@ -0,0 +1,50 @@
|
|
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, models
|
4
|
+
|
5
|
+
|
6
|
+
class StockPackageLevel(models.Model):
|
7
|
+
_name = "stock.package_level"
|
8
|
+
_inherit = ["stock.package_level", "shopfloor.priority.postpone.mixin"]
|
9
|
+
|
10
|
+
# we search package levels based on their package in some workflows
|
11
|
+
package_id = fields.Many2one(index=True)
|
12
|
+
# allow domain on picking_id.xxx without too much perf penalty
|
13
|
+
picking_id = fields.Many2one(auto_join=True)
|
14
|
+
|
15
|
+
def explode_package(self):
|
16
|
+
"""Unlink but keep the moves.
|
17
|
+
|
18
|
+
Original motivation:
|
19
|
+
|
20
|
+
A package level has a relation to "move_lines" only when the
|
21
|
+
package level was created first from the UI and it created
|
22
|
+
its move.
|
23
|
+
When we unlink a package level, it deletes the move it created.
|
24
|
+
But in some cases, we want to keep the move, e.g.:
|
25
|
+
|
26
|
+
* create a package level from the UI to move a package
|
27
|
+
* it generates a move for the matching product quantity
|
28
|
+
* we use a barcode scenario such as cluster or zone picking
|
29
|
+
* we use the "replace package" button
|
30
|
+
* when replacing the package, we have to delete the package level,
|
31
|
+
but we still have the same need in term of "I want X products",
|
32
|
+
so we have to keep the move
|
33
|
+
* another case is when we "dismiss" the package level in the location
|
34
|
+
content transfer scenario, we want to keep the "need" in moves, but
|
35
|
+
we are no longer moving the entire package level
|
36
|
+
|
37
|
+
Commit
|
38
|
+
|
39
|
+
https://github.com/odoo/odoo/commit/b33e72d0bf027fb2c789b1b9476f7edf1a40b0a6
|
40
|
+
|
41
|
+
introduced the handling of pkg level deletion
|
42
|
+
which is doing what was done by this method.
|
43
|
+
|
44
|
+
Moreover it has been fixed here https://github.com/odoo/odoo/pull/66517.
|
45
|
+
|
46
|
+
Hence, we keep this method to unify the action of "exploding a package"
|
47
|
+
especially to avoid to refactor many places every time the core changes.
|
48
|
+
"""
|
49
|
+
# This will trigger the deletion of the pkg level
|
50
|
+
self.move_line_ids.result_package_id = False
|
@@ -0,0 +1,118 @@
|
|
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 _, api, fields, models
|
4
|
+
|
5
|
+
|
6
|
+
class StockPicking(models.Model):
|
7
|
+
_inherit = "stock.picking"
|
8
|
+
|
9
|
+
total_weight = fields.Float(
|
10
|
+
compute="_compute_picking_info",
|
11
|
+
help="Technical field. Indicates total weight of transfers included.",
|
12
|
+
)
|
13
|
+
move_line_count = fields.Integer(
|
14
|
+
compute="_compute_picking_info",
|
15
|
+
help="Technical field. Indicates number of move lines included.",
|
16
|
+
)
|
17
|
+
package_level_count = fields.Integer(
|
18
|
+
compute="_compute_picking_info",
|
19
|
+
help="Technical field. Indicates number of package_level included.",
|
20
|
+
)
|
21
|
+
bulk_line_count = fields.Integer(
|
22
|
+
compute="_compute_picking_info",
|
23
|
+
help="Technical field. Indicates number of move lines without package included.",
|
24
|
+
)
|
25
|
+
|
26
|
+
@api.depends(
|
27
|
+
"move_line_ids", "move_line_ids.reserved_qty", "move_line_ids.product_id.weight"
|
28
|
+
)
|
29
|
+
def _compute_picking_info(self):
|
30
|
+
for item in self:
|
31
|
+
item.update(
|
32
|
+
{
|
33
|
+
"total_weight": item._calc_weight(),
|
34
|
+
"move_line_count": len(item.move_line_ids),
|
35
|
+
"package_level_count": len(item.package_level_ids),
|
36
|
+
# NOTE: not based on 'move_line_ids_without_package' field
|
37
|
+
# on purpose as it also takes into account the
|
38
|
+
# 'Move entire packs' option from the picking type.
|
39
|
+
"bulk_line_count": len(
|
40
|
+
item.move_line_ids.filtered(lambda ml: not ml.package_level_id)
|
41
|
+
),
|
42
|
+
}
|
43
|
+
)
|
44
|
+
|
45
|
+
def _calc_weight(self):
|
46
|
+
weight = 0.0
|
47
|
+
for move_line in self.mapped("move_line_ids"):
|
48
|
+
weight += move_line.reserved_qty * move_line.product_id.weight
|
49
|
+
return weight
|
50
|
+
|
51
|
+
def _check_move_lines_map_quant_package(self, package):
|
52
|
+
# see tests/test_move_action_assign.py for details
|
53
|
+
pack_move_lines = self.move_line_ids.filtered(
|
54
|
+
lambda ml: ml.package_id == package
|
55
|
+
)
|
56
|
+
# if we set a qty_done on any line, it's picked, we don't want
|
57
|
+
# to change it in any case, so we ignore the package level
|
58
|
+
if any(pack_move_lines.mapped("qty_done")):
|
59
|
+
return False
|
60
|
+
# if we already changed the destination package, do not create
|
61
|
+
# a new package level
|
62
|
+
if any(
|
63
|
+
line.result_package_id != package
|
64
|
+
for line in pack_move_lines
|
65
|
+
if line.result_package_id
|
66
|
+
):
|
67
|
+
return False
|
68
|
+
return super()._check_move_lines_map_quant_package(package)
|
69
|
+
|
70
|
+
def split_assigned_move_lines(self, move_lines=None):
|
71
|
+
"""Put all reserved quantities (move lines) in their own moves and transfer.
|
72
|
+
|
73
|
+
As a result, the current transfer will contain only confirmed moves.
|
74
|
+
"""
|
75
|
+
self.ensure_one()
|
76
|
+
# Check in the picking all the moves which are partially available or confirmed
|
77
|
+
moves = self.move_lines.filtered(
|
78
|
+
lambda m: m.state in ("partially_available", "confirmed")
|
79
|
+
)
|
80
|
+
# If one of these moves has an ancestor, split the moves
|
81
|
+
# then extract all the assigned moves in a new transfer.
|
82
|
+
# Indeed, a move without ancestor won't see its reserved qty changed
|
83
|
+
# automatically over time.
|
84
|
+
has_ancestors = bool(
|
85
|
+
moves.move_orig_ids.filtered(lambda m: m.state not in ("cancel", "done"))
|
86
|
+
)
|
87
|
+
if not has_ancestors:
|
88
|
+
return self.id
|
89
|
+
# Get only transfers composed of moves assigned or confirmed
|
90
|
+
moves.split_other_move_lines(moves.move_line_ids)
|
91
|
+
# Put assigned moves related to processed move lines into a separate transfer
|
92
|
+
if move_lines:
|
93
|
+
assigned_moves = self.move_lines & move_lines.move_id
|
94
|
+
else:
|
95
|
+
assigned_moves = self.move_lines.filtered(lambda m: m.state == "assigned")
|
96
|
+
if assigned_moves == self.move_lines:
|
97
|
+
return self.id
|
98
|
+
new_picking = self.copy(
|
99
|
+
{
|
100
|
+
"name": "/",
|
101
|
+
"move_lines": [],
|
102
|
+
"move_line_ids": [],
|
103
|
+
"backorder_id": self.id,
|
104
|
+
}
|
105
|
+
)
|
106
|
+
message = _(
|
107
|
+
'The backorder <a href="#" '
|
108
|
+
'data-oe-model="stock.picking" '
|
109
|
+
'data-oe-id="%(new_picking_id)d">%(new_picking_name)s</a> has been created.'
|
110
|
+
) % dict(new_picking_id=new_picking.id, new_picking_name=new_picking.name)
|
111
|
+
self.message_post(body=message)
|
112
|
+
assigned_moves.write({"picking_id": new_picking.id})
|
113
|
+
assigned_moves.mapped("move_line_ids").write({"picking_id": new_picking.id})
|
114
|
+
assigned_moves.move_line_ids.package_level_id.write(
|
115
|
+
{"picking_id": new_picking.id}
|
116
|
+
)
|
117
|
+
assigned_moves._action_assign()
|
118
|
+
return new_picking.id
|
@@ -0,0 +1,41 @@
|
|
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 api, fields, models
|
4
|
+
|
5
|
+
|
6
|
+
class StockPickingBatch(models.Model):
|
7
|
+
_inherit = "stock.picking.batch"
|
8
|
+
|
9
|
+
picking_count = fields.Integer(
|
10
|
+
compute="_compute_picking_info",
|
11
|
+
help="Technical field. Indicates number of transfers included.",
|
12
|
+
)
|
13
|
+
move_line_count = fields.Integer(
|
14
|
+
compute="_compute_picking_info",
|
15
|
+
help="Technical field. Indicates number of move lines included.",
|
16
|
+
)
|
17
|
+
total_weight = fields.Float(
|
18
|
+
compute="_compute_picking_info",
|
19
|
+
help="Technical field. Indicates total weight of transfers included.",
|
20
|
+
)
|
21
|
+
|
22
|
+
@api.depends(
|
23
|
+
"picking_ids.state", "picking_ids.total_weight", "picking_ids.move_line_ids"
|
24
|
+
)
|
25
|
+
def _compute_picking_info(self):
|
26
|
+
for item in self:
|
27
|
+
assigned_pickings = item.picking_ids.filtered(
|
28
|
+
lambda picking: picking.state == "assigned"
|
29
|
+
)
|
30
|
+
item.update(
|
31
|
+
{
|
32
|
+
"picking_count": len(assigned_pickings.ids),
|
33
|
+
"move_line_count": len(
|
34
|
+
assigned_pickings.mapped("move_line_ids").ids
|
35
|
+
),
|
36
|
+
"total_weight": item._calc_weight(assigned_pickings),
|
37
|
+
}
|
38
|
+
)
|
39
|
+
|
40
|
+
def _calc_weight(self, pickings):
|
41
|
+
return sum(pickings.mapped("total_weight"))
|
@@ -0,0 +1,26 @@
|
|
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 api, fields, models
|
4
|
+
|
5
|
+
|
6
|
+
class StockPickingType(models.Model):
|
7
|
+
_inherit = "stock.picking.type"
|
8
|
+
|
9
|
+
shopfloor_menu_ids = fields.Many2many(
|
10
|
+
comodel_name="shopfloor.menu",
|
11
|
+
string="Shopfloor Menus",
|
12
|
+
readonly=True,
|
13
|
+
)
|
14
|
+
shopfloor_zero_check = fields.Boolean(
|
15
|
+
string="Activate Zero Check",
|
16
|
+
help="For Shopfloor scenarios using it (Cluster Picking, Zone Picking,"
|
17
|
+
" Discrete order Picking), the zero check step will be activated when"
|
18
|
+
" a location becomes empty after a move.",
|
19
|
+
)
|
20
|
+
|
21
|
+
@api.constrains("show_entire_packs")
|
22
|
+
def _check_move_entire_packages(self):
|
23
|
+
menu_items = self.env["shopfloor.menu"].search(
|
24
|
+
[("picking_type_ids", "in", self.ids)]
|
25
|
+
)
|
26
|
+
menu_items._check_move_entire_packages()
|