Merge pull request #25944 from ruchamahabal/fix-employee-onboarding-status

fix: boarding status in Employee Onboarding and Separation
This commit is contained in:
Nabin Hait 2021-07-29 15:24:54 +05:30 committed by GitHub
commit ebbabf11e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 248 additions and 245 deletions

View File

@ -0,0 +1,127 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _
from frappe.desk.form import assign_to
from frappe.model.document import Document
from frappe.utils import flt, unique
class EmployeeBoardingController(Document):
'''
Create the project and the task for the boarding process
Assign to the concerned person and roles as per the onboarding/separation template
'''
def validate(self):
# remove the task if linked before submitting the form
if self.amended_from:
for activity in self.activities:
activity.task = ''
def on_submit(self):
# create the project for the given employee onboarding
project_name = _(self.doctype) + ' : '
if self.doctype == 'Employee Onboarding':
project_name += self.job_applicant
else:
project_name += self.employee
project = frappe.get_doc({
'doctype': 'Project',
'project_name': project_name,
'expected_start_date': self.date_of_joining if self.doctype == 'Employee Onboarding' else self.resignation_letter_date,
'department': self.department,
'company': self.company
}).insert(ignore_permissions=True, ignore_mandatory=True)
self.db_set('project', project.name)
self.db_set('boarding_status', 'Pending')
self.reload()
self.create_task_and_notify_user()
def create_task_and_notify_user(self):
# create the task for the given project and assign to the concerned person
for activity in self.activities:
if activity.task:
continue
task = frappe.get_doc({
'doctype': 'Task',
'project': self.project,
'subject': activity.activity_name + ' : ' + self.employee_name,
'description': activity.description,
'department': self.department,
'company': self.company,
'task_weight': activity.task_weight
}).insert(ignore_permissions=True)
activity.db_set('task', task.name)
users = [activity.user] if activity.user else []
if activity.role:
user_list = frappe.db.sql_list('''
SELECT
DISTINCT(has_role.parent)
FROM
`tabHas Role` has_role
LEFT JOIN `tabUser` user
ON has_role.parent = user.name
WHERE
has_role.parenttype = 'User'
AND user.enabled = 1
AND has_role.role = %s
''', activity.role)
users = unique(users + user_list)
if 'Administrator' in users:
users.remove('Administrator')
# assign the task the users
if users:
self.assign_task_to_users(task, users)
def assign_task_to_users(self, task, users):
for user in users:
args = {
'assign_to': [user],
'doctype': task.doctype,
'name': task.name,
'description': task.description or task.subject,
'notify': self.notify_users_by_email
}
assign_to.add(args)
def on_cancel(self):
# delete task project
for task in frappe.get_all('Task', filters={'project': self.project}):
frappe.delete_doc('Task', task.name, force=1)
frappe.delete_doc('Project', self.project, force=1)
self.db_set('project', '')
for activity in self.activities:
activity.db_set('task', '')
@frappe.whitelist()
def get_onboarding_details(parent, parenttype):
return frappe.get_all('Employee Boarding Activity',
fields=['activity_name', 'role', 'user', 'required_for_employee_creation', 'description', 'task_weight'],
filters={'parent': parent, 'parenttype': parenttype},
order_by= 'idx')
def update_employee_boarding_status(project):
employee_onboarding = frappe.db.exists('Employee Onboarding', {'project': project.name})
employee_separation = frappe.db.exists('Employee Separation', {'project': project.name})
if not (employee_onboarding or employee_separation):
return
status = 'Pending'
if flt(project.percent_complete) > 0.0 and flt(project.percent_complete) < 100.0:
status = 'In Process'
elif flt(project.percent_complete) == 100.0:
status = 'Completed'
if employee_onboarding:
frappe.db.set_value('Employee Onboarding', employee_onboarding, 'boarding_status', status)
elif employee_separation:
frappe.db.set_value('Employee Separation', employee_separation, 'boarding_status', status)

View File

