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

[14.0][IMP] stock_reserve_rule: add full lot strategy #1834

Closed
wants to merge 11 commits into from
18 changes: 15 additions & 3 deletions stock_reserve_rule/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Stock Reservation Rules
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:80f49ac641c2a8bede920d6378719dcedc8e81d45ee91c5388c8450d5c526d26
!! source digest: sha256:f0f580bceed65878a874bcbfe6aaf70570a3821643867b635e36496e354e1df1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
Expand Down Expand Up @@ -51,6 +51,18 @@ The included advanced removal strategies are:
* Full Packaging: tries to remove full packaging (configured on the products)
first, by largest to smallest package or based on a pre-selected package
(default removal strategy is then applied for equal quantities).
* Full lot: tries to remove a complete lot.
This strategy requires to select if a tolerance should be applied on lot selection,
allowing to select lots with qty higher or lower than qty requested in picking.
Rules can be applied in sequence, for example, to first check for exact lot qty,
then for a lot with qty 5% higher than requested, then for a lot with qty 10% higher
than requested, and so on.

If lot selected must have same qty as requested, set "Tolerance on = No tolerance";
otherwise it's possible to select a lot with higher qty (Tolerance on = Upper Limit")
or lower qty ("Lower Limit"), either in percentage of qty or absolute value.
Please note that three "No tolerance" or "Upper limit" or "Lower limit" rules.


Examples of scenario:

Expand Down Expand Up @@ -128,8 +140,7 @@ Scenario:
and see the rules (by default in demo, the rules are created inactive)
* Open Transfer: Outgoing shipment (reservation rules demo 1)
* Check availability: it has 150 units, as it will not empty Zone A, it will not
take products there, it should take 100 in B and 50 in C (following the rules
order)
take products there, it should take 100 in B and 50 in C (following the rules order)
* Unreserve this transfer (to test the second case)
* Open Transfer: Outgoing shipment (reservation rules demo 2)
* Check availability: it has 250 units, it can empty Zone A, it will take 200 in
Expand Down Expand Up @@ -160,6 +171,7 @@ Contributors

* Guewen Baconnier <[email protected]>
* Jacques-Etienne Baudoux (BCIM) <[email protected]>
* Cetmix <https://cetmix.com>

Maintainers
~~~~~~~~~~~
Expand Down
8 changes: 8 additions & 0 deletions stock_reserve_rule/demo/stock_location_demo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
<field name="name">Zone C</field>
<field name="location_id" ref="stock.stock_location_stock" />
</record>
<record id="stock_location_zone_d_demo" model="stock.location">
<field name="name">Zone D</field>
<field name="location_id" ref="stock.stock_location_stock" />
</record>
<record id="stock_location_zone_a_bin_1_demo" model="stock.location">
<field name="name">Bin A1</field>
<field name="location_id" ref="stock_location_zone_a_demo" />
Expand All @@ -24,4 +28,8 @@
<field name="name">Bin C1</field>
<field name="location_id" ref="stock_location_zone_c_demo" />
</record>
<record id="stock_location_zone_d_bin_1_demo" model="stock.location">
<field name="name">Bin D1</field>
<field name="location_id" ref="stock_location_zone_d_demo" />
</record>
</odoo>
6 changes: 6 additions & 0 deletions stock_reserve_rule/demo/stock_reserve_rule_demo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@
<field name="location_id" ref="stock_location_zone_c_demo" />
<field name="removal_strategy">default</field>
</record>
<record id="stock_reserve_rule_4_removal_demo" model="stock.reserve.rule.removal">
<field name="rule_id" ref="stock_reserve_rule_1_demo" />
<field name="sequence">4</field>
<field name="location_id" ref="stock_location_zone_d_demo" />
<field name="removal_strategy">full_lot</field>
</record>
</odoo>
10 changes: 10 additions & 0 deletions stock_reserve_rule/models/stock_quant.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,13 @@ def _group_by_location(self):
else:
seen[location] = quant
return [(loc, quants) for loc, quants in seen.items()]

