feat: added negative inventory validation and restrict to make backdated entry for serial nos
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 85624d5..b55574f 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -38,6 +38,7 @@
 		self.set_supplier_address()
 		self.validate_asset_return()
 		self.validate_auto_repeat_subscription_dates()
+		self.create_package_for_transfer()
 
 		if self.doctype == "Purchase Invoice":
 			self.validate_purchase_receipt_if_update_stock()
@@ -69,6 +70,36 @@
 			),
 		)
 
+	def create_package_for_transfer(self) -> None:
+		"""Create serial and batch package for Sourece Warehouse in case of inter transfer."""
+
+		if self.is_internal_transfer() and (
+			self.doctype == "Purchase Receipt" or (self.doctype == "Purchase Invoice" and self.update_stock)
+		):
+			field = "delivery_note_item" if self.doctype == "Purchase Receipt" else "sales_invoice_item"
+
+			doctype = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"
+
+			ids = [d.get(field) for d in self.get("items") if d.get(field)]
+			bundle_ids = {}
+			if ids:
+				for bundle in frappe.get_all(
+					doctype, filters={"name": ("in", ids)}, fields=["serial_and_batch_bundle", "name"]
+				):
+					bundle_ids[bundle.name] = bundle.serial_and_batch_bundle
+
+			if not bundle_ids:
+				return
+
+			for item in self.get("items"):
+				if item.get(field) and not item.serial_and_batch_bundle:
+					item.serial_and_batch_bundle = self.make_package_for_transfer(
+						bundle_ids.get(item.get(field)),
+						item.from_warehouse,
+						type_of_transaction="Outward",
+						do_not_submit=True,
+					)
+
 	def set_missing_values(self, for_validate=False):
 		super(BuyingController, self).set_missing_values(for_validate)
 
@@ -467,7 +498,11 @@
 						{
 							"actual_qty": flt(pr_qty),
 							"serial_no": cstr(d.serial_no).strip(),
-							"serial_and_batch_bundle": d.serial_and_batch_bundle,
+							"serial_and_batch_bundle": (
+								d.serial_and_batch_bundle
+								if not self.is_internal_transfer()
+								else self.get_package_for_target_warehouse(d)
+							),
 						},
 					)
 
@@ -494,7 +529,6 @@
 								"recalculate_rate": 1
 								if (self.is_subcontracted and (d.bom or d.fg_item)) or d.from_warehouse
 								else 0,
-								"serial_and_batch_bundle": d.serial_and_batch_bundle,
 							}
 						)
 					sl_entries.append(sle)
@@ -531,6 +565,15 @@
 			via_landed_cost_voucher=via_landed_cost_voucher,
 		)
 
+	def get_package_for_target_warehouse(self, item) -> str:
+		if not item.serial_and_batch_bundle:
+			return ""
+
+		return self.make_package_for_transfer(
+			item.serial_and_batch_bundle,
+			item.warehouse,
+		)
+
 	def update_ordered_and_reserved_qty(self):
 		po_map = {}
 		for d in self.get("items"):
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 15c84a9..1dd7209 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -531,6 +531,11 @@
 				if item_row.warehouse:
 					sle.dependant_sle_voucher_detail_no = item_row.name
 
+			if item_row.serial_and_batch_bundle:
+				sle["serial_and_batch_bundle"] = self.make_package_for_transfer(
+					item_row.serial_and_batch_bundle, item_row.target_warehouse
+				)
+
 		return sle
 
 	def set_po_nos(self, for_validate=False):
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 2e705ea..2048a42 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -372,6 +372,44 @@
 
 				row.db_set("serial_and_batch_bundle", None)
 