@ -50,28 +50,13 @@ frappe.ui.form.on('Employee Onboarding', {
}, __('Create'));
frm.page.set_inner_btn_group_as_primary(__('Create'));
}
if (frm.doc.docstatus === 1 && frm.doc.project) {
frappe.call({
method: "erpnext.hr.utils.get_boarding_status",
args: {
"project": frm.doc.project
},
callback: function(r) {
if (r.message) {
frm.set_value('boarding_status', r.message);
}
refresh_field("boarding_status");
}
});
}
},
employee_onboarding_template: function(frm) {
frm.set_value("activities" ,"");
if (frm.doc.employee_onboarding_template) {
frappe.call({
method: "erpnext.hr.utils.get_onboarding_details",
method: "erpnext.controllers.employee_boarding_controller.get_onboarding_details",
args: {
"parent": frm.doc.employee_onboarding_template,
"parenttype": "Employee Onboarding Template"

View File

@ -30,18 +30,14 @@
"fieldtype": "Link",
"label": "Job Applicant",
"options": "Job Applicant",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "job_offer",
"fieldtype": "Link",
"label": "Job Offer",
"options": "Job Offer",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fetch_from": "job_applicant.applicant_name",
@ -49,116 +45,90 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Employee Name",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "employee",
"fieldtype": "Link",
"label": "Employee",
"options": "Employee",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"fieldname": "date_of_joining",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date of Joining",
"show_days": 1,
"show_seconds": 1
"label": "Date of Joining"
},
{
"allow_on_submit": 1,
"default": "Pending",
"fieldname": "boarding_status",
"fieldtype": "Select",
"label": "Status",
"options": "\nPending\nIn Process\nCompleted",
"show_days": 1,
"show_seconds": 1
"options": "Pending\nIn Process\nCompleted",
"read_only": 1
},
{
"allow_on_submit": 1,
"default": "0",
"fieldname": "notify_users_by_email",
"fieldtype": "Check",
"label": "Notify users by email",
"show_days": 1,
"show_seconds": 1
"label": "Notify users by email"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "employee_onboarding_template",
"fieldtype": "Link",
"label": "Employee Onboarding Template",
"options": "Employee Onboarding Template",
"show_days": 1,
"show_seconds": 1
"options": "Employee Onboarding Template"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"show_days": 1,
"show_seconds": 1
"options": "Company"
},
{
"fieldname": "department",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Department",
"options": "Department",
"show_days": 1,
"show_seconds": 1
"options": "Department"
},
{
"fieldname": "designation",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Designation",
"options": "Designation",
"show_days": 1,
"show_seconds": 1
"options": "Designation"
},
{
"fieldname": "employee_grade",
"fieldtype": "Link",
"label": "Employee Grade",
"options": "Employee Grade",
"show_days": 1,
"show_seconds": 1
"options": "Employee Grade"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"fieldname": "table_for_activity",
"fieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "activities",
"fieldtype": "Table",
"label": "Activities",
"options": "Employee Boarding Activity",
"show_days": 1,
"show_seconds": 1
"options": "Employee Boarding Activity"
},
{
"fieldname": "amended_from",
@ -167,14 +137,12 @@
"no_copy": 1,
"options": "Employee Onboarding",
"print_hide": 1,
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-06-25 15:22:24.923835",
"modified": "2021-06-03 18:01:51.097927",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Onboarding",

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from erpnext.hr.utils import EmployeeBoardingController
from erpnext.controllers.employee_boarding_controller import EmployeeBoardingController
from frappe.model.mapper import get_mapped_doc
class IncompleteTaskError(frappe.ValidationError): pass
@ -16,9 +16,9 @@ class EmployeeOnboarding(EmployeeBoardingController):
self.validate_duplicate_employee_onboarding()
def validate_duplicate_employee_onboarding(self):
emp_onboarding = frappe.db.exists("Employee Onboarding",{"job_applicant": self.job_applicant})
emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant})
if emp_onboarding and emp_onboarding != self.name:
frappe.throw(_("Employee Onboarding: {0} is already for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant)))
frappe.throw(_("Employee Onboarding: {0} already exists for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant)))
def validate_employee_creation(self):
if self.docstatus != 1:
@ -30,7 +30,7 @@ class EmployeeOnboarding(EmployeeBoardingController):
else:
task_status = frappe.db.get_value("Task", activity.task, "status")
if task_status not in ["Completed", "Cancelled"]:
frappe.throw(_("All the mandatory Task for employee creation hasn't been done yet."), IncompleteTaskError)
frappe.throw(_("All the mandatory tasks for employee creation are not completed yet."), IncompleteTaskError)
def on_submit(self):
super(EmployeeOnboarding, self).on_submit()

View File

@ -11,39 +11,26 @@ from erpnext.hr.doctype.employee_onboarding.employee_onboarding import Incomplet
from erpnext.hr.doctype.job_offer.test_job_offer import create_job_offer
class TestEmployeeOnboarding(unittest.TestCase):
def test_employee_onboarding_incomplete_task(self):
def setUp(self):
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
_set_up()
applicant = get_job_applicant()
job_offer = create_job_offer(job_applicant=applicant.name)
job_offer.submit()
project = "Employee Onboarding : Test Researcher - test@researcher.com"
frappe.db.sql("delete from tabProject where name=%s", project)
frappe.db.sql("delete from tabTask where project=%s", project)
onboarding = frappe.new_doc('Employee Onboarding')
onboarding.job_applicant = applicant.name
onboarding.job_offer = job_offer.name
onboarding.company = '_Test Company'
onboarding.designation = 'Researcher'
onboarding.append('activities', {
'activity_name': 'Assign ID Card',
'role': 'HR User',
'required_for_employee_creation': 1
})
onboarding.append('activities', {
'activity_name': 'Assign a laptop',
'role': 'HR User'
})
onboarding.status = 'Pending'
onboarding.insert()
onboarding.submit()
def test_employee_onboarding_incomplete_task(self):
onboarding = create_employee_onboarding()
project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
project_name = frappe.db.get_value('Project', onboarding.project, 'project_name')
self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
# don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
# boarding status
self.assertEqual(onboarding.boarding_status, 'Pending')
# complete the task
project = frappe.get_doc('Project', onboarding.project)
for task in frappe.get_all('Task', dict(project=project.name)):
@ -51,6 +38,10 @@ class TestEmployeeOnboarding(unittest.TestCase):
task.status = 'Completed'
task.save()
# boarding status
onboarding.reload()
self.assertEqual(onboarding.boarding_status, 'Completed')
# make employee
onboarding.reload()
employee = make_employee(onboarding.name)
@ -61,6 +52,13 @@ class TestEmployeeOnboarding(unittest.TestCase):
employee.insert()
self.assertEqual(employee.employee_name, 'Test Researcher')
def tearDown(self):
for entry in frappe.get_all('Employee Onboarding'):
doc = frappe.get_doc('Employee Onboarding', entry.name)
doc.cancel()
doc.delete()
def get_job_applicant():
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
@ -72,10 +70,35 @@ def get_job_applicant():
applicant.insert()
return applicant
def _set_up():
for doctype in ["Employee Onboarding"]:
frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
def get_job_offer(applicant_name):
job_offer = frappe.db.exists('Job Offer', {'job_applicant': applicant_name})
if job_offer:
return frappe.get_doc('Job Offer', job_offer)
project = "Employee Onboarding : Test Researcher - test@researcher.com"
frappe.db.sql("delete from tabProject where name=%s", project)
frappe.db.sql("delete from tabTask where project=%s", project)
job_offer = create_job_offer(job_applicant=applicant_name)
job_offer.submit()
return job_offer
def create_employee_onboarding():
applicant = get_job_applicant()
job_offer = get_job_offer(applicant.name)
onboarding = frappe.new_doc('Employee Onboarding')
onboarding.job_applicant = applicant.name
onboarding.job_offer = job_offer.name
onboarding.company = '_Test Company'
onboarding.designation = 'Researcher'
onboarding.append('activities', {
'activity_name': 'Assign ID Card',
'role': 'HR User',
'required_for_employee_creation': 1
})
onboarding.append('activities', {
'activity_name': 'Assign a laptop',
'role': 'HR User'
})
onboarding.status = 'Pending'
onboarding.insert()
onboarding.submit()
return onboarding

View File

@ -23,27 +23,13 @@ frappe.ui.form.on('Employee Separation', {
frappe.set_route('List', 'Task', {project: frm.doc.project});
},__("View"));
}
if (frm.doc.docstatus === 1 && frm.doc.project) {
frappe.call({
method: "erpnext.hr.utils.get_boarding_status",
args: {
"project": frm.doc.project
},
callback: function(r) {
if (r.message) {
frm.set_value('boarding_status', r.message);
}
refresh_field("boarding_status");
}
});
}
},
employee_separation_template: function(frm) {
frm.set_value("activities" ,"");
if (frm.doc.employee_separation_template) {
frappe.call({
method: "erpnext.hr.utils.get_onboarding_details",
method: "erpnext.controllers.employee_boarding_controller.get_onboarding_details",
args: {
"parent": frm.doc.employee_separation_template,
"parenttype": "Employee Separation Template"

View File

@ -50,11 +50,12 @@
},
{
"allow_on_submit": 1,
"default": "Pending",
"fieldname": "boarding_status",
"fieldtype": "Select",
"label": "Status",
"options": "\nPending\nIn Process\nCompleted",
"reqd": 1
"options": "Pending\nIn Process\nCompleted",
"read_only": 1
},
{
"allow_on_submit": 1,
@ -147,7 +148,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-04-28 15:58:36.020196",
"modified": "2021-06-03 18:02:54.007313",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Separation",

View File

@ -3,7 +3,7 @@
# For license information, please see license.txt
from __future__ import unicode_literals
from erpnext.hr.utils import EmployeeBoardingController
from erpnext.controllers.employee_boarding_controller import EmployeeBoardingController
class EmployeeSeparation(EmployeeBoardingController):
def validate(self):

View File

@ -6,21 +6,43 @@ from __future__ import unicode_literals
import frappe
import unittest
test_dependencies = ["Employee Onboarding"]
test_dependencies = ['Employee Onboarding']
class TestEmployeeSeparation(unittest.TestCase):
def test_employee_separation(self):
employee = frappe.db.get_value("Employee", {"status": "Active"})
separation = frappe.new_doc('Employee Separation')
separation.employee = employee
separation.company = '_Test Company'
separation.append('activities', {
'activity_name': 'Deactivate Employee',
'role': 'HR User'
})
separation.boarding_status = 'Pending'
separation.insert()
separation.submit()
separation = create_employee_separation()
self.assertEqual(separation.docstatus, 1)
self.assertEqual(separation.boarding_status, 'Pending')
project = frappe.get_doc('Project', separation.project)
project.percent_complete_method = 'Manual'
project.status = 'Completed'
project.save()
separation.reload()
self.assertEqual(separation.boarding_status, 'Completed')
separation.cancel()
self.assertEqual(separation.project, "")
self.assertEqual(separation.project, '')
def tearDown(self):
for entry in frappe.get_all('Employee Separation'):
doc = frappe.get_doc('Employee Separation', entry.name)
if doc.docstatus == 1:
doc.cancel()
doc.delete()
def create_employee_separation():
employee = frappe.db.get_value('Employee', {'status': 'Active'})
separation = frappe.new_doc('Employee Separation')
separation.employee = employee
separation.company = '_Test Company'
separation.append('activities', {
'activity_name': 'Deactivate Employee',
'role': 'HR User'
})
separation.boarding_status = 'Pending'
separation.insert()
separation.submit()
return separation

View File

@ -13,118 +13,6 @@ from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate,
class DuplicateDeclarationError(frappe.ValidationError): pass
class EmployeeBoardingController(Document):
'''
Create the project and the task for the boarding process
Assign to the concerned person and roles as per the onboarding/separation template
'''
def validate(self):
# remove the task if linked before submitting the form
if self.amended_from:
for activity in self.activities:
activity.task = ''
def on_submit(self):
# create the project for the given employee onboarding
project_name = _(self.doctype) + " : "
if self.doctype == "Employee Onboarding":
project_name += self.job_applicant
else:
project_name += self.employee
project = frappe.get_doc({
"doctype": "Project",
"project_name": project_name,
"expected_start_date": self.date_of_joining if self.doctype == "Employee Onboarding" else self.resignation_letter_date,
"department": self.department,
"company": self.company
}).insert(ignore_permissions=True, ignore_mandatory=True)
self.db_set("project", project.name)
self.db_set("boarding_status", "Pending")
self.reload()
self.create_task_and_notify_user()
def create_task_and_notify_user(self):
# create the task for the given project and assign to the concerned person
for activity in self.activities:
if activity.task:
continue
task = frappe.get_doc({
"doctype": "Task",
"project": self.project,
"subject": activity.activity_name + " : " + self.employee_name,
"description": activity.description,
"department": self.department,
"company": self.company,
"task_weight": activity.task_weight
}).insert(ignore_permissions=True)
activity.db_set("task", task.name)
users = [activity.user] if activity.user else []
if activity.role:
user_list = frappe.db.sql_list('''
SELECT
DISTINCT(has_role.parent)
FROM
`tabHas Role` has_role
LEFT JOIN `tabUser` user
ON has_role.parent = user.name
WHERE
has_role.parenttype = 'User'
AND user.enabled = 1
AND has_role.role = %s
''', activity.role)
users = unique(users + user_list)
if "Administrator" in users:
users.remove("Administrator")
# assign the task the users
if users:
self.assign_task_to_users(task, users)
def assign_task_to_users(self, task, users):
for user in users:
args = {
'assign_to': [user],
'doctype': task.doctype,
'name': task.name,
'description': task.description or task.subject,
'notify': self.notify_users_by_email
}
assign_to.add(args)
def on_cancel(self):
# delete task project
for task in frappe.get_all("Task", filters={"project": self.project}):
frappe.delete_doc("Task", task.name, force=1)
frappe.delete_doc("Project", self.project, force=1)
self.db_set('project', '')
for activity in self.activities:
activity.db_set("task", "")
@frappe.whitelist()
def get_onboarding_details(parent, parenttype):
return frappe.get_all("Employee Boarding Activity",
fields=["activity_name", "role", "user", "required_for_employee_creation", "description", "task_weight"],
filters={"parent": parent, "parenttype": parenttype},
order_by= "idx")
@frappe.whitelist()
def get_boarding_status(project):
status = 'Pending'
if project:
doc = frappe.get_doc('Project', project)
if flt(doc.percent_complete) > 0.0 and flt(doc.percent_complete) < 100.0:
status = 'In Process'
elif flt(doc.percent_complete) == 100.0:
status = 'Completed'
return status
def set_employee_name(doc):
if doc.employee and not doc.employee_name:
doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name")

View File

@ -14,6 +14,7 @@ from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_e
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from frappe.model.document import Document
from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
from erpnext.controllers.employee_boarding_controller import update_employee_boarding_status
class Project(Document):
def get_feed(self):
@ -37,6 +38,7 @@ class Project(Document):
self.send_welcome_email()
self.update_costing()
self.update_percent_complete()
update_employee_boarding_status(self)
def copy_from_template(self):
'''
@ -132,6 +134,7 @@ class Project(Document):
def update_project(self):
'''Called externally by Task'''
self.update_percent_complete()
update_employee_boarding_status(self)
self.update_costing()
self.db_update()