diff --git a/stock_reserve_rule/demo/stock_reserve_rule_demo.xml b/stock_reserve_rule/demo/stock_reserve_rule_demo.xml
index aeb547af4284..7804313d8e6a 100644
--- a/stock_reserve_rule/demo/stock_reserve_rule_demo.xml
+++ b/stock_reserve_rule/demo/stock_reserve_rule_demo.xml
@@ -29,6 +29,6 @@
4
- single_lot
+ full_lot
diff --git a/stock_reserve_rule/models/stock_quant.py b/stock_reserve_rule/models/stock_quant.py
index 1236b6883b78..51486b5426e3 100644
--- a/stock_reserve_rule/models/stock_quant.py
+++ b/stock_reserve_rule/models/stock_quant.py
@@ -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()]
diff --git a/stock_reserve_rule/models/stock_reserve_rule.py b/stock_reserve_rule/models/stock_reserve_rule.py
index 05dd7b1fcaa7..bd0d1cdb25ac 100644
--- a/stock_reserve_rule/models/stock_reserve_rule.py
+++ b/stock_reserve_rule/models/stock_reserve_rule.py
@@ -134,7 +134,7 @@ class StockReserveRuleRemoval(models.Model):
("empty_bin", "Empty Bins"),
("packaging", "Full Packaging"),
("full_bin", "Full Bin"),
- ("single_lot", "Single lot"),
+ ("full_lot", "Full Lot"),
],
required=True,
default="default",
@@ -190,35 +190,6 @@ class StockReserveRuleRemoval(models.Model):
compute="_compute_tolerance_display", store=True, string="Tolerance"
)
- @api.constrains("tolerance_requested_limit")
- def _check_tolerance_requested_limit(self):
- if self.filtered(
- lambda r: (
- r.tolerance_requested_limit == "no_tolerance"
- and (
- r.tolerance_requested_computation != "percentage"
- or r.tolerance_requested_value != 0.0
- )
- )
- ):
- raise models.UserError(
- _(
- '"No Tolerance" must be have values -'
- ' "Tolerance computation" with value Percentage (%)\' '
- "and \"Tolerance value\" with value '0.0'"
- )
- )
-
- @api.onchange("tolerance_requested_limit")
- def _onchange_tolerance_requested_limit(self):
- if self.tolerance_requested_limit == "no_tolerance":
- self.update(
- {
- "tolerance_requested_computation": "percentage",
- "tolerance_requested_value": 0.0,
- }
- )
-
@api.depends(
"tolerance_requested_limit",
"tolerance_requested_computation",
@@ -228,9 +199,6 @@ def _compute_tolerance_display(self):
for rec in self:
tolerance_on = rec.tolerance_requested_limit
tolerance_computation = rec.tolerance_requested_computation
- if not tolerance_computation or not tolerance_on:
- rec.tolerance_display = ""
- continue
value = rec.tolerance_requested_value
if value == 0.0:
rec.tolerance_display = "Requested Qty = Lot Qty"
@@ -431,70 +399,67 @@ def _apply_strategy_full_bin(self, quants):
if float_compare(need, location_quantity, rounding) != -1:
need = yield location, location_quantity, need, None, None
- @api.model
- def _get_requested_comparisons(self, rounding):
+ def _compare_with_tolerance(self, need, product_qty, rounding):
tolerance = self.tolerance_requested_value
limit = self.tolerance_requested_limit
- if limit == "no_tolerance" or float_compare(tolerance, 0, rounding) == 0:
- return lambda product_qty, need: product_qty == need
computation = self.tolerance_requested_computation
- if limit == "upper_limit":
-
- def wrap(product_qty, need):
- if computation == "percentage":
- return (
- need + rounding < product_qty <= need * (100 + tolerance) / 100
- )
- # computation == "absolute"
- else:
- return need + rounding < product_qty <= need + tolerance
-
- return wrap
-
- if limit == "lower_limit":
-
- def wrap(product_qty, need):
- if computation == "percentage":
- return (
- need * (100 - tolerance) / 100 <= product_qty < need - rounding
+ 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
+ and float_compare(
+ product_qty, need * (100 + tolerance) / 100, rounding
)
- # computation == "absolute"
- else:
- return need - tolerance <= product_qty < need - rounding
-
- return wrap
+ <= 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_single_lot(self, quants):
+ 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
- comparison = self._get_requested_comparisons(rounding)
- if product.tracking == "lot" or comparison is not None:
- for location, location_quants in quants._group_by_location():
- grouped_quants = self.env["stock.quant"].read_group(
- [
- ("lot_id", "!=", False),
- ("location_id", "in", location.ids),
- ("product_id", "=", product.id),
- ("quantity", ">", 0),
- ],
- ["lot_id", "quantity"],
- ["lot_id"],
- orderby="id",
- )
- lot_ids_with_quantity = {
- group["lot_id"][0]: group["quantity"] for group in grouped_quants
- }
- location_quantity = sum(location_quants.mapped("quantity")) - sum(
- location_quants.mapped("reserved_quantity")
+ 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")
)
- lot_id = False
- for rec_id, product_qty in lot_ids_with_quantity.items():
- if comparison(product_qty, need):
- lot_id = rec_id
- break
- if location_quantity > 0 and lot_id:
- lot_id = self.env["stock.production.lot"].browse(lot_id)
- need = yield location, location_quantity, need, lot_id, None
+ 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,
+ )
diff --git a/stock_reserve_rule/tests/test_reserve_rule.py b/stock_reserve_rule/tests/test_reserve_rule.py
index 43969df17b69..34796d08532e 100644
--- a/stock_reserve_rule/tests/test_reserve_rule.py
+++ b/stock_reserve_rule/tests/test_reserve_rule.py
@@ -844,35 +844,7 @@ def test_rule_excluded_not_child_location(self):
)
self.assertEqual(move.state, "assigned")
- def test_rule_no_tolerance_error(self):
- with self.assertRaises(exceptions.UserError):
- self._create_rule(
- {"picking_type_ids": [(6, 0, self.wh.out_type_id.ids)], "sequence": 1},
- [
- {
- "location_id": self.loc_zone4.id,
- "removal_strategy": "single_lot",
- "tolerance_requested_limit": "no_tolerance",
- "tolerance_requested_computation": "absolute",
- "tolerance_requested_value": 0.0,
- }
- ],
- )
- with self.assertRaises(exceptions.UserError):
- self._create_rule(
- {"picking_type_ids": [(6, 0, self.wh.out_type_id.ids)], "sequence": 1},
- [
- {
- "location_id": self.loc_zone4.id,
- "removal_strategy": "single_lot",
- "tolerance_requested_limit": "no_tolerance",
- "tolerance_requested_computation": "percentage",
- "tolerance_requested_value": 1.0,
- }
- ],
- )
-
- def test_rule_single_lot_equals(self):
+ def test_rule_full_lot_equals(self):
self._update_qty_in_location(
self.loc_zone4_bin1, self.product3, 100, lot_id=self.lot0_id
)
@@ -883,7 +855,7 @@ def test_rule_single_lot_equals(self):
[
{
"location_id": self.loc_zone4.id,
- "removal_strategy": "single_lot",
+ "removal_strategy": "full_lot",
"tolerance_requested_limit": "no_tolerance",
"tolerance_requested_computation": "percentage",
"tolerance_requested_value": 0.0,
@@ -930,7 +902,7 @@ def test_rule_validation(self):
[
{
"location_id": self.loc_zone4.id,
- "removal_strategy": "single_lot",
+ "removal_strategy": "full_lot",
"tolerance_requested_limit": "lower_limit",
"tolerance_requested_computation": "absolute",
}
@@ -952,7 +924,7 @@ def test_rule_tolerance_absolute(self):
[
{
"location_id": self.loc_zone4.id,
- "removal_strategy": "single_lot",
+ "removal_strategy": "full_lot",
"tolerance_requested_limit": "upper_limit",
"tolerance_requested_computation": "absolute",
"tolerance_requested_value": 1.0,
@@ -992,7 +964,7 @@ def test_rule_tolerance_percent(self):
[
{
"location_id": self.loc_zone4.id,
- "removal_strategy": "single_lot",
+ "removal_strategy": "full_lot",
"tolerance_requested_limit": "upper_limit",
"tolerance_requested_computation": "percentage",
"tolerance_requested_value": 50.0,
@@ -1028,14 +1000,14 @@ def test_rule_tolerance_lower_limit(self):
[
{
"location_id": self.loc_zone1.id,
- "removal_strategy": "single_lot",
+ "removal_strategy": "full_lot",
"tolerance_requested_limit": "lower_limit",
"tolerance_requested_computation": "percentage",
"tolerance_requested_value": 50.0,
},
{
"location_id": self.loc_zone4.id,
- "removal_strategy": "single_lot",
+ "removal_strategy": "full_lot",
"tolerance_requested_limit": "no_tolerance",
"tolerance_requested_computation": "percentage",
"tolerance_requested_value": 0.0,
diff --git a/stock_reserve_rule/views/stock_reserve_rule_views.xml b/stock_reserve_rule/views/stock_reserve_rule_views.xml
index 04e925cf6f94..9f80f53d3e37 100644
--- a/stock_reserve_rule/views/stock_reserve_rule_views.xml
+++ b/stock_reserve_rule/views/stock_reserve_rule_views.xml
@@ -65,11 +65,11 @@
/>