feat: serial and batch bundle for Subcontracting
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 342b8e9..74ba2b8 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -714,8 +714,11 @@
 					message = self.prepare_over_receipt_message(rule, values)
 					frappe.throw(msg=message, title=_("Over Receipt"))
 
-	def set_serial_and_batch_bundle(self):
-		for row in self.items:
+	def set_serial_and_batch_bundle(self, table_name=None):
+		if not table_name:
+			table_name = "items"
+
+		for row in self.get(table_name):
 			if row.serial_and_batch_bundle:
 				frappe.get_doc(
 					"Serial and Batch Bundle", row.serial_and_batch_bundle
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 1e9c4dc..f7bc5d5 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -11,6 +11,9 @@
 from frappe.utils import cint, cstr, flt, get_link_to_form
 
 from erpnext.controllers.stock_controller import StockController
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+	get_voucher_wise_serial_batch_from_bundle,
+)
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 from erpnext.stock.utils import get_incoming_rate
 
@@ -48,6 +51,7 @@
 		if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
 			self.validate_items()
 			self.create_raw_materials_supplied()
+			self.set_serial_and_batch_bundle("supplied_items")
 		else:
 			super(SubcontractingController, self).validate()
 
@@ -169,7 +173,11 @@
 				self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
 
 	def __get_transferred_items(self):
-		fields = [f"`tabStock Entry`.`{self.subcontract_data.order_field}`"]
+		fields = [
+			f"`tabStock Entry`.`{self.subcontract_data.order_field}`",
+			"`tabStock Entry`.`name` as voucher_no",
+		]
+
 		alias_dict = {
 			"item_code": "rm_item_code",
 			"subcontracted_item": "main_item_code",
@@ -234,9 +242,11 @@
 				"serial_no",
 				"rm_item_code",
 				"reference_name",
+				"serial_and_batch_bundle",
 				"batch_no",
 				"consumed_qty",
 				"main_item_code",
+				"parent as voucher_no",
 			],
 			filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype},
 		)
@@ -253,6 +263,13 @@
 		}
 		consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys())
 
+		voucher_nos = [d.voucher_no for d in consumed_materials if d.voucher_no]
+		voucher_bundle_data = get_voucher_wise_serial_batch_from_bundle(
+			voucher_no=voucher_nos,
+			is_outward=1,
+			get_subcontracted_item=("Subcontracting Receipt Supplied Item", "main_item_code"),
+		)
+
 		if return_consumed_items:
 			return (consumed_materials, receipt_items)
 
@@ -262,11 +279,26 @@
 				continue
 
 			self.available_materials[key]["qty"] -= row.consumed_qty
+
+			bundle_key = (row.rm_item_code, row.main_item_code, self.supplier_warehouse, row.voucher_no)
+			consumed_bundles = voucher_bundle_data.get(bundle_key, frappe._dict())
+
+			if consumed_bundles.serial_nos:
+				self.available_materials[key]["serial_no"] = list(
+					set(self.available_materials[key]["serial_no"]) - set(consumed_bundles.serial_nos)
+				)
+
+			if consumed_bundles.batch_nos:
+				for batch_no, qty in consumed_bundles.batch_nos.items():
+					self.available_materials[key]["batch_no"][batch_no] -= abs(qty)
+
+			# Will be deperecated in v16
 			if row.serial_no:
 				self.available_materials[key]["serial_no"] = list(
 					set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
 				)
 
+			# Will be deperecated in v16
 			if row.batch_no:
 				self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
 
@@ -281,7 +313,16 @@
 		if not self.subcontract_orders:
 			return
 
-		for row in self.__get_transferred_items():
+		transferred_items = self.__get_transferred_items()
+
+		voucher_nos = [row.voucher_no for row in transferred_items]
+		voucher_bundle_data = get_voucher_wise_serial_batch_from_bundle(
+			voucher_no=voucher_nos,
+			is_outward=0,
+			get_subcontracted_item=("Stock Entry Detail", "subcontracted_item"),
+		)
+
+		for row in transferred_items:
 			key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
 
 			if key not in self.available_materials:
@@ -310,6 +351,17 @@
 			if row.batch_no:
 				details.batch_no[row.batch_no] += row.qty
 
