Skip to content

Commit

Permalink
Move is_zone flag to stock.location picking_type M2o
Browse files Browse the repository at this point in the history
Replace occurences of zone for routing operation
  • Loading branch information
grindtildeath committed Sep 11, 2019
1 parent 63a5e32 commit 6a94a28
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 78 deletions.
2 changes: 1 addition & 1 deletion stock_picking_zone/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 2 additions & 4 deletions stock_picking_zone/demo/stock_location_demo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

<record id="stock_location_highbay_demo" model="stock.location">
<field name="name">Highbay</field>
<field name="location_id" model="stock.location"
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
<field name="location_id" ref="stock.stock_location_stock"/>
</record>
<record id="stock_location_highbay_a_demo" model="stock.location">
<field name="name">Bay A</field>
Expand All @@ -21,8 +20,7 @@

<record id="stock_location_handover_demo" model="stock.location">
<field name="name">Handover</field>
<field name="location_id" model="stock.location"
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"/>
<field name="location_id" ref="stock.stock_location_stock"/>
</record>

</odoo>
1 change: 1 addition & 0 deletions stock_picking_zone/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import stock_location
from . import stock_move
from . import stock_picking_type
from . import stock_quant
44 changes: 44 additions & 0 deletions stock_picking_zone/models/stock_location.py
Original file line number Diff line number Diff line change
@@ -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']
59 changes: 31 additions & 28 deletions stock_picking_zone/models/stock_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,36 +31,41 @@ 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():
new_moves = self.browse(new_move_ids)
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)
Expand All @@ -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()
Expand Down
61 changes: 24 additions & 37 deletions stock_picking_zone/models/stock_picking_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
1 change: 1 addition & 0 deletions stock_picking_zone/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
* Joël Grand-Guillaume <[email protected]>
* Guewen Baconnier <[email protected]>
* Akim Juillerat <[email protected]>
24 changes: 16 additions & 8 deletions stock_picking_zone/tests/test_picking_zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from odoo.tests import common


class TestPickingZone(common.SavepointCase):
class TestPickingTypeRoutingOperation(common.SavepointCase):

@classmethod
def setUpClass(cls):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)]
)
Expand All @@ -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)
Expand Down Expand Up @@ -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/
Expand Down Expand Up @@ -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'
Expand Down
13 changes: 13 additions & 0 deletions stock_picking_zone/views/stock_location.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_location_form_inherit" model="ir.ui.view">
<field name="name">stock.location.form.inherit</field>
<field name="model">stock.location</field>
<field name="inherit_id" ref="stock.view_location_form" />
<field name="arch" type="xml">
<field name="putaway_strategy_id" position="after">
<field name="routing_operation_picking_type_id" />
</field>
</field>
</record>
</odoo>

0 comments on commit 6a94a28

Please sign in to comment.