From ca9d4e193a25818f414a62bd5fa99acab14cee14 Mon Sep 17 00:00:00 2001 From: iamkhanraheel Date: Mon, 13 Apr 2026 19:29:58 +0530 Subject: [PATCH 01/29] fix: handle zero-value additional salary components to avoid invalid salary slip rows --- hrms/payroll/doctype/salary_slip/salary_slip.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hrms/payroll/doctype/salary_slip/salary_slip.py b/hrms/payroll/doctype/salary_slip/salary_slip.py index bf74651ea8..ba67198767 100644 --- a/hrms/payroll/doctype/salary_slip/salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/salary_slip.py @@ -1627,6 +1627,11 @@ def add_additional_salary_components(self, component_type): for additional_salary in additional_salaries: component_data = get_salary_component_data(additional_salary.component) + remove_if_zero_valued = frappe.get_cached_value( + "Salary Component", additional_salary.component, "remove_if_zero_valued" + ) + if flt(additional_salary.amount) == 0 and remove_if_zero_valued: + continue self.update_component_row( component_data, additional_salary.amount, @@ -1827,7 +1832,7 @@ def update_component_row( ): component_row.set(attr, component_data.get(attr)) - if additional_salary and amount: + if additional_salary: if additional_salary.overwrite: component_row.additional_amount = flt( flt(amount) - flt(component_row.get("default_amount", 0)), From eb8d2985836540c9fc78bd2393135b40c7eafa12 Mon Sep 17 00:00:00 2001 From: iamkhanraheel Date: Mon, 13 Apr 2026 20:34:20 +0530 Subject: [PATCH 02/29] test: disable remove_if_zero_valued check in salary component to add zero valued component in salary slip --- hrms/payroll/doctype/salary_slip/test_salary_slip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hrms/payroll/doctype/salary_slip/test_salary_slip.py b/hrms/payroll/doctype/salary_slip/test_salary_slip.py index 2c5341015d..01f09dacf2 100644 --- a/hrms/payroll/doctype/salary_slip/test_salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/test_salary_slip.py @@ -1932,6 +1932,7 @@ def test_salary_component_for_additional_salary_zero(self): "type": "Earning", "is_income_tax_component": 0, "amount": 350, + "remove_if_zero_valued": 0, }, ] make_salary_component(data, False, company_list=["_Test Company"]) From a8b15d35b8cb5dc0ed49f98104eef762cd201af0 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 16 Feb 2026 11:28:12 +0530 Subject: [PATCH 03/29] fix(Recruitment): better sidebar --- hrms/workspace_sidebar/recruitment.json | 102 ++++++++++++++++-------- 1 file changed, 67 insertions(+), 35 deletions(-) diff --git a/hrms/workspace_sidebar/recruitment.json b/hrms/workspace_sidebar/recruitment.json index bed0cad6ce..945cc3a3f0 100644 --- a/hrms/workspace_sidebar/recruitment.json +++ b/hrms/workspace_sidebar/recruitment.json @@ -30,6 +30,18 @@ "show_arrow": 0, "type": "Link" }, + { + "child": 0, + "collapsible": 1, + "icon": "user-round-plus", + "indent": 0, + "keep_closed": 0, + "label": "Job Opening", + "link_to": "Job Opening", + "link_type": "DocType", + "show_arrow": 0, + "type": "Link" + }, { "child": 0, "collapsible": 1, @@ -43,7 +55,7 @@ "type": "Link" }, { - "child": 1, + "child": 0, "collapsible": 1, "icon": "videotape", "indent": 0, @@ -55,7 +67,7 @@ "type": "Link" }, { - "child": 1, + "child": 0, "collapsible": 1, "icon": "user-round-check", "indent": 0, @@ -67,9 +79,9 @@ "type": "Link" }, { - "child": 1, + "child": 0, "collapsible": 1, - "icon": "file", + "icon": "file-text", "indent": 0, "keep_closed": 0, "label": "Appointment Letter", @@ -81,10 +93,10 @@ { "child": 0, "collapsible": 1, - "icon": "notepad-text", + "icon": "align-start-horizontal", "indent": 1, "keep_closed": 1, - "label": "Reports", + "label": "Planning", "link_type": "DocType", "show_arrow": 0, "type": "Section Break" @@ -95,22 +107,11 @@ "icon": "", "indent": 0, "keep_closed": 0, - "label": "Recruitment Analytics", - "link_to": "Recruitment Analytics", - "link_type": "Report", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 0, - "collapsible": 1, - "icon": "database", - "indent": 1, - "keep_closed": 1, - "label": "Setup", + "label": "Job Requisition", + "link_to": "Job Requisition", "link_type": "DocType", "show_arrow": 0, - "type": "Section Break" + "type": "Link" }, { "child": 1, @@ -127,26 +128,24 @@ { "child": 1, "collapsible": 1, - "icon": "", "indent": 0, "keep_closed": 0, - "label": "Job Requisition", - "link_to": "Job Requisition", + "label": "Employee Referral", + "link_to": "Employee Referral", "link_type": "DocType", "show_arrow": 0, "type": "Link" }, { - "child": 1, + "child": 0, "collapsible": 1, - "icon": "", - "indent": 0, - "keep_closed": 0, - "label": "Job Opening", - "link_to": "Job Opening", + "icon": "notepad-text", + "indent": 1, + "keep_closed": 1, + "label": "Reports", "link_type": "DocType", "show_arrow": 0, - "type": "Link" + "type": "Section Break" }, { "child": 1, @@ -154,12 +153,23 @@ "icon": "", "indent": 0, "keep_closed": 0, - "label": "Interview Type", - "link_to": "Interview Type", - "link_type": "DocType", + "label": "Recruitment Analytics", + "link_to": "Recruitment Analytics", + "link_type": "Report", "show_arrow": 0, "type": "Link" }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "show_arrow": 0, + "type": "Section Break" + }, { "child": 1, "collapsible": 1, @@ -184,6 +194,29 @@ "show_arrow": 0, "type": "Link" }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Job Offer Term Template", + "link_to": "Job Offer Term Template", + "link_type": "DocType", + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "web", + "indent": 0, + "keep_closed": 0, + "label": "Job Portal", + "link_type": "URL", + "show_arrow": 0, + "type": "Link", + "url": "/jobs" + }, { "child": 0, "collapsible": 1, @@ -193,12 +226,11 @@ "label": "Settings", "link_to": "HR Settings", "link_type": "DocType", - "navigate_to_tab": "recruitment_tab", "show_arrow": 0, "type": "Link" } ], - "modified": "2026-01-08 14:16:38.419516", + "modified": "2026-02-16 11:26:04.338308", "modified_by": "Administrator", "module": "HR", "name": "Recruitment", From cf0b0b41bb438b7c7ffd714484311385842c155d Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Thu, 19 Feb 2026 16:50:48 +0530 Subject: [PATCH 04/29] fix(Recruitment)!: rename and merge interview round with interview type --- hrms/hr/doctype/interview/interview.js | 10 +- hrms/hr/doctype/interview/interview.json | 28 ++--- hrms/hr/doctype/interview/interview.py | 20 +-- hrms/hr/doctype/interview/test_interview.py | 39 ++---- .../interview_feedback/interview_feedback.js | 4 +- .../interview_feedback.json | 33 ++--- .../test_interview_feedback.py | 8 +- hrms/hr/doctype/interview_round/__init__.py | 0 .../interview_round/interview_round.js | 38 ------ .../interview_round/interview_round.json | 118 ------------------ .../interview_round/interview_round.py | 47 ------- .../interview_round/test_interview_round.py | 10 -- .../doctype/interview_type/interview_type.js | 35 +++++- .../interview_type/interview_type.json | 73 +++++++++-- .../doctype/interview_type/interview_type.py | 22 +++- .../interview_type/test_interview_type.py | 2 + .../hr/doctype/job_applicant/job_applicant.js | 10 +- .../hr/doctype/job_applicant/job_applicant.py | 14 +-- .../job_applicant_dashboard.html | 4 +- .../hr/workspace/recruitment/recruitment.json | 95 +++++++------- hrms/patches.txt | 1 + ...rge_interview_round_with_interview_type.py | 13 ++ hrms/workspace_sidebar/recruitment.json | 6 +- 23 files changed, 255 insertions(+), 375 deletions(-) delete mode 100644 hrms/hr/doctype/interview_round/__init__.py delete mode 100644 hrms/hr/doctype/interview_round/interview_round.js delete mode 100644 hrms/hr/doctype/interview_round/interview_round.json delete mode 100644 hrms/hr/doctype/interview_round/interview_round.py delete mode 100644 hrms/hr/doctype/interview_round/test_interview_round.py create mode 100644 hrms/patches/v16_0/merge_interview_round_with_interview_type.py diff --git a/hrms/hr/doctype/interview/interview.js b/hrms/hr/doctype/interview/interview.js index 4df982e8c8..705ce75bca 100644 --- a/hrms/hr/doctype/interview/interview.js +++ b/hrms/hr/doctype/interview/interview.js @@ -72,7 +72,7 @@ frappe.ui.form.on("Interview", { frappe.call({ method: "hrms.hr.doctype.interview.interview.get_expected_skill_set", args: { - interview_round: frm.doc.interview_round, + interview_type: frm.doc.interview_type, }, callback: function (r) { frm.events.show_feedback_dialog(frm, r.message); @@ -198,15 +198,15 @@ frappe.ui.form.on("Interview", { }); }, - interview_round: function (frm) { + interview_type: function (frm) { frm.set_value("job_applicant", ""); frm.trigger("set_applicable_interviewers"); }, job_applicant: function (frm) { - if (!frm.doc.interview_round) { + if (!frm.doc.interview_type) { frm.set_value("job_applicant", ""); - frappe.throw(__("Select Interview Round First")); + frappe.throw(__("Select Interview Type First")); } if (frm.doc.job_applicant && !frm.doc.designation) { @@ -218,7 +218,7 @@ frappe.ui.form.on("Interview", { frappe.call({ method: "hrms.hr.doctype.interview.interview.get_interviewers", args: { - interview_round: frm.doc.interview_round || "", + interview_type: frm.doc.interview_type || "", }, callback: function (r) { frm.clear_table("interview_details"); diff --git a/hrms/hr/doctype/interview/interview.json b/hrms/hr/doctype/interview/interview.json index 0ed0e06b91..60e7caad44 100644 --- a/hrms/hr/doctype/interview/interview.json +++ b/hrms/hr/doctype/interview/interview.json @@ -7,7 +7,7 @@ "engine": "InnoDB", "field_order": [ "interview_details_section", - "interview_round", + "interview_type", "job_applicant", "job_opening", "designation", @@ -48,15 +48,6 @@ "options": "Job Opening", "read_only": 1 }, - { - "fieldname": "interview_round", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Interview Round", - "options": "Interview Round", - "reqd": 1 - }, { "default": "Pending", "fieldname": "status", @@ -102,7 +93,7 @@ "label": "Details" }, { - "fetch_from": "interview_round.expected_average_rating", + "fetch_from": "interview_type.expected_average_rating", "fieldname": "expected_average_rating", "fieldtype": "Rating", "label": "Expected Average Rating", @@ -119,7 +110,7 @@ "fieldtype": "Column Break" }, { - "fetch_from": "interview_round.designation", + "fetch_from": "interview_type.designation", "fieldname": "designation", "fieldtype": "Link", "in_list_view": 1, @@ -189,6 +180,15 @@ "fieldtype": "Table", "label": "Interviewers", "options": "Interview Detail" + }, + { + "fieldname": "interview_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Interview Type", + "options": "Interview Type", + "reqd": 1 } ], "index_web_pages_for_search": 1, @@ -199,11 +199,11 @@ "link_fieldname": "interview" } ], - "modified": "2025-11-21 17:24:02.676395", + "modified": "2026-02-19 15:40:38.547112", "modified_by": "Administrator", "module": "HR", "name": "Interview", - "naming_rule": "Expression (old style)", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { diff --git a/hrms/hr/doctype/interview/interview.py b/hrms/hr/doctype/interview/interview.py index 98490da12a..aca286eb34 100644 --- a/hrms/hr/doctype/interview/interview.py +++ b/hrms/hr/doctype/interview/interview.py @@ -58,13 +58,13 @@ def on_submit(self): def validate_duplicate_interview(self): duplicate_interview = frappe.db.exists( "Interview", - {"job_applicant": self.job_applicant, "interview_round": self.interview_round, "docstatus": 1}, + {"job_applicant": self.job_applicant, "interview_type": self.interview_type, "docstatus": 1}, ) if duplicate_interview: frappe.throw( _( - "Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}" + "Job Applicants are not allowed to appear twice for the same Interview Type. Interview {0} already scheduled for Job Applicant {1}" ).format( frappe.bold(get_link_to_form("Interview", duplicate_interview)), frappe.bold(self.job_applicant), @@ -77,8 +77,8 @@ def validate_designation(self): if self.designation != applicant_designation: frappe.throw( _( - "Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}" - ).format(self.interview_round, frappe.bold(self.designation), applicant_designation), + "Interview Type {0} is only for Designation {1}. Job Applicant has applied for the role {2}" + ).format(self.interview_type, frappe.bold(self.designation), applicant_designation), exc=DuplicateInterviewRoundError, ) else: @@ -155,8 +155,8 @@ def on_discard(self): @frappe.whitelist() -def get_interviewers(interview_round: str) -> list[str]: - return frappe.get_all("Interviewer", filters={"parent": interview_round}, fields=["user as interviewer"]) +def get_interviewers(interview_type: str) -> list[str]: + return frappe.get_all("Interviewer", filters={"parent": interview_type}, fields=["user as interviewer"]) def get_recipients(name, for_feedback=0): @@ -337,9 +337,9 @@ def send_daily_feedback_reminder(): @frappe.whitelist() -def get_expected_skill_set(interview_round: str) -> list[dict]: +def get_expected_skill_set(interview_type: str) -> list[dict]: return frappe.get_all( - "Expected Skill Set", filters={"parent": interview_round}, fields=["skill"], order_by="idx" + "Expected Skill Set", filters={"parent": interview_type}, fields=["skill"], order_by="idx" ) @@ -424,7 +424,7 @@ def get_events(start: str, end: str, filters: str | None = None): interviews = frappe.db.sql( f""" SELECT DISTINCT - `tabInterview`.name, `tabInterview`.job_applicant, `tabInterview`.interview_round, + `tabInterview`.name, `tabInterview`.job_applicant, `tabInterview`.interview_type, `tabInterview`.scheduled_on, `tabInterview`.status, `tabInterview`.from_time as from_time, `tabInterview`.to_time as to_time from @@ -441,7 +441,7 @@ def get_events(start: str, end: str, filters: str | None = None): for d in interviews: subject_data = [] - for field in ["name", "job_applicant", "interview_round"]: + for field in ["name", "job_applicant", "interview_type"]: if not d.get(field): continue subject_data.append(d.get(field)) diff --git a/hrms/hr/doctype/interview/test_interview.py b/hrms/hr/doctype/interview/test_interview.py index 39c78ae58a..ce307d425b 100644 --- a/hrms/hr/doctype/interview/test_interview.py +++ b/hrms/hr/doctype/interview/test_interview.py @@ -111,7 +111,7 @@ def test_get_interview_details_for_applicant_dashboard(self): details.get("interviews").get(interview.name), { "name": interview.name, - "interview_round": interview.interview_round, + "interview_type": interview.interview_type, "scheduled_on": interview.scheduled_on, "average_rating": interview.average_rating * 5, "status": "Pending", @@ -221,7 +221,7 @@ def create_interview_and_dependencies( create_user("test_interviewer1@example.com", "Interviewer") create_user("test_interviewer2@example.com", "Interviewer") - interview_round = create_interview_round( + interview_type = create_interview_type( "Technical Round", ["Python", "JS"], ["test_interviewer1@example.com", "test_interviewer2@example.com"], @@ -230,7 +230,7 @@ def create_interview_and_dependencies( ) interview = frappe.new_doc("Interview") - interview.interview_round = interview_round.name + interview.interview_type = interview_type.name interview.job_applicant = job_applicant interview.scheduled_on = scheduled_on or getdate() interview.from_time = from_time or nowtime() @@ -247,26 +247,25 @@ def create_interview_and_dependencies( return interview -def create_interview_round(name, skill_set, interviewers=None, designation=None, save=True): +def create_interview_type(name, skill_set, interviewers=None, designation=None, save=True): create_skill_set(skill_set) - interview_round = frappe.new_doc("Interview Round") - interview_round.round_name = name - interview_round.interview_type = create_interview_type() + interview_type = frappe.new_doc("Interview Type") + interview_type.interview_type_name = name + interview_type.description = "_Test_Description" # average rating = 4 - interview_round.expected_average_rating = 0.8 + interview_type.expected_average_rating = 0.8 if designation: - interview_round.designation = designation + interview_type.designation = designation for skill in skill_set: - interview_round.append("expected_skill_set", {"skill": skill}) + interview_type.append("expected_skill_set", {"skill": skill}) for interviewer in interviewers: - interview_round.append("interviewers", {"user": interviewer}) + interview_type.append("interviewers", {"user": interviewer}) if save: - interview_round.save() - - return interview_round + interview_type.save() + return interview_type def create_skill_set(skill_set): @@ -277,18 +276,6 @@ def create_skill_set(skill_set): doc.save() -def create_interview_type(name="test_interview_type"): - if frappe.db.exists("Interview Type", name): - return frappe.get_doc("Interview Type", name).name - else: - doc = frappe.new_doc("Interview Type") - doc.name = name - doc.description = "_Test_Description" - doc.save() - - return doc.name - - def setup_reminder_settings(): if not frappe.db.exists("Email Template", _("Interview Reminder")): base_path = frappe.get_app_path("erpnext", "hr", "doctype") diff --git a/hrms/hr/doctype/interview_feedback/interview_feedback.js b/hrms/hr/doctype/interview_feedback/interview_feedback.js index 0487db1889..42382b38f9 100644 --- a/hrms/hr/doctype/interview_feedback/interview_feedback.js +++ b/hrms/hr/doctype/interview_feedback/interview_feedback.js @@ -14,11 +14,11 @@ frappe.ui.form.on("Interview Feedback", { }); }, - interview_round: function (frm) { + interview_type: function (frm) { frappe.call({ method: "hrms.hr.doctype.interview.interview.get_expected_skill_set", args: { - interview_round: frm.doc.interview_round, + interview_type: frm.doc.interview_type, }, callback: function (r) { frm.set_value("skill_assessment", r.message); diff --git a/hrms/hr/doctype/interview_feedback/interview_feedback.json b/hrms/hr/doctype/interview_feedback/interview_feedback.json index 508af6aec2..f756cd8249 100644 --- a/hrms/hr/doctype/interview_feedback/interview_feedback.json +++ b/hrms/hr/doctype/interview_feedback/interview_feedback.json @@ -8,7 +8,7 @@ "field_order": [ "details_section", "interview", - "interview_round", + "interview_type", "job_applicant", "column_break_3", "interviewer", @@ -31,18 +31,6 @@ "options": "Interview", "reqd": 1 }, - { - "allow_in_quick_entry": 1, - "fetch_from": "interview.interview_round", - "fieldname": "interview_round", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Interview Round", - "options": "Interview Round", - "read_only": 1, - "reqd": 1 - }, { "allow_in_quick_entry": 1, "fieldname": "interviewer", @@ -118,16 +106,28 @@ "label": "Job Applicant", "options": "Job Applicant", "read_only": 1 + }, + { + "allow_in_quick_entry": 1, + "fetch_from": "interview.interview_type", + "fieldname": "interview_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Interview Type", + "options": "Interview Type", + "read_only": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-03-27 13:09:51.973613", + "modified": "2026-02-19 16:31:33.463199", "modified_by": "Administrator", "module": "HR", "name": "Interview Feedback", - "naming_rule": "Expression (old style)", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { @@ -164,9 +164,10 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "title_field": "interviewer", "track_changes": 1 -} \ No newline at end of file +} diff --git a/hrms/hr/doctype/interview_feedback/test_interview_feedback.py b/hrms/hr/doctype/interview_feedback/test_interview_feedback.py index c0847dc5b2..3b62164718 100644 --- a/hrms/hr/doctype/interview_feedback/test_interview_feedback.py +++ b/hrms/hr/doctype/interview_feedback/test_interview_feedback.py @@ -19,7 +19,7 @@ def test_validation_for_skill_set(self): interview = create_interview_and_dependencies( job_applicant.name, scheduled_on=add_days(getdate(), -1) ) - skill_ratings = get_skills_rating(interview.interview_round) + skill_ratings = get_skills_rating(interview.interview_type) interviewer = "test_interviewer1@example.com" create_skill_set(["Leadership"]) @@ -37,7 +37,7 @@ def test_average_ratings_on_feedback_submission_and_cancellation(self): interview = create_interview_and_dependencies( job_applicant.name, scheduled_on=add_days(getdate(), -1) ) - skill_ratings = get_skills_rating(interview.interview_round) + skill_ratings = get_skills_rating(interview.interview_type) # For First Interviewer Feedback interviewer = "test_interviewer1@example.com" @@ -85,10 +85,10 @@ def create_interview_feedback(interview, interviewer, skills_ratings): return interview_feedback -def get_skills_rating(interview_round): +def get_skills_rating(interview_type): import random - skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields=["skill"]) + skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_type}, fields=["skill"]) for d in skills: d["rating"] = random.random() return skills diff --git a/hrms/hr/doctype/interview_round/__init__.py b/hrms/hr/doctype/interview_round/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hrms/hr/doctype/interview_round/interview_round.js b/hrms/hr/doctype/interview_round/interview_round.js deleted file mode 100644 index c6edcae1d4..0000000000 --- a/hrms/hr/doctype/interview_round/interview_round.js +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("Interview Round", { - refresh: function (frm) { - if (!frm.doc.__islocal) { - frm.add_custom_button(__("Create Interview"), function () { - frm.events.create_interview(frm); - }); - } - }, - designation: function (frm) { - if (frm.doc.designation) { - frappe.db.get_doc("Designation", frm.doc.designation).then((designation) => { - frappe.model.clear_table(frm.doc, "expected_skill_set"); - - designation.skills.forEach((designation_skill) => { - const row = frm.add_child("expected_skill_set"); - row.skill = designation_skill.skill; - }); - - refresh_field("expected_skill_set"); - }); - } - }, - create_interview: function (frm) { - frappe.call({ - method: "hrms.hr.doctype.interview_round.interview_round.create_interview", - args: { - interview_round: frm.doc.name, - }, - callback: function (r) { - var doclist = frappe.model.sync(r.message); - frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - }, - }); - }, -}); diff --git a/hrms/hr/doctype/interview_round/interview_round.json b/hrms/hr/doctype/interview_round/interview_round.json deleted file mode 100644 index 9ae7e73b92..0000000000 --- a/hrms/hr/doctype/interview_round/interview_round.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "field:round_name", - "creation": "2021-04-12 12:57:19.902866", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "round_name", - "interview_type", - "interviewers", - "column_break_3", - "expected_average_rating", - "expected_skills_section", - "designation", - "expected_skill_set" - ], - "fields": [ - { - "fieldname": "round_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Round Name", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "designation", - "fieldtype": "Link", - "label": "Designation", - "options": "Designation" - }, - { - "fieldname": "expected_skills_section", - "fieldtype": "Section Break" - }, - { - "fieldname": "expected_skill_set", - "fieldtype": "Table", - "label": "Expected Skillset", - "options": "Expected Skill Set", - "reqd": 1 - }, - { - "fieldname": "expected_average_rating", - "fieldtype": "Rating", - "label": "Expected Average Rating" - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fieldname": "interview_type", - "fieldtype": "Link", - "label": "Interview Type", - "options": "Interview Type" - }, - { - "fieldname": "interviewers", - "fieldtype": "Table MultiSelect", - "label": "Interviewers", - "options": "Interviewer" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2024-05-01 11:57:32.754037", - "modified_by": "Administrator", - "module": "HR", - "name": "Interview Round", - "naming_rule": "By fieldname", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Interviewer", - "select": 1, - "share": 1, - "write": 1 - } - ], - "sort_field": "creation", - "sort_order": "DESC", - "states": [], - "track_changes": 1 -} \ No newline at end of file diff --git a/hrms/hr/doctype/interview_round/interview_round.py b/hrms/hr/doctype/interview_round/interview_round.py deleted file mode 100644 index 8cc64847e1..0000000000 --- a/hrms/hr/doctype/interview_round/interview_round.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import json - -import frappe -from frappe.model.document import Document - - -class InterviewRound(Document): - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from hrms.hr.doctype.expected_skill_set.expected_skill_set import ExpectedSkillSet - from hrms.hr.doctype.interviewer.interviewer import Interviewer - - designation: DF.Link | None - expected_average_rating: DF.Rating - expected_skill_set: DF.Table[ExpectedSkillSet] - interview_type: DF.Link | None - interviewers: DF.TableMultiSelect[Interviewer] - round_name: DF.Data - # end: auto-generated types - - pass - - -@frappe.whitelist() -def create_interview(interview_round: str) -> Document: - doc = frappe.get_doc("Interview Round", interview_round) - - interview = frappe.new_doc("Interview") - interview.interview_round = doc.name - interview.designation = doc.designation - - if doc.interviewers: - interview.interview_details = [] - for d in doc.interviewers: - interview.append("interview_details", {"interviewer": d.user}) - - return interview diff --git a/hrms/hr/doctype/interview_round/test_interview_round.py b/hrms/hr/doctype/interview_round/test_interview_round.py deleted file mode 100644 index d887042988..0000000000 --- a/hrms/hr/doctype/interview_round/test_interview_round.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -from hrms.tests.utils import HRMSTestSuite - -# import frappe - - -class TestInterviewRound(HRMSTestSuite): - pass diff --git a/hrms/hr/doctype/interview_type/interview_type.js b/hrms/hr/doctype/interview_type/interview_type.js index 9f74c8dfee..7732faffa4 100644 --- a/hrms/hr/doctype/interview_type/interview_type.js +++ b/hrms/hr/doctype/interview_type/interview_type.js @@ -2,6 +2,37 @@ // For license information, please see license.txt frappe.ui.form.on("Interview Type", { - // refresh: function(frm) { - // } + refresh: function (frm) { + if (!frm.doc.__islocal) { + frm.add_custom_button(__("Create Interview"), function () { + frm.events.create_interview(frm); + }); + } + }, + designation: function (frm) { + if (frm.doc.designation) { + frappe.db.get_doc("Designation", frm.doc.designation).then((designation) => { + frappe.model.clear_table(frm.doc, "expected_skill_set"); + + designation.skills.forEach((designation_skill) => { + const row = frm.add_child("expected_skill_set"); + row.skill = designation_skill.skill; + }); + + refresh_field("expected_skill_set"); + }); + } + }, + create_interview: function (frm) { + frappe.call({ + method: "hrms.hr.doctype.interview_type.interview_type.create_interview", + args: { + doc: frm.doc, + }, + callback: function (r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + }, + }); + }, }); diff --git a/hrms/hr/doctype/interview_type/interview_type.json b/hrms/hr/doctype/interview_type/interview_type.json index 1a3c8d525d..ba0e651fea 100644 --- a/hrms/hr/doctype/interview_type/interview_type.json +++ b/hrms/hr/doctype/interview_type/interview_type.json @@ -1,33 +1,78 @@ { "actions": [], "allow_rename": 1, - "autoname": "Prompt", - "creation": "2021-04-12 14:44:40.664034", + "autoname": "field:interview_type_name", + "creation": "2021-04-12 12:57:19.902866", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "interview_type_name", + "interviewers", + "column_break_3", + "expected_average_rating", + "expected_skills_section", + "designation", + "expected_skill_set", + "section_break_xlzv", "description" ], "fields": [ + { + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation" + }, + { + "fieldname": "expected_skills_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "expected_skill_set", + "fieldtype": "Table", + "label": "Expected Skillset", + "options": "Expected Skill Set", + "reqd": 1 + }, + { + "fieldname": "expected_average_rating", + "fieldtype": "Rating", + "label": "Expected Average Rating" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "interviewers", + "fieldtype": "Table MultiSelect", + "label": "Interviewers", + "options": "Interviewer" + }, + { + "fieldname": "section_break_xlzv", + "fieldtype": "Section Break" + }, { "fieldname": "description", "fieldtype": "Text", - "in_list_view": 1, "label": "Description" - } - ], - "index_web_pages_for_search": 1, - "links": [ + }, { - "link_doctype": "Interview Round", - "link_fieldname": "interview_type" + "fieldname": "interview_type_name", + "fieldtype": "Data", + "label": "Interview Type Name", + "unique": 1 } ], - "modified": "2024-03-27 13:09:52.310012", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-02-16 15:52:26.011116", "modified_by": "Administrator", "module": "HR", "name": "Interview Type", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -38,7 +83,7 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "HR User", "share": 1, "write": 1 }, @@ -62,13 +107,15 @@ "print": 1, "read": 1, "report": 1, - "role": "HR User", + "role": "Interviewer", + "select": 1, "share": 1, "write": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/hrms/hr/doctype/interview_type/interview_type.py b/hrms/hr/doctype/interview_type/interview_type.py index 5d391bca5a..ba42ad5661 100644 --- a/hrms/hr/doctype/interview_type/interview_type.py +++ b/hrms/hr/doctype/interview_type/interview_type.py @@ -2,7 +2,9 @@ # For license information, please see license.txt -# import frappe +import json + +import frappe from frappe.model.document import Document @@ -19,3 +21,21 @@ class InterviewType(Document): # end: auto-generated types pass + + +@frappe.whitelist() +def create_interview(doc): + if isinstance(doc, str): + doc = json.loads(doc) + doc = frappe.get_doc(doc) + + interview = frappe.new_doc("Interview") + interview.interview_type = doc.name + interview.designation = doc.designation + + if doc.interviewers: + interview.interview_details = [] + for d in doc.interviewers: + interview.append("interview_details", {"interviewer": d.user}) + + return interview diff --git a/hrms/hr/doctype/interview_type/test_interview_type.py b/hrms/hr/doctype/interview_type/test_interview_type.py index 17d9e7546f..a141363af9 100644 --- a/hrms/hr/doctype/interview_type/test_interview_type.py +++ b/hrms/hr/doctype/interview_type/test_interview_type.py @@ -4,6 +4,8 @@ # import frappe from hrms.tests.utils import HRMSTestSuite +# import frappe + class TestInterviewType(HRMSTestSuite): pass diff --git a/hrms/hr/doctype/job_applicant/job_applicant.js b/hrms/hr/doctype/job_applicant/job_applicant.js index 7eb7cb1aac..0f8f889496 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.js +++ b/hrms/hr/doctype/job_applicant/job_applicant.js @@ -83,13 +83,13 @@ frappe.ui.form.on("Job Applicant", { create_dialog: function (frm) { let d = new frappe.ui.Dialog({ - title: __("Enter Interview Round"), + title: __("Enter Interview Type"), fields: [ { - label: "Interview Round", - fieldname: "interview_round", + label: "Interview Type", + fieldname: "interview_type", fieldtype: "Link", - options: "Interview Round", + options: "Interview Type", }, ], primary_action_label: __("Create Interview"), @@ -106,7 +106,7 @@ frappe.ui.form.on("Job Applicant", { method: "hrms.hr.doctype.job_applicant.job_applicant.create_interview", args: { job_applicant: frm.doc.name, - interview_round: values.interview_round, + interview_type: values.interview_type, }, callback: function (r) { var doclist = frappe.model.sync(r.message); diff --git a/hrms/hr/doctype/job_applicant/job_applicant.py b/hrms/hr/doctype/job_applicant/job_applicant.py index f1c820528e..5361998d7c 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.py +++ b/hrms/hr/doctype/job_applicant/job_applicant.py @@ -86,26 +86,26 @@ def set_status_for_employee_referral(self): @frappe.whitelist() -def create_interview(job_applicant: str, interview_round: str) -> Document: +def create_interview(job_applicant: str, interview_type: str) -> Document: doc = frappe.get_doc("Job Applicant", job_applicant) - round_designation = frappe.db.get_value("Interview Round", interview_round, "designation") + round_designation = frappe.db.get_value("Interview Type", interview_type, "designation") if round_designation and doc.designation and round_designation != doc.designation: frappe.throw( - _("Interview Round {0} is only applicable for the Designation {1}").format( - interview_round, round_designation + _("Interview Type {0} is only applicable for the Designation {1}").format( + interview_type, round_designation ) ) interview = frappe.new_doc("Interview") - interview.interview_round = interview_round + interview.interview_type = interview_type interview.job_applicant = doc.name interview.designation = doc.designation interview.resume_link = doc.resume_link interview.job_opening = doc.job_title - interviewers = get_interviewers(interview_round) + interviewers = get_interviewers(interview_type) for d in interviewers: interview.append("interview_details", {"interviewer": d.interviewer}) @@ -117,7 +117,7 @@ def get_interview_details(job_applicant: str) -> dict: interview_details = frappe.db.get_all( "Interview", filters={"job_applicant": job_applicant, "docstatus": ["!=", 2]}, - fields=["name", "interview_round", "scheduled_on", "average_rating", "status"], + fields=["name", "interview_type", "scheduled_on", "average_rating", "status"], ) interview_detail_map = {} meta = frappe.get_meta("Interview") diff --git a/hrms/hr/doctype/job_applicant/job_applicant_dashboard.html b/hrms/hr/doctype/job_applicant/job_applicant_dashboard.html index 55ebedb1db..4787cbd6ba 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant_dashboard.html +++ b/hrms/hr/doctype/job_applicant/job_applicant_dashboard.html @@ -5,7 +5,7 @@ {{ __("Interview") }} - {{ __("Interview Round") }} + {{ __("Interview Type") }} {{ __("Date") }} {{ __("Status") }} {{ __("Rating") }} @@ -17,7 +17,7 @@ {%= key %} - {%= value["interview_round"] %} + {%= value["interview_type"] %} {%= frappe.datetime.str_to_user(value["scheduled_on"]) %} {%= value["status"] %} {% let right_class = ''; %} diff --git a/hrms/hr/workspace/recruitment/recruitment.json b/hrms/hr/workspace/recruitment/recruitment.json index 8aee5de705..023995b4b0 100644 --- a/hrms/hr/workspace/recruitment/recruitment.json +++ b/hrms/hr/workspace/recruitment/recruitment.json @@ -14,60 +14,10 @@ "for_user": "", "hide_custom": 0, "icon": "users", - "idx": 4, + "idx": 7, "is_hidden": 0, "label": "Recruitment", "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Interviews", - "link_count": 4, - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Interview Type", - "link_count": 0, - "link_to": "Interview Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Interview Round", - "link_count": 0, - "link_to": "Interview Round", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Interview Type, Interview Round", - "hidden": 0, - "is_query_report": 0, - "label": "Interview", - "link_count": 0, - "link_to": "Interview", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Interview", - "hidden": 0, - "is_query_report": 0, - "label": "Interview Feedback", - "link_count": 0, - "link_to": "Interview Feedback", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -182,9 +132,50 @@ "link_type": "DocType", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interviews", + "link_count": 3, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Interview Type", + "link_count": 0, + "link_to": "Interview Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Interview Type", + "hidden": 0, + "is_query_report": 0, + "label": "Interview", + "link_count": 0, + "link_to": "Interview", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Interview", + "hidden": 0, + "is_query_report": 0, + "label": "Interview Feedback", + "link_count": 0, + "link_to": "Interview Feedback", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2026-01-09 18:00:59.928738", + "modified": "2026-02-19 16:36:49.582837", "modified_by": "Administrator", "module": "HR", "name": "Recruitment", diff --git a/hrms/patches.txt b/hrms/patches.txt index 0c080d0361..5fac4c44ea 100644 --- a/hrms/patches.txt +++ b/hrms/patches.txt @@ -43,3 +43,4 @@ hrms.patches.v16_0.create_holiday_list_assignments hrms.patches.v16_0.set_base_paid_amount_in_employee_advance hrms.patches.v16_0.set_currency_and_base_fields_in_expense_claim hrms.patches.v16_0.remove_ess_user_type_limit +hrms.patches.v16_0.merge_interview_round_with_interview_type diff --git a/hrms/patches/v16_0/merge_interview_round_with_interview_type.py b/hrms/patches/v16_0/merge_interview_round_with_interview_type.py new file mode 100644 index 0000000000..dfe14c66ae --- /dev/null +++ b/hrms/patches/v16_0/merge_interview_round_with_interview_type.py @@ -0,0 +1,13 @@ +import frappe + + +def execute(): + InterviewType = frappe.qb.DocType("Interview Type") + InterviewRound = frappe.qb.DocType("Interview Round") + + ( + frappe.qb.update(InterviewRound) + .left_join(InterviewType) + .on(InterviewType.name == InterviewRound.interview_type) + .set(InterviewRound.description, InterviewType.description) + ).run() diff --git a/hrms/workspace_sidebar/recruitment.json b/hrms/workspace_sidebar/recruitment.json index 945cc3a3f0..eed5da558e 100644 --- a/hrms/workspace_sidebar/recruitment.json +++ b/hrms/workspace_sidebar/recruitment.json @@ -176,8 +176,8 @@ "icon": "", "indent": 0, "keep_closed": 0, - "label": "Interview Round", - "link_to": "Interview Round", + "label": "Interview Type", + "link_to": "Interview Type", "link_type": "DocType", "show_arrow": 0, "type": "Link" @@ -230,7 +230,7 @@ "type": "Link" } ], - "modified": "2026-02-16 11:26:04.338308", + "modified": "2026-02-16 12:12:43.728243", "modified_by": "Administrator", "module": "HR", "name": "Recruitment", From 92527aaf6226fdd72451af8d12855f25ac2ac850 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 23 Feb 2026 15:01:46 +0530 Subject: [PATCH 05/29] fix(Interview): missing type hints for whilelisted methods fix(Interview Type): missing type hints for whilelisted methods fix(Job Applicant): missing type hints for whilelisted methods --- hrms/hr/doctype/interview/interview.py | 2 +- hrms/hr/doctype/interview_type/interview_type.js | 2 +- hrms/hr/doctype/interview_type/interview_type.py | 14 ++++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/hrms/hr/doctype/interview/interview.py b/hrms/hr/doctype/interview/interview.py index aca286eb34..b23e7775f0 100644 --- a/hrms/hr/doctype/interview/interview.py +++ b/hrms/hr/doctype/interview/interview.py @@ -337,7 +337,7 @@ def send_daily_feedback_reminder(): @frappe.whitelist() -def get_expected_skill_set(interview_type: str) -> list[dict]: +def get_expected_skill_set(interview_type: str): return frappe.get_all( "Expected Skill Set", filters={"parent": interview_type}, fields=["skill"], order_by="idx" ) diff --git a/hrms/hr/doctype/interview_type/interview_type.js b/hrms/hr/doctype/interview_type/interview_type.js index 7732faffa4..6babac9401 100644 --- a/hrms/hr/doctype/interview_type/interview_type.js +++ b/hrms/hr/doctype/interview_type/interview_type.js @@ -27,7 +27,7 @@ frappe.ui.form.on("Interview Type", { frappe.call({ method: "hrms.hr.doctype.interview_type.interview_type.create_interview", args: { - doc: frm.doc, + docname: frm.doc.name, }, callback: function (r) { var doclist = frappe.model.sync(r.message); diff --git a/hrms/hr/doctype/interview_type/interview_type.py b/hrms/hr/doctype/interview_type/interview_type.py index ba42ad5661..747c3bfdf6 100644 --- a/hrms/hr/doctype/interview_type/interview_type.py +++ b/hrms/hr/doctype/interview_type/interview_type.py @@ -24,18 +24,16 @@ class InterviewType(Document): @frappe.whitelist() -def create_interview(doc): - if isinstance(doc, str): - doc = json.loads(doc) - doc = frappe.get_doc(doc) +def create_interview(docname: str): + interview_type = frappe.get_doc("Interview Type", docname) interview = frappe.new_doc("Interview") - interview.interview_type = doc.name - interview.designation = doc.designation + interview.interview_type = interview_type.name + interview.designation = interview_type.designation - if doc.interviewers: + if interview_type.interviewers: interview.interview_details = [] - for d in doc.interviewers: + for d in interview_type.interviewers: interview.append("interview_details", {"interviewer": d.user}) return interview From 19e805148b5c2da930c8fe935a9a5f2bd725bc7e Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 30 Mar 2026 14:49:17 +0530 Subject: [PATCH 06/29] fix: autofill job title on selecting designation fix: make designation part of job opening template --- hrms/hr/doctype/job_opening/job_opening.js | 4 ++++ .../job_opening_template/job_opening_template.json | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/hrms/hr/doctype/job_opening/job_opening.js b/hrms/hr/doctype/job_opening/job_opening.js index 2cff2b24ff..e9f323b45a 100644 --- a/hrms/hr/doctype/job_opening/job_opening.js +++ b/hrms/hr/doctype/job_opening/job_opening.js @@ -12,6 +12,9 @@ frappe.ui.form.on("Job Opening", { }); }, designation: function (frm) { + if (frm.doc.designation) { + frm.set_value("job_title", frm.doc.designation); + } if (frm.doc.designation && frm.doc.company) { frappe.call({ method: "hrms.hr.doctype.staffing_plan.staffing_plan.get_active_staffing_plan_details", @@ -48,6 +51,7 @@ frappe.ui.form.on("Job Opening", { frappe.db.get_doc("Job Opening Template", frm.doc.job_opening_template).then((doc) => { frm.set_value({ + designation: doc.designation, department: doc.department, employment_type: doc.employment_type, location: doc.location, diff --git a/hrms/hr/doctype/job_opening_template/job_opening_template.json b/hrms/hr/doctype/job_opening_template/job_opening_template.json index b1762284b9..7b417f6594 100644 --- a/hrms/hr/doctype/job_opening_template/job_opening_template.json +++ b/hrms/hr/doctype/job_opening_template/job_opening_template.json @@ -8,6 +8,7 @@ "field_order": [ "template_title", "department", + "designation", "column_break_wkcr", "employment_type", "location", @@ -54,12 +55,18 @@ "fieldname": "description", "fieldtype": "Text Editor", "label": "Description" + }, + { + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-12-12 12:52:12.217926", + "modified": "2026-03-30 14:42:46.967984", "modified_by": "Administrator", "module": "HR", "name": "Job Opening Template", From e8ff733a497528321818352eb580ef13c5d6ff15 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 30 Mar 2026 14:59:52 +0530 Subject: [PATCH 07/29] fix(Job Opening): split pay details into separate tab fix(Job Opening Template): make pay details part of job opening template --- hrms/hr/doctype/job_opening/job_opening.js | 5 ++ hrms/hr/doctype/job_opening/job_opening.json | 26 +++++----- .../templates/job_opening_list.html | 5 ++ .../job_opening_template.json | 47 ++++++++++++++++++- 4 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 hrms/hr/doctype/job_opening/templates/job_opening_list.html diff --git a/hrms/hr/doctype/job_opening/job_opening.js b/hrms/hr/doctype/job_opening/job_opening.js index e9f323b45a..7d2519ca56 100644 --- a/hrms/hr/doctype/job_opening/job_opening.js +++ b/hrms/hr/doctype/job_opening/job_opening.js @@ -56,6 +56,11 @@ frappe.ui.form.on("Job Opening", { employment_type: doc.employment_type, location: doc.location, description: doc.description, + currency: doc.currency, + upper_range: doc.upper_range, + lower_range: doc.lower_range, + salary_per: doc.salary_per, + publish_salary_range: doc.publish_salary_range, }); frm.refresh_fields(); diff --git a/hrms/hr/doctype/job_opening/job_opening.json b/hrms/hr/doctype/job_opening/job_opening.json index 96b587f393..eb0a8f3f96 100644 --- a/hrms/hr/doctype/job_opening/job_opening.json +++ b/hrms/hr/doctype/job_opening/job_opening.json @@ -37,11 +37,11 @@ "job_application_route", "section_break_14", "description", - "section_break_16", + "pay_details_tab", "currency", "lower_range", "upper_range", - "column_break_20", + "column_break_pfir", "salary_per", "publish_salary_range" ], @@ -131,11 +131,6 @@ "fieldname": "section_break_14", "fieldtype": "Section Break" }, - { - "collapsible": 1, - "fieldname": "section_break_16", - "fieldtype": "Section Break" - }, { "fieldname": "currency", "fieldtype": "Link", @@ -156,10 +151,6 @@ "options": "currency", "precision": "0" }, - { - "fieldname": "column_break_20", - "fieldtype": "Column Break" - }, { "depends_on": "publish", "description": "Route to the custom Job Application Webform", @@ -258,6 +249,15 @@ "fieldtype": "Link", "label": "Job Opening Template", "options": "Job Opening Template" + }, + { + "fieldname": "pay_details_tab", + "fieldtype": "Tab Break", + "label": "Pay Details" + }, + { + "fieldname": "column_break_pfir", + "fieldtype": "Column Break" } ], "has_web_view": 1, @@ -265,11 +265,11 @@ "idx": 1, "is_published_field": "publish", "links": [], - "modified": "2025-12-11 19:18:36.145062", + "modified": "2026-03-30 14:55:58.135980", "modified_by": "Administrator", "module": "HR", "name": "Job Opening", - "naming_rule": "Expression (old style)", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { diff --git a/hrms/hr/doctype/job_opening/templates/job_opening_list.html b/hrms/hr/doctype/job_opening/templates/job_opening_list.html new file mode 100644 index 0000000000..3e892a5603 --- /dev/null +++ b/hrms/hr/doctype/job_opening/templates/job_opening_list.html @@ -0,0 +1,5 @@ +{% extends "templates/web.html" %} + +{% block page_content %} + {% include "templates/includes/list/list.html" %} +{% endblock %} \ No newline at end of file diff --git a/hrms/hr/doctype/job_opening_template/job_opening_template.json b/hrms/hr/doctype/job_opening_template/job_opening_template.json index 7b417f6594..7b57250e1c 100644 --- a/hrms/hr/doctype/job_opening_template/job_opening_template.json +++ b/hrms/hr/doctype/job_opening_template/job_opening_template.json @@ -12,6 +12,13 @@ "column_break_wkcr", "employment_type", "location", + "pay_details_section", + "currency", + "upper_range", + "lower_range", + "column_break_vqyu", + "salary_per", + "publish_salary_range", "section_break_dwfh", "description" ], @@ -61,12 +68,50 @@ "fieldtype": "Link", "label": "Designation", "options": "Designation" + }, + { + "fieldname": "pay_details_section", + "fieldtype": "Section Break", + "label": "Pay Details" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "upper_range", + "fieldtype": "Currency", + "label": "Upper Range" + }, + { + "fieldname": "lower_range", + "fieldtype": "Currency", + "label": "Lower Range" + }, + { + "fieldname": "column_break_vqyu", + "fieldtype": "Column Break" + }, + { + "default": "Month", + "fieldname": "salary_per", + "fieldtype": "Select", + "label": "Salary Paid Per", + "options": "Month\nYear" + }, + { + "default": "0", + "fieldname": "publish_salary_range", + "fieldtype": "Check", + "label": "Publish Salary Range" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-03-30 14:42:46.967984", + "modified": "2026-03-30 14:57:27.717762", "modified_by": "Administrator", "module": "HR", "name": "Job Opening Template", From 307df2cf1be29001203947642666181752cedf9d Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 30 Mar 2026 17:04:03 +0530 Subject: [PATCH 08/29] fix(Job Opening Template): add "Create Job Opening" button --- hrms/hr/doctype/job_opening/job_opening.js | 1 - .../job_opening_template.js | 17 +++++++++---- .../job_opening_template.py | 25 ++++++++++++++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/hrms/hr/doctype/job_opening/job_opening.js b/hrms/hr/doctype/job_opening/job_opening.js index 7d2519ca56..44d77500d3 100644 --- a/hrms/hr/doctype/job_opening/job_opening.js +++ b/hrms/hr/doctype/job_opening/job_opening.js @@ -48,7 +48,6 @@ frappe.ui.form.on("Job Opening", { job_opening_template: function (frm) { if (!frm.doc.job_opening_template) return; - frappe.db.get_doc("Job Opening Template", frm.doc.job_opening_template).then((doc) => { frm.set_value({ designation: doc.designation, diff --git a/hrms/hr/doctype/job_opening_template/job_opening_template.js b/hrms/hr/doctype/job_opening_template/job_opening_template.js index 9006d24aa5..99e88a7c61 100644 --- a/hrms/hr/doctype/job_opening_template/job_opening_template.js +++ b/hrms/hr/doctype/job_opening_template/job_opening_template.js @@ -1,8 +1,15 @@ // Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -// frappe.ui.form.on("Job Opening Template", { -// refresh(frm) { - -// }, -// }); +frappe.ui.form.on("Job Opening Template", { + refresh(frm) { + if (!frm.doc.__islocal) { + frm.add_custom_button(__("Create Job Opening"), () => { + frappe.model.open_mapped_doc({ + method: "hrms.hr.doctype.job_opening_template.job_opening_template.create_job_opening_from_template", + frm: frm, + }); + }); + } + }, +}); diff --git a/hrms/hr/doctype/job_opening_template/job_opening_template.py b/hrms/hr/doctype/job_opening_template/job_opening_template.py index 4ced9ab0ed..994d4a7e1b 100644 --- a/hrms/hr/doctype/job_opening_template/job_opening_template.py +++ b/hrms/hr/doctype/job_opening_template/job_opening_template.py @@ -1,8 +1,9 @@ # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc class JobOpeningTemplate(Document): @@ -22,3 +23,25 @@ class JobOpeningTemplate(Document): # end: auto-generated types pass + + +@frappe.whitelist() +def create_job_opening_from_template(source: str | Document) -> Document: + target_doc = frappe.new_doc("Job Opening") + + def set_missing_values(source_doc, target_doc): + target_doc.job_title = source_doc.designation + + doc = get_mapped_doc( + "Job Opening Template", + source, + { + "Job Opening Template": {"doctype": "Job Opening"}, + "field_map": { + "job_opening_template": "name", + }, + }, + target_doc, + set_missing_values, + ) + return doc From 3eab1f7090c54e60a83705f8e50624da62938099 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 30 Mar 2026 17:20:32 +0530 Subject: [PATCH 09/29] fix: automatically set route on clicking publish on website --- hrms/hr/doctype/job_opening/job_opening.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/hrms/hr/doctype/job_opening/job_opening.js b/hrms/hr/doctype/job_opening/job_opening.js index 44d77500d3..219f50383e 100644 --- a/hrms/hr/doctype/job_opening/job_opening.js +++ b/hrms/hr/doctype/job_opening/job_opening.js @@ -65,4 +65,15 @@ frappe.ui.form.on("Job Opening", { frm.refresh_fields(); }); }, + publish: function (frm) { + if (frm.doc.publish && !frm.doc.route) { + frm.trigger("set_route"); + } + }, + set_route: function (frm) { + if (frm.doc.publish && !frm.doc.route) { + route = `jobs/${frappe.scrub(frm.doc.company)}/${frappe.scrub(frm.doc.designation)}`; + frm.set_value("route", route); + } + }, }); From 60cccc006696934ddf8ca73f9aa65c80f11d3ad0 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 30 Mar 2026 17:22:16 +0530 Subject: [PATCH 10/29] fix: rename Job Application Route to Application Web Form Route --- hrms/hr/doctype/job_opening/job_opening.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hrms/hr/doctype/job_opening/job_opening.json b/hrms/hr/doctype/job_opening/job_opening.json index eb0a8f3f96..4b248553d0 100644 --- a/hrms/hr/doctype/job_opening/job_opening.json +++ b/hrms/hr/doctype/job_opening/job_opening.json @@ -156,7 +156,7 @@ "description": "Route to the custom Job Application Webform", "fieldname": "job_application_route", "fieldtype": "Data", - "label": "Job Application Route" + "label": "Application Web From Route" }, { "default": "0", @@ -265,7 +265,7 @@ "idx": 1, "is_published_field": "publish", "links": [], - "modified": "2026-03-30 14:55:58.135980", + "modified": "2026-03-30 17:21:23.211297", "modified_by": "Administrator", "module": "HR", "name": "Job Opening", From cb96cf69d257ca15c5bb6b0c40c2f5f4302179e2 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 31 Mar 2026 12:59:57 +0530 Subject: [PATCH 11/29] fix(Job Opening): Added relevant fields in list view and list view quick filters --- hrms/hr/doctype/job_opening/job_opening.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hrms/hr/doctype/job_opening/job_opening.json b/hrms/hr/doctype/job_opening/job_opening.json index 4b248553d0..f798a60da6 100644 --- a/hrms/hr/doctype/job_opening/job_opening.json +++ b/hrms/hr/doctype/job_opening/job_opening.json @@ -56,6 +56,8 @@ { "fieldname": "company", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Company", "options": "Company", "reqd": 1 @@ -75,6 +77,8 @@ { "fieldname": "designation", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Designation", "options": "Designation", "reqd": 1 @@ -82,6 +86,8 @@ { "fieldname": "department", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Department", "options": "Department" }, @@ -107,6 +113,8 @@ "default": "0", "fieldname": "publish", "fieldtype": "Check", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Publish on website" }, { @@ -201,6 +209,8 @@ { "fieldname": "employment_type", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Employment Type", "options": "Employment Type" }, @@ -265,7 +275,7 @@ "idx": 1, "is_published_field": "publish", "links": [], - "modified": "2026-03-30 17:21:23.211297", + "modified": "2026-03-31 12:54:28.114314", "modified_by": "Administrator", "module": "HR", "name": "Job Opening", From f8ed6ea6fcbb4c272b1ce5922424acfe43787ca6 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 31 Mar 2026 14:31:37 +0530 Subject: [PATCH 12/29] fix(Job Applicant): rename Applicant Name to Full Name fix(Job Applicant): added 3 coumn layout in the first section fix(Job Applicant): reordered resume section --- .../doctype/job_applicant/job_applicant.json | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/hrms/hr/doctype/job_applicant/job_applicant.json b/hrms/hr/doctype/job_applicant/job_applicant.json index 1147f51565..d70c76c20c 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.json +++ b/hrms/hr/doctype/job_applicant/job_applicant.json @@ -14,10 +14,11 @@ "applicant_name", "email_id", "phone_number", - "country", "column_break_3", - "job_title", "designation", + "country", + "column_break_enue", + "job_title", "status", "source_and_rating_section", "source", @@ -26,10 +27,10 @@ "column_break_13", "applicant_rating", "section_break_6", + "resume_link", + "resume_attachment", "notes", "cover_letter", - "resume_attachment", - "resume_link", "section_break_16", "currency", "column_break_18", @@ -42,7 +43,7 @@ "fieldname": "applicant_name", "fieldtype": "Data", "in_global_search": 1, - "label": "Applicant Name", + "label": "Full Name", "reqd": 1 }, { @@ -189,17 +190,21 @@ "fieldtype": "Link", "label": "Designation", "options": "Designation" + }, + { + "fieldname": "column_break_enue", + "fieldtype": "Column Break" } ], "icon": "fa fa-user", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-01-16 13:06:05.312255", + "modified": "2026-03-31 14:29:57.420235", "modified_by": "Administrator", "module": "HR", "name": "Job Applicant", - "naming_rule": "Expression (old style)", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { @@ -214,6 +219,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "applicant_name, email_id, job_title, phone_number", "sender_field": "email_id", "sort_field": "creation", @@ -221,4 +227,4 @@ "states": [], "subject_field": "notes", "title_field": "applicant_name" -} \ No newline at end of file +} From 21b424ede7ae2303bd343d63367cf80d5ce30b69 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 31 Mar 2026 18:06:03 +0530 Subject: [PATCH 13/29] fix(Job Applicant): Unnest Action buttons when there's only one fix(Job Applicant): filter interview type by designation while creating interview fix(Job Applicant): new Shortlisted status --- .../hr/doctype/job_applicant/job_applicant.js | 45 ++++++++----------- .../doctype/job_applicant/job_applicant.json | 4 +- .../hr/doctype/job_applicant/job_applicant.py | 2 +- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/hrms/hr/doctype/job_applicant/job_applicant.js b/hrms/hr/doctype/job_applicant/job_applicant.js index 0f8f889496..dc598666d1 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.js +++ b/hrms/hr/doctype/job_applicant/job_applicant.js @@ -21,41 +21,29 @@ frappe.ui.form.on("Job Applicant", { create_custom_buttons: function (frm) { if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") { - frm.add_custom_button( - __("Interview"), - function () { - frm.events.create_dialog(frm); - }, - __("Create"), - ); + frm.add_custom_button(__("Create Interview"), function () { + frm.events.create_dialog(frm); + }); } if (!frm.doc.__islocal && frm.doc.status == "Accepted") { if (frm.doc.__onload && frm.doc.__onload.job_offer) { $('[data-doctype="Employee Onboarding"]').find("button").show(); $('[data-doctype="Job Offer"]').find("button").hide(); - frm.add_custom_button( - __("Job Offer"), - function () { - frappe.set_route("Form", "Job Offer", frm.doc.__onload.job_offer); - }, - __("View"), - ); + frm.add_custom_button(__("View Job Offer"), function () { + frappe.set_route("Form", "Job Offer", frm.doc.__onload.job_offer); + }); } else { $('[data-doctype="Employee Onboarding"]').find("button").hide(); $('[data-doctype="Job Offer"]').find("button").show(); - frm.add_custom_button( - __("Job Offer"), - function () { - frappe.route_options = { - job_applicant: frm.doc.name, - applicant_name: frm.doc.applicant_name, - designation: frm.doc.job_opening || frm.doc.designation, - }; - frappe.new_doc("Job Offer"); - }, - __("Create"), - ); + frm.add_custom_button(__("Create Job Offer"), function () { + frappe.route_options = { + job_applicant: frm.doc.name, + applicant_name: frm.doc.applicant_name, + designation: frm.doc.job_opening || frm.doc.designation, + }; + frappe.new_doc("Job Offer"); + }); } } }, @@ -90,6 +78,11 @@ frappe.ui.form.on("Job Applicant", { fieldname: "interview_type", fieldtype: "Link", options: "Interview Type", + get_query: function () { + return { + filters: [["designation", "=", frm.doc.designation]], + }; + }, }, ], primary_action_label: __("Create Interview"), diff --git a/hrms/hr/doctype/job_applicant/job_applicant.json b/hrms/hr/doctype/job_applicant/job_applicant.json index d70c76c20c..1c7921a471 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.json +++ b/hrms/hr/doctype/job_applicant/job_applicant.json @@ -60,7 +60,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "options": "Open\nReplied\nRejected\nHold\nAccepted", + "options": "Open\nReplied\nShortlisted\nRejected\nHold\nAccepted", "reqd": 1 }, { @@ -200,7 +200,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-03-31 14:29:57.420235", + "modified": "2026-03-31 18:02:22.236160", "modified_by": "Administrator", "module": "HR", "name": "Job Applicant", diff --git a/hrms/hr/doctype/job_applicant/job_applicant.py b/hrms/hr/doctype/job_applicant/job_applicant.py index 5361998d7c..49e274f0a3 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.py +++ b/hrms/hr/doctype/job_applicant/job_applicant.py @@ -42,7 +42,7 @@ class JobApplicant(Document): resume_link: DF.Data | None source: DF.Link | None source_name: DF.Link | None - status: DF.Literal["Open", "Replied", "Rejected", "Hold", "Accepted"] + status: DF.Literal["Open", "Replied", "Shortlisted", "Rejected", "Hold", "Accepted"] upper_range: DF.Currency # end: auto-generated types From 89957a619a2cdb764927629f4e3c5245ac173461 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 7 Apr 2026 15:01:18 +0530 Subject: [PATCH 14/29] fix: added quick action buttons on navbar to Reject and Shortlist applicants fix: moved Salary Expeactations to new tab fix: moved source section at the bottom --- .../hr/doctype/job_applicant/job_applicant.js | 23 ++++++++++++--- .../doctype/job_applicant/job_applicant.json | 28 +++++++++---------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/hrms/hr/doctype/job_applicant/job_applicant.js b/hrms/hr/doctype/job_applicant/job_applicant.js index dc598666d1..12b501294a 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.js +++ b/hrms/hr/doctype/job_applicant/job_applicant.js @@ -17,13 +17,28 @@ frappe.ui.form.on("Job Applicant", { }); frm.events.create_custom_buttons(frm); frm.events.make_dashboard(frm); + frappe.boot.desk_settings.form_navigation_buttons = 1; }, create_custom_buttons: function (frm) { - if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") { - frm.add_custom_button(__("Create Interview"), function () { - frm.events.create_dialog(frm); - }); + if (!frm.doc.__islocal) { + if (frm.doc.status == "Open") { + frm.add_custom_button(__("Shortlist"), () => { + frm.set_value("status", "Shortlisted"); + frm.save(); + frm.refresh(); + }); + frm.add_custom_button(__("Reject"), () => { + frm.set_value("status", "Rejected"); + frm.save(); + frm.refresh(); + }); + } + if (frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") { + frm.add_custom_button(__("Create Interview"), function () { + frm.events.create_dialog(frm); + }); + } } if (!frm.doc.__islocal && frm.doc.status == "Accepted") { diff --git a/hrms/hr/doctype/job_applicant/job_applicant.json b/hrms/hr/doctype/job_applicant/job_applicant.json index 1c7921a471..fafddba334 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.json +++ b/hrms/hr/doctype/job_applicant/job_applicant.json @@ -17,21 +17,21 @@ "column_break_3", "designation", "country", - "column_break_enue", "job_title", + "column_break_enue", "status", - "source_and_rating_section", - "source", - "source_name", - "employee_referral", - "column_break_13", "applicant_rating", "section_break_6", "resume_link", "resume_attachment", "notes", "cover_letter", - "section_break_16", + "source_and_rating_section", + "source", + "source_name", + "column_break_13", + "employee_referral", + "salary_expectation_tab", "currency", "column_break_18", "lower_range", @@ -133,11 +133,6 @@ "in_list_view": 1, "label": "Applicant Rating" }, - { - "fieldname": "section_break_16", - "fieldtype": "Section Break", - "label": "Salary Expectation" - }, { "fieldname": "lower_range", "fieldtype": "Currency", @@ -177,7 +172,7 @@ { "fieldname": "source_and_rating_section", "fieldtype": "Section Break", - "label": "Source and Rating" + "label": "Source" }, { "fieldname": "column_break_13", @@ -194,13 +189,18 @@ { "fieldname": "column_break_enue", "fieldtype": "Column Break" + }, + { + "fieldname": "salary_expectation_tab", + "fieldtype": "Tab Break", + "label": "Salary Expectation" } ], "icon": "fa fa-user", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-03-31 18:02:22.236160", + "modified": "2026-04-07 14:34:24.139833", "modified_by": "Administrator", "module": "HR", "name": "Job Applicant", From b6311dfef70eda8d5d01433e333a15327e1efe8c Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 7 Apr 2026 15:01:55 +0530 Subject: [PATCH 15/29] fix: added navigation buttons to quickly move between applicants --- hrms/hr/doctype/job_applicant/job_applicant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrms/hr/doctype/job_applicant/job_applicant.js b/hrms/hr/doctype/job_applicant/job_applicant.js index 12b501294a..2df0040b27 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.js +++ b/hrms/hr/doctype/job_applicant/job_applicant.js @@ -17,7 +17,7 @@ frappe.ui.form.on("Job Applicant", { }); frm.events.create_custom_buttons(frm); frm.events.make_dashboard(frm); - frappe.boot.desk_settings.form_navigation_buttons = 1; + frm.toolbar.make_navigation(); }, create_custom_buttons: function (frm) { From 90306fb9f12dc1772ad8eabed8b3c42525c2000d Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Sun, 12 Apr 2026 14:51:45 +0530 Subject: [PATCH 16/29] fix: hide interview summary section if there's no interview created fix: make get_interview_details return None instead of partical dictionary if no interview exists --- .../hr/doctype/job_applicant/job_applicant.js | 24 +++++++++++-------- .../hr/doctype/job_applicant/job_applicant.py | 3 +++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/hrms/hr/doctype/job_applicant/job_applicant.js b/hrms/hr/doctype/job_applicant/job_applicant.js index 2df0040b27..6fb1fb73ac 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.js +++ b/hrms/hr/doctype/job_applicant/job_applicant.js @@ -16,7 +16,7 @@ frappe.ui.form.on("Job Applicant", { }; }); frm.events.create_custom_buttons(frm); - frm.events.make_dashboard(frm); + frm.events.get_interview_for_dashboard(frm); frm.toolbar.make_navigation(); }, @@ -63,7 +63,8 @@ frappe.ui.form.on("Job Applicant", { } }, - make_dashboard: function (frm) { + get_interview_for_dashboard: function (frm) { + $("div").remove(".form-dashboard-section.custom"); frappe.call({ method: "hrms.hr.doctype.job_applicant.job_applicant.get_interview_details", args: { @@ -71,19 +72,22 @@ frappe.ui.form.on("Job Applicant", { }, callback: function (r) { if (r.message) { - $("div").remove(".form-dashboard-section.custom"); - frm.dashboard.add_section( - frappe.render_template("job_applicant_dashboard", { - data: r.message.interviews, - number_of_stars: r.message.stars, - }), - __("Interview Summary"), - ); + frm.events.make_dashboard(frm, r.message); } }, }); }, + make_dashboard: function (frm, message) { + frm.dashboard.add_section( + frappe.render_template("job_applicant_dashboard", { + data: message.interviews, + number_of_stars: message.stars, + }), + __("Interview Summary"), + ); + }, + create_dialog: function (frm) { let d = new frappe.ui.Dialog({ title: __("Enter Interview Type"), diff --git a/hrms/hr/doctype/job_applicant/job_applicant.py b/hrms/hr/doctype/job_applicant/job_applicant.py index 49e274f0a3..01b1139ebd 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.py +++ b/hrms/hr/doctype/job_applicant/job_applicant.py @@ -119,6 +119,9 @@ def get_interview_details(job_applicant: str) -> dict: filters={"job_applicant": job_applicant, "docstatus": ["!=", 2]}, fields=["name", "interview_type", "scheduled_on", "average_rating", "status"], ) + if not interview_details: + return None + interview_detail_map = {} meta = frappe.get_meta("Interview") number_of_stars = meta.get_options("average_rating") or 5 From 649a99b0c1536bedb5ae07a0fadc5652ab9314ff Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 11:43:43 +0530 Subject: [PATCH 17/29] fix: embed resume in job applicant fix: show button to open resume link in new tab if preview fails --- .../hr/doctype/job_applicant/job_applicant.js | 30 +++++++++++++++++++ .../doctype/job_applicant/job_applicant.json | 24 +++++++++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/hrms/hr/doctype/job_applicant/job_applicant.js b/hrms/hr/doctype/job_applicant/job_applicant.js index 6fb1fb73ac..15ac8db7fa 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.js +++ b/hrms/hr/doctype/job_applicant/job_applicant.js @@ -15,11 +15,41 @@ frappe.ui.form.on("Job Applicant", { }, }; }); + frm.events.show_resume(frm); frm.events.create_custom_buttons(frm); frm.events.get_interview_for_dashboard(frm); frm.toolbar.make_navigation(); }, + show_resume: function (frm) { + if (frm.doc.resume_link) { + const src_url = frappe.utils.escape_html(frm.doc.resume_link); + if (src_url.endsWith(".pdf")) { + frm.toggle_display("resume_preview_html", true); + frm.toggle_display("open_resume_button", false); + frm.events.show_pdf_preview(frm, src_url); + } else { + frm.toggle_display("resume_preview_html", false); + frm.toggle_display("open_resume_button", true); + } + } + }, + show_pdf_preview: function (frm, src_url) { + $preview = $(`
+ + + +
`); + frm.get_field("resume_preview_html").$wrapper.html($preview); + }, + open_resume_button: function (frm) { + window.open(frm.doc.resume_link, "_blank"); + }, create_custom_buttons: function (frm) { if (!frm.doc.__islocal) { if (frm.doc.status == "Open") { diff --git a/hrms/hr/doctype/job_applicant/job_applicant.json b/hrms/hr/doctype/job_applicant/job_applicant.json index fafddba334..33e34a0325 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.json +++ b/hrms/hr/doctype/job_applicant/job_applicant.json @@ -15,17 +15,19 @@ "email_id", "phone_number", "column_break_3", + "job_title", "designation", "country", - "job_title", "column_break_enue", "status", "applicant_rating", "section_break_6", "resume_link", + "resume_preview_html", + "open_resume_button", + "cover_letter", "resume_attachment", "notes", - "cover_letter", "source_and_rating_section", "source", "source_name", @@ -102,7 +104,7 @@ { "fieldname": "resume_attachment", "fieldtype": "Attach", - "label": "Resume Attachment" + "label": "Attachment" }, { "fieldname": "notes", @@ -125,7 +127,7 @@ { "fieldname": "resume_link", "fieldtype": "Data", - "label": "Resume Link" + "label": "Link" }, { "fieldname": "applicant_rating", @@ -194,13 +196,25 @@ "fieldname": "salary_expectation_tab", "fieldtype": "Tab Break", "label": "Salary Expectation" + }, + { + "fieldname": "resume_preview_html", + "fieldtype": "HTML", + "hidden": 1, + "label": "Preview" + }, + { + "fieldname": "open_resume_button", + "fieldtype": "Button", + "hidden": 1, + "label": "Open Resume in New Tab" } ], "icon": "fa fa-user", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-04-07 14:34:24.139833", + "modified": "2026-04-14 11:35:48.142348", "modified_by": "Administrator", "module": "HR", "name": "Job Applicant", From 87a24bdf0ef4cf78e4912dd0b5cd47691f0c8615 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 12:41:01 +0530 Subject: [PATCH 18/29] fix: add job opening template shortcut to sidebar fix: change height of review frame to 1000px --- hrms/hr/doctype/job_applicant/job_applicant.js | 2 +- hrms/workspace_sidebar/recruitment.json | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/hrms/hr/doctype/job_applicant/job_applicant.js b/hrms/hr/doctype/job_applicant/job_applicant.js index 15ac8db7fa..f56b442205 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.js +++ b/hrms/hr/doctype/job_applicant/job_applicant.js @@ -40,7 +40,7 @@ frappe.ui.form.on("Job Applicant", { diff --git a/hrms/workspace_sidebar/recruitment.json b/hrms/workspace_sidebar/recruitment.json index eed5da558e..2b64ff56d1 100644 --- a/hrms/workspace_sidebar/recruitment.json +++ b/hrms/workspace_sidebar/recruitment.json @@ -182,6 +182,17 @@ "show_arrow": 0, "type": "Link" }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Job Opening Template", + "link_to": "Job Opening Template", + "link_type": "DocType", + "show_arrow": 0, + "type": "Link" + }, { "child": 1, "collapsible": 1, @@ -230,7 +241,7 @@ "type": "Link" } ], - "modified": "2026-02-16 12:12:43.728243", + "modified": "2026-04-14 12:19:55.146861", "modified_by": "Administrator", "module": "HR", "name": "Recruitment", From 849d6a2ae0523d68a34db1cd435a47fe319cb657 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 14:59:43 +0530 Subject: [PATCH 19/29] fix: patch rename interview type to interview round --- hrms/patches.txt | 3 ++- .../merge_interview_round_with_interview_type.py | 14 +++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/hrms/patches.txt b/hrms/patches.txt index 5fac4c44ea..c2880c30e7 100644 --- a/hrms/patches.txt +++ b/hrms/patches.txt @@ -1,5 +1,6 @@ [pre_model_sync] hrms.patches.v15_0.check_version_compatibility_with_frappe #2023-06-27 +hrms.patches.v16_0.merge_interview_round_with_interview_type [post_model_sync] hrms.patches.post_install.set_payroll_entry_status @@ -43,4 +44,4 @@ hrms.patches.v16_0.create_holiday_list_assignments hrms.patches.v16_0.set_base_paid_amount_in_employee_advance hrms.patches.v16_0.set_currency_and_base_fields_in_expense_claim hrms.patches.v16_0.remove_ess_user_type_limit -hrms.patches.v16_0.merge_interview_round_with_interview_type + diff --git a/hrms/patches/v16_0/merge_interview_round_with_interview_type.py b/hrms/patches/v16_0/merge_interview_round_with_interview_type.py index dfe14c66ae..af232a857a 100644 --- a/hrms/patches/v16_0/merge_interview_round_with_interview_type.py +++ b/hrms/patches/v16_0/merge_interview_round_with_interview_type.py @@ -1,13 +1,9 @@ import frappe +from frappe.model.rename_doc import rename_doc def execute(): - InterviewType = frappe.qb.DocType("Interview Type") - InterviewRound = frappe.qb.DocType("Interview Round") - - ( - frappe.qb.update(InterviewRound) - .left_join(InterviewType) - .on(InterviewType.name == InterviewRound.interview_type) - .set(InterviewRound.description, InterviewType.description) - ).run() + for interview_round, interview_type in frappe.get_all( + "Interview Round", fields=["name", "interview_type"], as_list=True + ): + rename_doc("Interview Type", interview_type, interview_round) From 202417a4a59161d986a458fba2df6d08a4f4e16d Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 15:46:17 +0530 Subject: [PATCH 20/29] fix: don't patch if interview round is already merged --- .../v16_0/merge_interview_round_with_interview_type.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hrms/patches/v16_0/merge_interview_round_with_interview_type.py b/hrms/patches/v16_0/merge_interview_round_with_interview_type.py index af232a857a..3fb176c8b4 100644 --- a/hrms/patches/v16_0/merge_interview_round_with_interview_type.py +++ b/hrms/patches/v16_0/merge_interview_round_with_interview_type.py @@ -3,7 +3,8 @@ def execute(): - for interview_round, interview_type in frappe.get_all( - "Interview Round", fields=["name", "interview_type"], as_list=True - ): - rename_doc("Interview Type", interview_type, interview_round) + if frappe.db.has_table("Interview Round"): + for interview_round, interview_type in frappe.get_all( + "Interview Round", fields=["name", "interview_type"], as_list=True + ): + rename_doc("Interview Type", interview_type, interview_round) From bca1962a066bee6d577bd28150d1b101c16c16a6 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 15:47:26 +0530 Subject: [PATCH 21/29] chore: typo --- hrms/hr/doctype/job_opening/job_opening.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hrms/hr/doctype/job_opening/job_opening.json b/hrms/hr/doctype/job_opening/job_opening.json index f798a60da6..95729f8777 100644 --- a/hrms/hr/doctype/job_opening/job_opening.json +++ b/hrms/hr/doctype/job_opening/job_opening.json @@ -164,7 +164,7 @@ "description": "Route to the custom Job Application Webform", "fieldname": "job_application_route", "fieldtype": "Data", - "label": "Application Web From Route" + "label": "Application Web Form Route" }, { "default": "0", @@ -275,7 +275,7 @@ "idx": 1, "is_published_field": "publish", "links": [], - "modified": "2026-03-31 12:54:28.114314", + "modified": "2026-04-14 15:47:13.527606", "modified_by": "Administrator", "module": "HR", "name": "Job Opening", From b4111911850c28a8c621ae59c187b8c3c6a3bcae Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 16:28:03 +0530 Subject: [PATCH 22/29] chore: export type checks in doctype --- hrms/hr/doctype/interview/interview.py | 2 +- hrms/hr/doctype/interview_feedback/interview_feedback.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hrms/hr/doctype/interview/interview.py b/hrms/hr/doctype/interview/interview.py index b23e7775f0..7d37720995 100644 --- a/hrms/hr/doctype/interview/interview.py +++ b/hrms/hr/doctype/interview/interview.py @@ -32,8 +32,8 @@ class Interview(Document): expected_average_rating: DF.Rating from_time: DF.Time interview_details: DF.Table[InterviewDetail] - interview_round: DF.Link interview_summary: DF.Text | None + interview_type: DF.Link job_applicant: DF.Link job_opening: DF.Link | None reminded: DF.Check diff --git a/hrms/hr/doctype/interview_feedback/interview_feedback.py b/hrms/hr/doctype/interview_feedback/interview_feedback.py index 1286a57aa7..31220e18ee 100644 --- a/hrms/hr/doctype/interview_feedback/interview_feedback.py +++ b/hrms/hr/doctype/interview_feedback/interview_feedback.py @@ -24,7 +24,7 @@ class InterviewFeedback(Document): average_rating: DF.Rating feedback: DF.Text | None interview: DF.Link - interview_round: DF.Link + interview_type: DF.Link interviewer: DF.Link job_applicant: DF.Link | None result: DF.Literal["", "Cleared", "Rejected"] From d5000db3aaf291a2d13dd33785cdf3f7ecc92f35 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 17:43:33 +0530 Subject: [PATCH 23/29] fix: hide resume preview and button if resume link is removed --- hrms/hr/doctype/job_applicant/job_applicant.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hrms/hr/doctype/job_applicant/job_applicant.js b/hrms/hr/doctype/job_applicant/job_applicant.js index f56b442205..29406aa81c 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.js +++ b/hrms/hr/doctype/job_applicant/job_applicant.js @@ -22,14 +22,14 @@ frappe.ui.form.on("Job Applicant", { }, show_resume: function (frm) { + frm.toggle_display("resume_preview_html", false); + frm.toggle_display("open_resume_button", false); if (frm.doc.resume_link) { const src_url = frappe.utils.escape_html(frm.doc.resume_link); if (src_url.endsWith(".pdf")) { frm.toggle_display("resume_preview_html", true); - frm.toggle_display("open_resume_button", false); frm.events.show_pdf_preview(frm, src_url); } else { - frm.toggle_display("resume_preview_html", false); frm.toggle_display("open_resume_button", true); } } From 0a0220b89e7a67b92cb296a2598ea18d55943c6e Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 17:44:27 +0530 Subject: [PATCH 24/29] chore: correct return typehint --- hrms/hr/doctype/interview/interview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrms/hr/doctype/interview/interview.py b/hrms/hr/doctype/interview/interview.py index 7d37720995..414caf16a4 100644 --- a/hrms/hr/doctype/interview/interview.py +++ b/hrms/hr/doctype/interview/interview.py @@ -155,7 +155,7 @@ def on_discard(self): @frappe.whitelist() -def get_interviewers(interview_type: str) -> list[str]: +def get_interviewers(interview_type: str) -> list[dict]: return frappe.get_all("Interviewer", filters={"parent": interview_type}, fields=["user as interviewer"]) From df9ed347a3afefc9c1688fdf25ff553c032bbbf4 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 18:01:33 +0530 Subject: [PATCH 25/29] fix: correct mapping of fields while making mapped doc --- hrms/hr/doctype/job_opening_template/job_opening_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrms/hr/doctype/job_opening_template/job_opening_template.py b/hrms/hr/doctype/job_opening_template/job_opening_template.py index 994d4a7e1b..a5bb148b17 100644 --- a/hrms/hr/doctype/job_opening_template/job_opening_template.py +++ b/hrms/hr/doctype/job_opening_template/job_opening_template.py @@ -38,7 +38,7 @@ def set_missing_values(source_doc, target_doc): { "Job Opening Template": {"doctype": "Job Opening"}, "field_map": { - "job_opening_template": "name", + "name": "job_opening_template", }, }, target_doc, From 9f0f805f52c84202934d95bcf871ee8e9ff6019a Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 18:04:30 +0530 Subject: [PATCH 26/29] fix: use job title to set_route --- hrms/hr/doctype/job_opening/job_opening.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrms/hr/doctype/job_opening/job_opening.js b/hrms/hr/doctype/job_opening/job_opening.js index 219f50383e..41e04c0e03 100644 --- a/hrms/hr/doctype/job_opening/job_opening.js +++ b/hrms/hr/doctype/job_opening/job_opening.js @@ -72,7 +72,7 @@ frappe.ui.form.on("Job Opening", { }, set_route: function (frm) { if (frm.doc.publish && !frm.doc.route) { - route = `jobs/${frappe.scrub(frm.doc.company)}/${frappe.scrub(frm.doc.designation)}`; + route = `jobs/${frappe.scrub(frm.doc.company)}/${frappe.scrub(frm.doc.job_title)}`; frm.set_value("route", route); } }, From a497440b0997c5e76034fc56749deeb23680e1c0 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Tue, 14 Apr 2026 18:37:43 +0530 Subject: [PATCH 27/29] fix: declare preview locally --- hrms/hr/doctype/job_applicant/job_applicant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrms/hr/doctype/job_applicant/job_applicant.js b/hrms/hr/doctype/job_applicant/job_applicant.js index 29406aa81c..f4864c763d 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.js +++ b/hrms/hr/doctype/job_applicant/job_applicant.js @@ -35,7 +35,7 @@ frappe.ui.form.on("Job Applicant", { } }, show_pdf_preview: function (frm, src_url) { - $preview = $(`
+ let $preview = $(`
Date: Tue, 14 Apr 2026 18:47:51 +0530 Subject: [PATCH 28/29] fix: prevent the newly opened window from accessing the original window --- hrms/hr/doctype/job_applicant/job_applicant.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrms/hr/doctype/job_applicant/job_applicant.js b/hrms/hr/doctype/job_applicant/job_applicant.js index f4864c763d..a19dcbc6cb 100644 --- a/hrms/hr/doctype/job_applicant/job_applicant.js +++ b/hrms/hr/doctype/job_applicant/job_applicant.js @@ -48,7 +48,7 @@ frappe.ui.form.on("Job Applicant", { frm.get_field("resume_preview_html").$wrapper.html($preview); }, open_resume_button: function (frm) { - window.open(frm.doc.resume_link, "_blank"); + window.open(frm.doc.resume_link, "_blank", "noopener"); }, create_custom_buttons: function (frm) { if (!frm.doc.__islocal) { From d65cc00a3b25bd8070823f1cfb30ca9742aa6ca7 Mon Sep 17 00:00:00 2001 From: MochaMind Date: Tue, 14 Apr 2026 21:52:38 +0530 Subject: [PATCH 29/29] fix: Thai translations --- hrms/locale/th.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hrms/locale/th.po b/hrms/locale/th.po index 5b66ade46e..d68a89c4ff 100644 --- a/hrms/locale/th.po +++ b/hrms/locale/th.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: contact@frappe.io\n" "POT-Creation-Date: 2026-04-12 09:46+0000\n" -"PO-Revision-Date: 2026-04-13 16:27\n" +"PO-Revision-Date: 2026-04-14 16:22\n" "Last-Translator: contact@frappe.io\n" "Language-Team: Thai\n" "MIME-Version: 1.0\n" @@ -6319,7 +6319,7 @@ msgstr "สีเขียวมะนาว" #. Label of the limits_tab (Tab Break) field in DocType 'Leave Type' #: hrms/hr/doctype/leave_type/leave_type.json msgid "Limits" -msgstr "" +msgstr "ขีดจำกัด" #. Description of the 'Appraisal Linking' (Section Break) field in DocType #. 'Goal'