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

[WIP][WMS][12.0] Available to Promise Release #709

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fccde3b
Add stock_virtual_reservation
guewen Sep 3, 2019
3b9a06f
Add virtual_reserved_qty field
guewen Sep 6, 2019
bcdc3cb
Defer creation of chained moves
guewen Sep 17, 2019
0ce3fc1
add creation of origin moves on assign (WIP)
guewen Sep 17, 2019
78114e0
Add todos
guewen Sep 23, 2019
5266f09
Fix backorders, add wizard
guewen Sep 27, 2019
a900e1f
Move fields around
guewen Sep 27, 2019
bd22ef3
Compute available per warehouse
guewen Sep 27, 2019
cd49bf4
Support "one" delivery type
guewen Sep 27, 2019
9850979
Add sale_stock_virtual_reservation_rule
guewen Sep 27, 2019
f25aad8
Track moves to release
guewen Sep 27, 2019
7f5ab0a
Set decimal precision on field
guewen Sep 27, 2019
8fb47cc
Add test for picking with several moves
guewen Sep 27, 2019
6df26bd
Fix stock qty and assign
guewen Sep 27, 2019
95f390a
Fix different UOM
guewen Sep 27, 2019
dc81494
Add readme
guewen Sep 27, 2019
f02ea74
Rename to stock_available_to_promise_release
guewen Nov 4, 2019
d501d75
Move release option from warehouse to location route
guewen Nov 4, 2019
1581146
Remove unused field
guewen Nov 5, 2019
836668f
Fix test
guewen Nov 5, 2019
54f5641
fixup! Rename to stock_available_to_promise_release
guewen Nov 5, 2019
d9b04e2
Improve UI: group moves to release by picking
guewen Nov 5, 2019
c13ad92
Use sale's confirmation date as priority date
guewen Nov 6, 2019
651d86e
Rename field for clarity
guewen Nov 6, 2019
6a9cd38
Rename method for clarity
guewen Nov 6, 2019
84cd0ba
Exclude canceled moves from computation
guewen Nov 19, 2019
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 sale_stock_available_to_promise_release/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
20 changes: 20 additions & 0 deletions sale_stock_available_to_promise_release/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

{
'name': 'Stock Available to Promise Release - Sale Integration',
'version': '12.0.1.0.0',
'summary': 'Integration between Sales and Available to Promise Release',
'author': "Camptocamp,Odoo Community Association (OCA)",
'category': 'Stock Management',
'depends': [
'sale_stock',
'stock_available_to_promise_release',
],
'data': [
],
'installable': True,
'license': 'AGPL-3',
'application': False,
'development_status': 'Alpha',
}
1 change: 1 addition & 0 deletions sale_stock_available_to_promise_release/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import sale_order_line
14 changes: 14 additions & 0 deletions sale_stock_available_to_promise_release/models/sale_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import api, models


class SaleOrderLine(models.Model):
_inherit = "sale.order.line"

@api.multi
def _prepare_procurement_values(self, group_id=False):
values = super()._prepare_procurement_values(group_id)
values['date_priority'] = self.order_id.confirmation_date
return values
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Guewen Baconnier <[email protected]>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Integrate the Release of Operation based on Available to Promise with Sales. The Priority Date of Stock
Moves will be equal to the confirmation date of their sales order.
6 changes: 6 additions & 0 deletions setup/sale_stock_available_to_promise_release/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,
)
6 changes: 6 additions & 0 deletions setup/stock_available_to_promise_release/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,
)
2 changes: 2 additions & 0 deletions stock_available_to_promise_release/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizards
23 changes: 23 additions & 0 deletions stock_available_to_promise_release/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

{
'name': 'Stock Available to Promise Release',
'version': '12.0.1.0.0',
'summary': 'Release Operations based on available to promise',
'author': "Camptocamp,Odoo Community Association (OCA)",
'category': 'Stock Management',
'depends': [
'stock',
],
'data': [
'views/stock_move_views.xml',
'views/stock_picking_views.xml',
'views/stock_location_route_views.xml',
'wizards/stock_move_release_views.xml',
],
'installable': True,
'license': 'AGPL-3',
'application': False,
'development_status': 'Alpha',
}
4 changes: 4 additions & 0 deletions stock_available_to_promise_release/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import stock_move
from . import stock_location_route
from . import stock_picking
from . import stock_rule
16 changes: 16 additions & 0 deletions stock_available_to_promise_release/models/stock_location_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import fields, models


