feat: bulk questionnaire sending
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.js b/erpnext/hr/doctype/exit_interview/exit_interview.js
index 31c9610..849e854 100644
--- a/erpnext/hr/doctype/exit_interview/exit_interview.js
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.js
@@ -23,29 +23,15 @@
 	},
 
 	send_exit_questionnaire: function(frm) {
-		frappe.db.get_value('HR Settings', 'HR Settings',
-			['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], (r) => {
-			if (!r.exit_questionnaire_web_form || !r.exit_questionnaire_notification_template) {
-				frappe.throw({
-					message: __('Please set {0} and {1} in {2}.',
-						['Exit Questionnaire Web Form'.bold(),
-						'Notification Template'.bold(),
-						'<a href="/app/hr-settings" target="_blank">HR Settings</a>']
-					),
-					title: __('Settings Missing')
-				});
-			} else {
-				frappe.call({
-					method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
-					args: {
-						'exit_interview': frm.doc.name
-					},
-					callback: function(r) {
-						if (!r.exc) {
-							frm.refresh_field('questionnaire_email_sent');
-						}
-					}
-				});
+		frappe.call({
+			method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
+			args: {
+				'interviews': [frm.doc]
+			},
+			callback: function(r) {
+				if (!r.exc) {
+					frm.refresh_field('questionnaire_email_sent');
+				}
 			}
 		});
 	}
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.json b/erpnext/hr/doctype/exit_interview/exit_interview.json
index d4d4aac..4b39640 100644
--- a/erpnext/hr/doctype/exit_interview/exit_interview.json
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.json
@@ -87,8 +87,7 @@
    "in_list_view": 1,
    "in_standard_filter": 1,
    "label": "Date",
-   "mandatory_depends_on": "eval:doc.status==='Scheduled';",
-   "reqd": 1
+   "mandatory_depends_on": "eval:doc.status==='Scheduled';"
   },
   {
    "fieldname": "exit_questionnaire_section",
@@ -174,6 +173,7 @@
    "fieldtype": "Check",
    "in_standard_filter": 1,
    "label": "Questionnaire Email Sent",
+   "no_copy": 1,
    "read_only": 1
   },
   {
@@ -218,7 +218,7 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-12-05 17:49:44.839277",
+ "modified": "2021-12-05 18:56:34.856854",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Exit Interview",
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview.py b/erpnext/hr/doctype/exit_interview/exit_interview.py
index 878380b..b2ada4d 100644
--- a/erpnext/hr/doctype/exit_interview/exit_interview.py
+++ b/erpnext/hr/doctype/exit_interview/exit_interview.py
@@ -38,28 +38,76 @@
 
 
 @frappe.whitelist()
-def send_exit_questionnaire(exit_interview):
-	exit_interview = frappe.get_doc('Exit Interview', exit_interview)
-	context = exit_interview.as_dict()
+def send_exit_questionnaire(interviews):
+	interviews = get_interviews(interviews)
+	validate_questionnaire_settings()
 
-	employee = frappe.get_doc('Employee', exit_interview.employee)
-	context.update(employee.as_dict())
+	email_success = []
+	email_failure = []
 
-	email = get_employee_email(employee)
-	template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template')
-	template = frappe.get_doc('Email Template', template_name)
+	for exit_interview in interviews:
+		interview = frappe.get_doc('Exit Interview', exit_interview.get('name'))
+		if interview.get('questionnaire_email_sent'):
+			continue
 
-	if email:
-		frappe.sendmail(
-			recipients=email,
-			subject=template.subject,
-			message=frappe.render_template(template.response, context),
-			reference_doctype=exit_interview.doctype,
-			reference_name=exit_interview.name
+		employee = frappe.get_doc('Employee', interview.employee)
+		email = get_employee_email(employee)
+
+		context = interview.as_dict()
+		context.update(employee.as_dict())
+		template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template')
+		template = frappe.get_doc('Email Template', template_name)
+
+		if email:
+			frappe.sendmail(
+				recipients=email,
+				subject=template.subject,
+				message=frappe.render_template(template.response, context),
+				reference_doctype=interview.doctype,
+				reference_name=interview.name
+			)
+			interview.db_set('questionnaire_email_sent', True)
+			interview.notify_update()
+			email_success.append(email)
+		else:
+			email_failure.append(get_link_to_form('Employee', employee.name))
+
+	show_email_summary(email_success, email_failure)
+
+
+def get_interviews(interviews):
+	import json
+
+	if isinstance(interviews, str):
+		interviews = json.loads(interviews)
+
+	if not len(interviews):
+		frappe.throw(_('Atleast one interview has to be selected.'))
+
+	return interviews
+
+
+def validate_questionnaire_settings():
+	settings = frappe.db.get_value('HR Settings', 'HR Settings',
+		['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], as_dict=True)
+
+	if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template:
+		frappe.throw(
+			message=_('Please set {0} and {1} in {2}.').format(
+				frappe.bold('Exit Questionnaire Web Form'),
+				frappe.bold('Notification Template'),
+				get_link_to_form('HR Settings', 'HR Settings')),
+			title=_('Settings Missing')
 		)
-		frappe.msgprint(_('Exit Questionnaire sent to {0}').format(email),
-			title='Success', indicator='green')
-		exit_interview.db_set('questionnaire_email_sent', True)
-		exit_interview.notify_update()
-	else:
-		frappe.msgprint(_('Email IDs for employee not found.'))
\ No newline at end of file
+
+
+def show_email_summary(email_success, email_failure):
+	message = ''
+	if email_success:
+		message += _('{0}: {1}').format(
+			frappe.bold('Sent Successfully'), ', '.join(email_success))
+	if email_failure:
+		message += + '<br><br>' + _('{0} due to missing email information for employee(s): {1}').format(
+			frappe.bold('Sending Failed'), ', '.join(email_failure))
+
+	frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)
\ No newline at end of file
diff --git a/erpnext/hr/doctype/exit_interview/exit_interview_list.js b/erpnext/hr/doctype/exit_interview/exit_interview_list.js
new file mode 100644
index 0000000..93d7b21
--- /dev/null
+++ b/erpnext/hr/doctype/exit_interview/exit_interview_list.js
@@ -0,0 +1,27 @@
+frappe.listview_settings['Exit Interview'] = {
+	has_indicator_for_draft: 1,
+	get_indicator: function(doc) {
+		let status_color = {
+			'Pending': 'orange',
+			'Scheduled': 'yellow',
+			'Completed': 'green',
+			'Cancelled': 'red',
+		};
+		return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
+	},
+
+	onload: function(listview) {
+		if (frappe.boot.user.can_write.includes('Exit Interview')) {
+			listview.page.add_action_item(__('Send Exit Questionnaires'), function() {
+				const interviews = listview.get_checked_items();
+				frappe.call({
+					method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
+					freeze: true,
+					args: {
+						'interviews': interviews
+					}
+				});
+			});
+		}
+	}
+};