Enhancement(HR): hiring process (#18129)

* feat: allow update of vacancies in staffing plan

* feat: update staffing plan on creation of job offer

* feat: update staffing plan after submit

* fix: change staffing plan on creation of employee

* fix: calculate vacancies based on job offers and staffing-plan

* test: job applicant creation

* feat: update job applicant on creation of job offer

* test: staffing plan creation

* fix: number of positions calculation

* test: job offer creation and update

* fix: update status of job applicant on change of job offer

* fix: linting

* fix: set number of positions

* fix: linting

* fix(job-offer): add a more descriptive message

* fix: translations in validation message

* Update validation message
This commit is contained in:
Mangesh-Khairnar 2019-07-22 11:47:53 +05:30 committed by Nabin Hait
parent 305da799a4
commit 270c44a556
11 changed files with 305 additions and 361 deletions

View File

@ -4,4 +4,17 @@ from __future__ import unicode_literals
import frappe import frappe
test_records = frappe.get_test_records('Designation') # test_records = frappe.get_test_records('Designation')
def create_designation(**args):
args = frappe._dict(args)
if frappe.db.exists("Designation", args.designation_name or "_Test designation"):
return frappe.get_doc("Designation", args.designation_name or "_Test designation")
designation = frappe.get_doc({
"doctype": "Designation",
"designation_name": args.designation_name or "_Test designation",
"description": args.description or "_Test description"
})
designation.save()
return designation

View File

@ -12,6 +12,7 @@ from frappe.permissions import add_user_permission, remove_user_permission, \
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail
class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeUserDisabledError(frappe.ValidationError): pass
class EmployeeLeftValidationError(frappe.ValidationError): pass class EmployeeLeftValidationError(frappe.ValidationError): pass

View File

@ -3,6 +3,7 @@
"doctype": "DocType", "doctype": "DocType",
"document_type": "Other", "document_type": "Other",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB",
"field_order": [ "field_order": [
"employee_settings", "employee_settings",
"retirement_age", "retirement_age",
@ -22,7 +23,9 @@
"leave_status_notification_template", "leave_status_notification_template",
"column_break_18", "column_break_18",
"leave_approver_mandatory_in_leave_application", "leave_approver_mandatory_in_leave_application",
"show_leaves_of_all_department_members_in_calendar" "show_leaves_of_all_department_members_in_calendar",
"hiring_settings",
"check_vacancies"
], ],
"fields": [ "fields": [
{ {
@ -44,18 +47,6 @@
"label": "Employee Records to be created by", "label": "Employee Records to be created by",
"options": "Naming Series\nEmployee Number\nFull Name" "options": "Naming Series\nEmployee Number\nFull Name"
}, },
{
"fieldname": "leave_approval_notification_template",
"fieldtype": "Link",
"label": "Leave Approval Notification Template",
"options": "Email Template"
},
{
"fieldname": "leave_status_notification_template",
"fieldtype": "Link",
"label": "Leave Status Notification Template",
"options": "Email Template"
},
{ {
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -67,12 +58,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Stop Birthday Reminders" "label": "Stop Birthday Reminders"
}, },
{
"default": "1",
"fieldname": "leave_approver_mandatory_in_leave_application",
"fieldtype": "Check",
"label": "Leave Approver Mandatory In Leave Application"
},
{ {
"default": "1", "default": "1",
"fieldname": "expense_approver_mandatory_in_expense_claim", "fieldname": "expense_approver_mandatory_in_expense_claim",
@ -91,6 +76,15 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Include holidays in Total no. of Working Days" "label": "Include holidays in Total no. of Working Days"
}, },
{
"fieldname": "max_working_hours_against_timesheet",
"fieldtype": "Float",
"label": "Max working hours against Timesheet"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{ {
"default": "1", "default": "1",
"description": "Emails salary slip to employee based on preferred email selected in Employee", "description": "Emails salary slip to employee based on preferred email selected in Employee",
@ -115,15 +109,33 @@
"label": "Password Policy" "label": "Password Policy"
}, },
{ {
"fieldname": "max_working_hours_against_timesheet", "collapsible": 1,
"fieldtype": "Float",
"label": "Max working hours against Timesheet"
},
{
"fieldname": "leave_settings", "fieldname": "leave_settings",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Leave Settings" "label": "Leave Settings"
}, },
{
"fieldname": "leave_approval_notification_template",
"fieldtype": "Link",
"label": "Leave Approval Notification Template",
"options": "Email Template"
},
{
"fieldname": "leave_status_notification_template",
"fieldtype": "Link",
"label": "Leave Status Notification Template",
"options": "Email Template"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"default": "1",
"fieldname": "leave_approver_mandatory_in_leave_application",
"fieldtype": "Check",
"label": "Leave Approver Mandatory In Leave Application"
},
{ {
"default": "0", "default": "0",
"fieldname": "show_leaves_of_all_department_members_in_calendar", "fieldname": "show_leaves_of_all_department_members_in_calendar",
@ -131,18 +143,22 @@
"label": "Show Leaves Of All Department Members In Calendar" "label": "Show Leaves Of All Department Members In Calendar"
}, },
{ {
"fieldname": "column_break_11", "collapsible": 1,
"fieldtype": "Column Break" "fieldname": "hiring_settings",
"fieldtype": "Section Break",
"label": "Hiring Settings"
}, },
{ {
"fieldname": "column_break_18", "default": "0",
"fieldtype": "Column Break" "fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"modified": "2019-05-31 16:18:50.245872", "modified": "2019-07-01 18:59:55.256878",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR Settings", "name": "HR Settings",
@ -158,5 +174,6 @@
"write": 1 "write": 1
} }
], ],
"sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC"
} }

