refactor: serial and batch package creation for finished item and cleanup code
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 789ca6c..b5e780b 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -328,8 +328,6 @@
 				{
 					"item_code": self.target_item_code,
 					"warehouse": self.target_warehouse,
-					"batch_no": self.target_batch_no,
-					"serial_no": self.target_serial_no,
 					"actual_qty": flt(self.target_qty),
 					"incoming_rate": flt(self.target_incoming_rate),
 				},
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index b55574f..c064e5a 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -5,7 +5,7 @@
 import frappe
 from frappe import ValidationError, _, msgprint
 from frappe.contacts.doctype.address.address import get_address_display
-from frappe.utils import cint, cstr, flt, getdate
+from frappe.utils import cint, flt, getdate
 from frappe.utils.data import nowtime
 
 from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
@@ -497,7 +497,6 @@
 						d,
 						{
 							"actual_qty": flt(pr_qty),
-							"serial_no": cstr(d.serial_no).strip(),
 							"serial_and_batch_bundle": (
 								d.serial_and_batch_bundle
 								if not self.is_internal_transfer()
@@ -550,7 +549,6 @@
 						{
 							"warehouse": d.rejected_warehouse,
 							"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
-							"serial_no": cstr(d.rejected_serial_no).strip(),
 							"incoming_rate": 0.0,
 							"serial_and_batch_bundle": d.rejected_serial_and_batch_bundle,
 						},
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 2048a42..8c3bd4d 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -407,7 +407,6 @@
 		else:
 			bundle_doc.save(ignore_permissions=True)
 
-		print(bundle_doc.name)
 		return bundle_doc.name
 
 	def get_sl_entries(self, d, args):
@@ -428,7 +427,6 @@
 				),
 				"incoming_rate": 0,
 				"company": self.company,
-				"serial_no": d.get("serial_no"),
 				"project": d.get("project") or self.get("project"),
 				"is_cancelled": 1 if self.docstatus == 2 else 0,
 			}
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 0e666ff..1418e5f 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -8,7 +8,7 @@
 import frappe
 from frappe import _
 from frappe.model.mapper import get_mapped_doc
-from frappe.utils import cint, cstr, flt, get_link_to_form
+from frappe.utils import cint, flt, get_link_to_form
 
 from erpnext.controllers.stock_controller import StockController
 from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
@@ -768,9 +768,7 @@
 				scr_qty = flt(item.qty) * flt(item.conversion_factor)
 
 				if scr_qty:
-					sle = self.get_sl_entries(
-						item, {"actual_qty": flt(scr_qty), "serial_no": cstr(item.serial_no).strip()}
-					)
+					sle = self.get_sl_entries(item, {"actual_qty": flt(scr_qty)})
 					rate_db_precision = 6 if cint(self.precision("rate", item)) <= 6 else 9
 					incoming_rate = flt(item.rate, rate_db_precision)
 					sle.update(
@@ -788,7 +786,6 @@
 							{
 								"warehouse": item.rejected_warehouse,
 								"actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor),
-								"serial_no": cstr(item.rejected_serial_no).strip(),
 								"incoming_rate": 0.0,
 							},
 						)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index d83bd1d..aecace6 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -42,7 +42,6 @@
   "has_serial_no",
   "has_batch_no",
   "column_break_18",
-  "serial_no",
   "batch_size",
   "required_items_section",
   "materials_and_operations_tab",
@@ -533,14 +532,6 @@
    "read_only": 1
   },
   {
-   "depends_on": "has_serial_no",
-   "fieldname": "serial_no",
-   "fieldtype": "Small Text",
-   "label": "Serial Nos",
-   "no_copy": 1,
-   "read_only": 1
-  },
-  {
    "default": "0",
    "depends_on": "has_batch_no",
    "fieldname": "batch_size",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index e30a302..a5b8972 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -17,6 +17,7 @@
 	get_datetime,
 	get_link_to_form,
 	getdate,
+	now,
 	nowdate,
 	time_diff_in_hours,
 )
@@ -32,11 +33,7 @@
 )
 from erpnext.stock.doctype.batch.batch import make_batch
 from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
-from erpnext.stock.doctype.serial_no.serial_no import (
-	clean_serial_no_string,
-	get_auto_serial_nos,
-	get_serial_nos,
-)
+from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, get_serial_nos
 from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
 from erpnext.stock.utils import get_bin, get_latest_stock_qty, validate_warehouse_company
 from erpnext.utilities.transaction_base import validate_uom_is_integer
@@ -447,24 +444,53 @@
 			frappe.delete_doc("Batch", row.name)
 
 	def make_serial_nos(self, args):
-		self.serial_no = clean_serial_no_string(self.serial_no)
-		serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
-		if serial_no_series:
-			self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
+		item_details = frappe.get_cached_value(
+			"Item", self.production_item, ["serial_no_series", "item_name", "description"], as_dict=1
+		)
 
-		if self.serial_no:
-			args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
-			# auto_make_serial_nos(args)
+		serial_nos = []
+		if item_details.serial_no_series:
+			serial_nos = get_auto_serial_nos(item_details.serial_no_series, self.qty)
 
