From c328d9b79974d76492f17d8239b899c1b76cc908 Mon Sep 17 00:00:00 2001 From: BernatPForgeFlow Date: Wed, 14 Feb 2024 13:07:35 +0100 Subject: [PATCH] [FIX] mrp_multi_level: Prioritize safety stock with mrp moves today If I have 0 units, my safety stock is 5 units and today I have a supply for 10 units, the procurement recommendation should be 0 units --- mrp_multi_level/tests/test_mrp_multi_level.py | 85 +++++++++++++++++++ mrp_multi_level/wizards/mrp_multi_level.py | 46 ++-------- 2 files changed, 92 insertions(+), 39 deletions(-) diff --git a/mrp_multi_level/tests/test_mrp_multi_level.py b/mrp_multi_level/tests/test_mrp_multi_level.py index 5ffd134c5..6095aec3f 100644 --- a/mrp_multi_level/tests/test_mrp_multi_level.py +++ b/mrp_multi_level/tests/test_mrp_multi_level.py @@ -757,3 +757,88 @@ class TestMrpMultiLevel(TestMrpMultiLevelCommon): f"unexpected value for {key}: {inv[key]} " f"(expected {test_vals[key]} on {inv.date})", ) + + def test_23_prioritize_safety_stock_with_mrp_moves_today(self): + """Test MRP but with moves today. Safety stock should not be ordered.""" + now = datetime.now() + product = self.prod_test # has Buy route + product.seller_ids[0].delay = 2 # set a purchase lead time + self.quant_obj._update_available_quantity(product, self.cases_loc, 5) + self.product_mrp_area_obj.create( + { + "product_id": product.id, + "mrp_area_id": self.cases_area.id, + "mrp_minimum_stock": 15, + } + ) + self._create_picking_out(product, 10.0, now, location=self.cases_loc) + self._create_picking_in(product, 20.0, now, location=self.cases_loc) + self.mrp_multi_level_wiz.create( + {"mrp_area_ids": [(6, 0, self.cases_area.ids)]} + ).run_mrp_multi_level() + inventory = self.mrp_inventory_obj.search( + [("mrp_area_id", "=", self.cases_area.id), ("product_id", "=", product.id)] + ) + expected = [ + { + "date": now.date(), + "demand_qty": 10.0, + "final_on_hand_qty": 15.0, + "initial_on_hand_qty": 5.0, + "running_availability": 15.0, + "supply_qty": 20.0, + "to_procure": 0.0, + }, + ] + self.assertEqual(len(expected), len(inventory)) + for test_vals, inv in zip(expected, inventory): + for key in test_vals: + self.assertEqual( + test_vals[key], + inv[key], + f"unexpected value for {key}: {inv[key]} " + f"(expected {test_vals[key]} on {inv.date})", + ) + + def test_24_prioritize_safety_stock_with_mrp_moves_today_grouped(self): + """Test grouped demand MRP but with moves today. Safety stock should not be ordered.""" + now = datetime.now() + product = self.prod_test # has Buy route + product.seller_ids[0].delay = 2 # set a purchase lead time + self.quant_obj._update_available_quantity(product, self.cases_loc, 5) + self.product_mrp_area_obj.create( + { + "product_id": product.id, + "mrp_area_id": self.cases_area.id, + "mrp_minimum_stock": 15, + "mrp_nbr_days": 2, + } + ) + self._create_picking_out(product, 10.0, now, location=self.cases_loc) + self._create_picking_in(product, 20.0, now, location=self.cases_loc) + self.mrp_multi_level_wiz.create( + {"mrp_area_ids": [(6, 0, self.cases_area.ids)]} + ).run_mrp_multi_level() + inventory = self.mrp_inventory_obj.search( + [("mrp_area_id", "=", self.cases_area.id), ("product_id", "=", product.id)] + ) + expected = [ + { + "date": now.date(), + "demand_qty": 10.0, + "final_on_hand_qty": 15.0, + "initial_on_hand_qty": 5.0, + "running_availability": 15.0, + "supply_qty": 20.0, + "to_procure": 0.0, + }, + ] + self.assertEqual(len(expected), len(inventory)) + for test_vals, inv in zip(expected, inventory): + for key in test_vals: + self.assertEqual( + test_vals[key], + inv[key], + f"unexpected value for {key}: {inv[key]} " + f"(expected {test_vals[key]} on {inv.date})", + ) diff --git a/mrp_multi_level/wizards/mrp_multi_level.py b/mrp_multi_level/wizards/mrp_multi_level.py index 1b2c6725a..36dfd96d5 100644 --- a/mrp_multi_level/wizards/mrp_multi_level.py +++ b/mrp_multi_level/wizards/mrp_multi_level.py @@ -532,7 +532,7 @@ class MultiLevelMrp(models.TransientModel): return product_mrp_area.mrp_minimum_stock - onhand - move_qty @api.model - def _init_mrp_move_grouped_demand(self, nbr_create, product_mrp_area): + def _init_mrp_move_grouped_demand(self, product_mrp_area): last_date = None last_qty = 0.00 onhand = product_mrp_area.qty_available @@ -566,7 +566,6 @@ class MultiLevelMrp(models.TransientModel): onhand = onhand + qty_ordered last_date = None last_qty = 0.00 - nbr_create += 1 demand_origin = [] for move in product_mrp_area.mrp_move_ids: @@ -605,7 +604,6 @@ class MultiLevelMrp(models.TransientModel): onhand = onhand + last_qty + qty_ordered last_date = None last_qty = 0.00 - nbr_create += 1 demand_origin = [] if ( onhand + last_qty + move.mrp_qty @@ -644,7 +642,6 @@ class MultiLevelMrp(models.TransientModel): qty_ordered = cm.get("qty_ordered", 0.0) onhand += qty_ordered last_qty -= qty_ordered - nbr_create += 1 if (onhand + last_qty) < product_mrp_area.mrp_minimum_stock: mrp_date = self._get_safety_stock_target_date(product_mrp_area) @@ -659,9 +656,6 @@ class MultiLevelMrp(models.TransientModel): ) qty_ordered = cm["qty_ordered"] onhand += qty_ordered - nbr_create += 1 - - return nbr_create def _get_safety_stock_target_date(self, product_mrp_area): """Get the date at which the safety stock rebuild should be targeted @@ -670,12 +664,14 @@ class MultiLevelMrp(models.TransientModel): return date.today() @api.model - def _init_mrp_move_non_grouped_demand(self, nbr_create, product_mrp_area): + def _init_mrp_move_non_grouped_demand(self, product_mrp_area): onhand = product_mrp_area.qty_available for move in product_mrp_area.mrp_move_ids: if self._exclude_move(move): continue - if onhand < product_mrp_area.mrp_minimum_stock: + # This works because mrp moves are ordered by: + # product_mrp_area_id, mrp_date, mrp_type desc, id + if onhand + move.mrp_qty < product_mrp_area.mrp_minimum_stock: qtytoorder = self._get_qty_to_order( product_mrp_area, self._get_safety_stock_target_date(product_mrp_area), @@ -692,7 +688,6 @@ class MultiLevelMrp(models.TransientModel): ) qty_ordered = cm["qty_ordered"] onhand += qty_ordered - nbr_create += 1 qtytoorder = self._get_qty_to_order( product_mrp_area, move.mrp_date, move.mrp_qty, onhand @@ -707,7 +702,6 @@ class MultiLevelMrp(models.TransientModel): ) qty_ordered = cm["qty_ordered"] onhand += move.mrp_qty + qty_ordered - nbr_create += 1 else: onhand += move.mrp_qty if onhand < product_mrp_area.mrp_minimum_stock: @@ -723,9 +717,6 @@ class MultiLevelMrp(models.TransientModel): ) qty_ordered = cm["qty_ordered"] onhand += qty_ordered - nbr_create += 1 - - return nbr_create @api.model def _exclude_move(self, move): @@ -748,33 +739,10 @@ class MultiLevelMrp(models.TransientModel): llc += 1 for product_mrp_area in product_mrp_areas: - nbr_create = 0 - onhand = product_mrp_area.qty_available - if product_mrp_area.mrp_nbr_days == 0: - nbr_create = self._init_mrp_move_non_grouped_demand( - nbr_create, product_mrp_area - ) + self._init_mrp_move_non_grouped_demand(product_mrp_area) else: - nbr_create = self._init_mrp_move_grouped_demand( - nbr_create, product_mrp_area - ) - - if onhand < product_mrp_area.mrp_minimum_stock and nbr_create == 0: - mrp_date = date.today() - qtytoorder = self._get_qty_to_order( - product_mrp_area, mrp_date, 0, onhand - ) - name = _("Safety Stock") - cm = self.create_action( - product_mrp_area_id=product_mrp_area, - mrp_date=mrp_date, - mrp_qty=qtytoorder, - name=name, - values=dict(origin=name), - ) - qty_ordered = cm["qty_ordered"] - onhand += qty_ordered + self._init_mrp_move_grouped_demand(product_mrp_area) counter += 1 log_msg = "MRP Calculation LLC {} Finished - Nbr. products: {}".format(