feat: serial and batch bundle for Packing Items
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index f6e1e05..15c84a9 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -38,7 +38,9 @@
 		self.validate_for_duplicate_items()
 		self.validate_target_warehouse()
 		self.validate_auto_repeat_subscription_dates()
-		self.set_serial_and_batch_bundle()
+		for table_field in ["items", "packed_items"]:
+			if self.get(table_field):
+				self.set_serial_and_batch_bundle(table_field)
 
 	def set_missing_values(self, for_validate=False):
 
@@ -426,8 +428,7 @@
 							"posting_date": self.get("posting_date") or self.get("transaction_date"),
 							"posting_time": self.get("posting_time") or nowtime(),
 							"qty": qty if cint(self.get("is_return")) else (-1 * qty),
-							"serial_no": d.get("serial_no"),
-							"batch_no": d.get("batch_no"),
+							"serial_and_batch_bundle": d.serial_and_batch_bundle,
 							"company": self.company,
 							"voucher_type": self.doctype,
 							"voucher_no": self.name,
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 74ba2b8..2e705ea 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -719,7 +719,7 @@
 			table_name = "items"
 
 		for row in self.get(table_name):
-			if row.serial_and_batch_bundle:
+			if row.get("serial_and_batch_bundle"):
 				frappe.get_doc(
 					"Serial and Batch Bundle", row.serial_and_batch_bundle
 				).set_serial_and_batch_values(self, row)
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index f7bc5d5..0e666ff 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -51,7 +51,9 @@
 		if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
 			self.validate_items()
 			self.create_raw_materials_supplied()
-			self.set_serial_and_batch_bundle("supplied_items")
+			for table_field in ["items", "supplied_items"]:
+				if self.get(table_field):
+					self.set_total_in_words(table_field)
 		else:
 			super(SubcontractingController, self).validate()
 
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 4d17f4e..6c18b74 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -431,6 +431,7 @@
 					item.has_serial_no = r.message.has_serial_no;
 					item.has_batch_no = r.message.has_batch_no;
 					item.type_of_transaction = item.qty > 0 ? "Outward":"Inward";
+					item.outward = item.qty > 0 ? 1 : 0;
 
 					item.title = item.has_serial_no ?
 						__("Select Serial No") : __("Select Batch No");
@@ -446,6 +447,8 @@
 									me.frm.refresh_fields();
 									frappe.model.set_value(cdt, cdn,
 										"serial_and_batch_bundle", r.name);
+
+									me.frm.save();
 								}
 							}
 						);
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 35a3ca8..98da0af 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
@@ -5,7 +5,7 @@
 from typing import Dict, List
 
 import frappe
-from frappe import _
+from frappe import _, bold
 from frappe.model.document import Document
 from frappe.query_builder.functions import Sum
 from frappe.utils import cint, flt, today
@@ -301,6 +301,15 @@
 		for batch in batches:
 			frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
 
+	def on_cancel(self):
+		self.validate_voucher_no_docstatus()
+
+	def validate_voucher_no_docstatus(self):
+		if frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1:
+			msg = f"""The {self.voucher_type} {bold(self.voucher_no)}
+				is in submitted state, please cancel it first"""
+			frappe.throw(_(msg))
+
 	def on_trash(self):
 		self.delink_refernce_from_voucher()
 		self.delink_reference_from_batch()
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 0691d63..a7f5b80 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -1220,11 +1220,18 @@
 					sle.recalculate_rate = 1
 
 				if d.serial_and_batch_bundle and self.docstatus == 1:
-					self.copy_serial_and_batch_bundle(sle, d)
+					d.serial_and_batch_bundle = self.copy_serial_and_batch_bundle(sle)
 
 				if d.serial_and_batch_bundle and self.docstatus == 2:
 					bundle_id = frappe.get_cached_value(
-						"Serial and Batch Bundle", {"voucher_detail_no": d.name, "is_cancelled": 0}, "name"
+						"Serial and Batch Bundle",
+						{
+							"voucher_detail_no": d.name,
+							"voucher_no": self.name,
+							"is_cancelled": 0,
+							"type_of_transaction": "Inward",
+						},
+						"name",
 					)
 
 					if d.serial_and_batch_bundle != bundle_id:
@@ -1232,7 +1239,7 @@
 
 				sl_entries.append(sle)
 
-	def copy_serial_and_batch_bundle(self, sle, child):
+	def copy_serial_and_batch_bundle(self, child):
 		allowed_types = [
 			"Material Transfer",
 			"Send to Subcontractor",
@@ -1260,7 +1267,7 @@
 			bundle_doc.set_avg_rate()
 			bundle_doc.flags.ignore_permissions = True
 			bundle_doc.submit()
-			sle.serial_and_batch_bundle = bundle_doc.name
+			return bundle_doc.name
 
 	def get_gl_entries(self, warehouse_account):
 		gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 7c4f062..a4fac4d 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -307,6 +307,9 @@
 		return False
 
 	def post_process(self):
+		if not self.sle.serial_and_batch_bundle:
+			return
+
 		if self.item_details.has_serial_no == 1:
 			self.set_warehouse_and_status_in_serial_nos()
 
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index c8fffdf..18e0b90 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -259,8 +259,11 @@
 		"Item", args.get("item_code"), ["has_serial_no", "has_batch_no"], as_dict=1
 	)
 
+	if isinstance(args, dict):
+		args = frappe._dict(args)
+
 	if item_details.has_serial_no and args.get("serial_and_batch_bundle"):
-		args["actual_qty"] = args["qty"]
+		args.actual_qty = args.qty
 		sn_obj = SerialNoBundleValuation(
 			sle=args,
 			warehouse=args.get("warehouse"),
@@ -270,7 +273,7 @@
 		in_rate = sn_obj.get_incoming_rate()
 
 	elif item_details.has_batch_no and args.get("serial_and_batch_bundle"):
-		args["actual_qty"] = args["qty"]
+		args.actual_qty = args.qty
 		batch_obj = BatchNoBundleValuation(
 			sle=args,
 			warehouse=args.get("warehouse"),