From a48317769064911d8bc315789f4baac0fb689555 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 21 Jun 2026 09:44:48 +0530 Subject: [PATCH] fix(projects): make Project timeline GROUP BY Postgres-valid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_timeline_data grouped Timesheet Detail by Date(from_time) but selected UnixTimestamp(from_time) (the full timestamp, ungrouped). MariaDB arbitrary-picks a row's timestamp; Postgres rejects it ("must appear in the GROUP BY clause"), so the Project timeline (calendar heatmap) is broken on PG. Select UnixTimestamp(Date(from_time)) — the day's epoch — which is the timeline key and matches the GROUP BY. CurDate() - Interval(years=1) is portable and kept as-is. Adds a test (no coverage existed) that records a timesheet against a project and asserts get_timeline_data returns day-bucketed counts, on both engines. Co-Authored-By: Claude Opus 4.8 (1M context) --- erpnext/projects/doctype/project/project.py | 4 +++- erpnext/projects/doctype/project/test_project.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 7be1c3a2b6e..f5f06685f53 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -441,8 +441,10 @@ def get_timeline_data(doctype: str, name: str) -> dict[int, int]: timesheet_detail = frappe.qb.DocType("Timesheet Detail") return dict( + # select the same day-bucket expression that is grouped on (postgres rejects selecting the + # ungrouped from_time); UnixTimestamp(Date(...)) is the day's epoch, which is the timeline key. frappe.qb.from_(timesheet_detail) - .select(UnixTimestamp(timesheet_detail.from_time), Count("*")) + .select(UnixTimestamp(Date(timesheet_detail.from_time)), Count("*")) .where(timesheet_detail.project == name) .where(timesheet_detail.from_time > CurDate() - Interval(years=1)) .where(timesheet_detail.docstatus < 2) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 3ece3ea36c2..4f284860e3e 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -12,6 +12,21 @@ from erpnext.tests.utils import ERPNextTestSuite class TestProject(ERPNextTestSuite): + def test_get_timeline_data_runs(self): + # get_timeline_data groups Timesheet Detail by Date(from_time); the selected day key must be the + # same grouped expression (UnixTimestamp(Date(from_time))) to be valid on Postgres. + from erpnext.projects.doctype.project.project import get_timeline_data + from erpnext.projects.doctype.timesheet.test_timesheet import make_timesheet + from erpnext.setup.doctype.employee.test_employee import make_employee + + project = make_project({"project_name": "_Test Timeline Project", "company": "_Test Company"}) + emp = make_employee("test_timeline@example.com", company="_Test Company") + make_timesheet(emp, simulate=True, project=project.name) + + data = get_timeline_data("Project", project.name) + self.assertIsInstance(data, dict) + self.assertGreaterEqual(sum(data.values()), 1) + def test_project_total_costing_and_billing_amount(self): from erpnext.projects.doctype.timesheet.test_timesheet import make_timesheet from erpnext.setup.doctype.employee.test_employee import make_employee