fix: travis issue
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index e14f9e6..bf393c0 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -128,6 +128,7 @@
 				doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
 
 				if doc.docstatus == 0:
+					doc.flags.ignore_voucher_validation = True
 					doc.submit()
 
 	def check_phone_payments(self):
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index 8b4279b..b7c5d57 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -76,7 +76,6 @@
 
 	@deprecated
 	def get_sle_for_batches(self):
-		batch_nos = list(self.batch_nos.keys())
 		sle = frappe.qb.DocType("Stock Ledger Entry")
 
 		timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
@@ -88,7 +87,11 @@
 				== CombineDatetime(self.sle.posting_date, self.sle.posting_time)
 			) & (sle.creation < self.sle.creation)
 
-		return (
+		batch_nos = self.batch_nos
+		if isinstance(self.batch_nos, dict):
+			batch_nos = list(self.batch_nos.keys())
+
+		query = (
 			frappe.qb.from_(sle)
 			.select(
 				sle.batch_no,
@@ -97,11 +100,15 @@
 			)
 			.where(
 				(sle.item_code == self.sle.item_code)
-				& (sle.name != self.sle.name)
 				& (sle.warehouse == self.sle.warehouse)
 				& (sle.batch_no.isin(batch_nos))
 				& (sle.is_cancelled == 0)
 			)
 			.where(timestamp_condition)
 			.groupby(sle.batch_no)
-		).run(as_dict=True)
+		)
+
+		if self.sle.name:
+			query = query.where(sle.name != self.sle.name)
+
+		return query.run(as_dict=True)
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 84ab74a..88a0372 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -8,8 +8,8 @@
 from frappe import _
 from frappe.model.document import Document
 from frappe.model.naming import make_autoname, revert_series_if_last
-from frappe.query_builder.functions import CombineDatetime, CurDate, Sum
-from frappe.utils import cint, flt, get_link_to_form, nowtime
+from frappe.query_builder.functions import CurDate, Sum
+from frappe.utils import cint, flt, get_link_to_form
 from frappe.utils.data import add_days
 from frappe.utils.jinja import render_template
 