-		serial_nos_length = len(get_serial_nos(self.serial_no))
-		if serial_nos_length != self.qty:
-			frappe.throw(
-				_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(
-					self.qty, self.production_item, serial_nos_length
-				),
-				SerialNoQtyError,
+		if not serial_nos:
+			return
+
+		fields = [
+			"name",
+			"serial_no",
+			"creation",
+			"modified",
+			"owner",
+			"modified_by",
+			"company",
+			"item_code",
+			"item_name",
+			"description",
+			"status",
+			"work_order",
+		]
+
+		serial_nos_details = []
+		for serial_no in serial_nos:
+			serial_nos_details.append(
+				(
+					serial_no,
+					serial_no,
+					now(),
+					now(),
+					frappe.session.user,
+					frappe.session.user,
+					self.company,
+					self.production_item,
+					item_details.item_name,
+					item_details.description,
+					"Inactive",
+					self.name,
+				)
 			)
 
+		frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
+
 	def create_job_card(self):
 		manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
 
@@ -1041,24 +1067,6 @@
 		bom.set_bom_material_details()
 		return bom
 
-	def update_batch_produced_qty(self, stock_entry_doc):
-		if not cint(
-			frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")
-		):
-			return
-
-		for row in stock_entry_doc.items:
-			if row.batch_no and (row.is_finished_item or row.is_scrap_item):
-				qty = frappe.get_all(
-					"Stock Entry Detail",
-					filters={"batch_no": row.batch_no, "docstatus": 1},
-					or_filters={"is_finished_item": 1, "is_scrap_item": 1},
-					fields=["sum(qty)"],
-					as_list=1,
-				)[0][0]
-
-				frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty))
-
 
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
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 824691c..4969713 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
@@ -27,8 +27,8 @@
 		self.validate_serial_nos_inventory()
 
 	def before_save(self):
-		self.set_total_qty()
 		self.set_is_outward()
+		self.set_total_qty()
 		self.set_warehouse()
 		self.set_incoming_rate()
 		self.validate_qty_and_stock_value_difference()
@@ -51,7 +51,9 @@
 		)
 
 		for serial_no in serial_nos:
-			if serial_no_warehouse.get(serial_no) != self.warehouse:
+			if (
+				not serial_no_warehouse.get(serial_no) or 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)}.")
 				)
@@ -73,6 +75,9 @@
 			if d.stock_value_difference and d.stock_value_difference > 0:
 				d.stock_value_difference *= -1
 
+	def get_serial_nos(self):
+		return [d.serial_no for d in self.ledgers if d.serial_no]
+
 	def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
 		sle = self.get_sle_for_outward_transaction(row)
 		if self.has_serial_no:
@@ -271,6 +276,11 @@
 
 	def set_is_outward(self):
 		for row in self.ledgers:
+			if self.type_of_transaction == "Outward" and row.qty > 0:
+				row.qty *= -1
+			elif self.type_of_transaction == "Inward" and row.qty < 0:
+				row.qty *= -1
+
 			row.is_outward = 1 if self.type_of_transaction == "Outward" else 0
 
 	@frappe.whitelist()
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 4c5156c..5b4f41e 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -9,10 +9,9 @@
 from frappe import ValidationError, _
 from frappe.model.naming import make_autoname
 from frappe.query_builder.functions import Coalesce
-from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate, safe_json_loads
+from frappe.utils import cint, cstr, getdate, nowdate, safe_json_loads
 
 from erpnext.controllers.stock_controller import StockController
-from erpnext.stock.get_item_details import get_reserved_qty_for_so
 
 
 class SerialNoCannotCreateDirectError(ValidationError):
@@ -108,384 +107,12 @@
 			)
 
 
