feat: serial and batch bundle for pick list
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index f926512..e14f9e6 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -108,6 +108,20 @@
 
 			update_coupon_code_count(self.coupon_code, "cancelled")
 
+		self.delink_serial_and_batch_bundle()
+
+	def delink_serial_and_batch_bundle(self):
+		for row in self.items:
+			if row.serial_and_batch_bundle:
+				if not self.consolidated_invoice:
+					frappe.db.set_value(
+						"Serial and Batch Bundle",
+						row.serial_and_batch_bundle,
+						{"is_cancelled": 1, "voucher_no": ""},
+					)
+
+				row.db_set("serial_and_batch_bundle", None)
+
 	def submit_serial_batch_bundle(self):
 		for item in self.items:
 			if item.serial_and_batch_bundle:
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 8213adb..54e2631 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -3,6 +3,8 @@
 
 frappe.ui.form.on('Pick List', {
 	setup: (frm) => {
+		frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
+
 		frm.set_indicator_formatter('item_code',
 			function(doc) { return (doc.stock_qty === 0) ? "red" : "green"; });
 
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 1ffc4ca..8035c7a 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -63,25 +63,6 @@
 				# if the user has not entered any picked qty, set it to stock_qty, before submit
 				item.picked_qty = item.stock_qty
 
-			if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
-				continue
-
-			if not item.serial_no:
-				frappe.throw(
-					_("Row #{0}: {1} does not have any available serial numbers in {2}").format(
-						frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse)
-					),
-					title=_("Serial Nos Required"),
-				)
-
-			if len(item.serial_no.split("\n")) != item.picked_qty:
-				frappe.throw(
-					_(
-						"For item {0} at row {1}, count of serial numbers does not match with the picked quantity"
-					).format(frappe.bold(item.item_code), frappe.bold(item.idx)),
-					title=_("Quantity Mismatch"),
-				)
-
 	def on_submit(self):
 		self.validate_serial_and_batch_bundle()
 		self.update_status()
@@ -90,10 +71,24 @@
 		self.update_sales_order_picking_status()
 
 	def on_cancel(self):
+		self.ignore_linked_doctypes = "Serial and Batch Bundle"
+
 		self.update_status()
 		self.update_bundle_picked_qty()
 		self.update_reference_qty()
 		self.update_sales_order_picking_status()
+		self.delink_serial_and_batch_bundle()
+
+	def delink_serial_and_batch_bundle(self):
+		for row in self.locations:
+			if row.serial_and_batch_bundle:
+				frappe.db.set_value(
+					"Serial and Batch Bundle",
+					row.serial_and_batch_bundle,
+					{"is_cancelled": 1, "voucher_no": ""},
+				)
+
+				row.db_set("serial_and_batch_bundle", None)
 
 	def on_update(self):
 		self.linked_serial_and_batch_bundle()
@@ -546,11 +541,7 @@
 	has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
 	has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
 
-	if has_batch_no and has_serial_no:
-		locations = get_available_item_locations_for_serial_and_batched_item(
-			item_code, from_warehouses, required_qty, company, total_picked_qty
-		)
-	elif has_serial_no:
+	if has_serial_no:
 		locations = get_available_item_locations_for_serialized_item(
 			item_code, from_warehouses, required_qty, company, total_picked_qty
 		)
@@ -613,12 +604,39 @@
 	serial_nos = query.run(as_list=True)
 
 	warehouse_serial_nos_map = frappe._dict()
+	picked_qty = required_qty
 	for serial_no, warehouse in serial_nos:
+		if picked_qty <= 0:
+			break
+
 		warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
+		picked_qty -= 1
 
 	locations = []
 	for warehouse, serial_nos in warehouse_serial_nos_map.items():
-		locations.append({"qty": len(serial_nos), "warehouse": warehouse, "serial_no": serial_nos})
+		qty = len(serial_nos)
+
+		bundle_doc = SerialBatchCreation(
+			{
+				"item_code": item_code,
+				"warehouse": warehouse,
+				"voucher_type": "Pick List",
+				"total_qty": qty * -1,
+				"serial_nos": serial_nos,
+				"type_of_transaction": "Outward",
+				"company": company,
+				"do_not_submit": True,
+			}
+		).make_serial_and_batch_bundle()
+
+		locations.append(
+			{
+				"qty": qty,
+				"warehouse": warehouse,
+				"item_code": item_code,
+				"serial_and_batch_bundle": bundle_doc.name,
+			}
+		)
 
 	return locations
 
@@ -652,7 +670,7 @@
 				"item_code": item_code,
 				"warehouse": warehouse,
 				"voucher_type": "Pick List",
-				"total_qty": qty,
+				"total_qty": qty * -1,
 				"batches": batches,
 				"type_of_transaction": "Outward",
 				"company": company,
@@ -672,40 +690,6 @@
 	return locations
 
 
-def get_available_item_locations_for_serial_and_batched_item(
-	item_code, from_warehouses, required_qty, company, total_picked_qty=0
-):
-	# Get batch nos by FIFO
-	locations = get_available_item_locations_for_batched_item(
-		item_code, from_warehouses, required_qty, company
-	)
-
-	if locations:
-		sn = frappe.qb.DocType("Serial No")
-		conditions = (sn.item_code == item_code) & (sn.company == company)
-
-		for location in locations:
-			location.qty = (
-				required_qty if location.qty > required_qty else location.qty
-			)  # if extra qty in batch
-
-			serial_nos = (
-				frappe.qb.from_(sn)
-				.select(sn.name)
-				.where(
-					(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
-				)
-				.orderby(sn.purchase_date)
-				.limit(cint(location.qty + total_picked_qty))
-			).run(as_dict=True)
-
-			serial_nos = [sn.name for sn in serial_nos]
-			location.serial_no = serial_nos
-			location.qty = len(serial_nos)
-
-	return locations
-
-
 def get_available_item_locations_for_other_item(
 	item_code, from_warehouses, required_qty, company, total_picked_qty=0
 ):
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 80cbf02..afcc676 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
@@ -78,6 +78,9 @@
 
 	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:
+			sle.actual_qty = sle.qty
+
 		if self.has_serial_no:
 			sn_obj = SerialNoValuation(
 				sle=sle,
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 926863e..c14df3b 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -5,7 +5,7 @@
 from frappe import _, bold
 from frappe.model.naming import make_autoname
 from frappe.query_builder.functions import CombineDatetime, Sum
-from frappe.utils import cint, flt, now
+from frappe.utils import cint, flt, now, today
 
 from erpnext.stock.deprecated_serial_batch import (
 	DeprecatedBatchNoValuation,
@@ -557,6 +557,7 @@
 	def __init__(self, args):
 		self.set(args)
 		self.set_item_details()
+		self.set_other_details()
 
 	def set(self, args):
 		self.__dict__ = {}
@@ -585,6 +586,11 @@
 
 		self.__dict__.update(item_details)
 
+	def set_other_details(self):
+		if not self.get("posting_date"):
+			setattr(self, "posting_date", today())
+			self.__dict__["posting_date"] = self.posting_date
+
 	def duplicate_package(self):
 		if not self.serial_and_batch_bundle:
 			return
@@ -611,6 +617,7 @@
 			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:
@@ -633,7 +640,7 @@
 
 		if self.has_serial_no and not self.get("serial_nos"):
 			self.serial_nos = get_serial_nos_for_outward(kwargs)
-		elif self.has_batch_no and not self.get("batches"):
+		elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
 			self.batches = get_available_batches(kwargs)
 
 	def set_auto_serial_batch_entries_for_inward(self):