From 8a566e6ba5b994bfa8ff1b5a47023d73bd9ad6bd Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 13:19:00 +0530 Subject: [PATCH] fix(postgres): satisfy strict GROUP BY in Sales Pipeline Analytics report Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict GROUP BY. No behaviour change on MariaDB. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../sales_pipeline_analytics.py | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 847e871f61e..48fffca8a26 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -86,10 +86,12 @@ class SalesPipelineAnalytics: if self.filters.get("range") == "Monthly": self.group_by_period = Month(opp.expected_closing) - self.duration = MonthName(opp.expected_closing).as_("month") + self.duration_expr = MonthName(opp.expected_closing) + self.duration = self.duration_expr.as_("month") else: self.group_by_period = Quarter(opp.expected_closing) - self.duration = Quarter(opp.expected_closing).as_("quarter") + self.duration_expr = Quarter(opp.expected_closing) + self.duration = self.duration_expr.as_("quarter") self.pipeline_by = {"Owner": "opportunity_owner", "Sales Stage": "sales_stage"}[ self.filters.get("pipeline_by") @@ -101,27 +103,35 @@ class SalesPipelineAnalytics: self.get_fields() opp = frappe.qb.DocType("Opportunity") - query = frappe.qb.get_query( - "Opportunity", - filters=self.get_conditions(), - ignore_permissions=True, - ) - pipeline_field = opp._assign if self.group_by_based_on == "_assign" else opp.sales_stage if self.filters.get("based_on") == "Number": + # Ask get_query for exactly the grouped columns via `fields`, instead of taking its + # default un-grouped "name" select and stripping it. Group by the displayed period + # expression too, so postgres accepts MonthName alongside the numeric Month used for + # chronological ordering (for Quarterly they're the same expression). self.query_result = ( - query.select( - pipeline_field.as_(self.pipeline_by), - frappe.query_builder.functions.Count("*").as_("count"), - self.duration, + frappe.qb.get_query( + "Opportunity", + filters=self.get_conditions(), + fields=[ + pipeline_field.as_(self.pipeline_by), + frappe.query_builder.functions.Count("*").as_("count"), + self.duration, + ], + ignore_permissions=True, ) - .groupby(pipeline_field, self.group_by_period) + .groupby(pipeline_field, self.group_by_period, self.duration_expr) .orderby(self.group_by_period) .run(as_dict=True) ) if self.filters.get("based_on") == "Amount": + query = frappe.qb.get_query( + "Opportunity", + filters=self.get_conditions(), + ignore_permissions=True, + ) self.query_result = query.select( pipeline_field.as_(self.pipeline_by), opp.opportunity_amount.as_("amount"),