Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WMS][12.0] Add stock_picking_type_routing_operation - alpha version #639

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup/stock_picking_zone/odoo/addons/stock_picking_zone
6 changes: 6 additions & 0 deletions setup/stock_picking_zone/setup.py
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,
)
102 changes: 102 additions & 0 deletions stock_picking_zone/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
==================
Stock Picking Zone
==================

.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github
:target: https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_picking_zone
:alt: OCA/stock-logistics-warehouse
.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/153/12.0
:alt: Try me on Runbot

|badge1| |badge2| |badge3| |badge4|

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

**Table of contents**

.. contents::
:local:

Configuration
=============

In Inventory Settings, you must have:

* Storage Locations
* Multi-Warehouses
* Multi-Step Routes

Create an operation type and activate the "Is Zone" checkbox.
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.

Known issues / Roadmap
======================

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.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-warehouse/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* Camptocamp

Contributors
~~~~~~~~~~~~

* Joël Grand-Guillaume <[email protected]>
* Guewen Baconnier <[email protected]>

Maintainers
~~~~~~~~~~~

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_picking_zone>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions stock_picking_zone/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
22 changes: 22 additions & 0 deletions stock_picking_zone/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 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',
],
'demo': [
'demo/stock_location_demo.xml',
'demo/stock_picking_type_demo.xml',
],
'data': [
'views/stock_picking_type_views.xml',
],
'installable': True,
'development_status': 'Alpha',
}
28 changes: 28 additions & 0 deletions stock_picking_zone/demo/stock_location_demo.xml
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"/>
guewen marked this conversation as resolved.
Show resolved Hide resolved
</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').lot_stock_id.id"/>
guewen marked this conversation as resolved.
Show resolved Hide resolved
</record>

</odoo>
24 changes: 24 additions & 0 deletions stock_picking_zone/demo/stock_picking_type_demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?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="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>
3 changes: 3 additions & 0 deletions stock_picking_zone/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import stock_move
from . import stock_picking_type
from . import stock_quant
149 changes: 149 additions & 0 deletions stock_picking_zone/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)

from itertools import chain
from odoo import models


class StockMove(models.Model):
_inherit = 'stock.move'

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()

def _split_per_zone(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
# if needed.
move_lines = {}
for move_line in move.move_line_ids:
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 have to take products
zone_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.
continue

move._do_unreserve()
move_to_assign_ids.add(move.id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not using recordset directly? So, under you can avoid a browse()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have had awful performance issues with adding ids to a recordset in a loop. Maybe it's not an issue anymore in 12.0 though...

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
guewen marked this conversation as resolved.
Show resolved Hide resolved
# not a zone
if zone:
# 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)

# 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@guewen Using new context should be avoided as possible (especially in a loop).

Did you try to override the 'should_bypass_reservation()' function defined on location model ?
That one would bypass the calls to '_update_reserved_quantity' etc... on quant model. So, you can call your process to reserve.

For the exclude_apply_zone, would a test on move location_id is a zone on _action_assign will work ?

def _action_assign(self):
        super()._action_assign()
        moves_not_zone = self.filtered(lambda m: m.<is not a zone()>)
            moves = moves_not_zone._split_per_zone()
            moves._apply_move_location_zone()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll have a look.

# Prevent to call _apply_move_location_zone, will be called
# when all lines are processed.
exclude_apply_zone=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)
gather_in_location_id=location_id,
)._action_assign()

# reassign the moves which have been unreserved for the split
moves_to_assign = self.browse(move_to_assign_ids)
if moves_to_assign:
moves_to_assign._action_assign()
new_moves = self.browse(chain.from_iterable(
new_move_per_location.values()
))
return self + new_moves

def _apply_move_location_zone(self):
for move in self:
if move.state not in ('assigned', 'partially_available'):
continue

pick_type_model = self.env['stock.picking.type']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put it outside the loop


# 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
source = move.move_line_ids[0].location_id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer using first:

from odoo.fields import first
first(move.move_line_ids).location_id

zone = pick_type_model._find_zone_for_location(source)
if not zone:
continue
if (move.picking_type_id == zone and
move.location_dest_id == zone.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,
})
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)],
})
self.write({
'move_dest_ids': [(3, dest_move.id), (4, middle_move.id)],
})
middle_move._action_confirm(merge=False)

def _prepare_middle_move_values(self, destination):
return {
'picking_id': False,
'location_id': self.location_id.id,
'location_dest_id': destination.id,
'state': 'waiting',
'picking_type_id': self.picking_id.picking_type_id.id,
}
Loading