-def process_serial_no(sle):
-	item_det = get_item_details(sle.item_code)
-	validate_serial_no(sle, item_det)
-
-
-def validate_serial_no(sle, item_det):
-	serial_nos = get_serial_nos(sle.serial_and_batch_bundle) if sle.serial_and_batch_bundle else []
-	validate_material_transfer_entry(sle)
-
-	if item_det.has_serial_no == 0:
-		if serial_nos:
-			frappe.throw(
-				_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code),
-				SerialNoNotRequiredError,
-			)
-	elif not sle.is_cancelled:
-		return
-		if serial_nos:
-			if cint(sle.actual_qty) != flt(sle.actual_qty):
-				frappe.throw(
-					_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty)
-				)
-
-			if len(serial_nos) and len(serial_nos) != abs(cint(sle.actual_qty)):
-				frappe.throw(
-					_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(
-						abs(sle.actual_qty), sle.item_code, len(serial_nos)
-					),
-					SerialNoQtyError,
-				)
-
-			if len(serial_nos) != len(set(serial_nos)):
-				frappe.throw(
-					_("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError
-				)
-
-			for serial_no in serial_nos:
-				if frappe.db.exists("Serial No", serial_no):
-					sr = frappe.db.get_value(
-						"Serial No",
-						serial_no,
-						[
-							"name",
-							"item_code",
-							"batch_no",
-							"sales_order",
-							"delivery_document_no",
-							"delivery_document_type",
-							"warehouse",
-							"purchase_document_type",
-							"purchase_document_no",
-							"company",
-							"status",
-						],
-						as_dict=1,
-					)
-
-					if sr.item_code != sle.item_code:
-						if not allow_serial_nos_with_different_item(serial_no, sle):
-							frappe.throw(
-								_("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code),
-								SerialNoItemError,
-							)
-
-					if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle):
-						doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no))
-						frappe.throw(
-							_("Serial No {0} has already been received in the {1} #{2}").format(
-								frappe.bold(serial_no), sr.purchase_document_type, doc_name
-							),
-							SerialNoDuplicateError,
-						)
-
-					if (
-						sr.delivery_document_no
-						and sle.voucher_type not in ["Stock Entry", "Stock Reconciliation"]
-						and sle.voucher_type == sr.delivery_document_type
-					):
-						return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, "return_against")
-						if return_against and return_against != sr.delivery_document_no:
-							frappe.throw(_("Serial no {0} has been already returned").format(sr.name))
-
-					if cint(sle.actual_qty) < 0:
-						if sr.warehouse != sle.warehouse:
-							frappe.throw(
-								_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse),
-								SerialNoWarehouseError,
-							)
-
-						if not sr.purchase_document_no:
-							frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError)
-
-						if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
-
-							if sr.batch_no and sr.batch_no != sle.batch_no:
-								frappe.throw(
-									_("Serial No {0} does not belong to Batch {1}").format(serial_no, sle.batch_no),
-									SerialNoBatchError,
-								)
-
-							if not sle.is_cancelled and not sr.warehouse:
-								frappe.throw(
-									_("Serial No {0} does not belong to any Warehouse").format(serial_no),
-									SerialNoWarehouseError,
-								)
-
-							# if Sales Order reference in Serial No validate the Delivery Note or Invoice is against the same
-							if sr.sales_order:
-								if sle.voucher_type == "Sales Invoice":
-									if not frappe.db.exists(
-										"Sales Invoice Item",
-										{"parent": sle.voucher_no, "item_code": sle.item_code, "sales_order": sr.sales_order},
-									):
-										frappe.throw(
-											_(
-												"Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}"
-											).format(sr.name, sle.item_code, sr.sales_order)
-										)
-								elif sle.voucher_type == "Delivery Note":
-									if not frappe.db.exists(
-										"Delivery Note Item",
-										{
-											"parent": sle.voucher_no,
-											"item_code": sle.item_code,
-											"against_sales_order": sr.sales_order,
-										},
-									):
-										invoice = frappe.db.get_value(
-											"Delivery Note Item",
-											{"parent": sle.voucher_no, "item_code": sle.item_code},
-											"against_sales_invoice",
-										)
-										if not invoice or frappe.db.exists(
-											"Sales Invoice Item",
-											{"parent": invoice, "item_code": sle.item_code, "sales_order": sr.sales_order},
-										):
-											frappe.throw(
-												_(
-													"Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}"
-												).format(sr.name, sle.item_code, sr.sales_order)
-											)
-							# if Sales Order reference in Delivery Note or Invoice validate SO reservations for item
-							if sle.voucher_type == "Sales Invoice":
-								sales_order = frappe.db.get_value(
-									"Sales Invoice Item",
-									{"parent": sle.voucher_no, "item_code": sle.item_code},
-									"sales_order",
-								)
-								if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
-									validate_so_serial_no(sr, sales_order)
-							elif sle.voucher_type == "Delivery Note":
-								sales_order = frappe.get_value(
-									"Delivery Note Item",
-									{"parent": sle.voucher_no, "item_code": sle.item_code},
-									"against_sales_order",
-								)
-								if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
-									validate_so_serial_no(sr, sales_order)
-								else:
-									sales_invoice = frappe.get_value(
-										"Delivery Note Item",
-										{"parent": sle.voucher_no, "item_code": sle.item_code},
-										"against_sales_invoice",
-									)
-									if sales_invoice:
-										sales_order = frappe.db.get_value(
-											"Sales Invoice Item",
-											{"parent": sales_invoice, "item_code": sle.item_code},
-											"sales_order",
-										)
-										if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code):
-											validate_so_serial_no(sr, sales_order)
-				elif cint(sle.actual_qty) < 0:
-					# transfer out
-					frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError)
-		elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series:
-			frappe.throw(
-				_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError
-			)
-	elif serial_nos:
-		return
-		# SLE is being cancelled and has serial nos
-		for serial_no in serial_nos:
-			check_serial_no_validity_on_cancel(serial_no, sle)
-
-
-def check_serial_no_validity_on_cancel(serial_no, sle):
-	sr = frappe.db.get_value(
-		"Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1
-	)
-	sr_link = frappe.utils.get_link_to_form("Serial No", serial_no)
-	doc_link = frappe.utils.get_link_to_form(sle.voucher_type, sle.voucher_no)
-	actual_qty = cint(sle.actual_qty)
-	is_stock_reco = sle.voucher_type == "Stock Reconciliation"
-	msg = None
-
-	if sr and (actual_qty < 0 or is_stock_reco) and (sr.warehouse and sr.warehouse != sle.warehouse):
-		# receipt(inward) is being cancelled
-		msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the warehouse {3}").format(
-			sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse)
-		)
-	elif sr and actual_qty > 0 and not is_stock_reco:
-		# delivery is being cancelled, check for warehouse.
-		if sr.warehouse:
-			# serial no is active in another warehouse/company.
-			msg = _("Cannot cancel {0} {1} as Serial No {2} is active in warehouse {3}").format(
-				sle.voucher_type, doc_link, sr_link, frappe.bold(sr.warehouse)
-			)
-		elif sr.company != sle.company and sr.status == "Delivered":
-			# serial no is inactive (allowed) or delivered from another company (block).
-			msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the company {3}").format(
-				sle.voucher_type, doc_link, sr_link, frappe.bold(sle.company)
-			)
-
-	if msg:
-		frappe.throw(msg, title=_("Cannot cancel"))
-
-
-def validate_material_transfer_entry(sle_doc):
-	sle_doc.update({"skip_update_serial_no": False, "skip_serial_no_validaiton": False})
-
-	if (
-		sle_doc.voucher_type == "Stock Entry"
-		and not sle_doc.is_cancelled
-		and frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"
-	):
-		if sle_doc.actual_qty < 0:
-			sle_doc.skip_update_serial_no = True
-		else:
-			sle_doc.skip_serial_no_validaiton = True
-
-
-def validate_so_serial_no(sr, sales_order):
-	if not sr.sales_order or sr.sales_order != sales_order:
-		msg = _(
-			"Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}."
-		).format(sales_order, sr.item_code)
-
-		frappe.throw(_("""{0} Serial No {1} cannot be delivered""").format(msg, sr.name))
-
-
-def has_serial_no_exists(sn, sle):
-	if (
-		sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation"
-	):
-		return True
-
-	if sn.company != sle.company:
-		return False
-
-
-def allow_serial_nos_with_different_item(sle_serial_no, sle):
-	"""
-	Allows same serial nos for raw materials and finished goods
-	in Manufacture / Repack type Stock Entry
-	"""
-	allow_serial_nos = False
-	if sle.voucher_type == "Stock Entry" and cint(sle.actual_qty) > 0:
-		stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no)
-		if stock_entry.purpose in ("Repack", "Manufacture"):
-			for d in stock_entry.get("items"):
-				if d.serial_no and (d.s_warehouse if not sle.is_cancelled else d.t_warehouse):
-					serial_nos = get_serial_nos(d.serial_no)
-					if sle_serial_no in serial_nos:
-						allow_serial_nos = True
-
-	return allow_serial_nos
-
-
-def update_warehouse_in_serial_no(sle, item_det):
-	serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
-	serial_no_data = get_serial_nos_warehouse(sle.item_code, serial_nos)
-
-	if not serial_no_data:
-		for serial_no in serial_nos:
-			frappe.db.set_value("Serial No", serial_no, "warehouse", None)
-
-	else:
-		for row in serial_no_data:
-			if not row.serial_no:
-				continue
-
-			warehouse = row.warehouse if row.actual_qty > 0 else None
-			frappe.db.set_value("Serial No", row.serial_no, "warehouse", warehouse)
-
-
-def get_serial_nos_warehouse(item_code, serial_nos):
-	ledger_table = frappe.qb.DocType("Serial and Batch Ledger")
-	sle_table = frappe.qb.DocType("Stock Ledger Entry")
-
-	return (
-		frappe.qb.from_(ledger_table)
-		.inner_join(sle_table)
-		.on(ledger_table.parent == sle_table.serial_and_batch_bundle)
-		.select(
-			ledger_table.serial_no,
-			sle_table.actual_qty,
-			ledger_table.warehouse,
-		)
-		.where(
-			(ledger_table.serial_no.isin(serial_nos))
-			& (sle_table.is_cancelled == 0)
-			& (sle_table.item_code == item_code)
-			& (sle_table.serial_and_batch_bundle.isnotnull())
-		)
-		.orderby(sle_table.posting_date, order=frappe.qb.desc)
-		.orderby(sle_table.posting_time, order=frappe.qb.desc)
-		.orderby(sle_table.creation, order=frappe.qb.desc)
-		.groupby(ledger_table.serial_no)
-	).run(as_dict=True)
-
-
-def create_batch_for_serial_no(sle):
-	from erpnext.stock.doctype.batch.batch import make_batch
-
-	return make_batch(
-		frappe._dict(
-			{
-				"item": sle.item_code,
-				"reference_doctype": sle.voucher_type,
-				"reference_name": sle.voucher_no,
-			}
-		)
-	)
-
-
-def auto_create_serial_nos(sle, item_details) -> List[str]:
-	sr_nos = []
-	serial_nos_details = []
-	current_series = frappe.db.sql(
-		"select current from `tabSeries` where name = %s", item_details.serial_no_series
-	)
-
-	for i in range(cint(sle.actual_qty)):
-		serial_no = make_autoname(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,
-				sle.warehouse,
-				sle.company,
-				sle.item_code,
-				item_details.item_name,
-				item_details.description,
-			)
-		)
-
-	if serial_nos_details:
-		fields = [
-			"name",
-			"serial_no",
-			"creation",
-			"modified",
-			"owner",
-			"modified_by",
-			"warehouse",
-			"company",
-			"item_code",
-			"item_name",
-			"description",
-		]
-
-		frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
-
-	return sr_nos
-
-
-def get_auto_serial_nos(serial_no_series, qty):
+def get_auto_serial_nos(serial_no_series, qty) -> List[str]:
 	serial_nos = []
 	for i in range(cint(qty)):
 		serial_nos.append(get_new_serial_number(serial_no_series))
 
