-
-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by jgrandguillaume
- Loading branch information
Showing
29 changed files
with
877 additions
and
0 deletions.
There are no files selected for viewing
1 change: 1 addition & 0 deletions
1
setup/stock_move_source_relocate/odoo/addons/stock_move_source_relocate
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_move_source_relocate |
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, | ||
) |
1 change: 1 addition & 0 deletions
1
...ve_source_relocate_dynamic_routing/odoo/addons/stock_move_source_relocate_dynamic_routing
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_move_source_relocate_dynamic_routing |
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,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"], | ||
} |
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,3 @@ | ||
from . import stock_move | ||
from . import stock_location | ||
from . import stock_source_relocate |
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,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) |
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,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
149
stock_move_source_relocate/models/stock_source_relocate.py
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,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 |
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,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. |
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 @@ | ||
* Guewen Baconnier <guewen.baconnier@camptocamp.com> |
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,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. |
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,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 |
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_source_relocate |
Oops, something went wrong.