feat: auto create serial and batch bundle
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index 2943500..0b7ea24 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -237,10 +237,6 @@
 	item_list = args.get("items")
 	args.pop("items")
 
-	set_serial_nos_based_on_fifo = frappe.db.get_single_value(
-		"Stock Settings", "automatically_set_serial_nos_based_on_fifo"
-	)
-
 	item_code_list = tuple(item.get("item_code") for item in item_list)
 	query_items = frappe.get_all(
 		"Item",
@@ -258,28 +254,9 @@
 		data = get_pricing_rule_for_item(args_copy, doc=doc)
 		out.append(data)
 
-		if (
-			serialized_items.get(item.get("item_code"))
-			and not item.get("serial_no")
-			and set_serial_nos_based_on_fifo
-			and not args.get("is_return")
-		):
-			out[0].update(get_serial_no_for_item(args_copy))
-
 	return out
 
 
-def get_serial_no_for_item(args):
-	from erpnext.stock.get_item_details import get_serial_no
-
-	item_details = frappe._dict(
-		{"doctype": args.doctype, "name": args.name, "serial_no": args.serial_no}
-	)
-	if args.get("parenttype") in ("Sales Invoice", "Delivery Note") and flt(args.stock_qty) > 0:
-		item_details.serial_no = get_serial_no(args)
-	return item_details
-
-
 def update_pricing_rule_uom(pricing_rule, args):
 	child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get(
 		pricing_rule.apply_on
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 69e0cf2..e603709 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -36,7 +36,6 @@
 from erpnext.controllers.selling_controller import SellingController
 from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
 from erpnext.setup.doctype.company.company import update_company_current_month_sales
-from erpnext.stock.doctype.batch.batch import set_batch_nos
 from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
 from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
 
@@ -125,9 +124,6 @@
 		if not self.is_opening:
 			self.is_opening = "No"
 
-		if self._action != "submit" and self.update_stock and not self.is_return:
-			set_batch_nos(self, "warehouse", True)
-
 		if self.redeem_loyalty_points:
 			lp = frappe.get_doc("Loyalty Program", self.loyalty_program)
 			self.loyalty_redemption_account = (
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 8b9e0aa..d776b79 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -372,6 +372,16 @@
 
 				row.db_set("serial_and_batch_bundle", None)
 
+	def set_serial_and_batch_bundle(self, table_name=None):
+		if not table_name:
+			table_name = "items"
+
+		for row in self.get(table_name):
+			if row.get("serial_and_batch_bundle"):
+				frappe.get_doc(
+					"Serial and Batch Bundle", row.serial_and_batch_bundle
+				).set_serial_and_batch_values(self, row)
+
 	def make_package_for_transfer(
 		self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
 	):
@@ -749,16 +759,6 @@
 					message = self.prepare_over_receipt_message(rule, values)
 					frappe.throw(msg=message, title=_("Over Receipt"))
 
-	def set_serial_and_batch_bundle(self, table_name=None):
-		if not table_name:
-			table_name = "items"
-
-		for row in self.get(table_name):
-			if row.get("serial_and_batch_bundle"):
-				frappe.get_doc(
-					"Serial and Batch Bundle", row.serial_and_batch_bundle
-				).set_serial_and_batch_values(self, row)
-
 	def prepare_over_receipt_message(self, rule, values):
 		message = _(
 			"{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}."
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 2ee197b..b607244 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -196,48 +196,6 @@
 		refresh_field("incentives",row.name,row.parentfield);
 	}
 
-	warehouse(doc, cdt, cdn) {
-		var me = this;
-		var item = frappe.get_doc(cdt, cdn);
-
-		// check if serial nos entered are as much as qty in row
-		if (item.serial_no) {
-			let serial_nos = item.serial_no.split(`\n`).filter(sn => sn.trim()); // filter out whitespaces
-			if (item.qty === serial_nos.length) return;
-		}
-
-		if (item.serial_no && !item.batch_no) {
-			item.serial_no = null;
-		}
-
-		var has_batch_no;
-		frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => {
-			has_batch_no = r && r.has_batch_no;
-			if(item.item_code && item.warehouse) {
-				return this.frm.call({
-					method: "erpnext.stock.get_item_details.get_bin_details_and_serial_nos",
-					child: item,
-					args: {
-						item_code: item.item_code,
-						warehouse: item.warehouse,
-						has_batch_no: has_batch_no || 0,
-						stock_qty: item.stock_qty,
-						serial_no: item.serial_no || "",
-					},
-					callback:function(r){
-						if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
-							if (doc.doctype === 'Sales Invoice' && (!doc.update_stock)) return;
-							if (has_batch_no) {
-								me.set_batch_number(cdt, cdn);
-								me.batch_no(doc, cdt, cdn);
-							}
-						}
-					}
-				});
-			}
-		})
-	}
-
 	toggle_editable_price_list_rate() {
 		var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name);
 		var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate"));
@@ -298,36 +256,6 @@
 		}
 	}
 
-	batch_no(doc, cdt, cdn) {
-		super.batch_no(doc, cdt, cdn);
-
-		var item = frappe.get_doc(cdt, cdn);
-
-		if (item.serial_no) {
-			return;
-		}
-
-		item.serial_no = null;
-		var has_serial_no;
-		frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_serial_no', (r) => {
-			has_serial_no = r && r.has_serial_no;
-			if(item.warehouse && item.item_code && item.batch_no) {
-				return this.frm.call({
-					method: "erpnext.stock.get_item_details.get_batch_qty_and_serial_no",
-					child: item,
-					args: {
-						"batch_no": item.batch_no,
-						"stock_qty": item.stock_qty || item.qty, //if stock_qty field is not available fetch qty (in case of Packed Items table)
-						"warehouse": item.warehouse,
-						"item_code": item.item_code,
-						"has_serial_no": has_serial_no
-					},
-					"fieldname": "actual_batch_qty"
-				});
-			}
-		})
-	}
-
 	set_dynamic_labels() {
 		super.set_dynamic_labels();
 		this.set_product_bundle_help(this.frm.doc);
@@ -388,38 +316,6 @@
 		}
 	}
 