-	return "\n".join(serial_nos)
+	return serial_nos
 
 
 def get_new_serial_number(series):
@@ -534,72 +161,6 @@
 	return "\n".join(serial_no_list)
 
 
-def update_serial_nos_after_submit(controller, parentfield):
-	return
-	stock_ledger_entries = frappe.db.sql(
-		"""select voucher_detail_no, serial_no, actual_qty, warehouse
-		from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""",
-		(controller.doctype, controller.name),
-		as_dict=True,
-	)
-
-	if not stock_ledger_entries:
-		return
-
-	for d in controller.get(parentfield):
-		if d.serial_no:
-			continue
-
-		update_rejected_serial_nos = (
-			True
-			if (
-				controller.doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt")
-				and d.rejected_qty
-			)
-			else False
-		)
-		accepted_serial_nos_updated = False
-
-		if controller.doctype == "Stock Entry":
-			warehouse = d.t_warehouse
-			qty = d.transfer_qty
-		elif controller.doctype in ("Sales Invoice", "Delivery Note"):
-			warehouse = d.warehouse
-			qty = d.stock_qty
-		else:
-			warehouse = d.warehouse
-			qty = (
-				d.qty
-				if controller.doctype in ["Stock Reconciliation", "Subcontracting Receipt"]
-				else d.stock_qty
-			)
-		for sle in stock_ledger_entries:
-			if sle.voucher_detail_no == d.name:
-				if (
-					not accepted_serial_nos_updated
-					and qty
-					and abs(sle.actual_qty) == abs(qty)
-					and sle.warehouse == warehouse
-					and sle.serial_no != d.serial_no
-				):
-					d.serial_no = sle.serial_no
-					frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no)
-					accepted_serial_nos_updated = True
-					if not update_rejected_serial_nos:
-						break
-				elif (
-					update_rejected_serial_nos
-					and abs(sle.actual_qty) == d.rejected_qty
-					and sle.warehouse == d.rejected_warehouse
-					and sle.serial_no != d.rejected_serial_no
-				):
-					d.rejected_serial_no = sle.serial_no
-					frappe.db.set_value(d.doctype, d.name, "rejected_serial_no", sle.serial_no)
-					update_rejected_serial_nos = False
-					if accepted_serial_nos_updated:
-						break
-
-
 def update_maintenance_status():
 	serial_nos = frappe.db.sql(
 		"""select name from `tabSerial No` where (amc_expiry_date<%s or
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 5e8aff3..d71814b 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -4,6 +4,7 @@
 
 import json
 from collections import defaultdict
+from typing import List
 
 import frappe
 from frappe import _
@@ -37,8 +38,8 @@
 	get_bin_details,
 	get_conversion_factor,
 	get_default_cost_center,
-	get_reserved_qty_for_so,
 )
+from erpnext.stock.serial_batch_bundle import get_empty_batches_based_work_order
 from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get_valuation_rate
 from erpnext.stock.utils import get_bin, get_incoming_rate
 
@@ -203,13 +204,9 @@
 
 		self.repost_future_sle_and_gle()
 		self.update_cost_in_project()
-		self.validate_reserved_serial_no_consumption()
 		self.update_transferred_qty()
 		self.update_quality_inspection()
 
-		if self.work_order and self.purpose == "Manufacture":
-			self.update_so_in_serial_number()
-
 		if self.purpose == "Material Transfer" and self.add_to_transit:
 			self.set_material_request_transfer_status("In Transit")
 		if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
@@ -359,7 +356,6 @@
 
 	def validate_item(self):
 		stock_items = self.get_stock_items()
-		serialized_items = self.get_serialized_items()
 		for item in self.get("items"):
 			if flt(item.qty) and flt(item.qty) < 0:
 				frappe.throw(
@@ -401,16 +397,6 @@
 					flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item)
 				)
 
-			# if (
-			# 	self.purpose in ("Material Transfer", "Material Transfer for Manufacture")
-			# 	and not item.serial_and_batch_bundle
-			# 	and item.item_code in serialized_items
-			# ):
-			# 	frappe.throw(
-			# 		_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
-			# 		frappe.MandatoryError,
-			# 	)
-
 	def validate_qty(self):
 		manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"]
 
@@ -1352,7 +1338,6 @@
 				pro_doc.run_method("update_work_order_qty")
 				if self.purpose == "Manufacture":
 					pro_doc.run_method("update_planned_qty")
-					pro_doc.update_batch_produced_qty(self)
 
 			pro_doc.run_method("update_status")
 			if not pro_doc.operations:
@@ -1479,8 +1464,6 @@
 						"ste_detail": d.name,
 						"stock_uom": d.stock_uom,
 						"conversion_factor": d.conversion_factor,
-						"serial_no": d.serial_no,
-						"batch_no": d.batch_no,
 					},
 				)
 
@@ -1651,6 +1634,7 @@
 		if (
 			self.work_order
 			and self.pro_doc.has_batch_no
+			and not self.pro_doc.has_serial_no
 			and cint(
 				frappe.db.get_single_value(
 					"Manufacturing Settings", "make_serial_no_batch_from_work_order", cache=True
@@ -1662,42 +1646,34 @@
 			self.add_finished_goods(args, item)
 
 	def set_batchwise_finished_goods(self, args, item):
-		filters = {
-			"reference_name": self.pro_doc.name,
-			"reference_doctype": self.pro_doc.doctype,
-			"qty_to_produce": (">", 0),
-			"batch_qty": ("=", 0),
-		}
+		batches = get_empty_batches_based_work_order(self.work_order, self.pro_doc.production_item)
 
-		fields = ["qty_to_produce as qty", "produced_qty", "name"]
-
-		data = frappe.get_all("Batch", filters=filters, fields=fields, order_by="creation asc")
-
-		if not data:
+		if not batches:
 			self.add_finished_goods(args, item)
 		else:
-			self.add_batchwise_finished_good(data, args, item)
+			self.add_batchwise_finished_good(batches, args, item)
 
-	def add_batchwise_finished_good(self, data, args, item):
+	def add_batchwise_finished_good(self, batches, args, item):
 		qty = flt(self.fg_completed_qty)
+		row = frappe._dict({"batches_to_be_consume": defaultdict(float)})
 
-		for row in data:
-			batch_qty = flt(row.qty) - flt(row.produced_qty)
-			if not batch_qty:
-				continue
+		self.update_batches_to_be_consume(batches, row, qty)
 
-			if qty <= 0:
-				break
+		if not row.batches_to_be_consume:
+			return
 
-			fg_qty = batch_qty
-			if batch_qty >= qty:
-				fg_qty = qty
+		id = create_serial_and_batch_bundle(
+			row,
+			frappe._dict(
+				{
+					"item_code": self.pro_doc.production_item,
+					"warehouse": args.get("to_warehouse"),
+				}
+			),
+		)
 
-			qty -= batch_qty
-			args["qty"] = fg_qty
-			args["batch_no"] = row.name
-
-			self.add_finished_goods(args, item)
+		args["serial_and_batch_bundle"] = id
+		self.add_finished_goods(args, item)
 
 	def add_finished_goods(self, args, item):
 		self.add_to_stock_entry_detail({item.name: args}, bom_no=self.bom_no)
@@ -1902,27 +1878,8 @@
 
 			if row.batch_details:
 				row.batches_to_be_consume = defaultdict(float)
-				batches = sorted(row.batch_details.items(), key=lambda x: x[0])
-				qty_to_be_consumed = qty
-				for batch_no, batch_qty in batches:
-					if qty_to_be_consumed <= 0 or batch_qty <= 0:
-						continue
-
-					if batch_qty > qty_to_be_consumed:
-						batch_qty = qty_to_be_consumed
-
-					row.batches_to_be_consume[batch_no] += batch_qty
-
-					if batch_no and row.serial_nos:
-						serial_nos = self.get_serial_nos_based_on_transferred_batch(batch_no, row.serial_nos)
-						serial_nos = serial_nos[0 : cint(batch_qty)]
-
-						# remove consumed serial nos from list
-						for sn in serial_nos:
-							row.serial_nos.remove(sn)
-
-					row.batch_details[batch_no] -= batch_qty
-					qty_to_be_consumed -= batch_qty
+				batches = row.batch_details
+				self.update_batches_to_be_consume(batches, row, qty)
 
 			elif row.serial_nos:
 				serial_nos = row.serial_nos[0 : cint(qty)]
@@ -1930,6 +1887,32 @@
 
 			self.update_item_in_stock_entry_detail(row, item, qty)
 
+	def update_batches_to_be_consume(self, batches, row, qty):
+		qty_to_be_consumed = qty
+		batches = sorted(batches.items(), key=lambda x: x[0])
+
+		for batch_no, batch_qty in batches:
+			if qty_to_be_consumed <= 0 or batch_qty <= 0:
+				continue
+
+			if batch_qty > qty_to_be_consumed:
+				batch_qty = qty_to_be_consumed
+
+			row.batches_to_be_consume[batch_no] += batch_qty
+
+			if batch_no and row.serial_nos:
+				serial_nos = self.get_serial_nos_based_on_transferred_batch(batch_no, row.serial_nos)
+				serial_nos = serial_nos[0 : cint(batch_qty)]
+
+				# remove consumed serial nos from list
+				for sn in serial_nos:
+					row.serial_nos.remove(sn)
+
+			if "batch_details" in row:
+				row.batch_details[batch_no] -= batch_qty
+
+			qty_to_be_consumed -= batch_qty
+
 	def update_item_in_stock_entry_detail(self, row, item, qty) -> None:
 		if not qty:
 			return
@@ -1939,7 +1922,7 @@
 			"to_warehouse": "",
 			"qty": qty,
 			"item_name": item.item_name,
-			"serial_and_batch_bundle": create_serial_and_batch_bundle(row, item),
+			"serial_and_batch_bundle": create_serial_and_batch_bundle(row, item, "Outward"),
 			"description": item.description,
 			"stock_uom": item.stock_uom,
 			"expense_account": item.expense_account,
@@ -2099,8 +2082,6 @@
 				"expense_account",
 				"description",
 				"item_name",
-				"serial_no",
-				"batch_no",
 				"serial_and_batch_bundle",
 				"allow_zero_valuation_rate",
 			]:
@@ -2210,42 +2191,6 @@
 				stock_bin = get_bin(item_code, reserve_warehouse)
 				stock_bin.update_reserved_qty_for_sub_contracting()
 
-	def update_so_in_serial_number(self):
-		so_name, item_code = frappe.db.get_value(
-			"Work Order", self.work_order, ["sales_order", "production_item"]
-		)
-		if so_name and item_code:
-			qty_to_reserve = get_reserved_qty_for_so(so_name, item_code)
-			if qty_to_reserve:
-				reserved_qty = frappe.db.sql(
-					"""select count(name) from `tabSerial No` where item_code=%s and
-					sales_order=%s""",
-					(item_code, so_name),
-				)
-				if reserved_qty and reserved_qty[0][0]:
-					qty_to_reserve -= reserved_qty[0][0]
-				if qty_to_reserve > 0:
-					for item in self.items:
-						has_serial_no = frappe.get_cached_value("Item", item.item_code, "has_serial_no")
-						if item.item_code == item_code and has_serial_no:
-							serial_nos = (item.serial_no).split("\n")
-							for serial_no in serial_nos:
-								if qty_to_reserve > 0:
-									frappe.db.set_value("Serial No", serial_no, "sales_order", so_name)
-									qty_to_reserve -= 1
-
-	def validate_reserved_serial_no_consumption(self):
-		for item in self.items:
-			if item.s_warehouse and not item.t_warehouse and item.serial_no:
-				for sr in get_serial_nos(item.serial_no):
-					sales_order = frappe.db.get_value("Serial No", sr, "sales_order")
-					if sales_order:
-						msg = _(
-							"(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}."
-						).format(sr, sales_order)
-
-						frappe.throw(_("Item {0} {1}").format(item.item_code, msg))
-
 	def update_transferred_qty(self):
 		if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
 			stock_entries = {}
@@ -2338,40 +2283,48 @@
 				frappe.db.set_value("Material Request", material_request, "transfer_status", status)
 
 	def set_serial_no_batch_for_finished_good(self):
-		serial_nos = []
-		if self.pro_doc.serial_no:
-			serial_nos = self.get_serial_nos_for_fg() or []
+		if not (
+			(self.pro_doc.has_serial_no or self.pro_doc.has_batch_no)
+			and frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")
+		):
+			return
 
-		for row in self.items:
-			if row.is_finished_item and row.item_code == self.pro_doc.production_item:
+		for d in self.items:
+			if d.is_finished_item and d.item_code == self.pro_doc.production_item:
+				serial_nos = self.get_available_serial_nos()
 				if serial_nos:
-					row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)])
+					row = frappe._dict({"serial_nos": serial_nos[0 : cint(d.qty)]})
 
-	def get_serial_nos_for_fg(self):
-		fields = [
-			"`tabStock Entry`.`name`",
-			"`tabStock Entry Detail`.`qty`",
-			"`tabStock Entry Detail`.`serial_no`",
-			"`tabStock Entry Detail`.`batch_no`",
-		]
+					id = create_serial_and_batch_bundle(
+						row,
+						frappe._dict(
+							{
+								"item_code": d.item_code,
+								"warehouse": d.t_warehouse,
+							}
+						),
+					)
 
-		filters = [
-			["Stock Entry", "work_order", "=", self.work_order],
-			["Stock Entry", "purpose", "=", "Manufacture"],
-			["Stock Entry", "docstatus", "<", 2],
-			["Stock Entry Detail", "item_code", "=", self.pro_doc.production_item],
-		]
+					d.serial_and_batch_bundle = id
 
-		stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
-		return self.get_available_serial_nos(stock_entries)
+	def get_available_serial_nos(self) -> List[str]:
+		serial_nos = []
+		data = frappe.get_all(
+			"Serial No",
+			filters={
+				"item_code": self.pro_doc.production_item,
+				"warehouse": ("is", "not set"),
+				"status": "Inactive",
+				"work_order": self.pro_doc.name,
+			},
+			fields=["name"],
+			order_by="creation asc",
+		)
 
-	def get_available_serial_nos(self, stock_entries):
-		used_serial_nos = []
-		for row in stock_entries:
-			if row.serial_no:
-				used_serial_nos.extend(get_serial_nos(row.serial_no))
+		for row in data:
+			serial_nos.append(row.name)
 
-		return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
+		return serial_nos
 
 	def update_subcontracting_order_status(self):
 		if self.subcontracting_order and self.purpose in ["Send to Subcontractor", "Material Transfer"]:
@@ -2847,14 +2800,24 @@
 	return data
 
 
-def create_serial_and_batch_bundle(row, child):
+def create_serial_and_batch_bundle(row, child, type_of_transaction=None):
+	item_details = frappe.get_cached_value(
+		"Item", child.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
+	)
+
+	if not (item_details.has_serial_no or item_details.has_batch_no):
+		return
+
+	if not type_of_transaction:
+		type_of_transaction = "Inward"
+
 	doc = frappe.get_doc(
 		{
 			"doctype": "Serial and Batch Bundle",
 			"voucher_type": "Stock Entry",
 			"item_code": child.item_code,
 			"warehouse": child.warehouse,
-			"type_of_transaction": "Outward",
+			"type_of_transaction": type_of_transaction,
 		}
 	)
 
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 3b01287..56802d9 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -127,8 +127,6 @@
 
 	out.update(data)
 
-	update_stock(args, out)
-
 	if args.transaction_date and item.lead_time_days:
 		out.schedule_date = out.lead_time_date = add_days(args.transaction_date, item.lead_time_days)
 
@@ -150,28 +148,6 @@
 	return details
 
 
-def update_stock(args, out):
-	if (
-		(
-			args.get("doctype") == "Delivery Note"
-			or (args.get("doctype") == "Sales Invoice" and args.get("update_stock"))
-		)
-		and out.warehouse
-		and out.stock_qty > 0
-	):
-		if out.has_serial_no and args.get("batch_no"):
-			reserved_so = get_so_reservation_for_item(args)
-			out.batch_no = args.get("batch_no")
-			out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so)
-
-		elif out.has_serial_no:
-			reserved_so = get_so_reservation_for_item(args)
-			out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so)
-
-	if not out.serial_no:
-		out.pop("serial_no", None)
-
-
 def set_valuation_rate(out, args):
 	if frappe.db.exists("Product Bundle", args.item_code, cache=True):
 		valuation_rate = 0.0
@@ -1490,41 +1466,3 @@
 		blanket_order_details = blanket_order_details[0] if blanket_order_details else ""
 
 	return blanket_order_details
-
-
-def get_so_reservation_for_item(args):
-	reserved_so = None
-	if args.get("against_sales_order"):
-		if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")):
-			reserved_so = args.get("against_sales_order")
-	elif args.get("against_sales_invoice"):
-		sales_order = frappe.db.get_all(
-			"Sales Invoice Item",
-			filters={
-				"parent": args.get("against_sales_invoice"),
-				"item_code": args.get("item_code"),
-				"docstatus": 1,
-			},
-			fields="sales_order",
-		)
-		if sales_order and sales_order[0]:
-			if get_reserved_qty_for_so(sales_order[0].sales_order, args.get("item_code")):
-				reserved_so = sales_order[0]
-	elif args.get("sales_order"):
-		if get_reserved_qty_for_so(args.get("sales_order"), args.get("item_code")):
-			reserved_so = args.get("sales_order")
-	return reserved_so
-
-
-def get_reserved_qty_for_so(sales_order, item_code):
-	reserved_qty = frappe.db.get_value(
-		"Sales Order Item",
-		filters={
-			"parent": sales_order,
-			"item_code": item_code,
-			"ensure_delivery_based_on_produced_serial_no": 1,
-		},
-		fieldname="sum(qty)",
-	)
-
-	return reserved_qty or 0
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 2b88e8b..e375223 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -586,3 +586,62 @@
 
 	def get_incoming_rate(self):
 		return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
+
+
+def get_empty_batches_based_work_order(work_order, item_code):
+	batches = get_batches_from_work_order(work_order)
+	if not batches:
+		return batches
+
+	entries = get_batches_from_stock_entries(work_order)
+	if not entries:
+		return batches
+
+	ids = [d.serial_and_batch_bundle for d in entries if d.serial_and_batch_bundle]
+	if ids:
+		set_batch_details_from_package(ids, batches)
+
+	# Will be deprecated in v16
+	for d in entries:
+		if not d.batch_no:
+			continue
+
+		batches[d.batch_no] -= d.qty
+
+	return batches
+
+
+def get_batches_from_work_order(work_order):
+	return frappe._dict(
+		frappe.get_all(
+			"Batch", fields=["name", "qty_to_produce"], filters={"reference_name": work_order}, as_list=1
+		)
+	)
+
+
+def get_batches_from_stock_entries(work_order):
+	entries = frappe.get_all(
+		"Stock Entry",
+		filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
+		fields=["name"],
+	)
+
+	return frappe.get_all(
+		"Stock Entry Detail",
+		fields=["batch_no", "qty", "serial_and_batch_bundle"],
+		filters={
+			"parent": ("in", [d.name for d in entries]),
+			"is_finished_item": 1,
+		},
+	)
+
+
+def set_batch_details_from_package(ids, batches):
+	entries = frappe.get_all(
+		"Serial and Batch Ledger",
+		filters={"parent": ("in", ids), "is_outward": 0},
+		fields=["batch_no", "qty"],
+	)
+
+	for d in entries:
+		batches[d.batch_no] -= d.qty