make erpnext.SerialNoBatchSelector
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 393e90c..9405719 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -30,6 +30,7 @@
         "public/js/payment/payment_details.html",
         "public/js/templates/item_selector.html",
         "public/js/utils/item_selector.js",
+        "public/js/utils/serial_no_batch_selector.js",
         "public/js/help_links.js",
         "public/js/schools/student_button.html",
         "public/js/schools/assessment_result_tool.html"
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
new file mode 100644
index 0000000..f54fc0f
--- /dev/null
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -0,0 +1,294 @@
+
+erpnext.SerialNoBatchSelector = Class.extend({
+	init: function(opts) {
+		$.extend(this, opts);
+		// frm, item, warehouse_details, has_batch, oldest
+		this.setup();
+	},
+
+	setup: function() {
+		this.item_code = this.item.item_code;
+		this.qty = this.item.qty;
+		this.make_dialog();
+	},
+
+	make_dialog: function() {
+		var me = this;
+
+		this.data = this.oldest ? this.oldest : [];
+		let title = "";
+		let fields = [
+			{fieldname: 'item_code', read_only: 1, fieldtype:'Link', options: 'Item',
+				label: __('Item Code'), 'default': me.item_code},
+			{fieldtype:'Column Break'},
+			{
+				fieldname: 'warehouse',
+				fieldtype:'Link',
+				options: 'Warehouse',
+				label: __(me.warehouse_details.type),
+				default: me.warehouse_details.name,
+				onchange: function(e) {
+					me.warehouse_details.name = this.get_value();
+					var batches = this.layout.fields_dict.batches;
+					if(batches) {
+						batches.grid.df.data = [];
+						batches.grid.refresh();
+						batches.grid.add_new_row(null, null, null);
+					}
+				}
+			},
+			{fieldtype:'Column Break'},
+			{fieldname: 'qty', fieldtype:'Float', label: __(me.has_batch ? 'Total Qty' : 'Qty'), 'default': me.qty},
+		];
+
+		if(this.has_batch) {
+			title = __("Select Batch Numbers");
+			fields = fields.concat(this.get_batch_fields());
+		} else {
+			title = __("Select Serial Numbers");
+			fields = fields.concat(this.get_serial_no_fields());
+		}
+
+		this.dialog = new frappe.ui.Dialog({
+			title: title,
+			fields: fields
+		});
+
+		this.bind_qty();
+
+		this.dialog.set_primary_action(__('Insert'), function() {
+			me.values = me.dialog.get_values();
+			if(!me.validate()) return;
+			me.set_items();
+			refresh_field("items");
+			me.dialog.hide();
+		});
+
+		this.dialog.show();
+	},
+
+	validate: function() {
+		let values = this.values;
+		if(!values.warehouse) {
+			frappe.throw(__("Please select a warehouse"));
+			return false;
+		}
+		if(this.has_batch) {
+			if(values.batches.length === 0 || !values.batches) {
+				frappe.throw(__("Please select batches for batched item "
+					+ values.item_code));
+				return false;
+			}
+			values.batches.map((batch, i) => {
+				if(!batch.selected_qty || batch.selected_qty === 0 ) {
+					frappe.throw(__("Please select quantity on row " + (i+1)));
+					return false;
+				}
+			});
+			return true;
+
+		} else {
+			let serial_nos = values.serial_no || '';
+			if (!serial_nos || !serial_nos.replace(/\s/g, '').length) {
+				frappe.throw(__("Please enter serial numbers for serialized item "
+					+ values.item_code));
+				return false;
+			}
+			return true;
+		}
+	},
+
+	set_items: function() {
+		if(this.has_batch) {
+			this.values.batches.map((batch, i) => {
+				if(i === 0) {
+					this.map_item_values(this.item, batch, 'batch_no',
+						'selected_qty', this.values.warehouse);
+				} else {
+					let row = this.frm.add_child("items");
+					row.item_code = this.item_code;
+					this.map_item_values(row, batch, 'batch_no',
+						'selected_qty', this.values.warehouse);
+				}
+			});
+		} else {
+			this.map_item_values(this.item, this.values, 'serial_no', 'qty');
+		}
+	},
+
+	map_item_values: function(item, values, attribute, qty_field, warehouse) {
+		item[attribute] = values[attribute];
+		if(this.warehouse_details.type === 'Source Warehouse') {
+			item.s_warehouse = values.warehouse || warehouse;
+		} else {
+			item.t_warehouse = values.warehouse || warehouse;
+		}
+		item.qty = values[qty_field];
+	},
+
+	bind_qty: function() {
+		let serial_no_link = this.dialog.fields_dict.serial_no_select;
+		let serial_no_list_field = this.dialog.fields_dict.serial_no;
+		let batches_field = this.dialog.fields_dict.batches;
+
+		let qty_field = this.dialog.fields_dict.qty;
+		let item_code = this.item_code;
+
+		let update_quantity = (batch) => {
+			if(batch) {
+				let total_qty = 0;
+				batches_field.grid.wrapper.find(
+					'input[data-fieldname="selected_qty"]').each(function() {
+
+					total_qty += Number($(this).val());
+				});
+				qty_field.set_input(total_qty);
+			} else {
+				let serial_numbers = serial_no_list_field.get_value()
+					.replace(/\n/g, ' ').match(/\S+/g) || [];
+				qty_field.set_input(serial_numbers.length);
+			}
+		}
+
+		if(serial_no_link) {
+			let serial_list = [];
+			serial_no_link.$input.on('awesomplete-selectcomplete', function() {
+				if(serial_no_link.get_value().length > 0) {
+					let new_no = serial_no_link.get_value();
+					let list_value = serial_no_list_field.get_value();
+					let new_line = '\n';
+					if(!serial_no_list_field.get_value()) {
+						new_line = '';
+					} else {
+						serial_list = list_value.replace(/\s+/g, ' ').split(' ');
+					}
+					if(!serial_list.includes(new_no)) {
+						serial_no_link.set_new_description('');
+						serial_no_list_field.set_value(list_value + new_line + new_no);
+						update_quantity(0);
+					} else {
+						serial_no_link.set_new_description(new_no + ' is already selected.');
+					}
+				}
+
+				// Should, but doesn't work
+				serial_no_link.set_input('');
+				serial_no_link.$input.blur();
+			});
+
+			serial_no_list_field.$input.on('input', function() {
+				serial_list = serial_no_list_field.get_value().replace(/\s+/g, ' ').split(' ');
+				update_quantity(0);
+			});
+		}
+
+		if(batches_field) {
+			batches_field.grid.wrapper.on('change', function() {
+				update_quantity(1);
+			});
+		}
+	},
+
+	get_batch_fields: function() {
+		var me = this;
+		return [
+			{fieldtype:'Section Break', label: __('Batches')},
+			{fieldname: 'batches', fieldtype: 'Table',
+				fields: [
+					{
+						fieldtype:'Link',
+						fieldname:'batch_no',
+						options: 'Batch',
+						label: __('Select Batch'),
+						in_list_view:1,
+						get_query: function() {
+							return {filters: {item: me.item_code }};
+						},
+						onchange: function(e) {
+							if(this.get_value().length === 0) {
+								this.grid_row.on_grid_fields_dict
+									.available_qty.set_value(0);
+								return;
+							}
+							if(me.warehouse_details.name) {
+								frappe.call({
+									method: 'erpnext.stock.doctype.batch.batch.get_batch_qty',
+									args: {
+										batch_no: this.doc.batch_no,
+										warehouse: me.warehouse_details.name,
+										item_code: me.item_code
+									},
+									callback: (r) => {
+										this.grid_row.on_grid_fields_dict
+											.available_qty.set_value(r.message || 0);
+									}
+								});
+
+							} else {
+								frappe.throw(__(`Please select a warehouse to get available
+									quantities`));
+							}
+							// e.stopImmediatePropagation();
+						}
+					},
+					{
+						fieldtype:'Float',
+						read_only:1,
+						fieldname:'available_qty',
+						label: __('Available'),
+						in_list_view:1,
+						default: 0,
+						onchange: function() {
+							this.grid_row.on_grid_fields_dict.selected_qty.set_value('0');
+						}
+					},
+					{
+						fieldtype:'Float',
+						fieldname:'selected_qty',
+						label: __('Qty'),
+						in_list_view:1,
+						'default': 0,
+						onchange: function(e) {
+							var batch_no = this.grid_row.on_grid_fields_dict.batch_no.get_value();
+							var available_qty = this.grid_row.on_grid_fields_dict.available_qty.get_value();
+							var selected_qty = this.grid_row.on_grid_fields_dict.selected_qty.get_value();
+
+							if(batch_no.length === 0 && parseInt(selected_qty)!==0) {
+								frappe.throw(__("Please select a batch"));
+							}
+							if(me.warehouse_details.type === 'Source Warehouse' &&
+								parseFloat(available_qty) < parseFloat(selected_qty)) {
+									this.set_value('0');
+									frappe.throw(__(`For transfer from source, selected quantity cannot be
+										greater than available quantity`));
+							} else {
+								this.grid.refresh();
+							}
+						}
+					},
+				],
+				in_place_edit: true,
+				data: this.data,
+				get_data: function() {
+					return this.data;
+				},
+			}
+		];
+	},
+
+	get_serial_no_fields: function() {
+		var me = this;
+		return [
+			{fieldtype: 'Section Break', label: __('Serial No')},
+			{
+				fieldtype: 'Link', fieldname: 'serial_no_select', options: 'Serial No',
+				label: __('Select'),
+				get_query: function() {
+					return { filters: {item_code: me.item_code}};
+				}
+			},
+			{fieldtype: 'Column Break'},
+			{fieldname: 'serial_no', fieldtype: 'Small Text'}
+		];
+	}
+});
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index ee555f4..1504f71 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -546,8 +546,8 @@
 	}
 });
 
