feat: serial and batch bundle for POS
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index dca93e8..f926512 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -3,7 +3,7 @@
 
 
 import frappe
-from frappe import _
+from frappe import _, bold
 from frappe.query_builder.functions import IfNull, Sum
 from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
 
@@ -16,12 +16,7 @@
 	update_multi_mode_option,
 )
 from erpnext.accounts.party import get_due_date, get_party_account
-from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
-from erpnext.stock.doctype.serial_no.serial_no import (
-	get_delivered_serial_nos,
-	get_pos_reserved_serial_nos,
-	get_serial_nos,
-)
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 
 
 class POSInvoice(SalesInvoice):
@@ -71,6 +66,7 @@
 			self.apply_loyalty_points()
 		self.check_phone_payments()
 		self.set_status(update=True)
+		self.submit_serial_batch_bundle()
 
 		if self.coupon_code:
 			from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
@@ -112,6 +108,14 @@
 
 			update_coupon_code_count(self.coupon_code, "cancelled")
 
+	def submit_serial_batch_bundle(self):
+		for item in self.items:
+			if item.serial_and_batch_bundle:
+				doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
+
+				if doc.docstatus == 0:
+					doc.submit()
+
 	def check_phone_payments(self):
 		for pay in self.payments:
 			if pay.type == "Phone" and pay.amount >= 0:
@@ -129,88 +133,6 @@
 				if paid_amt and pay.amount != paid_amt:
 					return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
 
-	def validate_pos_reserved_serial_nos(self, item):
-		serial_nos = get_serial_nos(item.serial_no)
-		filters = {"item_code": item.item_code, "warehouse": item.warehouse}
-		if item.batch_no:
-			filters["batch_no"] = item.batch_no
-
-		reserved_serial_nos = get_pos_reserved_serial_nos(filters)
-		invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
-
-		bold_invalid_serial_nos = frappe.bold(", ".join(invalid_serial_nos))
-		if len(invalid_serial_nos) == 1:
-			frappe.throw(
-				_(
-					"Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no."
-				).format(item.idx, bold_invalid_serial_nos),
-				title=_("Item Unavailable"),
-			)
-		elif invalid_serial_nos:
-			frappe.throw(
-				_(
-					"Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no."
-				).format(item.idx, bold_invalid_serial_nos),
-				title=_("Item Unavailable"),
-			)
-
-	def validate_pos_reserved_batch_qty(self, item):
-		filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no}
-
-		available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
-		reserved_batch_qty = get_pos_reserved_batch_qty(filters)
-
-		bold_item_name = frappe.bold(item.item_name)
-		bold_extra_batch_qty_needed = frappe.bold(
-			abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
-		)
-		bold_invalid_batch_no = frappe.bold(item.batch_no)
-
-		if (available_batch_qty - reserved_batch_qty) == 0:
-			frappe.throw(
-				_(
-					"Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no."
-				).format(item.idx, bold_invalid_batch_no, bold_item_name),
-				title=_("Item Unavailable"),
-			)
-		elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
-			frappe.throw(
-				_(
-					"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
-				).format(
-					item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed
-				),
-				title=_("Item Unavailable"),
-			)
-
-	def validate_delivered_serial_nos(self, item):
-		delivered_serial_nos = get_delivered_serial_nos(item.serial_no)
-
-		if delivered_serial_nos:
-			bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos))
-			frappe.throw(
-				_(
-					"Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no."
-				).format(item.idx, bold_delivered_serial_nos),
-				title=_("Item Unavailable"),
-			)
-
-	def validate_invalid_serial_nos(self, item):
-		serial_nos = get_serial_nos(item.serial_no)
-		error_msg = []
-		invalid_serials, msg = "", ""
-		for serial_no in serial_nos:
-			if not frappe.db.exists("Serial No", serial_no):
-				invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no
-		msg = _("Row #{}: Following Serial numbers for item {} are <b>Invalid</b>: {}").format(
-			item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)
-		)
-		if invalid_serials:
-			error_msg.append(msg)
-
-		if error_msg:
-			frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
-
 	def validate_stock_availablility(self):
 		if self.is_return:
 			return
@@ -223,13 +145,7 @@
 		from erpnext.stock.stock_ledger import is_negative_stock_allowed
 
 		for d in self.get("items"):
-			if d.serial_no:
-				self.validate_pos_reserved_serial_nos(d)
-				self.validate_delivered_serial_nos(d)
-				self.validate_invalid_serial_nos(d)
-			elif d.batch_no:
-				self.validate_pos_reserved_batch_qty(d)
-			else:
+			if not d.serial_and_batch_bundle:
 				if is_negative_stock_allowed(item_code=d.item_code):
 					return
 
@@ -258,36 +174,15 @@
 	def validate_serialised_or_batched_item(self):
 		error_msg = []
 		for d in self.get("items"):