-	/* Determine appropriate batch number and set it in the form.
-	* @param {string} cdt - Document Doctype
-	* @param {string} cdn - Document name
-	*/
-	set_batch_number(cdt, cdn) {
-		const doc = frappe.get_doc(cdt, cdn);
-		if (doc && doc.has_batch_no && doc.warehouse) {
-			this._set_batch_number(doc);
-		}
-	}
-
-	_set_batch_number(doc) {
-		if (doc.batch_no) {
-			return
-		}
-
-		let args = {'item_code': doc.item_code, 'warehouse': doc.warehouse, 'qty': flt(doc.qty) * flt(doc.conversion_factor)};
-		if (doc.has_serial_no && doc.serial_no) {
-			args['serial_no'] = doc.serial_no
-		}
-
-		return frappe.call({
-			method: 'erpnext.stock.doctype.batch.batch.get_batch_no',
-			args: args,
-			callback: function(r) {
-				if(r.message) {
-					frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
-				}
-			}
-		});
-	}
-
 	pick_serial_and_batch(doc, cdt, cdn) {
 		let item = locals[cdt][cdn];
 		let me = this;
diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py
index eed8f73..756409b 100644
--- a/erpnext/setup/setup_wizard/operations/defaults_setup.py
+++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py
@@ -36,7 +36,6 @@
 	stock_settings.stock_uom = _("Nos")
 	stock_settings.auto_indent = 1
 	stock_settings.auto_insert_price_list_rate_if_missing = 1
-	stock_settings.automatically_set_serial_nos_based_on_fifo = 1
 	stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
 	stock_settings.save()
 
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 6bc1771..8e61fe2 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -486,7 +486,6 @@
 	stock_settings.stock_uom = _("Nos")
 	stock_settings.auto_indent = 1
 	stock_settings.auto_insert_price_list_rate_if_missing = 1
-	stock_settings.automatically_set_serial_nos_based_on_fifo = 1
 	stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
 	stock_settings.save()
 
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 35d862b..a9df1e8 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -2,6 +2,8 @@
 # License: GNU General Public License v3. See license.txt
 
 
+from collections import defaultdict
+
 import frappe
 from frappe import _
 from frappe.model.document import Document
@@ -257,54 +259,6 @@
 	return batch.name
 
 
-def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"):
-	"""Automatically select `batch_no` for outgoing items in item table"""
-	for d in doc.get(child_table):
-		qty = d.get("stock_qty") or d.get("transfer_qty") or d.get("qty") or 0
-		warehouse = d.get(warehouse_field, None)
-		if warehouse and qty > 0 and frappe.db.get_value("Item", d.item_code, "has_batch_no"):
-			if not d.batch_no:
-				pass
-			else:
-				batch_qty = get_batch_qty(batch_no=d.batch_no, warehouse=warehouse)
-				if flt(batch_qty, d.precision("qty")) < flt(qty, d.precision("qty")):
-					frappe.throw(
-						_(
-							"Row #{0}: The batch {1} has only {2} qty. Please select another batch which has {3} qty available or split the row into multiple rows, to deliver/issue from multiple batches"
-						).format(d.idx, d.batch_no, batch_qty, qty)
-					)
-
-
-@frappe.whitelist()
-def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
-	"""
-	Get batch number using First Expiring First Out method.
-	:param item_code: `item_code` of Item Document
-	:param warehouse: name of Warehouse to check
-	:param qty: quantity of Items
-	:return: String represent batch number of batch with sufficient quantity else an empty String
-	"""
-
-	batch_no = None
-	batches = get_batches(item_code, warehouse, qty, throw, serial_no)
-
-	for batch in batches:
-		if flt(qty) <= flt(batch.qty):
-			batch_no = batch.batch_id
-			break
-
-	if not batch_no:
-		frappe.msgprint(
-			_(
-				"Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement"
-			).format(frappe.bold(item_code))
-		)
-		if throw:
-			raise UnableToSelectBatchError
-
-	return batch_no
-
-
 def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
 	from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 
@@ -398,3 +352,17 @@
 
 	flt_reserved_batch_qty = flt(reserved_batch_qty[0][0])
 	return flt_reserved_batch_qty
+
+
+def get_available_batches(kwargs):
+	from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+		get_auto_batch_nos,
+	)
+
+	batchwise_qty = defaultdict(float)
+
+	batches = get_auto_batch_nos(kwargs)
+	for batch in batches:
+		batchwise_qty[batch.get("batch_no")] += batch.get("qty")
+
+	return batchwise_qty
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index ce0684a..ea20a26 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -137,6 +137,7 @@
 		self.validate_uom_is_integer("stock_uom", "stock_qty")
 		self.validate_uom_is_integer("uom", "qty")
 		self.validate_with_previous_doc()
+		self.set_serial_and_batch_bundle_from_pick_list()
 
 		from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
 
@@ -187,6 +188,24 @@
 				]
 			)
 
+	def set_serial_and_batch_bundle_from_pick_list(self):
+		if not self.pick_list:
+			return
+
+		for item in self.items:
+			if item.pick_list_item:
+				filters = {
+					"item_code": item.item_code,
+					"voucher_type": "Pick List",
+					"voucher_no": self.pick_list,
+					"voucher_detail_no": item.pick_list_item,
+				}
+
+				bundle_id = frappe.db.get_value("Serial and Batch Bundle", filters, "name")
+
+				if bundle_id:
+					item.serial_and_batch_bundle = bundle_id
+
 	def validate_proj_cust(self):
 		"""check for does customer belong to same project as entered.."""
 		if self.project and self.customer:
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index a9a9a1d..1ffc4ca 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -12,14 +12,18 @@
 from frappe.model.mapper import map_child_doc
 from frappe.query_builder import Case
 from frappe.query_builder.custom import GROUP_CONCAT
-from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum
-from frappe.utils import cint, floor, flt, today
+from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
+from frappe.utils import cint, floor, flt
 from frappe.utils.nestedset import get_descendants_of
 
 from erpnext.selling.doctype.sales_order.sales_order import (
 	make_delivery_note as create_delivery_note_from_sales_order,
 )
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+	get_auto_batch_nos,
+)
 from erpnext.stock.get_item_details import get_conversion_factor
+from erpnext.stock.serial_batch_bundle import SerialBatchCreation
 
 # TODO: Prioritize SO or WO group warehouse
 
@@ -79,6 +83,7 @@
 				)
 
 	def on_submit(self):
+		self.validate_serial_and_batch_bundle()
 		self.update_status()
 		self.update_bundle_picked_qty()
 		self.update_reference_qty()
@@ -90,7 +95,29 @@
 		self.update_reference_qty()
 		self.update_sales_order_picking_status()
 
-	def update_status(self, status=None):
+	def on_update(self):
+		self.linked_serial_and_batch_bundle()
+
+	def linked_serial_and_batch_bundle(self):
+		for row in self.locations:
+			if row.serial_and_batch_bundle:
+				frappe.get_doc(
+					"Serial and Batch Bundle", row.serial_and_batch_bundle
+				).set_serial_and_batch_values(self, row)
+
+	def remove_serial_and_batch_bundle(self):
+		for row in self.locations:
+			if row.serial_and_batch_bundle:
+				frappe.delete_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
+
+	def validate_serial_and_batch_bundle(self):
+		for row in self.locations:
+			if row.serial_and_batch_bundle:
+				doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
+				if doc.docstatus == 0:
+					doc.submit()
+
+	def update_status(self, status=None, update_modified=True):
 		if not status:
 			if self.docstatus == 0:
 				status = "Draft"
@@ -192,6 +219,7 @@
 		locations_replica = self.get("locations")
 
 		# reset
+		self.remove_serial_and_batch_bundle()
 		self.delete_key("locations")
 		updated_locations = frappe._dict()
 		for item_doc in items:
@@ -476,18 +504,13 @@
 			if not stock_qty:
 				break
 
-		serial_nos = None
-		if item_location.serial_no:
-			serial_nos = "\n".join(item_location.serial_no[0 : cint(stock_qty)])
-
 		locations.append(
 			frappe._dict(
 				{
 					"qty": qty,
 					"stock_qty": stock_qty,
 					"warehouse": item_location.warehouse,
-					"serial_no": serial_nos,
-					"batch_no": item_location.batch_no,
+					"serial_and_batch_bundle": item_location.serial_and_batch_bundle,
 				}
 			)
 		)