+			if voucher_bundle_data:
+				bundle_key = (row.rm_item_code, row.main_item_code, row.t_warehouse, row.voucher_no)
+
+				bundle_data = voucher_bundle_data.get(bundle_key, frappe._dict())
+				if bundle_data.serial_nos:
+					details.serial_no.extend(bundle_data.serial_nos)
+
+				if bundle_data.batch_nos:
+					for batch_no, qty in bundle_data.batch_nos.items():
+						details.batch_no[batch_no] += qty
+
 			self.__set_alternative_item_details(row)
 
 		self.__transferred_items = copy.deepcopy(self.available_materials)
@@ -327,6 +379,7 @@
 		self.set(self.raw_material_table, [])
 		for item in self._doc_before_save.supplied_items:
 			if item.reference_name in self.__changed_name:
+				self.__remove_serial_and_batch_bundle(item)
 				continue
 
 			if item.reference_name not in self.__reference_name:
@@ -337,6 +390,10 @@
 
 			i += 1
 
+	def __remove_serial_and_batch_bundle(self, item):
+		if item.serial_and_batch_bundle:
+			frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
+
 	def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
 		doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
 		fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
@@ -403,42 +460,88 @@
 		rm_obj.required_qty = required_qty
 		rm_obj.consumed_qty = consumed_qty
 
-	def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
+	def __set_serial_and_batch_bundle(self, item_row, rm_obj, qty):
 		key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
+		if not self.available_materials.get(key):
+			return
 
-		if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
-			new_rm_obj = None
-			for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
-				if batch_qty >= qty or (
-					rm_obj.consumed_qty == 0
-					and self.backflush_based_on == "BOM"
-					and len(self.available_materials[key]["batch_no"]) == 1
-				):
-					if rm_obj.consumed_qty == 0:
-						self.__set_consumed_qty(rm_obj, qty)
+		if (
+			not self.available_materials[key]["serial_no"] and not self.available_materials[key]["batch_no"]
+		):
+			return
 
-					self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
-					self.available_materials[key]["batch_no"][batch_no] -= qty
-					return
+		bundle = frappe.get_doc(
+			{
+				"doctype": "Serial and Batch Bundle",
+				"company": self.company,
+				"item_code": rm_obj.rm_item_code,
+				"warehouse": self.supplier_warehouse,
+				"posting_date": self.posting_date,
+				"posting_time": self.posting_time,
+				"voucher_type": "Subcontracting Receipt",
+				"voucher_no": self.name,
+				"type_of_transaction": "Outward",
+			}
+		)
 
-				elif qty > 0 and batch_qty > 0:
-					qty -= batch_qty
-					new_rm_obj = self.append(self.raw_material_table, bom_item)
-					new_rm_obj.reference_name = item_row.name
-					self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
-					self.available_materials[key]["batch_no"][batch_no] = 0
+		if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
+			self.__set_serial_nos_for_bundle(bundle, qty, key)
 
-			if abs(qty) > 0 and not new_rm_obj:
-				self.__set_consumed_qty(rm_obj, qty)
-		else:
-			self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
-			self.__set_serial_nos(item_row, rm_obj)
+		elif self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
+			self.__set_batch_nos_for_bundle(bundle, qty, key)
+
+		bundle.flags.ignore_links = True
+		bundle.flags.ignore_mandatory = True
+		bundle.save(ignore_permissions=True)
+		return bundle.name
+
+	def __set_batch_nos_for_bundle(self, bundle, qty, key):
+		bundle.has_batch_no = 1
+		for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
+			qty_to_consumed = 0
+			if qty > 0:
+				if batch_qty >= qty:
+					qty_to_consumed = qty
+				else:
+					qty_to_consumed = batch_qty
+
+				qty -= qty_to_consumed
+				if qty_to_consumed > 0:
+					bundle.append("ledgers", {"batch_no": batch_no, "qty": qty_to_consumed * -1})
+
+	def __set_serial_nos_for_bundle(self, bundle, qty, key):
+		bundle.has_serial_no = 1
+
+		used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(qty)]
+
+		# Removed the used serial nos from the list
+		for sn in used_serial_nos:
+			batch_no = ""
+			if self.available_materials[key]["batch_no"]:
+				bundle.has_batch_no = 1
+				batch_no = frappe.get_cached_value("Serial No", sn, "batch_no")
+				if batch_no:
+					self.available_materials[key]["batch_no"][batch_no] -= 1
+
+			bundle.append("ledgers", {"serial_no": sn, "batch_no": batch_no, "qty": -1})
+
+			self.available_materials[key]["serial_no"].remove(sn)
 
 	def __add_supplied_item(self, item_row, bom_item, qty):
 		bom_item.conversion_factor = item_row.conversion_factor
 		rm_obj = self.append(self.raw_material_table, bom_item)
 		rm_obj.reference_name = item_row.name
 
