Merge pull request #35426 from rohitwaghchaure/fixed-incorrect-actual-qty-bin

fix: incorrect available quantity in BIN
diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py
index cd5f366..f0ca405 100644
--- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py
+++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py
@@ -125,12 +125,14 @@
 
 	data_to_be_removed = True
 	while data_to_be_removed:
-		revenue, data_to_be_removed = remove_parent_with_no_child(revenue, period_list)
-	revenue = adjust_account(revenue, period_list)
+		revenue, data_to_be_removed = remove_parent_with_no_child(revenue)
+
+	adjust_account_totals(revenue, period_list)
+
 	return copy.deepcopy(revenue)
 
 
-def remove_parent_with_no_child(data, period_list):
+def remove_parent_with_no_child(data):
 	data_to_be_removed = False
 	for parent in data:
 		if "is_group" in parent and parent.get("is_group") == 1:
@@ -147,16 +149,19 @@
 	return data, data_to_be_removed
 
 
-def adjust_account(data, period_list, consolidated=False):
-	leaf_nodes = [item for item in data if item["is_group"] == 0]
+def adjust_account_totals(data, period_list):
 	totals = {}
-	for node in leaf_nodes:
-		set_total(node, node["total"], data, totals)
-	for d in data:
-		for period in period_list:
-			key = period if consolidated else period.key
-			d["total"] = totals[d["account"]]
-	return data
+	for d in reversed(data):
+		if d.get("is_group"):
+			for period in period_list:
+				# reset totals for group accounts as totals set by get_data doesn't consider include_in_gross check
+				d[period.key] = sum(
+					item[period.key] for item in data if item.get("parent_account") == d.get("account")
+				)
+		else:
+			set_total(d, d["total"], data, totals)
+
+		d["total"] = totals[d["account"]]
 
 
 def set_total(node, value, complete_list, totals):
@@ -191,6 +196,9 @@
 
 		if profit_loss[key]:
 			has_value = True
+			if not profit_loss.get("total"):
+				profit_loss["total"] = 0
+			profit_loss["total"] += profit_loss[key]
 
 	if has_value:
 		return profit_loss
@@ -229,6 +237,9 @@
 
 		if profit_loss[key]:
 			has_value = True
+			if not profit_loss.get("total"):
+				profit_loss["total"] = 0
+			profit_loss["total"] += profit_loss[key]
 
 	if has_value:
 		return profit_loss
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 78bb056..20b332e 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2826,6 +2826,17 @@
 	parent.update_billing_percentage()
 	parent.set_status()
 
+	# Cancel and Recreate Stock Reservation Entries.
+	if parent_doctype == "Sales Order":
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			cancel_stock_reservation_entries,
+			has_reserved_stock,
+		)
+
+		if has_reserved_stock(parent.doctype, parent.name):
+			cancel_stock_reservation_entries(parent.doctype, parent.name)
+			parent.create_stock_reservation_entries()
+
 
 @erpnext.allow_regional
 def validate_regional(doc):
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 7e68ec1..3a59d3c 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -333,3 +333,4 @@
 execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
 erpnext.patches.v15_0.enable_all_leads
 erpnext.patches.v14_0.update_company_in_ldc
+erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
diff --git a/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py b/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py
new file mode 100644
index 0000000..1aeb2e6
--- /dev/null
+++ b/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py
@@ -0,0 +1,60 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.query_builder.functions import Sum
+
+
+def execute():
+	ps = frappe.qb.DocType("Packing Slip")
+	dn = frappe.qb.DocType("Delivery Note")
+	ps_item = frappe.qb.DocType("Packing Slip Item")
+
+	ps_details = (
+		frappe.qb.from_(ps)
+		.join(ps_item)
+		.on(ps.name == ps_item.parent)
+		.join(dn)
+		.on(ps.delivery_note == dn.name)
+		.select(
+			dn.name.as_("delivery_note"),
+			ps_item.item_code.as_("item_code"),
+			Sum(ps_item.qty).as_("packed_qty"),
+		)
+		.where((ps.docstatus == 1) & (dn.docstatus == 0))
+		.groupby(dn.name, ps_item.item_code)
+	).run(as_dict=True)
+
+	if ps_details:
+		dn_list = set()
+		item_code_list = set()
+		for ps_detail in ps_details:
+			dn_list.add(ps_detail.delivery_note)
+			item_code_list.add(ps_detail.item_code)
+
+		dn_item = frappe.qb.DocType("Delivery Note Item")
+		dn_item_query = (
+			frappe.qb.from_(dn_item)
+			.select(
+				dn.parent.as_("delivery_note"),
+				dn_item.name,
+				dn_item.item_code,
+				dn_item.qty,
+			)
+			.where((dn_item.parent.isin(dn_list)) & (dn_item.item_code.isin(item_code_list)))
+		)
+
+		dn_details = frappe._dict()
+		for r in dn_item_query.run(as_dict=True):
+			dn_details.setdefault((r.delivery_note, r.item_code), frappe._dict()).setdefault(r.name, r.qty)
+
+		for ps_detail in ps_details:
+			dn_items = dn_details.get((ps_detail.delivery_note, ps_detail.item_code))
+
+			if dn_items:
+				remaining_qty = ps_detail.packed_qty
+				for name, qty in dn_items.items():
+					if remaining_qty > 0:
+						row_packed_qty = min(qty, remaining_qty)
+						frappe.db.set_value("Delivery Note Item", name, "packed_qty", row_packed_qty)
+						remaining_qty -= row_packed_qty
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index e9a6cc3..5d43a07 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -47,21 +47,50 @@
 		frm.set_df_property('packed_items', 'cannot_add_rows', true);
 		frm.set_df_property('packed_items', 'cannot_delete_rows', true);
 	},
+
 	refresh: function(frm) {
-		if(frm.doc.docstatus === 1 && frm.doc.status !== 'Closed'
-			&& flt(frm.doc.per_delivered, 6) < 100 && flt(frm.doc.per_billed, 6) < 100) {
-			frm.add_custom_button(__('Update Items'), () => {
-				erpnext.utils.update_child_items({
-					frm: frm,
-					child_docname: "items",
-					child_doctype: "Sales Order Detail",
-					cannot_add_row: false,
-				})
-			});
+		if(frm.doc.docstatus === 1) {
+			if (frm.doc.status !== 'Closed' && flt(frm.doc.per_delivered, 6) < 100 && flt(frm.doc.per_billed, 6) < 100) {
+				frm.add_custom_button(__('Update Items'), () => {
+					erpnext.utils.update_child_items({
+						frm: frm,
+						child_docname: "items",
+						child_doctype: "Sales Order Detail",
+						cannot_add_row: false,
+					})
+				});
+
+				// Stock Reservation > Reserve button will be only visible if the SO has unreserved stock.
+				if (frm.doc.__onload && frm.doc.__onload.has_unreserved_stock) {
+					frm.add_custom_button(__('Reserve'), () => frm.events.create_stock_reservation_entries(frm), __('Stock Reservation'));
+				}
+			}
+
+			// Stock Reservation > Unreserve button will be only visible if the SO has reserved stock.
+			if (frm.doc.__onload && frm.doc.__onload.has_reserved_stock) {
+				frm.add_custom_button(__('Unreserve'), () => frm.events.cancel_stock_reservation_entries(frm), __('Stock Reservation'));
+			}
 		}
 
-		if (frm.doc.docstatus === 0 && frm.doc.is_internal_customer) {
-			frm.events.get_items_from_internal_purchase_order(frm);
+		if (frm.doc.docstatus === 0) {
+			if (frm.doc.is_internal_customer) {
+				frm.events.get_items_from_internal_purchase_order(frm);
+			}
+
+			if (frm.is_new()) {
+				frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => {
+					if (value) {
+						frappe.db.get_single_value("Stock Settings", "reserve_stock_on_sales_order_submission").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.
+						frm.set_value("reserve_stock", 0);
+						frm.set_df_property("reserve_stock", "read_only", 1);
+					}
+				})
+			}
 		}
 	},
 
@@ -137,6 +166,108 @@
 			if(!d.delivery_date) d.delivery_date = frm.doc.delivery_date;
 		});
 		refresh_field("items");
+	},
+
+	create_stock_reservation_entries(frm) {
+		let items_data = [];
+
+		const dialog = frappe.prompt({fieldname: 'items', fieldtype: 'Table', label: __('Items to Reserve'),
+			fields: [
+				{
+					fieldtype: 'Data',
+					fieldname: 'name',
+					label: __('Name'),
+					reqd: 1,
+					read_only: 1,
+				},
+				{
+					fieldtype: 'Link',
+					fieldname: 'item_code',
+					label: __('Item Code'),
+					options: 'Item',
+					reqd: 1,
+					read_only: 1,
+					in_list_view: 1,
+				},
+				{
+					fieldtype: 'Link',
+					fieldname: 'warehouse',
+					label: __('Warehouse'),
+					options: 'Warehouse',
+					reqd: 1,
+					in_list_view: 1,
+					get_query: function () {
+						return {
+							filters: [
+								["Warehouse", "is_group", "!=", 1]
+							]
+						};
+					},
+				},
+				{
+					fieldtype: 'Float',
+					fieldname: 'qty_to_reserve',
+					label: __('Qty'),
+					reqd: 1,
+					in_list_view: 1
+				}
+			],
+			data: items_data,
+			in_place_edit: true,
+			get_data: function() {
+				return items_data;
+			}
+		}, function(data) {
+			if (data.items.length > 0) {
+				frappe.call({
+					doc: frm.doc,
+					method: 'create_stock_reservation_entries',
+					args: {
+						items_details: data.items,
+						notify: true
+					},
+					freeze: true,
+					freeze_message: __('Reserving Stock...'),
+					callback: (r) => {
+						frm.doc.__onload.has_unreserved_stock = false;
+						frm.reload_doc();
+					}
+				});
+			}
+		}, __("Stock Reservation"), __("Reserve Stock"));
+
+		frm.doc.items.forEach(item => {
+			if (item.reserve_stock) {
+				let unreserved_qty = (flt(item.stock_qty) - (flt(item.delivered_qty) * flt(item.conversion_factor)) - flt(item.stock_reserved_qty))
+
+				if (unreserved_qty > 0) {
+					dialog.fields_dict.items.df.data.push({
+						'name': item.name,
+						'item_code': item.item_code,
+						'warehouse': item.warehouse,
+						'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor))
+					});
+				}
+			}
+		});
+
+		dialog.fields_dict.items.grid.refresh();
+	},
+
+	cancel_stock_reservation_entries(frm) {
+		frappe.call({
+			method: 'erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.cancel_stock_reservation_entries',
+			args: {
+				voucher_type: frm.doctype,
+				voucher_no: frm.docname
+			},
+			freeze: true,
+			freeze_message: __('Unreserving Stock...'),
+			callback: (r) => {
+				frm.doc.__onload.has_reserved_stock = false;
+				frm.reload_doc();
+			}
+		})
 	}
 });
 
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 4f498fb..f7143d7 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -42,6 +42,7 @@
   "scan_barcode",
   "column_break_28",
   "set_warehouse",
+  "reserve_stock",
   "items_section",
   "items",
   "section_break_31",
@@ -1625,13 +1626,24 @@
    "fieldname": "named_place",
    "fieldtype": "Data",
    "label": "Named Place"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)",
+   "description": "If checked, Stock Reservation Entries will be created on <b>Submit</b>",
+   "fieldname": "reserve_stock",
+   "fieldtype": "Check",
+   "label": "Reserve Stock",
+   "no_copy": 1,
+   "print_hide": 1,
+   "report_hide": 1
   }
  ],
  "icon": "fa fa-file-text",
  "idx": 105,
  "is_submittable": 1,
  "links": [],
- "modified": "2023-04-20 11:14:01.036202",
+ "modified": "2023-04-22 09:55:37.008190",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order",
@@ -1664,7 +1676,6 @@
    "read": 1,
    "report": 1,
    "role": "Sales Manager",
-   "set_user_permissions": 1,
    "share": 1,
    "submit": 1,
    "write": 1
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 06467e5..353fa9b 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -30,6 +30,11 @@
 from erpnext.selling.doctype.customer.customer import check_credit_limit
 from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
 from erpnext.stock.doctype.item.item import get_item_defaults
+from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+	cancel_stock_reservation_entries,
+	get_sre_reserved_qty_details_for_voucher,
+	has_reserved_stock,
+)
 from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate
 from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty
 
@@ -44,6 +49,14 @@
 	def __init__(self, *args, **kwargs):
 		super(SalesOrder, self).__init__(*args, **kwargs)
 
+	def onload(self) -> None:
+		if frappe.get_cached_value("Stock Settings", None, "enable_stock_reservation"):
+			if self.has_unreserved_stock():
+				self.set_onload("has_unreserved_stock", True)
+
+		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()
@@ -241,6 +254,9 @@
 
 			update_coupon_code_count(self.coupon_code, "used")
 
+		if self.get("reserve_stock"):
+			self.create_stock_reservation_entries()
+
 	def on_cancel(self):
 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
 		super(SalesOrder, self).on_cancel()
@@ -257,6 +273,7 @@
 		self.db_set("status", "Cancelled")
 
 		self.update_blanket_order()