@@ -179,44 +179,28 @@
 	:param warehouse: Optional - give qty for this warehouse
 	:param item_code: Optional - give qty for this item"""
 
-	sle = frappe.qb.DocType("Stock Ledger Entry")
+	from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+		get_auto_batch_nos,
+	)
 
-	out = 0
-	if batch_no and warehouse:
-		query = (
-			frappe.qb.from_(sle)
-			.select(Sum(sle.actual_qty))
-			.where((sle.is_cancelled == 0) & (sle.warehouse == warehouse) & (sle.batch_no == batch_no))
-		)
+	batchwise_qty = defaultdict(float)
+	kwargs = frappe._dict({
+		"item_code": item_code,
+		"warehouse": warehouse,
+		"posting_date": posting_date,
+		"posting_time": posting_time,
+		"batch_no": batch_no
+	})
 
-		if posting_date:
-			if posting_time is None:
-				posting_time = nowtime()
+	batches = get_auto_batch_nos(kwargs)
 
-			query = query.where(
-				CombineDatetime(sle.posting_date, sle.posting_time)
-				<= CombineDatetime(posting_date, posting_time)
-			)
+	if not (batch_no and warehouse):
+		return batches
 
-		out = query.run(as_list=True)[0][0] or 0
+	for batch in batches:
+		batchwise_qty[batch.get("batch_no")] += batch.get("qty")
 
-	if batch_no and not warehouse:
-		out = (
-			frappe.qb.from_(sle)
-			.select(sle.warehouse, Sum(sle.actual_qty).as_("qty"))
-			.where((sle.is_cancelled == 0) & (sle.batch_no == batch_no))
-			.groupby(sle.warehouse)
-		).run(as_dict=True)
-
-	if not batch_no and item_code and warehouse:
-		out = (
-			frappe.qb.from_(sle)
-			.select(sle.batch_no, Sum(sle.actual_qty).as_("qty"))
-			.where((sle.is_cancelled == 0) & (sle.item_code == item_code) & (sle.warehouse == warehouse))
-			.groupby(sle.batch_no)
-		).run(as_dict=True)
-
-	return out
+	return batchwise_qty[batch_no]
 
 
 @frappe.whitelist()
@@ -366,3 +350,14 @@
 		batchwise_qty[batch.get("batch_no")] += batch.get("qty")
 
 	return batchwise_qty
+
+
+def get_batch_no(bundle_id):
+	from erpnext.stock.serial_batch_bundle import get_batch_nos
+
+	batches = defaultdict(float)
+
+	for batch_id, d in get_batch_nos(bundle_id).items():
+		batches[batch_id] += abs(d.get("qty"))
+
+	return batches
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 271e2e0..cf0d3f2 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -10,15 +10,15 @@
 from frappe.utils.data import add_to_date, getdate
 
 from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
-from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
+from erpnext.stock.doctype.batch.batch import get_batch_no, get_batch_qty
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
-from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
-	create_stock_reconciliation,
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+	BatchNegativeStockError,
 )
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 from erpnext.stock.get_item_details import get_item_details
-from erpnext.stock.stock_ledger import get_valuation_rate
+from erpnext.stock.serial_batch_bundle import SerialBatchCreation
 
 
 class TestBatch(FrappeTestCase):
@@ -49,8 +49,10 @@
 		).insert()
 		receipt.submit()
 
-		self.assertTrue(receipt.items[0].batch_no)
-		self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), batch_qty)
+		receipt.load_from_db()
+		self.assertTrue(receipt.items[0].serial_and_batch_bundle)
+		batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
+		self.assertEqual(get_batch_qty(batch_no, receipt.items[0].warehouse), batch_qty)
 
 		return receipt
 
@@ -80,9 +82,12 @@
 		stock_entry.insert()
 		stock_entry.submit()
 
-		self.assertTrue(stock_entry.items[0].batch_no)
+		stock_entry.load_from_db()
+
+		bundle = stock_entry.items[0].serial_and_batch_bundle
+		self.assertTrue(bundle)
 		self.assertEqual(
-			get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90
+			get_batch_qty(get_batch_from_bundle(bundle), stock_entry.items[0].t_warehouse), 90
 		)
 
 	def test_delivery_note(self):
@@ -103,25 +108,35 @@
 		).insert()
 		delivery_note.submit()
 
+		receipt.load_from_db()
+		delivery_note.load_from_db()
+
 		# shipped from FEFO batch
 		self.assertEqual(
-			delivery_note.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty)
+			get_batch_no(delivery_note.items[0].serial_and_batch_bundle),
+			get_batch_no(receipt.items[0].serial_and_batch_bundle),
 		)
 
-	def test_delivery_note_fail(self):
+	def test_batch_negative_stock_error(self):
 		"""Test automatic batch selection for outgoing items"""
 		receipt = self.test_purchase_receipt(100)
-		delivery_note = frappe.get_doc(
-			dict(
-				doctype="Delivery Note",
-				customer="_Test Customer",
-				company=receipt.company,
-				items=[
-					dict(item_code="ITEM-BATCH-1", qty=5000, rate=10, warehouse=receipt.items[0].warehouse)
-				],
-			)
+
+		receipt.load_from_db()
+		batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
+		sn_doc = SerialBatchCreation(
+			{
+				"item_code": "ITEM-BATCH-1",
+				"warehouse": receipt.items[0].warehouse,
+				"voucher_type": "Delivery Note",
+				"qty": 5000,
+				"avg_rate": 10,
+				"batches": frappe._dict({batch_no: 90}),
+				"type_of_transaction": "Outward",
+				"company": receipt.company,
+			}
 		)
-		self.assertRaises(UnableToSelectBatchError, delivery_note.insert)
+
+		self.assertRaises(BatchNegativeStockError, sn_doc.make_serial_and_batch_bundle)
 
 	def test_stock_entry_outgoing(self):
 		"""Test automatic batch selection for outgoing stock entry"""
@@ -149,9 +164,9 @@
 		stock_entry.insert()
 		stock_entry.submit()
 
-		# assert same batch is selected
 		self.assertEqual(
-			stock_entry.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty)
+			get_batch_no(stock_entry.items[0].serial_and_batch_bundle),
+			get_batch_no(receipt.items[0].serial_and_batch_bundle),
 		)
 
 	def test_batch_split(self):
@@ -201,6 +216,19 @@
 			)
 			batch.save()
 
+		sn_doc = SerialBatchCreation(
+			{
+				"item_code": item_name,
+				"warehouse": warehouse,
+				"voucher_type": "Stock Entry",
+				"qty": 90,
+				"avg_rate": 10,
+				"batches": frappe._dict({batch_name: 90}),
+				"type_of_transaction": "Inward",
+				"company": "_Test Company",
+			}
+		).make_serial_and_batch_bundle()
+
 		stock_entry = frappe.get_doc(
 			dict(
 				doctype="Stock Entry",
@@ -210,10 +238,10 @@
 					dict(
 						item_code=item_name,
 						qty=90,
+						serial_and_batch_bundle=sn_doc.name,
 						t_warehouse=warehouse,
 						cost_center="Main - _TC",
 						rate=10,
-						batch_no=batch_name,
 						allow_zero_valuation_rate=1,
 					)
 				],
@@ -320,7 +348,8 @@
 		batches = {}
 		for rate in rates:
 			se = make_stock_entry(item_code=item_code, qty=10, rate=rate, target=warehouse)
-			batches[se.items[0].batch_no] = rate
+			batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
+			batches[batch_no] = rate
 
 		LOW, HIGH = list(batches.keys())
 
@@ -341,7 +370,9 @@
 
 			sle = frappe.get_last_doc("Stock Ledger Entry", {"is_cancelled": 0, "voucher_no": se.name})
 
-			stock_value_difference = sle.actual_qty * batches[sle.batch_no]
+			stock_value_difference = (
+				sle.actual_qty * batches[get_batch_from_bundle(sle.serial_and_batch_bundle)]
+			)
 			self.assertAlmostEqual(sle.stock_value_difference, stock_value_difference)
 
 			stock_value += stock_value_difference
@@ -353,45 +384,6 @@
 
 			self.assertEqual(json.loads(sle.stock_queue), [])  # queues don't apply on batched items
 
-	def test_moving_batch_valuation_rates(self):
-		item_code = "_TestBatchWiseVal"
-		warehouse = "_Test Warehouse - _TC"
-		self.make_batch_item(item_code)
-
-		def assertValuation(expected):
-			actual = get_valuation_rate(
-				item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no
-			)
-			self.assertAlmostEqual(actual, expected)
-
-		se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse)
-		batch_no = se.items[0].batch_no
-		assertValuation(10)
-
-		# consumption should never affect current valuation rate
-		make_stock_entry(item_code=item_code, qty=20, source=warehouse)
-		assertValuation(10)
-
-		make_stock_entry(item_code=item_code, qty=30, source=warehouse)
-		assertValuation(10)
-
-		# 50 * 10 = 500 current value, add more item with higher valuation
-		make_stock_entry(item_code=item_code, qty=50, rate=20, target=warehouse, batch_no=batch_no)
-		assertValuation(15)
-
-		# consuming again shouldn't do anything
-		make_stock_entry(item_code=item_code, qty=20, source=warehouse)
-		assertValuation(15)
-
-		# reset rate with stock reconiliation
-		create_stock_reconciliation(
-			item_code=item_code, warehouse=warehouse, qty=10, rate=25, batch_no=batch_no
-		)
-		assertValuation(25)
-
-		make_stock_entry(item_code=item_code, qty=20, rate=20, target=warehouse, batch_no=batch_no)
-		assertValuation((20 * 20 + 10 * 25) / (10 + 20))
-
 	def test_update_batch_properties(self):
 		item_code = "_TestBatchWiseVal"
 		self.make_batch_item(item_code)
@@ -430,6 +422,12 @@
 		self.assertEqual("BATCHEXISTING002", pr_2.items[0].batch_no)
 
 
+def get_batch_from_bundle(bundle):
+	batches = get_batch_no(bundle)
+
+	return list(batches.keys())[0]
+
+
 def create_batch(item_code, rate, create_item_price_for_batch):
 	pi = make_purchase_invoice(
 		company="_Test Company",
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 0624ae9..6f15215 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
@@ -9,7 +9,7 @@
 from frappe import _, bold
 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 frappe.utils import add_days, cint, flt, get_link_to_form, nowtime, today
 
 from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
 
@@ -18,6 +18,10 @@
 	pass
 
 
+class BatchNegativeStockError(frappe.ValidationError):
+	pass
+
+
 class SerialandBatchBundle(Document):
 	def validate(self):
 		self.validate_serial_and_batch_no()
@@ -81,7 +85,7 @@
 
 	def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
 		sle = self.get_sle_for_outward_transaction(row)
-		if not sle.actual_qty:
+		if not sle.actual_qty and sle.qty:
 			sle.actual_qty = sle.qty
 
 		if self.has_serial_no:
@@ -122,7 +126,7 @@
 				of quantity {bold(available_qty)} in the
 				warehouse {self.warehouse}"""
 