+		if self.doctype == self.subcontract_data.order_doctype:
+			rm_obj.required_qty = qty
+			rm_obj.amount = rm_obj.required_qty * rm_obj.rate
+		else:
+			rm_obj.consumed_qty = qty
+			rm_obj.required_qty = bom_item.required_qty or qty
+			setattr(
+				rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
+			)
+
 		if self.doctype == "Subcontracting Receipt":
 			args = frappe._dict(
 				{
@@ -447,25 +550,21 @@
 					"posting_date": self.posting_date,
 					"posting_time": self.posting_time,
 					"qty": -1 * flt(rm_obj.consumed_qty),
-					"serial_no": rm_obj.serial_no,
-					"batch_no": rm_obj.batch_no,
+					"actual_qty": -1 * flt(rm_obj.consumed_qty),
 					"voucher_type": self.doctype,
 					"voucher_no": self.name,
+					"voucher_detail_no": item_row.name,
 					"company": self.company,
 					"allow_zero_valuation": 1,
 				}
 			)
-			rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
 
-		if self.doctype == self.subcontract_data.order_doctype:
-			rm_obj.required_qty = qty
-			rm_obj.amount = rm_obj.required_qty * rm_obj.rate
-		else:
-			rm_obj.consumed_qty = 0
-			setattr(
-				rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
-			)
-			self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
+			rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(item_row, rm_obj, qty)
+
+			if rm_obj.serial_and_batch_bundle:
+				args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle
+
+			rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
 
 	def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
 		key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index bdfc2f0..73c3868 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -891,10 +891,11 @@
 					doc: this.frm.doc,
 				}
 			}).then(r => {
-				debugger
 				this.callback && this.callback(r.message);
 				this.dialog.hide();
 			})
+		} else {
+			frappe.msgprint(__('Please save the document first'));
 		}
 	}
 
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index 1dbe915..33b8955 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -4,6 +4,8 @@
 
 
 class DeprecatedSerialNoValuation:
+	# Will be deperecated in v16
+
 	def calculate_stock_value_from_deprecarated_ledgers(self):
 		serial_nos = list(
 			filter(lambda x: x not in self.serial_no_incoming_rate and x, self.get_serial_nos())
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 5e9b706..382e6a9 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
@@ -19,12 +19,14 @@
 		self.validate_serial_and_batch_no()
 		self.validate_duplicate_serial_and_batch_no()
 		self.validate_voucher_no()
+		self.validate_serial_nos()
 
 	def before_save(self):
 		self.set_total_qty()
 		self.set_is_outward()
 		self.set_warehouse()
 		self.set_incoming_rate()
+		self.validate_qty_and_stock_value_difference()
 
 		if self.ledgers:
 			self.set_avg_rate()
@@ -35,6 +37,17 @@
 		else:
 			self.set_incoming_rate_for_inward_transaction(row, save)
 
+	def validate_qty_and_stock_value_difference(self):
+		if self.type_of_transaction != "Outward":
+			return
+
+		for d in self.ledgers:
+			if d.qty and d.qty > 0:
+				d.qty *= -1
+
+			if d.stock_value_difference and d.stock_value_difference > 0:
+				d.stock_value_difference *= -1
+
 	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:
@@ -53,12 +66,12 @@
 
 		for d in self.ledgers:
 			if self.has_serial_no:
-				d.incoming_rate = sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0)
+				d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
 			else:
-				d.incoming_rate = sn_obj.batch_avg_rate.get(d.batch_no)
+				d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no))
 
 			if self.has_batch_no:
-				d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) * -1
+				d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
 
 			if save:
 				d.db_set(
@@ -73,7 +86,7 @@
 				"item_code": self.item_code,
 				"warehouse": self.warehouse,
 				"serial_and_batch_bundle": self.name,
-				"actual_qty": self.total_qty * -1,
+				"actual_qty": self.total_qty,
 				"company": self.company,
 				"serial_nos": [row.serial_no for row in self.ledgers if row.serial_no],
 				"batch_nos": {row.batch_no: row for row in self.ledgers if row.batch_no},
@@ -126,6 +139,9 @@
 		self.set_incoming_rate(save=True, row=row)
 
 	def validate_voucher_no(self):
+		if self.is_new():
+			return
+
 		if not (self.voucher_type and self.voucher_no):
 			return
 
@@ -150,14 +166,22 @@
 				)
 			)
 
+	def validate_serial_nos(self):
+		if not self.has_serial_no:
+			return
+
 	def validate_quantity(self, row):
 		self.set_total_qty(save=True)
 
 		precision = row.precision
-		if abs(flt(self.total_qty, precision) - flt(row.qty, precision)) > 0.01:
+		qty_field = "qty"
+		if self.voucher_type in ["Subcontracting Receipt"]:
+			qty_field = "consumed_qty"
+
+		if abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision)) > 0.01:
 			frappe.throw(
 				_(
-					f"Total quantity {self.total_qty} in the Serial and Batch Bundle {self.name} does not match with the Item {row.item_code} in the {self.voucher_type} # {self.voucher_no}"
+					f"Total quantity {self.total_qty} in the Serial and Batch Bundle {self.name} does not match with the Item {self.item_code} in the {self.voucher_type} # {self.voucher_no}"
 				)
 			)
 
@@ -368,7 +392,7 @@
 		doc.append(
 			"ledgers",
 			{
-				"qty": row.qty or 1.0,
+				"qty": (row.qty or 1.0) * (1 if type_of_transaction == "Inward" else -1),
 				"warehouse": warehouse,
 				"batch_no": row.batch_no,
 				"serial_no": row.serial_no,
@@ -535,14 +559,24 @@
 
 def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> Dict[str, Dict]:
 	data = get_ledgers_from_serial_batch_bundle(**kwargs)
+	if not data:
+		return {}
 
 	group_by_voucher = {}
 
 	for row in data:
 		key = (row.item_code, row.warehouse, row.voucher_no)
+		if kwargs.get("get_subcontracted_item"):
+			# get_subcontracted_item = ("doctype", "field_name")
+			doctype, field_name = kwargs.get("get_subcontracted_item")
+
+			subcontracted_item_code = frappe.get_cached_value(doctype, row.voucher_detail_no, field_name)
+			key = (row.item_code, subcontracted_item_code, row.warehouse, row.voucher_no)
+
 		if key not in group_by_voucher:
 			group_by_voucher.setdefault(
-				key, {"serial_nos": [], "batch_nos": collections.defaultdict(float)}
+				key,
+				frappe._dict({"serial_nos": [], "batch_nos": collections.defaultdict(float), "item_row": row}),
 			)
 
 		child_row = group_by_voucher[key]
@@ -579,6 +613,9 @@
 	)
 
 	for key, val in kwargs.items():
+		if key in ["get_subcontracted_item"]:
+			continue
+
 		if key in ["name", "item_code", "warehouse", "voucher_no", "company", "voucher_detail_no"]:
 			if isinstance(val, list):
 				query = query.where(bundle_table[key].isin(val))
@@ -593,3 +630,56 @@
 				query = query.where(serial_batch_table[key] == val)
 
 	return query.run(as_dict=True)
+
+
+def get_available_serial_nos(item_code, warehouse):
+	filters = {
+		"item_code": item_code,
+		"warehouse": ("is", "set"),
+	}
+
+	fields = ["name", "warehouse", "batch_no"]
+
+	if warehouse:
+		filters["warehouse"] = warehouse
+
+	return frappe.get_all("Serial No", filters=filters, fields=fields)
+
+
+def get_available_batch_nos(item_code, warehouse):
+	sl_entries = get_stock_ledger_entries(item_code, warehouse)
+	batchwise_qty = collections.defaultdict(float)
+
+	precision = frappe.get_precision("Stock Ledger Entry", "qty")
+	for entry in sl_entries:
+		batchwise_qty[entry.batch_no] += flt(entry.qty, precision)
+
+
+def get_stock_ledger_entries(item_code, warehouse):
+	stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
+	batch_ledger = frappe.qb.DocType("Serial and Batch Ledger")
+
+	return (
+		frappe.qb.from_(stock_ledger_entry)
+		.left_join(batch_ledger)
+		.on(stock_ledger_entry.serial_and_batch_bundle == batch_ledger.parent)
+		.select(
+			stock_ledger_entry.warehouse,
+			stock_ledger_entry.item_code,
+			Sum(
+				Case()
+				.when(stock_ledger_entry.serial_and_batch_bundle, batch_ledger.qty)
+				.else_(stock_ledger_entry.actual_qty)
+				.as_("qty")
+			),
+			Case()
+			.when(stock_ledger_entry.serial_and_batch_bundle, batch_ledger.batch_no)
+			.else_(stock_ledger_entry.batch_no)
+			.as_("batch_no"),
+		)
+		.where(
+			(stock_ledger_entry.item_code == item_code)
+			& (stock_ledger_entry.warehouse == warehouse)
+			& (stock_ledger_entry.is_cancelled == 0)
+		)
+	).run(as_dict=True)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 6d652e4..e4e8e17 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -1120,6 +1120,8 @@
 								frm.refresh_fields();
 								frappe.model.set_value(item.doctype, item.name,
 									"serial_and_batch_bundle", r.name);
+
+								frm.save();
 							}
 						}
 					);
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index a6eb9bf..0691d63 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -779,7 +779,6 @@
 				if reset_outgoing_rate:
 					args = self.get_args_for_incoming_rate(d)
 					rate = get_incoming_rate(args, raise_error_if_no_rate)