class Route(models.Model):
_inherit = 'stock.location.route'

available_to_promise_defer_pull = fields.Boolean(
string="Release based on Available to Promise",
default=False,
help="Do not create chained moved automatically for delivery. "
"Transfers must be released manually when they have enough available"
" to promise.",
)
154 changes: 154 additions & 0 deletions stock_available_to_promise_release/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import api, fields, models
from odoo.addons import decimal_precision as dp
from odoo.osv import expression
from odoo.tools import float_compare


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

date_priority = fields.Datetime(
string="Priority Date",
index=True,
default=fields.Datetime.now,
help="Date/time used to sort moves to deliver first. "
"Used to calculate the ordered available to promise.",
)
ordered_available_to_promise = fields.Float(
"Ordered Available to Promise",
compute="_compute_ordered_available_to_promise",
digits=dp.get_precision("Product Unit of Measure"),
help="Available to Promise quantity minus quantities promised "
" to older promised operations.",
)
need_release = fields.Boolean()

@api.depends()
def _compute_ordered_available_to_promise(self):
for move in self:
move.ordered_available_to_promise = (
move._ordered_available_to_promise()
)

def _should_compute_ordered_available_to_promise(self):
return (
self.picking_code == "outgoing"
and not self.product_id.type == "consu"
and not self.location_id.should_bypass_reservation()
)

def _ordered_available_to_promise(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this method not part of _compute_ordered_available_to_promise ? Retrieving the value can be done by reading the field.

Copy link
Member Author

Choose a reason for hiding this comment

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

As we can't have proper depends, if values change in the transaction, the field will not be recomputed.
We could add invalidations everywhere, not sure that's useful. The field is good for the view, but the code better has to recompute the value on every access. I should add a docstring about this.

if not self._should_compute_ordered_available_to_promise():
return 0.
available = self.product_id.with_context(
location=self.warehouse_id.lot_stock_id.id
).virtual_available
return max(
min(available - self._previous_promised_qty(), self.product_qty),
0.,
)

def _previous_promised_quantity_domain(self):
domain = [
("need_release", "=", True),
("product_id", "=", self.product_id.id),
("date_priority", "<=", self.date_priority),
("warehouse_id", "=", self.warehouse_id.id),
]
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally we should exclude all the moves planned in the future that you can resupply on time. For example, there is no reason a delivery planned 3 months is blocking quantity while you can resupply in 7 days.
Simple solution would be configuring an horizon for considering blocking moves. A more complex and accurate solution would be by looking at the warehouse product orderpoint planned date.
To support sales order commitment dates, something must be done

Copy link
Member

Choose a reason for hiding this comment

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

We go for the horizon solution here

return domain

def _previous_promised_qty(self):
previous_moves = self.search(
expression.AND(
[
self._previous_promised_quantity_domain(),
[("id", "!=", self.id)],
]
)
)
promised_qty = sum(
previous_moves.mapped(
lambda move: max(
move.product_qty - move.reserved_availability, 0.
)
)
)
return promised_qty

@api.multi
def release_available_to_promise(self):
self._run_stock_rule()

def _prepare_move_split_vals(self, qty):
vals = super()._prepare_move_split_vals(qty)
# The method set procure_method as 'make_to_stock' by default on split,
# but we want to keep 'make_to_order' for chained moves when we split
# a partially available move in _run_stock_rule().
if self.env.context.get("release_available_to_promise"):
vals.update(
{"procure_method": self.procure_method, "need_release": True}
)
return vals

@api.multi
def _run_stock_rule(self):
"""Launch procurement group run method with remaining quantity

As we only generate chained moves for the quantity available minus the
quantity promised to older moves, to delay the reservation at the
latest, we have to periodically retry to assign the remaining
quantities.
"""
precision = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
for move in self:
if not move.need_release:
continue
if move.state not in ("confirmed", "waiting"):
continue
# do not use the computed field, because it will keep
# a value in cache that we cannot invalidate declaratively
available_quantity = move._ordered_available_to_promise()
Copy link
Contributor

Choose a reason for hiding this comment

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

use the field "move._ordered_available_to_promise" instead

Copy link
Member Author

@guewen guewen Nov 8, 2019

Choose a reason for hiding this comment

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

There is the explanation in the comment just above why the field is not used ;)
I can rephrase if not understandable enough.