-erpnext.stock.select_batch_and_serial_no = (frm, d = undefined) => {
-	let get_warehouse = (item) => {
+erpnext.stock.select_batch_and_serial_no = (frm, item) => {
+	let get_warehouse_type_and_name = (item) => {
 		let value = '';
 		if(frm.fields_dict.from_warehouse.disp_status === "Write") {
 			value = cstr(item.s_warehouse) || '';
@@ -564,275 +564,20 @@
 		}
 	}
 
-	if(d && d.has_batch_no && !d.batch_no) {
-		erpnext.stock.show_batch_serial_modal(frm, d, d.item_code, d.qty, get_warehouse(d), 1);
-	} else if(d && d.has_serial_no && !d.serial_no && frm.doc.purpose !== 'Material Receipt') {
-		erpnext.stock.show_batch_serial_modal(frm, d, d.item_code, d.qty, get_warehouse(d), 0);
+	let opts = {
+		frm: frm,
+		item: item,
+		warehouse_details: get_warehouse_type_and_name(item),
 	}
+
+	if(item && item.has_batch_no && !item.batch_no) {
+		opts.has_batch = 1;
+	} else if(item && item.has_serial_no && !item.serial_no
+		&& frm.doc.purpose !== 'Material Receipt') {
+
+		opts.has_batch = 0;
+	}
+
+	let serial_no_batch_selector = new erpnext.SerialNoBatchSelector(opts);
+
 }
-
-erpnext.stock.show_batch_serial_modal = (frm, item, item_code, qty, warehouse_details,
-	has_batch, oldest = undefined) => {
-
-	let data = oldest ? oldest : [];
-	let title = "";
-	let fields = [
-		{fieldname: 'item_code', read_only: 1, fieldtype:'Link', options: 'Item',
-			label: __('Item Code'), 'default': item_code},
-		{fieldtype:'Column Break'},
-		{
-			fieldname: 'warehouse',
-			fieldtype:'Link',
-			options: 'Warehouse',
-			label: __(warehouse_details.type),
-			default: warehouse_details.name,
-			onchange: function(e) {
-				warehouse_details.name = this.get_value();
-				var batches = this.layout.fields_dict.batches;
-				if(batches) {
-					batches.grid.df.data = [];
-					batches.grid.refresh();
-					batches.grid.add_new_row(null, null, null);
-				}
-			}
-		},
-		{fieldtype:'Column Break'},
-		{fieldname: 'qty', fieldtype:'Float', label: __(has_batch ? 'Total Qty' : 'Qty'), 'default': qty},
-	];
-
-	if(has_batch) {
-		title = __("Select Batch Numbers");
-		fields = fields.concat([
-			{fieldtype:'Section Break', label: __('Batches')},
-			{fieldname: 'batches', fieldtype: 'Table',
-				fields: [
-					{
-						fieldtype:'Link',
-						fieldname:'batch_no',
-						options: 'Batch',
-						label: __('Select Batch'),
-						in_list_view:1,
-						get_query: function() {
-							return {filters: {item: item_code }};
-						},
-						onchange: function(e) {
-							if(this.get_value().length === 0) {
-								this.grid_row.on_grid_fields_dict
-									.available_qty.set_value(0);
-								return;
-							}
-							if(warehouse_details.name) {
-								frappe.call({
-									method: 'erpnext.stock.doctype.batch.batch.get_batch_qty',
-									args: {
-										batch_no: this.doc.batch_no,
-										warehouse: warehouse_details.name,
-										item_code: item_code
-									},
-									callback: (r) => {
-										this.grid_row.on_grid_fields_dict
-											.available_qty.set_value(r.message || 0);
-									}
-								});
-
-							} else {
-								frappe.throw(__("Please select a warehouse to get available quantities"));
-							}
-							// e.stopImmediatePropagation();
-						}
-					},
-					{
-						fieldtype:'Float',
-						read_only:1,
-						fieldname:'available_qty',
-						label: __('Available'),
-						in_list_view:1,
-						default: 0,
-						onchange: function() {
-							this.grid_row.on_grid_fields_dict.selected_qty.set_value('0');
-						}
-					},
-					{
-						fieldtype:'Float',
-						fieldname:'selected_qty',
-						label: __('Qty'),
-						in_list_view:1,
-						'default': 0,
-						onchange: function(e) {
-							var batch_no = this.grid_row.on_grid_fields_dict.batch_no.get_value();
-							var available_qty = this.grid_row.on_grid_fields_dict.available_qty.get_value();
-							var selected_qty = this.grid_row.on_grid_fields_dict.selected_qty.get_value();
-
-							if(batch_no.length === 0 && parseInt(selected_qty)!==0) {
-								frappe.throw(__("Please select a batch"));
-							}
-							if(warehouse_details.type === 'Source Warehouse' &&
-								parseFloat(available_qty) < parseFloat(selected_qty)) {
-									this.set_value('0');
-									frappe.throw(__(`For transfer from source, selected quantity cannot be
-										greater than available quantity`));
-							} else {
-								this.grid.refresh();
-							}
-						}
-					},
-				],
-				in_place_edit: true,
-				data: data,
-				get_data: function() {
-					return this.data;
-				},
-			}
-		]);
-
-	} else {
-		title = __("Select Serial Numbers");
-		fields = fields.concat([
-			{fieldtype: 'Section Break', label: __('Serial No')},
-			{
-				fieldtype: 'Link', fieldname: 'serial_no_select', options: 'Serial No',
-				label: __('Select'),
-				get_query: function() {
-					return { filters: {item_code: item_code}};
-				}
-			},
-			{fieldtype: 'Column Break'},
-			{fieldname: 'serial_no', fieldtype: 'Small Text'}
-		]);
-	}
-
-	let dialog = new frappe.ui.Dialog({
-		title: title,
-		fields: fields
-	});
-
-	erpnext.stock.bind_batch_serial_dialog_qty(dialog, warehouse_details);
-
-	let map_item_values = (item, values, attribute, qty_field, warehouse) => {
-		item[attribute] = values[attribute];
-		if(warehouse_details.type === 'Source Warehouse') {
-			item.s_warehouse = values.warehouse || warehouse;
-		} else {
-			item.t_warehouse = values.warehouse || warehouse;
-		}
-		item.qty = values[qty_field];
-	}
-
-	let validate_batch_dialog = (values) => {
-		if(values.batches.length === 0 || !values.batches) {
-			frappe.throw(__("Please select batches for batched item " + values.item_code));
-			return false;
-		}
-		values.batches.map((batch, i) => {
-			if(!batch.selected_qty || batch.selected_qty === 0 ) {
-				frappe.throw(__("Please select quantity on row " + (i+1)));
-				return false;
-			}
-		});
-		return true;
-	}
-
-	let set_batched_items = () => {
-		let values = dialog.get_values();
-		if(!validate_batch_dialog(values)) {
-			return;
-		}
-
-		values.batches.map((batch, i) => {
-			if(i === 0) {
-				map_item_values(item, batch, 'batch_no',
-					'selected_qty', values.warehouse);
-			} else {
-				let row = frm.add_child("items");
-				row.item_code = item.item_code;
-				map_item_values(row, batch, 'batch_no',
-					'selected_qty', values.warehouse);
-			}
-		});
-	}
-
-	let validate_serial_no_dialog = (values) => {
-		let serial_nos = values.serial_no || '';
-		if (!serial_nos || !serial_nos.replace(/\s/g, '').length) {
-			frappe.throw(__("Please enter serial numbers for serialized item " + values.item_code));
-			return false;
-		}
-		return true;
-	}
-
-	let set_serialized_items = () => {
-		let values = dialog.get_values();
-		if (!validate_serial_no_dialog(values)) {
-			return;
-		}
-		map_item_values(item, values, 'serial_no', 'qty');
-	}
-
-	dialog.set_primary_action(__('Get Items'), function() {
-		if(!dialog.get_values().warehouse) {
-			frappe.throw(__("Please select a warehouse"));
-		}
-		has_batch ? set_batched_items() : set_serialized_items();
-		refresh_field("items");
-		dialog.hide();
-	})
-	dialog.show();
-}
-
-erpnext.stock.bind_batch_serial_dialog_qty = (dialog, warehouse_details) => {
-	let serial_no_link = dialog.fields_dict.serial_no_select;
-	let serial_no_list_field = dialog.fields_dict.serial_no;
-	let batches_field = dialog.fields_dict.batches;
-
-	let qty_field = dialog.fields_dict.qty;
-	let item_code = dialog.get_value('item_code');
-
-	let update_quantity = (batch) => {
-		if(batch) {
-			let total_qty = 0;
-			batches_field.grid.wrapper.find('input[data-fieldname="selected_qty"]').each(function() {
-				total_qty += Number($(this).val());
-			});
-			qty_field.set_input(total_qty);
-		} else {
-			let serial_numbers = serial_no_list_field.get_value().replace(/\n/g, ' ').match(/\S+/g) || [];
-			qty_field.set_input(serial_numbers.length);
-		}
-	}
-
-	if(serial_no_link) {
-		let serial_list = [];
-		serial_no_link.$input.on('awesomplete-selectcomplete', function() {
-			if(serial_no_link.get_value().length > 0) {
-				let new_no = serial_no_link.get_value();
-				let list_value = serial_no_list_field.get_value();
-				let new_line = '\n';
-				if(!serial_no_list_field.get_value()) {
-					new_line = '';
-				} else {
-					serial_list = list_value.replace(/\s+/g, ' ').split(' ');
-				}
-				if(!serial_list.includes(new_no)) {
-					serial_no_link.set_new_description('');
-					serial_no_list_field.set_value(list_value + new_line + new_no);
-					update_quantity(0);
-				} else {
-					serial_no_link.set_new_description(new_no + ' is already selected.');
-				}
-			}
-			serial_no_link.set_input('');
-			serial_no_link.$input.blur();
-		});
-
-		serial_no_list_field.$input.on('input', function() {
-			serial_list = serial_no_list_field.get_value().replace(/\s+/g, ' ').split(' ');
-			update_quantity(0);
-		});
-	}
-
-	if(batches_field) {
-		batches_field.grid.wrapper.on('change', function() {
-			update_quantity(1);
-		});
-	}
-}
\ No newline at end of file