From db91a79d3189482dcbe9ed9154ba40eeb7597464 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Sat, 25 Jul 2026 23:54:52 +0530 Subject: [PATCH 1/2] fix(subcontracting): release raw-material reservation when closing a subcontracting order the bin reserved-qty recalc filtered out closed purchase orders but not closed subcontracting orders, so closing a partially-received sco kept the reservation for the unreceived qty and left projected qty understated. apply the same closed-status filter to the subcontracting order path. --- erpnext/stock/doctype/bin/bin.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index f5417439ded..b004975d2fe 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -164,11 +164,8 @@ class Bin(Document): & (subcontract_order.name == supplied_item.parent) & (subcontract_order.per_received < 100) & (supplied_item.reserve_warehouse == self.warehouse) - & ( - ((subcontract_order.status != "Closed") & (subcontract_order.docstatus == 1)) - if subcontract_doctype == "Purchase Order" - else (subcontract_order.docstatus == 1) - ) + & (subcontract_order.status != "Closed") + & (subcontract_order.docstatus == 1) ) reserved_qty_for_sub_contract = ( @@ -203,6 +200,7 @@ class Bin(Document): else ( (Coalesce(se.subcontracting_order, "") != "") & (subcontract_order.name == se.subcontracting_order) + & (subcontract_order.status != "Closed") ) ) ) From e4b8065a69e085bfd825d4e01885b4d4de903950 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Sat, 25 Jul 2026 23:54:54 +0530 Subject: [PATCH 2/2] test(subcontracting): cover reservation release on closing a subcontracting order close a partially-received sco with a reserve warehouse and assert the raw-material reservation is released and projected qty recovers. --- .../test_subcontracting_order.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 0186549bb4d..9c936c5aff4 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -394,6 +394,85 @@ class TestSubcontractingOrder(ERPNextTestSuite): bin_after_cancel_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract ) + def test_close_subcontracting_order_releases_reserved_qty(self): + # RM in stock at the reserve warehouse for transfer + make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 + ) + + bin_before_sco = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + # Create SCO with a reserve warehouse on the supplied items + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + + # Transfer only 90% of the raw materials to the supplier warehouse + ste = frappe.get_doc(make_rm_stock_entry(sco.name)) + for item in ste.items: + item.qty *= 0.9 + ste.save() + ste.submit() + sco.load_from_db() + self.assertEqual(sco.status, "Partial Material Transferred") + + # Receive only a partial qty so the order stays open (per_received < 100) + scr = make_subcontracting_receipt(sco.name) + scr.items[0].qty -= 1 + scr.save() + scr.submit() + sco.load_from_db() + self.assertEqual(sco.status, "Partially Received") + + # Keep another SCO open so transfers from the closed SCO must not reduce its reservation + open_sco = get_subcontracting_order(service_items=service_items) + self.assertEqual(open_sco.status, "Open") + + bin_before_close = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname=["reserved_qty_for_sub_contract", "projected_qty"], + as_dict=1, + ) + + # One unit remains reserved for the partially transferred SCO, plus ten for the open SCO + self.assertEqual( + bin_before_close.reserved_qty_for_sub_contract, + bin_before_sco.reserved_qty_for_sub_contract + 11, + ) + + # Close the partially-received order + sco.update_status("Closed") + self.assertEqual(sco.status, "Closed") + + bin_after_close = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname=["reserved_qty_for_sub_contract", "projected_qty"], + as_dict=1, + ) + + # Closing releases the remaining unit without applying its transfer against the open SCO + self.assertEqual( + bin_after_close.reserved_qty_for_sub_contract, + bin_before_sco.reserved_qty_for_sub_contract + 10, + ) + self.assertEqual(bin_after_close.projected_qty, bin_before_close.projected_qty + 1) + def test_send_to_subcontractor_ste_submit_without_sco_write_permission(self): """A Stock-only user (can submit Stock Entries but has no Subcontracting Order write) must be able to submit and cancel a 'Send to Subcontractor' Stock Entry. The SCO status update on the