-
-
Notifications
You must be signed in to change notification settings - Fork 723
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
Changes from 5 commits
41e2088
e6affeb
e6de1fa
9fe2fbb
9a62218
dc8b21f
bf210e9
10af719
b4adad6
99c8226
7c4bfb9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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: | ||
|
||
|
@@ -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 | ||
|
@@ -160,6 +171,7 @@ Contributors | |
|
||
* Guewen Baconnier <[email protected]> | ||
* Jacques-Etienne Baudoux (BCIM) <[email protected]> | ||
* Cetmix <https://cetmix.com> | ||
|
||
Maintainers | ||
~~~~~~~~~~~ | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -134,6 +134,7 @@ | |||||
("empty_bin", "Empty Bins"), | ||||||
("packaging", "Full Packaging"), | ||||||
("full_bin", "Full Bin"), | ||||||
("full_lot", "Full Lot"), | ||||||
], | ||||||
required=True, | ||||||
default="default", | ||||||
|
@@ -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( | ||||||
|
@@ -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"), | ||||||
] | ||||||
|
||||||
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.""" | ||||||
|
@@ -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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some unit tests on
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @elvise This still needs to be fixed or answered There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @francesco-ooops @geomer198 please take care There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jbaudoux Done! Please check tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@geomer198 can you add tests with decimals (qty)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 ( | ||||||
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, | ||||||
) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! |
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> |
There was a problem hiding this comment.
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)