def group_by_lot(self):
seen = OrderedDict()
for quant in self:
lot = quant.lot_id
if lot in seen:
seen[lot] = seen[lot] | quant
else:
seen[lot] = quant
return [(lot, quants) for lot, quants in seen.items()]
148 changes: 147 additions & 1 deletion stock_reserve_rule/models/stock_reserve_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
("empty_bin", "Empty Bins"),
("packaging", "Full Packaging"),
("full_bin", "Full Bin"),
("full_lot", "Full Lot"),
],
required=True,
default="default",
Expand All @@ -144,7 +145,9 @@
" empty afterwards.\n"
"Full Packaging: take goods from a location only if the location "
"quantity matches a packaging quantity (do not open boxes).\n"
"Full Bin: take goods from a location if it reserves all its content",
"Full Bin: take goods from a location if it reserves all its content"
"quantity matches a packaging quantity (do not open boxes)."
"By lot: ",
)

packaging_type_ids = fields.Many2many(
Expand All @@ -154,6 +157,68 @@
"When empty, any packaging can be removed.",
)

TOLERANCE_LIMIT = [
("no_tolerance", "No tolerance"),
("upper_limit", "Upper Limit"),
("lower_limit", "Lower Limit"),
]
Copy link
Contributor

@jbaudoux jbaudoux Nov 9, 2023

Choose a reason for hiding this comment

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

What about having also "Within Limits" ?

(suggestion, non-blocking and non required)


tolerance_requested_limit = fields.Selection(
jbaudoux marked this conversation as resolved.
Show resolved Hide resolved
selection=TOLERANCE_LIMIT,
string="Tolerance on",
jbaudoux marked this conversation as resolved.
Show resolved Hide resolved
help="Selecting a tolerance limit type"
"No tolerance: lot qty should be equal required"
"Upper Limit: quantity higher than demand but within tolerance"
"Lower Limit: lot with lower qty than required",
default="no_tolerance",
required=True,
)

tolerance_requested_computation = fields.Selection(
selection=[
("percentage", "Percentage (%)"),
("absolute", "Absolute Value"),
],
string="Tolerance computation",
jbaudoux marked this conversation as resolved.
Show resolved Hide resolved
required=True,
default="percentage",
)

tolerance_requested_value = fields.Float(string="Tolerance value", default=0.0)

tolerance_display = fields.Char(
compute="_compute_tolerance_display", store=True, string="Tolerance"
)

@api.depends(
"tolerance_requested_limit",
"tolerance_requested_computation",
"tolerance_requested_value",
)
def _compute_tolerance_display(self):
for rec in self:
tolerance_on = rec.tolerance_requested_limit
tolerance_computation = rec.tolerance_requested_computation
value = rec.tolerance_requested_value
if value == 0.0 and tolerance_on == "no_tolerance":
rec.tolerance_display = "Requested Qty = Lot Qty"
continue
limit = "-" if tolerance_on == "lower_limit" else ""
computation = "%" if tolerance_computation == "percentage" else ""
tolerance_on_dict = dict(self.TOLERANCE_LIMIT)
rec.tolerance_display = "{} ({}{}{})".format(
tolerance_on_dict.get(tolerance_on), limit, value, computation
)

@api.onchange("tolerance_requested_value")
def _onchange_tolerance_limit(self):
if self.tolerance_requested_value < 0.0:
raise models.UserError(
_(
"Tolerance from requested qty value must be more than or equal to 0.0"
)
)

@api.constrains("location_id")
def _constraint_location_id(self):
"""The location has to be a child of the rule location."""
Expand Down Expand Up @@ -333,3 +398,84 @@

if float_compare(need, location_quantity, rounding) != -1:
need = yield location, location_quantity, need, None, None

