Skip to content

Commit

Permalink
Merge PR #28 into 13.0
Browse files Browse the repository at this point in the history
Signed-off-by jgrandguillaume
  • Loading branch information
OCA-git-bot committed Jul 6, 2020
2 parents 1d8e610 + 2b95709 commit cd9379e
Show file tree
Hide file tree
Showing 29 changed files with 877 additions and 0 deletions.
6 changes: 6 additions & 0 deletions setup/stock_move_source_relocate/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_move_source_relocate_dynamic_routing/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,
)
1 change: 1 addition & 0 deletions stock_move_source_relocate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
16 changes: 16 additions & 0 deletions stock_move_source_relocate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
{
"name": "Stock Move Source Relocation",
"summary": "Change source location of unavailable moves",
"version": "13.0.1.0.0",
"development_status": "Alpha",
"category": "Warehouse Management",
"website": "https://github.com/OCA/wms",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": ["stock"],
"data": ["views/stock_source_relocate_views.xml", "security/ir.model.access.csv"],
}
3 changes: 3 additions & 0 deletions stock_move_source_relocate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import stock_move
from . import stock_location
from . import stock_source_relocate
14 changes: 14 additions & 0 deletions stock_move_source_relocate/models/stock_location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models


class StockLocation(models.Model):
_inherit = "stock.location"

def is_sublocation_of(self, others):
"""Return True if self is a sublocation of at least one other"""
self.ensure_one()
# Efficient way to verify that the current location is
# below one of the other location without using SQL.
return any(self.parent_path.startswith(other.parent_path) for other in others)
69 changes: 69 additions & 0 deletions stock_move_source_relocate/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)

from odoo import models
from odoo.tools.float_utils import float_is_zero


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

def _action_assign(self):
unconfirmed_moves = self.filtered(
lambda m: m.state in ["confirmed", "partially_available"]
)
super()._action_assign()
# could not be (entirely) reserved
unconfirmed_moves = unconfirmed_moves.filtered(
lambda m: m.state in ["confirmed", "partially_available"]
)
unconfirmed_moves._apply_source_relocate()

def _apply_source_relocate(self):
# Read the `reserved_availability` field of the moves out of the loop
# to prevent unwanted cache invalidation when actually reserving.
reserved_availability = {move: move.reserved_availability for move in self}
roundings = {move: move.product_id.uom_id.rounding for move in self}
for move in self:
# We don't need to ignore moves with "_should_bypass_reservation()
# is True" because they are reserved at this point.
relocation = self.env["stock.source.relocate"]._rule_for_move(move)
if not relocation or relocation.relocate_location_id == move.location_id:
continue
move._apply_source_relocate_rule(
relocation, reserved_availability, roundings
)

def _apply_source_relocate_rule(self, relocation, reserved_availability, roundings):
relocated = self.env["stock.move"].browse()

rounding = roundings[self]
if not reserved_availability[self]:
# nothing could be reserved, however, we want to source the
# move on the specific relocation (for replenishment), so
# update it's source location
self.location_id = relocation.relocate_location_id
relocated = self
else:
missing_reserved_uom_quantity = (
self.product_uom_qty - reserved_availability[self]
)
need = self.product_uom._compute_quantity(
missing_reserved_uom_quantity,
self.product_id.uom_id,
rounding_method="HALF-UP",
)

if float_is_zero(need, precision_rounding=rounding):
return relocated

# A part of the quantity could be reserved in the original
# location, so keep this part in the move and split the rest
# in a new move, where will take the goods in the relocation
new_move_id = self._split(need)
# recheck first move which should now be available
new_move = self.browse(new_move_id)
new_move.location_id = relocation.relocate_location_id
self._action_assign()
relocated = new_move
return relocated
149 changes: 149 additions & 0 deletions stock_move_source_relocate/models/stock_source_relocate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.osv import expression
from odoo.tools.safe_eval import safe_eval

_logger = logging.getLogger(__name__)


def _default_sequence(record):
maxrule = record.search([], order="sequence desc", limit=1)
if maxrule:
return maxrule.sequence + 10
else:
return 0


class StockSourceRelocate(models.Model):
"""Rules for move source relocating
Each rule can have many removal rules, they configure the conditions and
advanced removal strategies to apply on a specific location (sub-location
of the rule).
The rules are selected for a move based on their source location and a
configurable domain on the rule.
"""

_name = "stock.source.relocate"
_description = "Stock Move Source Relocate"
_order = "sequence, id"

sequence = fields.Integer(default=lambda s: _default_sequence(s))
active = fields.Boolean(default=True)
company_id = fields.Many2one(
comodel_name="res.company", default=lambda self: self.env.user.company_id.id
)

location_id = fields.Many2one(comodel_name="stock.location", required=True)
relocate_location_id = fields.Many2one(comodel_name="stock.location", required=True)
picking_type_id = fields.Many2one(comodel_name="stock.picking.type", required=True)

rule_domain = fields.Char(
string="Rule Domain",
default=[],
help="Domain based on Stock Moves, to define if the "
"rule is applicable or not.",
)
rule_message = fields.Html(compute="_compute_rule_message")

