diff --git a/hrms/hooks.py b/hrms/hooks.py index 61f7afc821..f5d2a496ec 100644 --- a/hrms/hooks.py +++ b/hrms/hooks.py @@ -164,6 +164,7 @@ "Employee": { "validate": "hrms.overrides.employee_master.validate_onboarding_process", "on_update": "hrms.overrides.employee_master.update_approver_role", + "after_insert": "hrms.overrides.employee_master.update_job_applicant_and_offer", "on_trash": "hrms.overrides.employee_master.update_employee_transfer", }, "Project": { diff --git a/hrms/hr/doctype/employee_onboarding/employee_onboarding.js b/hrms/hr/doctype/employee_onboarding/employee_onboarding.js index 506e374444..3aa2f75715 100644 --- a/hrms/hr/doctype/employee_onboarding/employee_onboarding.js +++ b/hrms/hr/doctype/employee_onboarding/employee_onboarding.js @@ -44,6 +44,11 @@ frappe.ui.form.on('Employee Onboarding', { }, __('Create')); frm.page.set_inner_btn_group_as_primary(__('Create')); } + if (frm.doc.docstatus === 1 && (frm.doc.boarding_status === "Pending" || frm.doc.boarding_status === "In Process")) { + frm.add_custom_button(__("Mark as Completed"), function() { + frm.trigger("mark_as_completed"); + }); + } }, employee_onboarding_template: function(frm) { @@ -79,5 +84,18 @@ frappe.ui.form.on('Employee Onboarding', { } else { frm.set_value('employee', ''); } - } + }, + + mark_as_completed(frm) { + frm + .call({ + method: "mark_onboarding_as_completed", + doc: frm.doc, + freeze: true, + freeze_message: __("Completing onboarding"), + }) + .then((r) => { + frm.refresh(); + }); + }, }); diff --git a/hrms/hr/doctype/employee_onboarding/employee_onboarding.py b/hrms/hr/doctype/employee_onboarding/employee_onboarding.py index 3fb5a64fee..fa1a257227 100644 --- a/hrms/hr/doctype/employee_onboarding/employee_onboarding.py +++ b/hrms/hr/doctype/employee_onboarding/employee_onboarding.py @@ -58,6 +58,14 @@ def on_update_after_submit(self): def on_cancel(self): super(EmployeeOnboarding, self).on_cancel() + @frappe.whitelist() + def mark_onboarding_as_completed(self): + for activity in self.activities: + frappe.db.set_value("Task", activity.task, "status", "Completed") + frappe.db.set_value("Project", self.project, "status", "Completed") + self.boarding_status = "Completed" + self.save() + @frappe.whitelist() def make_employee(source_name, target_doc=None): diff --git a/hrms/hr/doctype/employee_onboarding/test_employee_onboarding.py b/hrms/hr/doctype/employee_onboarding/test_employee_onboarding.py index fbb8e3f24b..a4d9da3dcd 100644 --- a/hrms/hr/doctype/employee_onboarding/test_employee_onboarding.py +++ b/hrms/hr/doctype/employee_onboarding/test_employee_onboarding.py @@ -11,15 +11,17 @@ ) from hrms.hr.doctype.job_offer.test_job_offer import create_job_offer from hrms.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list +from hrms.tests.test_utils import create_company class TestEmployeeOnboarding(FrappeTestCase): def setUp(self): + create_company() if frappe.db.exists("Employee Onboarding", {"employee_name": "Test Researcher"}): - frappe.delete_doc("Employee Onboarding", {"employee_name": "Test Researcher"}) + frappe.db.sql("delete from `tabEmployee Onboarding` where employee_name=%s", "Test Researcher") project = "Employee Onboarding : test@researcher.com" - frappe.db.sql("delete from tabProject where name=%s", project) + frappe.db.sql("delete from tabProject where project_name=%s", project) frappe.db.sql("delete from tabTask where project=%s", project) def test_employee_onboarding_incomplete_task(self): @@ -70,6 +72,26 @@ def test_employee_onboarding_incomplete_task(self): employee.insert() self.assertEqual(employee.employee_name, "Test Researcher") + def test_mark_onboarding_as_completed(self): + onboarding = create_employee_onboarding() + + # before marking as completed + self.assertEqual(onboarding.boarding_status, "Pending") + project = frappe.get_doc("Project", onboarding.project) + self.assertEqual(project.status, "Open") + for task_status in frappe.get_all("Task", dict(project=project.name), pluck="status"): + self.assertEqual(task_status, "Open") + + onboarding.reload() + onboarding.mark_onboarding_as_completed() + + # after marking as completed + self.assertEqual(onboarding.boarding_status, "Completed") + project.reload() + self.assertEqual(project.status, "Completed") + for task_status in frappe.get_all("Task", dict(project=project.name), pluck="status"): + self.assertEqual(task_status, "Completed") + def tearDown(self): frappe.db.rollback() diff --git a/hrms/hr/doctype/interview/test_interview.py b/hrms/hr/doctype/interview/test_interview.py index 938c2a03d4..ca7c298044 100644 --- a/hrms/hr/doctype/interview/test_interview.py +++ b/hrms/hr/doctype/interview/test_interview.py @@ -14,7 +14,7 @@ from hrms.hr.doctype.interview.interview import DuplicateInterviewRoundError from hrms.hr.doctype.job_applicant.job_applicant import get_interview_details -from hrms.hr.doctype.job_applicant.test_job_applicant import create_job_applicant +from hrms.tests.test_utils import create_job_applicant class TestInterview(FrappeTestCase): diff --git a/hrms/hr/doctype/interview_feedback/test_interview_feedback.py b/hrms/hr/doctype/interview_feedback/test_interview_feedback.py index e8fe8fb956..1fff4c86e2 100644 --- a/hrms/hr/doctype/interview_feedback/test_interview_feedback.py +++ b/hrms/hr/doctype/interview_feedback/test_interview_feedback.py @@ -9,7 +9,7 @@ create_interview_and_dependencies, create_skill_set, ) -from hrms.hr.doctype.job_applicant.test_job_applicant import create_job_applicant +from hrms.tests.test_utils import create_job_applicant class TestInterviewFeedback(FrappeTestCase): diff --git a/hrms/hr/doctype/job_applicant/test_job_applicant.py b/hrms/hr/doctype/job_applicant/test_job_applicant.py index a5fe9797ed..a1869dd5c9 100644 --- a/hrms/hr/doctype/job_applicant/test_job_applicant.py +++ b/hrms/hr/doctype/job_applicant/test_job_applicant.py @@ -3,8 +3,12 @@ import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import nowdate -from erpnext.setup.doctype.designation.test_designation import create_designation +from erpnext.setup.doctype.employee.test_employee import make_employee + +from hrms.hr.doctype.job_offer.test_job_offer import create_job_offer +from hrms.tests.test_utils import create_job_applicant class TestJobApplicant(FrappeTestCase): @@ -29,30 +33,23 @@ def test_job_applicant_naming(self): ).insert() self.assertEqual(applicant.name, "job_applicant_naming@example.com-1") - def tearDown(self): - frappe.db.rollback() - + def test_update_applicant_to_employee(self): + applicant = create_job_applicant() + job_offer = create_job_offer(job_applicant=applicant.name, status="Awaiting Response") + job_offer.save() -def create_job_applicant(**args): - args = frappe._dict(args) + # before creating employee + self.assertEqual(applicant.status, "Open") + self.assertEqual(job_offer.status, "Awaiting Response") - filters = { - "applicant_name": args.applicant_name or "_Test Applicant", - "email_id": args.email_id or "test_applicant@example.com", - } + # create employee + make_employee(user=applicant.name, job_applicant=applicant.name) - if frappe.db.exists("Job Applicant", filters): - return frappe.get_doc("Job Applicant", filters) + # after creating employee + applicant.reload() + self.assertEqual(applicant.status, "Accepted") + job_offer.reload() + self.assertEqual(job_offer.status, "Accepted") - job_applicant = frappe.get_doc( - { - "doctype": "Job Applicant", - "status": args.status or "Open", - "designation": create_designation().name, - } - ) - - job_applicant.update(filters) - job_applicant.save() - - return job_applicant + def tearDown(self): + frappe.db.rollback() diff --git a/hrms/hr/doctype/job_offer/test_job_offer.py b/hrms/hr/doctype/job_offer/test_job_offer.py index 8a88aa6bc6..497d66a41a 100644 --- a/hrms/hr/doctype/job_offer/test_job_offer.py +++ b/hrms/hr/doctype/job_offer/test_job_offer.py @@ -8,9 +8,9 @@ from erpnext.setup.doctype.designation.test_designation import create_designation from hrms.hr.doctype.job_applicant.job_applicant import get_applicant_to_hire_percentage -from hrms.hr.doctype.job_applicant.test_job_applicant import create_job_applicant from hrms.hr.doctype.job_offer.job_offer import get_offer_acceptance_rate from hrms.hr.doctype.staffing_plan.test_staffing_plan import make_company +from hrms.tests.test_utils import create_job_applicant class TestJobOffer(FrappeTestCase): diff --git a/hrms/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py b/hrms/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py index f13fabf06e..f9fec8237b 100644 --- a/hrms/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py +++ b/hrms/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py @@ -45,7 +45,7 @@ def get_employees(filters): holiday_names[holiday.holiday_date] = holiday.description if holidays_list: - cond = " attendance_date in %(holidays_list)s" + cond = " attendance_date in %(holidays_list)s and status not in ('On Leave','Absent') " if filters.holiday_list: cond += ( diff --git a/hrms/overrides/employee_master.py b/hrms/overrides/employee_master.py index e54e174d04..6c4876c462 100644 --- a/hrms/overrides/employee_master.py +++ b/hrms/overrides/employee_master.py @@ -4,7 +4,7 @@ import frappe from frappe import _ from frappe.model.naming import set_name_by_naming_series -from frappe.utils import add_years, cint, getdate +from frappe.utils import add_years, cint, get_link_to_form, getdate from erpnext.setup.doctype.employee.employee import Employee @@ -45,6 +45,40 @@ def validate_onboarding_process(doc, method=None): onboarding.db_set("employee", doc.name) +def update_job_applicant_and_offer(doc, method=None): + """Updates Job Applicant and Job Offer status as 'Accepted' and submits them""" + if not doc.job_applicant: + return + + applicant_status_before_change = frappe.db.get_value("Job Applicant", doc.job_applicant, "status") + if applicant_status_before_change != "Accepted": + frappe.db.set_value("Job Applicant", doc.job_applicant, "status", "Accepted") + frappe.msgprint( + _("Updated the status of linked Job Applicant {0} to {1}").format( + get_link_to_form("Job Applicant", doc.job_applicant), frappe.bold(_("Accepted")) + ) + ) + offer_status_before_change = frappe.db.get_value( + "Job Offer", {"job_applicant": doc.job_applicant, "docstatus": ["!=", 2]}, "status" + ) + if offer_status_before_change and offer_status_before_change != "Accepted": + job_offer = frappe.get_last_doc("Job Offer", filters={"job_applicant": doc.job_applicant}) + job_offer.status = "Accepted" + job_offer.flags.ignore_mandatory = True + job_offer.flags.ignore_permissions = True + job_offer.save() + + msg = _("Updated the status of Job Offer {0} for the linked Job Applicant {1} to {2}").format( + get_link_to_form("Job Offer", job_offer.name), + frappe.bold(doc.job_applicant), + frappe.bold(_("Accepted")), + ) + if job_offer.docstatus == 0: + msg += "
" + _("You may add additional details, if any, and submit the offer.") + + frappe.msgprint(msg) + + def update_approver_role(doc, method=None): """Adds relevant approver role for the user linked to Employee""" if doc.leave_approver: diff --git a/hrms/tests/test_utils.py b/hrms/tests/test_utils.py index b68f172d67..3d18006cdd 100644 --- a/hrms/tests/test_utils.py +++ b/hrms/tests/test_utils.py @@ -2,6 +2,7 @@ from frappe.utils import add_months, get_first_day, get_last_day, getdate, now_datetime from erpnext.setup.doctype.department.department import get_abbreviated_name +from erpnext.setup.doctype.designation.test_designation import create_designation from erpnext.setup.utils import enable_all_roles_and_domains @@ -92,3 +93,25 @@ def create_department(name: str, company: str = "_Test Company") -> str: department.update({"doctype": "Department", "department_name": name, "company": "_Test Company"}) department.insert() return department.name + + +def create_job_applicant(**args): + args = frappe._dict(args) + filters = { + "applicant_name": args.applicant_name or "_Test Applicant", + "email_id": args.email_id or "test_applicant@example.com", + } + + if frappe.db.exists("Job Applicant", filters): + return frappe.get_doc("Job Applicant", filters) + + job_applicant = frappe.get_doc( + { + "doctype": "Job Applicant", + "status": args.status or "Open", + "designation": create_designation().name, + } + ) + job_applicant.update(filters) + job_applicant.save() + return job_applicant