-			serialized = d.get("has_serial_no")
-			batched = d.get("has_batch_no")
-			no_serial_selected = not d.get("serial_no")
-			no_batch_selected = not d.get("batch_no")
+			error_msg = ""
+			if d.get("has_serial_no") and not d.serial_and_batch_bundle:
+				error_msg = f"Row #{d.idx}: Please select Serial No. for item {bold(d.item_code)}"
 
-			msg = ""
-			item_code = frappe.bold(d.item_code)
-			serial_nos = get_serial_nos(d.serial_no)
-			if serialized and batched and (no_batch_selected or no_serial_selected):
-				msg = _(
-					"Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction."
-				).format(d.idx, item_code)
-			elif serialized and no_serial_selected:
-				msg = _(
-					"Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction."
-				).format(d.idx, item_code)
-			elif batched and no_batch_selected:
-				msg = _(
-					"Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction."
-				).format(d.idx, item_code)
-			elif serialized and not no_serial_selected and len(serial_nos) != d.qty:
-				msg = _("Row #{}: You must select {} serial numbers for item {}.").format(
-					d.idx, frappe.bold(cint(d.qty)), item_code
-				)
-
-			if msg:
-				error_msg.append(msg)
+			elif d.get("has_batch_no") and not d.serial_and_batch_bundle:
+				error_msg = f"Row #{d.idx}: Please select Batch No. for item {bold(d.item_code)}"
 
 		if error_msg:
-			frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
+			frappe.throw(error_msg, title=_("Serial / Batch Bundle Missing"), as_list=True)
 
 	def validate_return_items_qty(self):
 		if not self.get("is_return"):
@@ -652,7 +547,7 @@
 		item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
 		available_qty = item_bin_qty - item_pos_reserved_qty
 
-		max_available_bundles = available_qty / item.stock_qty
+		max_available_bundles = available_qty / item.qty
 		if bundle_bin_qty > max_available_bundles and frappe.get_value(
 			"Item", item.item_code, "is_stock_item"
 		):
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index d8aed21..db64d06 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -184,6 +184,8 @@
 					item.base_amount = item.base_net_amount
 					item.price_list_rate = 0
 					si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
+					if item.serial_and_batch_bundle:
+						si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
 					items.append(si_item)
 
 			for tax in doc.get("taxes"):
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 71fee9f..86cef3b 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -408,6 +408,7 @@
 				{
 					"type_of_transaction": type_of_transaction,
 					"serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
+					"returned_against": source_doc.name,
 				}
 			)
 
@@ -429,6 +430,7 @@
 				{
 					"type_of_transaction": type_of_transaction,
 					"serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle,
+					"returned_against": source_doc.name,
 				}
 			)
 
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index f9b5bb2..1091c46 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -44,7 +44,8 @@
 				<div class="item-image"></div>
 			</div>
 			<div class="discount-section"></div>
-			<div class="form-container"></div>`
+			<div class="form-container"></div>
+			<div class="serial-batch-container"></div>`
 		)
 
 		this.$item_name = this.$component.find('.item-name');
@@ -53,6 +54,7 @@
 		this.$item_image = this.$component.find('.item-image');
 		this.$form_container = this.$component.find('.form-container');
 		this.$dicount_section = this.$component.find('.discount-section');