-					print(rate, "set rate for outgoing items")
 					if rate > 0:
 						d.basic_rate = rate
 
@@ -1223,6 +1222,14 @@
 				if d.serial_and_batch_bundle and self.docstatus == 1:
 					self.copy_serial_and_batch_bundle(sle, d)
 
+				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"
+					)
+
+					if d.serial_and_batch_bundle != bundle_id:
+						sle.serial_and_batch_bundle = bundle_id
+
 				sl_entries.append(sle)
 
 	def copy_serial_and_batch_bundle(self, sle, child):
@@ -1240,9 +1247,17 @@
 			bundle_doc.type_of_transaction = "Inward"
 
 			for row in bundle_doc.ledgers:
+				if row.qty < 0:
+					row.qty = abs(row.qty)
+
+				if row.stock_value_difference < 0:
+					row.stock_value_difference = abs(row.stock_value_difference)
+
 				row.warehouse = child.t_warehouse
 				row.is_outward = 0
 
+			bundle_doc.set_total_qty()
+			bundle_doc.set_avg_rate()
 			bundle_doc.flags.ignore_permissions = True
 			bundle_doc.submit()
 			sle.serial_and_batch_bundle = bundle_doc.name
@@ -2859,6 +2874,8 @@
 	)
 
 	if row.serial_nos and row.batches_to_be_consume:
+		doc.has_serial_no = 1
+		doc.has_batch_no = 1
 		batchwise_serial_nos = get_batchwise_serial_nos(child.item_code, row)
 		for batch_no, qty in row.batches_to_be_consume.items():
 
@@ -2870,17 +2887,19 @@
 						"batch_no": batch_no,
 						"serial_no": batchwise_serial_nos.get(batch_no).pop(0),
 						"warehouse": row.warehouse,
-						"qty": qty,
+						"qty": -1,
 					},
 				)
 
 	elif row.serial_nos:
+		doc.has_serial_no = 1
 		for serial_no in row.serial_nos:
-			doc.append("ledgers", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": 1})
+			doc.append("ledgers", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": -1})
 
 	elif row.batches_to_be_consume:
+		doc.has_batch_no = 1
 		for batch_no, qty in row.batches_to_be_consume.items():
-			doc.append("ledgers", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty})
+			doc.append("ledgers", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1})
 
 	return doc.insert(ignore_permissions=True).name
 
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
index f3943eb..8e148f7 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -20,13 +20,13 @@
   "serial_and_batch_bundle",
   "batch_no",
   "column_break_11",
+  "current_serial_and_batch_bundle",
   "serial_no",
   "section_break_3",
   "current_qty",
   "current_amount",
   "column_break_9",
   "current_valuation_rate",
-  "current_serial_and_batch_bundle",
   "current_serial_no",
   "section_break_14",
   "quantity_difference",