@api.constrains("relocate_location_id")
def _constraint_relocate_location_id(self):
"""The relocate location has to be a child of the main location."""
for rule in self:
if not rule.relocate_location_id.is_sublocation_of(rule.location_id):
msg = _("Relocate location has to be a sub-location of '{}'.").format(
rule.location_id.display_name
)
raise ValidationError(msg)

def _rule_message_template(self):
message = _(
"When a move with operation type "
"<strong>{rule.picking_type_id.display_name}</strong>"
" is inside the location"
" <strong>{rule.location_id.display_name}</strong> and a check of"
" availability returns no reservation, the move is relocated"
" to the location"
" <strong>{rule.relocate_location_id.display_name}</strong>"
" (source location changed).<br/>"
"If a move is partially unavailable, the move is split in two"
" parts:<ul>"
"<li>the available part is adjusted to the reserved quantity,"
" and its source location stays the same </li>"
"<li>the unavailable part is split in a new move in the"
" relocation location</li>"
"</ul>"
)
# we need to eval the domain to see if it's not "[]"
if safe_eval(self.rule_domain) or []:
message += _(
"<br/>"
"This rule is applied only if the <strong>domain</strong>"
" matches with the move."
)
return message

@api.depends(
"location_id", "relocate_location_id", "picking_type_id", "rule_domain"
)
def _compute_rule_message(self):
"""Generate dynamically describing the rule for humans"""
for rule in self:
if not (
rule.picking_type_id and rule.location_id and rule.relocate_location_id
):
rule.rule_message = ""
continue
rule.rule_message = rule._rule_message_template().format(rule=rule)

def name_get(self):
res = []
for record in self:
res.append(
(
record.id,
"{} → {}".format(
self.location_id.display_name,
self.relocate_location_id.display_name,
),
)
)
return res

def _rule_for_move(self, move):
rules = self.search(
[
("picking_type_id", "=", move.picking_type_id.id),
("location_id", "parent_of", move.location_id.id),
]
)
for rule in rules:
if rule._is_rule_applicable(move):
return rule
return self.browse()

def _eval_rule_domain(self, move, domain):
move_domain = [("id", "=", move.id)]
# Warning: if we build a domain with dotted path such
# as group_id.is_urgent (hypothetic field), can become very
# slow as odoo searches all "procurement.group.is_urgent" first
# then uses "IN group_ids" on the stock move only.
# In such situations, it can be better either to add a related
# field on the stock.move, either extend _eval_rule_domain to
# add your own logic (based on SQL, ...).
return bool(
self.env["stock.move"].search(
expression.AND([move_domain, domain]), limit=1
)
)

def _is_rule_applicable(self, move):
domain = safe_eval(self.rule_domain) or []
if domain:
return self._eval_rule_domain(move, domain)
return True
13 changes: 13 additions & 0 deletions stock_move_source_relocate/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
The configuration of the source relocations is done in "Inventory > Configuration > Source Relocation".

Creation of a rule:

Properties that define where the rule will be applied:

* Location: any unreserved move in this location or sub-location is relocated
* Picking Type: any unreserved move in this picking type is relocated
* Rule Domain: filter the moves to relocate with arbitrary domains

Note: all of the above must be met to relocate a move.

The Relocate Location field defines what the move source location will be changed to. It must be a sub-location of the location.
1 change: 1 addition & 0 deletions stock_move_source_relocate/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Guewen Baconnier <guewen.baconnier@camptocamp.com>
31 changes: 31 additions & 0 deletions stock_move_source_relocate/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Relocate source location of unconfirmed moves

Add relocation rules for moves.

Some use cases:

* Handle all the replenishments at the same place
* Trigger minimum stock rules or DDMRP buffers in one location

Behavior:

* When we try to assign a stock move and the move is not available, a rule
matching the source location (sub-locations included), the picking type and an
optional domain is searched
* If a relocation is found, the move source location is updated with the new one
* If the move was partially available, it is split in 2 parts:

* one available part which keeps its source location
* one confirmed part which is updated with the new source location

Notes:

Goes well with ``stock_available_to_promise_release``.
When using the mentioned module, we assume that we release moves (which
creates the whole chain of moves) only when we know that we have the
quantities in stock (otherwise the module splits the delivery). So generally,
we have the goods are available, but maybe not at the correct place: this
module is handy to organize internal replenishments.

Compatible with ``stock_dynamic_routing``: when the source location is updated
by this module, a dynamic routing may be applied.
3 changes: 3 additions & 0 deletions stock_move_source_relocate/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_stock_source_relocate_stock_user,access_stock_source_relocate stock user,model_stock_source_relocate,stock.group_stock_user,1,0,0,0
access_stock_source_relocate_manager,access_stock_source_relocate stock manager,model_stock_source_relocate,stock.group_stock_manager,1,1,1,1
1 change: 1 addition & 0 deletions stock_move_source_relocate/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_source_relocate
Loading

0 comments on commit cd9379e

Please sign in to comment.