@@ -553,23 +576,6 @@
 
 	if picked_item_details:
 		for location in list(locations):
-			key = (
-				(location["warehouse"], location["batch_no"])
-				if location.get("batch_no")
-				else location["warehouse"]
-			)
-
-			if key in picked_item_details:
-				picked_detail = picked_item_details[key]
-
-				if picked_detail.get("serial_no") and location.get("serial_no"):
-					location["serial_no"] = list(
-						set(location["serial_no"]).difference(set(picked_detail["serial_no"]))
-					)
-					location["qty"] = len(location["serial_no"])
-				else:
-					location["qty"] -= picked_detail.get("picked_qty")
-
 			if location["qty"] < 1:
 				locations.remove(location)
 
@@ -620,31 +626,50 @@
 def get_available_item_locations_for_batched_item(
 	item_code, from_warehouses, required_qty, company, total_picked_qty=0
 ):
-	sle = frappe.qb.DocType("Stock Ledger Entry")
-	batch = frappe.qb.DocType("Batch")
-
-	query = (
-		frappe.qb.from_(sle)
-		.from_(batch)
-		.select(sle.warehouse, sle.batch_no, Sum(sle.actual_qty).as_("qty"))
-		.where(
-			(sle.batch_no == batch.name)
-			& (sle.item_code == item_code)
-			& (sle.company == company)
-			& (batch.disabled == 0)
-			& (sle.is_cancelled == 0)
-			& (IfNull(batch.expiry_date, "2200-01-01") > today())
+	locations = []
+	data = get_auto_batch_nos(
+		frappe._dict(
+			{
+				"item_code": item_code,
+				"warehouse": from_warehouses,
+				"qty": required_qty + total_picked_qty,
+			}
 		)
-		.groupby(sle.warehouse, sle.batch_no, sle.item_code)
-		.having(Sum(sle.actual_qty) > 0)
-		.orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse)
-		.limit(cint(required_qty + total_picked_qty))
 	)
 
-	if from_warehouses:
-		query = query.where(sle.warehouse.isin(from_warehouses))
+	warehouse_wise_batches = frappe._dict()
+	for d in data:
+		if d.warehouse not in warehouse_wise_batches:
+			warehouse_wise_batches.setdefault(d.warehouse, defaultdict(float))
 
-	return query.run(as_dict=True)
+		warehouse_wise_batches[d.warehouse][d.batch_no] += d.qty
+
+	for warehouse, batches in warehouse_wise_batches.items():
+		qty = sum(batches.values())
+
+		bundle_doc = SerialBatchCreation(
+			{
+				"item_code": item_code,
+				"warehouse": warehouse,
+				"voucher_type": "Pick List",
+				"total_qty": qty,
+				"batches": batches,
+				"type_of_transaction": "Outward",
+				"company": company,
+				"do_not_submit": True,
+			}
+		).make_serial_and_batch_bundle()
+
+		locations.append(
+			{
+				"qty": qty,
+				"warehouse": warehouse,
+				"item_code": item_code,
+				"serial_and_batch_bundle": bundle_doc.name,
+			}
+		)
+
+	return locations
 
 
 def get_available_item_locations_for_serial_and_batched_item(
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 c4f240a..80cbf02 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
@@ -10,7 +10,6 @@
 from frappe.model.document import Document
 from frappe.query_builder.functions import CombineDatetime, Sum
 from frappe.utils import add_days, cint, flt, get_link_to_form, today
-from pypika import Case
 
 from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
 
@@ -24,8 +23,6 @@
 		self.validate_serial_and_batch_no()
 		self.validate_duplicate_serial_and_batch_no()
 		self.validate_voucher_no()
-
-	def before_save(self):
 		if self.type_of_transaction == "Maintenance":
 			return
 
@@ -168,13 +165,16 @@
 		if not self.voucher_no or self.voucher_no != row.parent:
 			values_to_set["voucher_no"] = row.parent
 
+		if self.voucher_type != parent.doctype:
+			values_to_set["voucher_type"] = parent.doctype
+
 		if not self.voucher_detail_no or self.voucher_detail_no != row.name:
 			values_to_set["voucher_detail_no"] = row.name
 
 		if parent.get("posting_date") and (
 			not self.posting_date or self.posting_date != parent.posting_date
 		):
-			values_to_set["posting_date"] = parent.posting_date
+			values_to_set["posting_date"] = parent.posting_date or today()
 
 		if parent.get("posting_time") and (
 			not self.posting_time or self.posting_time != parent.posting_time
@@ -222,6 +222,9 @@
 		if self.voucher_no and not frappe.db.exists(self.voucher_type, self.voucher_no):
 			frappe.throw(_(f"The {self.voucher_type} # {self.voucher_no} does not exist"))
 
+		if frappe.get_cached_value(self.voucher_type, self.voucher_no, "docstatus") != 1:
+			frappe.throw(_(f"The {self.voucher_type} # {self.voucher_no} should be submit first."))
+
 	def check_future_entries_exists(self):
 		if not self.has_serial_no:
 			return
@@ -681,73 +684,43 @@
 
 	batches = []
 
-	reserved_batches = get_reserved_batches_for_pos(kwargs)
-	if reserved_batches:
-		remove_batches_reserved_for_pos(available_batches, reserved_batches)
+	stock_ledgers_batches = get_stock_ledgers_batches(kwargs)
+	if stock_ledgers_batches:
+		update_available_batches(available_batches, stock_ledgers_batches)
+
+	if not qty:
+		return batches
 
 	for batch in available_batches:
 		if qty > 0:
 			batch_qty = flt(batch.qty)
 			if qty > batch_qty:
 				batches.append(
-					{
-						"batch_no": batch.batch_no,
-						"qty": batch_qty,
-					}
+					frappe._dict(
+						{
+							"batch_no": batch.batch_no,
+							"qty": batch_qty,
+							"warehouse": batch.warehouse,
+						}
+					)
 				)
 				qty -= batch_qty
 			else:
 				batches.append(
-					{
-						"batch_no": batch.batch_no,
-						"qty": qty,
-					}
+					frappe._dict(
+						{
+							"batch_no": batch.batch_no,
+							"qty": qty,
+							"warehouse": batch.warehouse,
+						}
+					)
 				)
 				qty = 0
 
 	return batches
 
 
-def get_reserved_batches_for_pos(kwargs):
-	reserved_batches = defaultdict(float)
-
-	pos_invoices = frappe.get_all(
-		"POS Invoice",
-		fields=[
-			"`tabPOS Invoice Item`.batch_no",
-			"`tabPOS Invoice Item`.qty",
-			"`tabPOS Invoice Item`.serial_and_batch_bundle",
-		],
-		filters=[
-			["POS Invoice", "consolidated_invoice", "is", "not set"],
-			["POS Invoice", "docstatus", "=", 1],
-			["POS Invoice Item", "item_code", "=", kwargs.item_code],
-		],
-	)
-
-	ids = [
-		pos_invoice.serial_and_batch_bundle
-		for pos_invoice in pos_invoices
-		if pos_invoice.serial_and_batch_bundle
-	]
-
-	for d in get_serial_batch_ledgers(ids, docstatus=1, name=ids):
-		if not d.batch_no:
-			continue
-
-		reserved_batches[d.batch_no] += flt(d.qty)
-
-	# Will be deprecated in v16
-	for pos_invoice in pos_invoices:
-		if not pos_invoice.batch_no:
-			continue
-
-		reserved_batches[pos_invoice.batch_no] += flt(pos_invoice.qty)
-
-	return reserved_batches
-
-
-def remove_batches_reserved_for_pos(available_batches, reserved_batches):
+def update_available_batches(available_batches, reserved_batches):
 	for batch in available_batches:
 		if batch.batch_no in reserved_batches:
 			available_batches[batch.batch_no] -= reserved_batches[batch.batch_no]
@@ -766,16 +739,28 @@
 		.on(batch_ledger.batch_no == batch_table.name)
 		.select(
 			batch_ledger.batch_no,
+			batch_ledger.warehouse,
 			Sum(batch_ledger.qty).as_("qty"),
 		)
-		.where(
-			(stock_ledger_entry.item_code == kwargs.item_code)
-			& (stock_ledger_entry.warehouse == kwargs.warehouse)
-			& ((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
-		)
+		.where(((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull())))
 		.groupby(batch_ledger.batch_no)
 	)
 
+	for field in ["warehouse", "item_code"]:
+		if not kwargs.get(field):
+			continue
+
+		if isinstance(kwargs.get(field), list):
+			query = query.where(stock_ledger_entry[field].isin(kwargs.get(field)))
+		else:
+			query = query.where(stock_ledger_entry[field] == kwargs.get(field))
+
+	if kwargs.get("batch_no"):
+		if isinstance(kwargs.batch_no, list):
+			query = query.where(batch_ledger.name.isin(kwargs.batch_no))
+		else:
+			query = query.where(batch_ledger.name == kwargs.batch_no)
+
 	if kwargs.based_on == "LIFO":
 		query = query.orderby(batch_table.creation, order=frappe.qb.desc)
 	elif kwargs.based_on == "Expiry":
@@ -789,6 +774,7 @@
 	return data
 
 
+# For work order and subcontracting
 def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> Dict[str, Dict]:
 	data = get_ledgers_from_serial_batch_bundle(**kwargs)
 	if not data:
@@ -878,42 +864,34 @@
 	return frappe.get_all("Serial No", filters=filters, fields=fields)
 
 
-def get_available_batch_nos(item_code, warehouse):
-	sl_entries = get_stock_ledger_entries(item_code, warehouse)
-	batchwise_qty = defaultdict(float)
-
-	precision = frappe.get_precision("Stock Ledger Entry", "qty")
-	for entry in sl_entries:
-		batchwise_qty[entry.batch_no] += flt(entry.qty, precision)
-
-	return batchwise_qty
-
-
-def get_stock_ledger_entries(item_code, warehouse):
+def get_stock_ledgers_batches(kwargs):
 	stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
-	batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
 
-	return (
+	query = (
 		frappe.qb.from_(stock_ledger_entry)
-		.left_join(batch_ledger)
-		.on(stock_ledger_entry.serial_and_batch_bundle == batch_ledger.parent)
 		.select(
 			stock_ledger_entry.warehouse,
 			stock_ledger_entry.item_code,
-			Sum(
-				Case()
-				.when(stock_ledger_entry.serial_and_batch_bundle, batch_ledger.qty)
-				.else_(stock_ledger_entry.actual_qty)
-				.as_("qty")
-			),
-			Case()
-			.when(stock_ledger_entry.serial_and_batch_bundle, batch_ledger.batch_no)
-			.else_(stock_ledger_entry.batch_no)
-			.as_("batch_no"),
+			Sum(stock_ledger_entry.actual_qty).as_("qty"),
+			stock_ledger_entry.batch_no,
 		)
-		.where(
-			(stock_ledger_entry.item_code == item_code)
-			& (stock_ledger_entry.warehouse == warehouse)
-			& (stock_ledger_entry.is_cancelled == 0)
-		)
-	).run(as_dict=True)
+		.where((stock_ledger_entry.is_cancelled == 0))
+		.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
+	)
+
+	for field in ["warehouse", "item_code"]:
+		if not kwargs.get(field):
+			continue
+
+		if isinstance(kwargs.get(field), list):
+			query = query.where(stock_ledger_entry[field].isin(kwargs.get(field)))
+		else:
+			query = query.where(stock_ledger_entry[field] == kwargs.get(field))
+
+	data = query.run(as_dict=True)
+
+	batches = defaultdict(float)
+	for d in data:
+		batches[d.batch_no] += d.qty
+
+	return batches
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 5b4f41e..03c40eb 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -322,3 +322,16 @@
 
 	serial_numbers = query.run(as_dict=True)
 	return serial_numbers
+
+
+def get_serial_nos_for_outward(kwargs):
+	from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+		get_auto_serial_nos,
+	)
+
+	serial_nos = get_auto_serial_nos(kwargs)
+
+	if not serial_nos:
+		return []
+
+	return [d.serial_no for d in serial_nos]
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 6b0e5ae..8ba8d11 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -28,7 +28,7 @@
 from erpnext.manufacturing.doctype.bom.bom import add_additional_cost, validate_bom_no
 from erpnext.setup.doctype.brand.brand import get_brand_defaults
 from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
-from erpnext.stock.doctype.batch.batch import get_batch_no, get_batch_qty, set_batch_nos
+from erpnext.stock.doctype.batch.batch import get_batch_qty
 from erpnext.stock.doctype.item.item import get_item_defaults
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
@@ -39,7 +39,11 @@
 	get_conversion_factor,
 	get_default_cost_center,
 )
-from erpnext.stock.serial_batch_bundle import get_empty_batches_based_work_order
+from erpnext.stock.serial_batch_bundle import (
+	SerialBatchCreation,
+	get_empty_batches_based_work_order,
+	get_serial_or_batch_items,
+)
 from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get_valuation_rate
 from erpnext.stock.utils import get_bin, get_incoming_rate
 
@@ -143,9 +147,6 @@
 		if not self.from_bom:
 			self.fg_completed_qty = 0.0
 
-		if self._action != "submit":
-			set_batch_nos(self, "s_warehouse")
-
 		self.validate_serialized_batch()
 		self.set_actual_qty()
 		self.calculate_rate_and_amount()
@@ -242,6 +243,9 @@
 		if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
 			self.set_material_request_transfer_status("In Transit")
 
+	def before_save(self):
+		self.make_serial_and_batch_bundle_for_outward()
+
 	def on_update(self):
 		self.set_serial_and_batch_bundle()
 
@@ -894,6 +898,30 @@
 
 					serial_nos.append(sn)
 
+	def make_serial_and_batch_bundle_for_outward(self):
+		serial_or_batch_items = get_serial_or_batch_items(self.items)
+
+		for row in self.items:
+			if row.serial_and_batch_bundle or row.item_code not in serial_or_batch_items:
+				continue
+
+			bundle_doc = SerialBatchCreation(
+				{
+					"item_code": row.item_code,
+					"warehouse": row.s_warehouse,
+					"posting_date": self.posting_date,
+					"posting_time": self.posting_time,
+					"voucher_type": self.doctype,
+					"voucher_detail_no": row.name,
+					"total_qty": row.qty,
+					"type_of_transaction": "Outward",
+					"company": self.company,
+					"do_not_submit": True,
+				}
+			).make_serial_and_batch_bundle()
+
+			row.serial_and_batch_bundle = bundle_doc.name
+
 	def validate_subcontract_order(self):
 		"""Throw exception if more raw material is transferred against Subcontract Order than in
 		the raw materials supplied table"""
@@ -1445,15 +1473,6 @@
 		stock_and_rate = get_warehouse_details(args) if args.get("warehouse") else {}
 		ret.update(stock_and_rate)
 
-		# automatically select batch for outgoing item
-		if (
-			args.get("s_warehouse", None)
-			and args.get("qty")
-			and ret.get("has_batch_no")
-			and not args.get("batch_no")
-		):
-			args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"])
-
 		if (
 			self.purpose == "Send to Subcontractor"
 			and self.get(self.subcontract_data.order_field)
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 7b3d7f4..35d7661 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -8,7 +8,7 @@
 from frappe import _
 from frappe.core.doctype.role.role import get_users
 from frappe.model.document import Document
-from frappe.utils import add_days, cint, formatdate, get_datetime, get_link_to_form, getdate
+from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
 
 from erpnext.accounts.utils import get_fiscal_year
 from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@@ -51,7 +51,6 @@
 
 	def on_submit(self):
 		self.check_stock_frozen_date()
-		self.calculate_batch_qty()
 
 		if not self.get("via_landed_cost_voucher"):
 			SerialBatchBundle(
@@ -63,18 +62,6 @@
 
 		self.validate_serial_batch_no_bundle()
 
-	def calculate_batch_qty(self):
-		if self.batch_no:
-			batch_qty = (
-				frappe.db.get_value(
-					"Stock Ledger Entry",
-					{"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0},
-					"sum(actual_qty)",
-				)
-				or 0
-			)
-			frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
-
 	def validate_mandatory(self):
 		mandatory = ["warehouse", "posting_date", "voucher_type", "voucher_no", "company"]
 		for k in mandatory:
@@ -123,12 +110,15 @@
 				)
 
 				if bundle_data.docstatus != 1:
-					link = get_link_to_form("Serial and Batch Bundle", self.serial_and_batch_bundle)
-					frappe.throw(_(f"Serial and Batch Bundle {link} should be submitted first"))
+					self.submit_serial_and_batch_bundle()
 
 		if self.serial_and_batch_bundle and not (item_detail.has_serial_no or item_detail.has_batch_no):
 			frappe.throw(_(f"Serial No and Batch No are not allowed for Item {self.item_code}"))
 
+	def submit_serial_and_batch_bundle(self):
+		doc = frappe.get_doc("Serial and Batch Bundle", self.serial_and_batch_bundle)
+		doc.submit()
+
 	def check_stock_frozen_date(self):
 		stock_settings = frappe.get_cached_doc("Stock Settings")
 
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 19f48e7..58484b1 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -13,7 +13,7 @@
 from erpnext.controllers.stock_controller import StockController
 from erpnext.stock.doctype.batch.batch import get_batch_qty
 from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
-	get_available_batch_nos,
+	get_auto_batch_nos,
 	get_available_serial_nos,
 )
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -114,7 +114,14 @@
 						)
 
 				if item_details.has_batch_no:
-					batch_nos_details = get_available_batch_nos(item.item_code, item.warehouse)
+					batch_nos_details = get_auto_batch_nos(
+						frappe._dict(
+							{
+								"item_code": item.item_code,
+								"warehouse": item.warehouse,
+							}
+						)
+					)
 
 					for batch_no, qty in batch_nos_details.items():
 						serial_and_batch_bundle.append(
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index a37f671..948592b 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -38,10 +38,11 @@
   "allow_partial_reservation",
   "serial_and_batch_item_settings_tab",
   "section_break_7",
-  "automatically_set_serial_nos_based_on_fifo",
-  "set_qty_in_transactions_based_on_serial_no_input",
-  "column_break_10",
+  "auto_create_serial_and_batch_bundle_for_outward",
+  "pick_serial_and_batch_based_on",
+  "section_break_plhx",
   "disable_serial_no_and_batch_selector",
+  "column_break_mhzc",
   "use_naming_series",
   "naming_series_prefix",
   "stock_planning_tab",
@@ -150,22 +151,6 @@
    "label": "Allow Negative Stock"
   },
   {
-   "fieldname": "column_break_10",
-   "fieldtype": "Column Break"
-  },
-  {
-   "default": "1",
-   "fieldname": "automatically_set_serial_nos_based_on_fifo",
-   "fieldtype": "Check",
-   "label": "Automatically Set Serial Nos Based on FIFO"
-  },
-  {
-   "default": "1",
-   "fieldname": "set_qty_in_transactions_based_on_serial_no_input",
-   "fieldtype": "Check",
-   "label": "Set Qty in Transactions Based on Serial No Input"
-  },
-  {
    "fieldname": "auto_material_request",
    "fieldtype": "Section Break",
    "label": "Auto Material Request"
@@ -376,6 +361,29 @@
    "fieldname": "allow_partial_reservation",
    "fieldtype": "Check",
    "label": "Allow Partial Reservation"
+  },
+  {
+   "fieldname": "section_break_plhx",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "column_break_mhzc",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "FIFO",
+   "depends_on": "auto_create_serial_and_batch_bundle_for_outward",
+   "fieldname": "pick_serial_and_batch_based_on",
+   "fieldtype": "Select",
+   "label": "Pick Serial / Batch Based On",
+   "mandatory_depends_on": "auto_create_serial_and_batch_bundle_for_outward",
+   "options": "FIFO\nLIFO\nExpiry"
+  },
+  {
+   "default": "1",
+   "fieldname": "auto_create_serial_and_batch_bundle_for_outward",
+   "fieldtype": "Check",
+   "label": "Auto Create Serial and Batch Bundle For Outward"
   }
  ],
  "icon": "icon-cog",
@@ -383,7 +391,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2023-05-29 15:09:54.959411",
+ "modified": "2023-05-29 15:10:54.959411",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Settings",
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 56802d9..64650bc 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -8,7 +8,7 @@
 from frappe import _, throw
 from frappe.model import child_table_fields, default_fields
 from frappe.model.meta import get_field_precision
-from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
+from frappe.query_builder.functions import IfNull, Sum
 from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
 
 from erpnext import get_company_currency
@@ -1089,28 +1089,6 @@
 	return pos_profile and pos_profile[0] or None
 
 
-def get_serial_nos_by_fifo(args, sales_order=None):
-	if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"):
-		sn = frappe.qb.DocType("Serial No")
-		query = (
-			frappe.qb.from_(sn)
-			.select(sn.name)
-			.where((sn.item_code == args.item_code) & (sn.warehouse == args.warehouse))
-			.orderby(CombineDatetime(sn.purchase_date, sn.purchase_time))
-			.limit(abs(cint(args.stock_qty)))
-		)
-
-		if sales_order:
-			query = query.where(sn.sales_order == sales_order)
-		if args.batch_no:
-			query = query.where(sn.batch_no == args.batch_no)
-
-		serial_nos = query.run(as_list=True)
-		serial_nos = [s[0] for s in serial_nos]
-
-		return "\n".join(serial_nos)
-
-
 @frappe.whitelist()
 def get_conversion_factor(item_code, uom):
 	variant_of = frappe.db.get_value("Item", item_code, "variant_of", cache=True)
@@ -1177,51 +1155,6 @@
 
 
 @frappe.whitelist()
-def get_serial_no_details(item_code, warehouse, stock_qty, serial_no):
-	args = frappe._dict(
-		{"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "serial_no": serial_no}
-	)
-	serial_no = get_serial_no(args)
-
-	return {"serial_no": serial_no}
-
-
-@frappe.whitelist()
-def get_bin_details_and_serial_nos(
-	item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None
-):
-	bin_details_and_serial_nos = {}
-	bin_details_and_serial_nos.update(get_bin_details(item_code, warehouse))
-	if flt(stock_qty) > 0:
-		if has_batch_no:
-			args = frappe._dict({"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty})
-			serial_no = get_serial_no(args)
-			bin_details_and_serial_nos.update({"serial_no": serial_no})
-			return bin_details_and_serial_nos
-
-		bin_details_and_serial_nos.update(
-			get_serial_no_details(item_code, warehouse, stock_qty, serial_no)
-		)
-
-	return bin_details_and_serial_nos
-
-
-@frappe.whitelist()
-def get_batch_qty_and_serial_no(batch_no, stock_qty, warehouse, item_code, has_serial_no):
-	batch_qty_and_serial_no = {}
-	batch_qty_and_serial_no.update(get_batch_qty(batch_no, warehouse, item_code))
-
-	if (flt(batch_qty_and_serial_no.get("actual_batch_qty")) >= flt(stock_qty)) and has_serial_no:
-		args = frappe._dict(
-			{"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "batch_no": batch_no}
-		)
-		serial_no = get_serial_no(args)
-		batch_qty_and_serial_no.update({"serial_no": serial_no})
-
-	return batch_qty_and_serial_no
-
-
-@frappe.whitelist()
 def get_batch_qty(batch_no, warehouse, item_code):
 	from erpnext.stock.doctype.batch import batch
 
@@ -1395,32 +1328,8 @@
 
 @frappe.whitelist()
 def get_serial_no(args, serial_nos=None, sales_order=None):
-	serial_no = None
-	if isinstance(args, str):
-		args = json.loads(args)
-		args = frappe._dict(args)
-	if args.get("doctype") == "Sales Invoice" and not args.get("update_stock"):
-		return ""
-	if args.get("warehouse") and args.get("stock_qty") and args.get("item_code"):
-		has_serial_no = frappe.get_value("Item", {"item_code": args.item_code}, "has_serial_no")
-		if args.get("batch_no") and has_serial_no == 1:
-			return get_serial_nos_by_fifo(args, sales_order)
-		elif has_serial_no == 1:
-			args = json.dumps(
-				{
-					"item_code": args.get("item_code"),
-					"warehouse": args.get("warehouse"),
-					"stock_qty": args.get("stock_qty"),
-				}
-			)
-			args = process_args(args)
-			serial_no = get_serial_nos_by_fifo(args, sales_order)
-
-	if not serial_no and serial_nos:
-		# For POS
-		serial_no = serial_nos
-
-	return serial_no
+	serial_nos = serial_nos or []
+	return serial_nos
 
 
 def update_party_blanket_order(args, out):
diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
index 858db81..c072874 100644
--- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
+++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
@@ -131,7 +131,7 @@
 			& (sle.has_batch_no == 1)
 			& (sle.posting_date <= filters["to_date"])
 		)
-		.groupby(batch_package.batch_no)
+		.groupby(batch_package.batch_no, batch_package.warehouse)
 		.orderby(sle.item_code, sle.warehouse)
 	)
 
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 038cce7..926863e 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -49,103 +49,64 @@
 		if (
 			not self.sle.is_cancelled
 			and not self.sle.serial_and_batch_bundle
-			and self.sle.actual_qty > 0
 			and self.item_details.has_serial_no == 1
-			and self.item_details.serial_no_series
-			and self.allow_to_make_auto_bundle()
 		):
 			self.make_serial_batch_no_bundle()
 		elif not self.sle.is_cancelled:
 			self.validate_item_and_warehouse()
 
-	def auto_create_serial_nos(self, batch_no=None):
-		sr_nos = []
-		serial_nos_details = []
-
-		for i in range(cint(self.sle.actual_qty)):
-			serial_no = make_autoname(self.item_details.serial_no_series, "Serial No")
-			sr_nos.append(serial_no)
-			serial_nos_details.append(
-				(
-					serial_no,
-					serial_no,
-					now(),
-					now(),
-					frappe.session.user,
-					frappe.session.user,
-					self.warehouse,
-					self.company,
-					self.item_code,
-					self.item_details.item_name,
-					self.item_details.description,
-					"Active",
-					batch_no,
-				)
-			)
-
-		if serial_nos_details:
-			fields = [
-				"name",
-				"serial_no",
-				"creation",
-				"modified",
-				"owner",
-				"modified_by",
-				"warehouse",
-				"company",
-				"item_code",
-				"item_name",
-				"description",
-				"status",
-				"batch_no",
-			]
-
-			frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
-
-		return sr_nos
-
 	def make_serial_batch_no_bundle(self):
-		sn_doc = frappe.new_doc("Serial and Batch Bundle")
-		sn_doc.item_code = self.item_code
-		sn_doc.warehouse = self.warehouse
-		sn_doc.item_name = self.item_details.item_name
-		sn_doc.item_group = self.item_details.item_group
-		sn_doc.has_serial_no = self.item_details.has_serial_no
-		sn_doc.has_batch_no = self.item_details.has_batch_no
-		sn_doc.voucher_type = self.sle.voucher_type
-		sn_doc.voucher_no = self.sle.voucher_no
-		sn_doc.voucher_detail_no = self.sle.voucher_detail_no
-		sn_doc.total_qty = self.sle.actual_qty
-		sn_doc.avg_rate = self.sle.incoming_rate
-		sn_doc.total_amount = flt(self.sle.actual_qty) * flt(self.sle.incoming_rate)
-		sn_doc.type_of_transaction = "Inward"
-		sn_doc.posting_date = self.sle.posting_date
-		sn_doc.posting_time = self.sle.posting_time
-		sn_doc.is_rejected = self.is_rejected_entry()
+		self.validate_item()
 
-		sn_doc.flags.ignore_mandatory = True
-		sn_doc.insert()
+		sn_doc = SerialBatchCreation(
+			{
+				"item_code": self.item_code,
+				"warehouse": self.warehouse,
+				"posting_date": self.sle.posting_date,
+				"posting_time": self.sle.posting_time,
+				"voucher_type": self.sle.voucher_type,
+				"voucher_no": self.sle.voucher_no,
+				"voucher_detail_no": self.sle.voucher_detail_no,
+				"total_qty": self.sle.actual_qty,
+				"avg_rate": self.sle.incoming_rate,
+				"total_amount": flt(self.sle.actual_qty) * flt(self.sle.incoming_rate),
+				"type_of_transaction": "Inward" if self.sle.actual_qty > 0 else "Outward",
+				"company": self.company,
+				"is_rejected": self.is_rejected_entry(),
+			}
+		).make_serial_and_batch_bundle()
 
-		batch_no = ""
-		if self.item_details.has_batch_no:
-			batch_no = self.create_batch()
-
-		incoming_rate = self.sle.incoming_rate
-		if not incoming_rate:
-			incoming_rate = frappe.get_cached_value(
-				self.child_doctype, self.sle.voucher_detail_no, "valuation_rate"
-			)
-
-		if self.item_details.has_serial_no:
-			sr_nos = self.auto_create_serial_nos(batch_no)
-			self.add_serial_no_to_bundle(sn_doc, sr_nos, incoming_rate, batch_no)
-		elif self.item_details.has_batch_no:
-			self.add_batch_no_to_bundle(sn_doc, batch_no, incoming_rate)
-
-		sn_doc.save()
-		sn_doc.submit()
 		self.set_serial_and_batch_bundle(sn_doc)
 
+	def validate_item(self):
+		msg = ""
+		if self.sle.actual_qty > 0:
+			if not self.item_details.has_batch_no and not self.item_details.has_serial_no:
+				msg = f"Item {self.item_code} is not a batch or serial no item"
+
+			if self.item_details.has_serial_no and not self.item_details.serial_no_series:
+				msg += f". If you want auto pick serial bundle, then kindly set Serial No Series in Item {self.item_code}"
+
+			if (
+				self.item_details.has_batch_no
+				and not self.item_details.batch_number_series
+				and not frappe.db.get_single_value("Stock Settings", "naming_series_prefix")
+			):
+				msg += f". If you want auto pick batch bundle, then kindly set Batch Number Series in Item {self.item_code}"
+
+		elif self.sle.actual_qty < 0:
+			if not frappe.db.get_single_value(
+				"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
+			):
+				msg += ". If you want auto pick serial/batch bundle, then kindly enable 'Auto Create Serial and Batch Bundle' in Stock Settings."
+
+		if msg:
+			error_msg = (
+				f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}."
+				+ msg
+			)
+			frappe.throw(_(error_msg))
+
 	def set_serial_and_batch_bundle(self, sn_doc):
 		self.sle.db_set("serial_and_batch_bundle", sn_doc.name)
 
@@ -169,72 +130,19 @@
 	def is_rejected_entry(self):
 		return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
 
-	def add_serial_no_to_bundle(self, sn_doc, serial_nos, incoming_rate, batch_no=None):
-		for serial_no in serial_nos:
-			sn_doc.append(
-				"entries",
-				{
-					"serial_no": serial_no,
-					"qty": 1,
-					"incoming_rate": incoming_rate,
-					"batch_no": batch_no,
-					"warehouse": self.warehouse,
-					"is_outward": 0,
-				},
-			)
-
-	def add_batch_no_to_bundle(self, sn_doc, batch_no, incoming_rate):
-		stock_value_difference = flt(self.sle.actual_qty) * flt(incoming_rate)
-
-		if self.sle.actual_qty < 0:
-			stock_value_difference *= -1
-
-		sn_doc.append(
-			"entries",
-			{
-				"batch_no": batch_no,
-				"qty": self.sle.actual_qty,
-				"incoming_rate": incoming_rate,
-				"stock_value_difference": stock_value_difference,
-			},
-		)
-
-	def create_batch(self):
-		from erpnext.stock.doctype.batch.batch import make_batch
-
-		return make_batch(
-			frappe._dict(
-				{
-					"item": self.item_code,
-					"reference_doctype": self.sle.voucher_type,
-					"reference_name": self.sle.voucher_no,
-				}
-			)
-		)
-
 	def process_batch_no(self):
 		if (
 			not self.sle.is_cancelled
 			and not self.sle.serial_and_batch_bundle
-			and self.sle.actual_qty > 0
 			and self.item_details.has_batch_no == 1
 			and self.item_details.create_new_batch
 			and self.item_details.batch_number_series
-			and self.allow_to_make_auto_bundle()
 		):
 			self.make_serial_batch_no_bundle()
 		elif not self.sle.is_cancelled:
 			self.validate_item_and_warehouse()
 
 	def validate_item_and_warehouse(self):
-
-		data = frappe.db.get_value(
-			"Serial and Batch Bundle",
-			self.sle.serial_and_batch_bundle,
-			["item_code", "warehouse", "voucher_no", "name"],
-			as_dict=1,
-		)
-
 		if self.sle.serial_and_batch_bundle and not frappe.db.exists(
 			"Serial and Batch Bundle",
 			{
@@ -270,18 +178,6 @@
 			{"is_cancelled": 1, "voucher_no": ""},
 		)
 
-	def allow_to_make_auto_bundle(self):
-		if self.sle.voucher_type in ["Stock Entry", "Purchase Receipt", "Purchase Invoice"]:
-			if self.sle.voucher_type == "Stock Entry":
-				stock_entry_type = frappe.get_cached_value("Stock Entry", self.sle.voucher_no, "purpose")
-
-				if stock_entry_type in ["Material Receipt", "Manufacture", "Repack"]:
-					return True
-
-			return True
-
-		return False
-
 	def post_process(self):
 		if not self.sle.serial_and_batch_bundle:
 			return
@@ -296,6 +192,9 @@
 		):
 			self.set_batch_no_in_serial_nos()
 
+		if self.item_details.has_batch_no == 1:
+			self.update_batch_qty()
+
 	def set_warehouse_and_status_in_serial_nos(self):
 		serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle, check_outward=False)
 		warehouse = self.warehouse if self.sle.actual_qty > 0 else None
@@ -330,6 +229,20 @@
 				.where(sn_table.name.isin(serial_nos))
 			).run()
 
