Skip to content

Commit

Permalink
Merge 6cf5977 into fcb0c7d
Browse files Browse the repository at this point in the history
  • Loading branch information
guewen committed Jan 7, 2020
2 parents fcb0c7d + 6cf5977 commit b1a1b9b
Show file tree
Hide file tree
Showing 29 changed files with 954 additions and 0 deletions.
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
16 changes: 16 additions & 0 deletions sale_stock_available_to_promise_release/__manifest__.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).

{
"name": "Stock Available to Promise Release - Sale Integration",
"version": "13.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
13 changes: 13 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,13 @@
# Copyright 2019 Camptocamp (https://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import models


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

def _prepare_procurement_values(self, group_id=False):
values = super()._prepare_procurement_values(group_id)
values["date_priority"] = self.order_id.date_order
return values
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
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
21 changes: 21 additions & 0 deletions stock_available_to_promise_release/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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": "13.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.",
)
147 changes: 147 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,147 @@
# 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.osv import expression
from odoo.tools import float_compare

from odoo.addons import decimal_precision as dp


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 self.need_release
and not self.product_id.type == "consu"
and not self.location_id.should_bypass_reservation()
)

def _action_cancel(self):
super()._action_cancel()
self.write({"need_release": False})
return True

def _ordered_available_to_promise(self):
if not self._should_compute_ordered_available_to_promise():
return 0.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.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),
]
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.0)
)
)
return promised_qty

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

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"
)
procurement_requests = []
pulled_moves = self.env["stock.move"]
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()
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 deliver unless we can deliver all at
# once
continue
move.with_context(release_available_to_promise=True)._split(remaining)

values = move._prepare_procurement_values()
procurement_requests.append(
self.env["procurement.group"].Procurement(
move.product_id,
move.product_uom_qty,
move.product_uom,
move.location_id,
move.rule_id and move.rule_id.name or "/",
move.origin,
move.company_id,
values,
)
)
pulled_moves |= move

self.env["procurement.group"].run_defer(procurement_requests)

while pulled_moves:
pulled_moves._action_assign()
pulled_moves = pulled_moves.mapped("move_orig_ids")

return True
21 changes: 21 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,21 @@
# 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)

def release_available_to_promise(self):
self.mapped("move_lines").release_available_to_promise()
66 changes: 66 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,66 @@
# 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 fields, models

_logger = logging.getLogger(__name__)


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

def _run_pull(self, procurements):
actions_to_run = []

for procurement, rule in procurements:
if (
not self.env.context.get("_rule_no_available_defer")
and rule.route_id.available_to_promise_defer_pull
# We still want to create the first part of the chain
and not rule.picking_type_id.code == "outgoing"
):
moves = procurement.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})
else:
actions_to_run.append((procurement, rule))

super()._run_pull(actions_to_run)
# use first of list of ids and browse it for performance
move_ids = [
move.id
for move in procurement.values.get("move_dest_ids", [])
for procurement, _rule in actions_to_run
]
if move_ids:
moves = self.env["stock.move"].browse(move_ids)
moves.filtered(lambda r: r.need_release).write({"need_release": False})
return True


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

def run_defer(self, procurements):
actions_to_run = []
for procurement in procurements:
values = procurement.values
values.setdefault("company_id", self.env.company)
values.setdefault("priority", "1")
values.setdefault("date_planned", fields.Datetime.now())
rule = self._get_rule(
procurement.product_id, procurement.location_id, procurement.values
)
if rule.action in ("pull", "pull_push"):
actions_to_run.append((procurement, rule))

if actions_to_run:
rule.with_context(_rule_no_available_defer=True)._run_pull(actions_to_run)
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 <guewen.baconnier@camptocamp.com>
28 changes: 28 additions & 0 deletions stock_available_to_promise_release/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Currently the reservation is performed by adding reserved quantities on quants,
which is fine as long as the reservation is made right after the order
confirmation. This way, the first arrived, first served principle is always
applied. But if you release warehouse operations in a chosen order (through
deliver round for example), then you need to be sure the reservations are made
in respect to the first arrived first served principle and not driven by the
order you choose to release your operations.

Allow each delivery move to mark a quantity as virtually reserved. Simple rule
would be first ordered, first served. More complex rules could be implemented.

When the reservation of a picking move occurs, the quantity that is reserved is
then based on the quantity that was promised to the customer (available to promise):

* The moves can be reserved in any order, the right quantity is always reserved
* The removal strategy is computed only when the reservation occurs. If you
reserve order 2 before order 1 (because you have/want to deliver order 2) you
can apply correctly fifo/fefo.

* For instance order 1 must be delivered in 1 month, order 2 must be delivered now.
* Virtually lock quantities to be able to serve order 1
* Reserve remaining quantity for order 2 and apply fefo

* Allow to limit the promised quantity in time. If a customer orders now for a
planned delivery in 2 months, then allow to not lock this quantity as
virtually reserved
* Allow to perform reservations jointly with your delivery rounds planning.
Reserve only the quants you planned to deliver.
7 changes: 7 additions & 0 deletions stock_available_to_promise_release/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
When an outgoing transfer would generate chained moves, it will not. The chained
moves need to be released manually. To do so, open "Inventory > Operations >
Stock Moves to Release", select the moves to release and use "action > Release
Stock Move". A move can be released only if the available to promise quantity is
greater than zero. This quantity is computed as the product's virtual quantity
minus the previous moves in the list (previous being defined by the field
"Priority Date").
1 change: 1 addition & 0 deletions stock_available_to_promise_release/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_reservation

0 comments on commit b1a1b9b

Please sign in to comment.