Merge pull request #39127 from s-aga-r/FIX-38702

feat: provision to close SCO
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 2efb46e..b830e7d 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -452,6 +452,7 @@
 		self.update_requested_qty()
 		self.update_ordered_qty()
 		self.update_reserved_qty_for_subcontract()
+		self.update_subcontracting_order_status()
 		self.notify_update()
 		clear_doctype_notifications(self)
 
@@ -627,6 +628,17 @@
 			if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"):
 				make_subcontracting_order(self.name, save=True, notify=True)
 
+	def update_subcontracting_order_status(self):
+		from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+			update_subcontracting_order_status as update_sco_status,
+		)
+
+		if self.is_subcontracted and not self.is_old_subcontracting_flow:
+			sco = frappe.db.get_value("Subcontracting Order", {"purchase_order": self.name, "docstatus": 1})
+
+			if sco:
+				update_sco_status(sco, "Closed" if self.status == "Closed" else None)
+
 
 def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
 	"""get last purchase rate for an item"""
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 2ccee94..bccbc28 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -24,6 +24,7 @@
 
 import erpnext
 from erpnext.accounts.general_ledger import process_gl_map
+from erpnext.buying.utils import check_on_hold_or_closed_status
 from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
 from erpnext.manufacturing.doctype.bom.bom import (
 	add_additional_cost,
@@ -208,7 +209,6 @@
 		self.validate_bom()
 		self.set_process_loss_qty()
 		self.validate_purchase_order()
-		self.validate_subcontracting_order()
 
 		if self.purpose in ("Manufacture", "Repack"):
 			self.mark_finished_and_scrap_items()
@@ -274,6 +274,7 @@
 		return False
 
 	def on_submit(self):
+		self.validate_closed_subcontracting_order()
 		self.update_stock_ledger()
 		self.update_work_order()
 		self.validate_subcontract_order()
@@ -294,6 +295,7 @@
 			self.set_material_request_transfer_status("Completed")
 
 	def on_cancel(self):
+		self.validate_closed_subcontracting_order()
 		self.update_subcontract_order_supplied_items()
 		self.update_subcontracting_order_status()
 
@@ -1203,19 +1205,9 @@
 					)
 				)
 
-	def validate_subcontracting_order(self):
-		if self.get("subcontracting_order") and self.purpose in [
-			"Send to Subcontractor",
-			"Material Transfer",
-		]:
-			sco_status = frappe.db.get_value("Subcontracting Order", self.subcontracting_order, "status")
-
-			if sco_status == "Closed":
-				frappe.throw(
-					_("Cannot create Stock Entry against a closed Subcontracting Order {0}.").format(
-						self.subcontracting_order
-					)
-				)
+	def validate_closed_subcontracting_order(self):
+		if self.get("subcontracting_order"):
+			check_on_hold_or_closed_status("Subcontracting Order", self.subcontracting_order)
 
 	def mark_finished_and_scrap_items(self):
 		if self.purpose != "Repack" and any(
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
index 587a3b4..4c8a0ad 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
@@ -101,9 +101,32 @@
 	},
 
 	refresh: function (frm) {
+		if (frm.doc.docstatus == 1 && frm.has_perm("submit")) {
+			if (frm.doc.status == "Closed") {
+				frm.add_custom_button(__('Re-open'), () => frm.events.update_subcontracting_order_status(frm), __("Status"));
+			} else if(flt(frm.doc.per_received, 2) < 100) {
+				frm.add_custom_button(__('Close'), () => frm.events.update_subcontracting_order_status(frm, "Closed"), __("Status"));
+			}
+		}
+
 		frm.trigger('get_materials_from_supplier');
 	},
 
+	update_subcontracting_order_status(frm, status) {
+		frappe.call({
+			method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.update_subcontracting_order_status",
+			args: {
+				sco: frm.doc.name,
+				status: status,
+			},
+			callback: function (r) {
+				if (!r.exc) {
+					frm.reload_doc();
+				}
+			},
+		});
+	},
+
 	get_materials_from_supplier: function (frm) {
 		let sco_rm_details = [];
 
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
index 28c52c9..507e233 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
@@ -370,7 +370,7 @@
    "in_standard_filter": 1,
    "label": "Status",
    "no_copy": 1,
-   "options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled",
+   "options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled\nClosed",
    "print_hide": 1,
    "read_only": 1,
    "reqd": 1,
@@ -454,7 +454,7 @@
  "icon": "fa fa-file-text",
  "is_submittable": 1,
  "links": [],
- "modified": "2023-06-03 16:18:17.782538",
+ "modified": "2024-01-03 20:56:04.670380",
  "modified_by": "Administrator",
  "module": "Subcontracting",
  "name": "Subcontracting Order",
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
index 0fe8c13..daccbbb 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -7,7 +7,7 @@
 from frappe.utils import flt
 
 from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
-from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status
+from erpnext.buying.utils import check_on_hold_or_closed_status
 from erpnext.controllers.subcontracting_controller import SubcontractingController
 from erpnext.stock.stock_balance import update_bin_qty
 from erpnext.stock.utils import get_bin
@@ -68,6 +68,7 @@
 			"Material Transferred",
 			"Partial Material Transferred",
 			"Cancelled",
+			"Closed",
 		]
 		supplied_items: DF.Table[SubcontractingOrderSuppliedItem]
 		supplier: DF.Link
