refactor: serial no normalization
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index befde71..6156aba 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -7,7 +7,7 @@
 
 import frappe
 from frappe import _
-from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
+from frappe.utils import cint, flt, get_link_to_form, getdate
 
 import erpnext
 from erpnext.accounts.general_ledger import (
@@ -328,26 +328,49 @@
 	def make_batches(self, warehouse_field):
 		"""Create batches if required. Called before submit"""
 		for d in self.items:
-			if d.get(warehouse_field) and not d.batch_no:
+			if d.get(warehouse_field) and not d.serial_and_batch_bundle:
 				has_batch_no, create_new_batch = frappe.get_cached_value(
 					"Item", d.item_code, ["has_batch_no", "create_new_batch"]
 				)
 
 				if has_batch_no and create_new_batch:
-					d.batch_no = (
+					batch_no = (
 						frappe.get_doc(
-							dict(
-								doctype="Batch",
-								item=d.item_code,
-								supplier=getattr(self, "supplier", None),
-								reference_doctype=self.doctype,
-								reference_name=self.name,
-							)
+							dict(doctype="Batch", item=d.item_code, supplier=getattr(self, "supplier", None))
 						)
 						.insert()
 						.name
 					)
 
+					d.serial_and_batch_bundle = (
+						frappe.get_doc(
+							{
+								"doctype": "Serial and Batch Bundle",
+								"item_code": d.item_code,
+								"voucher_type": self.doctype,
+								"voucher_no": self.name,
+								"ledgers": [
+									{
+										"batch_no": batch_no,
+										"qty": d.qty,
+										"warehouse": d.get(warehouse_field),
+									}
+								],
+							}
+						)
+						.submit()
+						.name
+					)
+
+					frappe.db.set_value(
+						"Batch",
+						batch_no,
+						{
+							"reference_doctype": "Serial and Batch Bundle",
+							"reference_name": d.serial_and_batch_bundle,
+						},
+					)
+
 	def check_expense_account(self, item):
 		if not item.get("expense_account"):
 			msg = _("Please set an Expense Account in the Items table")
@@ -387,27 +410,20 @@
 				)
 
 	def delete_auto_created_batches(self):
-		for d in self.items:
-			if not d.batch_no:
-				continue
+		for row in self.items:
+			if row.serial_and_batch_bundle:
+				frappe.db.set_value(
+					"Serial and Batch Bundle", row.serial_and_batch_bundle, {"is_cancelled": 1}
+				)
 
-			frappe.db.set_value(
-				"Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None
-			)
-
-			d.batch_no = None
-			d.db_set("batch_no", None)
-
-		for data in frappe.get_all(
-			"Batch", {"reference_name": self.name, "reference_doctype": self.doctype}
-		):
-			frappe.delete_doc("Batch", data.name)
+				row.db_set("serial_and_batch_bundle", None)
 
 	def get_sl_entries(self, d, args):
 		sl_dict = frappe._dict(
 			{
 				"item_code": d.get("item_code", None),
 				"warehouse": d.get("warehouse", None),
+				"serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
 				"posting_date": self.posting_date,
 				"posting_time": self.posting_time,
 				"fiscal_year": get_fiscal_year(self.posting_date, company=self.company)[0],
@@ -420,7 +436,6 @@
 				),
 				"incoming_rate": 0,
 				"company": self.company,
-				"batch_no": cstr(d.get("batch_no")).strip(),
 				"serial_no": d.get("serial_no"),
 				"project": d.get("project") or self.get("project"),
 				"is_cancelled": 1 if self.docstatus == 2 else 0,
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index b0e08cc..e37a9b7 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -341,10 +341,36 @@
 						}
 						frappe.throw(msg);
 					}
-				});
-
-			}
+				}
+			);
 		}
+	}
+
+	update_serial_batch_bundle(doc, cdt, cdn) {
+		let item = locals[cdt][cdn];
+		let me = this;
+		let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
+
+		frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
+			.then((r) => {
+				if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
+					item.has_serial_no = r.message.has_serial_no;
+					item.has_batch_no = r.message.has_batch_no;
+
+					frappe.require(path, function() {
+						new erpnext.SerialNoBatchBundleUpdate(
+							me.frm, item, (r) => {
+								if (r) {
+									me.frm.refresh_fields();
+									frappe.model.set_value(cdt, cdn,
+										"serial_and_batch_bundle", r.name);
+								}
+							}
+						);
+					});
+				}
+			});
+	}
 };
 
 cur_frm.add_fetch('project', 'cost_center', 'cost_center');
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 96ff44e..b4676c1 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -119,9 +119,14 @@
 			}
 		});
 
