diff --git a/edge/custom/property_setters/interview.py b/edge/custom/property_setters/interview.py
new file mode 100644
index 0000000..b55f572
--- /dev/null
+++ b/edge/custom/property_setters/interview.py
@@ -0,0 +1,31 @@
+def get_interview_property_setters():
+ '''
+ Edge specific property setters that need to be added to the Interview DocType
+ '''
+ return [
+ {
+ "doctype_or_field": "DocField",
+ "doc_type": "Interview",
+ "field_name": "scheduled_on",
+ "property": "set_only_once",
+ "property_type": "Check",
+ "value":0
+ },
+ {
+ "doctype_or_field": "DocField",
+ "doc_type": "Interview",
+ "field_name": "from_time",
+ "property": "set_only_once",
+ "property_type": "Check",
+ "value":0
+ },
+ {
+ "doctype_or_field": "DocField",
+ "doc_type": "Interview",
+ "field_name": "to_time",
+ "property": "set_only_once",
+ "property_type": "Check",
+ "value":0
+ },
+
+ ]
\ No newline at end of file
diff --git a/edge/edge/doctype/employee_interview_tool/__init__.py b/edge/edge/doctype/employee_interview_tool/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/edge/edge/doctype/employee_interview_tool/employee_interview_tool.js b/edge/edge/doctype/employee_interview_tool/employee_interview_tool.js
new file mode 100644
index 0000000..e3debe4
--- /dev/null
+++ b/edge/edge/doctype/employee_interview_tool/employee_interview_tool.js
@@ -0,0 +1,177 @@
+// Copyright (c) 2025, efeone and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Employee Interview Tool",{
+ onload(frm) {
+ frm.set_value('date', frappe.datetime.get_today());
+ frm.toggle_display('job_applicants', false);
+ !frm.doc.company && frappe.db.get_single_value('Global Defaults','default_company')
+ .then(value => frm.set_value('company',value))
+ },
+ refresh : function (frm){
+ frm.disable_save()
+ frm.set_value('interview_round', '');
+ frm.set_value('scheduled_on', '');
+ frm.set_value('from_time', '');
+ frm.set_value('to_time', '');
+ frm.set_value('department', '');
+ frm.set_value('designation', '');
+ frm.set_value('status', '');
+ frm.set_value('job_opening', '');
+ frm.set_value('location', '');
+ frm.clear_table('job_applicants');
+ frm.refresh_field('job_applicants');
+ frm.toggle_display('job_applicants', false);
+ fetch_job_applicant(frm);
+ toggle_create_interview_button(frm);
+
+ }
+});
+
+/**
+* create a button to fetch job applicants
+* On click, fetches job applicants based on filters and populates the child table.
+*/
+function fetch_job_applicant(frm){
+ let create_applicant_button = frm.add_custom_button("Get Job Applicants",function(){
+ const filters = {};
+ if (frm.doc.job_opening){
+ filters.job_title = frm.doc.job_opening;
+ }
+ if (frm.doc.designation){
+ filters.designation = frm.doc.designation;
+ }
+ if (frm.doc.status){
+ filters.status = frm.doc.status;
+ }
+ frappe.call({
+ method:"edge.edge.doctype.employee_interview_tool.employee_interview_tool.fetch_job_applicants",
+ args:{filters},
+ callback:function(r){
+ if (r.message){
+ frm.clear_table("job_applicants");
+ r.message.forEach(function(applicant){
+ let row = frm.add_child("job_applicants");
+ row.job_applicant = applicant.name;
+ row.applicant_name = applicant.applicant_name;
+ row.status = applicant.status;
+ row.designation = applicant.designation;
+ });
+ frm.refresh_field("job_applicants");
+ frm.toggle_display('job_applicants', true);
+ toggle_create_interview_button(frm);
+ }
+ }
+
+ });
+
+ })
+ create_applicant_button.removeClass('btn-default').addClass('btn-primary');
+}
+
+/**
+* create interview for the selected job applicants
+* On click, creates interviews or prompts for rescheduling if already exists.
+*/
+let create_interview_btn = null;
+function toggle_create_interview_button(frm) {
+ if (create_interview_btn) {
+ create_interview_btn.remove();
+ create_interview_btn = null;
+ }
+ create_interview_btn = frm.add_custom_button('Create Interview', function () {
+ let selected_rows = frm.fields_dict.job_applicants.grid.get_selected_children();
+ if (!selected_rows.length) {
+ frappe.msgprint(__('Please select one or more rows in the Job Applicants table.'));
+ return;
+ }
+ let missing_fields = [];
+ if (!frm.doc.interview_round) missing_fields.push(__('Interview Round'));
+ if (!frm.doc.scheduled_on) missing_fields.push(__('Scheduled On'));
+ if (!frm.doc.from_time) missing_fields.push(__('From Time'));
+ if (!frm.doc.to_time) missing_fields.push(__('To Time'));
+ if (missing_fields.length) {
+ frappe.msgprint({
+ title: __('Missing Required Scheduling Fields'),
+ message: __('Please ensure the following fields are filled:') +
+ '
' + missing_fields.join(', ') + '',
+ indicator: 'orange'
+ });
+ return;
+ }
+ frappe.call({
+ method: 'edge.edge.doctype.employee_interview_tool.employee_interview_tool.create_bulk_interviews',
+ args: {
+ applicants: selected_rows.map(row => ({
+ job_applicant: row.job_applicant,
+ applicant_name: row.applicant_name,
+ designation: row.designation,
+ interview_round: frm.doc.interview_round,
+ scheduled_on: frm.doc.scheduled_on,
+ from_time: frm.doc.from_time,
+ to_time: frm.doc.to_time
+ }))
+ },
+ callback: function (r) {
+ if (!r.exc) {
+ let data = r.message || {};
+ if (Array.isArray(data.created) && data.created.length > 0) {
+ const created_ids = data.created.map(c => c.job_applicant).join(', ');
+ frappe.msgprint(__('Interviews created successfully for: ') + created_ids);
+ }
+ if (Array.isArray(data.skipped_applicants) && data.skipped_applicants.length > 0) {
+ frappe.confirm(
+ __('Interviews already exist for: {0}.
Do you want to reschedule?', [data.skipped_applicants.join(', ')]),
+ () => {
+ frappe.prompt([
+ {
+ label: 'Scheduled On',
+ fieldname: 'scheduled_on',
+ fieldtype: 'Date',
+ default: frm.doc.scheduled_on,
+ reqd: 1
+ },
+ {
+ label: 'From Time',
+ fieldname: 'from_time',
+ fieldtype: 'Time',
+ default: frm.doc.from_time,
+ reqd: 1
+ },
+ {
+ label: 'To Time',
+ fieldname: 'to_time',
+ fieldtype: 'Time',
+ default: frm.doc.to_time,
+ reqd: 1
+ }
+ ], (values) => {
+ frappe.call({
+ method: 'edge.edge.doctype.employee_interview_tool.employee_interview_tool.reschedule_interviews',
+ args: {
+ applicants: data.skipped_applicants,
+ interview_round: frm.doc.interview_round,
+ scheduled_on: values.scheduled_on,
+ from_time: values.from_time,
+ to_time: values.to_time,
+ },
+ callback: function(r) {
+ if (!r.exc) {
+ frappe.msgprint(__('Interview rescheduled successfully.'));
+ frm.refresh();
+ }
+ }
+ });
+ }, __('Reschedule Interviews'));
+ },
+ () => {
+ frappe.msgprint(__('Reschedule cancelled.'));
+ }
+ );
+ }
+ }
+ }
+ });
+ });
+ create_interview_btn.removeClass('btn-default').addClass('btn-primary');
+}
\ No newline at end of file
diff --git a/edge/edge/doctype/employee_interview_tool/employee_interview_tool.json b/edge/edge/doctype/employee_interview_tool/employee_interview_tool.json
new file mode 100644
index 0000000..4f5bdd0
--- /dev/null
+++ b/edge/edge/doctype/employee_interview_tool/employee_interview_tool.json
@@ -0,0 +1,151 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-12-24 11:14:57.257478",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "date",
+ "column_break_kdkc",
+ "company",
+ "interview_schedule_details_section",
+ "interview_round",
+ "scheduled_on",
+ "column_break_kfim",
+ "from_time",
+ "to_time",
+ "get_job_applicants_section",
+ "job_opening",
+ "status",
+ "location",
+ "column_break_qwuf",
+ "designation",
+ "department",
+ "section_break_lsta",
+ "job_applicants"
+ ],
+ "fields": [
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "label": "Date"
+ },
+ {
+ "fieldname": "column_break_kdkc",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "interview_schedule_details_section",
+ "fieldtype": "Section Break",
+ "label": "Interview Schedule Details"
+ },
+ {
+ "fieldname": "interview_round",
+ "fieldtype": "Link",
+ "label": "Interview Round",
+ "options": "Interview Round"
+ },
+ {
+ "fieldname": "scheduled_on",
+ "fieldtype": "Date",
+ "label": "Scheduled On"
+ },
+ {
+ "fieldname": "column_break_kfim",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "from_time",
+ "fieldtype": "Time",
+ "label": "From Time"
+ },
+ {
+ "fieldname": "to_time",
+ "fieldtype": "Time",
+ "label": "To Time"
+ },
+ {
+ "collapsible": 1,
+ "description": "Set filters to fetch Job Applicants",
+ "fieldname": "get_job_applicants_section",
+ "fieldtype": "Section Break",
+ "label": "Get Job Applicants"
+ },
+ {
+ "fieldname": "job_opening",
+ "fieldtype": "Link",
+ "label": "Job Opening",
+ "options": "Job Opening"
+ },
+ {
+ "fieldname": "column_break_qwuf",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation"
+ },
+ {
+ "fieldname": "location",
+ "fieldtype": "Link",
+ "label": "Location",
+ "options": "Location"
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "options": "\nOpen\nReplied\nRejected\nHold\nAccepted"
+ },
+ {
+ "fieldname": "section_break_lsta",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "job_applicants",
+ "fieldtype": "Table",
+ "label": "Job Applicant",
+ "options": "Job Applicant Interview Detail"
+ },
+ {
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2025-12-29 16:03:19.071789",
+ "modified_by": "Administrator",
+ "module": "Edge",
+ "name": "Employee Interview Tool",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/edge/edge/doctype/employee_interview_tool/employee_interview_tool.py b/edge/edge/doctype/employee_interview_tool/employee_interview_tool.py
new file mode 100644
index 0000000..9b9e80f
--- /dev/null
+++ b/edge/edge/doctype/employee_interview_tool/employee_interview_tool.py
@@ -0,0 +1,109 @@
+# Copyright (c) 2025, efeone and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+import json
+from frappe.utils import get_datetime, now_datetime
+from frappe import _
+
+
+class EmployeeInterviewTool(Document):
+ pass
+
+
+
+@frappe.whitelist()
+def fetch_job_applicants(filters):
+ '''
+ Fetch job applicants based on filters like job_title, department, designation, status and job opening.
+ '''
+ if isinstance(filters,str):
+ filters = frappe.parse_json(filters)
+ applicants = frappe.get_all(
+ 'Job Applicant',
+ filters= filters,
+ fields= ['name','applicant_name','designation','status','job_title']
+ )
+ return applicants
+
+
+
+
+@frappe.whitelist()
+def create_bulk_interviews(applicants):
+ '''
+ Creates multiple Interview documents for a list of job applicants, skipping those with existing interviews.
+ '''
+ applicants = json.loads(applicants)
+ created_interviews = []
+ existing_interviews = []
+ for app in applicants:
+ interview_round = app.get('interview_round')
+ scheduled_on = app.get('scheduled_on')
+ from_time = app.get('from_time')
+ to_time = app.get('to_time')
+ scheduled_datetime = get_datetime(f"{scheduled_on} {from_time}")
+ now = now_datetime()
+ if scheduled_datetime < now:
+ frappe.throw(_("Interview date and time cannot be in the past for applicant: {0}").format(app.get('applicant_name')))
+ if not (interview_round and scheduled_on and from_time and to_time):
+ frappe.throw(
+ _("Missing required scheduling fields. Please ensure 'Interview Round', 'Scheduled On', 'From Time', and 'To Time' are all filled.")
+ )
+ interview_round_doc = frappe.get_doc('Interview Round', interview_round)
+ interviewers = interview_round_doc.get('interviewers')
+ if frappe.db.exists('Interview', {
+ 'job_applicant': app.get('job_applicant'),
+ 'interview_round': interview_round
+ }):
+ existing_interviews.append(app.get('job_applicant'))
+ continue
+ interview = frappe.get_doc({
+ 'doctype': 'Interview',
+ 'job_applicant': app.get('job_applicant'),
+ 'applicant_name': app.get('applicant_name'),
+ 'designation': app.get('designation'),
+ 'interview_round': interview_round,
+ 'scheduled_on': scheduled_on,
+ 'from_time': from_time,
+ 'to_time': to_time
+ })
+ for i in interviewers:
+ interviewer_id = getattr(i, 'employee', None) or getattr(i, 'user', None)
+ if interviewer_id:
+ interview.append('interview_details', {
+ 'interviewer': interviewer_id
+ })
+ interview.insert()
+ created_interviews.append({
+ "interview": interview.name,
+ "job_applicant": app.get("job_applicant"),
+ "applicant_name": app.get("applicant_name")
+ })
+ return {
+ "created": created_interviews,
+ "skipped_applicants": existing_interviews
+ }
+
+
+@frappe.whitelist()
+def reschedule_interviews(applicants, interview_round, scheduled_on, from_time, to_time):
+ """
+ Reschedules existing Interview documents for given job applicants and interview round.
+ """
+ if isinstance(applicants, str):
+ applicants = json.loads(applicants)
+ rescheduled = []
+ for app_id in applicants:
+ interviews = frappe.get_all("Interview", filters={
+ "job_applicant": app_id,
+ "interview_round": interview_round
+ })
+ for i in interviews:
+ doc = frappe.get_doc("Interview", i.name)
+ doc.scheduled_on = scheduled_on
+ doc.from_time = from_time
+ doc.to_time = to_time
+ doc.save()
+ rescheduled.append(i.name)
diff --git a/edge/edge/doctype/employee_interview_tool/test_employee_interview_tool.py b/edge/edge/doctype/employee_interview_tool/test_employee_interview_tool.py
new file mode 100644
index 0000000..125c663
--- /dev/null
+++ b/edge/edge/doctype/employee_interview_tool/test_employee_interview_tool.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2025, efeone and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestEmployeeInterviewTool(FrappeTestCase):
+ pass
diff --git a/edge/edge/doctype/job_applicant_interview_detail/__init__.py b/edge/edge/doctype/job_applicant_interview_detail/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/edge/edge/doctype/job_applicant_interview_detail/job_applicant_interview_detail.json b/edge/edge/doctype/job_applicant_interview_detail/job_applicant_interview_detail.json
new file mode 100644
index 0000000..b290572
--- /dev/null
+++ b/edge/edge/doctype/job_applicant_interview_detail/job_applicant_interview_detail.json
@@ -0,0 +1,64 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-12-24 13:23:40.270222",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "column_break_bsjp",
+ "job_applicant",
+ "applicant_name",
+ "designation",
+ "status"
+ ],
+ "fields": [
+ {
+ "fieldname": "job_applicant",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Job Applicant",
+ "options": "Job Applicant"
+ },
+ {
+ "fetch_from": "job_applicant.applicant_name",
+ "fieldname": "applicant_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Applicant Name"
+ },
+ {
+ "fieldname": "column_break_bsjp",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "job_applicant.designation",
+ "fieldname": "designation",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Designation"
+ },
+ {
+ "fetch_from": "job_applicant.status",
+ "fieldname": "status",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Status"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2025-12-24 15:34:21.189315",
+ "modified_by": "Administrator",
+ "module": "Edge",
+ "name": "Job Applicant Interview Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/edge/edge/doctype/job_applicant_interview_detail/job_applicant_interview_detail.py b/edge/edge/doctype/job_applicant_interview_detail/job_applicant_interview_detail.py
new file mode 100644
index 0000000..00336ab
--- /dev/null
+++ b/edge/edge/doctype/job_applicant_interview_detail/job_applicant_interview_detail.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2025, efeone and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class JobApplicantInterviewDetail(Document):
+ pass
diff --git a/edge/hooks.py b/edge/hooks.py
index 2f5062c..ad80ecc 100644
--- a/edge/hooks.py
+++ b/edge/hooks.py
@@ -85,12 +85,13 @@
# ------------
# before_install = "edge.install.before_install"
-# after_install = "edge.install.after_install"
+after_install = "edge.setup.after_install"
+after_migrate = "edge.setup.after_migrate"
# Uninstallation
# ------------
-# before_uninstall = "edge.uninstall.before_uninstall"
+before_uninstall = "edge.setup.before_uninstall"
# after_uninstall = "edge.uninstall.after_uninstall"
# Integration Setup
diff --git a/edge/setup.py b/edge/setup.py
new file mode 100644
index 0000000..d175017
--- /dev/null
+++ b/edge/setup.py
@@ -0,0 +1,54 @@
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+import frappe
+
+
+#Custom Property Setter imports for Edge
+from edge.custom.property_setters.interview import get_interview_property_setters
+
+def after_install():
+ #Creating edge specific Property Setters
+ create_property_setters(get_property_setters())
+
+
+def after_migrate():
+ after_install()
+
+
+
+def delete_custom_fields(custom_fields: dict):
+ '''
+ Method to Delete custom fields
+ args:
+ custom_fields: a dict like `{'Task': [{fieldname: 'your_fieldname', ...}]}`
+ '''
+ for doctype, fields in custom_fields.items():
+ frappe.db.delete(
+ "Custom Field",
+ {
+ "fieldname": ("in", [field["fieldname"] for field in fields]),
+ "dt": doctype,
+ },
+ )
+ frappe.clear_cache(doctype=doctype)
+
+
+def create_property_setters(property_setter_datas):
+ '''
+ Method to create custom property setters
+ args:
+ property_setter_datas : list of dict of property setter obj
+ '''
+ for property_setter_data in property_setter_datas:
+ if frappe.db.exists("Property Setter", property_setter_data):
+ continue
+ property_setter = frappe.new_doc("Property Setter")
+ property_setter.update(property_setter_data)
+ property_setter.flags.ignore_permissions = True
+ property_setter.insert()
+
+def get_property_setters():
+ '''
+ PW IOI specific property setters that need to be added to the Standard DocTypes
+ '''
+ property_setters = (get_interview_property_setters())
+ return property_setters
\ No newline at end of file