Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ca9d4e1
fix: handle zero-value additional salary components to avoid invalid …
iamkhanraheel Apr 13, 2026
eb8d298
test: disable remove_if_zero_valued check in salary component to add …
iamkhanraheel Apr 13, 2026
a8b15d3
fix(Recruitment): better sidebar
asmitahase Feb 16, 2026
cf0b0b4
fix(Recruitment)!: rename and merge interview round with interview type
asmitahase Feb 19, 2026
92527aa
fix(Interview): missing type hints for whilelisted methods
asmitahase Feb 23, 2026
19e8051
fix: autofill job title on selecting designation
asmitahase Mar 30, 2026
e8ff733
fix(Job Opening): split pay details into separate tab
asmitahase Mar 30, 2026
307df2c
fix(Job Opening Template): add "Create Job Opening" button
asmitahase Mar 30, 2026
3eab1f7
fix: automatically set route on clicking publish on website
asmitahase Mar 30, 2026
60cccc0
fix: rename Job Application Route to Application Web Form Route
asmitahase Mar 30, 2026
cb96cf6
fix(Job Opening): Added relevant fields in list view and list view qu…
asmitahase Mar 31, 2026
f8ed6ea
fix(Job Applicant): rename Applicant Name to Full Name
asmitahase Mar 31, 2026
21b424e
fix(Job Applicant): Unnest Action buttons when there's only one
asmitahase Mar 31, 2026
89957a6
fix: added quick action buttons on navbar to Reject and Shortlist app…
asmitahase Apr 7, 2026
b6311df
fix: added navigation buttons to quickly move between applicants
asmitahase Apr 7, 2026
90306fb
fix: hide interview summary section if there's no interview created
asmitahase Apr 12, 2026
649a99b
fix: embed resume in job applicant
asmitahase Apr 14, 2026
87a24bd
fix: add job opening template shortcut to sidebar
asmitahase Apr 14, 2026
849d6a2
fix: patch rename interview type to interview round
asmitahase Apr 14, 2026
202417a
fix: don't patch if interview round is already merged
asmitahase Apr 14, 2026
bca1962
chore: typo
asmitahase Apr 14, 2026
b411191
chore: export type checks in doctype
asmitahase Apr 14, 2026
d5000db
fix: hide resume preview and button if resume link is removed
asmitahase Apr 14, 2026
0a0220b
chore: correct return typehint
asmitahase Apr 14, 2026
df9ed34
fix: correct mapping of fields while making mapped doc
asmitahase Apr 14, 2026
9f0f805
fix: use job title to set_route
asmitahase Apr 14, 2026
a497440
fix: declare preview locally
asmitahase Apr 14, 2026
e7cba79
fix: prevent the newly opened window from accessing the original window
asmitahase Apr 14, 2026
d65cc00
fix: Thai translations
frappe-pr-bot Apr 14, 2026
0a1ef97
Merge pull request #4389 from frappe/l10n_develop
asmitahase Apr 15, 2026
84c7429
Merge pull request #4139 from asmitahase/ux-recruitment
asmitahase Apr 15, 2026
785c72f
Merge pull request #4378 from iamkhanraheel/fix/additional_salary_zer…
asmitahase Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions hrms/hr/doctype/interview/interview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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");
Expand Down
28 changes: 14 additions & 14 deletions hrms/hr/doctype/interview/interview.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"engine": "InnoDB",
"field_order": [
"interview_details_section",
"interview_round",
"interview_type",
"job_applicant",
"job_opening",
"designation",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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": [
{
Expand Down
22 changes: 11 additions & 11 deletions hrms/hr/doctype/interview/interview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -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:
Expand Down Expand Up @@ -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[dict]:
return frappe.get_all("Interviewer", filters={"parent": interview_type}, fields=["user as interviewer"])


def get_recipients(name, for_feedback=0):
Expand Down Expand Up @@ -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):
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"
)


Expand Down Expand Up @@ -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
Expand All @@ -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))
Expand Down
39 changes: 13 additions & 26 deletions hrms/hr/doctype/interview/test_interview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"],
Expand All @@ -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()
Expand All @@ -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):
Expand All @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions hrms/hr/doctype/interview_feedback/interview_feedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
33 changes: 17 additions & 16 deletions hrms/hr/doctype/interview_feedback/interview_feedback.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"field_order": [
"details_section",
"interview",
"interview_round",
"interview_type",
"job_applicant",
"column_break_3",
"interviewer",
Expand All @@ -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",
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -164,9 +164,10 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "interviewer",
"track_changes": 1
}
}
2 changes: 1 addition & 1 deletion hrms/hr/doctype/interview_feedback/interview_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
8 changes: 4 additions & 4 deletions hrms/hr/doctype/interview_feedback/test_interview_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand All @@ -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"
Expand Down Expand Up @@ -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
Empty file.
Loading
Loading