@@ -192,7 +192,7 @@
   {
    "fieldname": "serial_and_batch_bundle",
    "fieldtype": "Link",
-   "label": "Serial and Batch Bundle",
+   "label": "Serial / Batch Bundle",
    "no_copy": 1,
    "options": "Serial and Batch Bundle",
    "print_hide": 1
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 1e28988..7c4f062 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -6,7 +6,6 @@
 from frappe.model.naming import make_autoname
 from frappe.query_builder.functions import Sum
 from frappe.utils import cint, flt, now
-from pypika import Case
 
 from erpnext.stock.deprecated_serial_batch import (
 	DeprecatedBatchNoValuation,
@@ -209,13 +208,18 @@
 		frappe.db.bulk_insert("Serial and Batch Ledger", fields=fields, values=set(ledgers))
 
 	def add_batch_no_to_bundle(self, sn_doc, batch_no, incoming_rate):
+		stock_value_difference = flt(self.sle.actual_qty) * flt(incoming_rate)
+
+		if self.sle.actual_qty < 0:
+			stock_value_difference *= -1
+
 		sn_doc.append(
 			"ledgers",
 			{
 				"batch_no": batch_no,
 				"qty": self.sle.actual_qty,
 				"incoming_rate": incoming_rate,
-				"stock_value_difference": flt(self.sle.actual_qty) * flt(incoming_rate),
+				"stock_value_difference": stock_value_difference,
 			},
 		)
 
@@ -286,7 +290,7 @@
 
 		frappe.db.set_value(
 			"Serial and Batch Bundle",
-			self.sle.serial_and_batch_bundle,
+			{"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
 			{"is_cancelled": 1, "voucher_no": ""},
 		)
 
@@ -303,22 +307,24 @@
 		return False
 
 	def post_process(self):
-		if not self.sle.is_cancelled:
-			if self.item_details.has_serial_no == 1:
-				self.set_warehouse_and_status_in_serial_nos()
+		if self.item_details.has_serial_no == 1:
+			self.set_warehouse_and_status_in_serial_nos()
 
-			if self.item_details.has_serial_no == 1 and self.item_details.has_batch_no == 1:
-				self.set_batch_no_in_serial_nos()
-		else:
-			pass
-			# self.set_data_based_on_last_sle()
+		if (
+			self.sle.actual_qty > 0
+			and self.item_details.has_serial_no == 1
+			and self.item_details.has_batch_no == 1
+		):
+			self.set_batch_no_in_serial_nos()
 
 	def set_warehouse_and_status_in_serial_nos(self):
+		serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle, check_outward=False)
 		warehouse = self.warehouse if self.sle.actual_qty > 0 else None
 
-		sn_table = frappe.qb.DocType("Serial No")
-		serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle, check_outward=False)
+		if not serial_nos:
+			return
 
+		sn_table = frappe.qb.DocType("Serial No")
 		(
 			frappe.qb.update(sn_table)
 			.set(sn_table.warehouse, warehouse)
@@ -330,7 +336,7 @@
 		ledgers = frappe.get_all(
 			"Serial and Batch Ledger",
 			fields=["serial_no", "batch_no"],
-			filters={"parent": self.serial_and_batch_bundle},
+			filters={"parent": self.sle.serial_and_batch_bundle},
 		)
 
 		batch_serial_nos = {}
@@ -391,7 +397,7 @@
 					TIMESTAMP(
 						parent.posting_date, parent.posting_time
 					)
-				), child.name
+				), child.name, child.serial_no, child.warehouse
 			FROM
 				`tabSerial and Batch Bundle` as parent,
 				`tabSerial and Batch Ledger` as child
@@ -417,14 +423,18 @@
 		return frappe.db.sql(
 			f"""
 			SELECT
-				serial_no, incoming_rate
+				ledger.serial_no, ledger.incoming_rate, ledger.warehouse
 			FROM
 				`tabSerial and Batch Ledger` AS ledger,
 				({subquery}) AS SubQuery
 			WHERE
 				ledger.name = SubQuery.name
+				AND ledger.serial_no = SubQuery.serial_no
+				AND ledger.warehouse = SubQuery.warehouse
 			GROUP BY
 				ledger.serial_no
+			Order By
+				ledger.creation
 		""",
 			as_dict=1,
 		)