+	def update_batch_qty(self):
+		from erpnext.stock.doctype.batch.batch import get_available_batches
+
+		batches = get_batch_nos(self.sle.serial_and_batch_bundle)
+
+		batches_qty = get_available_batches(
+			frappe._dict(
+				{"item_code": self.item_code, "warehouse": self.warehouse, "batch_no": list(batches.keys())}
+			)
+		)
+
+		for batch_no, qty in batches_qty.items():
+			frappe.db.set_value("Batch", batch_no, "batch_qty", qty)
+
 
 def get_serial_nos(serial_and_batch_bundle, check_outward=True):
 	filters = {"parent": serial_and_batch_bundle}
@@ -489,6 +402,7 @@
 
 			self.batch_avg_rate = defaultdict(float)
 			self.available_qty = defaultdict(float)
+
 			for ledger in entries:
 				self.batch_avg_rate[ledger.batch_no] += flt(ledger.incoming_rate) / flt(ledger.qty)
 				self.available_qty[ledger.batch_no] += flt(ledger.qty)
@@ -502,11 +416,13 @@
 
 		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)
+		timestamp_condition = ""
+		if self.sle.posting_date and self.sle.posting_time:
+			timestamp_condition = CombineDatetime(
+				parent.posting_date, parent.posting_time
+			) < CombineDatetime(self.sle.posting_date, self.sle.posting_time)
 
