diff --git a/erpnext/projects/doctype/service_template/service_template.py b/erpnext/projects/doctype/service_template/service_template.py
index 2fd17fa13641..e1e4fef0a47c 100644
--- a/erpnext/projects/doctype/service_template/service_template.py
+++ b/erpnext/projects/doctype/service_template/service_template.py
@@ -6,6 +6,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, clean_whitespace, cstr
+from erpnext.projects.utils import validate_comma_separated_indices, check_for_circular_dependencies
import json
@@ -15,6 +16,7 @@ def validate(self):
self.validate_duplicate_applicable_item_groups()
self.validate_due_after()
self.validate_service_warranty()
+ self.validate_task_dependencies()
def onload(self):
pass
@@ -86,6 +88,16 @@ def filter_applicable_item(self, pt_item, applies_to_item=None, applies_to_custo
return False
+ def validate_task_dependencies(self):
+ max_idx = max((row.idx for row in self.tasks if row.idx), default=0)
+ dependency_map = {}
+
+ for row in self.tasks:
+ dep_indices = validate_comma_separated_indices(row.depends_on_task, row.idx, max_allowed_idx=max_idx)
+ dependency_map[row.idx] = dep_indices
+
+ check_for_circular_dependencies(dependency_map)
+
@frappe.whitelist()
def get_service_template_details(service_template):
@@ -219,7 +231,7 @@ def get_service_template_tasks(
'service_template_name': service_template_doc.service_template_name,
})
- for template_task_row in service_template_doc.tasks:
+ for idx, template_task_row in enumerate(service_template_doc.tasks):
task_details = frappe._dict()
task_details.subject = template_task_row.subject
task_details.description = template_task_row.description
@@ -228,6 +240,8 @@ def get_service_template_tasks(
task_details.service_template = service_template_detail.service_template
task_details.service_template_detail = service_template_detail.name
task_details.determine_time = template_task_row.determine_time
+ task_details.depends_on_task = template_task_row.depends_on_task or ""
+ task_details.idx = idx + 1
if template_task_row.use_template_name:
task_details.subject = service_template_detail.service_template_name
diff --git a/erpnext/projects/doctype/service_template_task/service_template_task.json b/erpnext/projects/doctype/service_template_task/service_template_task.json
index 7d3963dd19a7..803318a88cad 100644
--- a/erpnext/projects/doctype/service_template_task/service_template_task.json
+++ b/erpnext/projects/doctype/service_template_task/service_template_task.json
@@ -7,6 +7,7 @@
"field_order": [
"subject",
"task_type",
+ "depends_on_task",
"column_break_l9vb",
"expected_time",
"determine_time",
@@ -17,7 +18,7 @@
],
"fields": [
{
- "columns": 6,
+ "columns": 4,
"fieldname": "subject",
"fieldtype": "Data",
"in_list_view": 1,
@@ -69,11 +70,17 @@
"fieldname": "use_template_description",
"fieldtype": "Check",
"label": "Use Template Description"
+ },
+ {
+ "fieldname": "depends_on_task",
+ "fieldtype": "Data",
+ "label": "Depends On Task",
+ "in_list_view": 1
}
],
"istable": 1,
"links": [],
- "modified": "2025-02-01 13:22:34.628230",
+ "modified": "2025-07-07 14:00:59.617902",
"modified_by": "Administrator",
"module": "Projects",
"name": "Service Template Task",
diff --git a/erpnext/projects/doctype/service_template_task/service_template_task.py b/erpnext/projects/doctype/service_template_task/service_template_task.py
index 0e6490906594..eaa119934d9f 100644
--- a/erpnext/projects/doctype/service_template_task/service_template_task.py
+++ b/erpnext/projects/doctype/service_template_task/service_template_task.py
@@ -2,8 +2,24 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-# import frappe
+import frappe
from frappe.model.document import Document
+from frappe import _
class ServiceTemplateTask(Document):
- pass
+ def validate_depends_on_task(self):
+ if not self.depends_on_task:
+ return
+
+ parts = [x.strip() for x in self.depends_on_task.split(',') if x.strip()]
+ for part in parts:
+ if not part.isdigit():
+ frappe.throw(
+ _("Row #{0}: 'Depends On Task' must only contain comma-separated numbers. Found invalid value: {1}").format(
+ self.idx, part))
+
+ invalid_indices = [int(x) for x in parts if int(x) >= self.idx]
+ if invalid_indices:
+ frappe.throw(_("Row #{0}: 'Depends On Task' cannot refer to current or future rows: {1}").format(
+ self.idx, ", ".join(str(x) for x in invalid_indices)
+ ))
diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js
index 36244ba40e77..5b187a5a5abb 100644
--- a/erpnext/projects/doctype/task/task.js
+++ b/erpnext/projects/doctype/task/task.js
@@ -120,6 +120,14 @@ erpnext.projects.TaskController = class TaskController extends frappe.ui.form.Co
}, __("Actions"));
}
+ if (this.get_action_condition("rework_task")) {
+ this.frm.add_custom_button(__("Create Rework Task"), () => {
+ this.frm.check_if_unsaved();
+ return erpnext.task_actions.create_rework_task(this.frm.doc.name, this.frm.doc, null,
+ () => this.frm.reload_doc());
+ }, __("Actions"));
+ }
+
if (this.get_action_condition("cancel_task")) {
this.frm.add_custom_button(__("Cancel"), () => {
this.frm.check_if_unsaved();
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index 1be0c93d013a..e7587f5d9c92 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -20,6 +20,11 @@
"status",
"progress",
"priority",
+ "rework_section",
+ "rework_of",
+ "rework_column_break",
+ "rework_reason",
+ "is_rework_task",
"assignment_section",
"assigned_to",
"assigned_to_name",
@@ -542,6 +547,42 @@
{
"fieldname": "column_break_mwlw",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_rework_task",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Is Rework Task",
+ "read_only": 1
+ },
+ {
+ "depends_on": "is_rework_task",
+ "fieldname": "rework_of",
+ "fieldtype": "Link",
+ "label": "Rework For",
+ "mandatory_depends_on": "is_rework_task",
+ "options": "Task"
+ },
+ {
+ "depends_on": "is_rework_task",
+ "fieldname": "rework_reason",
+ "fieldtype": "Small Text",
+ "label": "Rework Reason",
+ "mandatory_depends_on": "is_rework_task"
+ },
+ {
+ "depends_on": "is_rework_task",
+ "fieldname": "rework_section",
+ "fieldtype": "Section Break",
+ "label": "Rework"
+ },
+ {
+ "fieldname": "rework_column_break",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
}
],
"icon": "fa fa-check",
@@ -549,7 +590,7 @@
"is_calendar_and_gantt": 1,
"is_tree": 1,
"links": [],
- "modified": "2025-07-02 12:29:30.295539",
+ "modified": "2025-07-11 11:25:29.385970",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 5f281be89885..c79441e882fc 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -78,7 +78,7 @@ def validate_after_status(self):
self.validate_project_status()
self.set_time_and_costing()
self.validate_progress()
- self.validate_status_depedency()
+ self.validate_status_dependency()
self.set_completion_values()
self.set_is_overdue()
@@ -194,12 +194,18 @@ def validate_progress(self):
if self.status == 'Completed':
self.progress = 100
- def validate_status_depedency(self):
+ def validate_status_dependency(self):
if self.value_changed("status") and self.status == "Completed":
for d in self.depends_on:
- if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
- frappe.throw(_("Cannot complete task {0} as its dependant {1} is not completed / cancelled.")
- .format(frappe.bold(self.name), frappe.get_desk_link("Task", d.task)))
+ dependency_task = frappe.db.get_value("Task", d.task, ["subject", "status"], as_dict=True)
+ if dependency_task.status not in ("Completed", "Cancelled"):
+ frappe.throw(_(
+ "Cannot complete task {0} because it depends on {1}: {2}, which is not completed or cancelled."
+ ).format(
+ frappe.bold(self.name),
+ frappe.get_desk_link("Task", d.task),
+ frappe.bold(dependency_task.subject)
+ ))
def set_completion_values(self):
if self.value_changed("status") and self.status == "Completed":
@@ -523,7 +529,11 @@ def create_service_template_tasks(project):
template_doc = frappe.get_cached_doc("Service Template", service_template_row.service_template)
template_tasks = get_service_template_tasks(service_template_row.service_template, service_template_detail=service_template_row)
- for template_task_details in template_tasks:
+
+ task_data_list = []
+ task_lookup_by_index = {}
+
+ for idx, template_task_details in enumerate(template_tasks):
task_doc = frappe.new_doc("Task")
for k, v in template_task_details.items():
if task_doc.meta.has_field(k):
@@ -542,6 +552,29 @@ def create_service_template_tasks(project):
task_doc.save()
tasks_created.append(task_doc)
+ task_data_list.append({
+ "row_index": idx + 1,
+ "doc": task_doc,
+ "depends_on_task": template_task_details.get("depends_on_task")
+ })
+ task_lookup_by_index[idx + 1] = task_doc
+
+ for data in task_data_list:
+ task_doc = data["doc"]
+ dependency_indices = [
+ int(x.strip()) for x in (data["depends_on_task"] or "").split(",") if x.strip().isdigit()
+ ]
+ for dependency_idx in dependency_indices:
+ dependency_task = task_lookup_by_index.get(dependency_idx)
+ if dependency_task:
+ task_doc.append("depends_on", {
+ "task": dependency_task.name,
+ "subject": dependency_task.subject,
+ "project": dependency_task.project
+ })
+ if dependency_indices:
+ task_doc.save()
+
if tasks_created:
message = _("{0} Service Template tasks created against {1}