+		cancel_stock_reservation_entries("Sales Order", self.name)
 
 		unlink_inter_company_doc(self.doctype, self.name, self.inter_company_order_reference)
 		if self.coupon_code:
@@ -485,6 +502,166 @@
 					).format(item.item_code)
 				)
 
+	def has_unreserved_stock(self) -> bool:
+		"""Returns True if there is any unreserved item in the Sales Order."""
+
+		reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", self.name)
+
+		for item in self.get("items"):
+			if not item.get("reserve_stock"):
+				continue
+
+			unreserved_qty = get_unreserved_qty(item, reserved_qty_details)
+			if unreserved_qty > 0:
+				return True
+
+		return False
+
+	@frappe.whitelist()
+	def create_stock_reservation_entries(self, items_details=None, notify=True):
+		"""Creates Stock Reservation Entries for Sales Order Items."""
+
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			get_available_qty_to_reserve,
+			validate_stock_reservation_settings,
+		)
+
+		validate_stock_reservation_settings(self)
+
+		allow_partial_reservation = frappe.db.get_single_value(
+			"Stock Settings", "allow_partial_reservation"
+		)
+
+		items = []
+		if items_details:
+			for item in items_details:
+				so_item = frappe.get_doc("Sales Order Item", item["name"])
+				so_item.reserve_stock = 1
+				so_item.warehouse = item["warehouse"]
+				so_item.qty_to_reserve = flt(item["qty_to_reserve"]) * flt(so_item.conversion_factor)
+				items.append(so_item)
+
+		sre_count = 0
+		reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", self.name)
+		for item in items or self.get("items"):
+			# Skip if `Reserved Stock` is not checked for the item.
+			if not item.get("reserve_stock"):
+				continue
+
+			# Skip if Non-Stock Item.
+			if not frappe.get_cached_value("Item", item.item_code, "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",
+				)
+				item.db_set("reserve_stock", 0)
+				continue
+
+			# Skip if Group Warehouse.
+			if frappe.get_cached_value("Warehouse", item.warehouse, "is_group"):
+				frappe.msgprint(
+					_("Row #{0}: Stock cannot be reserved in group warehouse {1}.").format(
+						item.idx, frappe.bold(item.warehouse)
+					),
+					title=_("Stock Reservation"),
+					indicator="yellow",
+				)
+				continue
+
+			unreserved_qty = get_unreserved_qty(item, reserved_qty_details)
+
+			# 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",
+				)
+				continue
+
+			available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse)
+
+			# 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(
+						item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse)
+					),
+					title=_("Stock Reservation"),
+					indicator="orange",
+				)
+				continue
+
+			# The quantity which can be reserved.
+			qty_to_be_reserved = min(unreserved_qty, available_qty_to_reserve)
+
+			if hasattr(item, "qty_to_reserve"):
+				if item.qty_to_reserve <= 0:
+					frappe.msgprint(
+						_("Row #{0}: Quantity to reserve for the Item {1} should be greater than 0.").format(
+							item.idx, frappe.bold(item.item_code)
+						),
+						title=_("Stock Reservation"),
+						indicator="orange",
+					)
+					continue
+				else:
+					qty_to_be_reserved = min(qty_to_be_reserved, item.qty_to_reserve)
+
+			# 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")):
+					frappe.msgprint(
+						_("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),
+							frappe.bold(item.item_code),
+						),
+						title=_("Stock Reservation"),
+						indicator="orange",
+					)
+
+				# Skip the item if `Partial Reservation` is disabled in the Stock Settings.
+				if not allow_partial_reservation:
+					continue
+
+			# Create and Submit Stock Reservation Entry
+			sre = frappe.new_doc("Stock Reservation Entry")
+			sre.item_code = item.item_code
+			sre.warehouse = item.warehouse
+			sre.voucher_type = self.doctype
+			sre.voucher_no = self.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 = self.company
+			sre.stock_uom = item.stock_uom
+			sre.project = self.project
+			sre.save()
+			sre.submit()
+
+			sre_count += 1
+
+		if sre_count and notify:
+			frappe.msgprint(_("Stock Reservation Entries Created"), alert=True, indicator="green")
+
+
+def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
+	"""Returns the unreserved quantity for the Sales Order Item."""
+
+	existing_reserved_qty = reserved_qty_details.get(item.name, 0)
+	return (
+		item.stock_qty
+		- flt(item.delivered_qty) * item.get("conversion_factor", 1)
+		- existing_reserved_qty
+	)
+
 
 def get_list_context(context=None):
 	from erpnext.controllers.website_list_for_contact import get_list_context
@@ -680,7 +857,6 @@
 		}
 
 	target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
-
 	target_doc.set_onload("ignore_price_list", True)
 
 	return target_doc
diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
index cbc40bb..c840097 100644
--- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
+++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
@@ -11,6 +11,7 @@
 			"Payment Request": "reference_name",
 			"Auto Repeat": "reference_document",
 			"Maintenance Visit": "prevdoc_docname",
+			"Stock Reservation Entry": "voucher_no",
 		},
 		"internal_links": {
 			"Quotation": ["items", "prevdoc_docname"],
@@ -23,7 +24,7 @@
 			{"label": _("Purchasing"), "items": ["Material Request", "Purchase Order"]},
 			{"label": _("Projects"), "items": ["Project"]},
 			{"label": _("Manufacturing"), "items": ["Work Order"]},
-			{"label": _("Reference"), "items": ["Quotation", "Auto Repeat"]},
+			{"label": _("Reference"), "items": ["Quotation", "Auto Repeat", "Stock Reservation Entry"]},
 			{"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
 		],
 	}
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 9854f15..88bc4bd 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1878,6 +1878,139 @@
 		self.assertEqual(pe.references[1].reference_name, so.name)
 		self.assertEqual(pe.references[1].allocated_amount, 300)
 
+	@change_settings("Stock Settings", {"enable_stock_reservation": 1})
+	def test_stock_reservation_against_sales_order(self) -> None:
+		from random import randint, uniform
+
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			cancel_stock_reservation_entries,
+			get_sre_reserved_qty_details_for_voucher,
+			get_stock_reservation_entries_for_voucher,
+			has_reserved_stock,
+		)
+		from erpnext.stock.doctype.stock_reservation_entry.test_stock_reservation_entry import (
+			create_items,
+			create_material_receipts,
+		)
+
+		items_details, warehouse = create_items(), "_Test Warehouse - _TC"
+		create_material_receipts(items_details, warehouse, qty=10)
+
+		item_list = []
+		for item_code, properties in items_details.items():
+			stock_uom = properties.stock_uom
+			item_list.append(
+				{
+					"item_code": item_code,
+					"warehouse": warehouse,
+					"qty": flt(uniform(11, 100), 0 if stock_uom == "Nos" else 3),
+					"uom": stock_uom,
+					"rate": randint(10, 200),
+				}
+			)
+
+		so = make_sales_order(
+			item_list=item_list,
+			warehouse="_Test Warehouse - _TC",
+		)
+
+		# Test - 1: Stock should not be reserved if the Available Qty to Reserve is less than the Ordered Qty and Partial Reservation is disabled in Stock Settings.
+		with change_settings("Stock Settings", {"allow_partial_reservation": 0}):
+			so.create_stock_reservation_entries()
+			self.assertFalse(has_reserved_stock("Sales Order", so.name))
+
+		# Test - 2: Stock should be Partially Reserved if the Partial Reservation is enabled in Stock Settings.
+		with change_settings("Stock Settings", {"allow_partial_reservation": 1}):
+			so.create_stock_reservation_entries()
+			so.load_from_db()
+			self.assertTrue(has_reserved_stock("Sales Order", so.name))
+
+			for item in so.items:
+				sre_details = get_stock_reservation_entries_for_voucher(
+					"Sales Order", so.name, item.name, fields=["reserved_qty", "status"]
+				)
+				self.assertEqual(item.stock_reserved_qty, sre_details[0].reserved_qty)
+				self.assertEqual(sre_details[0].status, "Partially Reserved")
+
+			# Test - 3: Stock should be fully Reserved if the Available Qty to Reserve is greater than the Un-reserved Qty.
+			create_material_receipts(items_details, warehouse, qty=100)
+			so.create_stock_reservation_entries()
+			so.load_from_db()
+
+			reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", so.name)
+			for item in so.items:
+				reserved_qty = reserved_qty_details[item.name]
+				self.assertEqual(item.stock_reserved_qty, reserved_qty)
+				self.assertEqual(item.stock_qty, item.stock_reserved_qty)
+
+			# Test - 4: Stock should get unreserved on cancellation of Stock Reservation Entries.
+			cancel_stock_reservation_entries("Sales Order", so.name)
+			so.load_from_db()
+			self.assertFalse(has_reserved_stock("Sales Order", so.name))
+
+			for item in so.items:
+				self.assertEqual(item.stock_reserved_qty, 0)
+
+			# Test - 5: Re-reserve the stock.
+			so.create_stock_reservation_entries()
+			self.assertTrue(has_reserved_stock("Sales Order", so.name))
+
+			# Test - 6: Stock should get unreserved on cancellation of Sales Order.
+			so.cancel()
+			so.load_from_db()
+			self.assertFalse(has_reserved_stock("Sales Order", so.name))
+
+			for item in so.items:
+				self.assertEqual(item.stock_reserved_qty, 0)
+
+			# Create Sales Order and Reserve Stock.
+			so = make_sales_order(
+				item_list=item_list,
+				warehouse="_Test Warehouse - _TC",
+			)
+			so.create_stock_reservation_entries()
+
+			# Test - 7: Partial Delivery against Sales Order.
+			dn1 = make_delivery_note(so.name)
+
+			for item in dn1.items:
+				item.qty = flt(uniform(1, 10), 0 if item.stock_uom == "Nos" else 3)
+
+			dn1.save()
+			dn1.submit()
+
+			for item in so.items:
+				sre_details = get_stock_reservation_entries_for_voucher(
+					"Sales Order", so.name, item.name, fields=["delivered_qty", "status"]
+				)
+				self.assertGreater(sre_details[0].delivered_qty, 0)
+				self.assertEqual(sre_details[0].status, "Partially Delivered")
+
+			# Test - 8: Over Delivery against Sales Order, SRE Delivered Qty should not be greater than the SRE Reserved Qty.
+			with change_settings("Stock Settings", {"over_delivery_receipt_allowance": 100}):
+				dn2 = make_delivery_note(so.name)
+
+				for item in dn2.items:
+					item.qty += flt(uniform(1, 10), 0 if item.stock_uom == "Nos" else 3)
+
+				dn2.save()
+				dn2.submit()
+
+			for item in so.items:
+				sre_details = frappe.db.get_all(
+					"Stock Reservation Entry",
+					filters={
+						"voucher_type": "Sales Order",
+						"voucher_no": so.name,
+						"voucher_detail_no": item.name,
+					},
+					fields=["status", "reserved_qty", "delivered_qty"],
+				)
+
+				for sre_detail in sre_details:
+					self.assertEqual(sre_detail.reserved_qty, sre_detail.delivered_qty)
+					self.assertEqual(sre_detail.status, "Delivered")
+
 	def test_delivered_item_material_request(self):
 		"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
 		from erpnext.manufacturing.doctype.work_order.work_order import (
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index d0dabad..5c7e10a 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -10,6 +10,7 @@
   "item_code",
   "customer_item_code",
   "ensure_delivery_based_on_produced_serial_no",
+  "reserve_stock",
   "col_break1",
   "delivery_date",
   "item_name",
@@ -27,6 +28,7 @@
   "uom",
   "conversion_factor",
   "stock_qty",
+  "stock_reserved_qty",
   "section_break_16",
   "price_list_rate",
   "base_price_list_rate",
@@ -859,12 +861,33 @@
    "fieldname": "material_request_item",
    "fieldtype": "Data",
    "label": "Material Request Item"
+  },
+  {
+   "allow_on_submit": 1,
+   "default": "1",
+   "fieldname": "reserve_stock",
+   "fieldtype": "Check",
+   "label": "Reserve Stock",
+   "print_hide": 1,
+   "report_hide": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: doc.stock_reserved_qty",
+   "fieldname": "stock_reserved_qty",
+   "fieldtype": "Float",
+   "label": "Stock Reserved Qty (in Stock UOM)",
+   "no_copy": 1,
+   "non_negative": 1,
+   "print_hide": 1,
+   "read_only": 1,
+   "report_hide": 1
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-12-25 02:51:10.247569",
+ "modified": "2023-04-04 10:44:05.707488",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order Item",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index ae56645..77545e0 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -185,11 +185,14 @@
 			}
 
 			if(doc.docstatus==0 && !doc.__islocal) {
-				this.frm.add_custom_button(__('Packing Slip'), function() {
-					frappe.model.open_mapped_doc({
-						method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip",
-						frm: me.frm
-					}) }, __('Create'));
+				if (doc.__onload && doc.__onload.has_unpacked_items) {
+					this.frm.add_custom_button(__('Packing Slip'), function() {
+						frappe.model.open_mapped_doc({
+							method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip",
+							frm: me.frm
+						}) }, __('Create')
+					);
+				}
 			}
 
 			if (!doc.__islocal && doc.docstatus==1) {
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index c18e851..2ee372e 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -86,6 +86,10 @@
 				]
 			)
 
