From e4cda0380aa5dd07f33752b49a3dba15c2e2cf15 Mon Sep 17 00:00:00 2001 From: Ranjith Date: Mon, 7 May 2018 13:54:49 +0530 Subject: [PATCH 1/4] 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 3cadfc56c51..17f03e67bfa 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 a5d26e6d4f5..229cc05ddd5 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 510d2dcc498..588e536b42f 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 7c395647bed..eb77b43914a 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/4] 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 e69de29bb2d..b0243103391 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 de15114a43c..a8771198090 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 60c911a0161..d3d662a743c 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/4] 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 a8771198090..79064390d1a 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, From 3881e98000cdf69f72f5d0c2e190b51b3a7ad06c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 8 May 2018 18:36:14 +0530 Subject: [PATCH 4/4] Staffing plan for group company --- erpnext/hr/doctype/job_opening/job_opening.js | 45 +++++++++---------- erpnext/hr/doctype/job_opening/job_opening.py | 35 ++++++++++----- .../hr/doctype/staffing_plan/staffing_plan.js | 5 ++- .../hr/doctype/staffing_plan/staffing_plan.py | 37 +++++++++------ 4 files changed, 73 insertions(+), 49 deletions(-) diff --git a/erpnext/hr/doctype/job_opening/job_opening.js b/erpnext/hr/doctype/job_opening/job_opening.js index b0243103391..960f5b3c650 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.js +++ b/erpnext/hr/doctype/job_opening/job_opening.js @@ -2,38 +2,37 @@ // For license information, please see license.txt frappe.ui.form.on('Job Opening', { - designation: function(frm) { - if(frm.doc.designation && frm.doc.company){ + 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? + 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]); + 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); - 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', ""); - } + 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.py b/erpnext/hr/doctype/job_opening/job_opening.py index d3d662a743c..b579d6f24a1 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.py +++ b/erpnext/hr/doctype/job_opening/job_opening.py @@ -20,20 +20,33 @@ class JobOpening(WebsiteGenerator): def validate(self): if not self.route: self.route = frappe.scrub(self.job_title).replace('_', '-') - - if self.staffing_plan: - self.validate_current_vacancies() + 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] + if not self.staffing_plan: + vacancies = get_active_staffing_plan_and_vacancies(self.company, + self.designation, self.department) + if vacancies: + self.staffing_plan = vacancies[0] + self.planned_vacancies = vacancies[1] + elif not self.planned_vacancies: + planned_vacancies = frappe.db.sql(""" + select vacancies from `tabStaffing Plan Detail` + where parent=%s and designation=%s""", (self.staffing_plan, self.designation)) + self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None - 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))) + if self.staffing_plan and self.planned_vacancies: + staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company") + lft, rgt = frappe.db.get_value("Company", staffing_plan_company, ["lft", "rgt"]) + + current_count = get_current_employee_count(self.designation, staffing_plan_company) + current_count+= frappe.db.sql("""select count(*) from `tabJob Opening` \ + where designation=%s and status='Open' + and company in (select name from tabCompany where lft>=%s and rgt<=%s) + """, (self.designation, lft, rgt))[0][0] + + if self.planned_vacancies <= current_count: + frappe.throw(_("Job Openings for designation {0} and company {1} already opened or hiring completed as per Staffing Plan {2}".format(self.designation, staffing_plan_company, self.staffing_plan))) def get_context(self, context): context.parents = [{'route': 'jobs', 'title': _('All Jobs') }] diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.js b/erpnext/hr/doctype/staffing_plan/staffing_plan.js index 17f03e67bfa..1c1a7200886 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.js +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.js @@ -23,11 +23,12 @@ frappe.ui.form.on('Staffing Plan', { frappe.ui.form.on('Staffing Plan Detail', { designation: function(frm, cdt, cdn) { let child = locals[cdt][cdn] - if(child.designation){ + if(frm.doc.company && child.designation){ frappe.call({ "method": "erpnext.hr.doctype.staffing_plan.staffing_plan.get_current_employee_count", args: { - designation: child.designation + designation: child.designation, + company: frm.doc.company }, callback: function (data) { if(data.message){ diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 588e536b42f..37ff5cbc908 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -20,36 +20,47 @@ class StaffingPlan(Document): 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))) + 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.throw(_("Staffing Plan {0} already exist for designation {1}" + .format(overlap[0][0], detail.designation))) @frappe.whitelist() -def get_current_employee_count(designation): +def get_current_employee_count(designation, company): if not designation: return False - employee_count = frappe.db.sql("""select count(*) from `tabEmployee` where \ - designation = '{0}' and status='Active'""".format(designation))[0][0] + + lft, rgt = frappe.db.get_value("Company", company, ["lft", "rgt"]) + 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] 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) - + conditions = "" if(department): #Department is an optional field - conditions += " and sp.department='{0}'".format(department) + conditions += " and sp.department='{0}'".format(frappe.db.escape(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 \ + staffing_plan = frappe.db.sql(""" + select sp.name, spd.vacancies from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name - where {0}""".format(conditions)) + where company=%s and spd.designation=%s and sp.docstatus=1 {0} + """.format(conditions), (company, designation)) + + if not staffing_plan: + parent_company = frappe.db.get_value("Company", company, "parent_company") + if parent_company: + staffing_plan = get_active_staffing_plan_and_vacancies(parent_company, + designation, department, date) # Only a signle staffing plan can be active for a designation on given date - return staffing_plan[0] if staffing_plan else False + return staffing_plan[0] if staffing_plan else None