+	def make_package_for_transfer(
+		self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
+	):
+		bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
+
+		if not type_of_transaction:
+			type_of_transaction = "Inward"
+
+		bundle_doc = frappe.copy_doc(bundle_doc)
+		bundle_doc.warehouse = warehouse
+		bundle_doc.type_of_transaction = type_of_transaction
+		bundle_doc.voucher_type = self.doctype
+		bundle_doc.voucher_no = self.name
+		bundle_doc.is_cancelled = 0
+
+		for row in bundle_doc.ledgers:
+			row.is_outward = 0
+			row.qty = abs(row.qty)
+			row.stock_value_difference = abs(row.stock_value_difference)
+			if type_of_transaction == "Outward":
+				row.qty *= -1
+				row.stock_value_difference *= row.stock_value_difference
+				row.is_outward = 1
+
+			row.warehouse = warehouse
+
+		bundle_doc.set_total_qty()
+		bundle_doc.set_avg_rate()
+		bundle_doc.flags.ignore_permissions = True
+
+		if not do_not_submit:
+			bundle_doc.submit()
+		else:
+			bundle_doc.save(ignore_permissions=True)
+
+		print(bundle_doc.name)
+		return bundle_doc.name
+
 	def get_sl_entries(self, d, args):
 		sl_dict = frappe._dict(
 			{
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index 33b8955..f2d266a 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -69,7 +69,8 @@
 	def calculate_avg_rate_from_deprecarated_ledgers(self):
 		ledgers = self.get_sle_for_batches()
 		for ledger in ledgers:
-			self.batch_avg_rate[ledger.batch_no] += flt(ledger.incoming_rate) / flt(ledger.qty)
+			self.batch_avg_rate[ledger.batch_no] += flt(ledger.batch_value) / flt(ledger.batch_qty)
+			self.available_qty[ledger.batch_no] += flt(ledger.batch_qty)
 
 	def get_sle_for_batches(self):
 		batch_nos = list(self.batch_nos.keys())
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index a647a17..ce0684a 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -1044,8 +1044,6 @@
 				"field_map": {
 					source_document_warehouse_field: target_document_warehouse_field,
 					"name": "delivery_note_item",
-					"batch_no": "batch_no",
-					"serial_no": "serial_no",
 					"purchase_order": "purchase_order",
 					"purchase_order_item": "purchase_order_item",
 					"material_request": "material_request",
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index 98da0af..824691c 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -7,19 +7,24 @@
 import frappe
 from frappe import _, bold
 from frappe.model.document import Document
-from frappe.query_builder.functions import Sum
-from frappe.utils import cint, flt, today
+from frappe.query_builder.functions import CombineDatetime, Sum
+from frappe.utils import cint, flt, get_link_to_form, today
 from pypika import Case
 
 from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation
 
 
+class SerialNoExistsInFutureTransactionError(frappe.ValidationError):
+	pass
+
+
 class SerialandBatchBundle(Document):
 	def validate(self):
 		self.validate_serial_and_batch_no()
 		self.validate_duplicate_serial_and_batch_no()
 		# self.validate_voucher_no()
-		self.validate_serial_nos()
+		self.check_future_entries_exists()
+		self.validate_serial_nos_inventory()
 
 	def before_save(self):
 		self.set_total_qty()
@@ -31,6 +36,26 @@
 		if self.ledgers:
 			self.set_avg_rate()
 
+	def validate_serial_nos_inventory(self):
+		if not (self.has_serial_no and self.type_of_transaction == "Outward"):
+			return
+
+		serial_nos = [d.serial_no for d in self.ledgers if d.serial_no]
+		serial_no_warehouse = frappe._dict(
+			frappe.get_all(
+				"Serial No",
+				filters={"name": ("in", serial_nos)},
+				fields=["name", "warehouse"],
+				as_list=1,
+			)
+		)
+
+		for serial_no in serial_nos:
+			if serial_no_warehouse.get(serial_no) != self.warehouse:
+				frappe.throw(
+					_(f"Serial No {bold(serial_no)} is not present in the warehouse {bold(self.warehouse)}.")
+				)
+
 	def set_incoming_rate(self, row=None, save=False):
 		if self.type_of_transaction == "Outward":
 			self.set_incoming_rate_for_outward_transaction(row, save)
@@ -65,10 +90,14 @@
 			)
 
 		for d in self.ledgers:
+			available_qty = 0
 			if self.has_serial_no:
 				d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
 			else:
 				d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no))
