fix(projects): don't overwrite existing Sales Order project link

Creating a Project with a `sales_order` set used `frappe.db.set_value`
to unconditionally write the Sales Order's `project` field. When several
projects were created for the same Sales Order, each new project silently
overwrote the previous link, leaving earlier projects orphaned.

Only back-link the Sales Order when it is not already tied to another
project, and write the value through the document's `db_set` so the
modified timestamp and realtime update are handled. A warning is shown
when an existing link is left untouched.

Fixes #52179
This commit is contained in:
Nabin Hait
2026-06-14 20:17:08 +05:30
parent c24e9796ae
commit b2eb6a69c1
2 changed files with 41 additions and 2 deletions

View File

@@ -240,8 +240,29 @@ class Project(Document):
def after_insert(self):
self.copy_from_template("after_insert")
if self.sales_order:
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
self.link_with_sales_order()
def link_with_sales_order(self) -> None:
"""Back-link the source Sales Order to this project.
The link is set only when the Sales Order is not already tied to another
project, so projects created concurrently for the same Sales Order cannot
overwrite each other's reference.
"""
if not self.sales_order:
return
sales_order = frappe.get_doc("Sales Order", self.sales_order)
if sales_order.project and sales_order.project != self.name:
frappe.msgprint(
_("Sales Order {0} is already linked to Project {1}, skipping the link.").format(
self.sales_order, sales_order.project
),
alert=True,
)
return
sales_order.db_set("project", self.name)
def on_trash(self):
frappe.db.set_value("Sales Order", {"project": self.name}, "project", "")

View File

@@ -174,6 +174,24 @@ class TestProject(ERPNextTestSuite):
so.reload()
self.assertFalse(so.project)
def test_sales_order_link_is_not_overwritten_by_second_project(self):
so = make_sales_order()
first_project = make_project_from_so(so.name).save()
so.reload()
self.assertEqual(so.project, first_project.name)
# A second project for the same sales order must not steal the link.
second_project = frappe.get_doc(
doctype="Project",
project_name="Second project for same sales order",
sales_order=so.name,
).insert()
self.assertEqual(second_project.sales_order, so.name)
so.reload()
self.assertEqual(so.project, first_project.name)
def test_project_with_template_tasks_having_common_name(self):
# Step - 1: Create Template Parent Tasks
template_parent_task1 = create_task(subject="Parent Task - 1", is_template=1, is_group=1)