-
-
Notifications
You must be signed in to change notification settings - Fork 729
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
When a move is assigned and its source location (or one of its parent) corresponds to a picking type flagged "is_zone", a new move will be inserted with this picking type.
- Loading branch information
Showing
14 changed files
with
386 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../stock_picking_zone |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import setuptools | ||
|
||
setuptools.setup( | ||
setup_requires=['setuptools-odoo'], | ||
odoo_addon=True, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Copyright 2019 Camptocamp (https://www.camptocamp.com) | ||
{ | ||
'name': "Stock Picking Zone", | ||
'summary': """Warehouse Operations By Zones""", | ||
'author': 'Camptocamp, Odoo Community Association (OCA)', | ||
'website': "https://github.com/OCA/stock-logistics-warehouse", | ||
'category': 'Warehouse Management', | ||
'version': '12.0.1.0.0', | ||
'license': 'AGPL-3', | ||
'depends': [ | ||
'stock', | ||
], | ||
'data': [ | ||
'views/stock_picking_type_views.xml', | ||
'demo/stock_location_demo.xml', | ||
'demo/stock_picking_type_demo.xml', | ||
], | ||
'installable': True, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<odoo noupdate="1"> | ||
|
||
<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"/> | ||
</record> | ||
<record id="stock_location_highbay_a_demo" model="stock.location"> | ||
<field name="name">Bay A</field> | ||
<field name="location_id" ref="stock_location_highbay_demo"/> | ||
</record> | ||
<record id="stock_location_highbay_a_1_demo" model="stock.location"> | ||
<field name="name">Bin 1</field> | ||
<field name="location_id" ref="stock_location_highbay_a_demo"/> | ||
</record> | ||
<record id="stock_location_highbay_demo_1_2" model="stock.location"> | ||
<field name="name">Bin 2</field> | ||
<field name="location_id" ref="stock_location_highbay_a_demo"/> | ||
</record> | ||
|
||
<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').view_location_id.id"/> | ||
</record> | ||
|
||
</odoo> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<odoo noupdate="1"> | ||
|
||
<record id="sequence_stock_picking_type_highbay_handover_demo" model="ir.sequence"> | ||
<field name="name">Highbay Handover</field> | ||
<field name="code">stock.ho</field> | ||
<field name="prefix">HO/</field> | ||
<field name="padding">5</field> | ||
<field name="number_next">1</field> | ||
<field name="number_increment">1</field> | ||
</record> | ||
|
||
<record model="stock.picking.type" id="stock_picking_type_highbay_handover_demo"> | ||
<field name="name">Highbay Handover</field> | ||
<field name="code">internal</field> | ||
<field name="use_create_lots" eval="False"/> | ||
<field name="use_existing_lots" eval="False"/> | ||
<field name="use_existing_lots" eval="True"/> | ||
<field name="default_location_src_id" ref="stock_picking_zone.stock_location_highbay_demo"/> | ||
<field name="default_location_dest_id" ref="stock_picking_zone.stock_location_handover_demo"/> | ||
<field name="is_zone" eval="True"/> | ||
<field name="sequence_id" ref="stock_picking_zone.sequence_stock_picking_type_highbay_handover_demo"/> | ||
</record> | ||
|
||
</odoo> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from . import stock_move | ||
from . import stock_picking_type |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# Copyright 2019 Camptocamp (https://www.camptocamp.com) | ||
|
||
from odoo import models | ||
|
||
|
||
class StockMove(models.Model): | ||
_inherit = 'stock.move' | ||
|
||
def _action_assign(self): | ||
super()._action_assign() | ||
self._apply_move_location_zone() | ||
|
||
def _apply_move_location_zone(self): | ||
for move in self: | ||
if move.state != 'assigned': | ||
continue | ||
pick_type_model = self.env['stock.picking.type'] | ||
# TODO what if we have more than one move line? | ||
# split? | ||
source = move.move_line_ids[0].location_id | ||
zone = pick_type_model._find_zone_for_location(source) | ||
if not zone: | ||
continue | ||
if move.location_dest_id == zone.default_location_dest_id: | ||
continue | ||
move._do_unreserve() | ||
move.write({ | ||
'location_dest_id': zone.default_location_dest_id.id, | ||
'picking_type_id': zone.id, | ||
}) | ||
move._insert_middle_moves() | ||
move._assign_picking() | ||
move._action_assign() | ||
|
||
def _insert_middle_moves(self): | ||
self.ensure_one() | ||
dest_moves = self.move_dest_ids | ||
dest_location = self.location_dest_id | ||
for dest_move in dest_moves: | ||
final_location = dest_move.location_id | ||
if dest_location == final_location: | ||
# shortcircuit to avoid a query checking if it is a child | ||
continue | ||
child_locations = self.env['stock.location'].search([ | ||
('id', 'child_of', final_location.id) | ||
]) | ||
if dest_location in child_locations: | ||
# normal behavior, we don't need a move between A and B | ||
continue | ||
# Insert move between the source and destination for the new | ||
# operation | ||
middle_move_values = self._prepare_middle_move_values( | ||
final_location | ||
) | ||
middle_move = self.copy(middle_move_values) | ||
dest_move.write({ | ||
'move_orig_ids': [(3, self.id), (4, middle_move.id)], | ||
}) | ||
# FIXME: if we have more than one move line on a move, | ||
# the move will only have the dest of the last one. | ||
# We have to split the move. | ||
self.write({ | ||
'move_dest_ids': [(3, dest_move.id), (4, middle_move.id)], | ||
}) | ||
middle_move._action_confirm() | ||
|
||
def _prepare_middle_move_values(self, destination): | ||
return { | ||
'picking_id': False, | ||
'location_id': self.location_dest_id.id, | ||
'location_dest_id': destination.id, | ||
'state': 'waiting', | ||
'picking_type_id': self.picking_id.picking_type_id.id, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# 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 zone in self: | ||
src_location = zone.default_location_src_id | ||
domain = [ | ||
('is_zone', '=', True), | ||
('default_location_src_id', '=', src_location.id), | ||
('id', '!=', zone.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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
* Joël Grand-Guillaume <[email protected]> | ||
* Guewen Baconnier <[email protected]> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import test_picking_zone |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
# Copyright 2019 Camptocamp (https://www.camptocamp.com) | ||
|
||
from odoo.tests import common | ||
|
||
|
||
class TestPickingZone(common.SavepointCase): | ||
|
||
@classmethod | ||
def setUpClass(cls): | ||
super().setUpClass() | ||
cls.partner_delta = cls.env.ref('base.res_partner_4') | ||
cls.wh = cls.env['stock.warehouse'].create({ | ||
'name': 'Base Warehouse', | ||
'reception_steps': 'one_step', | ||
'delivery_steps': 'pick_ship', | ||
'code': 'WHTEST', | ||
}) | ||
|
||
cls.customer_loc = cls.env.ref('stock.stock_location_customers') | ||
cls.location_hb = cls.env['stock.location'].create({ | ||
'name': 'Highbay', | ||
'location_id': cls.wh.lot_stock_id.id, | ||
}) | ||
cls.location_hb_1 = cls.env['stock.location'].create({ | ||
'name': 'Highbay Shelve 1', | ||
'location_id': cls.location_hb.id, | ||
}) | ||
cls.location_hb_1_1 = cls.env['stock.location'].create({ | ||
'name': 'Highbay Shelve 1 Bin 1', | ||
'location_id': cls.location_hb_1.id, | ||
}) | ||
cls.location_hb_1_2 = cls.env['stock.location'].create({ | ||
'name': 'Highbay Shelve 1 Bin 2', | ||
'location_id': cls.location_hb_1.id, | ||
}) | ||
|
||
cls.location_handover = cls.env['stock.location'].create({ | ||
'name': 'Handover', | ||
'location_id': cls.wh.view_location_id.id, | ||
}) | ||
|
||
cls.product_a = cls.env['product.product'].create({ | ||
'name': 'Product A', 'type': 'product', | ||
}) | ||
|
||
picking_type_sequence = cls.env['ir.sequence'].create({ | ||
'name': 'WH/Handover', | ||
'prefix': 'WH/HO/', | ||
'padding': 5, | ||
'company_id': cls.wh.company_id.id, | ||
}) | ||
cls.pick_type_zone = cls.env['stock.picking.type'].create({ | ||
'name': 'Zone', | ||
'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, | ||
}) | ||
|
||
def _create_pick_ship(self, wh): | ||
customer_picking = self.env['stock.picking'].create({ | ||
'location_id': wh.wh_output_stock_loc_id.id, | ||
'location_dest_id': self.customer_loc.id, | ||
'partner_id': self.partner_delta.id, | ||
'picking_type_id': wh.out_type_id.id, | ||
}) | ||
dest = self.env['stock.move'].create({ | ||
'name': self.product_a.name, | ||
'product_id': self.product_a.id, | ||
'product_uom_qty': 10, | ||
'product_uom': self.product_a.uom_id.id, | ||
'picking_id': customer_picking.id, | ||
'location_id': wh.wh_output_stock_loc_id.id, | ||
'location_dest_id': self.customer_loc.id, | ||
'state': 'waiting', | ||
'procure_method': 'make_to_order', | ||
}) | ||
|
||
pick_picking = self.env['stock.picking'].create({ | ||
'location_id': wh.lot_stock_id.id, | ||
'location_dest_id': wh.wh_output_stock_loc_id.id, | ||
'partner_id': self.partner_delta.id, | ||
'picking_type_id': wh.pick_type_id.id, | ||
}) | ||
|
||
self.env['stock.move'].create({ | ||
'name': self.product_a.name, | ||
'product_id': self.product_a.id, | ||
'product_uom_qty': 10, | ||
'product_uom': self.product_a.uom_id.id, | ||
'picking_id': pick_picking.id, | ||
'location_id': wh.lot_stock_id.id, | ||
'location_dest_id': wh.wh_output_stock_loc_id.id, | ||
'move_dest_ids': [(4, dest.id)], | ||
'state': 'confirmed', | ||
}) | ||
return pick_picking, customer_picking | ||
|
||
def _update_product_qty_in_location(self, location, product, quantity): | ||
self.env['stock.quant']._update_available_quantity( | ||
product, location, quantity | ||
) | ||
|
||
def test_change_location_to_zone(self): | ||
|
||
pick_picking, customer_picking = self._create_pick_ship(self.wh) | ||
move_a = pick_picking.move_lines | ||
move_b = customer_picking.move_lines | ||
|
||
self._update_product_qty_in_location( | ||
self.location_hb_1_2, move_a.product_id, 100 | ||
) | ||
pick_picking.action_assign() | ||
|
||
ml = move_a.move_line_ids | ||
self.assertEqual(len(ml), 1) | ||
self.assertEqual(ml.location_id, self.location_hb_1_2) | ||
self.assertEqual(ml.location_dest_id, self.location_handover) | ||
|
||
self.assertEqual(ml.picking_id.picking_type_id, self.pick_type_zone) | ||
|
||
self.assertEqual(move_a.location_id, self.wh.lot_stock_id) | ||
self.assertEqual(move_a.location_dest_id, self.location_handover) | ||
# the move stays B stays on the same source location (sticky) | ||
self.assertEqual(move_b.location_id, self.wh.wh_output_stock_loc_id) | ||
self.assertEqual(move_b.location_dest_id, self.customer_loc) | ||
|
||
move_middle = move_a.move_dest_ids | ||
self.assertEqual(move_middle.location_id, move_a.location_dest_id) | ||
self.assertEqual(move_middle.location_dest_id, move_b.location_id) | ||
|
||
self.assertEqual( | ||
move_a.picking_id.location_dest_id, | ||
self.location_handover | ||
) | ||
self.assertEqual( | ||
move_middle.picking_id.location_id, | ||
self.location_handover | ||
) | ||
|
||
self.assertEqual(move_a.state, 'assigned') | ||
self.assertEqual(move_middle.state, 'waiting') | ||
self.assertEqual(move_b.state, 'waiting') |
Oops, something went wrong.