+	def onload(self):
+		if self.docstatus == 0:
+			self.set_onload("has_unpacked_items", self.has_unpacked_items())
+
 	def before_print(self, settings=None):
 		def toggle_print_hide(meta, fieldname):
 			df = meta.get_field(fieldname)
@@ -147,6 +151,8 @@
 
 		if not self.installation_status:
 			self.installation_status = "Not Installed"
+
+		self.validate_against_stock_reservation_entries()
 		self.reset_default_field_value("set_warehouse", "items", "warehouse")
 
 	def validate_with_previous_doc(self):
@@ -239,6 +245,8 @@
 		self.update_prevdoc_status()
 		self.update_billing_status()
 
+		self.update_stock_reservation_entries()
+
 		if not self.is_return:
 			self.check_credit_limit()
 		elif self.issue_credit_note:
@@ -268,6 +276,90 @@
 		self.repost_future_sle_and_gle()
 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
 
+	def update_stock_reservation_entries(self) -> None:
+		"""Updates Delivered Qty in Stock Reservation Entries."""
+
+		# Don't update Delivered Qty on Return or Cancellation.
+		if self.is_return or self._action == "cancel":
+			return
+
+		for item in self.get("items"):
+			# Skip if `Sales Order` or `Sales Order Item` reference is not set.
+			if not item.against_sales_order or not item.so_detail:
+				continue
+
+			sre_list = frappe.db.get_all(
+				"Stock Reservation Entry",
+				{
+					"docstatus": 1,
+					"voucher_type": "Sales Order",
+					"voucher_no": item.against_sales_order,
+					"voucher_detail_no": item.so_detail,
+					"warehouse": item.warehouse,
+					"status": ["not in", ["Delivered", "Cancelled"]],
+				},
+				order_by="creation",
+			)
+
+			# Skip if no Stock Reservation Entries.
+			if not sre_list:
+				continue
+
+			available_qty_to_deliver = item.stock_qty
+			for sre in sre_list:
+				if available_qty_to_deliver <= 0:
+					break
+
+				sre_doc = frappe.get_doc("Stock Reservation Entry", sre)
+
+				# `Delivered Qty` should be less than or equal to `Reserved Qty`.
+				qty_to_be_deliver = min(sre_doc.reserved_qty - sre_doc.delivered_qty, available_qty_to_deliver)
+
+				sre_doc.delivered_qty += qty_to_be_deliver
+				sre_doc.db_update()
+
+				# Update Stock Reservation Entry `Status` based on `Delivered Qty`.
+				sre_doc.update_status()
+
+				available_qty_to_deliver -= qty_to_be_deliver
+
+	def validate_against_stock_reservation_entries(self):
+		"""Validates if Stock Reservation Entries are available for the Sales Order Item reference."""
+
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			get_sre_reserved_qty_details_for_voucher_detail_no,
+		)
+
+		# Don't validate if Return
+		if self.is_return:
+			return
+
+		for item in self.get("items"):
+			# Skip if `Sales Order` or `Sales Order Item` reference is not set.
+			if not item.against_sales_order or not item.so_detail:
+				continue
+
+			sre_data = get_sre_reserved_qty_details_for_voucher_detail_no(
+				"Sales Order", item.against_sales_order, item.so_detail
+			)
+
+			# Skip if stock is not reserved.
+			if not sre_data:
+				continue
+
+			# Set `Warehouse` from SRE if not set.
+			if not item.warehouse:
+				item.warehouse = sre_data[0]
+			else:
+				# Throw if `Warehouse` is different from SRE.
+				if item.warehouse != sre_data[0]:
+					frappe.throw(
+						_("Row #{0}: Stock is reserved for Item {1} in Warehouse {2}.").format(
+							item.idx, frappe.bold(item.item_code), frappe.bold(sre_data[0])
+						),
+						title=_("Stock Reservation Warehouse Mismatch"),
+					)
+
 	def check_credit_limit(self):
 		from erpnext.selling.doctype.customer.customer import check_credit_limit
 
@@ -302,20 +394,21 @@
 			)
 
 	def validate_packed_qty(self):
-		"""
-		Validate that if packed qty exists, it should be equal to qty
-		"""
-		if not any(flt(d.get("packed_qty")) for d in self.get("items")):
-			return
-		has_error = False
-		for d in self.get("items"):
-			if flt(d.get("qty")) != flt(d.get("packed_qty")):
-				frappe.msgprint(
-					_("Packed quantity must equal quantity for Item {0} in row {1}").format(d.item_code, d.idx)
-				)
-				has_error = True
-		if has_error:
-			raise frappe.ValidationError
+		"""Validate that if packed qty exists, it should be equal to qty"""
+
+		if frappe.db.exists("Packing Slip", {"docstatus": 1, "delivery_note": self.name}):
+			product_bundle_list = self.get_product_bundle_list()
+			for item in self.items + self.packed_items:
+				if (
+					item.item_code not in product_bundle_list
+					and flt(item.packed_qty)
+					and flt(item.packed_qty) != flt(item.qty)
+				):
+					frappe.throw(
+						_("Row {0}: Packed Qty must be equal to {1} Qty.").format(
+							item.idx, frappe.bold(item.doctype)
+						)
+					)
 
 	def update_pick_list_status(self):
 		from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
@@ -393,6 +486,23 @@
 				)
 			)
 
+	def has_unpacked_items(self):
+		product_bundle_list = self.get_product_bundle_list()
+
+		for item in self.items + self.packed_items:
+			if item.item_code not in product_bundle_list and flt(item.packed_qty) < flt(item.qty):
+				return True
+
+		return False
+
+	def get_product_bundle_list(self):
+		items_list = [item.item_code for item in self.items]
+		return frappe.db.get_all(
+			"Product Bundle",
+			filters={"new_item_code": ["in", items_list]},
+			pluck="name",
+		)
+
 
 def update_billed_amount_based_on_so(so_detail, update_modified=True):
 	from frappe.query_builder.functions import Sum
@@ -684,6 +794,12 @@
 
 @frappe.whitelist()
 def make_packing_slip(source_name, target_doc=None):
+	def set_missing_values(source, target):
+		target.run_method("set_missing_values")
+
+	def update_item(obj, target, source_parent):
+		target.qty = flt(obj.qty) - flt(obj.packed_qty)
+
 	doclist = get_mapped_doc(
 		"Delivery Note",
 		source_name,
@@ -698,12 +814,34 @@
 				"field_map": {
 					"item_code": "item_code",
 					"item_name": "item_name",
+					"batch_no": "batch_no",
 					"description": "description",
 					"qty": "qty",
+					"stock_uom": "stock_uom",
+					"name": "dn_detail",
 				},
+				"postprocess": update_item,
+				"condition": lambda item: (
+					not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code})
+					and flt(item.packed_qty) < flt(item.qty)
+				),
+			},
+			"Packed Item": {
+				"doctype": "Packing Slip Item",
+				"field_map": {
+					"item_code": "item_code",
+					"item_name": "item_name",
+					"batch_no": "batch_no",
+					"description": "description",
+					"qty": "qty",
+					"name": "pi_detail",
+				},
+				"postprocess": update_item,
+				"condition": lambda item: (flt(item.packed_qty) < flt(item.qty)),
 			},
 		},
 		target_doc,
+		set_missing_values,
 	)
 
 	return doclist
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index e46cab0..3853bd1 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -84,6 +84,7 @@
   "installed_qty",
   "item_tax_rate",
   "column_break_atna",
+  "packed_qty",
   "received_qty",
   "accounting_details_section",
   "expense_account",
@@ -850,6 +851,16 @@
    "print_hide": 1,
    "read_only": 1,
    "report_hide": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: doc.packed_qty",
+   "fieldname": "packed_qty",
+   "fieldtype": "Float",
+   "label": "Packed Qty",
+   "no_copy": 1,
+   "non_negative": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index cb8eb30..c5fb241 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -27,6 +27,7 @@
   "actual_qty",
   "projected_qty",
   "ordered_qty",
+  "packed_qty",
   "column_break_16",
   "incoming_rate",
   "picked_qty",
@@ -242,13 +243,23 @@
    "label": "Picked Qty",
    "no_copy": 1,
    "read_only": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: doc.packed_qty",
+   "fieldname": "packed_qty",
+   "fieldtype": "Float",
+   "label": "Packed Qty",
+   "no_copy": 1,
+   "non_negative": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-04-27 05:23:08.683245",
+ "modified": "2023-04-28 13:16:38.460806",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Packed Item",
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js
index 40d4685..95e5ea3 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.js
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.js
@@ -1,113 +1,46 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// License: GNU General Public License v3. See license.txt
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
 