-			frappe.throw(_(msg))
+			frappe.throw(_(msg), BatchNegativeStockError)
 
 	def get_sle_for_outward_transaction(self, row):
 		return frappe._dict(
@@ -228,7 +232,13 @@
 		if self.voucher_no and not frappe.db.exists(self.voucher_type, self.voucher_no):
 			self.throw_error_message(f"The {self.voucher_type} # {self.voucher_no} does not exist")
 
-		if frappe.get_cached_value(self.voucher_type, self.voucher_no, "docstatus") != 1:
+		if self.flags.ignore_voucher_validation:
+			return
+
+		if (
+			self.docstatus == 1
+			and frappe.get_cached_value(self.voucher_type, self.voucher_no, "docstatus") != 1
+		):
 			self.throw_error_message(f"The {self.voucher_type} # {self.voucher_no} should be submit first.")
 
 	def check_future_entries_exists(self):
@@ -750,6 +760,16 @@
 		.groupby(batch_ledger.batch_no)
 	)
 
+	if kwargs.get("posting_date"):
+		if kwargs.get("posting_time") is None:
+			kwargs.posting_time = nowtime()
+
+		timestamp_condition = CombineDatetime(
+			stock_ledger_entry.posting_date, stock_ledger_entry.posting_time
+		) <= CombineDatetime(kwargs.posting_date, kwargs.posting_time)
+
+		query = query.where(timestamp_condition)
+
 	for field in ["warehouse", "item_code"]:
 		if not kwargs.get(field):
 			continue
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 8788e15..17e6d83 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -405,28 +405,6 @@
 		}
 	},
 