def _compare_with_tolerance(self, need, product_qty, rounding):
tolerance = self.tolerance_requested_value
limit = self.tolerance_requested_limit
computation = self.tolerance_requested_computation
if (
limit == "no_tolerance"
or float_compare(tolerance, 0, precision_rounding=rounding) == 0
):
return float_compare(need, product_qty, rounding) == 0
elif limit == "upper_limit":
if computation == "percentage":
# need + rounding < product_qty <= need * (100 + tolerance) / 100
return (
float_compare(need, product_qty, precision_rounding=rounding) == -1
and float_compare(
product_qty,
need * (100 + tolerance) / 100,
precision_rounding=rounding,
)
<= 0
)
else:
# need + rounding < product_qty <= need + tolerance
return (
float_compare(need, product_qty, precision_rounding=rounding) == -1
and float_compare(
product_qty, need + tolerance, precision_rounding=rounding
)
<= 0
)
elif limit == "lower_limit":
if computation == "percentage":
# need * (100 - tolerance) / 100 <= product_qty < need - rounding
return (
float_compare(
need * (100 - tolerance) / 100,
product_qty,
precision_rounding=rounding,
)
<= 0
and float_compare(product_qty, need, precision_rounding=rounding)
== -1
)
# computation == "absolute"
else:
# need - tolerance <= product_qty < need - rounding
return (
float_compare(
need - tolerance, product_qty, precision_rounding=rounding
)
<= 0
and float_compare(product_qty, need, precision_rounding=rounding)
== -1
)

def _apply_strategy_full_lot(self, quants):
need = yield
# We take goods only if we empty the bin.
# The original ordering (fefo, fifo, ...) must be kept.
product = fields.first(quants).product_id
if product.tracking == "lot":
rounding = product.uom_id.rounding
for lot, lot_quants in quants.filtered(
lambda quant: quant.product_id.id == product.id and quant.lot_id
).group_by_lot():
if any(quant.reserved_quantity for quant in lot_quants):
# skip lots that are partially reserved
continue

Check warning on line 469 in stock_reserve_rule/models/stock_reserve_rule.py

View check run for this annotation

Codecov / codecov/patch

stock_reserve_rule/models/stock_reserve_rule.py#L469

Added line #L469 was not covered by tests
product_qty = sum(lot_quants.mapped("quantity"))
if not self._compare_with_tolerance(need, product_qty, rounding):
continue
for lot_quant in lot_quants:
lot_quantity = lot_quant.quantity - lot_quant.reserved_quantity
need = yield (
lot_quant.location_id,
lot_quantity,
need,
lot,
None,
)
1 change: 1 addition & 0 deletions stock_reserve_rule/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
* Guewen Baconnier <[email protected]>
* Jacques-Etienne Baudoux (BCIM) <[email protected]>
* Cetmix <https://cetmix.com>
12 changes: 12 additions & 0 deletions stock_reserve_rule/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ The included advanced removal strategies are:
* Full Packaging: tries to remove full packaging (configured on the products)
first, by largest to smallest package or based on a pre-selected package
(default removal strategy is then applied for equal quantities).
* Full lot: tries to remove a complete lot.
This strategy requires to select if a tolerance should be applied on lot selection,
allowing to select lots with qty higher or lower than qty requested in picking.
Rules can be applied in sequence, for example, to first check for exact lot qty,
then for a lot with qty 5% higher than requested, then for a lot with qty 10% higher
than requested, and so on.

If lot selected must have same qty as requested, set "Tolerance on = No tolerance";
otherwise it's possible to select a lot with higher qty (Tolerance on = Upper Limit")
or lower qty ("Lower Limit"), either in percentage of qty or absolute value.
Please note that three "No tolerance" or "Upper limit" or "Lower limit" rules.


Examples of scenario:

Expand Down
3 changes: 1 addition & 2 deletions stock_reserve_rule/readme/USAGE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ Scenario:
and see the rules (by default in demo, the rules are created inactive)
* Open Transfer: Outgoing shipment (reservation rules demo 1)
* Check availability: it has 150 units, as it will not empty Zone A, it will not
take products there, it should take 100 in B and 50 in C (following the rules
order)
take products there, it should take 100 in B and 50 in C (following the rules order)
* Unreserve this transfer (to test the second case)
* Open Transfer: Outgoing shipment (reservation rules demo 2)
* Check availability: it has 250 units, it can empty Zone A, it will take 200 in
Expand Down
32 changes: 23 additions & 9 deletions stock_reserve_rule/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ <h1 class="title">Stock Reservation Rules</h1>
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:80f49ac641c2a8bede920d6378719dcedc8e81d45ee91c5388c8450d5c526d26
!! source digest: sha256:f0f580bceed65878a874bcbfe6aaf70570a3821643867b635e36496e354e1df1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/stock-logistics-warehouse/tree/14.0/stock_reserve_rule"><img alt="OCA/stock-logistics-warehouse" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/stock-logistics-warehouse-14-0/stock-logistics-warehouse-14-0-stock_reserve_rule"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-warehouse&amp;target_branch=14.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module adds rules for advanced reservation / removal strategies.</p>
Expand All @@ -380,14 +380,28 @@ <h1 class="title">Stock Reservation Rules</h1>
<p>The advanced removal strategies are applied on top of the default one (fifo,
fefo, …).</p>
<p>The included advanced removal strategies are:</p>
<ul class="simple">
<li>Default Removal Strategy: apply the default configured one (fifo, fefo, …)</li>
<li>Empty Bins: goods are removed from a bin only if the bin will be empty after
<ul>
<li><p class="first">Default Removal Strategy: apply the default configured one (fifo, fefo, …)</p>
</li>
<li><p class="first">Empty Bins: goods are removed from a bin only if the bin will be empty after
the removal (favor largest bins first to minimize the number of operations,
then apply the default removal strategy for equal quantities).</li>
<li>Full Packaging: tries to remove full packaging (configured on the products)
then apply the default removal strategy for equal quantities).</p>
</li>
<li><p class="first">Full Packaging: tries to remove full packaging (configured on the products)
first, by largest to smallest package or based on a pre-selected package
(default removal strategy is then applied for equal quantities).</li>
(default removal strategy is then applied for equal quantities).</p>
</li>
<li><p class="first">Full lot: tries to remove a complete lot.
This strategy requires to select if a tolerance should be applied on lot selection,
allowing to select lots with qty higher or lower than qty requested in picking.
Rules can be applied in sequence, for example, to first check for exact lot qty,
then for a lot with qty 5% higher than requested, then for a lot with qty 10% higher
than requested, and so on.</p>
<p>If lot selected must have same qty as requested, set “Tolerance on = No tolerance”;
otherwise it’s possible to select a lot with higher qty (Tolerance on = Upper Limit”)
or lower qty (“Lower Limit”), either in percentage of qty or absolute value.
Please note that three “No tolerance” or “Upper limit” or “Lower limit” rules.</p>
</li>
</ul>
<p>Examples of scenario:</p>
<p>rules:</p>
Expand Down Expand Up @@ -465,8 +479,7 @@ <h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
and see the rules (by default in demo, the rules are created inactive)</li>
<li>Open Transfer: Outgoing shipment (reservation rules demo 1)</li>
<li>Check availability: it has 150 units, as it will not empty Zone A, it will not
take products there, it should take 100 in B and 50 in C (following the rules
order)</li>
take products there, it should take 100 in B and 50 in C (following the rules order)</li>
<li>Unreserve this transfer (to test the second case)</li>
<li>Open Transfer: Outgoing shipment (reservation rules demo 2)</li>
<li>Check availability: it has 250 units, it can empty Zone A, it will take 200 in
Expand Down Expand Up @@ -496,6 +509,7 @@ <h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul class="simple">
<li>Guewen Baconnier &lt;<a class="reference external" href="mailto:guewen.baconnier&#64;camptocamp.com">guewen.baconnier&#64;camptocamp.com</a>&gt;</li>
<li>Jacques-Etienne Baudoux (BCIM) &lt;<a class="reference external" href="mailto:je&#64;bcim.be">je&#64;bcim.be</a>&gt;</li>
<li>Cetmix &lt;<a class="reference external" href="https://cetmix.com">https://cetmix.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
Expand Down
2 changes: 2 additions & 0 deletions stock_reserve_rule/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from . import common
from . import test_reserve_rule
from . import test_reserve_rule_full_lot
Loading
Loading