@@ -112,16 +113,10 @@
 
 	def on_submit(self):
 		self.update_prevdoc_status()
-		self.update_requested_qty()
-		self.update_ordered_qty_for_subcontracting()
-		self.update_reserved_qty_for_subcontracting()
 		self.update_status()
 
 	def on_cancel(self):
 		self.update_prevdoc_status()
-		self.update_requested_qty()
-		self.update_ordered_qty_for_subcontracting()
-		self.update_reserved_qty_for_subcontracting()
 		self.update_status()
 
 	def validate_purchase_order_for_subcontracting(self):
@@ -277,6 +272,9 @@
 		self.set_missing_values()
 
 	def update_status(self, status=None, update_modified=True):
+		if self.status == "Closed" and self.status != status:
+			check_on_hold_or_closed_status("Purchase Order", self.purchase_order)
+
 		if self.docstatus >= 1 and not status:
 			if self.docstatus == 1:
 				if self.status == "Draft":
@@ -285,11 +283,6 @@
 					status = "Completed"
 				elif self.per_received > 0 and self.per_received < 100:
 					status = "Partially Received"
-					for item in self.supplied_items:
-						if not item.returned_qty or (item.supplied_qty - item.consumed_qty - item.returned_qty) > 0:
-							break
-					else:
-						status = "Closed"
 				else:
 					total_required_qty = total_supplied_qty = 0
 					for item in self.supplied_items:
@@ -304,13 +297,12 @@
 			elif self.docstatus == 2:
 				status = "Cancelled"
 
-		if status:
-			frappe.db.set_value(
-				"Subcontracting Order", self.name, "status", status, update_modified=update_modified
-			)
+		if status and self.status != status:
+			self.db_set("status", status, update_modified=update_modified)
 
-			if status == "Closed":
-				update_po_status("Closed", self.purchase_order)
+		self.update_requested_qty()
+		self.update_ordered_qty_for_subcontracting()
+		self.update_reserved_qty_for_subcontracting()
 
 
 @frappe.whitelist()
@@ -357,8 +349,8 @@
 
 
 @frappe.whitelist()
-def update_subcontracting_order_status(sco):
+def update_subcontracting_order_status(sco, status=None):
 	if isinstance(sco, str):
 		sco = frappe.get_doc("Subcontracting Order", sco)
 
-	sco.update_status()
+	sco.update_status(status)
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
index 7ca1264..ec54944 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
@@ -10,7 +10,7 @@
 			"Completed": "green",
 			"Partial Material Transferred": "purple",
 			"Material Transferred": "blue",
-			"Closed": "red",
+			"Closed": "green",
 			"Cancelled": "red",
 		};
 		return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
index 37dabf1..6c0ee45 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -95,14 +95,14 @@
 		self.assertEqual(sco.status, "Partially Received")
 
 		# Closed
-		ste = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
-		ste.save()
-		ste.submit()
-		sco.load_from_db()
+		sco.update_status("Closed")
 		self.assertEqual(sco.status, "Closed")
-		ste.cancel()
-		sco.load_from_db()
+		scr = make_subcontracting_receipt(sco.name)
+		scr.save()
+		self.assertRaises(frappe.exceptions.ValidationError, scr.submit)
+		sco.update_status()
 		self.assertEqual(sco.status, "Partially Received")
+		scr.cancel()
 
 		# Completed
 		scr = make_subcontracting_receipt(sco.name)
@@ -564,7 +564,6 @@
 
 		sco.load_from_db()
 
-		self.assertEqual(sco.status, "Closed")
 		self.assertEqual(sco.supplied_items[0].returned_qty, 5)
 
 	def test_ordered_qty_for_subcontracting_order(self):
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index 575c4ed..0535799 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -93,7 +93,8 @@
 					get_query_filters: {
 						docstatus: 1,
 						per_received: ['<', 100],
-						company: frm.doc.company
+						company: frm.doc.company,
+						status: ['!=', 'Closed'],
 					}
 				});
 			}, __('Get Items From'));
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 52bf13c..7c2a1f1 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -8,6 +8,7 @@
 
 import erpnext
 from erpnext.accounts.utils import get_account_currency
+from erpnext.buying.utils import check_on_hold_or_closed_status
 from erpnext.controllers.subcontracting_controller import SubcontractingController
 from erpnext.stock.stock_ledger import get_valuation_rate
 
@@ -142,6 +143,7 @@
 		self.get_current_stock()
 
 	def on_submit(self):
+		self.validate_closed_subcontracting_order()
 		self.validate_available_qty_for_consumption()
 		self.update_status_updater_args()
 		self.update_prevdoc_status()
@@ -165,6 +167,7 @@
 			"Repost Item Valuation",
 			"Serial and Batch Bundle",
 		)
+		self.validate_closed_subcontracting_order()
 		self.update_status_updater_args()
 		self.update_prevdoc_status()
 		self.set_consumed_qty_in_subcontract_order()
@@ -175,6 +178,11 @@
 		self.update_status()
 		self.delete_auto_created_batches()
 
+	def validate_closed_subcontracting_order(self):
+		for item in self.items:
+			if item.subcontracting_order:
+				check_on_hold_or_closed_status("Subcontracting Order", item.subcontracting_order)
+
 	def validate_items_qty(self):
 		for item in self.items:
 			if not (item.qty or item.rejected_qty):