-	set_serial_no: function(frm, cdt, cdn, callback) {
-		var d = frappe.model.get_doc(cdt, cdn);
-		if(!d.item_code && !d.s_warehouse && !d.qty) return;
-		var	args = {
-			'item_code'	: d.item_code,
-			'warehouse'	: cstr(d.s_warehouse),
-			'stock_qty'		: d.transfer_qty
-		};
-		frappe.call({
-			method: "erpnext.stock.get_item_details.get_serial_no",
-			args: {"args": args},
-			callback: function(r) {
-				if (!r.exe && r.message){
-					frappe.model.set_value(cdt, cdn, "serial_no", r.message);
-				}
-				if (callback) {
-					callback();
-				}
-			}
-		});
-	},
-
 	make_retention_stock_entry: function(frm) {
 		frappe.call({
 			method: "erpnext.stock.doctype.stock_entry.stock_entry.move_sample_to_retention_warehouse",
@@ -682,9 +660,7 @@
 
 frappe.ui.form.on('Stock Entry Detail', {
 	qty(frm, cdt, cdn) {
-		frm.events.set_serial_no(frm, cdt, cdn, () => {
-			frm.events.set_basic_rate(frm, cdt, cdn);
-		});
+		frm.events.set_basic_rate(frm, cdt, cdn);
 	},
 
 	conversion_factor(frm, cdt, cdn) {
@@ -692,9 +668,7 @@
 	},
 
 	s_warehouse(frm, cdt, cdn) {
-		frm.events.set_serial_no(frm, cdt, cdn, () => {
-			frm.events.get_warehouse_details(frm, cdt, cdn);
-		});
+		frm.events.get_warehouse_details(frm, cdt, cdn);
 
 		// set allow_zero_valuation_rate to 0 if s_warehouse is selected.
 		let item = frappe.get_doc(cdt, cdn);
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index fb5a93c..056a3ae 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -747,7 +747,7 @@
 					currency=erpnext.get_company_currency(self.company),
 					company=self.company,
 					raise_error_if_no_rate=raise_error_if_no_rate,
-					batch_no=d.batch_no,
+					serial_and_batch_bundle=d.serial_and_batch_bundle,
 				)
 
 			# do not round off basic rate to avoid precision loss
@@ -904,6 +904,9 @@
 			return
 
 		for row in self.items:
+			if not row.s_warehouse:
+				continue
+
 			if row.serial_and_batch_bundle or row.item_code not in serial_or_batch_items:
 				continue
 
@@ -915,7 +918,7 @@
 					"posting_time": self.posting_time,
 					"voucher_type": self.doctype,
 					"voucher_detail_no": row.name,
-					"total_qty": row.qty,
+					"qty": row.qty * -1,
 					"type_of_transaction": "Outward",
 					"company": self.company,
 					"do_not_submit": True,
@@ -1437,10 +1440,8 @@
 				"qty": args.get("qty"),
 				"transfer_qty": args.get("qty"),
 				"conversion_factor": 1,
-				"batch_no": "",
 				"actual_qty": 0,
 				"basic_rate": 0,
-				"serial_no": "",
 				"has_serial_no": item.has_serial_no,
 				"has_batch_no": item.has_batch_no,
 				"sample_quantity": item.sample_quantity,
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index 0f90013..674a49b 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -52,6 +52,7 @@
 	:do_not_save: Optional flag
 	:do_not_submit: Optional flag
 	"""
+	from erpnext.stock.serial_batch_bundle import SerialBatchCreation
 
 	def process_serial_numbers(serial_nos_list):
 		serial_nos_list = [
@@ -131,16 +132,27 @@
 	# We can find out the serial number using the batch source document
 	serial_number = args.serial_no
 
+	bundle_id = None
 	if not args.serial_no and args.qty and args.batch_no:
-		serial_number_list = frappe.get_list(
-			doctype="Stock Ledger Entry",
-			fields=["serial_no"],
-			filters={"batch_no": args.batch_no, "warehouse": args.from_warehouse},
+		batches = frappe._dict({args.batch_no: args.qty})
+
+		bundle_id = (
+			SerialBatchCreation(
+				{
+					"item_code": args.item,
+					"warehouse": args.source or args.target,
+					"voucher_type": "Stock Entry",
+					"total_qty": args.qty * (-1 if args.source else 1),
+					"batches": batches,
+					"type_of_transaction": "Outward" if args.source else "Inward",
+					"company": s.company,
+				}
+			)
+			.make_serial_and_batch_bundle()
+			.name
 		)
-		serial_number = process_serial_numbers(serial_number_list)
 
 	args.serial_no = serial_number
-
 	s.append(
 		"items",
 		{
@@ -148,6 +160,7 @@
 			"s_warehouse": args.source,
 			"t_warehouse": args.target,
 			"qty": args.qty,
+			"serial_and_batch_bundle": bundle_id,
 			"basic_rate": args.rate or args.basic_rate,
 			"conversion_factor": args.conversion_factor or 1.0,
 			"transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0),
@@ -164,4 +177,7 @@
 		s.insert()
 		if not args.do_not_submit:
 			s.submit()
+
+	s.load_from_db()
+
 	return s
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index c14df3b..7a6190e 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -67,7 +67,7 @@
 				"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,
+				"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",
@@ -136,7 +136,6 @@
 			and not self.sle.serial_and_batch_bundle
 			and self.item_details.has_batch_no == 1
 			and self.item_details.create_new_batch
-			and self.item_details.batch_number_series
 		):
 			self.make_serial_batch_no_bundle()
 		elif not self.sle.is_cancelled:
@@ -393,7 +392,7 @@
 		self.calculate_valuation_rate()
 
 	def calculate_avg_rate(self):
-		if self.sle.actual_qty > 0:
+		if flt(self.sle.actual_qty) > 0:
 			self.stock_value_change = frappe.get_cached_value(
 				"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
 			)
@@ -414,7 +413,9 @@
 		parent = frappe.qb.DocType("Serial and Batch Bundle")
 		child = frappe.qb.DocType("Serial and Batch Entry")
 
-		batch_nos = list(self.batch_nos.keys())
+		batch_nos = self.batch_nos
+		if isinstance(self.batch_nos, dict):
+			batch_nos = list(self.batch_nos.keys())
 
 		timestamp_condition = ""
 		if self.sle.posting_date and self.sle.posting_time:
@@ -433,7 +434,6 @@
 			)
 			.where(
 				(child.batch_no.isin(batch_nos))
-				& (child.parent != self.sle.serial_and_batch_bundle)
 				& (parent.warehouse == self.sle.warehouse)
 				& (parent.item_code == self.sle.item_code)
 				& (parent.docstatus == 1)
@@ -443,8 +443,11 @@
 			.groupby(child.batch_no)
 		)
 
+		if self.sle.serial_and_batch_bundle:
+			query = query.where(child.parent != self.sle.serial_and_batch_bundle)
+
 		if timestamp_condition:
-			query.where(timestamp_condition)
+			query = query.where(timestamp_condition)
 
 		return query.run(as_dict=True)
 
@@ -455,6 +458,9 @@
 		return get_batch_nos(self.sle.serial_and_batch_bundle)
 
 	def set_stock_value_difference(self):
+		if not self.sle.serial_and_batch_bundle:
+			return
+
 		self.stock_value_change = 0
 		for batch_no, ledger in self.batch_nos.items():
 			stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
@@ -471,11 +477,10 @@
 			self.wh_data.stock_value + self.stock_value_change
 		)
 
+		self.wh_data.qty_after_transaction += self.sle.actual_qty
 		if self.wh_data.qty_after_transaction:
 			self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
 
-		self.wh_data.qty_after_transaction += self.sle.actual_qty
-
 	def get_incoming_rate(self):
 		return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
 
@@ -484,7 +489,8 @@
 	entries = frappe.get_all(
 		"Serial and Batch Entry",
 		fields=["batch_no", "qty", "name"],
-		filters={"parent": serial_and_batch_bundle, "is_outward": 1},
+		filters={"parent": serial_and_batch_bundle},
+		order_by="idx",
 	)
 
 	return {d.batch_no: d for d in entries}
@@ -591,6 +597,12 @@
 			setattr(self, "posting_date", today())
 			self.__dict__["posting_date"] = self.posting_date
 
+		if not self.get("actual_qty"):
+			qty = self.get("qty") or self.get("total_qty")
+
+			setattr(self, "actual_qty", qty)
+			self.__dict__["actual_qty"] = self.actual_qty
+
 	def duplicate_package(self):
 		if not self.serial_and_batch_bundle:
 			return
@@ -613,14 +625,14 @@
 
 		if self.type_of_transaction == "Outward":
 			self.set_auto_serial_batch_entries_for_outward()
-		elif self.type_of_transaction == "Inward":
+		elif self.type_of_transaction == "Inward" and not self.get("batches"):
 			self.set_auto_serial_batch_entries_for_inward()
 
 		self.set_serial_batch_entries(doc)
-		doc.set_incoming_rate()
 		doc.save()
 
 		if not hasattr(self, "do_not_submit") or not self.do_not_submit:
+			doc.flags.ignore_voucher_validation = True
 			doc.submit()
 
 		return doc
@@ -633,7 +645,7 @@
 			{
 				"item_code": self.item_code,
 				"warehouse": self.warehouse,
-				"qty": abs(self.total_qty),
+				"qty": abs(self.actual_qty),
 				"based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
 			}
 		)
@@ -651,7 +663,7 @@
 		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)})
+			self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
 
 	def set_serial_batch_entries(self, doc):
 		if self.get("serial_nos"):
@@ -698,9 +710,9 @@
 		return make_batch(
 			frappe._dict(
 				{
-					"item": self.item_code,
-					"reference_doctype": self.voucher_type,
-					"reference_name": self.voucher_no,
+					"item": self.get("item_code"),
+					"reference_doctype": self.get("voucher_type"),
+					"reference_name": self.get("voucher_no"),
 				}
 			)
 		)
@@ -709,7 +721,7 @@
 		sr_nos = []
 		serial_nos_details = []
 
-		for i in range(abs(cint(self.total_qty))):
+		for i in range(abs(cint(self.actual_qty))):
 			serial_no = make_autoname(self.serial_no_series, "Serial No")
 			sr_nos.append(serial_no)
 			serial_nos_details.append(
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index e616ed0..aefc692 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -732,6 +732,7 @@
 		self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
 		if not self.wh_data.qty_after_transaction:
 			self.wh_data.stock_value = 0.0
+
 		stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
 		self.wh_data.prev_stock_value = self.wh_data.stock_value
 
@@ -1421,7 +1422,7 @@
 	currency=None,
 	company=None,
 	raise_error_if_no_rate=True,
-	batch_no=None,
+	serial_and_batch_bundle=None,
 ):
 
 	if not company:
@@ -1430,21 +1431,20 @@
 	last_valuation_rate = None
 
 	# Get moving average rate of a specific batch number
-	if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
-		last_valuation_rate = frappe.db.sql(
-			"""
-			select sum(stock_value_difference) / sum(actual_qty)
-			from `tabStock Ledger Entry`
-			where
-				item_code = %s
-				AND warehouse = %s
-				AND batch_no = %s
-				AND is_cancelled = 0
-				AND NOT (voucher_no = %s AND voucher_type = %s)
-			""",
-			(item_code, warehouse, batch_no, voucher_no, voucher_type),
+	if warehouse and serial_and_batch_bundle:
+		batch_obj = BatchNoValuation(
+			sle=frappe._dict(
+				{
+					"item_code": item_code,
+					"warehouse": warehouse,
+					"actual_qty": -1,
+					"serial_and_batch_bundle": serial_and_batch_bundle,
+				}
+			)
 		)
 
+		return batch_obj.get_incoming_rate()
+
 	# Get valuation rate from last sle for the same item and warehouse
 	if not last_valuation_rate or last_valuation_rate[0][0] is None:
 		last_valuation_rate = frappe.db.sql(