-		return (
+		query = (
 			frappe.qb.from_(parent)
 			.inner_join(child)
 			.on(parent.name == child.parent)
@@ -524,21 +440,19 @@
 				& (parent.is_cancelled == 0)
 				& (parent.type_of_transaction != "Maintenance")
 			)
-			.where(timestamp_condition)
 			.groupby(child.batch_no)
-		).run(as_dict=True)
+		)
+
+		if timestamp_condition:
+			query.where(timestamp_condition)
+
+		return query.run(as_dict=True)
 
 	def get_batch_nos(self) -> list:
 		if self.sle.get("batch_nos"):
 			return self.sle.batch_nos
 
-		entries = frappe.get_all(
-			"Serial and Batch Entry",
-			fields=["batch_no", "qty", "name"],
-			filters={"parent": self.sle.serial_and_batch_bundle, "is_outward": 1},
-		)
-
-		return {d.batch_no: d for d in entries}
+		return get_batch_nos(self.sle.serial_and_batch_bundle)
 
 	def set_stock_value_difference(self):
 		self.stock_value_change = 0
@@ -566,6 +480,16 @@
 		return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
 
 
+def get_batch_nos(serial_and_batch_bundle):
+	entries = frappe.get_all(
+		"Serial and Batch Entry",
+		fields=["batch_no", "qty", "name"],
+		filters={"parent": serial_and_batch_bundle, "is_outward": 1},
+	)
+
+	return {d.batch_no: d for d in entries}
+
+
 def get_empty_batches_based_work_order(work_order, item_code):
 	batches = get_batches_from_work_order(work_order, item_code)
 	if not batches:
