Merge pull request #32082 from s-aga-r/t3

fix: validate available qty for consumption in SCR
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
index 098242a..f4a943f 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -187,22 +187,13 @@
 		self.assertEqual(len(ste.items), len(rm_items))
 
 	def test_update_reserved_qty_for_subcontracting(self):
-		# Make stock available for raw materials
-		make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
+		# Create RM Material Receipt
+		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
 		)
-		make_stock_entry(
-			target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100
-		)
-		make_stock_entry(
-			target="_Test Warehouse 1 - _TC",
-			item_code="_Test Item Home Desktop 100",
-			qty=30,
-			basic_rate=100,
-		)
 
-		bin1 = frappe.db.get_value(
+		bin_before_sco = frappe.db.get_value(
 			"Bin",
 			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
 			fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
@@ -222,102 +213,97 @@
 		]
 		sco = get_subcontracting_order(service_items=service_items)
 
-		bin2 = frappe.db.get_value(
+		bin_after_sco = frappe.db.get_value(
 			"Bin",
 			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
 			fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
 			as_dict=1,
 		)
 
-		self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
-		self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
-		self.assertNotEqual(bin1.modified, bin2.modified)
+		# reserved_qty_for_sub_contract should be increased by 10
+		self.assertEqual(
+			bin_after_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract + 10
+		)
 
-		# Create stock transfer
+		# projected_qty should be decreased by 10
+		self.assertEqual(bin_after_sco.projected_qty, bin_before_sco.projected_qty - 10)
+
+		self.assertNotEqual(bin_before_sco.modified, bin_after_sco.modified)
+
+		# Create Stock Entry(Send to Subcontractor)
 		rm_items = [
 			{
 				"item_code": "_Test FG Item",
 				"rm_item_code": "_Test Item",
 				"item_name": "_Test Item",
-				"qty": 6,
+				"qty": 10,
 				"warehouse": "_Test Warehouse - _TC",
 				"rate": 100,
-				"amount": 600,
+				"amount": 1000,
 				"stock_uom": "Nos",
-			}
+			},
+			{
+				"item_code": "_Test FG Item",
+				"rm_item_code": "_Test Item Home Desktop 100",
+				"item_name": "_Test Item Home Desktop 100",
+				"qty": 20,
+				"warehouse": "_Test Warehouse - _TC",
+				"rate": 100,
+				"amount": 2000,
+				"stock_uom": "Nos",
+			},
 		]
 		ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
 		ste.to_warehouse = "_Test Warehouse 1 - _TC"
 		ste.save()
 		ste.submit()
 
-		bin3 = frappe.db.get_value(
+		bin_after_rm_transfer = frappe.db.get_value(
 			"Bin",
 			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
 			fieldname="reserved_qty_for_sub_contract",
 			as_dict=1,
 		)
 
-		self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
-
-		make_stock_entry(
-			target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100
-		)
-		make_stock_entry(
-			target="_Test Warehouse 1 - _TC",
-			item_code="_Test Item Home Desktop 100",
-			qty=40,
-			basic_rate=100,
+		# reserved_qty_for_sub_contract should be decreased by 10
+		self.assertEqual(
+			bin_after_rm_transfer.reserved_qty_for_sub_contract,
+			bin_after_sco.reserved_qty_for_sub_contract - 10,
 		)
 
-		# Make SCR against the SCO
-		scr = make_subcontracting_receipt(sco.name)
-		scr.save()
-		scr.submit()
-
-		bin4 = frappe.db.get_value(
-			"Bin",
-			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
-			fieldname="reserved_qty_for_sub_contract",
-			as_dict=1,
-		)
-
-		self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
-
-		# Cancel SCR
-		scr.reload()
-		scr.cancel()
-		bin5 = frappe.db.get_value(
-			"Bin",
-			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
-			fieldname="reserved_qty_for_sub_contract",
-			as_dict=1,
-		)
-
-		self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
-
-		# Cancel Stock Entry
+		# Cancel Stock Entry(Send to Subcontractor)
 		ste.cancel()
-		bin6 = frappe.db.get_value(
+		bin_after_cancel_ste = frappe.db.get_value(
 			"Bin",
 			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
 			fieldname="reserved_qty_for_sub_contract",
 			as_dict=1,
 		)
 
-		self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
+		# reserved_qty_for_sub_contract should be increased by 10
+		self.assertEqual(
+			bin_after_cancel_ste.reserved_qty_for_sub_contract,
+			bin_after_rm_transfer.reserved_qty_for_sub_contract + 10,
+		)
 
-		# Cancel PO
+		# Cancel SCO
 		sco.reload()
 		sco.cancel()
-		bin7 = frappe.db.get_value(
+		bin_after_cancel_sco = frappe.db.get_value(
 			"Bin",
 			filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
 			fieldname="reserved_qty_for_sub_contract",
 			as_dict=1,
 		)
 
-		self.assertEqual(bin7.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
+		# reserved_qty_for_sub_contract should be decreased by 10
+		self.assertEqual(
+			bin_after_cancel_sco.reserved_qty_for_sub_contract,
+			bin_after_cancel_ste.reserved_qty_for_sub_contract - 10,
+		)
+		self.assertEqual(
+			bin_after_cancel_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract
+		)
 
 	def test_exploded_items(self):
 		item_code = "_Test Subcontracted FG Item 11"
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 021d9aa..1da7340 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -75,6 +75,7 @@
 		self.get_current_stock()
 
 	def on_submit(self):
+		self.validate_available_qty_for_consumption()
 		self.update_status_updater_args()
 		self.update_prevdoc_status()
 		self.set_subcontracting_order_status()
@@ -107,10 +108,42 @@
 		self.set_missing_values_in_supplied_items()
 		self.set_missing_values_in_items()
 
+	def set_available_qty_for_consumption(self):
+		supplied_items_details = {}
+
+		sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item")
+		for item in self.get("items"):
+			supplied_items = (
+				frappe.qb.from_(sco_supplied_item)
+				.select(
+					sco_supplied_item.rm_item_code,
+					sco_supplied_item.reference_name,
+					(sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"),
+				)
+				.where(
+					(sco_supplied_item.parent == item.subcontracting_order)
+					& (sco_supplied_item.main_item_code == item.item_code)
+					& (sco_supplied_item.reference_name == item.subcontracting_order_item)
+				)
+			).run(as_dict=True)
+
+			if supplied_items:
+				supplied_items_details[item.name] = {}
+
+				for supplied_item in supplied_items:
+					supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty
+		else:
+			for item in self.get("supplied_items"):
+				item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get(
+					item.rm_item_code, 0
+				)
+
 	def set_missing_values_in_supplied_items(self):
 		for item in self.get("supplied_items") or []:
 			item.amount = item.rate * item.consumed_qty
 
+		self.set_available_qty_for_consumption()
+
 	def set_missing_values_in_items(self):
 		rm_supp_cost = {}
 		for item in self.get("supplied_items") or []:
@@ -147,6 +180,17 @@
 						_("Rejected Warehouse is mandatory against rejected Item {0}").format(item.item_code)
 					)
 
+	def validate_available_qty_for_consumption(self):
+		for item in self.get("supplied_items"):
+			if (
+				item.available_qty_for_consumption and item.available_qty_for_consumption < item.consumed_qty
+			):
+				frappe.throw(
+					_(
+						"Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table."
+					).format(item.idx)
+				)
+
 	def set_items_cost_center(self):
 		if self.company:
 			cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index 763e768..a47af52 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -70,6 +70,55 @@
 		rm_supp_cost = sum(item.amount for item in scr.get("supplied_items"))
 		self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost))
 
+	def test_available_qty_for_consumption(self):
+		make_stock_entry(
+			item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100
+		)
+		make_stock_entry(
+			item_code="_Test Item Home Desktop 100",
+			qty=100,
+			target="_Test Warehouse 1 - _TC",
+			basic_rate=100,
+		)
+		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)
+		rm_items = [
+			{
+				"main_item_code": "_Test FG Item",
+				"item_code": "_Test Item",
+				"qty": 5.0,
+				"rate": 100.0,
+				"stock_uom": "_Test UOM",
+				"warehouse": "_Test Warehouse - _TC",
+			},
+			{
+				"main_item_code": "_Test FG Item",
+				"item_code": "_Test Item Home Desktop 100",
+				"qty": 10.0,
+				"rate": 100.0,
+				"stock_uom": "_Test UOM",
+				"warehouse": "_Test Warehouse - _TC",
+			},
+		]
+		itemwise_details = make_stock_in_entry(rm_items=rm_items)
+		make_stock_transfer_entry(
+			sco_no=sco.name,
+			rm_items=rm_items,
+			itemwise_details=copy.deepcopy(itemwise_details),
+		)
+		scr = make_subcontracting_receipt(sco.name)
+		scr.save()
+		self.assertRaises(frappe.ValidationError, scr.submit)
+
 	def test_subcontracting_gle_fg_item_rate_zero(self):
 		from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
 
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
index 100a806..ddbb806 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
@@ -19,6 +19,7 @@
         "col_break2",
         "amount",
         "secbreak_2",
+        "available_qty_for_consumption",
         "required_qty",
         "col_break3",
         "consumed_qty",
@@ -75,8 +76,7 @@
         {
             "fieldname": "required_qty",
             "fieldtype": "Float",
-            "in_list_view": 1,
-            "label": "Available Qty For Consumption",
+            "label": "Required Qty",
             "print_hide": 1,
             "read_only": 1
         },
@@ -85,7 +85,7 @@
             "fieldname": "consumed_qty",
             "fieldtype": "Float",
             "in_list_view": 1,
-            "label": "Qty to be Consumed",
+            "label": "Consumed Qty",
             "reqd": 1
         },
         {
@@ -179,12 +179,21 @@
             "options": "Subcontracting Order",
             "print_hide": 1,
             "read_only": 1
+        },
+        {
+            "default": "0",
+            "fieldname": "available_qty_for_consumption",
+            "fieldtype": "Float",
+            "in_list_view": 1,
+            "label": "Available Qty For Consumption",
+            "print_hide": 1,
+            "read_only": 1
         }
     ],
     "idx": 1,
     "istable": 1,
     "links": [],
-    "modified": "2022-04-18 10:45:16.538479",
+    "modified": "2022-09-02 22:28:53.392381",
     "modified_by": "Administrator",
     "module": "Subcontracting",
     "name": "Subcontracting Receipt Supplied Item",
@@ -193,6 +202,6 @@
     "permissions": [],
     "sort_field": "modified",
     "sort_order": "DESC",
-    "track_changes": 1,
-    "states": []
+    "states": [],
+    "track_changes": 1
 }
\ No newline at end of file