fix: capacity planning issue in the job card (#40092)

* fix: capacity planning issue in the job card

* test: test case to test capacity planning for workstation
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 3daec20..35aebb9 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -239,12 +239,12 @@
 		for row in self.sub_operations:
 			self.total_completed_qty += row.completed_qty
 
-	def get_overlap_for(self, args, check_next_available_slot=False):
+	def get_overlap_for(self, args):
 		time_logs = []
 
-		time_logs.extend(self.get_time_logs(args, "Job Card Time Log", check_next_available_slot))
+		time_logs.extend(self.get_time_logs(args, "Job Card Time Log"))
 
-		time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time", check_next_available_slot))
+		time_logs.extend(self.get_time_logs(args, "Job Card Scheduled Time"))
 
 		if not time_logs:
 			return {}
@@ -269,7 +269,7 @@
 				self.workstation = workstation_time.get("workstation")
 				return workstation_time
 
-		return time_logs[-1]
+		return time_logs[0]
 
 	def has_overlap(self, production_capacity, time_logs):
 		overlap = False
@@ -308,7 +308,7 @@
 			return True
 		return overlap
 
-	def get_time_logs(self, args, doctype, check_next_available_slot=False):
+	def get_time_logs(self, args, doctype):
 		jc = frappe.qb.DocType("Job Card")
 		jctl = frappe.qb.DocType(doctype)
 
@@ -318,9 +318,6 @@
 			((jctl.from_time >= args.from_time) & (jctl.to_time <= args.to_time)),
 		]
 