+				available_qty = sn_obj.batch_available_qty.get(d.batch_no) + d.qty
+
+				self.validate_negative_batch(d.batch_no, available_qty)
 
 			if self.has_batch_no:
 				d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
@@ -78,6 +107,14 @@
 					{"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference}
 				)
 
+	def validate_negative_batch(self, batch_no, available_qty):
+		if available_qty < 0:
+			msg = f"""Batch No {bold(batch_no)} has negative stock
+				of quantity {bold(available_qty)} in the
+				warehouse {self.warehouse}"""
+
+			frappe.throw(_(msg))
+
 	def get_sle_for_outward_transaction(self, row):
 		return frappe._dict(
 			{
@@ -169,10 +206,54 @@
 				)
 			)
 
-	def validate_serial_nos(self):
+	def check_future_entries_exists(self):
 		if not self.has_serial_no:
 			return
 
+		serial_nos = [d.serial_no for d in self.ledgers if d.serial_no]
+
+		parent = frappe.qb.DocType("Serial and Batch Bundle")
+		child = frappe.qb.DocType("Serial and Batch Ledger")
+
+		timestamp_condition = CombineDatetime(
+			parent.posting_date, parent.posting_time
+		) > CombineDatetime(self.posting_date, self.posting_time)
+
+		future_entries = (
+			frappe.qb.from_(parent)
+			.inner_join(child)
+			.on(parent.name == child.parent)
+			.select(
+				child.serial_no,
+				parent.voucher_type,
+				parent.voucher_no,
+			)
+			.where(
+				(child.serial_no.isin(serial_nos))
+				& (child.parent != self.name)
+				& (parent.item_code == self.item_code)
+				& (parent.docstatus == 1)
+				& (parent.is_cancelled == 0)
+			)
+			.where(timestamp_condition)
+		).run(as_dict=True)
+
+		if future_entries:
+			msg = """The serial nos has been used in the future
+				transactions so you need to cancel them first.
+				The list of serial nos and their respective
+				transactions are as below."""
+
+			msg += "<br><br><ul>"
+
+			for d in future_entries:
+				msg += f"<li>{d.serial_no} in {get_link_to_form(d.voucher_type, d.voucher_no)}</li>"
+			msg += "</li></ul>"
+
+			title = "Serial No Exists In Future Transaction(s)"
+
+			frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransactionError)
+
 	def validate_quantity(self, row):
 		self.set_total_qty(save=True)
 
@@ -429,8 +510,19 @@
 	doc.posting_date = parent_doc.posting_date
 	doc.posting_time = parent_doc.posting_time
 	doc.set("ledgers", [])
-	doc.set("ledgers", ledgers)
-	doc.save()
+
+	for d in ledgers:
+		doc.append(
+			"ledgers",
+			{
+				"qty": 1 if doc.type_of_transaction == "Inward" else -1,
+				"warehouse": d.get("warehouse"),
+				"batch_no": d.get("batch_no"),
+				"serial_no": d.get("serial_no"),
+			},
+		)
+
+	doc.save(ignore_permissions=True)
 
 	frappe.msgprint(_("Serial and Batch Bundle updated"), alert=True)
 
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index a7f5b80..5e8aff3 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1219,8 +1219,16 @@
 				if cstr(d.s_warehouse) or (finished_item_row and d.name == finished_item_row.name):
 					sle.recalculate_rate = 1
 
