Merge branch 'ESS-LLP-staffing' into enterprise_sprint
diff --git a/erpnext/hr/doctype/job_opening/job_opening.js b/erpnext/hr/doctype/job_opening/job_opening.js
index e69de29..960f5b3 100644
--- a/erpnext/hr/doctype/job_opening/job_opening.js
+++ b/erpnext/hr/doctype/job_opening/job_opening.js
@@ -0,0 +1,38 @@
+// 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 de15114..7906439 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
   }, 
   {
@@ -224,13 +218,43 @@
    "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, 
+   "depends_on": "staffing_plan", 
+   "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, 
+   "search_index": 0, 
+   "set_only_once": 0, 
    "unique": 0
   }, 
   {
@@ -260,7 +284,6 @@
    "reqd": 0, 
    "search_index": 0, 
    "set_only_once": 0, 
-   "translatable": 0, 
    "unique": 0
   }, 
   {
@@ -291,7 +314,6 @@
    "reqd": 0, 
    "search_index": 0, 
    "set_only_once": 0, 
-   "translatable": 0, 
    "unique": 0
   }, 
   {
@@ -323,7 +345,6 @@
    "reqd": 0, 
    "search_index": 0, 
    "set_only_once": 0, 
-   "translatable": 0, 
    "unique": 1
   }, 
   {
@@ -354,7 +375,6 @@
    "reqd": 0, 
    "search_index": 0, 
    "set_only_once": 0, 
-   "translatable": 0, 
    "unique": 0
   }
  ], 
@@ -369,7 +389,7 @@
  "issingle": 0, 
  "istable": 0, 
  "max_attachments": 0, 
- "modified": "2018-04-13 18:52:56.109392", 
+ "modified": "2018-05-07 14:16:50.300247", 
  "modified_by": "Administrator", 
  "module": "HR", 
  "name": "Job Opening", 
@@ -377,6 +397,7 @@
  "permissions": [
   {
    "amend": 0, 
+   "apply_user_permissions": 0, 
    "cancel": 0, 
    "create": 1, 
    "delete": 1, 
@@ -396,6 +417,7 @@
   }, 
   {
    "amend": 0, 
+   "apply_user_permissions": 0, 
    "cancel": 0, 
    "create": 0, 
    "delete": 0, 
diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py
index 60c911a..b579d6f 100644
--- a/erpnext/hr/doctype/job_opening/job_opening.py
+++ b/erpnext/hr/doctype/job_opening/job_opening.py
@@ -8,6 +8,7 @@
 
 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(
@@ -19,6 +20,33 @@
 	def validate(self):
 		if not self.route:
 			self.route = frappe.scrub(self.job_title).replace('_', '-')
+		self.validate_current_vacancies()
+
+	def validate_current_vacancies(self):
+		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
+
+		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 3cadfc5..1c1a720 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.js
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.js
@@ -2,7 +2,92 @@
 // 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(frm.doc.company && child.designation){
+			frappe.call({
+				"method": "erpnext.hr.doctype.staffing_plan.staffing_plan.get_current_employee_count",
+				args: {
+					designation: child.designation,
+					company: frm.doc.company
+				},
+				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 a5d26e6..229cc05 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 510d2dc..37ff5cb 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
@@ -5,6 +5,62 @@
 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, company):
+	if not designation:
+		return False
+
+	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
+
+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 = ""
+	if(department): #Department is an optional field
+		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 sp.name, spd.vacancies
+		from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name
+		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 None
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 7c39564..eb77b43 100644
--- a/erpnext/hr/doctype/staffing_plan_detail/staffing_plan_detail.json
+++ b/erpnext/hr/doctype/staffing_plan_detail/staffing_plan_detail.json
@@ -81,68 +81,6 @@
    "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, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
    "fieldname": "estimated_cost_per_position", 
    "fieldtype": "Currency", 
    "hidden": 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",