Merge pull request #39478 from rohitwaghchaure/fixed-ux-improvement-for-SABB

fix: UX improvements for Serial and Batch Bundle
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index cf7fab8..aacab0f 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -105,32 +105,47 @@
 				this.frm.has_items = false;
 			}
 
-			if (serial_no && this.is_duplicate_serial_no(row, item_code, serial_no)) {
-				this.clean_up();
-				reject();
-				return;
+			if (serial_no) {
+				this.is_duplicate_serial_no(row, item_code, serial_no)
+					.then((is_duplicate) => {
+						if (!is_duplicate) {
+							this.run_serially_tasks(row, data, resolve);
+						} else {
+							this.clean_up();
+							reject();
+							return;
+						}
+					});
+			} else {
+				this.run_serially_tasks(row, data, resolve);
 			}
 
-			frappe.run_serially([
-				() => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
-				() => this.set_barcode(row, barcode),
-				() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
-					this.show_scan_message(row.idx, row.item_code, qty);
-				}),
-				() => this.set_barcode_uom(row, uom),
-				() => this.clean_up(),
-				() => resolve(row),
-				() => {
-					if (row.serial_and_batch_bundle && !this.frm.is_new()) {
-						this.frm.save();
-					}
 
-					frappe.flags.trigger_from_barcode_scanner = false;
-				}
-			]);
 		});
 	}
 
+	run_serially_tasks(row, data, resolve) {
+		const {item_code, barcode, batch_no, serial_no, uom} = data;
+
+		frappe.run_serially([
+			() => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
+			() => this.set_barcode(row, barcode),
+			() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
+				this.show_scan_message(row.idx, row.item_code, qty);
+			}),
+			() => this.set_barcode_uom(row, uom),
+			() => this.clean_up(),
+			() => {
+				if (row.serial_and_batch_bundle && !this.frm.is_new()) {
+					this.frm.save();
+				}
+
+				frappe.flags.trigger_from_barcode_scanner = false;
+			},
+			() => resolve(row),
+		]);
+	}
+
 	set_item(row, item_code, barcode, batch_no, serial_no) {
 		return new Promise(resolve => {
 			const increment = async (value = 1) => {
@@ -475,26 +490,32 @@
 		}
 	}
 
-	is_duplicate_serial_no(row, item_code, serial_no) {
-		if (this.frm.is_new() || !row.serial_and_batch_bundle) {
-			let is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
-			if (is_duplicate) {
-				this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
-			}
-
-			return is_duplicate;
-		} else if (row.serial_and_batch_bundle) {
-			this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
-				if (r.message) {
+	async is_duplicate_serial_no(row, item_code, serial_no) {
+		let is_duplicate = false;
+		const promise = new Promise((resolve, reject) => {
+			if (this.frm.is_new() || !row.serial_and_batch_bundle) {
+				is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
+				if (is_duplicate) {
 					this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
 				}
 
-				return r.message;
-			})
-		}
+				resolve(is_duplicate);
+			} else if (row.serial_and_batch_bundle) {
+				this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
+					if (r.message) {
+						this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
+					}
+
+					is_duplicate = r.message;
+					resolve(is_duplicate);
+				})
+			}
+		});
+
+		return await promise;
 	}
 
-	async check_duplicate_serial_no_in_db(row, serial_no, response) {
+	check_duplicate_serial_no_in_db(row, serial_no, response) {
 		frappe.call({
 			method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no",
 			args: {
@@ -504,7 +525,7 @@
 			callback(r) {
 				response(r);
 			}
-		})
+		});
 	}
 
 	check_duplicate_serial_no_in_localstorage(item_code, serial_no) {
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index bf362e3..44a4957 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -135,7 +135,7 @@
 						filters: this.get_serial_no_filters()
 					};
 				},
-				onchange: () => this.update_serial_batch_no()
+				onchange: () => this.scan_barcode_data()
 			});
 		}
 
@@ -145,7 +145,7 @@
 				options: 'Barcode',
 				fieldname: 'scan_batch_no',
 				label: __('Scan Batch No'),
-				onchange: () => this.update_serial_batch_no()
+				onchange: () => this.scan_barcode_data()
 			});
 		}
 
@@ -179,11 +179,54 @@
 			label = __('Serial Nos / Batch Nos');
 		}
 
