feat: add option to unreserve stock in SO
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index d222c3e..1bb181b 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -291,6 +291,11 @@
 				}
 				this.frm.page.set_inner_btn_group_as_primary(__('Create'));
 			}
+
+			// Stock Reservation
+			if (this.frm.doc.__onload && this.frm.doc.__onload.has_reserved_stock) {
+				this.frm.add_custom_button(__('Unreserve'), () => this.cancel_stock_reservation_entries(), __('Stock Reservation'));
+			}
 		}
 
 		if (this.frm.doc.docstatus===0) {
@@ -330,6 +335,22 @@
 		this.order_type(doc);
 	}
 
+	cancel_stock_reservation_entries() {
+		frappe.call({
+			method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.cancel_stock_reservation_entries",
+			args: {
+				voucher_type: this.frm.doctype,
+				voucher_no: this.frm.docname
+			},
+			freeze: true,
+			freeze_message: __("Unreserving Stock..."),
+			callback: (r) => {
+				this.frm.doc.__onload.has_reserved_stock = false;
+				this.frm.refresh();
+			}
+		})
+	}
+
 	create_pick_list() {
 		frappe.model.open_mapped_doc({
 			method: "erpnext.selling.doctype.sales_order.sales_order.create_pick_list",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index d492f71..06c84b0 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -44,6 +44,14 @@
 	def __init__(self, *args, **kwargs):
 		super(SalesOrder, self).__init__(*args, **kwargs)
 
+	def onload(self):
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			has_reserved_stock,
+		)
+
+		if has_reserved_stock(self.doctype, self.name):
+			self.set_onload("has_reserved_stock", True)
+
 	def validate(self):
 		super(SalesOrder, self).validate()
 		self.validate_delivery_date()
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
index c80ae57..1b6388d 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -85,7 +85,6 @@
 			)
 
 
-@frappe.whitelist()
 def get_available_qty_to_reserve(item_code, warehouse):
 	from frappe.query_builder.functions import Sum
 
@@ -170,15 +169,6 @@
 	).run(as_list=True)[0]
 
 
-def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool:
-	if get_stock_reservation_entries_for_voucher(
-		voucher_type, voucher_no, voucher_detail_no, fields=["name"]
-	):
-		return True
-
-	return False
-
-
 def get_sre_reserved_qty_details(item_code: str | list, warehouse: str | list) -> dict:
 	sre_details = {}
 
@@ -209,3 +199,25 @@
 			sre_details = {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in sre_data}
 
 	return sre_details
+
+
+@frappe.whitelist()
+def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool:
+	if get_stock_reservation_entries_for_voucher(
+		voucher_type, voucher_no, voucher_detail_no, fields=["name"]
+	):
+		return True
+
+	return False
+
+
+@frappe.whitelist()
+def cancel_stock_reservation_entries(
+	voucher_type: str, voucher_no: str, voucher_detail_no: str = None
+) -> None:
+	sre_list = get_stock_reservation_entries_for_voucher(
+		voucher_type, voucher_no, voucher_detail_no, fields=["name"]
+	)
+
+	for sre in sre_list:
+		frappe.get_doc("Stock Reservation Entry", sre.name).cancel()