refactor: serial no ledger and batchwise balance history report
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index 14717c6..ac30f82 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -4,7 +4,7 @@
 
 
 class DeprecatedSerialNoValuation:
-	# Will be deperecated in v16
+	# Will be deprecated in v16
 
 	def calculate_stock_value_from_deprecarated_ledgers(self):
 		serial_nos = list(
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 c06f63f..311b35f 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
@@ -11,7 +11,7 @@
 from frappe.utils import add_days, cint, flt, get_link_to_form, today
 from pypika import Case
 
-from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation
+from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
 
 
 class SerialNoExistsInFutureTransactionError(frappe.ValidationError):
@@ -81,14 +81,14 @@
 	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:
-			sn_obj = SerialNoBundleValuation(
+			sn_obj = SerialNoValuation(
 				sle=sle,
 				warehouse=self.item_code,
 				item_code=self.warehouse,
 			)
 
 		else:
-			sn_obj = BatchNoBundleValuation(
+			sn_obj = BatchNoValuation(
 				sle=sle,
 				warehouse=self.item_code,
 				item_code=self.warehouse,
@@ -187,9 +187,12 @@
 		self.set_incoming_rate(save=True, row=row)
 		self.calculate_qty_and_amount(save=True)
 		self.validate_quantity(row)
-		self.set_warranty_expiry_date(row)
+		self.set_warranty_expiry_date()
 
 	def set_warranty_expiry_date(self):
+		if self.type_of_transaction != "Outward":
+			return
+
 		if not (self.docstatus == 1 and self.voucher_type == "Delivery Note" and self.has_serial_no):
 			return
 
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index e0c32e4..6ffe5b3 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -493,8 +493,7 @@
 						'item_code': child.item_code,
 						'warehouse': cstr(child.s_warehouse) || cstr(child.t_warehouse),
 						'transfer_qty': child.transfer_qty,
-						'serial_no': child.serial_no,
-						'batch_no': child.batch_no,
+						'serial_and_batch_bundle': child.serial_and_batch_bundle,
 						'qty': child.s_warehouse ? -1* child.transfer_qty : child.transfer_qty,
 						'posting_date': frm.doc.posting_date,
 						'posting_time': frm.doc.posting_time,
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 a902655..7b3d7f4 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -92,6 +92,16 @@
 			as_dict=1,
 		)
 
+		values_to_be_change = {}
+		if self.has_batch_no != item_detail.has_batch_no:
+			values_to_be_change["has_batch_no"] = item_detail.has_batch_no
+
+		if self.has_serial_no != item_detail.has_serial_no:
+			values_to_be_change["has_serial_no"] = item_detail.has_serial_no
+
+		if values_to_be_change:
+			self.db_set(values_to_be_change)
+
 		if not item_detail:
 			frappe.throw(_("Item {0} not found").format(self.item_code))
 
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 621b9df..66bef50 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -157,7 +157,9 @@
 			item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=200
 		)
 
-		serial_nos = get_serial_nos(sr.items[0].serial_no)
+		serial_nos = frappe.get_doc(
+			"Serial and Batch Bundle", sr.items[0].serial_and_batch_bundle
+		).get_serial_nos()
 		self.assertEqual(len(serial_nos), 5)
 
 		args = {
@@ -165,7 +167,7 @@
 			"warehouse": serial_warehouse,
 			"posting_date": nowdate(),
 			"posting_time": nowtime(),
-			"serial_no": sr.items[0].serial_no,
+			"serial_and_batch_bundle": sr.items[0].serial_and_batch_bundle,
 		}
 
 		valuation_rate = get_incoming_rate(args)
@@ -177,7 +179,10 @@
 			item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=300
 		)
 
-		serial_nos1 = get_serial_nos(sr.items[0].serial_no)
+		serial_nos1 = frappe.get_doc(
+			"Serial and Batch Bundle", sr.items[0].serial_and_batch_bundle
+		).get_serial_nos()
+
 		self.assertEqual(len(serial_nos1), 5)
 
 		args = {
@@ -185,7 +190,7 @@
 			"warehouse": serial_warehouse,
 			"posting_date": nowdate(),
 			"posting_time": nowtime(),
-			"serial_no": sr.items[0].serial_no,
+			"serial_and_batch_bundle": sr.items[0].serial_and_batch_bundle,
 		}
 
 		valuation_rate = get_incoming_rate(args)
@@ -257,7 +262,7 @@
 		sr.save()
 		sr.submit()
 
-		batch_no = sr.items[0].batch_no
+		batch_no = sr.items[0].serial_and_batch_bundle
 		self.assertTrue(batch_no)
 		to_delete_records.append(sr.name)
 
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 0d57938..2c46082 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
@@ -67,8 +67,16 @@
 	return columns
 
 
-# get all details
 def get_stock_ledger_entries(filters):
+	# Will be deprecated in v16
+	entries = get_stock_ledger_entries_for_batch_no(filters)
+
+	entries += get_stock_ledger_entries_for_batch_bundle(filters)
+	return entries
+
+
+# get all details
+def get_stock_ledger_entries_for_batch_no(filters):
 	if not filters.get("from_date"):
 		frappe.throw(_("'From Date' is required"))
 	if not filters.get("to_date"):
@@ -99,7 +107,43 @@
 		if filters.get(field):
 			query = query.where(sle[field] == filters.get(field))
 
-	return query.run(as_dict=True)
+	return query.run(as_dict=True) or []
+
+
+def get_stock_ledger_entries_for_batch_bundle(filters):
+	sle = frappe.qb.DocType("Stock Ledger Entry")
+	batch_package = frappe.qb.DocType("Serial and Batch Entry")
+
+	query = (
+		frappe.qb.from_(sle)
+		.inner_join(batch_package)
+		.on(batch_package.parent == sle.serial_and_batch_bundle)
+		.select(
+			sle.item_code,
+			sle.warehouse,
+			batch_package.batch_no,
+			sle.posting_date,
+			fn.Sum(batch_package.qty).as_("actual_qty"),
+		)
+		.where(
+			(sle.docstatus < 2)
+			& (sle.is_cancelled == 0)
+			& (sle.has_batch_no == 1)
+			& (sle.posting_date <= filters["to_date"])
+		)
+		.groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse)
+		.orderby(sle.item_code, sle.warehouse)
+	)
+
+	query = apply_warehouse_filter(query, sle, filters)
+	for field in ["item_code", "batch_no", "company"]:
+		if filters.get(field):
+			if field == "batch_no":
+				query = query.where(batch_package[field] == filters.get(field))
+			else:
+				query = query.where(sle[field] == filters.get(field))
+
+	return query.run(as_dict=True) or []
 
 
 def get_item_warehouse_batch_map(filters, float_precision):
diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js
index 616312e..976e515 100644
--- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js
+++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.js
@@ -19,13 +19,6 @@
 			}
 		},
 		{
-			'label': __('Serial No'),
-			'fieldtype': 'Link',
-			'fieldname': 'serial_no',
-			'options': 'Serial No',
-			'reqd': 1
-		},
-		{
 			'label': __('Warehouse'),
 			'fieldtype': 'Link',
 			'fieldname': 'warehouse',
@@ -43,10 +36,35 @@
 			}
 		},
 		{
+			'label': __('Serial No'),
+			'fieldtype': 'Link',
+			'fieldname': 'serial_no',
+			'options': 'Serial No',
+			get_query: function() {
+				let item_code = frappe.query_report.get_filter_value('item_code');
+				let warehouse = frappe.query_report.get_filter_value('warehouse');
+
+				let query_filters = {'item_code': item_code};
+				if (warehouse) {
+					query_filters['warehouse'] = warehouse;
+				}
+
+				return {
+					filters: query_filters
+				}
+			}
+		},
+		{
 			'label': __('As On Date'),
 			'fieldtype': 'Date',
 			'fieldname': 'posting_date',
 			'default': frappe.datetime.get_today()
 		},
+		{
+			'label': __('Posting Time'),
+			'fieldtype': 'Time',
+			'fieldname': 'posting_time',
+			'default': frappe.datetime.get_time()
+		},
 	]
 };
diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py
index e439f51..99f1a94 100644
--- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py
+++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py
@@ -1,7 +1,7 @@
 # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
-
+import frappe
 from frappe import _
 
 from erpnext.stock.stock_ledger import get_stock_ledger_entries
@@ -45,10 +45,71 @@
 			"options": "Warehouse",
 			"width": 220,
 		},
+		{
+			"label": _("Serial No"),
+			"fieldtype": "Link",
+			"fieldname": "serial_no",
+			"options": "Serial No",
+			"width": 220,
+		},
 	]
 
 	return columns
 
 
 def get_data(filters):
-	return get_stock_ledger_entries(filters, "<=", order="asc") or []
+	stock_ledgers = get_stock_ledger_entries(filters, "<=", order="asc", check_serial_no=False)
+
+	if not stock_ledgers:
+		return []
+
+	data = []
+	serial_bundle_ids = [
+		d.serial_and_batch_bundle for d in stock_ledgers if d.serial_and_batch_bundle
+	]
+
+	bundle_wise_serial_nos = get_serial_nos(filters, serial_bundle_ids)
+
+	for row in stock_ledgers:
+		args = frappe._dict(
+			{
+				"posting_date": row.posting_date,
+				"posting_time": row.posting_time,
+				"voucher_type": row.voucher_type,
+				"voucher_no": row.voucher_no,
+				"company": row.company,
+				"warehouse": row.warehouse,
+			}
+		)
+
+		serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
+
+		for index, serial_no in enumerate(serial_nos):
+			if index == 0:
+				args.serial_no = serial_no
+				data.append(args)
+			else:
+				data.append(
+					{
+						"serial_no": serial_no,
+					}
+				)
+
+	return data
+
+
+def get_serial_nos(filters, serial_bundle_ids):
+	bundle_wise_serial_nos = {}
+	bundle_filters = {"parent": ["in", serial_bundle_ids]}
+	if filters.get("serial_no"):
+		bundle_filters["serial_no"] = filters.get("serial_no")
+
+	for d in frappe.get_all(
+		"Serial and Batch Entry",
+		fields=["serial_no", "parent"],
+		filters=bundle_filters,
+		order_by="idx asc",
+	):
+		bundle_wise_serial_nos.setdefault(d.parent, []).append(d.serial_no)
+
+	return bundle_wise_serial_nos
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index f2de819..1266133 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -341,7 +341,7 @@
 	return [d.serial_no for d in entries]
 
 
