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:84cf76b9b331b48fc2a1f2bd01c1721bcae3aa5cd0eef552ca8641f744363537
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |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).
* Single lot: tries to remove a single 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 @@
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

Check warning on line 37 in stock_reserve_rule/models/stock_quant.py

View check run for this annotation

Codecov / codecov/patch

stock_reserve_rule/models/stock_quant.py#L37

Added line #L37 was not covered by tests
else:
seen[lot] = quant
return [(lot, quants) for lot, quants in seen.items()]
132 changes: 131 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:
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,68 @@

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, 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, rounding) == -1
Copy link
Contributor

@jbaudoux jbaudoux Nov 21, 2023

Choose a reason for hiding this comment

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

Why don't you accept exact value when you give a tolerance? It should be in the limit defined by the tolerance but if the quantity is exactly the need, it should also be valid, isn't it?

Suggested change
float_compare(need, product_qty, rounding) == -1
float_compare(need, product_qty, rounding) <= 0

Copy link
Contributor

@jbaudoux jbaudoux Nov 21, 2023

Choose a reason for hiding this comment

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

Some unit tests on _compare_with_tolerance with real figures would be helpful. Just the call to the method without picking.
Tests with inside tolerance, at the lower limit of tolerance, at the upper limit of the tolerance, below tolerance, above tolerance

  • no tolerance
  • upper_limit, percentage
  • upper_limit, absolute
  • lower_limit, percentage
  • lower_limit, absolute

Copy link
Contributor

Choose a reason for hiding this comment

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

@elvise This still needs to be fixed or answered

Copy link

Choose a reason for hiding this comment

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

@francesco-ooops @geomer198 please take care

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jbaudoux Done! Please check tests.

Copy link

Choose a reason for hiding this comment

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

@jbaudoux Done! Please check tests.

@geomer198 can you add tests with decimals (qty)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jbaudoux Done! Please check tests.

@geomer198 can you add tests with decimals (qty)?

Yes) Done!

and float_compare(
product_qty, need * (100 + tolerance) / 100, rounding
)
<= 0
)
else:
# need + rounding < product_qty <= need + tolerance
return (
float_compare(need, product_qty, rounding) == -1
and float_compare(product_qty, need + tolerance, 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, rounding)
<= 0
and float_compare(product_qty, need, rounding) == -1
)
# computation == "absolute"
else:
# need - tolerance <= product_qty < need - rounding
return (

Check warning on line 435 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#L435

Added line #L435 was not covered by tests
float_compare(need - tolerance, product_qty, rounding) <= 0
and float_compare(product_qty, need, 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
rounding = product.uom_id.rounding
if product.tracking == "lot":
for lot, lot_quants in quants.filtered(
lambda quant, product_id=product.id: quant.product_id.id == product_id
).group_by_lot():
product_qty = sum(lot_quants.mapped("quantity"))
lot_quantity = sum(lot_quants.mapped("quantity")) - sum(
lot_quants.mapped("reserved_quantity")
)
if (
lot
and self._compare_with_tolerance(need, product_qty, rounding)
and lot_quantity > 0
):
need = (
yield fields.first(lot_quants).location_id,
lot_quantity,
need,
lot,
None,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

You have selected a lot and it can have multiple quants. I think you need to yield each quant one by one by looping on the quants (they should be already sorted by fifo/fefo) decreasing the need until need is 0 or all quants are consumed.
Can you add a test with this?
Example:

  • Loc1, Lot A, 10 units
  • Loc2, Lot A, 5 units
    Need for 15 units. Lot A is selected

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jbaudoux Thank you for your solution! I updated function implementation. Now the strategy looks better!

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).
* Single lot: tries to remove a single lot.
jbaudoux marked this conversation as resolved.
Show resolved Hide resolved
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:84cf76b9b331b48fc2a1f2bd01c1721bcae3aa5cd0eef552ca8641f744363537
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<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">Single lot: tries to remove a single 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
Loading
Loading