+		this.$serial_batch_container = this.$component.find('.serial-batch-container');
 	}
 
 	compare_with_current_item(item) {
@@ -101,12 +103,9 @@
 
 		const serialized = item_row.has_serial_no;
 		const batched = item_row.has_batch_no;
-		const no_serial_selected = !item_row.serial_no;
-		const no_batch_selected = !item_row.batch_no;
+		const no_bundle_selected = !item_row.serial_and_batch_bundle;
 
-		if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
-			(serialized && batched && (no_batch_selected || no_serial_selected))) {
-
+		if ((serialized && no_bundle_selected) || (batched && no_bundle_selected)) {
 			frappe.show_alert({
 				message: __("Item is removed since no serial / batch no selected."),
 				indicator: 'orange'
@@ -200,13 +199,8 @@
 	}
 
 	make_auto_serial_selection_btn(item) {
-		if (item.has_serial_no) {
-			if (!item.has_batch_no) {
-				this.$form_container.append(
-					`<div class="grid-filler no-select"></div>`
-				);
-			}
-			const label = __('Auto Fetch Serial Numbers');
+		if (item.has_serial_no || item.has_batch_no) {
+			const label = item.has_serial_no ? __('Select Serial No') : __('Select Batch No');
 			this.$form_container.append(
 				`<div class="btn btn-sm btn-secondary auto-fetch-btn">${label}</div>`
 			);
@@ -382,40 +376,19 @@
 
 	bind_auto_serial_fetch_event() {
 		this.$form_container.on('click', '.auto-fetch-btn', () => {
-			this.batch_no_control && this.batch_no_control.set_value('');
-			let qty = this.qty_control.get_value();
-			let conversion_factor = this.conversion_factor_control.get_value();
-			let expiry_date = this.item_row.has_batch_no ? this.events.get_frm().doc.posting_date : "";
+			frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", () => {
+				let frm = this.events.get_frm();
+				let item_row = this.item_row;
+				item_row.outward = 1;
+				item_row.type_of_transaction = "Outward";
 
-			let numbers = frappe.call({
-				method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
-				args: {
-					qty: qty * conversion_factor,
-					item_code: this.current_item.item_code,
-					warehouse: this.warehouse_control.get_value() || '',
-					batch_nos: this.current_item.batch_no || '',
-					posting_date: expiry_date,
-					for_doctype: 'POS Invoice'
-				}
-			});
-
-			numbers.then((data) => {
-				let auto_fetched_serial_numbers = data.message;
-				let records_length = auto_fetched_serial_numbers.length;
-				if (!records_length) {
-					const warehouse = this.warehouse_control.get_value().bold();
-					const item_code = this.current_item.item_code.bold();
-					frappe.msgprint(
-						__('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [item_code, warehouse])
-					);
-				} else if (records_length < qty) {
-					frappe.msgprint(
-						__('Fetched only {0} available serial numbers.', [records_length])
-					);
-					this.qty_control.set_value(records_length);
-				}
-				numbers = auto_fetched_serial_numbers.join(`\n`);
-				this.serial_no_control.set_value(numbers);
+				new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
+					if (r) {
+						frm.refresh_fields();
+						frappe.model.set_value(item_row.doctype, item_row.name,
+							"serial_and_batch_bundle", r.name);
+					}
+				});
 			});
 		})
 	}
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
index 337c6dd..788c79d 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
@@ -31,6 +31,7 @@
   "column_break_aouy",
   "posting_date",
   "posting_time",
+  "returned_against",
   "section_break_wzou",
   "is_cancelled",
   "is_rejected",
@@ -232,12 +233,18 @@
    "fieldtype": "Table",
    "options": "Serial and Batch Entry",
    "reqd": 1
+  },
+  {
+   "fieldname": "returned_against",
+   "fieldtype": "Data",
+   "label": "Returned Against",
+   "read_only": 1
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2023-03-22 18:56:37.035516",
+ "modified": "2023-03-23 13:39:17.843812",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Serial and Batch Bundle",
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 311b35f..c4f240a 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
@@ -2,6 +2,7 @@
 # For license information, please see license.txt
 
 import collections
+from collections import defaultdict
 from typing import Dict, List
 
 import frappe
@@ -31,10 +32,10 @@
 		self.check_future_entries_exists()
 		self.validate_serial_nos_inventory()
 		self.set_is_outward()
+		self.validate_qty_and_stock_value_difference()
 		self.calculate_qty_and_amount()
 		self.set_warehouse()
 		self.set_incoming_rate()
-		self.validate_qty_and_stock_value_difference()
 
 	def validate_serial_nos_inventory(self):
 		if not (self.has_serial_no and self.type_of_transaction == "Outward"):
@@ -100,7 +101,7 @@
 				d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
 			else:
 				d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no))
-				available_qty = flt(sn_obj.batch_available_qty.get(d.batch_no)) + flt(d.qty)
+				available_qty = flt(sn_obj.available_qty.get(d.batch_no)) + flt(d.qty)
 
 				self.validate_negative_batch(d.batch_no, available_qty)
 
@@ -417,6 +418,7 @@
 			frappe.throw(_(msg))
 
 	def on_trash(self):
+		self.validate_voucher_no_docstatus()
 		self.delink_refernce_from_voucher()
 		self.delink_reference_from_batch()
 		self.clear_table()
@@ -439,25 +441,48 @@
 
 
 @frappe.whitelist()
-def get_serial_batch_ledgers(item_code, voucher_no, name=None):
+def get_serial_batch_ledgers(item_code, docstatus=None, voucher_no=None, name=None):
+	filters = get_filters_for_bundle(item_code, docstatus=docstatus, voucher_no=voucher_no, name=name)
+
 	return frappe.get_all(
 		"Serial and Batch Bundle",
 		fields=[
-			"`tabSerial and Batch Entry`.`name`",
+			"`tabSerial and Batch Bundle`.`name`",
 			"`tabSerial and Batch Entry`.`qty`",
 			"`tabSerial and Batch Entry`.`warehouse`",
 			"`tabSerial and Batch Entry`.`batch_no`",
 			"`tabSerial and Batch Entry`.`serial_no`",
 		],
-		filters=[
-			["Serial and Batch Bundle", "item_code", "=", item_code],
-			["Serial and Batch Entry", "parent", "=", name],
-			["Serial and Batch Bundle", "voucher_no", "=", voucher_no],
-			["Serial and Batch Bundle", "docstatus", "!=", 2],
-		],
+		filters=filters,
 	)
 
 
+def get_filters_for_bundle(item_code, docstatus=None, voucher_no=None, name=None):
+	filters = [
+		["Serial and Batch Bundle", "item_code", "=", item_code],
+		["Serial and Batch Bundle", "is_cancelled", "=", 0],
+	]
+
+	if not docstatus:
+		docstatus = [0, 1]
+
+	if isinstance(docstatus, list):
+		filters.append(["Serial and Batch Bundle", "docstatus", "in", docstatus])
+	else:
+		filters.append(["Serial and Batch Bundle", "docstatus", "=", docstatus])
+
+	if voucher_no:
+		filters.append(["Serial and Batch Bundle", "voucher_no", "=", voucher_no])
+
+	if name:
+		if isinstance(name, list):
+			filters.append(["Serial and Batch Entry", "parent", "in", name])
+		else:
+			filters.append(["Serial and Batch Entry", "parent", "=", name])
+
+	return filters
+
+
 @frappe.whitelist()
 def add_serial_batch_ledgers(entries, child_row, doc) -> object:
 	if isinstance(child_row, str):
@@ -603,15 +628,52 @@
 	elif kwargs.based_on == "Expiry":
 		order_by = "amc_expiry_date asc"
 
+	ignore_serial_nos = get_reserved_serial_nos_for_pos(kwargs)
+
 	return frappe.get_all(
 		"Serial No",
 		fields=fields,
-		filters={"item_code": kwargs.item_code, "warehouse": kwargs.warehouse},
+		filters={
+			"item_code": kwargs.item_code,
+			"warehouse": kwargs.warehouse,
+			"name": ("not in", ignore_serial_nos),
+		},
 		limit=cint(kwargs.qty),
 		order_by=order_by,
 	)
 
 
+def get_reserved_serial_nos_for_pos(kwargs):
+	from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+	ignore_serial_nos = []
+	pos_invoices = frappe.get_all(
+		"POS Invoice",
+		fields=["`tabPOS Invoice Item`.serial_no", "`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):
+		ignore_serial_nos.append(d.serial_no)
+
+	# Will be deprecated in v16
+	for pos_invoice in pos_invoices:
+		if pos_invoice.serial_no:
+			ignore_serial_nos.extend(get_serial_nos(pos_invoice.serial_no))
+
+	return ignore_serial_nos
+
+
 def get_auto_batch_nos(kwargs):
 	available_batches = get_available_batches(kwargs)
 
@@ -619,6 +681,10 @@
 
 	batches = []
 
+	reserved_batches = get_reserved_batches_for_pos(kwargs)
+	if reserved_batches:
+		remove_batches_reserved_for_pos(available_batches, reserved_batches)
+
 	for batch in available_batches:
 		if qty > 0:
 			batch_qty = flt(batch.qty)
@@ -642,6 +708,51 @@
 	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):
+	for batch in available_batches:
+		if batch.batch_no in reserved_batches:
+			available_batches[batch.batch_no] -= reserved_batches[batch.batch_no]
+
+
 def get_available_batches(kwargs):
 	stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
 	batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
@@ -655,9 +766,7 @@
 		.on(batch_ledger.batch_no == batch_table.name)
 		.select(
 			batch_ledger.batch_no,
-			Sum(
-				Case().when(stock_ledger_entry.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)
-			).as_("qty"),
+			Sum(batch_ledger.qty).as_("qty"),
 		)
 		.where(
 			(stock_ledger_entry.item_code == kwargs.item_code)
@@ -699,7 +808,7 @@
 		if key not in group_by_voucher:
 			group_by_voucher.setdefault(
 				key,
-				frappe._dict({"serial_nos": [], "batch_nos": collections.defaultdict(float), "item_row": row}),
+				frappe._dict({"serial_nos": [], "batch_nos": defaultdict(float), "item_row": row}),
 			)
 
 		child_row = group_by_voucher[key]
@@ -771,7 +880,7 @@
 
 def get_available_batch_nos(item_code, warehouse):
 	sl_entries = get_stock_ledger_entries(item_code, warehouse)
-	batchwise_qty = collections.defaultdict(float)
+	batchwise_qty = defaultdict(float)
 
 	precision = frappe.get_precision("Stock Ledger Entry", "qty")
 	for entry in sl_entries:
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 2c46082..483a1f1 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(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse)
+		.groupby(batch_package.batch_no)
 		.orderby(sle.item_code, sle.warehouse)
 	)
 
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 1266133..038cce7 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -642,6 +642,8 @@
 		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.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