-				if d.serial_and_batch_bundle and self.docstatus == 1:
-					d.serial_and_batch_bundle = self.copy_serial_and_batch_bundle(sle)
+				allowed_types = [
+					"Material Transfer",
+					"Send to Subcontractor",
+					"Material Transfer for Manufacture",
+				]
+
+				if self.purpose in allowed_types and d.serial_and_batch_bundle and self.docstatus == 1:
+					d.serial_and_batch_bundle = self.make_package_for_transfer(
+						d.serial_and_batch_bundle, d.t_warehouse
+					)
 
 				if d.serial_and_batch_bundle and self.docstatus == 2:
 					bundle_id = frappe.get_cached_value(
@@ -1239,36 +1247,6 @@
 
 				sl_entries.append(sle)
 
-	def copy_serial_and_batch_bundle(self, child):
-		allowed_types = [
-			"Material Transfer",
-			"Send to Subcontractor",
-			"Material Transfer for Manufacture",
-		]
-
-		if self.purpose in allowed_types:
-			bundle_doc = frappe.get_doc("Serial and Batch Bundle", child.serial_and_batch_bundle)
-
-			bundle_doc = frappe.copy_doc(bundle_doc)
-			bundle_doc.warehouse = child.t_warehouse
-			bundle_doc.type_of_transaction = "Inward"
-
-			for row in bundle_doc.ledgers:
-				if row.qty < 0:
-					row.qty = abs(row.qty)
-
-				if row.stock_value_difference < 0:
-					row.stock_value_difference = abs(row.stock_value_difference)
-
-				row.warehouse = child.t_warehouse
-				row.is_outward = 0
-
-			bundle_doc.set_total_qty()
-			bundle_doc.set_avg_rate()
-			bundle_doc.flags.ignore_permissions = True
-			bundle_doc.submit()
-			return bundle_doc.name
-
 	def get_gl_entries(self, warehouse_account):
 		gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
 
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index a4fac4d..2b88e8b 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -4,7 +4,7 @@
 import frappe
 from frappe import _, bold
 from frappe.model.naming import make_autoname
-from frappe.query_builder.functions import Sum
+from frappe.query_builder.functions import CombineDatetime, Sum
 from frappe.utils import cint, flt, now
 
 from erpnext.stock.deprecated_serial_batch import (
@@ -255,7 +255,7 @@
 		data = frappe.db.get_value(
 			"Serial and Batch Bundle",
 			self.sle.serial_and_batch_bundle,
-			["item_code", "warehouse", "voucher_no"],
+			["item_code", "warehouse", "voucher_no", "name"],
 			as_dict=1,
 		)
 
@@ -408,7 +408,7 @@
 				parent.name = child.parent
 				AND child.serial_no IN ({', '.join([frappe.db.escape(s) for s in serial_nos])})
 				AND child.is_outward = 0
-				AND parent.docstatus < 2
+				AND parent.docstatus = 1
 				AND parent.is_cancelled = 0
 				AND child.warehouse = {frappe.db.escape(self.sle.warehouse)}
 				AND parent.item_code = {frappe.db.escape(self.sle.item_code)}
@@ -511,8 +511,10 @@
 			ledgers = self.get_batch_no_ledgers()
 
 			self.batch_avg_rate = defaultdict(float)
+			self.available_qty = defaultdict(float)
 			for ledger in ledgers:
 				self.batch_avg_rate[ledger.batch_no] += flt(ledger.incoming_rate) / flt(ledger.qty)
+				self.available_qty[ledger.batch_no] += flt(ledger.qty)
 
 			self.calculate_avg_rate_from_deprecarated_ledgers()
 			self.set_stock_value_difference()
@@ -523,6 +525,10 @@
 
 		batch_nos = list(self.batch_nos.keys())
 
+		timestamp_condition = CombineDatetime(
+			parent.posting_date, parent.posting_time
+		) < CombineDatetime(self.sle.posting_date, self.sle.posting_time)
+
 		return (
 			frappe.qb.from_(parent)
 			.inner_join(child)
@@ -537,8 +543,10 @@
 				& (child.parent != self.sle.serial_and_batch_bundle)
 				& (parent.warehouse == self.sle.warehouse)
 				& (parent.item_code == self.sle.item_code)
+				& (parent.docstatus == 1)
 				& (parent.is_cancelled == 0)
 			)
+			.where(timestamp_condition)
 			.groupby(child.batch_no)
 		).run(as_dict=True)