-class SerialNoBundleValuation(DeprecatedSerialNoValuation):
+class SerialNoValuation(DeprecatedSerialNoValuation):
 	def __init__(self, **kwargs):
 		for key, value in kwargs.items():
 			setattr(self, key, value)
@@ -470,7 +470,7 @@
 	return False
 
 
-class BatchNoBundleValuation(DeprecatedBatchNoValuation):
+class BatchNoValuation(DeprecatedBatchNoValuation):
 	def __init__(self, **kwargs):
 		for key, value in kwargs.items():
 			setattr(self, key, value)
@@ -567,11 +567,11 @@
 
 
 def get_empty_batches_based_work_order(work_order, item_code):
-	batches = get_batches_from_work_order(work_order)
+	batches = get_batches_from_work_order(work_order, item_code)
 	if not batches:
 		return batches
 
-	entries = get_batches_from_stock_entries(work_order)
+	entries = get_batches_from_stock_entries(work_order, item_code)
 	if not entries:
 		return batches
 
@@ -589,15 +589,18 @@
 	return batches
 
 
-def get_batches_from_work_order(work_order):
+def get_batches_from_work_order(work_order, item_code):
 	return frappe._dict(
 		frappe.get_all(
-			"Batch", fields=["name", "qty_to_produce"], filters={"reference_name": work_order}, as_list=1
+			"Batch",
+			fields=["name", "qty_to_produce"],
+			filters={"reference_name": work_order, "item": item_code},
+			as_list=1,
 		)
 	)
 
 
-def get_batches_from_stock_entries(work_order):
+def get_batches_from_stock_entries(work_order, item_code):
 	entries = frappe.get_all(
 		"Stock Entry",
 		filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
@@ -610,6 +613,7 @@
 		filters={
 			"parent": ("in", [d.name for d in entries]),
 			"is_finished_item": 1,
+			"item_code": item_code,
 		},
 	)
 
@@ -623,3 +627,21 @@
 
 	for d in entries:
 		batches[d.batch_no] -= d.qty
+
+
+class SerialBatchCreation:
+	def __init__(self, args):
+		for key, value in args.items():
+			setattr(self, key, value)
+
+	def duplicate_package(self):
+		if not self.serial_and_batch_bundle:
+			return
+
+		id = self.serial_and_batch_bundle
+		package = frappe.get_doc("Serial and Batch Bundle", id)
+		new_package = frappe.copy_doc(package)
+		new_package.type_of_transaction = self.type_of_transaction
+		new_package.save()
+
+		self.serial_and_batch_bundle = new_package.name
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index dfb7786..e616ed0 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -27,7 +27,7 @@
 from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
 	get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
 )
-from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation
+from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
 from erpnext.stock.utils import (
 	get_incoming_outgoing_rate_for_cancel,
 	get_or_make_bin,
@@ -693,7 +693,7 @@
 
 		if sle.serial_and_batch_bundle:
 			if frappe.get_cached_value("Item", sle.item_code, "has_serial_no"):
-				SerialNoBundleValuation(
+				SerialNoValuation(
 					sle=sle,
 					sle_self=self,
 					wh_data=self.wh_data,
@@ -701,7 +701,7 @@
 					item_code=sle.item_code,
 				)
 			else:
-				BatchNoBundleValuation(
+				BatchNoValuation(
 					sle=sle,
 					sle_self=self,
 					wh_data=self.wh_data,
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 18e0b90..8d1ec54 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -12,7 +12,7 @@
 
 import erpnext
 from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
-from erpnext.stock.serial_batch_bundle import BatchNoBundleValuation, SerialNoBundleValuation
+from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
 from erpnext.stock.valuation import FIFOValuation, LIFOValuation
 
 BarcodeScanResult = Dict[str, Optional[str]]
@@ -264,7 +264,7 @@
 
 	if item_details.has_serial_no and args.get("serial_and_batch_bundle"):
 		args.actual_qty = args.qty
-		sn_obj = SerialNoBundleValuation(
+		sn_obj = SerialNoValuation(
 			sle=args,
 			warehouse=args.get("warehouse"),
 			item_code=args.get("item_code"),
@@ -274,7 +274,7 @@
 
 	elif item_details.has_batch_no and args.get("serial_and_batch_bundle"):
 		args.actual_qty = args.qty
-		batch_obj = BatchNoBundleValuation(
+		batch_obj = BatchNoValuation(
 			sle=args,
 			warehouse=args.get("warehouse"),
 			item_code=args.get("item_code"),