if (
float_compare(
available_quantity, 0, precision_digits=precision
)
<= 0
):
continue

quantity = min(move.product_qty, available_quantity)
remaining = move.product_qty - quantity

if float_compare(remaining, 0, precision_digits=precision) > 0:
if move.picking_id.move_type == "one":
# we don't want to delivery unless we can deliver all at
# once
continue
move.with_context(release_available_to_promise=True)._split(
remaining
)

values = move._prepare_procurement_values()

self.env["procurement.group"].run_defer(
move.product_id,
move.product_id.uom_id._compute_quantity(
quantity, move.product_uom, rounding_method="HALF-UP"
),
move.product_uom,
move.location_id,
move.origin,
values,
)

pull_move = move
while pull_move:
pull_move._action_assign()
pull_move = pull_move.move_orig_ids

return True
24 changes: 24 additions & 0 deletions stock_available_to_promise_release/models/stock_picking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import api, fields, models


class StockPicking(models.Model):
_inherit = "stock.picking"

# Add store on the field, as it is quite used in the searches,
# and this is an easy-win to reduce the number of SQL queries.
picking_type_code = fields.Selection(store=True)
need_release = fields.Boolean(compute="_compute_need_release")

@api.depends("move_lines.need_release")
def _compute_need_release(self):
for picking in self:
picking.need_release = any(
move.need_release for move in picking.move_lines
)

@api.multi
def release_available_to_promise(self):
self.mapped("move_lines").release_available_to_promise()
83 changes: 83 additions & 0 deletions stock_available_to_promise_release/models/stock_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging

from odoo import api, fields, models

_logger = logging.getLogger(__name__)


class StockRule(models.Model):
_inherit = "stock.rule"

def _run_pull(
self,
product_id,
product_qty,
product_uom,
location_id,
name,
origin,
values,
):
if (
not self.env.context.get("_rule_no_available_defer")
and self.route_id.available_to_promise_defer_pull
# We still want to create the first part of the chain
and not self.picking_type_id.code == "outgoing"
):
moves = values.get("move_dest_ids")
# Track the moves that needs to have their pull rule
# done. Before the 'pull' is done, we don't know the
# which route is chosen. We update the destination
# move (ie. the outgoing) when the current route
# defers the pull rules and return so we don't create
# the next move of the chain (pick or pack).
if moves:
moves.write({"need_release": True})
return True

super()._run_pull(
product_id,
product_qty,
product_uom,
location_id,
name,
origin,
values,
)
moves = values.get("move_dest_ids")
if moves:
moves.filtered(lambda r: r.need_release).write(
{"need_release": False}
)
return True


class ProcurementGroup(models.Model):
_inherit = "procurement.group"

@api.model
def run_defer(
self, product_id, product_qty, product_uom, location_id, origin, values
):
values.setdefault(
"company_id",
self.env["res.company"]._company_default_get("procurement.group"),
)
values.setdefault("priority", "1")
values.setdefault("date_planned", fields.Datetime.now())
rule = self._get_rule(product_id, location_id, values)
if not rule or rule.action not in ("pull", "pull_push"):
return

rule.with_context(_rule_no_available_defer=True)._run_pull(
product_id,
product_qty,
product_uom,
location_id,
rule.name,
origin,
values,
)
return True
2 changes: 2 additions & 0 deletions stock_available_to_promise_release/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
In Inventory > Configuration > Warehouses, activate the option "Release based on Available to Promise"
when you want to use the feature.
1 change: 1 addition & 0 deletions stock_available_to_promise_release/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Guewen Baconnier <[email protected]>
Loading