-		if check_next_available_slot:
-			time_conditions.append(((jctl.from_time >= args.from_time) & (jctl.to_time >= args.to_time)))
-
 		query = (
 			frappe.qb.from_(jctl)
 			.from_(jc)
@@ -395,18 +392,28 @@
 
 	def validate_overlap_for_workstation(self, args, row):
 		# get the last record based on the to time from the job card
-		data = self.get_overlap_for(args, check_next_available_slot=True)
+		data = self.get_overlap_for(args)
+
 		if not self.workstation:
 			workstations = get_workstations(self.workstation_type)
 			if workstations:
 				# Get the first workstation
 				self.workstation = workstations[0]
 
+		if not data:
+			row.planned_start_time = args.from_time
+			return
+
 		if data:
 			if data.get("planned_start_time"):
-				row.planned_start_time = get_datetime(data.planned_start_time)
+				args.planned_start_time = get_datetime(data.planned_start_time)
 			else:
-				row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
+				args.planned_start_time = get_datetime(data.to_time + get_mins_between_operations())
+
+			args.from_time = args.planned_start_time
+			args.to_time = add_to_date(args.planned_start_time, minutes=row.remaining_time_in_mins)
+
+			self.validate_overlap_for_workstation(args, row)
 
 	def check_workstation_time(self, row):
 		workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index efe9f53..c72232a 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1822,6 +1822,113 @@
 		valuation_rate = sum([item.valuation_rate * item.transfer_qty for item in mce.items]) / 10
 		self.assertEqual(me.items[0].valuation_rate, valuation_rate)
 
+	def test_capcity_planning_for_workstation(self):
+		frappe.db.set_single_value(
+			"Manufacturing Settings",
+			{
+				"disable_capacity_planning": 0,
+				"capacity_planning_for_days": 1,
+				"mins_between_operations": 10,
+			},
+		)
+
+		properties = {"is_stock_item": 1, "valuation_rate": 100}
+		fg_item = make_item("Test FG Item For Capacity Planning", properties).name
+
+		rm_item = make_item("Test RM Item For Capacity Planning", properties).name
+
+		workstation = "Test Workstation For Capacity Planning"
+		if not frappe.db.exists("Workstation", workstation):
+			make_workstation(workstation=workstation, production_capacity=1)
+
+		operation = "Test Operation For Capacity Planning"
+		if not frappe.db.exists("Operation", operation):
+			make_operation(operation=operation, workstation=workstation)
+
+		bom_doc = make_bom(
+			item=fg_item,
+			source_warehouse="Stores - _TC",
+			raw_materials=[rm_item],
+			with_operations=1,
+			do_not_submit=True,
+		)
+
+		bom_doc.append(
+			"operations",
+			{"operation": operation, "time_in_mins": 1420, "hour_rate": 100, "workstation": workstation},
+		)
+		bom_doc.submit()
+
+		# 1st Work Order,
+		# Capacity to run parallel the operation 'Test Operation For Capacity Planning' is 2
+		wo_doc = make_wo_order_test_record(
+			production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
+		)
+
+		wo_doc.submit()
+		job_cards = frappe.get_all(
+			"Job Card",
+			filters={"work_order": wo_doc.name},
+		)
+
+		self.assertEqual(len(job_cards), 1)
+
+		# 2nd Work Order,
+		wo_doc = make_wo_order_test_record(
+			production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
+		)
+
+		wo_doc.submit()
+		job_cards = frappe.get_all(
+			"Job Card",
+			filters={"work_order": wo_doc.name},
+		)
+
+		self.assertEqual(len(job_cards), 1)
+
+		# 3rd Work Order, capacity is full
+		wo_doc = make_wo_order_test_record(
+			production_item=fg_item, qty=1, planned_start_date="2024-02-25 00:00:00", do_not_submit=1
+		)
+
+		self.assertRaises(CapacityError, wo_doc.submit)
+
+		frappe.db.set_single_value(
+			"Manufacturing Settings", {"disable_capacity_planning": 1, "mins_between_operations": 0}
+		)
+
+
+def make_operation(**kwargs):
+	kwargs = frappe._dict(kwargs)
+
+	operation_doc = frappe.get_doc(
+		{
+			"doctype": "Operation",
+			"name": kwargs.operation,
+			"workstation": kwargs.workstation,
+		}
+	)
+	operation_doc.insert()
+
+	return operation_doc
+
+
+def make_workstation(**kwargs):
+	kwargs = frappe._dict(kwargs)
+
+	workstation_doc = frappe.get_doc(
+		{
+			"doctype": "Workstation",
+			"workstation_name": kwargs.workstation,
+			"workstation_type": kwargs.workstation_type,
+			"production_capacity": kwargs.production_capacity or 0,
+			"hour_rate": kwargs.hour_rate or 100,
+		}
+	)
+	workstation_doc.insert()
+
+	return workstation_doc
+
 
 def prepare_boms_for_sub_assembly_test():
 	if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 39beb36..5e22707 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -242,8 +242,12 @@
 	def calculate_operating_cost(self):
 		self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0
 		for d in self.get("operations"):
-			d.planned_operating_cost = flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0)
-			d.actual_operating_cost = flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0)
+			d.planned_operating_cost = flt(
+				flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0), d.precision("planned_operating_cost")
+			)
+			d.actual_operating_cost = flt(
+				flt(d.hour_rate) * (flt(d.actual_operation_time) / 60.0), d.precision("actual_operating_cost")
+			)
 
 			self.planned_operating_cost += flt(d.planned_operating_cost)
 			self.actual_operating_cost += flt(d.actual_operating_cost)
@@ -588,7 +592,6 @@
 	def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
 		self.set_operation_start_end_time(index, row)
 
-		original_start_time = row.planned_start_time
 		job_card_doc = create_job_card(
 			self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning
 		)
@@ -597,11 +600,15 @@
 			row.planned_start_time = job_card_doc.scheduled_time_logs[-1].from_time
 			row.planned_end_time = job_card_doc.scheduled_time_logs[-1].to_time
 
-			if date_diff(row.planned_start_time, original_start_time) > plan_days:
+			if date_diff(row.planned_end_time, self.planned_start_date) > plan_days:
 				frappe.message_log.pop()
 				frappe.throw(
-					_("Unable to find the time slot in the next {0} days for the operation {1}.").format(
-						plan_days, row.operation
+					_(
+						"Unable to find the time slot in the next {0} days for the operation {1}. Please increase the 'Capacity Planning For (Days)' in the {2}."
+					).format(
+						plan_days,
+						row.operation,
+						get_link_to_form("Manufacturing Settings", "Manufacturing Settings"),
 					),
 					CapacityError,
 				)