@@ -468,7 +478,7 @@
 		return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
 
 	def get_incoming_rate(self):
-		return flt(self.stock_value_change) / flt(self.sle.actual_qty)
+		return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
 
 
 def is_rejected(voucher_type, voucher_detail_no, warehouse):
@@ -517,7 +527,7 @@
 			.select(
 				child.batch_no,
 				Sum(child.stock_value_difference).as_("incoming_rate"),
-				Sum(Case().when(child.is_outward == 1, child.qty * -1).else_(child.qty)).as_("qty"),
+				Sum(child.qty).as_("qty"),
 			)
 			.where(
 				(child.batch_no.isin(batch_nos))
@@ -544,7 +554,7 @@
 	def set_stock_value_difference(self):
 		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 * -1
+			stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
 			self.stock_value_change += stock_value_change
 			frappe.db.set_value(
 				"Serial and Batch Ledger", ledger.name, "stock_value_difference", stock_value_change
@@ -564,9 +574,4 @@
 		self.wh_data.qty_after_transaction += self.sle.actual_qty
 
 	def get_incoming_rate(self):
-		return flt(self.stock_value_change) / flt(self.sle.actual_qty)
-
-
-class GetAvailableSerialBatchBundle:
-	def __init__(self) -> None:
-		pass
+		return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index 4bf008a..78572a6 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -7,6 +7,7 @@
 
 frappe.ui.form.on('Subcontracting Receipt', {
 	setup: (frm) => {
+		frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
 		frm.get_field('supplied_items').grid.cannot_add_rows = true;
 		frm.get_field('supplied_items').grid.only_sortable();
 
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 4e500a6..40dfd0d 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -105,7 +105,12 @@
 		self.update_status()
 
 	def on_cancel(self):
-		self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
+		self.ignore_linked_doctypes = (
+			"GL Entry",
+			"Stock Ledger Entry",
+			"Repost Item Valuation",
+			"Serial and Batch Bundle",
+		)
 		self.update_status_updater_args()
 		self.update_prevdoc_status()
 		self.update_stock_ledger()
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
index 78e94c0..90bcf4e 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
@@ -33,6 +33,7 @@
  ],
  "fields": [
   {
+   "columns": 2,
    "fieldname": "main_item_code",
    "fieldtype": "Link",
    "in_list_view": 1,
@@ -41,6 +42,7 @@
    "read_only": 1
   },
   {
+   "columns": 2,
    "fieldname": "rm_item_code",
    "fieldtype": "Link",
    "in_list_view": 1,
@@ -77,14 +79,16 @@
    "fieldtype": "Column Break"
   },
   {
+   "columns": 1,
    "fieldname": "required_qty",
    "fieldtype": "Float",
+   "in_list_view": 1,
    "label": "Required Qty",
    "print_hide": 1,
    "read_only": 1
   },
   {
-   "columns": 2,
+   "columns": 1,
    "fieldname": "consumed_qty",
    "fieldtype": "Float",
    "in_list_view": 1,
@@ -102,6 +106,7 @@
   {
    "fieldname": "rate",
    "fieldtype": "Currency",
+   "in_list_view": 1,
    "label": "Rate",
    "options": "Company:company:default_currency",
    "read_only": 1
@@ -124,7 +129,6 @@
   {
    "fieldname": "current_stock",
    "fieldtype": "Float",
-   "in_list_view": 1,
    "label": "Current Stock",
    "read_only": 1
   },
@@ -188,25 +192,25 @@
    "default": "0",
    "fieldname": "available_qty_for_consumption",
    "fieldtype": "Float",
-   "in_list_view": 1,
    "label": "Available Qty For Consumption",
    "print_hide": 1,
    "read_only": 1
   },
   {
+   "columns": 2,
    "fieldname": "serial_and_batch_bundle",
    "fieldtype": "Link",
-   "label": "Serial and Batch Bundle",
+   "in_list_view": 1,
+   "label": "Serial / Batch Bundle",
    "no_copy": 1,
    "options": "Serial and Batch Bundle",
-   "print_hide": 1,
-   "read_only": 1
+   "print_hide": 1
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-03-12 14:11:48.816699",
+ "modified": "2023-03-15 13:55:08.132626",
  "modified_by": "Administrator",
  "module": "Subcontracting",
  "name": "Subcontracting Receipt Supplied Item",