diff --git a/stock_picking_zone/__manifest__.py b/stock_picking_zone/__manifest__.py index e86d9d487c88..8b4123dac55e 100644 --- a/stock_picking_zone/__manifest__.py +++ b/stock_picking_zone/__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_zone/demo/stock_location_demo.xml index d721e0a8f957..d29639c5d3f2 100644 --- a/stock_picking_zone/demo/stock_location_demo.xml +++ b/stock_picking_zone/demo/stock_location_demo.xml @@ -3,8 +3,7 @@ Highbay - + Bay A @@ -21,8 +20,7 @@ Handover - + diff --git a/stock_picking_zone/models/__init__.py b/stock_picking_zone/models/__init__.py index b2c5936e1170..200ba08be0a0 100644 --- a/stock_picking_zone/models/__init__.py +++ b/stock_picking_zone/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_zone/models/stock_location.py b/stock_picking_zone/models/stock_location.py new file mode 100644 index 000000000000..bf98759cccb1 --- /dev/null +++ b/stock_picking_zone/models/stock_location.py @@ -0,0 +1,44 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import api, models, fields + + +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", + # TODO add domain ? + ) + + @api.multi + def _find_picking_type_for_routing_operation(self): + self.ensure_one() + # 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.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_zone/models/stock_move.py index a2afed1eaf67..8a553b903e28 100644 --- a/stock_picking_zone/models/stock_move.py +++ b/stock_picking_zone/models/stock_move.py @@ -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 @@ -33,28 +31,33 @@ def _split_per_zone(self): # We'll split the move to have one move per different zones 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 + 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(zone_quantities) == 1: + if len(routing_quantities) == 1: # The whole quantity can be taken from only one zone (a # non-zone being equal to a zone 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(): + # FIXME routing_picking_type is the last element from loop above + # not sure it's correct + routing_location = routing_picking_type.default_location_src_id + for picking_type, qty in routing_quantities.items(): # if zone is False-ish, we take in a location which is # not a zone - if zone: + if 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 for location_id, new_move_ids in new_move_per_location.items(): @@ -62,7 +65,7 @@ def _split_per_zone(self): new_moves.with_context( # Prevent to call _apply_move_location_zone, will be called # when all lines are processed. - exclude_apply_zone=True, + exclude_apply_routing_operation=True, # Force reservation of quants in the zone they were # reserved in at the origin (so we keep the same quantities # at the same places) @@ -78,33 +81,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 + # 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_zone/models/stock_picking_type.py b/stock_picking_zone/models/stock_picking_type.py index 9842eac1ebe8..3776cb0b68e1 100644 --- a/stock_picking_zone/models/stock_picking_type.py +++ b/stock_picking_zone/models/stock_picking_type.py @@ -6,53 +6,40 @@ 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", + routing_operation_location_ids = fields.One2many( + 'stock.location', 'routing_operation_picking_type_id' ) - @api.constrains('is_zone', 'default_location_src_id') - def _check_zone_location_src_unique(self): + @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.is_zone: + 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 = [ - ('is_zone', '=', True), + ('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 zone picking type (%s) exists for' - ' the some source location.') % (other.display_name,) + _('Another routing operation picking type (%s) exists for' + ' the same 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/CONTRIBUTORS.rst b/stock_picking_zone/readme/CONTRIBUTORS.rst index 63ccd231e8e3..7c15b32aa74c 100644 --- a/stock_picking_zone/readme/CONTRIBUTORS.rst +++ b/stock_picking_zone/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Joël Grand-Guillaume * Guewen Baconnier +* Akim Juillerat diff --git a/stock_picking_zone/tests/test_picking_zone.py b/stock_picking_zone/tests/test_picking_zone.py index 670e523cf540..8d5020b121f6 100644 --- a/stock_picking_zone/tests/test_picking_zone.py +++ b/stock_picking_zone/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_zone/views/stock_location.xml b/stock_picking_zone/views/stock_location.xml new file mode 100644 index 000000000000..8a4c6f86cfff --- /dev/null +++ b/stock_picking_zone/views/stock_location.xml @@ -0,0 +1,13 @@ + + + + stock.location.form.inherit + stock.location + + + + + + + +