Merge pull request #50788 from vorasmit/cascade-available-qty

This commit is contained in:
Smit Vora
2025-12-15 14:38:22 +05:30
committed by GitHub
2 changed files with 83 additions and 8 deletions

View File

@@ -1408,14 +1408,21 @@ def get_material_request_items(
include_safety_stock,
warehouse,
bin_dict,
total_qty,
consumed_qty,
):
required_qty = 0
item_code = row.get("item_code")
if not ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
required_qty = total_qty[row.get("item_code")]
elif total_qty[row.get("item_code")] > bin_dict.get("projected_qty", 0):
required_qty = total_qty[row.get("item_code")] - bin_dict.get("projected_qty", 0)
total_qty[row.get("item_code")] -= required_qty
required_qty = flt(row.get("qty"))
else:
key = (item_code, warehouse)
available_qty = flt(bin_dict.get("projected_qty", 0)) - consumed_qty[key]
if available_qty > 0:
required_qty = max(0, flt(row.get("qty")) - available_qty)
consumed_qty[key] += min(flt(row.get("qty")), available_qty)
else:
required_qty = flt(row.get("qty"))
if doc.get("consider_minimum_order_qty") and required_qty > 0 and required_qty < row["min_order_qty"]:
required_qty = row["min_order_qty"]
@@ -1757,11 +1764,12 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
so_item_details[sales_order][key] = details
mr_items = []
consumed_qty = defaultdict(float)
for sales_order in so_item_details:
item_dict = so_item_details[sales_order]
total_qty = defaultdict(float)
for details in item_dict.values():
total_qty[details.item_code] += flt(details.qty)
warehouse = warehouse or details.get("source_warehouse") or details.get("default_warehouse")
bin_dict = get_bin_details(details, doc.company, warehouse)
bin_dict = bin_dict[0] if bin_dict else {}
@@ -1775,7 +1783,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
include_safety_stock,
warehouse,
bin_dict,
total_qty,
consumed_qty,
)
if items:
mr_items.append(items)

View File

@@ -151,6 +151,73 @@ class TestProductionPlan(IntegrationTestCase):
sr2.cancel()
pln.cancel()
def test_projected_qty_cascading_across_multiple_sales_orders(self):
rm_item = make_item(
"_Test RM For Cascading",
{"is_stock_item": 1, "valuation_rate": 100},
).name
fg_item_a = make_item(
"_Test FG A For Cascading",
{"is_stock_item": 1, "valuation_rate": 200},
).name
if not frappe.db.exists("BOM", {"item": fg_item_a, "docstatus": 1}):
make_bom(item=fg_item_a, raw_materials=[rm_item], rm_qty=1)
# Stock for RM
sr = create_stock_reconciliation(item_code=rm_item, target="_Test Warehouse - _TC", qty=1, rate=100)
# Sales orders
so1 = make_sales_order(item_code=fg_item_a, qty=1)
so2 = make_sales_order(item_code=fg_item_a, qty=1)
# Production plan
pln = frappe.get_doc(
{
"doctype": "Production Plan",
"company": "_Test Company",
"posting_date": nowdate(),
"get_items_from": "Sales Order",
"ignore_existing_ordered_qty": 1,
}
)
pln.append(
"sales_orders",
{
"sales_order": so1.name,
"sales_order_date": so1.transaction_date,
"customer": so1.customer,
"grand_total": so1.grand_total,
},
)
pln.append(
"sales_orders",
{
"sales_order": so2.name,
"sales_order_date": so2.transaction_date,
"customer": so2.customer,
"grand_total": so2.grand_total,
},
)
pln.get_items()
pln.insert()
mr_items = get_items_for_material_requests(pln.as_dict())
quantities = [d["quantity"] for d in mr_items]
rm_qty = sum(quantities)
self.assertEqual(len(mr_items), 2) # one for each SO
self.assertEqual(rm_qty, 1, "Cascading failed: total MR qty should be 1 (2 needed - 1 in stock)")
self.assertEqual(
quantities,
[0, 1],
"Cascading failed: first item should consume stock (qty=0), second should need procurement (qty=1)",
)
sr.cancel()
def test_production_plan_with_non_stock_item(self):
"Test if MR Planning table includes Non Stock RM."
pln = create_production_plan(item_code="Test Production Item 1", include_non_stock_items=1)