-cur_frm.fields_dict['delivery_note'].get_query = function(doc, cdt, cdn) {
-	return{
-		filters:{ 'docstatus': 0}
-	}
-}
+frappe.ui.form.on('Packing Slip', {
+    setup: (frm) => {
+        frm.set_query('delivery_note', () => {
+            return {
+                filters: {
+                    docstatus: 0,
+                }
+            }
+        });
 
+        frm.set_query('item_code', 'items', (doc, cdt, cdn) => {
+            if (!doc.delivery_note) {
+                frappe.throw(__('Please select a Delivery Note'));
+            } else {
+                let d = locals[cdt][cdn];
+                return {
+                    query: 'erpnext.stock.doctype.packing_slip.packing_slip.item_details',
+                    filters: {
+                        delivery_note: doc.delivery_note,
+                    }
+                }
+            }
+        });
+	},
 
-cur_frm.fields_dict['items'].grid.get_field('item_code').get_query = function(doc, cdt, cdn) {
-	if(!doc.delivery_note) {
-		frappe.throw(__("Please select a Delivery Note"));
-	} else {
-		return {
-			query: "erpnext.stock.doctype.packing_slip.packing_slip.item_details",
-			filters:{ 'delivery_note': doc.delivery_note}
+	refresh: (frm) => {
+		frm.toggle_display('misc_details', frm.doc.amended_from);
+	},
+
+	delivery_note: (frm) => {
+		frm.set_value('items', null);
+
+		if (frm.doc.delivery_note) {
+			erpnext.utils.map_current_doc({
+				method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip',
+				source_name: frm.doc.delivery_note,
+				target_doc: frm,
+				freeze: true,
+				freeze_message: __('Creating Packing Slip ...'),
+			});
 		}
-	}
-}
-
-cur_frm.cscript.onload_post_render = function(doc, cdt, cdn) {
-	if(doc.delivery_note && doc.__islocal) {
-		cur_frm.cscript.get_items(doc, cdt, cdn);
-	}
-}
-
-cur_frm.cscript.get_items = function(doc, cdt, cdn) {
-	return this.frm.call({
-		doc: this.frm.doc,
-		method: "get_items",
-		callback: function(r) {
-			if(!r.exc) cur_frm.refresh();
-		}
-	});
-}
-
-cur_frm.cscript.refresh = function(doc, dt, dn) {
-	cur_frm.toggle_display("misc_details", doc.amended_from);
-}
-
-cur_frm.cscript.validate = function(doc, cdt, cdn) {
-	cur_frm.cscript.validate_case_nos(doc);
-	cur_frm.cscript.validate_calculate_item_details(doc);
-}
-
-// To Case No. cannot be less than From Case No.
-cur_frm.cscript.validate_case_nos = function(doc) {
-	doc = locals[doc.doctype][doc.name];
-	if(cint(doc.from_case_no)==0) {
-		frappe.msgprint(__("The 'From Package No.' field must neither be empty nor it's value less than 1."));
-		frappe.validated = false;
-	} else if(!cint(doc.to_case_no)) {
-		doc.to_case_no = doc.from_case_no;
-		refresh_field('to_case_no');
-	} else if(cint(doc.to_case_no) < cint(doc.from_case_no)) {
-		frappe.msgprint(__("'To Case No.' cannot be less than 'From Case No.'"));
-		frappe.validated = false;
-	}
-}
-
-
-cur_frm.cscript.validate_calculate_item_details = function(doc) {
-	doc = locals[doc.doctype][doc.name];
-	var ps_detail = doc.items || [];
-
-	cur_frm.cscript.validate_duplicate_items(doc, ps_detail);
-	cur_frm.cscript.calc_net_total_pkg(doc, ps_detail);
-}
-
-
-// Do not allow duplicate items i.e. items with same item_code
-// Also check for 0 qty
-cur_frm.cscript.validate_duplicate_items = function(doc, ps_detail) {
-	for(var i=0; i<ps_detail.length; i++) {
-		for(var j=0; j<ps_detail.length; j++) {
-			if(i!=j && ps_detail[i].item_code && ps_detail[i].item_code==ps_detail[j].item_code) {
-				frappe.msgprint(__("You have entered duplicate items. Please rectify and try again."));
-				frappe.validated = false;
-				return;
-			}
-		}
-		if(flt(ps_detail[i].qty)<=0) {
-			frappe.msgprint(__("Invalid quantity specified for item {0}. Quantity should be greater than 0.", [ps_detail[i].item_code]));
-			frappe.validated = false;
-		}
-	}
-}
-
-
-// Calculate Net Weight of Package
-cur_frm.cscript.calc_net_total_pkg = function(doc, ps_detail) {
-	var net_weight_pkg = 0;
-	doc.net_weight_uom = (ps_detail && ps_detail.length) ? ps_detail[0].weight_uom : '';
-	doc.gross_weight_uom = doc.net_weight_uom;
-
-	for(var i=0; i<ps_detail.length; i++) {
-		var item = ps_detail[i];
-		if(item.weight_uom != doc.net_weight_uom) {
-			frappe.msgprint(__("Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM."));
-			frappe.validated = false;
-		}
-		net_weight_pkg += flt(item.net_weight) * flt(item.qty);
-	}
-
-	doc.net_weight_pkg = roundNumber(net_weight_pkg, 2);
-	if(!flt(doc.gross_weight_pkg)) {
-		doc.gross_weight_pkg = doc.net_weight_pkg;
-	}
-	refresh_many(['net_weight_pkg', 'net_weight_uom', 'gross_weight_uom', 'gross_weight_pkg']);
-}
-
-// TODO: validate gross weight field
+	},
+});
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.json b/erpnext/stock/doctype/packing_slip/packing_slip.json
index ec8d57c..86ed794 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.json
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.json
@@ -1,264 +1,262 @@
 {
-   "allow_import": 1,
-   "autoname": "MAT-PAC-.YYYY.-.#####",
-   "creation": "2013-04-11 15:32:24",
-   "description": "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.",
-   "doctype": "DocType",
-   "document_type": "Document",
-   "engine": "InnoDB",
-   "field_order": [
-    "packing_slip_details",
-    "column_break0",
-    "delivery_note",
-    "column_break1",
-    "naming_series",
-    "section_break0",
-    "column_break2",
-    "from_case_no",
-    "column_break3",
-    "to_case_no",
-    "package_item_details",
-    "get_items",
-    "items",
-    "package_weight_details",
-    "net_weight_pkg",
-    "net_weight_uom",
-    "column_break4",
-    "gross_weight_pkg",
-    "gross_weight_uom",
-    "letter_head_details",
-    "letter_head",
-    "misc_details",
-    "amended_from"
-   ],
-   "fields": [
-    {
-     "fieldname": "packing_slip_details",
-     "fieldtype": "Section Break"
-    },
-    {
-     "fieldname": "column_break0",
-     "fieldtype": "Column Break"
-    },
-    {
-     "description": "Indicates that the package is a part of this delivery (Only Draft)",
-     "fieldname": "delivery_note",
-     "fieldtype": "Link",
-     "in_global_search": 1,
-     "in_list_view": 1,
-     "label": "Delivery Note",
-     "options": "Delivery Note",
-     "reqd": 1
-    },
-    {
-     "fieldname": "column_break1",
-     "fieldtype": "Column Break"
-    },
-    {
-     "fieldname": "naming_series",
-     "fieldtype": "Select",
-     "label": "Series",
-     "options": "MAT-PAC-.YYYY.-",
-     "print_hide": 1,
-     "reqd": 1,
-     "set_only_once": 1
-    },
-    {
-     "fieldname": "section_break0",
-     "fieldtype": "Section Break"
-    },
-    {
-     "fieldname": "column_break2",
-     "fieldtype": "Column Break"
-    },
-    {
-     "description": "Identification of the package for the delivery (for print)",
-     "fieldname": "from_case_no",
-     "fieldtype": "Int",
-     "in_list_view": 1,
-     "label": "From Package No.",
-     "no_copy": 1,
-     "reqd": 1,
-     "width": "50px"
-    },
-    {
-     "fieldname": "column_break3",
-     "fieldtype": "Column Break"
-    },
-    {
-     "description": "If more than one package of the same type (for print)",
-     "fieldname": "to_case_no",
-     "fieldtype": "Int",
-     "in_list_view": 1,
-     "label": "To Package No.",
-     "no_copy": 1,
-     "width": "50px"
-    },
-    {
-     "fieldname": "package_item_details",
-     "fieldtype": "Section Break"
-    },
-    {
-     "fieldname": "get_items",
-     "fieldtype": "Button",
-     "label": "Get Items"
-    },
-    {
-     "fieldname": "items",
-     "fieldtype": "Table",
-     "label": "Items",
-     "options": "Packing Slip Item",
-     "reqd": 1
-    },
-    {
-     "fieldname": "package_weight_details",
-     "fieldtype": "Section Break",
-     "label": "Package Weight Details"
-    },
-    {
-     "description": "The net weight of this package. (calculated automatically as sum of net weight of items)",
-     "fieldname": "net_weight_pkg",
-     "fieldtype": "Float",
-     "label": "Net Weight",
-     "no_copy": 1,
-     "read_only": 1
-    },
-    {
-     "fieldname": "net_weight_uom",
-     "fieldtype": "Link",
-     "label": "Net Weight UOM",
-     "no_copy": 1,
-     "options": "UOM",
-     "read_only": 1
-    },
-    {
-     "fieldname": "column_break4",
-     "fieldtype": "Column Break"
-    },
-    {
-     "description": "The gross weight of the package. Usually net weight + packaging material weight. (for print)",
-     "fieldname": "gross_weight_pkg",
-     "fieldtype": "Float",
-     "label": "Gross Weight",
-     "no_copy": 1
-    },
-    {
-     "fieldname": "gross_weight_uom",
-     "fieldtype": "Link",
-     "label": "Gross Weight UOM",
-     "no_copy": 1,
-     "options": "UOM"
-    },
-    {
-     "fieldname": "letter_head_details",
-     "fieldtype": "Section Break",
-     "label": "Letter Head"
-    },
-    {
-     "allow_on_submit": 1,
-     "fieldname": "letter_head",
-     "fieldtype": "Link",
-     "label": "Letter Head",
-     "options": "Letter Head",
-     "print_hide": 1
-    },
-    {
-     "fieldname": "misc_details",
-     "fieldtype": "Section Break"
-    },
-    {
-     "fieldname": "amended_from",
-     "fieldtype": "Link",
-     "ignore_user_permissions": 1,
-     "label": "Amended From",
-     "no_copy": 1,
-     "options": "Packing Slip",
-     "print_hide": 1,
-     "read_only": 1
-    }
-   ],
-   "icon": "fa fa-suitcase",
-   "idx": 1,
-   "is_submittable": 1,
-   "modified": "2019-09-09 04:45:08.082862",
-   "modified_by": "Administrator",
-   "module": "Stock",
-   "name": "Packing Slip",
-   "owner": "Administrator",
-   "permissions": [
-    {
-     "amend": 1,
-     "cancel": 1,
-     "create": 1,
-     "delete": 1,
-     "email": 1,
-     "print": 1,
-     "read": 1,
-     "report": 1,
-     "role": "Stock User",
-     "share": 1,
-     "submit": 1,
-     "write": 1
-    },
-    {
-     "amend": 1,
-     "cancel": 1,
-     "create": 1,
-     "delete": 1,
-     "email": 1,
-     "print": 1,
-     "read": 1,
-     "report": 1,
-     "role": "Sales User",
-     "share": 1,
-     "submit": 1,
-     "write": 1
-    },
-    {
-     "amend": 1,
-     "cancel": 1,
-     "create": 1,
-     "delete": 1,
-     "email": 1,
-     "print": 1,
-     "read": 1,
-     "report": 1,
-     "role": "Item Manager",
-     "share": 1,
-     "submit": 1,
-     "write": 1
-    },
-    {
-     "amend": 1,
-     "cancel": 1,
-     "create": 1,
-     "delete": 1,
-     "email": 1,
-     "print": 1,
-     "read": 1,
-     "report": 1,
-     "role": "Stock Manager",
-     "share": 1,
-     "submit": 1,
-     "write": 1
-    },
-    {
-     "amend": 1,
-     "cancel": 1,
-     "create": 1,
-     "delete": 1,
-     "email": 1,
-     "print": 1,
-     "read": 1,
-     "report": 1,
-     "role": "Sales Manager",
-     "share": 1,
-     "submit": 1,
-     "write": 1
-    }
-   ],
-   "search_fields": "delivery_note",
-   "show_name_in_global_search": 1,
-   "sort_field": "modified",
-   "sort_order": "DESC"
+ "actions": [],
+ "allow_import": 1,
+ "autoname": "MAT-PAC-.YYYY.-.#####",
+ "creation": "2013-04-11 15:32:24",
+ "description": "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+  "packing_slip_details",
+  "column_break0",
+  "delivery_note",
+  "column_break1",
+  "naming_series",
+  "section_break0",
+  "column_break2",
+  "from_case_no",
+  "column_break3",
+  "to_case_no",
+  "package_item_details",
+  "items",
+  "package_weight_details",
+  "net_weight_pkg",
+  "net_weight_uom",
+  "column_break4",
+  "gross_weight_pkg",
+  "gross_weight_uom",
+  "letter_head_details",
+  "letter_head",
+  "misc_details",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "packing_slip_details",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "column_break0",
+   "fieldtype": "Column Break"
+  },
+  {
+   "description": "Indicates that the package is a part of this delivery (Only Draft)",
+   "fieldname": "delivery_note",
+   "fieldtype": "Link",
+   "in_global_search": 1,
+   "in_list_view": 1,
+   "label": "Delivery Note",
+   "options": "Delivery Note",
+   "reqd": 1
+  },
+  {
+   "fieldname": "column_break1",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "naming_series",
+   "fieldtype": "Select",
+   "label": "Series",
+   "options": "MAT-PAC-.YYYY.-",
+   "print_hide": 1,
+   "reqd": 1,
+   "set_only_once": 1
+  },
+  {
+   "fieldname": "section_break0",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "column_break2",
+   "fieldtype": "Column Break"
+  },
+  {
+   "description": "Identification of the package for the delivery (for print)",
+   "fieldname": "from_case_no",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "From Package No.",
+   "no_copy": 1,
+   "reqd": 1,
+   "width": "50px"
+  },
+  {
+   "fieldname": "column_break3",
+   "fieldtype": "Column Break"
+  },
+  {
+   "description": "If more than one package of the same type (for print)",
+   "fieldname": "to_case_no",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "To Package No.",
+   "no_copy": 1,
+   "width": "50px"
+  },
+  {
+   "fieldname": "package_item_details",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "items",
+   "fieldtype": "Table",
+   "label": "Items",
+   "options": "Packing Slip Item",
+   "reqd": 1
+  },
+  {
+   "fieldname": "package_weight_details",
+   "fieldtype": "Section Break",
+   "label": "Package Weight Details"
+  },
+  {
+   "description": "The net weight of this package. (calculated automatically as sum of net weight of items)",
+   "fieldname": "net_weight_pkg",
+   "fieldtype": "Float",
+   "label": "Net Weight",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "net_weight_uom",
+   "fieldtype": "Link",
+   "label": "Net Weight UOM",
+   "no_copy": 1,
+   "options": "UOM",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break4",
+   "fieldtype": "Column Break"
+  },
+  {
+   "description": "The gross weight of the package. Usually net weight + packaging material weight. (for print)",
+   "fieldname": "gross_weight_pkg",
+   "fieldtype": "Float",
+   "label": "Gross Weight",
+   "no_copy": 1
+  },
+  {
+   "fieldname": "gross_weight_uom",
+   "fieldtype": "Link",
+   "label": "Gross Weight UOM",
+   "no_copy": 1,
+   "options": "UOM"
+  },
+  {
+   "fieldname": "letter_head_details",
+   "fieldtype": "Section Break",
+   "label": "Letter Head"
+  },
+  {
+   "allow_on_submit": 1,
+   "fieldname": "letter_head",
+   "fieldtype": "Link",
+   "label": "Letter Head",
+   "options": "Letter Head",
+   "print_hide": 1
+  },
+  {
+   "fieldname": "misc_details",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "ignore_user_permissions": 1,
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Packing Slip",
+   "print_hide": 1,
+   "read_only": 1
   }
+ ],
+ "icon": "fa fa-suitcase",
+ "idx": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-04-28 18:01:37.341619",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Packing Slip",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Stock User",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Sales User",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Item Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Stock Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Sales Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  }
+ ],
+ "search_fields": "delivery_note",
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py
index e5b9de8..6ea5938 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.py
@@ -4,193 +4,181 @@
 
 import frappe
 from frappe import _
-from frappe.model import no_value_fields
-from frappe.model.document import Document
 from frappe.utils import cint, flt
 
+from erpnext.controllers.status_updater import StatusUpdater
 
-class PackingSlip(Document):
-	def validate(self):
-		"""
-		* Validate existence of submitted Delivery Note
-		* Case nos do not overlap
-		* Check if packed qty doesn't exceed actual qty of delivery note
 
-		It is necessary to validate case nos before checking quantity
-		"""
-		self.validate_delivery_note()
-		self.validate_items_mandatory()
-		self.validate_case_nos()
-		self.validate_qty()
+class PackingSlip(StatusUpdater):
+	def __init__(self, *args, **kwargs) -> None:
+		super(PackingSlip, self).__init__(*args, **kwargs)
+		self.status_updater = [
+			{
+				"target_dt": "Delivery Note Item",
+				"join_field": "dn_detail",
+				"target_field": "packed_qty",
+				"target_parent_dt": "Delivery Note",
+				"target_ref_field": "qty",
+				"source_dt": "Packing Slip Item",
+				"source_field": "qty",
+			},
+			{
+				"target_dt": "Packed Item",
+				"join_field": "pi_detail",
+				"target_field": "packed_qty",
+				"target_parent_dt": "Delivery Note",
+				"target_ref_field": "qty",
+				"source_dt": "Packing Slip Item",
+				"source_field": "qty",
+			},
+		]
 
