From 0b795a628f6b969d0caf4285167e1f1b05dee71a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 15:19:16 +0530 Subject: [PATCH] fix(postgres): use portable GroupConcat in Lost Opportunity report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../lost_opportunity/lost_opportunity.py | 8 ++++--- .../lost_opportunity/test_lost_opportunity.py | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 erpnext/crm/report/lost_opportunity/test_lost_opportunity.py diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index cfbee3901e2..03dc634589a 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -5,8 +5,7 @@ import frappe from frappe import _ from frappe.query_builder import DocType -from frappe.query_builder.custom import GROUP_CONCAT -from frappe.query_builder.functions import Date +from frappe.query_builder.functions import Date, GroupConcat Opportunity = DocType("Opportunity") OpportunityLostReasonDetail = DocType("Opportunity Lost Reason Detail") @@ -72,6 +71,9 @@ def get_columns(): def get_data(filters): + # db-aware GROUP_CONCAT (MariaDB) / STRING_AGG (postgres) with a ", " separator + lost_reasons = GroupConcat(OpportunityLostReasonDetail.lost_reason, ", ", alias="lost_reason") + query = ( frappe.qb.from_(Opportunity) .left_join(OpportunityLostReasonDetail) @@ -85,7 +87,7 @@ def get_data(filters): Opportunity.party_name, Opportunity.customer_name, Opportunity.opportunity_type, - GROUP_CONCAT(OpportunityLostReasonDetail.lost_reason, alias="lost_reason").separator(", "), + lost_reasons, Opportunity.sales_stage, Opportunity.territory, ) diff --git a/erpnext/crm/report/lost_opportunity/test_lost_opportunity.py b/erpnext/crm/report/lost_opportunity/test_lost_opportunity.py new file mode 100644 index 00000000000..3183f45cf87 --- /dev/null +++ b/erpnext/crm/report/lost_opportunity/test_lost_opportunity.py @@ -0,0 +1,22 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.utils import add_days, today + +from erpnext.crm.report.lost_opportunity.lost_opportunity import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestLostOpportunity(ERPNextTestSuite): + def test_report_aggregates_lost_reasons(self): + # Exercises the db-aware GROUP_CONCAT (MariaDB) / STRING_AGG (postgres) aggregation of the + # child "Opportunity Lost Reason Detail" rows. The MySQL-only GROUP_CONCAT term would fail to + # compile on postgres, so simply running the report query guards the portability fix on both + # databases. + company = frappe.db.get_value("Company", {}, "name") + columns, data = execute( + frappe._dict({"company": company, "from_date": add_days(today(), -365), "to_date": today()}) + ) + self.assertTrue(columns) + self.assertIsInstance(data, list)