View File

@ -39,7 +39,7 @@
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0, "remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 1,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0, "translatable": 0,
@ -71,7 +71,7 @@
"read_only": 0, "read_only": 0,
"remember_last_selected_value": 0, "remember_last_selected_value": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 1,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"translatable": 0, "translatable": 0,
@ -96,7 +96,7 @@
"label": "Status", "label": "Status",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"options": "Open\nReplied\nRejected\nHold", "options": "Open\nReplied\nRejected\nHold\nAccepted",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
@ -346,7 +346,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-08-21 16:15:43.552049", "modified": "2019-06-21 16:15:43.552049",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Job Applicant", "name": "Job Applicant",

View File

@ -10,3 +10,14 @@ import unittest
class TestJobApplicant(unittest.TestCase): class TestJobApplicant(unittest.TestCase):
pass pass
def create_job_applicant(**args):
args = frappe._dict(args)
job_applicant = frappe.get_doc({
"doctype": "Job Applicant",
"applicant_name": args.applicant_name or "_Test Applicant",
"email_id": args.email_id or "test_applicant@example.com",
"status": args.status or "Open"
})
job_applicant.save()
return job_applicant

View File

@ -5,12 +5,56 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe import _
from frappe.utils.data import get_link_to_form
class JobOffer(Document): class JobOffer(Document):
def onload(self): def onload(self):
employee = frappe.db.get_value("Employee", {"job_applicant": self.job_applicant}, "name") or "" employee = frappe.db.get_value("Employee", {"job_applicant": self.job_applicant}, "name") or ""
self.set_onload("employee", employee) self.set_onload("employee", employee)
def validate(self):
self.validate_vacancies()
def validate_vacancies(self):
staffing_plan = get_staffing_plan_detail(self.designation, self.company, self.offer_date)
check_vacancies = frappe.get_single("HR Settings").check_vacancies
if staffing_plan and check_vacancies:
vacancies = frappe.db.get_value("Staffing Plan Detail", filters={"name": staffing_plan.name}, fieldname=['vacancies'])
job_offers = len(self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date))
if vacancies - job_offers <= 0:
frappe.throw(_("There are no vacancies under staffing plan {0}").format(get_link_to_form("Staffing Plan", staffing_plan.parent)))
def on_change(self):
update_job_applicant(self.status, self.job_applicant)
def get_job_offer(self, from_date, to_date):
''' Returns job offer created during a time period '''
return frappe.get_all("Job Offer", filters={
"offer_date": ['between', (from_date, to_date)],
"designation": self.designation,
"company": self.company
}, fields=['name'])
def update_job_applicant(status, job_applicant):
if status in ("Accepted", "Rejected"):
frappe.set_value("Job Applicant", job_applicant, "status", status)
def get_staffing_plan_detail(designation, company, offer_date):
detail = frappe.db.sql("""
SELECT spd.name as name,
sp.from_date as from_date,
sp.to_date as to_date,
sp.name as parent
FROM `tabStaffing Plan Detail` spd, `tabStaffing Plan` sp
WHERE
sp.docstatus=1
AND spd.designation=%s
AND sp.company=%s
AND %s between sp.from_date and sp.to_date
""", (designation, company, offer_date), as_dict=1)
return detail[0] if detail else None
@frappe.whitelist() @frappe.whitelist()
def make_employee(source_name, target_doc=None): def make_employee(source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
@ -23,4 +67,3 @@ def make_employee(source_name, target_doc=None):
}} }}
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doc return doc