+	def validate(self) -> None:
 		from erpnext.utilities.transaction_base import validate_uom_is_integer
 
+		self.validate_delivery_note()
+		self.validate_case_nos()
+		self.validate_items()
+
 		validate_uom_is_integer(self, "stock_uom", "qty")
 		validate_uom_is_integer(self, "weight_uom", "net_weight")
 
-	def validate_delivery_note(self):
-		"""
-		Validates if delivery note has status as draft
-		"""
-		if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0:
-			frappe.throw(_("Delivery Note {0} must not be submitted").format(self.delivery_note))
+		self.set_missing_values()
+		self.calculate_net_total_pkg()
 
-	def validate_items_mandatory(self):
-		rows = [d.item_code for d in self.get("items")]
-		if not rows:
-			frappe.msgprint(_("No Items to pack"), raise_exception=1)
+	def on_submit(self):
+		self.update_prevdoc_status()
+
+	def on_cancel(self):
+		self.update_prevdoc_status()
+
+	def validate_delivery_note(self):
+		"""Raises an exception if the `Delivery Note` status is not Draft"""
+
+		if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0:
+			frappe.throw(
+				_("A Packing Slip can only be created for Draft Delivery Note.").format(self.delivery_note)
+			)
 
 	def validate_case_nos(self):
-		"""
-		Validate if case nos overlap. If they do, recommend next case no.
-		"""
-		if not cint(self.from_case_no):
-			frappe.msgprint(_("Please specify a valid 'From Case No.'"), raise_exception=1)
+		"""Validate if case nos overlap. If they do, recommend next case no."""
+
+		if cint(self.from_case_no) <= 0:
+			frappe.throw(
+				_("The 'From Package No.' field must neither be empty nor it's value less than 1.")
+			)
 		elif not self.to_case_no:
 			self.to_case_no = self.from_case_no
-		elif cint(self.from_case_no) > cint(self.to_case_no):
-			frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1)
+		elif cint(self.to_case_no) < cint(self.from_case_no):
+			frappe.throw(_("'To Package No.' cannot be less than 'From Package No.'"))
+		else:
+			ps = frappe.qb.DocType("Packing Slip")
+			res = (
+				frappe.qb.from_(ps)
+				.select(
+					ps.name,
+				)
+				.where(
+					(ps.delivery_note == self.delivery_note)
+					& (ps.docstatus == 1)
+					& (
+						(ps.from_case_no.between(self.from_case_no, self.to_case_no))
+						| (ps.to_case_no.between(self.from_case_no, self.to_case_no))
+						| ((ps.from_case_no <= self.from_case_no) & (ps.to_case_no >= self.from_case_no))
+					)
+				)
+			).run()
 
-		res = frappe.db.sql(
-			"""SELECT name FROM `tabPacking Slip`
-			WHERE delivery_note = %(delivery_note)s AND docstatus = 1 AND
-			((from_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s)
-			OR (to_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s)
-			OR (%(from_case_no)s BETWEEN from_case_no AND to_case_no))
-			""",
-			{
-				"delivery_note": self.delivery_note,
-				"from_case_no": self.from_case_no,
-				"to_case_no": self.to_case_no,
-			},
-		)
+			if res:
+				frappe.throw(
+					_("""Package No(s) already in use. Try from Package No {0}""").format(
+						self.get_recommended_case_no()
+					)
+				)
 