-		return [
+		let fields = [
 			{
 				fieldtype: 'Section Break',
 				label: __('{0} {1} via CSV File', [primary_label, label])
-			},
+			}
+		]
+
+		if (this.item?.has_serial_no) {
+			fields = [...fields,
+				{
+					fieldtype: 'Check',
+					label: __('Import Using CSV file'),
+					fieldname: 'import_using_csv_file',
+					default: 0,
+				},
+				{
+					fieldtype: 'Section Break',
+					label: __('{0} {1} Manually', [primary_label, label]),
+					depends_on: 'eval:doc.import_using_csv_file === 0',
+				},
+				{
+					fieldtype: 'Small Text',
+					label: __('Enter Serial Nos'),
+					fieldname: 'upload_serial_nos',
+					depends_on: 'eval:doc.import_using_csv_file === 0',
+					description: __('Enter each serial no in a new line'),
+				},
+				{
+					fieldtype: 'Column Break',
+					depends_on: 'eval:doc.import_using_csv_file === 0',
+				},
+				{
+					fieldtype: 'Button',
+					fieldname: 'make_serial_nos',
+					label: __('Create Serial Nos'),
+					depends_on: 'eval:doc.import_using_csv_file === 0',
+					click: () => {
+						this.create_serial_nos();
+					}
+				},
+				{
+					fieldtype: 'Section Break',
+					depends_on: 'eval:doc.import_using_csv_file === 1',
+				}
+			];
+		}
+
+		fields = [...fields,
 			{
 				fieldtype: 'Button',
 				fieldname: 'download_csv',
@@ -199,7 +242,32 @@
 				label: __('Attach CSV File'),
 				onchange: () => this.upload_csv_file()
 			}
-		]
+		];
+
+		return fields;
+	}
+
+	create_serial_nos() {
+		let {upload_serial_nos} = this.dialog.get_values();
+
+		if (!upload_serial_nos) {
+			frappe.throw(__('Please enter Serial Nos'));
+		}
+
+		frappe.call({
+			method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.create_serial_nos',
+			args: {
+				item_code: this.item.item_code,
+				serial_nos: upload_serial_nos
+			},
+			callback: (r) => {
+				if (r.message) {
+					this.dialog.fields_dict.entries.df.data = [];
+					this.set_data(r.message);
+					this.update_bundle_entries();
+				}
+			}
+		});
 	}
 
 	download_csv_file() {
@@ -374,6 +442,26 @@
 		}
 	}
 
+	scan_barcode_data() {
+		const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
+
+		if (scan_serial_no || scan_batch_no) {
+			frappe.call({
+				method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_serial_batch_no_exists',
+				args: {
+					item_code: this.item.item_code,
+					type_of_transaction: this.item.type_of_transaction,
+					serial_no: scan_serial_no,
+					batch_no: scan_batch_no,
+				},
+				callback: (r) => {
+					this.update_serial_batch_no();
+				}
+
+			})
+		}
+	}
+
 	update_serial_batch_no() {
 		const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
 
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
index 9f01ee9..91b7430 100644
--- 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
@@ -74,7 +74,7 @@
 
 		let fields = [
 			{
-				"label": __("Using CSV File"),
+				"label": __("Import Using CSV file"),
 				"fieldname": "using_csv_file",
 				"default": 1,
 				"fieldtype": "Check",
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 2b87fcd..63cc938 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
@@ -999,9 +999,25 @@
 
 		make_serial_nos(item_code, serial_nos)
 
+	if kwargs.get("_has_serial_nos"):
+		return serial_nos
+
 	return serial_nos, batch_nos
 
 
+@frappe.whitelist()
+def create_serial_nos(item_code, serial_nos):
+	serial_nos = get_serial_batch_from_data(
+		item_code,
+		{
+			"serial_nos": serial_nos,
+			"_has_serial_nos": True,
+		},
+	)
+
+	return serial_nos
+
+
 def make_serial_nos(item_code, serial_nos):
 	item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
 
@@ -2080,5 +2096,34 @@
 
 
 @frappe.whitelist()
+def is_serial_batch_no_exists(item_code, type_of_transaction, serial_no=None, batch_no=None):
+	if serial_no and not frappe.db.exists("Serial No", serial_no):
+		if type_of_transaction != "Inward":
+			frappe.throw(_("Serial No {0} does not exists").format(serial_no))
+
+		make_serial_no(serial_no, item_code)
+
+	if batch_no and frappe.db.exists("Batch", batch_no):
+		if type_of_transaction != "Inward":
+			frappe.throw(_("Batch No {0} does not exists").format(batch_no))
+
+		make_batch_no(batch_no, item_code)
+
+
+def make_serial_no(serial_no, item_code):
+	serial_no_doc = frappe.new_doc("Serial No")
+	serial_no_doc.serial_no = serial_no
+	serial_no_doc.item_code = item_code
+	serial_no_doc.save(ignore_permissions=True)
+
+
+def make_batch_no(batch_no, item_code):
+	batch_doc = frappe.new_doc("Batch")
+	batch_doc.batch_id = batch_no
+	batch_doc.item = item_code
+	batch_doc.save(ignore_permissions=True)
+
+
+@frappe.whitelist()
 def is_duplicate_serial_no(bundle_id, serial_no):
 	return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no})