View File

@ -4,8 +4,78 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import nowdate, add_days
from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
from erpnext.hr.doctype.designation.test_designation import create_designation
from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company
# test_records = frappe.get_test_records('Job Offer') # test_records = frappe.get_test_records('Job Offer')
class TestJobOffer(unittest.TestCase): class TestJobOffer(unittest.TestCase):
pass def test_job_offer_creation_against_vacancies(self):
create_staffing_plan(staffing_details=[{
"designation": "Designer",
"vacancies": 0,
"estimated_cost_per_position": 5000
}])
frappe.db.set_value("HR Settings", None, "check_vacancies", 1)
job_applicant = create_job_applicant(email_id="test_job_offer@example.com")
job_offer = create_job_offer(job_applicant=job_applicant.name, designation="Researcher")
self.assertRaises(frappe.ValidationError, job_offer.submit)
# test creation of job offer when vacancies are not present
frappe.db.set_value("HR Settings", None, "check_vacancies", 0)
job_offer.submit()
self.assertTrue(frappe.db.exists("Job Offer", job_offer.name))
def test_job_applicant_update(self):
create_staffing_plan()
job_applicant = create_job_applicant(email_id="test_job_applicants@example.com")
job_offer = create_job_offer(job_applicant=job_applicant.name)
job_offer.submit()
job_applicant.reload()
self.assertEquals(job_applicant.status, "Accepted")
# status update after rejection
job_offer.status = "Rejected"
job_offer.submit()
job_applicant.reload()
self.assertEquals(job_applicant.status, "Rejected")
def create_job_offer(**args):
args = frappe._dict(args)
if not args.job_applicant:
job_applicant = create_job_applicant()
if not frappe.db.exists("Designation", args.designation):
designation = create_designation(designation_name=args.designation)
job_offer = frappe.get_doc({
"doctype": "Job Offer",
"job_applicant": args.job_applicant or job_applicant.name,
"offer_date": args.offer_date or nowdate(),
"designation": args.designation or "Researcher",
"status": args.status or "Accepted"
})
return job_offer
def create_staffing_plan(**args):
args = frappe._dict(args)
make_company()
frappe.db.set_value("Company", "_Test Company", "is_group", 1)
if frappe.db.exists("Staffing Plan", args.name or "Test"):
return
staffing_plan = frappe.get_doc({
"doctype": "Staffing Plan",
"name": args.name or "Test",
"from_date": args.from_date or nowdate(),
"to_date": args.to_date or add_days(nowdate(), 10),
"staffing_details": args.staffing_details or [{
"designation": "Researcher",
"vacancies": 1,
"estimated_cost_per_position": 50000
}]
})
staffing_plan.insert()
staffing_plan.submit()
return staffing_plan

View File