-		if res:
-			frappe.throw(
-				_("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no())
+	def validate_items(self):
+		for item in self.items:
+			if item.qty <= 0:
+				frappe.throw(_("Row {0}: Qty must be greater than 0.").format(item.idx))
+
+			if not item.dn_detail and not item.pi_detail:
+				frappe.throw(
+					_("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory.").format(
+						item.idx
+					)
+				)
+
+			remaining_qty = frappe.db.get_value(
+				"Delivery Note Item" if item.dn_detail else "Packed Item",
+				{"name": item.dn_detail or item.pi_detail, "docstatus": 0},
+				["sum(qty - packed_qty)"],
 			)
 
-	def validate_qty(self):
-		"""Check packed qty across packing slips and delivery note"""
-		# Get Delivery Note Items, Item Quantity Dict and No. of Cases for this Packing slip
-		dn_details, ps_item_qty, no_of_cases = self.get_details_for_packing()
+			if remaining_qty is None:
+				frappe.throw(
+					_("Row {0}: Please provide a valid Delivery Note Item or Packed Item reference.").format(
+						item.idx
+					)
+				)
+			elif remaining_qty <= 0:
+				frappe.throw(
+					_("Row {0}: Packing Slip is already created for Item {1}.").format(
+						item.idx, frappe.bold(item.item_code)
+					)
+				)
+			elif item.qty > remaining_qty:
+				frappe.throw(
+					_("Row {0}: Qty cannot be greater than {1} for the Item {2}.").format(
+						item.idx, frappe.bold(remaining_qty), frappe.bold(item.item_code)
+					)
+				)
 
-		for item in dn_details:
-			new_packed_qty = (flt(ps_item_qty[item["item_code"]]) * no_of_cases) + flt(item["packed_qty"])
-			if new_packed_qty > flt(item["qty"]) and no_of_cases:
-				self.recommend_new_qty(item, ps_item_qty, no_of_cases)
-
-	def get_details_for_packing(self):
-		"""
-		Returns
-		* 'Delivery Note Items' query result as a list of dict
-		* Item Quantity dict of current packing slip doc
-		* No. of Cases of this packing slip
-		"""
-
-		rows = [d.item_code for d in self.get("items")]
-
-		# also pick custom fields from delivery note
-		custom_fields = ", ".join(
-			"dni.`{0}`".format(d.fieldname)
-			for d in frappe.get_meta("Delivery Note Item").get_custom_fields()
-			if d.fieldtype not in no_value_fields
-		)
-
-		if custom_fields:
-			custom_fields = ", " + custom_fields
-
-		condition = ""
-		if rows:
-			condition = " and item_code in (%s)" % (", ".join(["%s"] * len(rows)))
-
-		# gets item code, qty per item code, latest packed qty per item code and stock uom
-		res = frappe.db.sql(
-			"""select item_code, sum(qty) as qty,
-			(select sum(psi.qty * (abs(ps.to_case_no - ps.from_case_no) + 1))
-				from `tabPacking Slip` ps, `tabPacking Slip Item` psi
-				where ps.name = psi.parent and ps.docstatus = 1
-				and ps.delivery_note = dni.parent and psi.item_code=dni.item_code) as packed_qty,
-			stock_uom, item_name, description, dni.batch_no {custom_fields}
-			from `tabDelivery Note Item` dni
-			where parent=%s {condition}
-			group by item_code""".format(
-				condition=condition, custom_fields=custom_fields
-			),
-			tuple([self.delivery_note] + rows),
-			as_dict=1,
-		)
-
-		ps_item_qty = dict([[d.item_code, d.qty] for d in self.get("items")])
-		no_of_cases = cint(self.to_case_no) - cint(self.from_case_no) + 1
-
-		return res, ps_item_qty, no_of_cases
-
-	def recommend_new_qty(self, item, ps_item_qty, no_of_cases):
-		"""
-		Recommend a new quantity and raise a validation exception
-		"""
-		item["recommended_qty"] = (flt(item["qty"]) - flt(item["packed_qty"])) / no_of_cases
-		item["specified_qty"] = flt(ps_item_qty[item["item_code"]])
-		if not item["packed_qty"]:
-			item["packed_qty"] = 0
-
-		frappe.throw(
-			_("Quantity for Item {0} must be less than {1}").format(
-				item.get("item_code"), item.get("recommended_qty")
-			)
-		)
-
-	def update_item_details(self):
-		"""
-		Fill empty columns in Packing Slip Item
-		"""
+	def set_missing_values(self):
 		if not self.from_case_no:
 			self.from_case_no = self.get_recommended_case_no()
 
-		for d in self.get("items"):
-			res = frappe.db.get_value("Item", d.item_code, ["weight_per_unit", "weight_uom"], as_dict=True)
+		for item in self.items:
+			stock_uom, weight_per_unit, weight_uom = frappe.db.get_value(
+				"Item", item.item_code, ["stock_uom", "weight_per_unit", "weight_uom"]
+			)
 
-			if res and len(res) > 0:
-				d.net_weight = res["weight_per_unit"]
-				d.weight_uom = res["weight_uom"]
+			item.stock_uom = stock_uom
+			if weight_per_unit and not item.net_weight:
+				item.net_weight = weight_per_unit
+			if weight_uom and not item.weight_uom:
+				item.weight_uom = weight_uom
 
 	def get_recommended_case_no(self):
-		"""
-		Returns the next case no. for a new packing slip for a delivery
-		note
-		"""
-		recommended_case_no = frappe.db.sql(
-			"""SELECT MAX(to_case_no) FROM `tabPacking Slip`
-			WHERE delivery_note = %s AND docstatus=1""",
-			self.delivery_note,
+		"""Returns the next case no. for a new packing slip for a delivery note"""
+
+		return (
+			cint(
+				frappe.db.get_value(
+					"Packing Slip", {"delivery_note": self.delivery_note, "docstatus": 1}, ["max(to_case_no)"]
+				)
+			)
+			+ 1
 		)
 
-		return cint(recommended_case_no[0][0]) + 1
+	def calculate_net_total_pkg(self):
+		self.net_weight_uom = self.items[0].weight_uom if self.items else None
+		self.gross_weight_uom = self.net_weight_uom
 
-	@frappe.whitelist()
-	def get_items(self):
-		self.set("items", [])
+		net_weight_pkg = 0
+		for item in self.items:
+			if item.weight_uom != self.net_weight_uom:
+				frappe.throw(
+					_(
+						"Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM."
+					)
+				)
 
-		custom_fields = frappe.get_meta("Delivery Note Item").get_custom_fields()
+			net_weight_pkg += flt(item.net_weight) * flt(item.qty)
 
-		dn_details = self.get_details_for_packing()[0]
-		for item in dn_details:
-			if flt(item.qty) > flt(item.packed_qty):
-				ch = self.append("items", {})
-				ch.item_code = item.item_code
-				ch.item_name = item.item_name
-				ch.stock_uom = item.stock_uom
-				ch.description = item.description
-				ch.batch_no = item.batch_no
-				ch.qty = flt(item.qty) - flt(item.packed_qty)
+		self.net_weight_pkg = round(net_weight_pkg, 2)
 
-				# copy custom fields
-				for d in custom_fields:
-					if item.get(d.fieldname):
-						ch.set(d.fieldname, item.get(d.fieldname))
-
-		self.update_item_details()
+		if not flt(self.gross_weight_pkg):
+			self.gross_weight_pkg = self.net_weight_pkg
 
 
 @frappe.whitelist()
diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
index bc405b2..96da23d 100644
--- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
@@ -3,9 +3,118 @@
 
 import unittest
 
-# test_records = frappe.get_test_records('Packing Slip')
+import frappe
 from frappe.tests.utils import FrappeTestCase
 
+from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+from erpnext.stock.doctype.delivery_note.delivery_note import make_packing_slip
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.item.test_item import make_item
 
-class TestPackingSlip(unittest.TestCase):
-	pass
+
+class TestPackingSlip(FrappeTestCase):
+	def test_packing_slip(self):
+		# Step - 1: Create a Product Bundle
+		items = create_items()
+		make_product_bundle(items[0], items[1:], 5)
+
+		# Step - 2: Create a Delivery Note (Draft) with Product Bundle
+		dn = create_delivery_note(
+			item_code=items[0],
+			qty=2,
+			do_not_save=True,
+		)
+		dn.append(
+			"items",
+			{
+				"item_code": items[1],
+				"warehouse": "_Test Warehouse - _TC",
+				"qty": 10,
+			},
+		)
+		dn.save()
+
+		# Step - 3: Make a Packing Slip from Delivery Note for 4 Qty
+		ps1 = make_packing_slip(dn.name)
+		for item in ps1.items:
+			item.qty = 4
+		ps1.save()
+		ps1.submit()
+
+		# Test - 1: `Packed Qty` should be updated to 4 in Delivery Note Items and Packed Items.
+		dn.load_from_db()
+		for item in dn.items:
+			if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
+				self.assertEqual(item.packed_qty, 4)
+
+		for item in dn.packed_items:
+			self.assertEqual(item.packed_qty, 4)
+
+		# Step - 4: Make another Packing Slip from Delivery Note for 6 Qty
+		ps2 = make_packing_slip(dn.name)
+		ps2.save()
+		ps2.submit()
+
+		# Test - 2: `Packed Qty` should be updated to 10 in Delivery Note Items and Packed Items.
+		dn.load_from_db()
+		for item in dn.items:
+			if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
+				self.assertEqual(item.packed_qty, 10)
+
+		for item in dn.packed_items:
+			self.assertEqual(item.packed_qty, 10)
+
+		# Step - 5: Cancel Packing Slip [1]
+		ps1.cancel()
+
+		# Test - 3: `Packed Qty` should be updated to 4 in Delivery Note Items and Packed Items.
+		dn.load_from_db()
+		for item in dn.items:
+			if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
+				self.assertEqual(item.packed_qty, 6)
+
+		for item in dn.packed_items:
+			self.assertEqual(item.packed_qty, 6)
+
+		# Step - 6: Cancel Packing Slip [2]
+		ps2.cancel()
+
+		# Test - 4: `Packed Qty` should be updated to 0 in Delivery Note Items and Packed Items.
+		dn.load_from_db()
+		for item in dn.items:
+			if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
+				self.assertEqual(item.packed_qty, 0)
+
+		for item in dn.packed_items:
+			self.assertEqual(item.packed_qty, 0)
+
+		# Step - 7: Make Packing Slip for more Qty than Delivery Note
+		ps3 = make_packing_slip(dn.name)
+		ps3.items[0].qty = 20
+
+		# Test - 5: Should throw an ValidationError, as Packing Slip Qty is more than Delivery Note Qty
+		self.assertRaises(frappe.exceptions.ValidationError, ps3.save)
+
+		# Step - 8: Make Packing Slip for less Qty than Delivery Note
+		ps4 = make_packing_slip(dn.name)
+		ps4.items[0].qty = 5
+		ps4.save()
+		ps4.submit()
+
+		# Test - 6: Delivery Note should throw a ValidationError on Submit, as Packed Qty and Delivery Note Qty are not the same
+		dn.load_from_db()
+		self.assertRaises(frappe.exceptions.ValidationError, dn.submit)
+
+
+def create_items():
+	items_properties = [
+		{"is_stock_item": 0},
+		{"is_stock_item": 1, "stock_uom": "Nos"},
+		{"is_stock_item": 1, "stock_uom": "Box"},
+	]
+
+	items = []
+	for properties in items_properties:
+		items.append(make_item(properties=properties).name)
+
+	return items
diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
index 4270839..4bd9035 100644
--- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
+++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
@@ -20,7 +20,8 @@
   "stock_uom",
   "weight_uom",
   "page_break",
-  "dn_detail"
+  "dn_detail",
+  "pi_detail"
  ],
  "fields": [
   {
@@ -121,13 +122,23 @@
    "fieldtype": "Data",
    "hidden": 1,
    "in_list_view": 1,
-   "label": "DN Detail"
+   "label": "Delivery Note Item",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "pi_detail",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Delivery Note Packed Item",
+   "no_copy": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-12-14 01:22:00.715935",
+ "modified": "2023-04-28 15:00:14.079306",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Packing Slip Item",
@@ -136,5 +147,6 @@
  "permissions": [],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 3fd4cec..8d8b69d 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -47,6 +47,7 @@
 		self.validate_putaway_capacity()
 
 		if self._action == "submit":
+			self.validate_reserved_stock()
 			self.make_batches("warehouse")
 
 	def on_submit(self):
@@ -60,6 +61,7 @@
 
 	def on_cancel(self):
 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
+		self.validate_reserved_stock()
 		self.make_sle_on_cancel()
 		self.make_gl_entries_on_cancel()
 		self.repost_future_sle_and_gle()
@@ -224,6 +226,46 @@
 		except Exception as e:
 			self.validation_messages.append(_("Row #") + " " + ("%d: " % (row.idx)) + cstr(e))
 
+	def validate_reserved_stock(self) -> None:
+		"""Raises an exception if there is any reserved stock for the items in the Stock Reconciliation."""
+
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			get_sre_reserved_qty_details_for_item_and_warehouse as get_sre_reserved_qty_details,
+		)
+
+		item_code_list, warehouse_list = [], []
+		for item in self.items:
+			item_code_list.append(item.item_code)
+			warehouse_list.append(item.warehouse)
+
+		sre_reserved_qty_details = get_sre_reserved_qty_details(item_code_list, warehouse_list)
+
+		if sre_reserved_qty_details:
+			data = []
+			for (item_code, warehouse), reserved_qty in sre_reserved_qty_details.items():
+				data.append([item_code, warehouse, reserved_qty])
+
+			msg = ""
+			if len(data) == 1:
+				msg = _(
+					"{0} units are reserved for Item {1} in Warehouse {2}, please un-reserve the same to {3} the Stock Reconciliation."
+				).format(bold(data[0][2]), bold(data[0][0]), bold(data[0][1]), self._action)
+			else:
+				items_html = ""
+				for d in data:
+					items_html += "<li>{0} units of Item {1} in Warehouse {2}</li>".format(
+						bold(d[2]), bold(d[0]), bold(d[1])
+					)
+
+				msg = _(
+					"The stock has been reserved for the following Items and Warehouses, un-reserve the same to {0} the Stock Reconciliation: <br /><br /> {1}"
+				).format(self._action, items_html)
+
+			frappe.throw(
+				msg,
+				title=_("Stock Reservation"),
+			)
+
 	def update_stock_ledger(self):
 		"""find difference between current and expected entries
 		and create stock ledger entries based on the difference"""
diff --git a/erpnext/stock/doctype/stock_reservation_entry/__init__.py b/erpnext/stock/doctype/stock_reservation_entry/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reservation_entry/__init__.py
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
new file mode 100644
index 0000000..666fd24
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Stock Reservation Entry", {
+	refresh(frm) {
+        frm.page.btn_primary.hide()
+	},
+});
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
new file mode 100644
index 0000000..7c7abac
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
@@ -0,0 +1,234 @@
+{
+ "actions": [],
+ "allow_copy": 1,
+ "autoname": "MAT-SRE-.YYYY.-.#####",
+ "creation": "2023-03-20 10:45:59.258959",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item_code",
+  "warehouse",
+  "column_break_elik",
+  "voucher_type",
+  "voucher_no",
+  "voucher_detail_no",
+  "section_break_xt4m",
+  "available_qty",
+  "voucher_qty",
+  "stock_uom",
+  "column_break_o6ex",
+  "reserved_qty",
+  "delivered_qty",
+  "section_break_3vb3",
+  "company",
+  "column_break_jbyr",
+  "project",
+  "status",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "item_code",
+   "fieldtype": "Link",
+   "in_filter": 1,
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Item Code",
+   "oldfieldname": "item_code",
+   "oldfieldtype": "Link",
+   "options": "Item",
+   "print_width": "100px",
+   "read_only": 1,
+   "search_index": 1,
+   "width": "100px"
+  },
+  {
+   "fieldname": "warehouse",
+   "fieldtype": "Link",
+   "in_filter": 1,
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Warehouse",
+   "oldfieldname": "warehouse",
+   "oldfieldtype": "Link",
+   "options": "Warehouse",
+   "print_width": "100px",
+   "read_only": 1,
+   "search_index": 1,
+   "width": "100px"
+  },
+  {
+   "fieldname": "voucher_type",
+   "fieldtype": "Select",
+   "in_filter": 1,
+   "label": "Voucher Type",
+   "oldfieldname": "voucher_type",
+   "oldfieldtype": "Data",
+   "options": "\nSales Order",
+   "print_width": "150px",
+   "read_only": 1,
+   "width": "150px"
+  },
+  {
+   "fieldname": "voucher_no",
+   "fieldtype": "Dynamic Link",
+   "in_filter": 1,
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Voucher No",
+   "oldfieldname": "voucher_no",
+   "oldfieldtype": "Data",
+   "options": "voucher_type",
+   "print_width": "150px",
+   "read_only": 1,
+   "width": "150px"
+  },
+  {
+   "fieldname": "voucher_detail_no",
+   "fieldtype": "Data",
+   "label": "Voucher Detail No",
+   "oldfieldname": "voucher_detail_no",
+   "oldfieldtype": "Data",
+   "print_width": "150px",
+   "read_only": 1,
+   "search_index": 1,
+   "width": "150px"
+  },
+  {
+   "fieldname": "stock_uom",
+   "fieldtype": "Link",
+   "label": "Stock UOM",
+   "oldfieldname": "stock_uom",
+   "oldfieldtype": "Data",
+   "options": "UOM",
+   "print_width": "150px",
+   "read_only": 1,
+   "width": "150px"
+  },
+  {
+   "fieldname": "project",
+   "fieldtype": "Link",
+   "label": "Project",
+   "options": "Project",
+   "read_only": 1
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "in_filter": 1,
+   "label": "Company",
+   "oldfieldname": "company",
+   "oldfieldtype": "Data",
+   "options": "Company",
+   "print_width": "150px",
+   "read_only": 1,
+   "search_index": 1,
+   "width": "150px"
+  },
+  {
+   "fieldname": "reserved_qty",
+   "fieldtype": "Float",
+   "in_filter": 1,
+   "in_list_view": 1,
+   "label": "Reserved Qty",
+   "oldfieldname": "actual_qty",
+   "oldfieldtype": "Currency",
+   "print_width": "150px",
+   "read_only": 1,
+   "width": "150px"
+  },
+  {
+   "default": "Draft",
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "hidden": 1,
+   "label": "Status",
+   "options": "Draft\nPartially Reserved\nReserved\nPartially Delivered\nDelivered\nCancelled",
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "delivered_qty",
+   "fieldtype": "Float",
+   "label": "Delivered Qty",
+   "read_only": 1
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Stock Reservation Entry",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "available_qty",
+   "fieldtype": "Float",
+   "label": "Available Qty to Reserve",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fieldname": "voucher_qty",
+   "fieldtype": "Float",
+   "label": "Voucher Qty",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_elik",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_xt4m",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "column_break_o6ex",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_3vb3",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "column_break_jbyr",
+   "fieldtype": "Column Break"
+  }
+ ],
+ "hide_toolbar": 1,
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-03-29 18:36:26.752872",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock Reservation Entry",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  }
+ ],
+ "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.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
new file mode 100644
index 0000000..5819dd7
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -0,0 +1,312 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.query_builder.functions import Sum
+
+
+class StockReservationEntry(Document):
+	def validate(self) -> None:
+		from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
+
+		self.validate_mandatory()
+		self.validate_for_group_warehouse()
+		validate_disabled_warehouse(self.warehouse)
+		validate_warehouse_company(self.warehouse, self.company)
+
+	def on_submit(self) -> None:
+		self.update_reserved_qty_in_voucher()
+		self.update_status()
+
+	def on_cancel(self) -> None:
+		self.update_reserved_qty_in_voucher()
+		self.update_status()
+
+	def validate_mandatory(self) -> None:
+		"""Raises exception if mandatory fields are not set."""
+
+		mandatory = [
+			"item_code",
+			"warehouse",
+			"voucher_type",
+			"voucher_no",
+			"voucher_detail_no",
+			"available_qty",
+			"voucher_qty",
+			"stock_uom",
+			"reserved_qty",
+			"company",
+		]
+		for d in mandatory:
+			if not self.get(d):
+				frappe.throw(_("{0} is required").format(self.meta.get_label(d)))
+
+	def validate_for_group_warehouse(self) -> None:
+		"""Raises exception if `Warehouse` is a Group Warehouse."""
+
+		if frappe.get_cached_value("Warehouse", self.warehouse, "is_group"):
+			frappe.throw(
+				_("Stock cannot be reserved in group warehouse {0}.").format(frappe.bold(self.warehouse)),
+				title=_("Invalid Warehouse"),
+			)
+
+	def update_status(self, status: str = None, update_modified: bool = True) -> None:
+		"""Updates status based on Voucher Qty, Reserved Qty and Delivered Qty."""
+
+		if not status:
+			if self.docstatus == 2:
+				status = "Cancelled"
+			elif self.docstatus == 1:
+				if self.reserved_qty == self.delivered_qty:
+					status = "Delivered"
+				elif self.delivered_qty and self.delivered_qty < self.reserved_qty:
+					status = "Partially Delivered"
+				elif self.reserved_qty == self.voucher_qty:
+					status = "Reserved"
+				else:
+					status = "Partially Reserved"
+			else:
+				status = "Draft"
+
+		frappe.db.set_value(self.doctype, self.name, "status", status, update_modified=update_modified)
+
+	def update_reserved_qty_in_voucher(
+		self, reserved_qty_field: str = "stock_reserved_qty", update_modified: bool = True
+	) -> None:
+		"""Updates total reserved qty in the voucher."""
+
+		item_doctype = "Sales Order Item" if self.voucher_type == "Sales Order" else None
+
+		if item_doctype:
+			sre = frappe.qb.DocType("Stock Reservation Entry")
+			reserved_qty = (
+				frappe.qb.from_(sre)
+				.select(Sum(sre.reserved_qty))
+				.where(
+					(sre.docstatus == 1)
+					& (sre.voucher_type == self.voucher_type)
+					& (sre.voucher_no == self.voucher_no)
+					& (sre.voucher_detail_no == self.voucher_detail_no)
+				)
+			).run(as_list=True)[0][0] or 0
+
+			frappe.db.set_value(
+				item_doctype,
+				self.voucher_detail_no,
+				reserved_qty_field,
+				reserved_qty,
+				update_modified=update_modified,
+			)
+
+
+def validate_stock_reservation_settings(voucher: object) -> None:
+	"""Raises an exception if `Stock Reservation` is not enabled or `Voucher Type` is not allowed."""
+
+	if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
+		frappe.throw(
+			_("Please enable {0} in the {1}.").format(
+				frappe.bold("Stock Reservation"), frappe.bold("Stock Settings")
+			)
+		)
+
+	# Voucher types allowed for stock reservation
+	allowed_voucher_types = ["Sales Order"]
+
+	if voucher.doctype not in allowed_voucher_types:
+		frappe.throw(
+			_("Stock Reservation can only be created against {0}.").format(", ".join(allowed_voucher_types))
+		)
+
+
+def get_available_qty_to_reserve(item_code: str, warehouse: str) -> float:
+	"""Returns `Available Qty to Reserve (Actual Qty - Reserved Qty)` for Item and Warehouse combination."""
+
+	from erpnext.stock.utils import get_stock_balance
+
+	available_qty = get_stock_balance(item_code, warehouse)
+
+	if available_qty:
+		sre = frappe.qb.DocType("Stock Reservation Entry")
+		reserved_qty = (
+			frappe.qb.from_(sre)
+			.select(Sum(sre.reserved_qty - sre.delivered_qty))
+			.where(
+				(sre.docstatus == 1)
+				& (sre.item_code == item_code)
+				& (sre.warehouse == warehouse)
+				& (sre.status.notin(["Delivered", "Cancelled"]))
+			)
+		).run()[0][0] or 0.0
+
+		if reserved_qty:
+			return available_qty - reserved_qty
+
+	return available_qty
+
+
+def get_stock_reservation_entries_for_voucher(
+	voucher_type: str, voucher_no: str, voucher_detail_no: str = None, fields: list[str] = None
+) -> list[dict]:
+	"""Returns list of Stock Reservation Entries against a Voucher."""
+
+	if not fields or not isinstance(fields, list):
+		fields = [
+			"name",
+			"item_code",
+			"warehouse",
+			"voucher_detail_no",
+			"reserved_qty",
+			"delivered_qty",
+			"stock_uom",
+		]
+
+	sre = frappe.qb.DocType("Stock Reservation Entry")
+	query = (
+		frappe.qb.from_(sre)
+		.where(
+			(sre.docstatus == 1)
+			& (sre.voucher_type == voucher_type)
+			& (sre.voucher_no == voucher_no)
+			& (sre.status.notin(["Delivered", "Cancelled"]))
+		)
+		.orderby(sre.creation)
+	)
+
+	for field in fields:
+		query = query.select(sre[field])
+
+	if voucher_detail_no:
+		query = query.where(sre.voucher_detail_no == voucher_detail_no)
+
+	return query.run(as_dict=True)
+
+
+def get_sre_reserved_qty_details_for_item_and_warehouse(
+	item_code_list: list, warehouse_list: list
+) -> dict:
+	"""Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }."""
+
+	sre_details = {}
+
+	if item_code_list and warehouse_list:
+		sre = frappe.qb.DocType("Stock Reservation Entry")
+		sre_data = (
+			frappe.qb.from_(sre)
+			.select(
+				sre.item_code,
+				sre.warehouse,
+				Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"),
+			)
+			.where(
+				(sre.docstatus == 1)
+				& (sre.item_code.isin(item_code_list))
+				& (sre.warehouse.isin(warehouse_list))
+				& (sre.status.notin(["Delivered", "Cancelled"]))
+			)
+			.groupby(sre.item_code, sre.warehouse)
+		).run(as_dict=True)
+
+		if sre_data:
+			sre_details = {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in sre_data}
+
+	return sre_details
+
+
+def get_sre_reserved_qty_for_item_and_warehouse(item_code: str, warehouse: str) -> float:
+	"""Returns `Reserved Qty` for Item and Warehouse combination."""
+
+	reserved_qty = 0.0
+
+	if item_code and warehouse:
+		sre = frappe.qb.DocType("Stock Reservation Entry")
+		return (
+			frappe.qb.from_(sre)
+			.select(Sum(sre.reserved_qty - sre.delivered_qty))
+			.where(
+				(sre.docstatus == 1)
+				& (sre.item_code == item_code)
+				& (sre.warehouse == warehouse)
+				& (sre.status.notin(["Delivered", "Cancelled"]))
+			)
+		).run(as_list=True)[0][0] or 0.0
+
+	return reserved_qty
+
+
+def get_sre_reserved_qty_details_for_voucher(voucher_type: str, voucher_no: str) -> dict:
+	"""Returns a dict like {"voucher_detail_no": "reserved_qty", ... }."""
+
+	sre = frappe.qb.DocType("Stock Reservation Entry")
+	data = (
+		frappe.qb.from_(sre)
+		.select(
+			sre.voucher_detail_no,
+			(Sum(sre.reserved_qty) - Sum(sre.delivered_qty)).as_("reserved_qty"),
+		)
+		.where(
+			(sre.docstatus == 1)
+			& (sre.voucher_type == voucher_type)
+			& (sre.voucher_no == voucher_no)
+			& (sre.status.notin(["Delivered", "Cancelled"]))
+		)
+		.groupby(sre.voucher_detail_no)
+	).run(as_list=True)
+
+	return frappe._dict(data)
+
+
+def get_sre_reserved_qty_details_for_voucher_detail_no(
+	voucher_type: str, voucher_no: str, voucher_detail_no: str
+) -> list:
+	"""Returns a list like ["warehouse", "reserved_qty"]."""
+
+	sre = frappe.qb.DocType("Stock Reservation Entry")
+	reserved_qty_details = (
+		frappe.qb.from_(sre)
+		.select(sre.warehouse, (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)))
+		.where(
+			(sre.docstatus == 1)
+			& (sre.voucher_type == voucher_type)
+			& (sre.voucher_no == voucher_no)
+			& (sre.voucher_detail_no == voucher_detail_no)
+			& (sre.status.notin(["Delivered", "Cancelled"]))
+		)
+		.orderby(sre.creation)
+		.groupby(sre.warehouse)
+	).run(as_list=True)
+
+	if reserved_qty_details:
+		return reserved_qty_details[0]
+
+	return reserved_qty_details
+
+
+def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool:
+	"""Returns True if there is any Stock Reservation Entry for the given voucher."""
+
+	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, notify: bool = True
+) -> None:
+	"""Cancel Stock Reservation Entries for the given voucher."""
+
+	sre_list = get_stock_reservation_entries_for_voucher(
+		voucher_type, voucher_no, voucher_detail_no, fields=["name"]
+	)
+
+	if sre_list:
+		for sre in sre_list:
+			frappe.get_doc("Stock Reservation Entry", sre.name).cancel()
+
+		if notify:
+			frappe.msgprint(_("Stock Reservation Entries Cancelled"), alert=True, indicator="red")
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js
new file mode 100644
index 0000000..442ac39
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.listview_settings['Stock Reservation Entry'] = {
+	get_indicator: function (doc) {
+		const status_colors = {
+            'Draft': 'red',
+            'Partially Reserved': 'orange',
+            'Reserved': 'blue',
+            'Partially Delivered': 'purple',
+            'Delivered': 'green',
+            'Cancelled': 'red',
+		};
+		return [__(doc.status), status_colors[doc.status], 'status,=,' + doc.status];
+	},
+};
\ No newline at end of file
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
new file mode 100644
index 0000000..5a082dd
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
@@ -0,0 +1,317 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
+
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.stock.utils import get_stock_balance
+
+
+class TestStockReservationEntry(FrappeTestCase):
+	def setUp(self) -> None:
+		self.items = create_items()
+		create_material_receipts(self.items)
+
+	def tearDown(self) -> None:
+		return super().tearDown()
+
+	def test_validate_stock_reservation_settings(self) -> None:
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			validate_stock_reservation_settings,
+		)
+
+		voucher = frappe._dict(
+			{
+				"doctype": "Sales Order",
+			}
+		)
+
+		# Case - 1: When `Stock Reservation` is disabled in `Stock Settings`, throw `ValidationError`
+		with change_settings("Stock Settings", {"enable_stock_reservation": 0}):
+			self.assertRaises(frappe.ValidationError, validate_stock_reservation_settings, voucher)
+
+		with change_settings("Stock Settings", {"enable_stock_reservation": 1}):
+			# Case - 2: When `Voucher Type` is not allowed for `Stock Reservation`, throw `ValidationError`
+			voucher.doctype = "NOT ALLOWED"
+			self.assertRaises(frappe.ValidationError, validate_stock_reservation_settings, voucher)
+
+			# Case - 3: When `Voucher Type` is allowed for `Stock Reservation`
+			voucher.doctype = "Sales Order"
+			self.assertIsNone(validate_stock_reservation_settings(voucher), None)
+
+	def test_get_available_qty_to_reserve(self) -> None:
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			get_available_qty_to_reserve,
+		)
+
+		item_code, warehouse = "SR Item 1", "_Test Warehouse - _TC"
+
+		# Case - 1: When `Reserved Qty` is `0`, Available Qty to Reserve = Actual Qty
+		cancel_all_stock_reservation_entries()
+		available_qty_to_reserve = get_available_qty_to_reserve(item_code, warehouse)
+		expected_available_qty_to_reserve = get_stock_balance(item_code, warehouse)
+
+		self.assertEqual(available_qty_to_reserve, expected_available_qty_to_reserve)
+
+		# Case - 2: When `Reserved Qty` is `> 0`, Available Qty to Reserve = Actual Qty - Reserved Qty
+		sre = make_stock_reservation_entry(
+			item_code=item_code,
+			warehouse=warehouse,
+			ignore_validate=True,
+		)
+		available_qty_to_reserve = get_available_qty_to_reserve(item_code, warehouse)
+		expected_available_qty_to_reserve = get_stock_balance(item_code, warehouse) - sre.reserved_qty
+
+		self.assertEqual(available_qty_to_reserve, expected_available_qty_to_reserve)
+
+	def test_update_status(self) -> None:
+		sre = make_stock_reservation_entry(
+			reserved_qty=30,
+			ignore_validate=True,
+			do_not_submit=True,
+		)
+
+		# Draft: When DocStatus is `0`
+		sre.load_from_db()
+		self.assertEqual(sre.status, "Draft")
+
+		# Partially Reserved: When DocStatus is `1` and `Reserved Qty` < `Voucher Qty`
+		sre.submit()
+		sre.load_from_db()
+		self.assertEqual(sre.status, "Partially Reserved")
+
+		# Reserved: When DocStatus is `1` and `Reserved Qty` = `Voucher Qty`
+		sre.reserved_qty = sre.voucher_qty
+		sre.db_update()
+		sre.update_status()
+		sre.load_from_db()
+		self.assertEqual(sre.status, "Reserved")
+
+		# Partially Delivered: When DocStatus is `1` and (0 < `Delivered Qty` < `Voucher Qty`)
+		sre.delivered_qty = 10
+		sre.db_update()
+		sre.update_status()
+		sre.load_from_db()
+		self.assertEqual(sre.status, "Partially Delivered")
+
+		# Delivered: When DocStatus is `1` and `Delivered Qty` = `Voucher Qty`
+		sre.delivered_qty = sre.voucher_qty
+		sre.db_update()
+		sre.update_status()
+		sre.load_from_db()
+		self.assertEqual(sre.status, "Delivered")
+
+		# Cancelled: When DocStatus is `2`
+		sre.cancel()
+		sre.load_from_db()
+		self.assertEqual(sre.status, "Cancelled")
+
+	@change_settings("Stock Settings", {"enable_stock_reservation": 1})
+	def test_update_reserved_qty_in_voucher(self) -> None:
+		item_code, warehouse = "SR Item 1", "_Test Warehouse - _TC"
+
+		# Step - 1: Create a `Sales Order`
+		so = make_sales_order(
+			item_code=item_code,
+			warehouse=warehouse,
+			qty=50,
+			rate=100,
+			do_not_submit=True,
+		)
+		so.reserve_stock = 0  # Stock Reservation Entries won't be created on submit
+		so.items[0].reserve_stock = 1
+		so.save()
+		so.submit()
+
+		# Step - 2: Create a `Stock Reservation Entry[1]` for the `Sales Order Item`
+		sre1 = make_stock_reservation_entry(
+			item_code=item_code,
+			warehouse=warehouse,
+			voucher_type="Sales Order",
+			voucher_no=so.name,
+			voucher_detail_no=so.items[0].name,
+			reserved_qty=30,
+		)
+
+		so.load_from_db()
+		sre1.load_from_db()
+		self.assertEqual(sre1.status, "Partially Reserved")
+		self.assertEqual(so.items[0].stock_reserved_qty, sre1.reserved_qty)
+
+		# Step - 3: Create a `Stock Reservation Entry[2]` for the `Sales Order Item`
+		sre2 = make_stock_reservation_entry(
+			item_code=item_code,
+			warehouse=warehouse,
+			voucher_type="Sales Order",
+			voucher_no=so.name,
+			voucher_detail_no=so.items[0].name,
+			reserved_qty=20,
+		)
+
+		so.load_from_db()
+		sre2.load_from_db()
+		self.assertEqual(sre1.status, "Partially Reserved")
+		self.assertEqual(so.items[0].stock_reserved_qty, sre1.reserved_qty + sre2.reserved_qty)
+
+		# Step - 4: Cancel `Stock Reservation Entry[1]`
+		sre1.cancel()
+		so.load_from_db()
+		sre1.load_from_db()
+		self.assertEqual(sre1.status, "Cancelled")
+		self.assertEqual(so.items[0].stock_reserved_qty, sre2.reserved_qty)
+
+		# Step - 5: Cancel `Stock Reservation Entry[2]`
+		sre2.cancel()
+		so.load_from_db()
+		sre2.load_from_db()
+		self.assertEqual(sre1.status, "Cancelled")
+		self.assertEqual(so.items[0].stock_reserved_qty, 0)
+
+	@change_settings("Stock Settings", {"enable_stock_reservation": 1})
+	def test_cant_consume_reserved_stock(self) -> None:
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			cancel_stock_reservation_entries,
+		)
+		from erpnext.stock.stock_ledger import NegativeStockError
+
+		item_code, warehouse = "SR Item 1", "_Test Warehouse - _TC"
+
+		# Step - 1: Create a `Sales Order`
+		so = make_sales_order(
+			item_code=item_code,
+			warehouse=warehouse,
+			qty=50,
+			rate=100,
+			do_not_submit=True,
+		)
+		so.reserve_stock = 1  # Stock Reservation Entries will be created on submit
+		so.items[0].reserve_stock = 1
+		so.save()
+		so.submit()
+
+		actual_qty = get_stock_balance(item_code, warehouse)
+
+		# Step - 2: Try to consume (Transfer/Issue/Deliver) the Available Qty via Stock Entry or Delivery Note, should throw `NegativeStockError`.
+		se = make_stock_entry(
+			item_code=item_code,
+			qty=actual_qty,
+			from_warehouse=warehouse,
+			rate=100,
+			purpose="Material Issue",
+			do_not_submit=True,
+		)
+		self.assertRaises(NegativeStockError, se.submit)
+		se.cancel()
+
+		# Step - 3: Unreserve the stock and consume the Available Qty via Stock Entry.
+		cancel_stock_reservation_entries(so.doctype, so.name)
+
+		se = make_stock_entry(
+			item_code=item_code,
+			qty=actual_qty,
+			from_warehouse=warehouse,
+			rate=100,
+			purpose="Material Issue",
+			do_not_submit=True,
+		)
+		se.submit()
+		se.cancel()
+
+
+def create_items() -> dict:
+	from erpnext.stock.doctype.item.test_item import make_item
+
+	items_details = {
+		# Stock Items
+		"SR Item 1": {"is_stock_item": 1, "valuation_rate": 100},
+		"SR Item 2": {"is_stock_item": 1, "valuation_rate": 200, "stock_uom": "Kg"},
+		# Batch Items
+		"SR Batch Item 1": {
+			"is_stock_item": 1,
+			"valuation_rate": 100,
+			"has_batch_no": 1,
+			"create_new_batch": 1,
+			"batch_number_series": "SRBI-1-.#####.",
+		},
+		"SR Batch Item 2": {
+			"is_stock_item": 1,
+			"valuation_rate": 200,
+			"has_batch_no": 1,
+			"create_new_batch": 1,
+			"batch_number_series": "SRBI-2-.#####.",
+			"stock_uom": "Kg",
+		},
+		# Serial Item
+		"SR Serial Item 1": {
+			"is_stock_item": 1,
+			"valuation_rate": 100,
+			"has_serial_no": 1,
+			"serial_no_series": "SRSI-1-.#####",
+		},
+		# Batch and Serial Item
+		"SR Batch and Serial Item 1": {
+			"is_stock_item": 1,
+			"valuation_rate": 100,
+			"has_batch_no": 1,
+			"create_new_batch": 1,
+			"batch_number_series": "SRBSI-1-.#####.",
+			"has_serial_no": 1,
+			"serial_no_series": "SRBSI-1-.#####",
+		},
+	}
+
+	items = {}
+	for item_code, properties in items_details.items():
+		items[item_code] = make_item(item_code, properties)
+
+	return items
+
+
+def create_material_receipts(
+	items: dict, warehouse: str = "_Test Warehouse - _TC", qty: float = 100
+) -> None:
+	for item in items.values():
+		if item.is_stock_item:
+			make_stock_entry(
+				item_code=item.item_code,
+				qty=qty,
+				to_warehouse=warehouse,
+				rate=item.valuation_rate,
+				purpose="Material Receipt",
+			)
+
+
+def cancel_all_stock_reservation_entries() -> None:
+	sre_list = frappe.db.get_all("Stock Reservation Entry", filters={"docstatus": 1}, pluck="name")
+
+	for sre in sre_list:
+		frappe.get_doc("Stock Reservation Entry", sre).cancel()
+
+
+def make_stock_reservation_entry(**args):
+	doc = frappe.new_doc("Stock Reservation Entry")
+	args = frappe._dict(args)
+
+	doc.item_code = args.item_code or "SR Item 1"
+	doc.warehouse = args.warehouse or "_Test Warehouse - _TC"
+	doc.voucher_type = args.voucher_type
+	doc.voucher_no = args.voucher_no
+	doc.voucher_detail_no = args.voucher_detail_no
+	doc.available_qty = args.available_qty or 100
+	doc.voucher_qty = args.voucher_qty or 50
+	doc.stock_uom = args.stock_uom or "Nos"
+	doc.reserved_qty = args.reserved_qty or 50
+	doc.delivered_qty = args.delivered_qty or 0
+	doc.company = args.company or "_Test Company"
+
+	if args.ignore_validate:
+		doc.flags.ignore_validate = True
+
+	if not args.do_not_save:
+		doc.save()
+		if not args.do_not_submit:
+			doc.submit()
+
+	return doc
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index ec7fb0f..35970b1 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -31,6 +31,11 @@
   "action_if_quality_inspection_is_not_submitted",
   "column_break_23",
   "action_if_quality_inspection_is_rejected",
