Merge pull request #31130 from ruchamahabal/fix-job-opening

diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py
index c71407d..ce7caa3 100644
--- a/erpnext/hr/doctype/job_opening/job_opening.py
+++ b/erpnext/hr/doctype/job_opening/job_opening.py
@@ -6,6 +6,7 @@
 
 import frappe
 from frappe import _
+from frappe.utils import get_link_to_form
 from frappe.website.website_generator import WebsiteGenerator
 
 from erpnext.hr.doctype.staffing_plan.staffing_plan import (
@@ -33,26 +34,32 @@
 				self.staffing_plan = staffing_plan[0].name
 				self.planned_vacancies = staffing_plan[0].vacancies
 		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 = frappe.db.get_value(
+				"Staffing Plan Detail",
+				{"parent": self.staffing_plan, "designation": self.designation},
+				"vacancies",
 			)
-			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.get_cached_value("Company", staffing_plan_company, ["lft", "rgt"])
 
-			designation_counts = get_designation_counts(self.designation, self.company)
+			designation_counts = get_designation_counts(self.designation, self.company, self.name)
 			current_count = designation_counts["employee_count"] + designation_counts["job_openings"]
 
-			if self.planned_vacancies <= current_count:
+			number_of_positions = frappe.db.get_value(
+				"Staffing Plan Detail",
+				{"parent": self.staffing_plan, "designation": self.designation},
+				"number_of_positions",
+			)
+
+			if number_of_positions <= current_count:
 				frappe.throw(
 					_(
-						"Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}"
-					).format(self.designation, self.staffing_plan)
+						"Job Openings for the designation {0} are already open or the hiring is complete as per the Staffing Plan {1}"
+					).format(
+						frappe.bold(self.designation), get_link_to_form("Staffing Plan", self.staffing_plan)
+					),
+					title=_("Vacancies fulfilled"),
 				)
 
 	def get_context(self, context):
diff --git a/erpnext/hr/doctype/job_opening/test_job_opening.py b/erpnext/hr/doctype/job_opening/test_job_opening.py
index a72a6eb..e991054 100644
--- a/erpnext/hr/doctype/job_opening/test_job_opening.py
+++ b/erpnext/hr/doctype/job_opening/test_job_opening.py
@@ -3,8 +3,77 @@
 
 import unittest
 
-# test_records = frappe.get_test_records('Job Opening')
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company
 
 
-class TestJobOpening(unittest.TestCase):
-	pass
+class TestJobOpening(FrappeTestCase):
+	def setUp(self):
+		frappe.db.delete("Staffing Plan")
+		frappe.db.delete("Staffing Plan Detail")
+		frappe.db.delete("Job Opening")
+
+		make_company("_Test Opening Company", "_TOC")
+		frappe.db.delete("Employee", {"company": "_Test Opening Company"})
+
+	def test_vacancies_fulfilled(self):
+		make_employee(
+			"test_job_opening@example.com", company="_Test Opening Company", designation="Designer"
+		)
+
+		staffing_plan = frappe.get_doc(
+			{
+				"doctype": "Staffing Plan",
+				"company": "_Test Opening Company",
+				"name": "Test",
+				"from_date": getdate(),
+				"to_date": add_days(getdate(), 10),
+			}
+		)
+
+		staffing_plan.append(
+			"staffing_details",
+			{"designation": "Designer", "vacancies": 1, "estimated_cost_per_position": 50000},
+		)
+		staffing_plan.insert()
+		staffing_plan.submit()
+
+		self.assertEqual(staffing_plan.staffing_details[0].number_of_positions, 2)
+
+		# allows creating 1 job opening as per vacancy
+		opening_1 = get_job_opening()
+		opening_1.insert()
+
+		# vacancies as per staffing plan already fulfilled via job opening and existing employee count
+		opening_2 = get_job_opening(job_title="Designer New")
+		self.assertRaises(frappe.ValidationError, opening_2.insert)
+
+		# allows updating existing job opening
+		opening_1.status = "Closed"
+		opening_1.save()
+
+
+def get_job_opening(**args):
+	args = frappe._dict(args)
+
+	opening = frappe.db.exists("Job Opening", {"job_title": args.job_title or "Designer"})
+	if opening:
+		return frappe.get_doc("Job Opening", opening)
+
+	opening = frappe.get_doc(
+		{
+			"doctype": "Job Opening",
+			"job_title": "Designer",
+			"designation": "Designer",
+			"company": "_Test Opening Company",
+			"status": "Open",
+		}
+	)
+
+	opening.update(args)
+
+	return opening
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
index ce7e50f..82472de 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
@@ -172,27 +172,24 @@
 
 
 @frappe.whitelist()
-def get_designation_counts(designation, company):
+def get_designation_counts(designation, company, job_opening=None):
 	if not designation:
 		return False
 
-	employee_counts = {}
 	company_set = get_descendants_of("Company", company)
 	company_set.append(company)
 
-	employee_counts["employee_count"] = frappe.db.get_value(
-		"Employee",
-		filters={"designation": designation, "status": "Active", "company": ("in", company_set)},
-		fieldname=["count(name)"],
+	employee_count = frappe.db.count(
+		"Employee", {"designation": designation, "status": "Active", "company": ("in", company_set)}
 	)
 
-	employee_counts["job_openings"] = frappe.db.get_value(
-		"Job Opening",
-		filters={"designation": designation, "status": "Open", "company": ("in", company_set)},
-		fieldname=["count(name)"],
-	)
+	filters = {"designation": designation, "status": "Open", "company": ("in", company_set)}
+	if job_opening:
+		filters["name"] = ("!=", job_opening)
 
-	return employee_counts
+	job_openings = frappe.db.count("Job Opening", filters)
+
+	return {"employee_count": employee_count, "job_openings": job_openings}
 
 
 @frappe.whitelist()
diff --git a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py
index a3adbbd..ac69c21 100644
--- a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py
@@ -85,13 +85,16 @@
 	make_company()
 
 
-def make_company():
-	if frappe.db.exists("Company", "_Test Company 10"):
+def make_company(name=None, abbr=None):
+	if not name:
+		name = "_Test Company 10"
+
+	if frappe.db.exists("Company", name):
 		return
 
 	company = frappe.new_doc("Company")
-	company.company_name = "_Test Company 10"
-	company.abbr = "_TC10"
+	company.company_name = name
+	company.abbr = abbr or "_TC10"
 	company.parent_company = "_Test Company 3"
 	company.default_currency = "INR"
 	company.country = "Pakistan"