-		if(this.frm.fields_dict["items"].grid.get_field('batch_no')) {
-			this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) {
-				return me.set_query_for_batch(doc, cdt, cdn);
+		if(this.frm.fields_dict["items"].grid.get_field('serial_and_batch_bundle')) {
+			this.frm.set_query("serial_and_batch_bundle", "items", function(doc, cdt, cdn) {
+				let item_row = locals[cdt][cdn];
+				return {
+					filters: {
+						'item_code': item_row.item_code
+					}
+				}
 			});
 		}
 
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 64c5ee5..1c98037 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -616,3 +616,195 @@
 }
 
 //# sourceURL=serial_no_batch_selector.js
+
+
+erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate {
+	constructor(frm, item, callback) {
+		this.frm = frm;
+		this.item = item;
+		this.qty = item.qty;
+		this.callback = callback;
+		this.make();
+		this.render_data();
+	}
+
+	make() {
+		this.dialog = new frappe.ui.Dialog({
+			title: __('Update Serial No / Batch No'),
+			fields: this.get_dialog_fields(),
+			primary_action_label: __('Update'),
+			primary_action: () => this.update_ledgers()
+		});
+		this.dialog.show();
+	}
+
+	get_serial_no_filters() {
+		return {
+			'item_code': this.item.item_code,
+			'warehouse': ["=", ""],
+			'delivery_document_no': ["=", ""],
+		};
+	}
+
+	get_dialog_fields() {
+		let fields = [];
+
+		if (this.item.has_serial_no) {
+			fields.push({
+				fieldtype: 'Link',
+				fieldname: 'scan_serial_no',
+				label: __('Scan Serial No'),
+				options: 'Serial No',
+				get_query: () => {
+					return {
+						filters: this.get_serial_no_filters()
+					};
+				},
+				onchange: () => this.update_serial_batch_no()
+			});
+		}
+
+		if (this.item.has_batch_no && this.item.has_serial_no) {
+			fields.push({
+				fieldtype: 'Column Break',
+				label: __('Batch No')
+			});
+		}
+
+		if (this.item.has_batch_no) {
+			fields.push({
+				fieldtype: 'Link',
+				fieldname: 'scan_batch_no',
+				label: __('Scan Batch No'),
+				options: 'Batch',
+				onchange: () => this.update_serial_batch_no()
+			});
+		}
+
+		if (this.item.has_batch_no && this.item.has_serial_no) {
+			fields.push({
+				fieldtype: 'Section Break',
+			});
+		}
+
+		fields.push({
+			fieldname: 'ledgers',
+			fieldtype: 'Table',
+			allow_bulk_edit: true,
+			data: [],
+			fields: this.get_dialog_table_fields(),
+		});
+
+		return fields;
+	}
+
+	get_dialog_table_fields() {
+		let fields = []
+
+		if (this.item.has_serial_no) {
+			fields.push({
+				fieldtype: 'Link',
+				options: 'Serial No',
+				fieldname: 'serial_no',
+				label: __('Serial No'),
+				in_list_view: 1,
+				get_query: () => {
+					return {
+						filters: this.get_serial_no_filters()
+					}
+				}
+			})
+		} else if (this.item.has_batch_no) {
+			fields = [
+				{
+					fieldtype: 'Link',
+					options: 'Batch',
+					fieldname: 'batch_no',
+					label: __('Batch No'),
+					in_list_view: 1,
+				},
+				{
+					fieldtype: 'Float',
+					fieldname: 'qty',
+					label: __('Quantity'),
+					in_list_view: 1,
+				}
+			]
+		}
+
+		fields.push({
+			fieldtype: 'Data',
+			fieldname: 'name',
+			label: __('Name'),
+			hidden: 1,
+		})
+
+		return fields;
+	}
+
+	update_serial_batch_no() {
+		const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
+
+		if (scan_serial_no) {
+			this.dialog.fields_dict.ledgers.df.data.push({
+				serial_no: scan_serial_no
+			});
+
+			this.dialog.fields_dict.scan_serial_no.set_value('');
+		} else if (scan_batch_no) {
+			this.dialog.fields_dict.ledgers.df.data.push({
+				batch_no: scan_batch_no
+			});
+
+			this.dialog.fields_dict.scan_batch_no.set_value('');
+		}
+
+		this.dialog.fields_dict.ledgers.grid.refresh();
+	}
+
+	update_ledgers() {
+		if (!this.frm.is_new()) {
+			let ledgers = this.dialog.get_values().ledgers;
+
+			if (ledgers && !ledgers.length) {
+				frappe.throw(__('Please add atleast one Serial No / Batch No'));
+			}
+
+			frappe.call({
+				method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_no_ledgers',
+				args: {
+					ledgers: ledgers,
+					child_row: this.item
+				}
+			}).then(r => {
+				this.callback && this.callback(r.message);
+				this.dialog.hide();
+			})
+		}
+	}
+
+	render_data() {
+		if (!this.frm.is_new() && this.item.serial_and_batch_bundle) {
+			frappe.call({
+				method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_no_ledgers',
+				args: {
+					item_code: this.item.item_code,
+					name: this.item.serial_and_batch_bundle,
+					voucher_no: this.item.parent,
+				}
+			}).then(r => {
+				if (r.message) {
+					this.set_data(r.message);
+				}
+			})
+		}
+	}
+
+	set_data(data) {
+		data.forEach(d => {
+			this.dialog.fields_dict.ledgers.df.data.push(d);
+		});
+
+		this.dialog.fields_dict.ledgers.grid.refresh();
+	}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/package_item/__init__.py b/erpnext/stock/doctype/package_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/package_item/__init__.py
diff --git a/erpnext/stock/doctype/package_item/package_item.js b/erpnext/stock/doctype/package_item/package_item.js
new file mode 100644
index 0000000..65fda46
--- /dev/null
+++ b/erpnext/stock/doctype/package_item/package_item.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Package Item', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/stock/doctype/package_item/package_item.json b/erpnext/stock/doctype/package_item/package_item.json
new file mode 100644
index 0000000..5b0246f
--- /dev/null
+++ b/erpnext/stock/doctype/package_item/package_item.json
@@ -0,0 +1,138 @@
+{
+ "actions": [],
+ "creation": "2022-09-29 14:56:38.338267",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item_details_tab",
+  "company",
+  "item_code",
+  "column_break_4",
+  "warehouse",
+  "qty",
+  "serial_no_and_batch_no_tab",
+  "transactions",
+  "reference_details_tab",
+  "voucher_type",
+  "voucher_no",
+  "column_break_12",
+  "voucher_detail_no",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "item_code",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Item Code",
+   "options": "Item",
+   "reqd": 1
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Package Item",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "item_details_tab",
+   "fieldtype": "Tab Break",
+   "label": "Item Details"
+  },
+  {
+   "fieldname": "warehouse",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Warehouse",
+   "options": "Warehouse",
+   "reqd": 1
+  },
+  {
+   "fieldname": "column_break_4",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Company",
+   "options": "Company",
+   "reqd": 1
+  },
+  {
+   "fieldname": "qty",
+   "fieldtype": "Float",
+   "label": "Total Qty"
+  },
+  {
+   "fieldname": "reference_details_tab",
+   "fieldtype": "Tab Break",
+   "label": "Reference Details"
+  },
+  {
+   "fieldname": "voucher_type",
+   "fieldtype": "Link",
+   "label": "Voucher Type",
+   "options": "DocType",
+   "reqd": 1
+  },
+  {
+   "fieldname": "voucher_no",
+   "fieldtype": "Dynamic Link",
+   "label": "Voucher No",
+   "options": "voucher_type"
+  },
+  {
+   "fieldname": "voucher_detail_no",
+   "fieldtype": "Data",
+   "label": "Voucher Detail No",
+   "read_only": 1
+  },
+  {
+   "fieldname": "serial_no_and_batch_no_tab",
+   "fieldtype": "Tab Break",
+   "label": "Serial No and Batch No"
+  },
+  {
+   "fieldname": "column_break_12",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "transactions",
+   "fieldtype": "Table",
+   "label": "Items",
+   "options": "Serial and Batch No Transaction",
+   "reqd": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-10-06 22:07:31.732744",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Package Item",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/package_item/package_item.py b/erpnext/stock/doctype/package_item/package_item.py
new file mode 100644
index 0000000..c0a2eaa
--- /dev/null
+++ b/erpnext/stock/doctype/package_item/package_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class PackageItem(Document):
+	pass
diff --git a/erpnext/stock/doctype/package_item/test_package_item.py b/erpnext/stock/doctype/package_item/test_package_item.py
new file mode 100644
index 0000000..6dcc9cb
--- /dev/null
+++ b/erpnext/stock/doctype/package_item/test_package_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestPackageItem(FrappeTestCase):
+	pass
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 312c166..e0cb8ca 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -7,6 +7,8 @@
 
 frappe.ui.form.on("Purchase Receipt", {
 	setup: (frm) => {
+		frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
+
 		frm.make_methods = {
 			'Landed Cost Voucher': () => {
 				let lcv = frappe.model.get_new_doc('Landed Cost Voucher');
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 3373d8a..660504d 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -283,7 +283,12 @@
 		self.update_stock_ledger()
 		self.make_gl_entries_on_cancel()
 		self.repost_future_sle_and_gle()
-		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.delete_auto_created_batches()
 		self.set_consumed_qty_in_subcontract_order()
 
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index cd320fd..97e7d72 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -91,14 +91,12 @@
   "delivery_note_item",
   "putaway_rule",
   "section_break_45",
-  "allow_zero_valuation_rate",
-  "bom",
-  "serial_no",
+  "update_serial_batch_bundle",
+  "serial_and_batch_bundle",
   "col_break5",
+  "allow_zero_valuation_rate",
   "include_exploded_items",
-  "batch_no",
-  "rejected_serial_no",
-  "item_tax_rate",
+  "bom",
   "item_weight_details",
   "weight_per_unit",
   "total_weight",
@@ -110,6 +108,7 @@
   "manufacturer_part_no",
   "accounting_details_section",
   "expense_account",
+  "item_tax_rate",
   "column_break_102",
   "provisional_expense_account",
   "accounting_dimensions_section",
@@ -565,37 +564,8 @@
   },
   {
    "fieldname": "section_break_45",
-   "fieldtype": "Section Break"
-  },
-  {
-   "depends_on": "eval:!doc.is_fixed_asset",
-   "fieldname": "serial_no",
-   "fieldtype": "Small Text",
-   "in_list_view": 1,
-   "label": "Serial No",
-   "no_copy": 1,
-   "oldfieldname": "serial_no",
-   "oldfieldtype": "Text"
-  },
-  {
-   "depends_on": "eval:!doc.is_fixed_asset",
-   "fieldname": "batch_no",
-   "fieldtype": "Link",
-   "in_list_view": 1,
-   "label": "Batch No",
-   "no_copy": 1,
-   "oldfieldname": "batch_no",
-   "oldfieldtype": "Link",
-   "options": "Batch",
-   "print_hide": 1
-  },
-  {
-   "depends_on": "eval:!doc.is_fixed_asset",
-   "fieldname": "rejected_serial_no",
-   "fieldtype": "Small Text",
-   "label": "Rejected Serial No",
-   "no_copy": 1,
-   "print_hide": 1
+   "fieldtype": "Section Break",
+   "label": "Serial and Batch No"
   },
   {
    "fieldname": "item_tax_template",
@@ -1016,12 +986,23 @@
    "no_copy": 1,
    "print_hide": 1,
    "read_only": 1
+  },
+  {
+   "fieldname": "serial_and_batch_bundle",
+   "fieldtype": "Link",
+   "label": "Serial and Batch Bundle",
+   "options": "Serial and Batch Bundle"
+  },
+  {
+   "fieldname": "update_serial_batch_bundle",
+   "fieldtype": "Button",
+   "label": "Add Serial / Batch No"
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-02-28 15:43:04.470104",
+ "modified": "2023-02-28 16:43:04.470104",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/__init__.py b/erpnext/stock/doctype/serial_and_batch_bundle/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/__init__.py
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js
new file mode 100644
index 0000000..085e33d
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js
@@ -0,0 +1,80 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Serial and Batch Bundle', {
+	setup(frm) {
+		frm.trigger('set_queries');
+	},
+
+	refresh(frm) {
+		frm.trigger('toggle_fields');
+	},
+
+	set_queries(frm) {
+		frm.set_query('item_code', () => {
+			return {
+				query: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.item_query',
+			};
+		});
+
+		frm.set_query('voucher_type', () => {
+			return {
+				filters: {
+					'istable': 0,
+					'issingle': 0,
+					'is_submittable': 1,
+				}
+			};
+		});
+
+		frm.set_query('voucher_no', () => {
+			return {
+				filters: {
+					'docstatus': ["!=", 2],
+				}
+			};
+		});
+
+		frm.set_query('serial_no', 'ledgers', () => {
+			return {
+				filters: {
+					item_code: frm.doc.item_code,
+				}
+			};
+		});
+
+		frm.set_query('batch_no', 'ledgers', () => {
+			return {
+				filters: {
+					item: frm.doc.item_code,
+				}
+			};
+		});
+
+		frm.set_query('warehouse', 'ledgers', () => {
+			return {
+				filters: {
+					company: frm.doc.company,
+				}
+			};
+		});
+	},
+
+	has_serial_no(frm) {
+		frm.trigger('toggle_fields');
+	},
+
+	has_batch_no(frm) {
+		frm.trigger('toggle_fields');
+	},
+
+	toggle_fields(frm) {
+		frm.fields_dict.ledgers.grid.update_docfield_property(
+			'serial_no', 'read_only', !frm.doc.has_serial_no
+		);
+
+		frm.fields_dict.ledgers.grid.update_docfield_property(
+			'batch_no', 'read_only', !frm.doc.has_batch_no
+		);
+	}
+});
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
new file mode 100644
index 0000000..a08ed83
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
@@ -0,0 +1,162 @@
+{
+ "actions": [],
+ "creation": "2022-09-29 14:56:38.338267",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item_details_tab",
+  "company",
+  "item_group",
+  "has_serial_no",
+  "column_break_4",
+  "item_code",
+  "item_name",
+  "has_batch_no",
+  "serial_no_and_batch_no_tab",
+  "ledgers",
+  "qty",
+  "tab_break_12",
+  "voucher_type",
+  "voucher_no",
+  "is_cancelled",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "item_details_tab",
+   "fieldtype": "Tab Break",
+   "label": "Item Details"
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Company",
+   "options": "Company",
+   "reqd": 1
+  },
+  {
+   "fetch_from": "item_code.item_group",
+   "fieldname": "item_group",
+   "fieldtype": "Link",
+   "label": "Item Group",
+   "options": "Item Group"
+  },
+  {
+   "default": "0",
+   "fetch_from": "item_code.has_serial_no",
+   "fieldname": "has_serial_no",
+   "fieldtype": "Check",
+   "label": "Has Serial No",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_4",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "item_code",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Item Code",
+   "options": "Item",
+   "reqd": 1
+  },
+  {
+   "fetch_from": "item_code.item_name",
+   "fieldname": "item_name",
+   "fieldtype": "Data",
+   "label": "Item Name"
+  },
+  {
+   "default": "0",
+   "fetch_from": "item_code.has_batch_no",
+   "fieldname": "has_batch_no",
+   "fieldtype": "Check",
+   "label": "Has Batch No",
+   "read_only": 1
+  },
+  {
+   "fieldname": "serial_no_and_batch_no_tab",
+   "fieldtype": "Section Break"
+  },
+  {
+   "allow_bulk_edit": 1,
+   "fieldname": "ledgers",
+   "fieldtype": "Table",
+   "label": "Serial / Batch Ledgers",
+   "options": "Serial and Batch Ledger",
+   "reqd": 1
+  },
+  {
+   "fieldname": "qty",
+   "fieldtype": "Float",
+   "label": "Total Qty",
+   "read_only": 1
+  },
+  {
+   "fieldname": "voucher_type",
+   "fieldtype": "Link",
+   "label": "Voucher Type",
+   "options": "DocType",
+   "reqd": 1
+  },
+  {
+   "fieldname": "voucher_no",
+   "fieldtype": "Dynamic Link",
+   "label": "Voucher No",
+   "options": "voucher_type"
+  },
+  {
+   "default": "0",
+   "fieldname": "is_cancelled",
+   "fieldtype": "Check",
+   "label": "Is Cancelled",
+   "read_only": 1
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Serial and Batch Bundle",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "tab_break_12",
+   "fieldtype": "Tab Break",
+   "label": "Reference"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-11-24 13:05:11.623968",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Serial and Batch Bundle",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "item_code"
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..ae25aad
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+
+class SerialandBatchBundle(Document):
+	def validate(self):
+		self.validate_serial_and_batch_no()
+
+	def validate_serial_and_batch_no(self):
+		if self.item_code and not self.has_serial_no and not self.has_batch_no:
+			msg = f"The Item {self.item_code} does not have Serial No or Batch No"
+			frappe.throw(_(msg))
+
+	def before_cancel(self):
+		self.delink_serial_and_batch_bundle()
+		self.clear_table()
+
+	def delink_serial_and_batch_bundle(self):
+		self.voucher_no = None
+
+		sles = frappe.get_all("Stock Ledger Entry", filters={"serial_and_batch_bundle": self.name})
+
+		for sle in sles:
+			frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_and_batch_bundle", None)
+
+	def clear_table(self):
+		self.set("ledgers", [])
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
+	item_filters = {"disabled": 0}
+	if txt:
+		item_filters["name"] = ("like", f"%{txt}%")
+
+	return frappe.get_all(
+		"Item",
+		filters=item_filters,
+		or_filters={"has_serial_no": 1, "has_batch_no": 1},
+		fields=["name", "item_name"],
+		as_list=1,
+	)
+
+
+@frappe.whitelist()
+def get_serial_batch_no_ledgers(item_code, voucher_no, name=None):
+	return frappe.get_all(
+		"Serial and Batch Bundle",
+		fields=[
+			"`tabSerial and Batch Ledger`.`name`",
+			"`tabSerial and Batch Ledger`.`qty`",
+			"`tabSerial and Batch Ledger`.`warehouse`",
+			"`tabSerial and Batch Ledger`.`batch_no`",
+			"`tabSerial and Batch Ledger`.`serial_no`",
+		],
+		filters=[
+			["Serial and Batch Bundle", "item_code", "=", item_code],
+			["Serial and Batch Ledger", "parent", "=", name],
+			["Serial and Batch Bundle", "voucher_no", "=", voucher_no],
+			["Serial and Batch Bundle", "docstatus", "!=", 2],
+		],
+	)
+
+
+@frappe.whitelist()
+def add_serial_batch_no_ledgers(ledgers, child_row) -> object:
+	if isinstance(child_row, str):
+		child_row = frappe._dict(frappe.parse_json(child_row))
+
+	if isinstance(ledgers, str):
+		ledgers = frappe.parse_json(ledgers)
+
+	if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle):
+		doc = update_serial_batch_no_ledgers(ledgers, child_row)
+	else:
+		doc = create_serial_batch_no_ledgers(ledgers, child_row)
+
+	return doc
+
+
+def create_serial_batch_no_ledgers(ledgers, child_row) -> object:
+	doc = frappe.get_doc(
+		{
+			"doctype": "Serial and Batch Bundle",
+			"voucher_type": child_row.parenttype,
+			"voucher_no": child_row.parent,
+			"item_code": child_row.item_code,
+			"voucher_detail_no": child_row.name,
+		}
+	)
+
+	for row in ledgers:
+		row = frappe._dict(row)
+		doc.append(
+			"ledgers",
+			{
+				"qty": row.qty or 1.0,
+				"warehouse": child_row.warehouse,
+				"batch_no": row.batch_no,
+				"serial_no": row.serial_no,
+			},
+		)
+
+	doc.save()
+
+	frappe.db.set_value(child_row.doctype, child_row.name, "serial_and_batch_bundle", doc.name)
+
+	frappe.msgprint(_("Serial and Batch Bundle created"), alert=True)
+
+	return doc
+
+
+def update_serial_batch_no_ledgers(ledgers, child_row) -> object:
+	doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
+	doc.voucher_detail_no = child_row.name
+	doc.set("ledgers", [])
+	doc.set("ledgers", ledgers)
+	doc.save()
+
+	frappe.msgprint(_("Serial and Batch Bundle updated"), alert=True)
+
+	return doc
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
new file mode 100644
index 0000000..02e5349
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestSerialandBatchBundle(FrappeTestCase):
+	pass
diff --git a/erpnext/stock/doctype/serial_and_batch_ledger/__init__.py b/erpnext/stock/doctype/serial_and_batch_ledger/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_ledger/__init__.py
diff --git a/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json
new file mode 100644
index 0000000..7fa9574
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json
@@ -0,0 +1,73 @@
+{
+ "actions": [],
+ "creation": "2022-09-29 14:55:15.909881",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "serial_no",
+  "batch_no",
+  "column_break_2",
+  "qty",
+  "warehouse",
+  "is_rejected"
+ ],
+ "fields": [
+  {
+   "depends_on": "eval:parent.has_serial_no == 1",
+   "fieldname": "serial_no",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Serial No",
+   "mandatory_depends_on": "eval:parent.has_serial_no == 1",
+   "options": "Serial No"
+  },
+  {
+   "depends_on": "eval:parent.has_batch_no == 1",
+   "fieldname": "batch_no",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Batch No",
+   "mandatory_depends_on": "eval:parent.has_batch_no == 1",
+   "options": "Batch"
+  },
+  {
+   "fieldname": "qty",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Qty"
+  },
+  {
+   "fieldname": "warehouse",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Warehouse",
+   "options": "Warehouse"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval:parent.voucher_type == 'Purchase Receipt'",
+   "fieldname": "is_rejected",
+   "fieldtype": "Check",
+   "label": "Is Rejected"
+  },
+  {
+   "fieldname": "column_break_2",
+   "fieldtype": "Column Break"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-11-24 13:00:23.598351",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Serial and Batch Ledger",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.py b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.py
new file mode 100644
index 0000000..945fdc1
--- /dev/null
+++ b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SerialandBatchLedger(Document):
+	pass
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 541d4d1..9338dc5 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -189,6 +189,7 @@
 	def get_last_sle(self, serial_no=None):
 		entries = {}
 		sle_dict = self.get_stock_ledger_entries(serial_no)
+		print("sle_dict", sle_dict)
 		if sle_dict:
 			if sle_dict.get("incoming", []):
 				entries["purchase_sle"] = sle_dict["incoming"][0]
@@ -206,33 +207,23 @@
 		if not serial_no:
 			serial_no = self.name
 
+		print("serial_no", serial_no)
 		for sle in frappe.db.sql(
 			"""
-			SELECT voucher_type, voucher_no,
-				posting_date, posting_time, incoming_rate, actual_qty, serial_no
+			SELECT sle.voucher_type, sle.voucher_no, serial_and_batch_bundle,
+				sle.posting_date, sle.posting_time, sle.incoming_rate, sle.actual_qty, snb.serial_no
 			FROM
-				`tabStock Ledger Entry`
+				`tabStock Ledger Entry` sle, `tabSerial and Batch Ledger` snb
 			WHERE
-				item_code=%s AND company = %s
-				AND is_cancelled = 0
-				AND (serial_no = %s
-					OR serial_no like %s
-					OR serial_no like %s
-					OR serial_no like %s
-				)
+				sle.item_code=%s AND sle.company = %s
+				AND sle.is_cancelled = 0
+				AND snb.serial_no = %s and snb.parent = sle.serial_and_batch_bundle
 			ORDER BY
-				posting_date desc, posting_time desc, creation desc""",
-			(
-				self.item_code,
-				self.company,
-				serial_no,
-				serial_no + "\n%",
-				"%\n" + serial_no,
-				"%\n" + serial_no + "\n%",
-			),
+				sle.posting_date desc, sle.posting_time desc, sle.creation desc""",
+			(self.item_code, self.company, serial_no),
 			as_dict=1,
 		):
-			if serial_no.upper() in get_serial_nos(sle.serial_no):
+			if serial_no.upper() in get_serial_nos(sle.serial_and_batch_bundle):
 				if cint(sle.actual_qty) > 0:
 					sle_dict.setdefault("incoming", []).append(sle)
 				else:
@@ -262,6 +253,7 @@
 
 	def update_serial_no_reference(self, serial_no=None):
 		last_sle = self.get_last_sle(serial_no)
+		print(last_sle)
 		self.set_purchase_details(last_sle.get("purchase_sle"))
 		self.set_sales_details(last_sle.get("delivery_sle"))
 		self.set_maintenance_status()
@@ -275,7 +267,7 @@
 
 
 def validate_serial_no(sle, item_det):
-	serial_nos = get_serial_nos(sle.serial_no) if sle.serial_no else []
+	serial_nos = get_serial_nos(sle.serial_and_batch_bundle) if sle.serial_and_batch_bundle else []
 	validate_material_transfer_entry(sle)
 
 	if item_det.has_serial_no == 0:
@@ -541,7 +533,7 @@
 		return
 	if (
 		not sle.is_cancelled
-		and not sle.serial_no
+		and not sle.serial_and_batch_bundle
 		and cint(sle.actual_qty) > 0
 		and item_det.has_serial_no == 1
 		and item_det.serial_no_series
@@ -549,7 +541,7 @@
 		serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty)
 		sle.db_set("serial_no", serial_nos)
 		validate_serial_no(sle, item_det)
-	if sle.serial_no:
+	if sle.serial_and_batch_bundle:
 		auto_make_serial_nos(sle)
 
 
@@ -569,7 +561,7 @@
 
 
 def auto_make_serial_nos(args):
-	serial_nos = get_serial_nos(args.get("serial_no"))
+	serial_nos = get_serial_nos(args.get("serial_and_batch_bundle"))
 	created_numbers = []
 	voucher_type = args.get("voucher_type")
 	item_code = args.get("item_code")
@@ -624,13 +616,14 @@
 	)[0]
 
 
-def get_serial_nos(serial_no):
-	if isinstance(serial_no, list):
-		return serial_no
+def get_serial_nos(serial_and_batch_bundle):
+	serial_nos = frappe.get_all(
+		"Serial and Batch Ledger",
+		filters={"parent": serial_and_batch_bundle, "serial_no": ("is", "set")},
+		fields=["serial_no"],
+	)
 
-	return [
-		s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()
-	]
+	return [d.serial_no for d in serial_nos]
 
 
 def clean_serial_no_string(serial_no: str) -> str:
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
index 46ce9de..0df0a04 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
@@ -31,6 +31,7 @@
   "company",
   "stock_uom",
   "project",
+  "serial_and_batch_bundle",
   "batch_no",
   "column_break_26",
   "fiscal_year",
@@ -309,6 +310,13 @@
    "label": "Recalculate Incoming/Outgoing Rate",
    "no_copy": 1,
    "read_only": 1
+  },
+  {
+   "fieldname": "serial_and_batch_bundle",
+   "fieldtype": "Link",
+   "label": "Serial and Batch Bundle",
+   "options": "Serial and Batch Bundle",
+   "search_index": 1
   }
  ],
  "hide_toolbar": 1,
@@ -317,7 +325,7 @@
  "in_create": 1,
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-12-21 06:25:30.040801",
+ "modified": "2022-11-24 13:14:31.974743",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Ledger Entry",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 052f778..916b14a 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -40,7 +40,7 @@
 		from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
 
 		self.validate_mandatory()
-		self.validate_item()
+		self.validate_serial_batch_no_bundle()
 		self.validate_batch()
 		validate_disabled_warehouse(self.warehouse)
 		validate_warehouse_company(self.warehouse, self.company)
@@ -79,47 +79,43 @@
 		if self.voucher_type != "Stock Reconciliation" and not self.actual_qty:
 			frappe.throw(_("Actual Qty is mandatory"))
 
-	def validate_item(self):
-		item_det = frappe.db.sql(
-			"""select name, item_name, has_batch_no, docstatus,
-			is_stock_item, has_variants, stock_uom, create_new_batch
-			from tabItem where name=%s""",
+	def validate_serial_batch_no_bundle(self):
+		item_detail = frappe.get_cached_value(
+			"Item",
 			self.item_code,
-			as_dict=True,
+			["has_serial_no", "has_batch_no", "is_stock_item", "has_variants", "stock_uom"],
+			as_dict=1,
 		)
 
-		if not item_det:
+		if not item_detail:
 			frappe.throw(_("Item {0} not found").format(self.item_code))
 
-		item_det = item_det[0]
-
-		if item_det.is_stock_item != 1:
-			frappe.throw(_("Item {0} must be a stock Item").format(self.item_code))
-
-		# check if batch number is valid
-		if item_det.has_batch_no == 1:
-			batch_item = (
-				self.item_code
-				if self.item_code == item_det.item_name
-				else self.item_code + ":" + item_det.item_name
-			)
-			if not self.batch_no:
-				frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item))
-			elif not frappe.db.get_value("Batch", {"item": self.item_code, "name": self.batch_no}):
-				frappe.throw(
-					_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)
-				)
-
-		elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0:
-			frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code))
-
-		if item_det.has_variants:
+		if item_detail.has_variants:
 			frappe.throw(
 				_("Stock cannot exist for Item {0} since has variants").format(self.item_code),
 				ItemTemplateCannotHaveStock,
 			)
 
-		self.stock_uom = item_det.stock_uom
+		if item_detail.is_stock_item != 1:
+			frappe.throw(_("Item {0} must be a stock Item").format(self.item_code))
+
+		if item_detail.has_serial_no or item_detail.has_batch_no:
+			if not self.serial_and_batch_bundle:
+				frappe.throw(_(f"Serial No and Batch No are mandatory for Item {self.item_code}"))
+			elif self.item_code != frappe.get_cached_value(
+				"Serial and Batch Bundle", self.serial_and_batch_bundle, "item_code"
+			):
+				frappe.throw(
+					_(
+						f"Serial No and Batch No Bundle {self.serial_and_batch_bundle} is not for Item {self.item_code}"
+					)
+				)
+
+		if self.serial_and_batch_bundle and not (item_detail.has_serial_no or item_detail.has_batch_no):
+			frappe.throw(_(f"Serial No and Batch No are not allowed for Item {self.item_code}"))
+
+		if self.stock_uom != item_detail.stock_uom:
+			self.stock_uom = item_detail.stock_uom
 
 	def check_stock_frozen_date(self):
 		stock_settings = frappe.get_cached_doc("Stock Settings")