+  "stock_reservation_tab",
+  "enable_stock_reservation",
+  "column_break_rx3e",
+  "reserve_stock_on_sales_order_submission",
+  "allow_partial_reservation",
   "serial_and_batch_item_settings_tab",
   "section_break_7",
   "automatically_set_serial_nos_based_on_fifo",
@@ -339,6 +344,37 @@
   {
    "fieldname": "column_break_121",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "stock_reservation_tab",
+   "fieldtype": "Tab Break",
+   "label": "Stock Reservation"
+  },
+  {
+   "default": "0",
+   "fieldname": "enable_stock_reservation",
+   "fieldtype": "Check",
+   "label": "Enable Stock 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": "reserve_stock_on_sales_order_submission",
+   "fieldtype": "Check",
+   "label": "Reserve Stock on Sales Order Submission"
+  },
+  {
+   "fieldname": "column_break_rx3e",
+   "fieldtype": "Column Break"
+  },
+  {
+   "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 <b>Sales Order</b> 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"
   }
  ],
  "icon": "icon-cog",
@@ -346,7 +382,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2022-02-05 15:33:43.692736",
+ "modified": "2023-04-22 08:48:37.767646",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Settings",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 50807a9..e25c843 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -55,6 +55,7 @@
 		self.cant_change_valuation_method()
 		self.validate_clean_description_html()
 		self.validate_pending_reposts()
