Merge branch 'develop' into stock-reservation
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 1e4fabe..6e2fb2e 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -783,6 +783,75 @@
 
 		gl_entries.append(self.get_gl_dict(gl_entry, item=item))
 
+	def make_sr_entries(self):
+		if not self.get("reserve_stock"):
+			return
+
+		if self.doctype != "Sales Order":
+			frappe.throw(
+				_("Stock Reservation can only be created against a {0}.").format(frappe.bold("Sales Order"))
+			)
+
+		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")
+				)
+			)
+
+		if not frappe.db.get_single_value("Stock Settings", "reserve_stock_on_sales_order_submission"):
+			frappe.throw(
+				_("Please enable {0} in the {1}.").format(
+					frappe.bold("Reserve Stock on Sales Order Submission"), frappe.bold("Stock Settings")
+				)
+			)
+
+		for item in self.get("items"):
+			if not item.get("reserve_stock"):
+				continue
+
+			available_qty = get_available_qty_to_reserve(item.item_code, item.warehouse)
+			reserved_qty = min(item.stock_qty, available_qty)
+
+			if not reserved_qty:
+				frappe.msgprint(
+					_("Row {0}: No available stock to reserve for the Item {1}").format(
+						item.idx, frappe.bold(item.item_code)
+					),
+					title=_("Stock Reservation"),
+					indicator="orange",
+				)
+				continue
+
+			elif reserved_qty < item.stock_qty:
+				frappe.msgprint(
+					_("Row {0}: Only {1} available to reserve for the Item {2}").format(
+						item.idx,
+						frappe.bold(str(reserved_qty / item.conversion_factor) + " " + item.uom),
+						frappe.bold(item.item_code),
+					),
+					title=_("Stock Reservation"),
+					indicator="orange",
+				)
+
+				if not frappe.db.get_single_value("Stock Settings", "allow_partial_reservation"):
+					continue
+
+			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
+			sre.voucher_qty = item.stock_qty
+			sre.reserved_qty = reserved_qty
+			sre.company = self.company
+			sre.stock_uom = item.stock_uom
+			sre.project = self.project
+			sre.save()
+			sre.submit()
+
 
 def repost_required_for_queue(doc: StockController) -> bool:
 	"""check if stock document contains repeated item-warehouse with queue based valuation.
@@ -952,6 +1021,33 @@
 	return or_conditions
 
 
+@frappe.whitelist()
+def get_available_qty_to_reserve(item_code, warehouse):
+	from frappe.query_builder.functions import Sum
+
+	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 create_repost_item_valuation_entry(args):
 	args = frappe._dict(args)
 	repost_entry = frappe.new_doc("Repost Item Valuation")
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 449d461..d222c3e 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -46,6 +46,8 @@
 
 		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'
@@ -60,8 +62,27 @@
 			});
 		}
 
-		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 (value) {
+								frm.set_value("reserve_stock", 1);
+							} else {
+								frm.set_value("reserve_stock", 0);
+							}
+						})
+					} else {
+						frm.set_value("reserve_stock", 0);
+						frm.set_df_property("reserve_stock", "read_only", 1);
+					}
+				})
+			}
 		}
 	},
 
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index ccea840..40cb17d 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -46,6 +46,7 @@
   "scan_barcode",
   "column_break_28",
   "set_warehouse",
+  "reserve_stock",
   "items_section",
   "items",
   "section_break_31",
@@ -1637,13 +1638,20 @@
    "fieldname": "named_place",
    "fieldtype": "Data",
    "label": "Named Place"
+  },
+  {
+   "default": "0",
+   "fieldname": "reserve_stock",
+   "fieldtype": "Check",
+   "label": "Reserve Stock",
+   "no_copy": 1
   }
  ],
  "icon": "fa fa-file-text",
  "idx": 105,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-12-12 18:34:00.681780",
+ "modified": "2023-03-20 23:51:04.036757",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index ee9161b..5accaf6 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -241,6 +241,8 @@
 
 			update_coupon_code_count(self.coupon_code, "used")
 
+		self.make_sr_entries()
+
 	def on_cancel(self):
 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
 		super(SalesOrder, self).on_cancel()
@@ -620,7 +622,36 @@
 
 @frappe.whitelist()
 def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
+	from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+		get_stock_reservation_entries_for_voucher,
+		has_reserved_stock,
+	)
+
 	def set_missing_values(source, target):
+		if not target.items and has_reserved_stock("Sales Order", source_name):
+			sre_list = get_stock_reservation_entries_for_voucher("Sales Order", source_name)
+			sre_dict = {d.pop("voucher_detail_no"): d for d in sre_list}
+
+			for item in source.get("items"):
+				if item.name in sre_dict:
+					qty_to_deliver = (
+						sre_dict[item.name]["reserved_qty"] - sre_dict[item.name]["delivered_qty"]
+					) / item.conversion_factor
+
+					row = frappe.new_doc("Delivery Note Item")
+					row.against_sales_order = source.name
+					row.against_sre = sre_dict[item.name]["name"]
+					row.so_detail = item.name
+					row.item_code = item.item_code
+					row.item_name = item.item_name
+					row.description = item.description
+					row.qty = qty_to_deliver
+					row.stock_uom = item.stock_uom
+					row.uom = item.uom
+					row.conversion_factor = item.conversion_factor
+
+					target.append("items", row)
+
 		target.run_method("set_missing_values")
 		target.run_method("set_po_nos")
 		target.run_method("calculate_taxes_and_totals")
@@ -649,6 +680,9 @@
 				or item_group.get("buying_cost_center")
 			)
 
+	if has_reserved_stock("Sales Order", source_name):
+		skip_item_mapping = True
+
 	mapper = {
 		"Sales Order": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}},
 		"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
@@ -676,7 +710,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_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index d0dabad..be85d9a 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,28 @@
    "fieldname": "material_request_item",
    "fieldtype": "Data",
    "label": "Material Request Item"
+  },
+  {
+   "default": "1",
+   "depends_on": "eval: parent.reserve_stock",
+   "fieldname": "reserve_stock",
+   "fieldtype": "Check",
+   "label": "Reserve Stock"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: (parent.reserve_stock && doc.reserve_stock)",
+   "fieldname": "stock_reserved_qty",
+   "fieldtype": "Float",
+   "label": "Stock Reserved Qty (in Stock UOM)",
+   "no_copy": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-12-25 02:51:10.247569",
+ "modified": "2023-03-21 13:14:47.915610",
  "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..53b3576 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -77,6 +77,19 @@
 			}
 		});
 
+		frm.set_query("against_sre", "items", (doc, cdt, cdn) => {
+			var row  = locals[cdt][cdn];
+			return {
+				filters: {
+					"docstatus": 1,
+					"status": ["not in", ["Delivered", "Cancelled"]],
+					"voucher_type": "Sales Order",
+					"voucher_no": row.against_sales_order,
+					"voucher_detail_no": row.so_detail,
+				}
+			}
+		});
+
 		frm.set_df_property('packed_items', 'cannot_add_rows', true);
 		frm.set_df_property('packed_items', 'cannot_delete_rows', true);
 	},
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 9f9f5cb..77c435e 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -147,6 +147,8 @@
 
 		if not self.installation_status:
 			self.installation_status = "Not Installed"
+
+		self.validate_against_sre()
 		self.reset_default_field_value("set_warehouse", "items", "warehouse")
 
 	def validate_with_previous_doc(self):
@@ -239,6 +241,8 @@
 		self.update_prevdoc_status()
 		self.update_billing_status()
 
+		self.update_stock_reservation_entry()
+
 		if not self.is_return:
 			self.check_credit_limit()
 		elif self.issue_credit_note:
@@ -258,6 +262,8 @@
 		self.update_prevdoc_status()
 		self.update_billing_status()
 
+		self.update_stock_reservation_entry()
+
 		# Updating stock ledger should always be called after updating prevdoc status,
 		# because updating reserved qty in bin depends upon updated delivered qty in SO
 		self.update_stock_ledger()
@@ -268,6 +274,88 @@
 		self.repost_future_sle_and_gle()
 		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
 
+	def update_stock_reservation_entry(self):
+		if not self.is_return:
+			from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+				update_sre_delivered_qty,
+			)
+
+			for item in self.get("items"):
+				if item.against_sre:
+					update_sre_delivered_qty(item.doctype, item.against_sre)
+
+	def validate_against_sre(self):
+		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+			get_stock_reservation_entries_for_items,
+			has_reserved_stock,
+		)
+
+		sre_details = get_stock_reservation_entries_for_items(self.items)
+
+		for item in self.items:
+			if item.against_sre:
+				sre = sre_details[item.against_sre]
+
+				# SRE `docstatus` should be `1` (submitted)
+				if sre.docstatus == 0:
+					frappe.throw(
+						_("Row #{0}: Stock Reservation Entry {1} is not submitted").format(
+							item.idx, item.against_sre
+						)
+					)
+				elif sre.docstatus == 2:
+					frappe.throw(
+						_("Row #{0}: Stock Reservation Entry {0} is cancelled").format(item.idx, item.against_sre)
+					)
+
+				# SRE `status` should not be `Delivered`
+				if sre.status == "Delivered":
+					frappe.throw(
+						_("Row #{0}: Cannot deliver more against Stock Reservation Entry {1}").format(
+							item.idx, item.against_sre
+						)
+					)
+
+				for field in (
+					"item_code",
+					"warehouse",
+					("against_sales_order", "voucher_no"),
+					("so_detail", "voucher_detail_no"),
+				):
+					item_field = sre_field = None
+
+					if isinstance(field, tuple):
+						item_field, sre_field = field[0], field[1]
+					else:
+						item_field = sre_field = field
+
+					if item.get(item_field) != sre.get(sre_field):
+						frappe.throw(
+							_("Row #{0}: {1} {2} does not match with Stock Reservation Entry {3}").format(
+								item.idx,
+								frappe.get_meta(item.doctype).get_label(item_field),
+								item.get(item_field),
+								item.against_sre,
+							)
+						)
+
+				max_delivered_qty = (sre.reserved_qty - sre.delivered_qty) / item.conversion_factor
+				if item.qty > max_delivered_qty:
+					frappe.throw(
+						_("Row #{0}: Cannot deliver more than {1} {2} against Stock Reservation Entry {3}").format(
+							item.idx, max_delivered_qty, item.uom, item.against_sre
+						)
+					)
+			elif item.against_sales_order:
+				if not item.so_detail:
+					frappe.throw(_("Row #{0}: Sales Order Item reference is required").format(item.idx))
+				elif has_reserved_stock("Sales Order", item.against_sales_order, item.so_detail):
+					frappe.throw(
+						_("Row #{0}: Cannot deliver against Sales Order {1} without Stock Reservation Entry").format(
+							item.idx, item.against_sales_order
+						)
+					)
+
 	def check_credit_limit(self):
 		from erpnext.selling.doctype.customer.customer import check_credit_limit
 
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
index b6b5ff4..9c64c17 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
@@ -13,10 +13,14 @@
 			"Sales Order": ["items", "against_sales_order"],
 			"Material Request": ["items", "material_request"],
 			"Purchase Order": ["items", "purchase_order"],
+			"Stock Reservation Entry": ["items", "against_sre"],
 		},
 		"transactions": [
 			{"label": _("Related"), "items": ["Sales Invoice", "Packing Slip", "Delivery Trip"]},
-			{"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]},
+			{
+				"label": _("Reference"),
+				"items": ["Sales Order", "Shipment", "Quality Inspection", "Stock Reservation Entry"],
+			},
 			{"label": _("Returns"), "items": ["Stock Entry"]},
 			{"label": _("Subscription"), "items": ["Auto Repeat"]},
 			{"label": _("Internal Transfer"), "items": ["Material Request", "Purchase Order"]},
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 1763269..faa7748 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -76,6 +76,7 @@
   "si_detail",
   "dn_detail",
   "pick_list_item",
+  "against_sre",
   "section_break_40",
   "batch_no",
   "serial_no",
@@ -832,13 +833,22 @@
    "fieldname": "material_request_item",
    "fieldtype": "Data",
    "label": "Material Request Item"
+  },
+  {
+   "fieldname": "against_sre",
+   "fieldtype": "Link",
+   "label": "Against Stock Reservation Entry",
+   "no_copy": 1,
+   "options": "Stock Reservation Entry",
+   "print_hide": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-03-20 14:24:10.406746",
+ "modified": "2023-03-26 16:53:08.283469",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Delivery Note Item",
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..8b1bc43
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
@@ -0,0 +1,272 @@
+{
+ "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",
+  "posting_date",
+  "posting_time",
+  "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",
+  "project",
+  "column_break_jbyr",
+  "batch_no",
+  "serial_no",
+  "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": "posting_date",
+   "fieldtype": "Date",
+   "in_filter": 1,
+   "in_list_view": 1,
+   "label": "Posting Date",
+   "oldfieldname": "posting_date",
+   "oldfieldtype": "Date",
+   "print_width": "100px",
+   "read_only": 1,
+   "search_index": 1,
+   "width": "100px"
+  },
+  {
+   "fieldname": "posting_time",
+   "fieldtype": "Time",
+   "label": "Posting Time",
+   "oldfieldname": "posting_time",
+   "oldfieldtype": "Time",
+   "print_width": "100px",
+   "read_only": 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
+  },
+  {
+   "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"
+  },
+  {
+   "fieldname": "batch_no",
+   "fieldtype": "Data",
+   "label": "Batch No",
+   "read_only": 1
+  },
+  {
+   "fieldname": "serial_no",
+   "fieldtype": "Long Text",
+   "label": "Serial No",
+   "read_only": 1
+  }
+ ],
+ "hide_toolbar": 1,
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-03-24 16:22:08.859347",
+ "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..06e14da
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -0,0 +1,208 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.query_builder.functions import Sum
+
+from erpnext.utilities.transaction_base import TransactionBase
+
+
+class StockReservationEntry(TransactionBase):
+	def validate(self) -> None:
+		from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
+
+		self.validate_posting_time()
+		self.validate_mandatory()
+		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:
+		mandatory = [
+			"item_code",
+			"warehouse",
+			"posting_date",
+			"posting_time",
+			"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 update_status(self, status: str = None, update_modified: bool = True) -> None:
+		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:
+		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 get_stock_reservation_entries_for_voucher(
+	voucher_type: str, voucher_no: str, voucher_detail_no: str = None, fields: list[str] = None
+) -> list[dict]:
+	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 has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool:
+	if get_stock_reservation_entries_for_voucher(
+		voucher_type, voucher_no, voucher_detail_no, fields=["name"]
+	):
+		return True
+
+	return False
+
+
+def update_sre_delivered_qty(
+	doctype: str, sre_name: str, sre_field: str = "against_sre", qty_field: str = "stock_qty"
+) -> None:
+	table = frappe.qb.DocType(doctype)
+	delivered_qty = (
+		frappe.qb.from_(table)
+		.select(Sum(table[qty_field]))
+		.where((table.docstatus == 1) & (table[sre_field] == sre_name))
+	).run(as_list=True)[0][0] or 0.0
+
+	sre_doc = frappe.get_doc("Stock Reservation Entry", sre_name)
+	sre_doc.delivered_qty = delivered_qty
+	sre_doc.db_update()
+	sre_doc.update_status()
+
+
+def get_stock_reservation_entries_for_items(
+	items: list[dict | object], sre_field: str = "against_sre"
+) -> dict[dict]:
+	sre_details = {}
+
+	if items:
+		sre_list = [item.get(sre_field) for item in items if item.get(sre_field)]
+
+		if sre_list:
+			sre = frappe.qb.DocType("Stock Reservation Entry")
+			sre_data = (
+				frappe.qb.from_(sre)
+				.select(
+					sre.name,
+					sre.status,
+					sre.docstatus,
+					sre.item_code,
+					sre.warehouse,
+					sre.voucher_type,
+					sre.voucher_no,
+					sre.voucher_detail_no,
+					sre.reserved_qty,
+					sre.delivered_qty,
+					sre.stock_uom,
+				)
+				.where(sre.name.isin(sre_list))
+				.orderby(sre.creation)
+			).run(as_dict=True)
+
+			sre_details = {d.name: d for d in sre_data}
+
+	return sre_details
+
+
+def get_sre_reserved_qty_details(item_code_list: list, warehouse_list: list) -> dict:
+	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
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..e7b829e
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestStockReservationEntry(FrappeTestCase):
+	pass
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index ec7fb0f..02ea381 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,35 @@
   {
    "fieldname": "column_break_121",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "stock_reservation_tab",
+   "fieldtype": "Tab Break",
+   "label": "Stock Reservation"
+  },
+  {
+   "default": "1",
+   "fieldname": "enable_stock_reservation",
+   "fieldtype": "Check",
+   "label": "Enable Stock Reservation"
+  },
+  {
+   "default": "1",
+   "depends_on": "eval: doc.enable_stock_reservation",
+   "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 && doc.reserve_stock_on_sales_order_submission)",
+   "fieldname": "allow_partial_reservation",
+   "fieldtype": "Check",
+   "label": "Allow Partial Reservation"
   }
  ],
  "icon": "icon-cog",
@@ -346,7 +380,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2022-02-05 15:33:43.692736",
+ "modified": "2023-03-23 18:59:11.773360",
  "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..d761b66 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.cant_disable_stock_reservation()
 
 	def validate_warehouses(self):
 		warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
@@ -99,6 +100,19 @@
 		if self.stock_frozen_upto:
 			check_pending_reposting(self.stock_frozen_upto)
 
+	def cant_disable_stock_reservation(self):
+		if not self.enable_stock_reservation:
+			db_enable_stock_reservation = frappe.db.get_single_value(
+				"Stock Settings", "enable_stock_reservation"
+			)
+
+			if db_enable_stock_reservation and frappe.db.count("Stock Reservation Entry"):
+				frappe.throw(
+					_("As there are existing {0}, you can not change the value of {1}.").format(
+						frappe.bold("Stock Reservation Entries"), frappe.bold("Enable 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 0fc642e..b8d6b6c 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -58,6 +58,7 @@
 		return columns, []
 
 	iwb_map = get_item_warehouse_map(filters, sle)
+	sre_details = get_sre_reserved_qty_details(iwb_map)
 	item_map = get_item_details(items, sle, filters)
 	item_reorder_detail_map = get_item_reorder_details(item_map.keys())
 
@@ -88,6 +89,7 @@
 				"company": company,
 				"reorder_level": item_reorder_level,
 				"reorder_qty": item_reorder_qty,
+				"stock_reservation_qty": sre_details.get((item, warehouse), 0.0),
 			}
 			report_data.update(item_map[item])
 			report_data.update(qty_dict)
@@ -230,6 +232,13 @@
 				"convertible": "qty",
 			},
 			{
+				"label": _("Stock Reservation Qty"),
+				"fieldname": "stock_reservation_qty",
+				"fieldtype": "Float",
+				"width": 80,
+				"convertible": "qty",
+			},
+			{
 				"label": _("Company"),
 				"fieldname": "company",
 				"fieldtype": "Link",
@@ -388,6 +397,19 @@
 	return iwb_map
 
 
+def get_sre_reserved_qty_details(iwb_map: list) -> dict:
+	from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+		get_sre_reserved_qty_details as get_reserved_qty_details,
+	)
+
+	item_code_list, warehouse_list = [], []
+	for d in iwb_map:
+		item_code_list.append(d[1])
+		warehouse_list.append(d[2])
+
+	return get_reserved_qty_details(item_code_list, warehouse_list)
+
+
 def get_group_by_key(row, filters, inventory_dimension_fields) -> tuple:
 	group_by_key = [row.company, row.item_code, row.warehouse]
 
diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
index f477d8f..d3046d2 100644
--- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
+++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
@@ -20,6 +20,7 @@
 	include_uom = filters.get("include_uom")
 	columns = get_columns()
 	bin_list = get_bin_list(filters)
+	sre_details = get_sre_reserved_qty_details(bin_list)
 	item_map = get_item_map(filters.get("item_code"), include_uom)
 
 	warehouse_company = {}
@@ -75,6 +76,7 @@
 				bin.indented_qty,
 				bin.ordered_qty,
 				bin.reserved_qty,
+				sre_details.get((bin.item_code, bin.warehouse), 0.0),
 				bin.reserved_qty_for_production,
 				bin.reserved_qty_for_sub_contract,
 				reserved_qty_for_pos,
@@ -167,6 +169,13 @@
 			"convertible": "qty",
 		},
 		{
+			"label": _("Stock Reservation Qty"),
+			"fieldname": "stock_reservation_qty",
+			"fieldtype": "Float",
+			"width": 100,
+			"convertible": "qty",
+		},
+		{
 			"label": _("Reserved for Production"),
 			"fieldname": "reserved_qty_for_production",
 			"fieldtype": "Float",
@@ -264,6 +273,19 @@
 	return bin_list
 
 
+def get_sre_reserved_qty_details(bin_list: list) -> dict:
+	from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
+		get_sre_reserved_qty_details as get_reserved_qty_details,
+	)
+
+	item_code_list, warehouse_list = [], []
+	for bin in bin_list:
+		item_code_list.append(bin["item_code"])
+		warehouse_list.append(bin["warehouse"])
+
+	return get_reserved_qty_details(item_code_list, warehouse_list)
+
+
 def get_item_map(item_code, include_uom):
 	"""Optimization: get only the item doc and re_order_levels table"""