From e4cda0380aa5dd07f33752b49a3dba15c2e2cf15 Mon Sep 17 00:00:00 2001 From: Ranjith Date: Mon, 7 May 2018 13:54:49 +0530 Subject: [PATCH 1/3] Staffing Plan - validations, get employee count on designation change, calc estimated cost --- .../hr/doctype/staffing_plan/staffing_plan.js | 88 +++++++++- .../doctype/staffing_plan/staffing_plan.json | 5 +- .../hr/doctype/staffing_plan/staffing_plan.py | 47 +++++- .../staffing_plan_detail.json | 156 +++++++++++------- 4 files changed, 228 insertions(+), 68 deletions(-) diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.js b/erpnext/hr/doctype/staffing_plan/staffing_plan.js index 3cadfc56c5..17f03e67bf 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.js +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.js @@ -2,7 +2,91 @@ // For license information, please see license.txt frappe.ui.form.on('Staffing Plan', { - refresh: function(frm) { - + setup: function(frm) { + frm.set_query("designation", "staffing_details", function() { + let designations = []; + $.each(frm.doc.staffing_details, function(index, staff_detail) { + if(staff_detail.designation){ + designations.push(staff_detail.designation) + } + }) + // Filter out designations already selected in Staffing Plan Detail + return { + filters: [ + ['Designation', 'name', 'not in', designations], + ] + } + }); } }); + +frappe.ui.form.on('Staffing Plan Detail', { + designation: function(frm, cdt, cdn) { + let child = locals[cdt][cdn] + if(child.designation){ + frappe.call({ + "method": "erpnext.hr.doctype.staffing_plan.staffing_plan.get_current_employee_count", + args: { + designation: child.designation + }, + callback: function (data) { + if(data.message){ + frappe.model.set_value(cdt, cdn, 'current_count', data.message); + } + else{ // No employees for this designation + frappe.model.set_value(cdt, cdn, 'current_count', 0); + } + } + }); + } + }, + + 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) { + frappe.model.set_value(cdt, cdn, 'vacancies', child.number_of_positions - child.current_count); + } + else{ + frappe.model.set_value(cdt, cdn, 'vacancies', 0); + } + set_total_estimated_cost(frm, cdt, cdn); +} + +// Note: Estimated Cost is calculated on number of Vacancies +var set_total_estimated_cost = function(frm, cdt, cdn) { + let child = locals[cdt][cdn] + if(child.number_of_positions && child.estimated_cost_per_position) { + frappe.model.set_value(cdt, cdn, 'total_estimated_cost', child.vacancies * child.estimated_cost_per_position); + } + else { + frappe.model.set_value(cdt, cdn, 'total_estimated_cost', 0); + } + set_total_estimated_budget(frm); +}; + +var set_total_estimated_budget = function(frm) { + let estimated_budget = 0.0 + if(frm.doc.staffing_details) { + $.each(frm.doc.staffing_details, function(index, staff_detail) { + if(staff_detail.total_estimated_cost){ + estimated_budget += staff_detail.total_estimated_cost + } + }) + frm.set_value('total_estimated_budget', estimated_budget); + } +} diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.json b/erpnext/hr/doctype/staffing_plan/staffing_plan.json index a5d26e6d4f..229cc05ddd 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.json +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.json @@ -268,6 +268,7 @@ "bold": 0, "collapsible": 0, "columns": 0, + "default": "0.00", "fieldname": "total_estimated_budget", "fieldtype": "Currency", "hidden": 0, @@ -284,7 +285,7 @@ "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 0, + "read_only": 1, "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, @@ -335,7 +336,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-04-13 18:45:16.729979", + "modified": "2018-04-18 19:10:34.394249", "modified_by": "Administrator", "module": "HR", "name": "Staffing Plan", diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 510d2dcc49..588e536b42 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -5,6 +5,51 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe import _ +from frappe.utils import getdate, nowdate class StaffingPlan(Document): - pass + def validate(self): + # Validate Dates + 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")) + + # Validate if any submitted Staffing Plan exist for Designations in this plan + # and spd.vacancies>0 ? + for detail in self.get("staffing_details"): + overlap = (frappe.db.sql("""select spd.parent \ + from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name \ + where spd.designation='{0}' and sp.docstatus=1 \ + and sp.to_date >= '{1}' and sp.from_date <='{2}'""".format(detail.designation, self.from_date, self.to_date))) + + if overlap and overlap [0][0]: + frappe.throw(_("Staffing Plan {0} already exist for designation {1}".format(overlap[0][0], detail.designation))) + +@frappe.whitelist() +def get_current_employee_count(designation): + if not designation: + return False + employee_count = frappe.db.sql("""select count(*) from `tabEmployee` where \ + designation = '{0}' and status='Active'""".format(designation))[0][0] + return employee_count + +@frappe.whitelist() +def get_active_staffing_plan_and_vacancies(company, designation, department=None, date=getdate(nowdate())): + if not company or not designation: + frappe.throw(_("Please select Company and Designation")) + + conditions = "spd.designation='{0}' and sp.docstatus=1 and \ + sp.company='{1}'".format(designation, company) + + if(department): #Department is an optional field + conditions += " and sp.department='{0}'".format(department) + + if(date): #ToDo: Date should be mandatory? + conditions += " and '{0}' between sp.from_date and sp.to_date".format(date) + + staffing_plan = frappe.db.sql("""select spd.parent, spd.vacancies \ + from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name + where {0}""".format(conditions)) + + # Only a signle staffing plan can be active for a designation on given date + return staffing_plan[0] if staffing_plan else False diff --git a/erpnext/hr/doctype/staffing_plan_detail/staffing_plan_detail.json b/erpnext/hr/doctype/staffing_plan_detail/staffing_plan_detail.json index 7c395647be..eb77b43914 100644 --- a/erpnext/hr/doctype/staffing_plan_detail/staffing_plan_detail.json +++ b/erpnext/hr/doctype/staffing_plan_detail/staffing_plan_detail.json @@ -75,68 +75,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "current_count", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Current Count", - "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "vacancies", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Vacancies", - "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_on_submit": 0, @@ -190,6 +128,36 @@ "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_5", + "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, @@ -198,6 +166,68 @@ "set_only_once": 0, "translatable": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "current_count", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Current Count", + "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "vacancies", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "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 } ], "has_web_view": 0, @@ -210,7 +240,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-04-13 18:39:52.783341", + "modified": "2018-04-15 16:09:12.622186", "modified_by": "Administrator", "module": "HR", "name": "Staffing Plan Detail", From f220e89d9f9848c318a58543b468a487a9f2d7bb Mon Sep 17 00:00:00 2001 From: Ranjith Date: Mon, 7 May 2018 13:58:54 +0530 Subject: [PATCH 2/3] Job Opening - get planned opening, validate vacancies by Staffing Plan --- erpnext/hr/doctype/job_opening/job_opening.js | 39 +++++++++++++++++++ .../hr/doctype/job_opening/job_opening.json | 35 ++++++++++++++++- erpnext/hr/doctype/job_opening/job_opening.py | 15 +++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/job_opening/job_opening.js b/erpnext/hr/doctype/job_opening/job_opening.js index e69de29bb2..b024310339 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.js +++ b/erpnext/hr/doctype/job_opening/job_opening.js @@ -0,0 +1,39 @@ +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Job Opening', { + designation: function(frm) { + if(frm.doc.designation && frm.doc.company){ + frappe.call({ + "method": "erpnext.hr.doctype.staffing_plan.staffing_plan.get_active_staffing_plan_and_vacancies", + args: { + company: frm.doc.company, + designation: frm.doc.designation, + department: frm.doc.department, + date: frappe.datetime.now_date() // ToDo - Date in Job Opening? + }, + callback: function (data) { + if(data.message){ + frm.set_value('staffing_plan', data.message[0]); + frm.set_value('planned_vacancies', data.message[1]); + } + else{ + frm.set_value('staffing_plan', ""); + frm.set_value('planned_vacancies', 0); + frappe.show_alert({ + indicator: 'orange', + message: __('No Staffing Plans found for this Designation') + }); + } + } + }); + } + else{ + frm.set_value('staffing_plan', ""); + frm.set_value('planned_vacancies', 0); + } + }, + company: function(frm) { + frm.set_value('designation', ""); + } +}); diff --git a/erpnext/hr/doctype/job_opening/job_opening.json b/erpnext/hr/doctype/job_opening/job_opening.json index de15114a43..a877119809 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.json +++ b/erpnext/hr/doctype/job_opening/job_opening.json @@ -224,7 +224,38 @@ "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "planned_vacancies", + "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": "Planned number of Positions", + "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, @@ -369,7 +400,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-04-13 18:52:56.109392", + "modified": "2018-04-18 19:27:15.004385", "modified_by": "Administrator", "module": "HR", "name": "Job Opening", diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py index 60c911a016..d3d662a743 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.py +++ b/erpnext/hr/doctype/job_opening/job_opening.py @@ -8,6 +8,7 @@ import frappe from frappe.website.website_generator import WebsiteGenerator from frappe import _ +from erpnext.hr.doctype.staffing_plan.staffing_plan import get_current_employee_count, get_active_staffing_plan_and_vacancies class JobOpening(WebsiteGenerator): website = frappe._dict( @@ -20,6 +21,20 @@ class JobOpening(WebsiteGenerator): if not self.route: self.route = frappe.scrub(self.job_title).replace('_', '-') + if self.staffing_plan: + self.validate_current_vacancies() + + def validate_current_vacancies(self): + current_count = get_current_employee_count(self.designation) + current_count+= frappe.db.sql("""select count(*) from `tabJob Opening` \ + where designation = '{0}' and status='Open'""".format(self.designation))[0][0] + + vacancies = get_active_staffing_plan_and_vacancies(self.company, self.designation, self.department)[1] + # set staffing_plan too? + if vacancies and vacancies <= current_count: + frappe.throw(_("Job Openings for designation {0} already opened or hiring \ + completed as per Staffing Plan {1}".format(self.designation, self.staffing_plan))) + def get_context(self, context): context.parents = [{'route': 'jobs', 'title': _('All Jobs') }] From c36578e8d34cc3b3f534c2d5071d3560b8f0980d Mon Sep 17 00:00:00 2001 From: Ranjith Date: Mon, 7 May 2018 14:19:40 +0530 Subject: [PATCH 3/3] Job Opening - display vacancies only on staffing plan --- erpnext/hr/doctype/job_opening/job_opening.json | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/doctype/job_opening/job_opening.json b/erpnext/hr/doctype/job_opening/job_opening.json index a877119809..79064390d1 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.json +++ b/erpnext/hr/doctype/job_opening/job_opening.json @@ -41,7 +41,6 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 }, { @@ -73,7 +72,6 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 }, { @@ -104,7 +102,6 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 }, { @@ -134,7 +131,6 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 }, { @@ -166,7 +162,6 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 }, { @@ -198,7 +193,6 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 }, { @@ -230,7 +224,6 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 }, { @@ -239,6 +232,7 @@ "bold": 0, "collapsible": 0, "columns": 0, + "depends_on": "staffing_plan", "fieldname": "planned_vacancies", "fieldtype": "Int", "hidden": 0, @@ -261,7 +255,6 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 }, { @@ -291,7 +284,6 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 }, { @@ -322,7 +314,6 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 }, { @@ -354,7 +345,6 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 1 }, { @@ -385,7 +375,6 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "translatable": 0, "unique": 0 } ], @@ -400,7 +389,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-04-18 19:27:15.004385", + "modified": "2018-05-07 14:16:50.300247", "modified_by": "Administrator", "module": "HR", "name": "Job Opening", @@ -408,6 +397,7 @@ "permissions": [ { "amend": 0, + "apply_user_permissions": 0, "cancel": 0, "create": 1, "delete": 1, @@ -427,6 +417,7 @@ }, { "amend": 0, + "apply_user_permissions": 0, "cancel": 0, "create": 0, "delete": 0,