+		self.validate_stock_reservation()
 
 	def validate_warehouses(self):
 		warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
@@ -99,6 +100,74 @@
 		if self.stock_frozen_upto:
 			check_pending_reposting(self.stock_frozen_upto)
 
+	def validate_stock_reservation(self):
+		"""Raises an exception if the user tries to enable/disable `Stock Reservation` with `Negative Stock` or `Open Stock Reservation Entries`."""
+
+		# Skip validation for tests
+		if frappe.flags.in_test:
+			return
+
+		db_allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
+		db_enable_stock_reservation = frappe.db.get_single_value(
+			"Stock Settings", "enable_stock_reservation"
+		)
+
+		# Change in value of `Allow Negative Stock`
+		if db_allow_negative_stock != self.allow_negative_stock:
+
+			# Disable -> Enable: Don't allow if `Stock Reservation` is enabled
+			if self.allow_negative_stock and self.enable_stock_reservation:
+				frappe.throw(
+					_("As {0} is enabled, you can not enable {1}.").format(
+						frappe.bold("Stock Reservation"), frappe.bold("Allow Negative Stock")
+					)
+				)
+
+		# Change in value of `Enable Stock Reservation`
+		if db_enable_stock_reservation != self.enable_stock_reservation:
+
+			# Disable -> Enable
+			if self.enable_stock_reservation:
+
+				# Don't allow if `Allow Negative Stock` is enabled
+				if self.allow_negative_stock:
+					frappe.throw(
+						_("As {0} is enabled, you can not enable {1}.").format(
+							frappe.bold("Allow Negative Stock"), frappe.bold("Stock Reservation")
+						)
+					)
+
+				else:
+					# Don't allow if there are negative stock
+					from frappe.query_builder.functions import Round
+
+					precision = frappe.db.get_single_value("System Settings", "float_precision") or 3
+					bin = frappe.qb.DocType("Bin")
+					bin_with_negative_stock = (
+						frappe.qb.from_(bin).select(bin.name).where(Round(bin.actual_qty, precision) < 0).limit(1)
+					).run()
+
+					if bin_with_negative_stock:
+						frappe.throw(
+							_("As there are negative stock, you can not enable {0}.").format(
+								frappe.bold("Stock Reservation")
+							)
+						)
+
+			# Enable -> Disable
+			else:
+				# Don't allow if there are open Stock Reservation Entries
+				has_reserved_stock = frappe.db.exists(
+					"Stock Reservation Entry", {"docstatus": 1, "status": ["!=", "Delivered"]}
+				)
+
+				if has_reserved_stock:
+					frappe.throw(
+						_("As there are reserved stock, you cannot disable {0}.").format(
+							frappe.bold("Stock Reservation")
+						)
+					)
+
 	def on_update(self):
 		self.toggle_warehouse_field_for_inter_warehouse_transfer()
 
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index 68df918..f2c2e27 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -100,6 +100,7 @@
 		_func = itemgetter(1)
 
 		self.item_warehouse_map = self.get_item_warehouse_map()
+		sre_details = self.get_sre_reserved_qty_details()
 
 		variant_values = {}
 		if self.filters.get("show_variant_attributes"):
@@ -133,6 +134,9 @@
 
 				report_data.update(stock_ageing_data)
 
+			report_data.update(
+				{"reserved_stock": sre_details.get((report_data.item_code, report_data.warehouse), 0.0)}
+			)
 			self.data.append(report_data)
 
 	def get_item_warehouse_map(self):
@@ -159,6 +163,18 @@
 
 		return item_warehouse_map
 
+	def get_sre_reserved_qty_details(self) -> dict:
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			get_sre_reserved_qty_details_for_item_and_warehouse as get_reserved_qty_details,
+		)
+
+		item_code_list, warehouse_list = [], []
+		for d in self.item_warehouse_map:
+			item_code_list.append(d[1])
+			warehouse_list.append(d[2])
+
+		return get_reserved_qty_details(item_code_list, warehouse_list)
+
 	def prepare_item_warehouse_map(self, item_warehouse_map, entry, group_by_key):
 		qty_dict = item_warehouse_map[group_by_key]
 		for field in self.inventory_dimensions:
@@ -436,6 +452,13 @@
 					"options": "currency",
 				},
 				{
+					"label": _("Reserved Stock"),
+					"fieldname": "reserved_stock",
+					"fieldtype": "Float",
+					"width": 80,
+					"convertible": "qty",
+				},
+				{
 					"label": _("Company"),
 					"fieldname": "company",
 					"fieldtype": "Link",
@@ -573,7 +596,7 @@
 				"warehouse",
 				"item_name",
 				"item_group",
-				"projecy",
+				"project",
 				"stock_uom",
 				"company",
 				"opening_fifo_queue",
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 2f64edd..711694b 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -13,6 +13,9 @@
 
 import erpnext
 from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
+from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+	get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
+)
 from erpnext.stock.utils import (
 	get_incoming_outgoing_rate_for_cancel,
 	get_or_make_bin,
@@ -380,6 +383,7 @@
 		self.new_items_found = False
 		self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
 		self.affected_transactions: Set[Tuple[str, str]] = set()
+		self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse)
 
 		self.data = frappe._dict()
 		self.initialize_previous_data(self.args)
@@ -627,7 +631,7 @@
 		validate negative stock for entries current datetime onwards
 		will not consider cancelled entries
 		"""
-		diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
+		diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
 		diff = flt(diff, self.flt_precision)  # respect system precision
 
 		if diff < 0 and abs(diff) > 0.0001:
@@ -1038,7 +1042,7 @@
 			) in frappe.local.flags.currently_saving:
 
 				msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
-					abs(deficiency),
+					frappe.bold(abs(deficiency)),
 					frappe.get_desk_link("Item", exceptions[0]["item_code"]),
 					frappe.get_desk_link("Warehouse", warehouse),
 				)
@@ -1046,7 +1050,7 @@
 				msg = _(
 					"{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
 				).format(
-					abs(deficiency),
+					frappe.bold(abs(deficiency)),
 					frappe.get_desk_link("Item", exceptions[0]["item_code"]),
 					frappe.get_desk_link("Warehouse", warehouse),
 					exceptions[0]["posting_date"],
@@ -1055,6 +1059,12 @@
 				)
 
 			if msg:
+				if self.reserved_stock:
+					allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
+					msg = "{0} As {1} units are reserved, you are allowed to consume only {2} units.".format(
+						msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
+					)
+
 				msg_list.append(msg)
 
 		if msg_list: