diff --git a/stock_picking_zone/README.rst b/stock_picking_type_routing_operation/README.rst similarity index 100% rename from stock_picking_zone/README.rst rename to stock_picking_type_routing_operation/README.rst diff --git a/stock_picking_zone/__init__.py b/stock_picking_type_routing_operation/__init__.py similarity index 100% rename from stock_picking_zone/__init__.py rename to stock_picking_type_routing_operation/__init__.py diff --git a/stock_picking_zone/__manifest__.py b/stock_picking_type_routing_operation/__manifest__.py similarity index 92% rename from stock_picking_zone/__manifest__.py rename to stock_picking_type_routing_operation/__manifest__.py index e86d9d487c88..8b4123dac55e 100644 --- a/stock_picking_zone/__manifest__.py +++ b/stock_picking_type_routing_operation/__manifest__.py @@ -15,7 +15,7 @@ 'demo/stock_picking_type_demo.xml', ], 'data': [ - 'views/stock_picking_type_views.xml', + 'views/stock_location.xml', ], 'installable': True, 'development_status': 'Alpha', diff --git a/stock_picking_zone/demo/stock_location_demo.xml b/stock_picking_type_routing_operation/demo/stock_location_demo.xml similarity index 77% rename from stock_picking_zone/demo/stock_location_demo.xml rename to stock_picking_type_routing_operation/demo/stock_location_demo.xml index d721e0a8f957..d29639c5d3f2 100644 --- a/stock_picking_zone/demo/stock_location_demo.xml +++ b/stock_picking_type_routing_operation/demo/stock_location_demo.xml @@ -3,8 +3,7 @@ Highbay - + Bay A @@ -21,8 +20,7 @@ Handover - + diff --git a/stock_picking_zone/demo/stock_picking_type_demo.xml b/stock_picking_type_routing_operation/demo/stock_picking_type_demo.xml similarity index 70% rename from stock_picking_zone/demo/stock_picking_type_demo.xml rename to stock_picking_type_routing_operation/demo/stock_picking_type_demo.xml index 5761b5c40c19..1c583506eb6a 100644 --- a/stock_picking_zone/demo/stock_picking_type_demo.xml +++ b/stock_picking_type_routing_operation/demo/stock_picking_type_demo.xml @@ -15,10 +15,10 @@ internal - - + + - + diff --git a/stock_picking_zone/models/__init__.py b/stock_picking_type_routing_operation/models/__init__.py similarity index 74% rename from stock_picking_zone/models/__init__.py rename to stock_picking_type_routing_operation/models/__init__.py index b2c5936e1170..200ba08be0a0 100644 --- a/stock_picking_zone/models/__init__.py +++ b/stock_picking_type_routing_operation/models/__init__.py @@ -1,3 +1,4 @@ +from . import stock_location from . import stock_move from . import stock_picking_type from . import stock_quant diff --git a/stock_picking_type_routing_operation/models/stock_location.py b/stock_picking_type_routing_operation/models/stock_location.py new file mode 100644 index 000000000000..33c8032217e8 --- /dev/null +++ b/stock_picking_type_routing_operation/models/stock_location.py @@ -0,0 +1,55 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import api, models, fields, _ +from odoo.exceptions import ValidationError + + +class StockLocation(models.Model): + + _inherit = 'stock.location' + + routing_operation_picking_type_id = fields.Many2one( + 'stock.picking.type', + string='Routing operation', + help="Change destination of the move line according to the" + " default destination setup after reservation occurs", + ) + + @api.constrains('routing_operation_picking_type_id') + def _check_routing_operation_picking_type_id(self): + for location in self: + picking_type = location.routing_operation_picking_type_id + if picking_type.default_location_src_id != location: + raise ValidationError(_( + 'A picking type for routing operations cannot have a' + ' different default source location than the location it ' + 'is used on.' + )) + + @api.multi + def _find_picking_type_for_routing_operation(self): + self.ensure_one() + # First select all the parent locations and the matching + # picking types. In a second step, the picking type matching the closest location + # is searched in memory. This is to avoid doing an SQL query + # for each location in the tree. + tree = self.search( + [('id', 'parent_of', self.id)], + # the recordset will be ordered bottom location to top location + order='parent_path desc' + ) + picking_types = self.env['stock.picking.type'].search([ + ('routing_operation_location_ids', '!=', False), + ('default_location_src_id', 'in', tree.ids) + ]) + # the first location is the current move line's source location, + # then we climb up the tree of locations + for location in tree: + match = picking_types.filtered( + lambda p: p.default_location_src_id == location + ) + if match: + # we can only have one match as we have a unique + # constraint on is_zone + source location + return match + return self.env['stock.picking.type'] diff --git a/stock_picking_zone/models/stock_move.py b/stock_picking_type_routing_operation/models/stock_move.py similarity index 66% rename from stock_picking_zone/models/stock_move.py rename to stock_picking_type_routing_operation/models/stock_move.py index a2afed1eaf67..efd6dedf3d86 100644 --- a/stock_picking_zone/models/stock_move.py +++ b/stock_picking_type_routing_operation/models/stock_move.py @@ -1,5 +1,5 @@ # Copyright 2019 Camptocamp (https://www.camptocamp.com) - +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) from itertools import chain from odoo import models @@ -9,19 +9,17 @@ class StockMove(models.Model): def _action_assign(self): super()._action_assign() - if not self.env.context.get('exclude_apply_zone'): - moves = self._split_per_zone() - moves._apply_move_location_zone() + if not self.env.context.get('exclude_apply_routing_operation'): + moves = self._split_per_routing_operation() + moves._apply_move_location_routing_operation() - def _split_per_zone(self): + def _split_per_routing_operation(self): move_to_assign_ids = set() new_move_per_location = {} for move in self: if move.state not in ('assigned', 'partially_available'): continue - pick_type_model = self.env['stock.picking.type'] - # Group move lines per source location, some may need an additional # operations while others not. Store the number of products to # take from each location, so we'll be able to split the move @@ -31,39 +29,42 @@ def _split_per_zone(self): location = move_line.location_id move_lines[location] = sum(move_line.mapped('product_uom_qty')) - # We'll split the move to have one move per different zones where + # We'll split the move to have one move per different location where # we have to take products - zone_quantities = {} + routing_quantities = {} for source_location, qty in move_lines.items(): - zone = pick_type_model._find_zone_for_location(source_location) - zone_quantities.setdefault(zone, 0.0) - zone_quantities[zone] += qty - - if len(zone_quantities) == 1: - # The whole quantity can be taken from only one zone (a - # non-zone being equal to a zone here), nothing to split. + routing_picking_type = \ + source_location._find_picking_type_for_routing_operation() + routing_quantities.setdefault(routing_picking_type, 0.0) + routing_quantities[routing_picking_type] += qty + + if len(routing_quantities) == 1: + # The whole quantity can be taken from only one location (an + # empty routing picking type being equal to one location here), nothing to split. continue move._do_unreserve() move_to_assign_ids.add(move.id) - zone_location = zone.default_location_src_id - for zone, qty in zone_quantities.items(): - # if zone is False-ish, we take in a location which is + for picking_type, qty in routing_quantities.items(): + # if picking type is empty, we don't need a new move # not a zone - if zone: - # split returns the same move if the qty is the same + if picking_type: + routing_location = picking_type.default_location_src_id + # if we have a picking type, split returns the same move if the qty is the same new_move_id = move._split(qty) - new_move_per_location.setdefault(zone_location.id, []) - new_move_per_location[zone_location.id].append(new_move_id) + new_move_per_location.setdefault(routing_location.id, []) + new_move_per_location[routing_location.id].append( + new_move_id + ) - # it is important to assign the zones first + # it is important to assign the routed moves first for location_id, new_move_ids in new_move_per_location.items(): new_moves = self.browse(new_move_ids) new_moves.with_context( - # Prevent to call _apply_move_location_zone, will be called + # Prevent to call _apply_move_location_routing_operation, will be called # when all lines are processed. - exclude_apply_zone=True, - # Force reservation of quants in the zone they were + exclude_apply_routing_operation=True, + # Force reservation of quants in the location they were # reserved in at the origin (so we keep the same quantities # at the same places) gather_in_location_id=location_id, @@ -78,33 +79,33 @@ def _split_per_zone(self): )) return self + new_moves - def _apply_move_location_zone(self): + def _apply_move_location_routing_operation(self): for move in self: if move.state not in ('assigned', 'partially_available'): continue - pick_type_model = self.env['stock.picking.type'] - # Group move lines per source location, some may need an additional # operations while others not. Store the number of products to # take from each location, so we'll be able to split the move # if needed. - # At this point, we should not have lines with different zones, - # they have been split in _split_per_zone(), so we can take the - # first one + # At this point, we should not have lines with different source locations, + # they have been split in _split_per_routing_operation(), so we + # can take the first one source = move.move_line_ids[0].location_id - zone = pick_type_model._find_zone_for_location(source) - if not zone: + picking_type = source._find_picking_type_for_routing_operation() + if not picking_type: continue - if (move.picking_type_id == zone and - move.location_dest_id == zone.default_location_dest_id): + if ( + move.picking_type_id == picking_type and + move.location_dest_id == picking_type.default_location_dest_id + ): # already done continue move._do_unreserve() move.write({ - 'location_dest_id': zone.default_location_dest_id.id, - 'picking_type_id': zone.id, + 'location_dest_id': picking_type.default_location_dest_id.id, + 'picking_type_id': picking_type.id, }) move._insert_middle_moves() move._assign_picking() diff --git a/stock_picking_type_routing_operation/models/stock_picking_type.py b/stock_picking_type_routing_operation/models/stock_picking_type.py new file mode 100644 index 000000000000..6a270beb8959 --- /dev/null +++ b/stock_picking_type_routing_operation/models/stock_picking_type.py @@ -0,0 +1,45 @@ +# Copyright 2019 Camptocamp (https://www.camptocamp.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import _, api, exceptions, fields, models + + +class StockPickingType(models.Model): + _inherit = 'stock.picking.type' + + routing_operation_location_ids = fields.One2many( + 'stock.location', 'routing_operation_picking_type_id' + ) + + @api.constrains( + 'routing_operation_location_ids', 'default_location_src_id' + ) + def _check_routing_operation_location_src_unique(self): + for picking_type in self: + if not picking_type.routing_operation_location_ids: + continue + if len(picking_type.routing_operation_location_ids): + raise exceptions.ValidationError(_( + 'The same picking type cannot be used on different ' + 'locations having routing operations.' + )) + if ( + picking_type.routing_operation_location_ids + != picking_type.default_location_src_id + ): + raise exceptions.ValidationError(_( + 'A picking type for routing operations cannot have a' + ' different default source location than the location it ' + 'is used on.' + )) + src_location = picking_type.default_location_src_id + domain = [ + ('routing_operation_location_ids', '!=', False), + ('default_location_src_id', '=', src_location.id), + ('id', '!=', picking_type.id) + ] + other = self.search(domain) + if other: + raise exceptions.ValidationError( + _('Another routing operation picking type (%s) exists for' + ' the same source location.') % (other.display_name,) + ) diff --git a/stock_picking_zone/models/stock_quant.py b/stock_picking_type_routing_operation/models/stock_quant.py similarity index 97% rename from stock_picking_zone/models/stock_quant.py rename to stock_picking_type_routing_operation/models/stock_quant.py index f076476ea91a..2700a987471a 100644 --- a/stock_picking_zone/models/stock_quant.py +++ b/stock_picking_type_routing_operation/models/stock_quant.py @@ -1,5 +1,5 @@ # Copyright 2019 Camptocamp (https://www.camptocamp.com) - +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) """Allow forcing reservations of quants in a location (or children) When the context key "gather_in_location_id" is passed, it will look diff --git a/stock_picking_zone/readme/CONFIGURE.rst b/stock_picking_type_routing_operation/readme/CONFIGURE.rst similarity index 69% rename from stock_picking_zone/readme/CONFIGURE.rst rename to stock_picking_type_routing_operation/readme/CONFIGURE.rst index fbb186440aa4..9e880ec0c5c6 100644 --- a/stock_picking_zone/readme/CONFIGURE.rst +++ b/stock_picking_type_routing_operation/readme/CONFIGURE.rst @@ -4,7 +4,7 @@ In Inventory Settings, you must have: * Multi-Warehouses * Multi-Step Routes -Create an operation type and activate the "Is Zone" checkbox. +On stock location, create an "Routing operation" operation type. The default destination location will be the destination location of the new operation inserted when a move has a source location which -is a child of the type's source location. +is a child of the location. diff --git a/stock_picking_zone/readme/CONTRIBUTORS.rst b/stock_picking_type_routing_operation/readme/CONTRIBUTORS.rst similarity index 69% rename from stock_picking_zone/readme/CONTRIBUTORS.rst rename to stock_picking_type_routing_operation/readme/CONTRIBUTORS.rst index 63ccd231e8e3..7c15b32aa74c 100644 --- a/stock_picking_zone/readme/CONTRIBUTORS.rst +++ b/stock_picking_type_routing_operation/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Joël Grand-Guillaume * Guewen Baconnier +* Akim Juillerat diff --git a/stock_picking_type_routing_operation/readme/DESCRIPTION.rst b/stock_picking_type_routing_operation/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..99e1cf4dc2e6 --- /dev/null +++ b/stock_picking_type_routing_operation/readme/DESCRIPTION.rst @@ -0,0 +1,10 @@ +Route explains the steps you want to produce whereas the “picking routing +operation” defines how operations are grouped according to their final source +and destination location. + +This allows for example: + +* To parallelize picking operations in two locations of a warehouse, splitting + them in two different picking type +* To define pre-picking (wave) in some sub-locations, then roundtrip picking of + the sub-location waves diff --git a/stock_picking_zone/tests/__init__.py b/stock_picking_type_routing_operation/tests/__init__.py similarity index 100% rename from stock_picking_zone/tests/__init__.py rename to stock_picking_type_routing_operation/tests/__init__.py diff --git a/stock_picking_zone/tests/test_picking_zone.py b/stock_picking_type_routing_operation/tests/test_picking_zone.py similarity index 96% rename from stock_picking_zone/tests/test_picking_zone.py rename to stock_picking_type_routing_operation/tests/test_picking_zone.py index 670e523cf540..8d5020b121f6 100644 --- a/stock_picking_zone/tests/test_picking_zone.py +++ b/stock_picking_type_routing_operation/tests/test_picking_zone.py @@ -3,7 +3,7 @@ from odoo.tests import common -class TestPickingZone(common.SavepointCase): +class TestPickingTypeRoutingOperation(common.SavepointCase): @classmethod def setUpClass(cls): @@ -56,16 +56,18 @@ def setUpClass(cls): 'padding': 5, 'company_id': cls.wh.company_id.id, }) - cls.pick_type_zone = cls.env['stock.picking.type'].create({ - 'name': 'Zone', + cls.pick_type_routing_op = cls.env['stock.picking.type'].create({ + 'name': 'Routing operation', 'code': 'internal', 'use_create_lots': False, 'use_existing_lots': True, 'default_location_src_id': cls.location_hb.id, 'default_location_dest_id': cls.location_handover.id, - 'is_zone': True, 'sequence_id': picking_type_sequence.id, }) + cls.location_hb.write({ + 'routing_operation_picking_type_id': cls.pick_type_routing_op.id + }) def _create_pick_ship(self, wh, products=None): """Create pick+ship pickings @@ -156,7 +158,7 @@ def process_operations(self, move): move.move_line_ids.qty_done = qty move.picking_id.action_done() - def test_change_location_to_zone(self): + def test_change_location_to_routing_operation(self): pick_picking, customer_picking = self._create_pick_ship( self.wh, [(self.product1, 10)] ) @@ -173,7 +175,9 @@ def test_change_location_to_zone(self): self.assert_src_highbay_1_2(ml) self.assert_dest_handover(ml) - self.assertEqual(ml.picking_id.picking_type_id, self.pick_type_zone) + self.assertEqual( + ml.picking_id.picking_type_id, self.pick_type_routing_op + ) self.assert_src_stock(move_a) self.assert_dest_handover(move_a) @@ -266,7 +270,9 @@ def test_several_moves(self): self.assert_src_highbay_1_2(ml) self.assert_dest_handover(ml) # this is a new HO picking - self.assertEqual(ml.picking_id.picking_type_id, self.pick_type_zone) + self.assertEqual( + ml.picking_id.picking_type_id, self.pick_type_routing_op + ) self.assertEqual(ml.state, 'assigned') # this one stays in the PICK/ @@ -391,7 +397,9 @@ def test_several_move_lines(self): self.assert_src_highbay_1_2(ml) self.assert_dest_handover(ml) # this is a new HO picking - self.assertEqual(ml.picking_id.picking_type_id, self.pick_type_zone) + self.assertEqual( + ml.picking_id.picking_type_id, self.pick_type_routing_op + ) self.assertEqual(ml.state, 'assigned') # the split move is waiting for 'move_ho' diff --git a/stock_picking_type_routing_operation/views/stock_location.xml b/stock_picking_type_routing_operation/views/stock_location.xml new file mode 100644 index 000000000000..9cf4c24b4a60 --- /dev/null +++ b/stock_picking_type_routing_operation/views/stock_location.xml @@ -0,0 +1,13 @@ + + + + stock.location.form.inherit + stock.location + + + + + + + + diff --git a/stock_picking_zone/models/stock_picking_type.py b/stock_picking_zone/models/stock_picking_type.py deleted file mode 100644 index 9842eac1ebe8..000000000000 --- a/stock_picking_zone/models/stock_picking_type.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2019 Camptocamp (https://www.camptocamp.com) - -from odoo import _, api, exceptions, fields, models - - -class StockPickingType(models.Model): - _inherit = 'stock.picking.type' - - is_zone = fields.Boolean( - help="Change destination of the move line according to the" - " default destination setup after reservation occurs", - ) - - @api.constrains('is_zone', 'default_location_src_id') - def _check_zone_location_src_unique(self): - for picking_type in self: - if not picking_type.is_zone: - continue - src_location = picking_type.default_location_src_id - domain = [ - ('is_zone', '=', True), - ('default_location_src_id', '=', src_location.id), - ('id', '!=', picking_type.id) - ] - other = self.search(domain) - if other: - raise exceptions.ValidationError( - _('Another zone picking type (%s) exists for' - ' the some source location.') % (other.display_name,) - ) - - @api.model - def _find_zone_for_location(self, location): - # First select all the parent locations and the matching - # zones. In a second step, the zone matching the closest location - # is searched in memory. This is to avoid doing an SQL query - # for each location in the tree. - tree = self.env['stock.location'].search( - [('id', 'parent_of', location.id)], - # the recordset will be ordered bottom location to top location - order='parent_path desc' - ) - zones = self.search([ - ('is_zone', '=', True), - ('default_location_src_id', 'in', tree.ids) - ]) - # the first location is the current move line's source location, - # then we climb up the tree of locations - for location in tree: - match = [ - zone for zone in zones - if zone.default_location_src_id == location - ] - if match: - # we can only have one match as we have a unique - # constraint on is_zone + source location - return match[0] - return self.browse() diff --git a/stock_picking_zone/readme/DESCRIPTION.rst b/stock_picking_zone/readme/DESCRIPTION.rst deleted file mode 100644 index da4e3a5e0d2b..000000000000 --- a/stock_picking_zone/readme/DESCRIPTION.rst +++ /dev/null @@ -1,10 +0,0 @@ -Route explains the steps you want to produce whereas the “picking zone” defines -how operations are grouped according to their final source and destination -location. - -This allows for example: - -* To parallelize picking operations in two main zone of a warehouse, splitting - them in two different picking type -* To define pre-picking (wave) in some sub-zones, then roundtrip picking of the - sub-zone waves diff --git a/stock_picking_zone/readme/ROADMAP.rst b/stock_picking_zone/readme/ROADMAP.rst deleted file mode 100644 index ebc15a66e633..000000000000 --- a/stock_picking_zone/readme/ROADMAP.rst +++ /dev/null @@ -1,5 +0,0 @@ -The concept of "zone" is duplicated with the zones introduced by -"stock_location_zone" in -https://github.com/OCA/stock-logistics-warehouse/pull/653 -They will be merged in the same concept. Note that considering the -alpha version of this module, no data migration will be done. diff --git a/stock_picking_zone/views/stock_picking_type_views.xml b/stock_picking_zone/views/stock_picking_type_views.xml deleted file mode 100644 index cd57aa8a2d7c..000000000000 --- a/stock_picking_zone/views/stock_picking_type_views.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Operation Types - stock.picking.type - - - - - - - - -