diff --git a/stock_reserve_rule/models/stock_reserve_rule.py b/stock_reserve_rule/models/stock_reserve_rule.py index a7c39e5595d9..8494d690d17e 100644 --- a/stock_reserve_rule/models/stock_reserve_rule.py +++ b/stock_reserve_rule/models/stock_reserve_rule.py @@ -133,6 +133,7 @@ class StockReserveRuleRemoval(models.Model): ("default", "Default Removal Strategy"), ("empty_bin", "Empty Bins"), ("packaging", "Full Packaging"), + ("full_bin", "Full Bin"), ], required=True, default="default", @@ -142,7 +143,8 @@ class StockReserveRuleRemoval(models.Model): "Empty Bins: take goods from a location only if the bin is" " empty afterwards.\n" "Full Packaging: take goods from a location only if the location " - "quantity matches a packaging quantity (do not open boxes).", + "quantity matches a packaging quantity (do not open boxes).\n" + "Full Bin: take goods from a location if it reserves all its content", ) packaging_type_ids = fields.Many2many( @@ -296,3 +298,38 @@ def is_greater_eq(value, other): # compute how much packaging we can get take = (need // pack_quantity) * pack_quantity need = yield location, location_quantity, take, None, None + + def _apply_strategy_full_bin(self, quants): + need = yield + # Only location with nothing reserved can be fully emptied + quants = quants.filtered(lambda q: q.reserved_quantity == 0) + # Group by location (in this removal strategies, we want to consider + # the total quantity held in a location). + quants_per_bin = quants._group_by_location() + # 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 + locations_with_other_quants = [ + group["location_id"][0] + for group in quants.read_group( + [ + ("location_id", "in", quants.location_id.ids), + ("product_id", "not in", quants.product_id.ids), + ("quantity", ">", 0), + ], + ["location_id"], + "location_id", + ) + ] + for location, location_quants in quants_per_bin: + if location.id in locations_with_other_quants: + continue + + location_quantity = sum(location_quants.mapped("quantity")) + + if location_quantity <= 0: + continue + + if float_compare(need, location_quantity, rounding) != -1: + need = yield location, location_quantity, need, None, None diff --git a/stock_reserve_rule/tests/test_reserve_rule.py b/stock_reserve_rule/tests/test_reserve_rule.py index 7df3c6d9274d..e23db91fa7e6 100644 --- a/stock_reserve_rule/tests/test_reserve_rule.py +++ b/stock_reserve_rule/tests/test_reserve_rule.py @@ -435,6 +435,45 @@ def test_quant_domain_lot_and_owner(self): ) self.assertEqual(move.state, "assigned") + def test_rule_full_bin(self): + self._create_rule( + {}, + [ + { + "location_id": self.loc_zone1.id, + "sequence": 1, + "removal_strategy": "full_bin", + }, + {"location_id": self.loc_zone2.id, "sequence": 2}, + ], + ) + # 100 on location and reserving it with picking 1 + self._update_qty_in_location(self.loc_zone1_bin1, self.product1, 100) + picking1 = self._create_picking(self.wh, [(self.product1, 100)]) + picking1.action_assign() + self.assertEqual(picking1.state, "assigned") + # Add 300 on location + # There is 400 but 100 is reserved + self._update_qty_in_location(self.loc_zone1_bin1, self.product1, 300) + # A move for 300 will no be allowed (not fully empty) + picking2 = self._create_picking(self.wh, [(self.product1, 300)]) + picking2.action_assign() + self.assertEqual(picking2.state, "confirmed") + # But when picking 1 is done, no more reserved quantity + picking1.move_line_ids.qty_done = picking1.move_line_ids.product_uom_qty + picking1._action_done() + # Bin is fully emptied + picking2.action_assign() + move = picking2.move_lines + ml = move.move_line_ids + self.assertRecordValues( + ml, + [ + {"location_id": self.loc_zone1_bin1.id, "product_qty": 300.0}, + ], + ) + self.assertEqual(move.state, "assigned") + def test_rule_empty_bin(self): self._update_qty_in_location(self.loc_zone1_bin1, self.product1, 300) self._update_qty_in_location(self.loc_zone1_bin2, self.product1, 150)