@@ -631,8 +555,35 @@
 
 class SerialBatchCreation:
 	def __init__(self, args):
+		self.set(args)
+		self.set_item_details()
+
+	def set(self, args):
+		self.__dict__ = {}
 		for key, value in args.items():
 			setattr(self, key, value)
+			self.__dict__[key] = value
+
+	def get(self, key):
+		return self.__dict__.get(key)
+
+	def set_item_details(self):
+		fields = [
+			"has_batch_no",
+			"has_serial_no",
+			"item_name",
+			"item_group",
+			"serial_no_series",
+			"create_new_batch",
+			"batch_number_series",
+			"description",
+		]
+
+		item_details = frappe.get_cached_value("Item", self.item_code, fields, as_dict=1)
+		for key, value in item_details.items():
+			setattr(self, key, value)
+
+		self.__dict__.update(item_details)
 
 	def duplicate_package(self):
 		if not self.serial_and_batch_bundle:
@@ -643,7 +594,167 @@
 		new_package = frappe.copy_doc(package)
 		new_package.type_of_transaction = self.type_of_transaction
 		new_package.returned_against = self.returned_against
-		print(new_package.voucher_type, new_package.voucher_no)
 		new_package.save()
 
 		self.serial_and_batch_bundle = new_package.name
+
+	def make_serial_and_batch_bundle(self):
+		doc = frappe.new_doc("Serial and Batch Bundle")
+		valid_columns = doc.meta.get_valid_columns()
+		for key, value in self.__dict__.items():
+			if key in valid_columns:
+				doc.set(key, value)
+
+		if self.type_of_transaction == "Outward":
+			self.set_auto_serial_batch_entries_for_outward()
+		elif self.type_of_transaction == "Inward":
+			self.set_auto_serial_batch_entries_for_inward()
+
+		self.set_serial_batch_entries(doc)
+		doc.save()
+
+		if not hasattr(self, "do_not_submit") or not self.do_not_submit:
+			doc.submit()
+
+		return doc
+
+	def set_auto_serial_batch_entries_for_outward(self):
+		from erpnext.stock.doctype.batch.batch import get_available_batches
+		from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
+
+		kwargs = frappe._dict(
+			{
+				"item_code": self.item_code,
+				"warehouse": self.warehouse,
+				"qty": abs(self.total_qty),
+				"based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
+			}
+		)
+
+		if self.has_serial_no and not self.get("serial_nos"):
+			self.serial_nos = get_serial_nos_for_outward(kwargs)
+		elif self.has_batch_no and not self.get("batches"):
+			self.batches = get_available_batches(kwargs)
+
+	def set_auto_serial_batch_entries_for_inward(self):
+		self.batch_no = None
+		if self.has_batch_no:
+			self.batch_no = self.create_batch()
+
+		if self.has_serial_no:
+			self.serial_nos = self.get_auto_created_serial_nos()
+		else:
+			self.batches = frappe._dict({self.batch_no: abs(self.total_qty)})
+
+	def set_serial_batch_entries(self, doc):
+		if self.get("serial_nos"):
+			serial_no_wise_batch = frappe._dict({})
+			if self.has_batch_no:
+				serial_no_wise_batch = self.get_serial_nos_batch(self.serial_nos)
+
+			qty = -1 if self.type_of_transaction == "Outward" else 1
+			for serial_no in self.serial_nos:
+				doc.append(
+					"entries",
+					{
+						"serial_no": serial_no,
+						"qty": qty,
+						"batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
+						"incoming_rate": self.get("incoming_rate"),
+					},
+				)
+
+		if self.get("batches"):
+			for batch_no, batch_qty in self.batches.items():
+				doc.append(
+					"entries",
+					{
+						"batch_no": batch_no,
+						"qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
+						"incoming_rate": self.get("incoming_rate"),
+					},
+				)
+
+	def get_serial_nos_batch(self, serial_nos):
+		return frappe._dict(
+			frappe.get_all(
+				"Serial No",
+				fields=["name", "batch_no"],
+				filters={"name": ("in", serial_nos)},
+				as_list=1,
+			)
+		)
+
+	def create_batch(self):
+		from erpnext.stock.doctype.batch.batch import make_batch
+
+		return make_batch(
+			frappe._dict(
+				{
+					"item": self.item_code,
+					"reference_doctype": self.voucher_type,
+					"reference_name": self.voucher_no,
+				}
+			)
+		)
+
+	def get_auto_created_serial_nos(self):
+		sr_nos = []
+		serial_nos_details = []
+
+		for i in range(abs(cint(self.total_qty))):
+			serial_no = make_autoname(self.serial_no_series, "Serial No")
+			sr_nos.append(serial_no)
+			serial_nos_details.append(
+				(
+					serial_no,
+					serial_no,
+					now(),
+					now(),
+					frappe.session.user,
+					frappe.session.user,
+					self.warehouse,
+					self.company,
+					self.item_code,
+					self.item_name,
+					self.description,
+					"Active",
+					self.batch_no,
+				)
+			)
+
+		if serial_nos_details:
+			fields = [
+				"name",
+				"serial_no",
+				"creation",
+				"modified",
+				"owner",
+				"modified_by",
+				"warehouse",
+				"company",
+				"item_code",
+				"item_name",
+				"description",
+				"status",
+				"batch_no",
+			]
+
+			frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
+
+		return sr_nos
+
+
+def get_serial_or_batch_items(items):
+	serial_or_batch_items = frappe.get_all(
+		"Item",
+		filters={"name": ("in", [d.item_code for d in items])},
+		or_filters={"has_serial_no": 1, "has_batch_no": 1},
+	)
+
+	if not serial_or_batch_items:
+		return
+	else:
+		serial_or_batch_items = [d.name for d in serial_or_batch_items]
+
+	return serial_or_batch_items