Merge pull request #37603 from s-aga-r/AUTO-RESERVATION

feat: auto reserve stock for Sales Order on purchase
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 465fe96..7c40aaf 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -556,6 +556,8 @@
 					"bom": "bom",
 					"material_request": "material_request",
 					"material_request_item": "material_request_item",
+					"sales_order": "sales_order",
+					"sales_order_item": "sales_order_item",
 				},
 				"postprocess": update_item,
 				"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d59fe0e..53bddb5 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -340,5 +340,6 @@
 execute:frappe.delete_doc("Page", "welcome-to-erpnext")
 erpnext.patches.v15_0.delete_payment_gateway_doctypes
 erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
+erpnext.patches.v15_0.update_sre_from_voucher_details
 # below migration patch should always run last
 erpnext.patches.v14_0.migrate_gl_to_payment_ledger
\ No newline at end of file
diff --git a/erpnext/patches/v15_0/update_sre_from_voucher_details.py b/erpnext/patches/v15_0/update_sre_from_voucher_details.py
new file mode 100644
index 0000000..a9653cc
--- /dev/null
+++ b/erpnext/patches/v15_0/update_sre_from_voucher_details.py
@@ -0,0 +1,15 @@
+import frappe
+from frappe.query_builder.functions import IfNull
+
+
+def execute():
+	sre = frappe.qb.DocType("Stock Reservation Entry")
+	(
+		frappe.qb.update(sre)
+		.set(sre.from_voucher_type, "Pick List")
+		.set(sre.from_voucher_no, sre.against_pick_list)
+		.set(sre.from_voucher_detail_no, sre.against_pick_list_item)
+		.where(
+			(IfNull(sre.against_pick_list, "") != "") & (IfNull(sre.against_pick_list_item, "") != "")
+		)
+	).run()
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index ba8bc33..3ad18da 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -87,17 +87,13 @@
 				frm.events.get_items_from_internal_purchase_order(frm);
 			}
 
-			if (frm.is_new()) {
+			if (frm.doc.docstatus === 0) {
 				frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => {
-					if (value) {
-						frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order").then((value) => {
-							// If `Reserve Stock on Sales Order Submission` is enabled in Stock Settings, set Reserve Stock to 1 else 0.
-							frm.set_value("reserve_stock", value ? 1 : 0);
-						})
-					} else {
-						// If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and read only.
+					if (!value) {
+						// If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and make the field read-only and hidden.
 						frm.set_value("reserve_stock", 0);
 						frm.set_df_property("reserve_stock", "read_only", 1);
+						frm.set_df_property("reserve_stock", "hidden", 1);
 					}
 				})
 			}
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index a74084d..01d047c 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -1631,10 +1631,9 @@
   {
    "default": "0",
    "depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)",
-   "description": "If checked, Stock Reservation Entries will be created on <b>Submit</b>",
+   "description": "If checked, Stock will be reserved on <b>Submit</b>",
    "fieldname": "reserve_stock",
    "fieldtype": "Check",
-   "hidden": 1,
    "label": "Reserve Stock",
    "no_copy": 1,
    "print_hide": 1,
@@ -1645,7 +1644,7 @@
  "idx": 105,
  "is_submittable": 1,
  "links": [],
- "modified": "2023-07-24 08:59:11.599875",
+ "modified": "2023-10-18 12:41:54.813462",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index b91002e..94f9d6e 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -3,6 +3,7 @@
 
 
 import json
+from typing import Literal
 
 import frappe
 import frappe.utils
@@ -534,14 +535,24 @@
 		return False
 
 	@frappe.whitelist()
-	def create_stock_reservation_entries(self, items_details=None, notify=True) -> None:
+	def create_stock_reservation_entries(
+		self,
+		items_details: list[dict] = None,
+		from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
+		notify=True,
+	) -> None:
 		"""Creates Stock Reservation Entries for Sales Order Items."""
 
 		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
 			create_stock_reservation_entries_for_so_items as create_stock_reservation_entries,
 		)
 
-		create_stock_reservation_entries(so=self, items_details=items_details, notify=notify)
+		create_stock_reservation_entries(
+			sales_order=self,
+			items_details=items_details,
+			from_voucher_type=from_voucher_type,
+			notify=notify,
+		)
 
 	@frappe.whitelist()
 	def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None:
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index ae05b80..7cd171e 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -265,7 +265,8 @@
 			from_date: moment(frm.doc.creation).format('YYYY-MM-DD'),
 			to_date: to_date,
 			voucher_type: "Sales Order",
-			against_pick_list: frm.doc.name,
+			from_voucher_type: "Pick List",
+			from_voucher_no: frm.doc.name,
 		}
 		frappe.set_route("query-report", "Reserved Stock");
 	}
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 2fcd102..ed20209 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -229,20 +229,27 @@
 	def create_stock_reservation_entries(self, notify=True) -> None:
 		"""Creates Stock Reservation Entries for Sales Order Items against Pick List."""
 
-		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
-			create_stock_reservation_entries_for_so_items,
-		)
-
-		so_details = {}
+		so_items_details_map = {}
 		for location in self.locations:
 			if location.warehouse and location.sales_order and location.sales_order_item:
-				so_details.setdefault(location.sales_order, []).append(location)
+				item_details = {
+					"name": location.sales_order_item,
+					"item_code": location.item_code,
+					"warehouse": location.warehouse,
+					"qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)),
+					"from_voucher_no": location.parent,
+					"from_voucher_detail_no": location.name,
+					"serial_and_batch_bundle": location.serial_and_batch_bundle,
+				}
+				so_items_details_map.setdefault(location.sales_order, []).append(item_details)
 
-		if so_details:
-			for so, locations in so_details.items():
+		if so_items_details_map:
+			for so, items_details in so_items_details_map.items():
 				so_doc = frappe.get_doc("Sales Order", so)
-				create_stock_reservation_entries_for_so_items(
-					so=so_doc, items_details=locations, against_pick_list=True, notify=notify
+				so_doc.create_stock_reservation_entries(
+					items_details=items_details,
+					from_voucher_type="Pick List",
+					notify=notify,
 				)
 
 	@frappe.whitelist()
@@ -253,7 +260,9 @@
 			cancel_stock_reservation_entries,
 		)
 
-		cancel_stock_reservation_entries(against_pick_list=self.name, notify=notify)
+		cancel_stock_reservation_entries(
+			from_voucher_type="Pick List", from_voucher_no=self.name, notify=notify
+		)
 
 	def validate_picked_qty(self, data):
 		over_delivery_receipt_allowance = 100 + flt(
diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
index 0830fa2..29571a5 100644
--- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
+++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py
@@ -2,7 +2,7 @@
 	return {
 		"fieldname": "pick_list",
 		"non_standard_fieldnames": {
-			"Stock Reservation Entry": "against_pick_list",
+			"Stock Reservation Entry": "from_voucher_no",
 		},
 		"internal_links": {
 			"Sales Order": ["locations", "sales_order"],
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index d89d805..029d89c 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -263,6 +263,7 @@
 		self.make_gl_entries()
 		self.repost_future_sle_and_gle()
 		self.set_consumed_qty_in_subcontract_order()
+		self.reserve_stock_for_sales_order()
 
 	def check_next_docstatus(self):
 		submit_rv = frappe.db.sql(
@@ -759,6 +760,37 @@
 
 		self.load_from_db()
 
+	def reserve_stock_for_sales_order(self):
+		if self.is_return or not cint(
+			frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase")
+		):
+			return
+
+		self.reload()  # reload to get the Serial and Batch Bundle Details
+
+		so_items_details_map = {}
+		for item in self.items:
+			if item.sales_order and item.sales_order_item:
+				item_details = {
+					"name": item.sales_order_item,
+					"item_code": item.item_code,
+					"warehouse": item.warehouse,
+					"qty_to_reserve": item.stock_qty,
+					"from_voucher_no": item.parent,
+					"from_voucher_detail_no": item.name,
+					"serial_and_batch_bundle": item.serial_and_batch_bundle,
+				}
+				so_items_details_map.setdefault(item.sales_order, []).append(item_details)
+
+		if so_items_details_map:
+			for so, items_details in so_items_details_map.items():
+				so_doc = frappe.get_doc("Sales Order", so)
+				so_doc.create_stock_reservation_entries(
+					items_details=items_details,
+					from_voucher_type="Purchase Receipt",
+					notify=True,
+				)
+
 
 def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse):
 	return frappe.db.get_value(
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
index b3ae7b5..71489fb 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py
@@ -10,6 +10,7 @@
 			"Landed Cost Voucher": "receipt_document",
 			"Auto Repeat": "reference_document",
 			"Purchase Receipt": "return_against",
+			"Stock Reservation Entry": "from_voucher_no",
 		},
 		"internal_links": {
 			"Material Request": ["items", "material_request"],
@@ -18,7 +19,10 @@
 			"Quality Inspection": ["items", "quality_inspection"],
 		},
 		"transactions": [
-			{"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]},
+			{
+				"label": _("Related"),
+				"items": ["Purchase Invoice", "Landed Cost Voucher", "Asset", "Stock Reservation Entry"],
+			},
 			{
 				"label": _("Reference"),
 				"items": ["Material Request", "Purchase Order", "Quality Inspection", "Project"],
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index d93d21c..f5240a6 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -125,7 +125,9 @@
   "dimension_col_break",
   "cost_center",
   "section_break_80",
-  "page_break"
+  "page_break",
+  "sales_order",
+  "sales_order_item"
  ],
  "fields": [
   {
@@ -1062,12 +1064,32 @@
    "fieldtype": "Link",
    "label": "WIP Composite Asset",
    "options": "Asset"
+  },
+  {
+   "fieldname": "sales_order",
+   "fieldtype": "Link",
+   "label": "Sales Order",
+   "no_copy": 1,
+   "options": "Sales Order",
+   "print_hide": 1,
+   "read_only": 1,
+   "search_index": 1
+  },
+  {
+   "fieldname": "sales_order_item",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Sales Order Item",
+   "no_copy": 1,
+   "print_hide": 1,
+   "read_only": 1,
+   "search_index": 1
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-10-03 21:11:50.547261",
+ "modified": "2023-10-19 10:50:58.071735",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Purchase Receipt Item",
@@ -1078,4 +1100,4 @@
  "sort_field": "modified",
  "sort_order": "DESC",
  "states": []
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
index c5df319..f60a037 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
@@ -92,7 +92,7 @@
 			'qty', 'read_only', frm.doc.has_serial_no
 		);
 
-		frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.against_pick_list ? 0 : 1);
+		frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.from_voucher_type == "Pick List" ? 0 : 1);
 	},
 
 	hide_rate_related_fields(frm) {
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
index 5c3018f..76cedd4 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
@@ -17,8 +17,9 @@
   "voucher_no",
   "voucher_detail_no",
   "column_break_7dxj",
-  "against_pick_list",
-  "against_pick_list_item",
+  "from_voucher_type",
+  "from_voucher_no",
+  "from_voucher_detail_no",
   "section_break_xt4m",
   "stock_uom",
   "column_break_grdt",
@@ -158,7 +159,7 @@
    "oldfieldname": "actual_qty",
    "oldfieldtype": "Currency",
    "print_width": "150px",
-   "read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.against_pick_list) || (doc.delivered_qty > 0))",
+   "read_only_depends_on": "eval: ((doc.reservation_based_on == \"Serial and Batch\") || (doc.from_voucher_type == \"Pick List\") || (doc.delivered_qty > 0))",
    "width": "150px"
   },
   {
@@ -268,27 +269,7 @@
    "label": "Reservation Based On",
    "no_copy": 1,
    "options": "Qty\nSerial and Batch",
-   "read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.against_pick_list)"
-  },
-  {
-   "fieldname": "against_pick_list",
-   "fieldtype": "Link",
-   "label": "Against Pick List",
-   "no_copy": 1,
-   "options": "Pick List",
-   "print_hide": 1,
-   "read_only": 1,
-   "report_hide": 1,
-   "search_index": 1
-  },
-  {
-   "fieldname": "against_pick_list_item",
-   "fieldtype": "Data",
-   "label": "Against Pick List Item",
-   "no_copy": 1,
-   "print_hide": 1,
-   "read_only": 1,
-   "report_hide": 1
+   "read_only_depends_on": "eval: (doc.delivered_qty > 0 || doc.from_voucher_type == \"Pick List\")"
   },
   {
    "fieldname": "column_break_7dxj",
@@ -297,6 +278,36 @@
   {
    "fieldname": "column_break_grdt",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "from_voucher_type",
+   "fieldtype": "Select",
+   "label": "From Voucher Type",
+   "no_copy": 1,
+   "options": "\nPick List\nPurchase Receipt",
+   "print_hide": 1,
+   "read_only": 1,
+   "report_hide": 1
+  },
+  {
+   "fieldname": "from_voucher_detail_no",
+   "fieldtype": "Data",
+   "label": "From Voucher Detail No",
+   "no_copy": 1,
+   "print_hide": 1,
+   "read_only": 1,
+   "report_hide": 1
+  },
+  {
+   "fieldname": "from_voucher_no",
+   "fieldtype": "Dynamic Link",
+   "label": "From Voucher No",
+   "no_copy": 1,
+   "options": "from_voucher_type",
+   "print_hide": 1,
+   "read_only": 1,
+   "report_hide": 1,
+   "search_index": 1
   }
  ],
  "hide_toolbar": 1,
@@ -304,7 +315,7 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2023-08-08 17:15:13.317706",
+ "modified": "2023-10-19 16:41:16.545416",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Reservation Entry",
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 936be3f..81e9dfa 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -1,6 +1,8 @@
 # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
+from typing import Literal
+
 import frappe
 from frappe import _
 from frappe.model.document import Document
@@ -113,7 +115,7 @@
 		"""Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`."""
 
 		if (
-			not self.against_pick_list
+			not self.from_voucher_type
 			and (self.get("_action") == "submit")
 			and (self.has_serial_no or self.has_batch_no)
 			and cint(frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch"))
@@ -316,21 +318,24 @@
 	) -> None:
 		"""Updates total reserved qty in the Pick List."""
 
-		if self.against_pick_list and self.against_pick_list_item:
+		if (
+			self.from_voucher_type == "Pick List" and self.from_voucher_no and self.from_voucher_detail_no
+		):
 			sre = frappe.qb.DocType("Stock Reservation Entry")
 			reserved_qty = (
 				frappe.qb.from_(sre)
 				.select(Sum(sre.reserved_qty))
 				.where(
 					(sre.docstatus == 1)
-					& (sre.against_pick_list == self.against_pick_list)
-					& (sre.against_pick_list_item == self.against_pick_list_item)
+					& (sre.from_voucher_type == "Pick List")
+					& (sre.from_voucher_no == self.from_voucher_no)
+					& (sre.from_voucher_detail_no == self.from_voucher_detail_no)
 				)
 			).run(as_list=True)[0][0] or 0
 
 			frappe.db.set_value(
 				"Pick List Item",
-				self.against_pick_list_item,
+				self.from_voucher_detail_no,
 				reserved_qty_field,
 				reserved_qty,
 				update_modified=update_modified,
@@ -365,7 +370,7 @@
 			).format(self.status, self.doctype)
 			frappe.throw(msg)
 
-		if self.against_pick_list:
+		if self.from_voucher_type == "Pick List":
 			msg = _(
 				"Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one."
 			)
@@ -761,25 +766,27 @@
 
 
 def create_stock_reservation_entries_for_so_items(
-	so: object,
+	sales_order: object,
 	items_details: list[dict] = None,
-	against_pick_list: bool = False,
+	from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
 	notify=True,
 ) -> None:
 	"""Creates Stock Reservation Entries for Sales Order Items."""
 
 	from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty
 
-	if not against_pick_list and (
-		so.get("_action") == "submit"
-		and so.set_warehouse
-		and cint(frappe.get_cached_value("Warehouse", so.set_warehouse, "is_group"))
+	if not from_voucher_type and (
+		sales_order.get("_action") == "submit"
+		and sales_order.set_warehouse
+		and cint(frappe.get_cached_value("Warehouse", sales_order.set_warehouse, "is_group"))
 	):
 		return frappe.msgprint(
-			_("Stock cannot be reserved in the group warehouse {0}.").format(frappe.bold(so.set_warehouse))
+			_("Stock cannot be reserved in the group warehouse {0}.").format(
+				frappe.bold(sales_order.set_warehouse)
+			)
 		)
 
-	validate_stock_reservation_settings(so)
+	validate_stock_reservation_settings(sales_order)
 
 	allow_partial_reservation = frappe.db.get_single_value(
 		"Stock Settings", "allow_partial_reservation"
@@ -788,38 +795,36 @@
 	items = []
 	if items_details:
 		for item in items_details:
-			so_item = frappe.get_doc(
-				"Sales Order Item", item.get("sales_order_item") if against_pick_list else item.get("name")
-			)
-			so_item.reserve_stock = 1
+			so_item = frappe.get_doc("Sales Order Item", item.get("name"))
 			so_item.warehouse = item.get("warehouse")
 			so_item.qty_to_reserve = (
-				item.get("picked_qty") - item.get("stock_reserved_qty", 0)
-				if against_pick_list
-				else (flt(item.get("qty_to_reserve")) * flt(so_item.conversion_factor, 1))
+				flt(item.get("qty_to_reserve"))
+				if from_voucher_type in ["Pick List", "Purchase Receipt"]
+				else (
+					flt(item.get("qty_to_reserve"))
+					* (flt(item.get("conversion_factor")) or flt(so_item.conversion_factor) or 1)
+				)
 			)
-
-			if against_pick_list:
-				so_item.pick_list = item.get("parent")
-				so_item.pick_list_item = item.get("name")
-				so_item.pick_list_sbb = item.get("serial_and_batch_bundle")
+			so_item.from_voucher_no = item.get("from_voucher_no")
+			so_item.from_voucher_detail_no = item.get("from_voucher_detail_no")
+			so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle")
 
 			items.append(so_item)
 
 	sre_count = 0
-	reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", so.name)
+	reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", sales_order.name)
 
-	for item in items if items_details else so.get("items"):
+	for item in items if items_details else sales_order.get("items"):
 		# Skip if `Reserved Stock` is not checked for the item.
 		if not item.get("reserve_stock"):
 			continue
 
 		# Stock should be reserved from the Pick List if has Picked Qty.
-		if not against_pick_list and flt(item.picked_qty) > 0:
+		if not from_voucher_type == "Pick List" and flt(item.picked_qty) > 0:
 			frappe.throw(
-				_(
-					"Row #{0}: Item {1} has been picked, please create a Stock Reservation from the Pick List."
-				).format(item.idx, frappe.bold(item.item_code))
+				_("Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.").format(
+					item.idx, frappe.bold(item.item_code)
+				)
 			)
 
 		is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value(
@@ -828,13 +833,15 @@
 
 		# Skip if Non-Stock Item.
 		if not is_stock_item:
-			frappe.msgprint(
-				_("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format(
-					item.idx, frappe.bold(item.item_code)
-				),
-				title=_("Stock Reservation"),
-				indicator="yellow",
-			)
+			if not from_voucher_type:
+				frappe.msgprint(
+					_("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format(
+						item.idx, frappe.bold(item.item_code)
+					),
+					title=_("Stock Reservation"),
+					indicator="yellow",
+				)
+
 			item.db_set("reserve_stock", 0)
 			continue
 
@@ -853,13 +860,15 @@
 
 		# Stock is already reserved for the item, notify the user and skip the item.
 		if unreserved_qty <= 0:
-			frappe.msgprint(
-				_("Row #{0}: Stock is already reserved for the Item {1}.").format(
-					item.idx, frappe.bold(item.item_code)
-				),
-				title=_("Stock Reservation"),
-				indicator="yellow",
-			)
+			if not from_voucher_type:
+				frappe.msgprint(
+					_("Row #{0}: Stock is already reserved for the Item {1}.").format(
+						item.idx, frappe.bold(item.item_code)
+					),
+					title=_("Stock Reservation"),
+					indicator="yellow",
+				)
+
 			continue
 
 		available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse)
@@ -867,7 +876,7 @@
 		# No stock available to reserve, notify the user and skip the item.
 		if available_qty_to_reserve <= 0:
 			frappe.msgprint(
-				_("Row #{0}: No available stock to reserve for the Item {1} in Warehouse {2}.").format(
+				_("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format(
 					item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse)
 				),
 				title=_("Stock Reservation"),
@@ -893,7 +902,9 @@
 
 		# Partial Reservation
 		if qty_to_be_reserved < unreserved_qty:
-			if not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")):
+			if not from_voucher_type and (
+				not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve"))
+			):
 				msg = _("Row #{0}: Only {1} available to reserve for the Item {2}").format(
 					item.idx,
 					frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom),
@@ -915,33 +926,42 @@
 		sre.warehouse = item.warehouse
 		sre.has_serial_no = has_serial_no
 		sre.has_batch_no = has_batch_no
-		sre.voucher_type = so.doctype
-		sre.voucher_no = so.name
+		sre.voucher_type = sales_order.doctype
+		sre.voucher_no = sales_order.name
 		sre.voucher_detail_no = item.name
 		sre.available_qty = available_qty_to_reserve
 		sre.voucher_qty = item.stock_qty
 		sre.reserved_qty = qty_to_be_reserved
-		sre.company = so.company
+		sre.company = sales_order.company
 		sre.stock_uom = item.stock_uom
-		sre.project = so.project
+		sre.project = sales_order.project
 
-		if against_pick_list:
-			sre.against_pick_list = item.pick_list
-			sre.against_pick_list_item = item.pick_list_item
+		if from_voucher_type:
+			sre.from_voucher_type = from_voucher_type
+			sre.from_voucher_no = item.from_voucher_no
+			sre.from_voucher_detail_no = item.from_voucher_detail_no
 
-			if item.pick_list_sbb:
-				sbb = frappe.get_doc("Serial and Batch Bundle", item.pick_list_sbb)
-				sre.reservation_based_on = "Serial and Batch"
-				for entry in sbb.entries:
-					sre.append(
-						"sb_entries",
-						{
-							"serial_no": entry.serial_no,
-							"batch_no": entry.batch_no,
-							"qty": 1 if has_serial_no else abs(entry.qty),
-							"warehouse": entry.warehouse,
-						},
-					)
+		if item.get("serial_and_batch_bundle"):
+			sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
+			sre.reservation_based_on = "Serial and Batch"
+
+			index, picked_qty = 0, 0
+			while index < len(sbb.entries) and picked_qty < qty_to_be_reserved:
+				entry = sbb.entries[index]
+				qty = 1 if has_serial_no else min(abs(entry.qty), qty_to_be_reserved - picked_qty)
+
+				sre.append(
+					"sb_entries",
+					{
+						"serial_no": entry.serial_no,
+						"batch_no": entry.batch_no,
+						"qty": qty,
+						"warehouse": entry.warehouse,
+					},
+				)
+
+				index += 1
+				picked_qty += qty
 
 		sre.save()
 		sre.submit()
@@ -956,29 +976,37 @@
 	voucher_type: str = None,
 	voucher_no: str = None,
 	voucher_detail_no: str = None,
-	against_pick_list: str = None,
+	from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
+	from_voucher_no: str = None,
+	from_voucher_detail_no: str = None,
 	sre_list: list[dict] = None,
 	notify: bool = True,
 ) -> None:
 	"""Cancel Stock Reservation Entries."""
 
-	if not sre_list and against_pick_list:
-		sre = frappe.qb.DocType("Stock Reservation Entry")
-		sre_list = (
-			frappe.qb.from_(sre)
-			.select(sre.name)
-			.where(
-				(sre.docstatus == 1)
-				& (sre.against_pick_list == against_pick_list)
-				& (sre.status.notin(["Delivered", "Cancelled"]))
+	if not sre_list:
+		if voucher_type and voucher_no:
+			sre_list = get_stock_reservation_entries_for_voucher(
+				voucher_type, voucher_no, voucher_detail_no, fields=["name"]
 			)
-			.orderby(sre.creation)
-		).run(as_dict=True)
+		elif from_voucher_type and from_voucher_no:
+			sre = frappe.qb.DocType("Stock Reservation Entry")
+			query = (
+				frappe.qb.from_(sre)
+				.select(sre.name)
+				.where(
+					(sre.docstatus == 1)
+					& (sre.from_voucher_type == from_voucher_type)
+					& (sre.from_voucher_no == from_voucher_no)
+					& (sre.status.notin(["Delivered", "Cancelled"]))
+				)
+				.orderby(sre.creation)
+			)
 
-	elif not sre_list and (voucher_type and voucher_no):
-		sre_list = get_stock_reservation_entries_for_voucher(
-			voucher_type, voucher_no, voucher_detail_no, fields=["name"]
-		)
+			if from_voucher_detail_no:
+				query = query.where(sre.from_voucher_detail_no == from_voucher_detail_no)
+
+			sre_list = query.run(as_dict=True)
 
 	if sre_list:
 		for sre in sre_list:
diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
index 1168a4e..f4c74a8 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
@@ -5,6 +5,7 @@
 
 import frappe
 from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import today
 
 from erpnext.selling.doctype.sales_order.sales_order import create_pick_list, make_delivery_note
 from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@@ -28,10 +29,6 @@
 			items={self.sr_item.name: self.sr_item}, warehouse=self.warehouse, qty=100
 		)
 
-	def tearDown(self) -> None:
-		cancel_all_stock_reservation_entries()
-		return super().tearDown()
-
 	@change_settings("Stock Settings", {"allow_negative_stock": 0})
 	def test_validate_stock_reservation_settings(self) -> None:
 		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
@@ -555,8 +552,9 @@
 						(sre.voucher_type == "Sales Order")
 						& (sre.voucher_no == location.sales_order)
 						& (sre.voucher_detail_no == location.sales_order_item)
-						& (sre.against_pick_list == pl.name)
-						& (sre.against_pick_list_item == location.name)
+						& (sre.from_voucher_type == "Pick List")
+						& (sre.from_voucher_no == pl.name)
+						& (sre.from_voucher_detail_no == location.name)
 					)
 				).run(as_dict=True)
 				reserved_sb_details: set[tuple] = {
@@ -567,6 +565,90 @@
 				# Test - 3: Reserved Serial/Batch Nos should be equal to Picked Serial/Batch Nos.
 				self.assertSetEqual(picked_sb_details, reserved_sb_details)
 
+	@change_settings(
+		"Stock Settings",
+		{
+			"allow_negative_stock": 0,
+			"enable_stock_reservation": 1,
+			"auto_reserve_serial_and_batch": 1,
+			"pick_serial_and_batch_based_on": "FIFO",
+			"auto_reserve_stock_for_sales_order_on_purchase": 1,
+		},
+	)
+	def test_stock_reservation_from_purchase_receipt(self):
+		from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
+		from erpnext.selling.doctype.sales_order.sales_order import make_material_request
+		from erpnext.stock.doctype.material_request.material_request import make_purchase_order
+
+		items_details = create_items()
+		create_material_receipt(items_details, self.warehouse, qty=10)
+
+		item_list = []
+		for item_code, properties in items_details.items():
+			item_list.append(
+				{
+					"item_code": item_code,
+					"warehouse": self.warehouse,
+					"qty": randint(11, 100),
+					"uom": properties.stock_uom,
+					"rate": randint(10, 400),
+				}
+			)
+
+		so = make_sales_order(
+			item_list=item_list,
+			warehouse=self.warehouse,
+		)
+
+		mr = make_material_request(so.name)
+		mr.schedule_date = today()
+		mr.save().submit()
+
+		po = make_purchase_order(mr.name)
+		po.supplier = "_Test Supplier"
+		po.save().submit()
+
+		pr = make_purchase_receipt(po.name)
+		pr.save().submit()
+
+		for item in pr.items:
+			sre, status, reserved_qty = frappe.db.get_value(
+				"Stock Reservation Entry",
+				{
+					"from_voucher_type": "Purchase Receipt",
+					"from_voucher_no": pr.name,
+					"from_voucher_detail_no": item.name,
+				},
+				["name", "status", "reserved_qty"],
+			)
+
+			# Test - 1: SRE status should be `Reserved`.
+			self.assertEqual(status, "Reserved")
+
+			# Test - 2: SRE Reserved Qty should be equal to PR Item Qty.
+			self.assertEqual(reserved_qty, item.qty)
+
+			if item.serial_and_batch_bundle:
+				sb_details = frappe.db.get_all(
+					"Serial and Batch Entry",
+					filters={"parent": item.serial_and_batch_bundle},
+					fields=["serial_no", "batch_no", "qty"],
+					as_list=True,
+				)
+				reserved_sb_details = frappe.db.get_all(
+					"Serial and Batch Entry",
+					filters={"parent": sre},
+					fields=["serial_no", "batch_no", "qty"],
+					as_list=True,
+				)
+
+				# Test - 3: Reserved Serial/Batch Nos should be equal to PR Item Serial/Batch Nos.
+				self.assertEqual(set(sb_details), set(reserved_sb_details))
+
+	def tearDown(self) -> None:
+		cancel_all_stock_reservation_entries()
+		return super().tearDown()
+
 
 def create_items() -> dict:
 	items_properties = [
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 2052daa..1228290 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -38,8 +38,8 @@
   "stock_reservation_tab",
   "enable_stock_reservation",
   "column_break_rx3e",
-  "auto_reserve_stock_for_sales_order",
   "allow_partial_reservation",
+  "auto_reserve_stock_for_sales_order_on_purchase",
   "serial_and_batch_reservation_section",
   "auto_reserve_serial_and_batch",
   "serial_and_batch_item_settings_tab",
@@ -65,8 +65,7 @@
   "stock_frozen_upto_days",
   "column_break_26",
   "role_allowed_to_create_edit_back_dated_transactions",
-  "stock_auth_role",
-  "section_break_plhx"
+  "stock_auth_role"
  ],
  "fields": [
   {
@@ -356,7 +355,7 @@
   {
    "default": "1",
    "depends_on": "eval: doc.enable_stock_reservation",
-   "description": "If enabled, <b>Partial Stock Reservation Entries</b> can be created. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ",
+   "description": "Partial stock can be reserved. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ",
    "fieldname": "allow_partial_reservation",
    "fieldtype": "Check",
    "label": "Allow Partial Reservation"
@@ -383,7 +382,7 @@
   {
    "default": "1",
    "depends_on": "eval: doc.enable_stock_reservation",
-   "description": "If enabled, Serial and Batch Nos will be auto-reserved based on <b>Pick Serial / Batch Based On</b>",
+   "description": "Serial and Batch Nos will be auto-reserved based on <b>Pick Serial / Batch Based On</b>",
    "fieldname": "auto_reserve_serial_and_batch",
    "fieldtype": "Check",
    "label": "Auto Reserve Serial and Batch Nos"
@@ -394,14 +393,6 @@
    "label": "Serial and Batch Reservation"
   },
   {
-   "default": "0",
-   "depends_on": "eval: doc.enable_stock_reservation",
-   "description": "If enabled, <b>Stock Reservation Entries</b> will be created on submission of <b>Sales Order</b>",
-   "fieldname": "auto_reserve_stock_for_sales_order",
-   "fieldtype": "Check",
-   "label": "Auto Reserve Stock for Sales Order"
-  },
-  {
    "fieldname": "conversion_factor_section",
    "fieldtype": "Section Break",
    "label": "Stock UOM Quantity"
@@ -421,6 +412,14 @@
    "fieldname": "allow_to_edit_stock_uom_qty_for_purchase",
    "fieldtype": "Check",
    "label": "Allow to Edit Stock UOM Qty for Purchase Documents"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: doc.enable_stock_reservation",
+   "description": "Stock will be reserved on submission of <b>Purchase Receipt</b> created against Material Receipt for Sales Order.",
+   "fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
+   "fieldtype": "Check",
+   "label": "Auto Reserve Stock for Sales Order on Purchase"
   }
  ],
  "icon": "icon-cog",
@@ -428,7 +427,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2023-10-01 14:22:36.136111",
+ "modified": "2023-10-18 12:35:30.068799",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Settings",
@@ -453,4 +452,4 @@
  "sort_order": "ASC",
  "states": [],
  "track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.js b/erpnext/stock/report/reserved_stock/reserved_stock.js
index 2199f52..6872741 100644
--- a/erpnext/stock/report/reserved_stock/reserved_stock.js
+++ b/erpnext/stock/report/reserved_stock/reserved_stock.js
@@ -91,16 +91,30 @@
 			},
 		},
 		{
-			fieldname: "against_pick_list",
-			label: __("Against Pick List"),
+			fieldname: "from_voucher_type",
+			label: __("From Voucher Type"),
 			fieldtype: "Link",
-			options: "Pick List",
+			options: "DocType",
+			get_query: () => ({
+				filters: {
+					name: ["in", ["Pick List", "Purchase Receipt"]],
+				}
+			}),
+		},
+		{
+			fieldname: "from_voucher_no",
+			label: __("From Voucher No"),
+			fieldtype: "Dynamic Link",
+			options: "from_voucher_type",
 			get_query: () => ({
 				filters: {
 					docstatus: 1,
 					company: frappe.query_report.get_filter_value("company"),
 				},
 			}),
+			get_options: function () {
+				return frappe.query_report.get_filter_value("from_voucher_type");
+			},
 		},
 		{
 			fieldname: "reservation_based_on",
diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.py b/erpnext/stock/report/reserved_stock/reserved_stock.py
index d93ee1c..21ce203 100644
--- a/erpnext/stock/report/reserved_stock/reserved_stock.py
+++ b/erpnext/stock/report/reserved_stock/reserved_stock.py
@@ -44,7 +44,8 @@
 			(sre.available_qty - sre.reserved_qty).as_("available_qty"),
 			sre.voucher_type,
 			sre.voucher_no,
-			sre.against_pick_list,
+			sre.from_voucher_type,
+			sre.from_voucher_no,
 			sre.name.as_("stock_reservation_entry"),
 			sre.status,
 			sre.project,
@@ -65,7 +66,8 @@
 		"warehouse",
 		"voucher_type",
 		"voucher_no",
-		"against_pick_list",
+		"from_voucher_type",
+		"from_voucher_no",
 		"reservation_based_on",
 		"status",
 		"project",
@@ -142,7 +144,6 @@
 			"fieldname": "voucher_type",
 			"label": _("Voucher Type"),
 			"fieldtype": "Data",
-			"options": "Warehouse",
 			"width": 110,
 		},
 		{
@@ -153,11 +154,17 @@
 			"width": 120,
 		},
 		{
-			"fieldname": "against_pick_list",
-			"label": _("Against Pick List"),
-			"fieldtype": "Link",
-			"options": "Pick List",
-			"width": 130,
+			"fieldname": "from_voucher_type",
+			"label": _("From Voucher Type"),
+			"fieldtype": "Data",
+			"width": 110,
+		},
+		{
+			"fieldname": "from_voucher_no",
+			"label": _("From Voucher No"),
+			"fieldtype": "Dynamic Link",
+			"options": "from_voucher_type",
+			"width": 120,
 		},
 		{
 			"fieldname": "stock_reservation_entry",