@ -5,7 +5,7 @@ frappe.ui.form.on('Staffing Plan', {
setup: function(frm) { setup: function(frm) {
frm.set_query("designation", "staffing_details", function() { frm.set_query("designation", "staffing_details", function() {
let designations = []; let designations = [];
$.each(frm.doc.staffing_details, function(index, staff_detail) { (frm.doc.staffing_details || []).forEach(function(staff_detail) {
if(staff_detail.designation){ if(staff_detail.designation){
designations.push(staff_detail.designation) designations.push(staff_detail.designation)
} }
@ -25,13 +25,37 @@ frappe.ui.form.on('Staffing Plan', {
} }
}; };
}); });
} },
}); });
frappe.ui.form.on('Staffing Plan Detail', { frappe.ui.form.on('Staffing Plan Detail', {
designation: function(frm, cdt, cdn) { designation: function(frm, cdt, cdn) {
let child = locals[cdt][cdn] let child = locals[cdt][cdn];
if(frm.doc.company && child.designation){ if(frm.doc.company && child.designation) {
set_number_of_positions(frm, cdt, cdn);
}
},
vacancies: function(frm, cdt, cdn) {
let child = locals[cdt][cdn];
if(child.vacancies < child.current_openings) {
frappe.throw(__("Vacancies cannot be lower than the current openings"));
}
set_number_of_positions(frm, cdt, cdn);
},
current_count: function(frm, cdt, cdn) {
set_number_of_positions(frm, cdt, cdn);
},
estimated_cost_per_position: function(frm, cdt, cdn) {
set_total_estimated_cost(frm, cdt, cdn);
}
});
var set_number_of_positions = function(frm, cdt, cdn) {
let child = locals[cdt][cdn];
if (!child.designation) frappe.throw(__("Please enter the designation"));
frappe.call({ frappe.call({
"method": "erpnext.hr.doctype.staffing_plan.staffing_plan.get_designation_counts", "method": "erpnext.hr.doctype.staffing_plan.staffing_plan.get_designation_counts",
args: { args: {
@ -42,8 +66,9 @@ frappe.ui.form.on('Staffing Plan Detail', {
if(data.message){ if(data.message){
frappe.model.set_value(cdt, cdn, 'current_count', data.message.employee_count); frappe.model.set_value(cdt, cdn, 'current_count', data.message.employee_count);
frappe.model.set_value(cdt, cdn, 'current_openings', data.message.job_openings); frappe.model.set_value(cdt, cdn, 'current_openings', data.message.job_openings);
if (child.number_of_positions < (data.message.employee_count + data.message.job_openings)){ let total_positions = cint(data.message.employee_count) + cint(child.vacancies);
frappe.model.set_value(cdt, cdn, 'number_of_positions', data.message.employee_count + data.message.job_openings); if (cint(child.number_of_positions) < total_positions){
frappe.model.set_value(cdt, cdn, 'number_of_positions', total_positions);
} }
} }
else{ // No employees for this designation else{ // No employees for this designation
@ -52,42 +77,11 @@ frappe.ui.form.on('Staffing Plan Detail', {
} }
} }
}); });
} refresh_field("staffing_details");
},
number_of_positions: function(frm, cdt, cdn) {
set_vacancies(frm, cdt, cdn);
},
current_count: function(frm, cdt, cdn) {
set_vacancies(frm, cdt, cdn);
},
estimated_cost_per_position: function(frm, cdt, cdn) {
let child = locals[cdt][cdn];
set_total_estimated_cost(frm, cdt, cdn);
}
});
var set_vacancies = function(frm, cdt, cdn) {
let child = locals[cdt][cdn]
if (child.number_of_positions < (child.current_count + child.current_openings)){
frappe.throw(__("Number of positions cannot be less then current count of employees"))
}
if(child.number_of_positions > 0) {
frappe.model.set_value(cdt, cdn, 'vacancies', child.number_of_positions - (child.current_count + child.current_openings));
}
else{
frappe.model.set_value(cdt, cdn, 'vacancies', 0);
}
set_total_estimated_cost(frm, cdt, cdn); set_total_estimated_cost(frm, cdt, cdn);
} }
// Note: Estimated Cost is calculated on number of Vacancies // Note: Estimated Cost is calculated on number of Vacancies
// Validate for > 0 ?
var set_total_estimated_cost = function(frm, cdt, cdn) { var set_total_estimated_cost = function(frm, cdt, cdn) {
let child = locals[cdt][cdn] let child = locals[cdt][cdn]
if(child.vacancies > 0 && child.estimated_cost_per_position) { if(child.vacancies > 0 && child.estimated_cost_per_position) {
@ -102,7 +96,7 @@ var set_total_estimated_cost = function(frm, cdt, cdn) {
var set_total_estimated_budget = function(frm) { var set_total_estimated_budget = function(frm) {
let estimated_budget = 0.0 let estimated_budget = 0.0
if(frm.doc.staffing_details) { if(frm.doc.staffing_details) {
$.each(frm.doc.staffing_details, function(index, staff_detail) { (frm.doc.staffing_details || []).forEach(function(staff_detail) {
if(staff_detail.total_estimated_cost){ if(staff_detail.total_estimated_cost){
estimated_budget += staff_detail.total_estimated_cost estimated_budget += staff_detail.total_estimated_cost
} }

View File

@ -13,41 +13,39 @@ class ParentCompanyError(frappe.ValidationError): pass
class StaffingPlan(Document): class StaffingPlan(Document):
def validate(self): def validate(self):
self.validate_period()
self.validate_details()
self.set_total_estimated_budget()
def validate_period(self):
# Validate Dates # Validate Dates
if self.from_date and self.to_date and self.from_date > self.to_date: if self.from_date and self.to_date and self.from_date > self.to_date:
frappe.throw(_("From Date cannot be greater than To Date")) frappe.throw(_("From Date cannot be greater than To Date"))
self.total_estimated_budget = 0 def validate_details(self):
for detail in self.get("staffing_details"): for detail in self.get("staffing_details"):
self.set_vacancies(detail)
self.validate_overlap(detail) self.validate_overlap(detail)
self.validate_with_subsidiary_plans(detail) self.validate_with_subsidiary_plans(detail)
self.validate_with_parent_plan(detail) self.validate_with_parent_plan(detail)
def set_total_estimated_budget(self):
self.total_estimated_budget = 0
for detail in self.get("staffing_details"):
#Set readonly fields #Set readonly fields
self.set_number_of_positions(detail)
designation_counts = get_designation_counts(detail.designation, self.company) designation_counts = get_designation_counts(detail.designation, self.company)
detail.current_count = designation_counts['employee_count'] detail.current_count = designation_counts['employee_count']
detail.current_openings = designation_counts['job_openings'] detail.current_openings = designation_counts['job_openings']
if detail.number_of_positions < (detail.current_count + detail.current_openings): if detail.number_of_positions > 0:
frappe.throw(_("Number of positions cannot be less then current count of employees"))
elif detail.number_of_positions > 0:
detail.vacancies = detail.number_of_positions - (detail.current_count + detail.current_openings)
if detail.vacancies > 0 and detail.estimated_cost_per_position: if detail.vacancies > 0 and detail.estimated_cost_per_position:
detail.total_estimated_cost = detail.vacancies * detail.estimated_cost_per_position detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
else: detail.total_estimated_cost = 0
else: detail.vacancies = detail.number_of_positions = detail.total_estimated_cost = 0
self.total_estimated_budget += detail.total_estimated_cost self.total_estimated_budget += detail.total_estimated_cost
def set_vacancies(self, row): def set_number_of_positions(self, detail):
if not row.vacancies: detail.number_of_positions = cint(detail.vacancies) + cint(detail.current_count)
current_openings = 0
for field in ['current_count', 'current_openings']:
if row.get(field):
current_openings += row.get(field)
row.vacancies = row.number_of_positions - current_openings
def validate_overlap(self, staffing_plan_detail): def validate_overlap(self, staffing_plan_detail):
# Validate if any submitted Staffing Plan exist for any Designations in this plan # Validate if any submitted Staffing Plan exist for any Designations in this plan
@ -132,19 +130,24 @@ def get_designation_counts(designation, company):
if not designation: if not designation:
return False return False
employee_counts_dict = {} employee_counts = {}
lft, rgt = frappe.get_cached_value('Company', company, ["lft", "rgt"]) company_set = get_company_set(company)
employee_counts_dict["employee_count"] = frappe.db.sql("""select count(*) from `tabEmployee`
where designation = %s and status='Active'
and company in (select name from tabCompany where lft>=%s and rgt<=%s)
""", (designation, lft, rgt))[0][0]
employee_counts_dict['job_openings'] = frappe.db.sql("""select count(*) from `tabJob Opening` \ employee_counts["employee_count"] = frappe.db.get_value("Employee",
where designation=%s and status='Open' filters={
and company in (select name from tabCompany where lft>=%s and rgt<=%s) 'designation': designation,
""", (designation, lft, rgt))[0][0] 'status': 'Active',
'company': ('in', company_set)
}, fieldname=['count(name)'])
return employee_counts_dict employee_counts['job_openings'] = frappe.db.get_value("Job Opening",
filters={
'designation': designation,
'status': 'Open',
'company': ('in', company_set)
}, fieldname=['count(name)'])
return employee_counts
@frappe.whitelist() @frappe.whitelist()
def get_active_staffing_plan_details(company, designation, from_date=getdate(nowdate()), to_date=getdate(nowdate())): def get_active_staffing_plan_details(company, designation, from_date=getdate(nowdate()), to_date=getdate(nowdate())):
@ -165,3 +168,13 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now
# Only a single staffing plan can be active for a designation on given date # Only a single staffing plan can be active for a designation on given date
return staffing_plan if staffing_plan else None return staffing_plan if staffing_plan else None
def get_company_set(company):
return frappe.db.sql_list("""
SELECT
name
FROM `tabCompany`
WHERE
parent_company=%(company)s
OR name=%(company)s
""", (dict(company=company)))

View File

@ -24,7 +24,7 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.to_date = add_days(nowdate(), 10) staffing_plan.to_date = add_days(nowdate(), 10)
staffing_plan.append("staffing_details", { staffing_plan.append("staffing_details", {
"designation": "Designer", "designation": "Designer",
"number_of_positions": 6, "vacancies": 6,
"estimated_cost_per_position": 50000 "estimated_cost_per_position": 50000
}) })
staffing_plan.insert() staffing_plan.insert()
@ -42,7 +42,7 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.to_date = add_days(nowdate(), 10) staffing_plan.to_date = add_days(nowdate(), 10)
staffing_plan.append("staffing_details", { staffing_plan.append("staffing_details", {
"designation": "Designer", "designation": "Designer",
"number_of_positions": 3, "vacancies": 3,
"estimated_cost_per_position": 45000 "estimated_cost_per_position": 45000
}) })
self.assertRaises(SubsidiaryCompanyError, staffing_plan.insert) self.assertRaises(SubsidiaryCompanyError, staffing_plan.insert)
@ -58,7 +58,7 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.to_date = add_days(nowdate(), 10) staffing_plan.to_date = add_days(nowdate(), 10)
staffing_plan.append("staffing_details", { staffing_plan.append("staffing_details", {
"designation": "Designer", "designation": "Designer",
"number_of_positions": 7, "vacancies": 7,
"estimated_cost_per_position": 50000 "estimated_cost_per_position": 50000
}) })
staffing_plan.insert() staffing_plan.insert()
@ -73,7 +73,7 @@ class TestStaffingPlan(unittest.TestCase):
staffing_plan.to_date = add_days(nowdate(), 10) staffing_plan.to_date = add_days(nowdate(), 10)
staffing_plan.append("staffing_details", { staffing_plan.append("staffing_details", {
"designation": "Designer", "designation": "Designer",
"number_of_positions": 7, "vacancies": 7,
"estimated_cost_per_position": 60000 "estimated_cost_per_position": 60000
}) })
staffing_plan.insert() staffing_plan.insert()

View File

@ -1,297 +1,79 @@
{ {
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-04-13 18:04:20.978931", "creation": "2018-04-13 18:04:20.978931",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"designation",
"vacancies",
"estimated_cost_per_position",
"total_estimated_cost",
"column_break_5",
"current_count",
"current_openings",
"number_of_positions"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "designation", "fieldname": "designation",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Designation", "label": "Designation",
"length": 0,
"no_copy": 0,
"options": "Designation", "options": "Designation",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "number_of_positions", "fieldname": "number_of_positions",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Number Of Positions", "label": "Number Of Positions",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "estimated_cost_per_position", "fieldname": "estimated_cost_per_position",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Estimated Cost Per Position"
"label": "Estimated Cost Per Position",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "current_count", "fieldname": "current_count",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Current Count", "label": "Current Count",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "current_openings", "fieldname": "current_openings",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Current Openings", "label": "Current Openings",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "vacancies", "fieldname": "vacancies",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Vacancies"
"label": "Vacancies",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_estimated_cost", "fieldname": "total_estimated_cost",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Total Estimated Cost", "label": "Total Estimated Cost",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "modified": "2019-06-24 18:40:37.140178",
"modified": "2018-06-01 17:03:38.020993",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Staffing Plan Detail", "name": "Staffing Plan Detail",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0
} }