From 1be736cb6fe5a40440b1296fa84e52d2d4f54566 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 14 Jul 2025 14:16:10 +0530 Subject: [PATCH] fix: load dashboard layout from CRM Dashboard doctype and render (cherry picked from commit fd38f0ac985360eae2e370e86b3e8dd5a8baa587) --- crm/api/dashboard.py | 999 +++++++++--------- .../components/Dashboard/DashboardItem.vue | 8 +- frontend/src/pages/Dashboard.vue | 5 +- 3 files changed, 504 insertions(+), 508 deletions(-) diff --git a/crm/api/dashboard.py b/crm/api/dashboard.py index 4e9139b0..8a42e124 100644 --- a/crm/api/dashboard.py +++ b/crm/api/dashboard.py @@ -1,3 +1,5 @@ +import json + import frappe from frappe import _ @@ -6,295 +8,11 @@ from crm.utils import sales_user_only @frappe.whitelist() @sales_user_only -def get_dashboard_items(from_date="", to_date="", user="", lead_conds="", deal_conds=""): +def get_dashboard(from_date="", to_date="", user=""): """ - Get dashboard items for the CRM dashboard. - Returns a list of number cards with lead and deal statistics. + Get the dashboard data for the CRM dashboard. """ - number_cards = get_number_card_data(from_date, to_date, user, lead_conds, deal_conds) - sales_trend = get_sales_trend_data(from_date, to_date, user, lead_conds, deal_conds) - forecasted_revenue = get_forecasted_revenue(user, deal_conds) - funnel_conversion = get_funnel_conversion_data(from_date, to_date, user, lead_conds, deal_conds) - deals_by_stage = get_deals_by_stage(from_date, to_date, user, deal_conds) - deals_by_stage_axis = ( - [d for d in deals_by_stage if d.get("status_type") != "Lost"] if deals_by_stage else [] - ) - leads_by_source = get_leads_by_source(from_date, to_date, user, lead_conds) - deals_by_source = get_deals_by_source(from_date, to_date, user, deal_conds) - deals_by_territory = get_deals_by_territory(from_date, to_date, user, deal_conds) - deals_by_salesperson = get_deals_by_salesperson(from_date, to_date, user, deal_conds) - lost_deal_reasons = get_lost_deal_reasons(from_date, to_date, user, deal_conds) - - return [ - { - "id": "total-leads", - "type": "number-card", - "tooltip": _("Total number of leads"), - "data": number_cards.get("total_leads"), - "layout": {"x": 0, "y": 0, "w": 4, "h": 2, "i": "0"}, - }, - { - "id": "ongoing-deals", - "type": "number-card", - "tooltip": _("Total number of ongoing deals"), - "data": number_cards.get("ongoing_deals"), - "layout": {"x": 4, "y": 0, "w": 4, "h": 2, "i": "1"}, - }, - { - "id": "average-ongoing-deal-value", - "type": "number-card", - "tooltip": _("Average value of ongoing deals"), - "data": number_cards.get("average_ongoing_deal_value"), - "layout": {"x": 8, "y": 0, "w": 4, "h": 2, "i": "2"}, - }, - { - "id": "won-deals", - "type": "number-card", - "tooltip": _("Total number of won deals"), - "data": number_cards.get("won_deal_count"), - "layout": {"x": 12, "y": 0, "w": 4, "h": 2, "i": "3"}, - }, - { - "id": "average-won-deal-value", - "type": "number-card", - "tooltip": _("Average value of won deals"), - "data": number_cards.get("average_won_deal_value"), - "layout": {"x": 16, "y": 0, "w": 4, "h": 2, "i": "4"}, - }, - { - "id": "average-deal-value", - "type": "number-card", - "tooltip": _("Average deal value of ongoing and won deals"), - "data": number_cards.get("average_deal_value"), - "layout": {"x": 0, "y": 2, "w": 4, "h": 2, "i": "5"}, - }, - { - "id": "average-time-to-close-a-lead", - "type": "number-card", - "tooltip": _("Average time taken to close a lead"), - "data": number_cards.get("average_time_to_close_a_lead"), - "layout": {"x": 4, "y": 4, "w": 4, "h": 2, "i": "6"}, - }, - { - "id": "average-time-to-close-a-deal", - "type": "number-card", - "tooltip": _("Average time taken to close a deal"), - "data": number_cards.get("average_time_to_close_a_deal"), - "layout": {"x": 8, "y": 4, "w": 4, "h": 2, "i": "7"}, - }, - { - "id": "blank-card-1", - "type": "blank-card", - "layout": {"x": 12, "y": 4, "w": 8, "h": 2, "i": "8"}, - }, - { - "id": "sales-trend", - "type": "axis-card", - "data": { - "data": sales_trend, - "title": _("Sales trend"), - "subtitle": _("Daily performance of leads, deals, and wins"), - "xAxis": { - "title": _("Date"), - "key": "date", - "type": "time", - "timeGrain": "day", - }, - "yAxis": { - "title": _("Count"), - }, - "series": [ - {"name": "leads", "type": "line", "showDataPoints": True}, - {"name": "deals", "type": "line", "showDataPoints": True}, - {"name": "won_deals", "type": "line", "showDataPoints": True}, - ], - }, - "layout": {"x": 0, "y": 6, "w": 10, "h": 7, "i": "9"}, - }, - { - "id": "forecasted-revenue", - "type": "axis-card", - "data": { - "data": forecasted_revenue or [], - "title": _("Forecasted Revenue"), - "subtitle": _("Projected vs actual revenue based on deal probability"), - "xAxis": { - "title": _("Month"), - "key": "month", - "type": "time", - "timeGrain": "month", - }, - "yAxis": { - "title": _("Revenue") + f" ({get_base_currency_symbol()})", - }, - "series": [ - {"name": "forecasted", "type": "line", "showDataPoints": True}, - {"name": "actual", "type": "line", "showDataPoints": True}, - ], - }, - "layout": {"x": 10, "y": 6, "w": 10, "h": 7, "i": "10"}, - }, - { - "id": "funnel-conversion", - "type": "axis-card", - "data": { - "data": funnel_conversion or [], - "title": _("Funnel Conversion"), - "subtitle": _("Lead to deal conversion pipeline"), - "xAxis": { - "title": _("Stage"), - "key": "stage", - "type": "category", - }, - "yAxis": { - "title": _("Count"), - }, - "swapXY": True, - "series": [ - { - "name": "count", - "type": "bar", - "echartOptions": { - "colorBy": "data", - }, - }, - ], - }, - "layout": {"x": 0, "y": 14, "w": 10, "h": 7, "i": "11"}, - }, - { - "id": "deals-by-stage-axis", - "type": "axis-card", - "data": { - "data": deals_by_stage_axis, - "title": _("Deals by ongoing & won stage"), - "xAxis": { - "title": _("Stage"), - "key": "stage", - "type": "category", - }, - "yAxis": {"title": _("Count")}, - "series": [ - {"name": "count", "type": "bar"}, - ], - }, - "layout": {"x": 10, "y": 14, "w": 10, "h": 7, "i": "12"}, - }, - { - "id": "deals-by-stage-donut", - "type": "donut-card", - "data": { - "data": deals_by_stage, - "title": _("Deals by stage"), - "subtitle": _("Current pipeline distribution"), - "categoryColumn": "stage", - "valueColumn": "count", - }, - "layout": {"x": 0, "y": 22, "w": 10, "h": 7, "i": "13"}, - }, - { - "id": "lost-deal-reasons", - "type": "axis-card", - "data": { - "data": lost_deal_reasons, - "title": _("Lost deal reasons"), - "subtitle": _("Common reasons for losing deals"), - "xAxis": { - "title": _("Reason"), - "key": "reason", - "type": "category", - }, - "yAxis": { - "title": _("Count"), - }, - "series": [ - {"name": "count", "type": "bar"}, - ], - }, - "layout": {"x": 10, "y": 22, "w": 10, "h": 7, "i": "14"}, - }, - { - "id": "leads-by-source", - "type": "donut-card", - "data": { - "data": leads_by_source, - "title": _("Leads by source"), - "subtitle": _("Lead generation channel analysis"), - "categoryColumn": "source", - "valueColumn": "count", - }, - "layout": {"x": 0, "y": 30, "w": 10, "h": 7, "i": "15"}, - }, - { - "id": "deals-by-source", - "type": "donut-card", - "data": { - "data": deals_by_source, - "title": _("Deals by source"), - "subtitle": _("Deal generation channel analysis"), - "categoryColumn": "source", - "valueColumn": "count", - }, - "layout": {"x": 10, "y": 30, "w": 10, "h": 7, "i": "16"}, - }, - { - "id": "deals-by-territory", - "type": "axis-card", - "data": { - "data": deals_by_territory, - "title": _("Deals by territory"), - "subtitle": _("Geographic distribution of deals and revenue"), - "xAxis": { - "title": _("Territory"), - "key": "territory", - "type": "category", - }, - "yAxis": { - "title": _("Number of deals"), - }, - "y2Axis": { - "title": _("Deal value") + f" ({get_base_currency_symbol()})", - }, - "series": [ - {"name": "deals", "type": "bar"}, - {"name": "value", "type": "line", "showDataPoints": True, "axis": "y2"}, - ], - }, - "layout": {"x": 0, "y": 38, "w": 10, "h": 7, "i": "17"}, - }, - { - "id": "deals-by-salesperson", - "type": "axis-card", - "data": { - "data": deals_by_salesperson, - "title": _("Deals by salesperson"), - "subtitle": _("Number of deals and total value per salesperson"), - "xAxis": { - "title": _("Salesperson"), - "key": "salesperson", - "type": "category", - }, - "yAxis": { - "title": _("Number of deals"), - }, - "y2Axis": { - "title": _("Deal value") + f" ({get_base_currency_symbol()})", - }, - "series": [ - {"name": "deals", "type": "bar"}, - {"name": "value", "type": "line", "showDataPoints": True, "axis": "y2"}, - ], - }, - "layout": {"x": 10, "y": 38, "w": 10, "h": 7, "i": "18"}, - }, - ] - - -def get_number_card_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""): - """ - Get number card data for the dashboard. - """ if not from_date or not to_date: from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) @@ -304,22 +22,26 @@ def get_number_card_data(from_date="", to_date="", user="", lead_conds="", deal_ if is_sales_user and not user: user = frappe.session.user - return { - "total_leads": get_lead_count(from_date, to_date, user, lead_conds), - "ongoing_deals": get_ongoing_deal_count(from_date, to_date, user, deal_conds)["count"], - "average_ongoing_deal_value": get_ongoing_deal_count(from_date, to_date, user, deal_conds)["average"], - "won_deal_count": get_won_deal_count(from_date, to_date, user, deal_conds)["count"], - "average_won_deal_value": get_won_deal_count(from_date, to_date, user, deal_conds)["average"], - "average_deal_value": get_average_deal_value(from_date, to_date, user, deal_conds), - "average_time_to_close_a_lead": get_average_time_to_close(from_date, to_date, user, deal_conds)["lead"], - "average_time_to_close_a_deal": get_average_time_to_close(from_date, to_date, user, deal_conds)["deal"], - } + dashboard = frappe.get_cached_doc("CRM Dashboard", "Manager Dashboard", fields=["layout"]) + + layout = json.loads(dashboard.layout) if dashboard and dashboard.layout else [] + + for l in layout: + method_name = f"get_{l['id']}" + if hasattr(frappe.get_attr("crm.api.dashboard"), method_name): + method = getattr(frappe.get_attr("crm.api.dashboard"), method_name) + l["data"] = method(from_date, to_date, user) + else: + l["data"] = None + + return layout -def get_lead_count(from_date, to_date, user="", conds="", return_result=False): +def get_total_leads(from_date, to_date, user=""): """ Get lead count for the dashboard. """ + conds = "" diff = frappe.utils.date_diff(to_date, from_date) if diff == 0: @@ -354,9 +76,6 @@ def get_lead_count(from_date, to_date, user="", conds="", return_result=False): as_dict=1, ) - if return_result: - return result - current_month_leads = result[0].current_month_leads or 0 prev_month_leads = result[0].prev_month_leads or 0 @@ -372,10 +91,11 @@ def get_lead_count(from_date, to_date, user="", conds="", return_result=False): } -def get_ongoing_deal_count(from_date, to_date, user="", conds="", return_result=False): +def get_ongoing_deals(from_date, to_date, user=""): """ Get ongoing deal count for the dashboard, and also calculate average deal value for ongoing deals. """ + conds = "" diff = frappe.utils.date_diff(to_date, from_date) if diff == 0: @@ -401,8 +121,49 @@ def get_ongoing_deal_count(from_date, to_date, user="", conds="", return_result= {conds} THEN d.name ELSE NULL - END) as prev_month_deals, + END) as prev_month_deals + FROM `tabCRM Deal` d + JOIN `tabCRM Deal Status` s ON d.status = s.name + """, + { + "from_date": from_date, + "to_date": to_date, + "prev_from_date": frappe.utils.add_days(from_date, -diff), + }, + as_dict=1, + ) + current_month_deals = result[0].current_month_deals or 0 + prev_month_deals = result[0].prev_month_deals or 0 + + delta_in_percentage = ( + (current_month_deals - prev_month_deals) / prev_month_deals * 100 if prev_month_deals else 0 + ) + + return { + "title": _("Ongoing deals"), + "value": current_month_deals, + "delta": delta_in_percentage, + "deltaSuffix": "%", + } + + +def get_average_ongoing_deal_value(from_date, to_date, user=""): + """ + Get ongoing deal count for the dashboard, and also calculate average deal value for ongoing deals. + """ + conds = "" + + diff = frappe.utils.date_diff(to_date, from_date) + if diff == 0: + diff = 1 + + if user: + conds += f" AND d.deal_owner = '{user}'" + + result = frappe.db.sql( + f""" + SELECT AVG(CASE WHEN d.creation >= %(from_date)s AND d.creation < DATE_ADD(%(to_date)s, INTERVAL 1 DAY) AND s.type NOT IN ('Won', 'Lost') @@ -429,38 +190,21 @@ def get_ongoing_deal_count(from_date, to_date, user="", conds="", return_result= as_dict=1, ) - if return_result: - return result - - current_month_deals = result[0].current_month_deals or 0 - prev_month_deals = result[0].prev_month_deals or 0 current_month_avg_value = result[0].current_month_avg_value or 0 prev_month_avg_value = result[0].prev_month_avg_value or 0 - delta_in_percentage = ( - (current_month_deals - prev_month_deals) / prev_month_deals * 100 if prev_month_deals else 0 - ) avg_value_delta = current_month_avg_value - prev_month_avg_value if prev_month_avg_value else 0 return { - "count": { - "title": _("Ongoing deals"), - "value": current_month_deals, - "delta": delta_in_percentage, - "deltaSuffix": "%", - }, - "average": { - "title": _("Avg ongoing deal value"), - "value": current_month_avg_value, - "delta": avg_value_delta, - "prefix": get_base_currency_symbol(), - # "suffix": "K", - "tooltip": _("Average deal value of ongoing deals"), - }, + "title": _("Avg ongoing deal value"), + "value": current_month_avg_value, + "delta": avg_value_delta, + "prefix": get_base_currency_symbol(), + "tooltip": _("Average deal value of ongoing deals"), } -def get_won_deal_count(from_date, to_date, user="", conds="", return_result=False): +def get_won_deals(from_date, to_date, user=""): """ Get won deal count for the dashboard, and also calculate average deal value for won deals. """ @@ -469,6 +213,8 @@ def get_won_deal_count(from_date, to_date, user="", conds="", return_result=Fals if diff == 0: diff = 1 + conds = "" + if user: conds += f" AND d.deal_owner = '{user}'" @@ -489,8 +235,51 @@ def get_won_deal_count(from_date, to_date, user="", conds="", return_result=Fals {conds} THEN d.name ELSE NULL - END) as prev_month_deals, + END) as prev_month_deals + FROM `tabCRM Deal` d + JOIN `tabCRM Deal Status` s ON d.status = s.name + """, + { + "from_date": from_date, + "to_date": to_date, + "prev_from_date": frappe.utils.add_days(from_date, -diff), + }, + as_dict=1, + ) + current_month_deals = result[0].current_month_deals or 0 + prev_month_deals = result[0].prev_month_deals or 0 + + delta_in_percentage = ( + (current_month_deals - prev_month_deals) / prev_month_deals * 100 if prev_month_deals else 0 + ) + + return { + "title": _("Won deals"), + "value": current_month_deals, + "delta": delta_in_percentage, + "deltaSuffix": "%", + "tooltip": _("Total number of won deals based on its closure date"), + } + + +def get_average_won_deal_value(from_date, to_date, user=""): + """ + Get won deal count for the dashboard, and also calculate average deal value for won deals. + """ + + diff = frappe.utils.date_diff(to_date, from_date) + if diff == 0: + diff = 1 + + conds = "" + + if user: + conds += f" AND d.deal_owner = '{user}'" + + result = frappe.db.sql( + f""" + SELECT AVG(CASE WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY) AND s.type = 'Won' @@ -517,39 +306,21 @@ def get_won_deal_count(from_date, to_date, user="", conds="", return_result=Fals as_dict=1, ) - if return_result: - return result - - current_month_deals = result[0].current_month_deals or 0 - prev_month_deals = result[0].prev_month_deals or 0 current_month_avg_value = result[0].current_month_avg_value or 0 prev_month_avg_value = result[0].prev_month_avg_value or 0 - delta_in_percentage = ( - (current_month_deals - prev_month_deals) / prev_month_deals * 100 if prev_month_deals else 0 - ) avg_value_delta = current_month_avg_value - prev_month_avg_value if prev_month_avg_value else 0 return { - "count": { - "title": _("Won deals"), - "value": current_month_deals, - "delta": delta_in_percentage, - "deltaSuffix": "%", - "tooltip": _("Total number of won deals based on its closure date"), - }, - "average": { - "title": _("Avg won deal value"), - "value": current_month_avg_value, - "delta": avg_value_delta, - "prefix": get_base_currency_symbol(), - # "suffix": "K", - "tooltip": _("Average deal value of won deals"), - }, + "title": _("Avg won deal value"), + "value": current_month_avg_value, + "delta": avg_value_delta, + "prefix": get_base_currency_symbol(), + "tooltip": _("Average deal value of won deals"), } -def get_average_deal_value(from_date, to_date, user="", conds="", return_result=False): +def get_average_deal_value(from_date, to_date, user=""): """ Get average deal value for the dashboard. """ @@ -558,6 +329,8 @@ def get_average_deal_value(from_date, to_date, user="", conds="", return_result= if diff == 0: diff = 1 + conds = "" + if user: conds += f" AND d.deal_owner = '{user}'" @@ -606,18 +379,17 @@ def get_average_deal_value(from_date, to_date, user="", conds="", return_result= } -def get_average_time_to_close(from_date, to_date, user="", conds="", return_result=False): +def get_average_time_to_close_a_lead(from_date, to_date, user=""): """ Get average time to close deals for the dashboard. - Returns both: - - Average time from lead creation to deal closure - - Average time from deal creation to deal closure """ diff = frappe.utils.date_diff(to_date, from_date) if diff == 0: diff = 1 + conds = "" + if user: conds += f" AND d.deal_owner = '{user}'" @@ -630,7 +402,57 @@ def get_average_time_to_close(from_date, to_date, user="", conds="", return_resu AVG(CASE WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY) THEN TIMESTAMPDIFF(DAY, COALESCE(l.creation, d.creation), d.closed_date) END) as current_avg_lead, AVG(CASE WHEN d.closed_date >= %(prev_from_date)s AND d.closed_date < %(prev_to_date)s - THEN TIMESTAMPDIFF(DAY, COALESCE(l.creation, d.creation), d.closed_date) END) as prev_avg_lead, + THEN TIMESTAMPDIFF(DAY, COALESCE(l.creation, d.creation), d.closed_date) END) as prev_avg_lead + FROM `tabCRM Deal` AS d + JOIN `tabCRM Deal Status` s ON d.status = s.name + LEFT JOIN `tabCRM Lead` l ON d.lead = l.name + WHERE d.closed_date IS NOT NULL AND s.type = 'Won' + {conds} + """, + { + "from_date": from_date, + "to_date": to_date, + "prev_from_date": prev_from_date, + "prev_to_date": prev_to_date, + }, + as_dict=1, + ) + + current_avg_lead = result[0].current_avg_lead or 0 + prev_avg_lead = result[0].prev_avg_lead or 0 + delta_lead = current_avg_lead - prev_avg_lead if prev_avg_lead else 0 + + return { + "title": _("Avg time to close a lead"), + "value": current_avg_lead, + "tooltip": _("Average time taken from lead creation to deal closure"), + "suffix": " days", + "delta": delta_lead, + "deltaSuffix": " days", + "negativeIsBetter": True, + } + + +def get_average_time_to_close_a_deal(from_date, to_date, user=""): + """ + Get average time to close deals for the dashboard. + """ + + diff = frappe.utils.date_diff(to_date, from_date) + if diff == 0: + diff = 1 + + conds = "" + + if user: + conds += f" AND d.deal_owner = '{user}'" + + prev_from_date = frappe.utils.add_days(from_date, -diff) + prev_to_date = from_date + + result = frappe.db.sql( + f""" + SELECT AVG(CASE WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY) THEN TIMESTAMPDIFF(DAY, d.creation, d.closed_date) END) as current_avg_deal, AVG(CASE WHEN d.closed_date >= %(prev_from_date)s AND d.closed_date < %(prev_to_date)s @@ -650,40 +472,22 @@ def get_average_time_to_close(from_date, to_date, user="", conds="", return_resu as_dict=1, ) - if return_result: - return result - - current_avg_lead = result[0].current_avg_lead or 0 - prev_avg_lead = result[0].prev_avg_lead or 0 - delta_lead = current_avg_lead - prev_avg_lead if prev_avg_lead else 0 - current_avg_deal = result[0].current_avg_deal or 0 prev_avg_deal = result[0].prev_avg_deal or 0 delta_deal = current_avg_deal - prev_avg_deal if prev_avg_deal else 0 return { - "lead": { - "title": _("Avg time to close a lead"), - "value": current_avg_lead, - "tooltip": _("Average time taken from lead creation to deal closure"), - "suffix": " days", - "delta": delta_lead, - "deltaSuffix": " days", - "negativeIsBetter": True, - }, - "deal": { - "title": _("Avg time to close a deal"), - "value": current_avg_deal, - "tooltip": _("Average time taken from deal creation to deal closure"), - "suffix": " days", - "delta": delta_deal, - "deltaSuffix": " days", - "negativeIsBetter": True, - }, + "title": _("Avg time to close a deal"), + "value": current_avg_deal, + "tooltip": _("Average time taken from deal creation to deal closure"), + "suffix": " days", + "delta": delta_deal, + "deltaSuffix": " days", + "negativeIsBetter": True, } -def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""): +def get_sales_trend(from_date="", to_date="", user=""): """ Get sales trend data for the dashboard. [ @@ -693,6 +497,9 @@ def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_ ] """ + lead_conds = "" + deal_conds = "" + if not from_date or not to_date: from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) @@ -739,7 +546,7 @@ def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_ as_dict=True, ) - return [ + sales_trend = [ { "date": frappe.utils.get_datetime(row.date).strftime("%Y-%m-%d"), "leads": row.leads or 0, @@ -749,118 +556,28 @@ def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_ for row in result ] - -def get_deals_by_salesperson(from_date="", to_date="", user="", deal_conds=""): - """ - Get deal data by salesperson for the dashboard. - [ - { salesperson: 'John Smith', deals: 45, value: 2300000 }, - { salesperson: 'Jane Doe', deals: 30, value: 1500000 }, - ... - ] - """ - - if not from_date or not to_date: - from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) - to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) - - if user: - deal_conds += f" AND d.deal_owner = '{user}'" - - result = frappe.db.sql( - f""" - SELECT - IFNULL(u.full_name, d.deal_owner) AS salesperson, - COUNT(*) AS deals, - SUM(COALESCE(d.deal_value, 0) * IFNULL(d.exchange_rate, 1)) AS value - FROM `tabCRM Deal` AS d - LEFT JOIN `tabUser` AS u ON u.name = d.deal_owner - WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s - {deal_conds} - GROUP BY d.deal_owner - ORDER BY value DESC - """, - {"from": from_date, "to": to_date}, - as_dict=True, - ) - - return result or [] + return { + "data": sales_trend, + "title": _("Sales trend"), + "subtitle": _("Daily performance of leads, deals, and wins"), + "xAxis": { + "title": _("Date"), + "key": "date", + "type": "time", + "timeGrain": "day", + }, + "yAxis": { + "title": _("Count"), + }, + "series": [ + {"name": "leads", "type": "line", "showDataPoints": True}, + {"name": "deals", "type": "line", "showDataPoints": True}, + {"name": "won_deals", "type": "line", "showDataPoints": True}, + ], + } -def get_deals_by_territory(from_date="", to_date="", user="", deal_conds=""): - """ - Get deal data by territory for the dashboard. - [ - { territory: 'North America', deals: 45, value: 2300000 }, - { territory: 'Europe', deals: 30, value: 1500000 }, - ... - ] - """ - - if not from_date or not to_date: - from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) - to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) - - if user: - deal_conds += f" AND d.deal_owner = '{user}'" - - result = frappe.db.sql( - f""" - SELECT - IFNULL(d.territory, 'Empty') AS territory, - COUNT(*) AS deals, - SUM(COALESCE(d.deal_value, 0) * IFNULL(d.exchange_rate, 1)) AS value - FROM `tabCRM Deal` AS d - WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s - {deal_conds} - GROUP BY d.territory - ORDER BY value DESC - """, - {"from": from_date, "to": to_date}, - as_dict=True, - ) - - return result or [] - - -def get_lost_deal_reasons(from_date="", to_date="", user="", deal_conds=""): - """ - Get lost deal reasons for the dashboard. - [ - { reason: 'Price too high', count: 20 }, - { reason: 'Competitor won', count: 15 }, - ... - ] - """ - - if not from_date or not to_date: - from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) - to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) - - if user: - deal_conds += f" AND d.deal_owner = '{user}'" - - result = frappe.db.sql( - f""" - SELECT - d.lost_reason AS reason, - COUNT(*) AS count - FROM `tabCRM Deal` AS d - JOIN `tabCRM Deal Status` s ON d.status = s.name - WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s AND s.type = 'Lost' - {deal_conds} - GROUP BY d.lost_reason - HAVING reason IS NOT NULL AND reason != '' - ORDER BY count DESC - """, - {"from": from_date, "to": to_date}, - as_dict=True, - ) - - return result or [] - - -def get_forecasted_revenue(user="", deal_conds=""): +def get_forecasted_revenue(from_date="", to_date="", user=""): """ Get forecasted revenue for the dashboard. [ @@ -871,6 +588,7 @@ def get_forecasted_revenue(user="", deal_conds=""): ... ] """ + deal_conds = "" if user: deal_conds += f" AND d.deal_owner = '{user}'" @@ -908,10 +626,27 @@ def get_forecasted_revenue(user="", deal_conds=""): row["forecasted"] = row["forecasted"] or "" row["actual"] = row["actual"] or "" - return result or [] + return { + "data": result or [], + "title": _("Forecasted Revenue"), + "subtitle": _("Projected vs actual revenue based on deal probability"), + "xAxis": { + "title": _("Month"), + "key": "month", + "type": "time", + "timeGrain": "month", + }, + "yAxis": { + "title": _("Revenue") + f" ({get_base_currency_symbol()})", + }, + "series": [ + {"name": "forecasted", "type": "line", "showDataPoints": True}, + {"name": "actual", "type": "line", "showDataPoints": True}, + ], + } -def get_funnel_conversion_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""): +def get_funnel_conversion(from_date="", to_date="", user=""): """ Get funnel conversion data for the dashboard. [ @@ -923,6 +658,8 @@ def get_funnel_conversion_data(from_date="", to_date="", user="", lead_conds="", ... ] """ + lead_conds = "" + deal_conds = "" if not from_date or not to_date: from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) @@ -951,10 +688,32 @@ def get_funnel_conversion_data(from_date="", to_date="", user="", lead_conds="", result += get_deal_status_change_counts(from_date, to_date, deal_conds) - return result or [] + return { + "data": result or [], + "title": _("Funnel Conversion"), + "subtitle": _("Lead to deal conversion pipeline"), + "xAxis": { + "title": _("Stage"), + "key": "stage", + "type": "category", + }, + "yAxis": { + "title": _("Count"), + }, + "swapXY": True, + "series": [ + { + "name": "count", + "type": "bar", + "echartOptions": { + "colorBy": "data", + }, + }, + ], + } -def get_deals_by_stage(from_date="", to_date="", user="", deal_conds=""): +def get_deals_by_stage_axis(from_date="", to_date="", user=""): """ Get deal data by stage for the dashboard. [ @@ -963,6 +722,57 @@ def get_deals_by_stage(from_date="", to_date="", user="", deal_conds=""): ... ] """ + deal_conds = "" + + if not from_date or not to_date: + from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) + to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + + if user: + deal_conds += f" AND d.deal_owner = '{user}'" + + result = frappe.db.sql( + f""" + SELECT + d.status AS stage, + COUNT(*) AS count, + s.type AS status_type + FROM `tabCRM Deal` AS d + JOIN `tabCRM Deal Status` s ON d.status = s.name + WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s AND s.type NOT IN ('Lost') + {deal_conds} + GROUP BY d.status + ORDER BY count DESC + """, + {"from": from_date, "to": to_date}, + as_dict=True, + ) + + return { + "data": result or [], + "title": _("Deals by ongoing & won stage"), + "xAxis": { + "title": _("Stage"), + "key": "stage", + "type": "category", + }, + "yAxis": {"title": _("Count")}, + "series": [ + {"name": "count", "type": "bar"}, + ], + } + + +def get_deals_by_stage_donut(from_date="", to_date="", user=""): + """ + Get deal data by stage for the dashboard. + [ + { stage: 'Prospecting', count: 120 }, + { stage: 'Negotiation', count: 45 }, + ... + ] + """ + deal_conds = "" if not from_date or not to_date: from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) @@ -988,10 +798,70 @@ def get_deals_by_stage(from_date="", to_date="", user="", deal_conds=""): as_dict=True, ) - return result or [] + return { + "data": result or [], + "title": _("Deals by stage"), + "subtitle": _("Current pipeline distribution"), + "categoryColumn": "stage", + "valueColumn": "count", + } -def get_leads_by_source(from_date="", to_date="", user="", lead_conds=""): +def get_lost_deal_reasons(from_date="", to_date="", user=""): + """ + Get lost deal reasons for the dashboard. + [ + { reason: 'Price too high', count: 20 }, + { reason: 'Competitor won', count: 15 }, + ... + ] + """ + + deal_conds = "" + + if not from_date or not to_date: + from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) + to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + + if user: + deal_conds += f" AND d.deal_owner = '{user}'" + + result = frappe.db.sql( + f""" + SELECT + d.lost_reason AS reason, + COUNT(*) AS count + FROM `tabCRM Deal` AS d + JOIN `tabCRM Deal Status` s ON d.status = s.name + WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s AND s.type = 'Lost' + {deal_conds} + GROUP BY d.lost_reason + HAVING reason IS NOT NULL AND reason != '' + ORDER BY count DESC + """, + {"from": from_date, "to": to_date}, + as_dict=True, + ) + + return { + "data": result or [], + "title": _("Lost deal reasons"), + "subtitle": _("Common reasons for losing deals"), + "xAxis": { + "title": _("Reason"), + "key": "reason", + "type": "category", + }, + "yAxis": { + "title": _("Count"), + }, + "series": [ + {"name": "count", "type": "bar"}, + ], + } + + +def get_leads_by_source(from_date="", to_date="", user=""): """ Get lead data by source for the dashboard. [ @@ -1000,6 +870,7 @@ def get_leads_by_source(from_date="", to_date="", user="", lead_conds=""): ... ] """ + lead_conds = "" if not from_date or not to_date: from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) @@ -1023,10 +894,16 @@ def get_leads_by_source(from_date="", to_date="", user="", lead_conds=""): as_dict=True, ) - return result or [] + return { + "data": result or [], + "title": _("Leads by source"), + "subtitle": _("Lead generation channel analysis"), + "categoryColumn": "source", + "valueColumn": "count", + } -def get_deals_by_source(from_date="", to_date="", user="", deal_conds=""): +def get_deals_by_source(from_date="", to_date="", user=""): """ Get deal data by source for the dashboard. [ @@ -1035,6 +912,7 @@ def get_deals_by_source(from_date="", to_date="", user="", deal_conds=""): ... ] """ + deal_conds = "" if not from_date or not to_date: from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) @@ -1058,7 +936,126 @@ def get_deals_by_source(from_date="", to_date="", user="", deal_conds=""): as_dict=True, ) - return result or [] + return { + "data": result or [], + "title": _("Deals by source"), + "subtitle": _("Deal generation channel analysis"), + "categoryColumn": "source", + "valueColumn": "count", + } + + +def get_deals_by_territory(from_date="", to_date="", user=""): + """ + Get deal data by territory for the dashboard. + [ + { territory: 'North America', deals: 45, value: 2300000 }, + { territory: 'Europe', deals: 30, value: 1500000 }, + ... + ] + """ + deal_conds = "" + + if not from_date or not to_date: + from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) + to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + + if user: + deal_conds += f" AND d.deal_owner = '{user}'" + + result = frappe.db.sql( + f""" + SELECT + IFNULL(d.territory, 'Empty') AS territory, + COUNT(*) AS deals, + SUM(COALESCE(d.deal_value, 0) * IFNULL(d.exchange_rate, 1)) AS value + FROM `tabCRM Deal` AS d + WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s + {deal_conds} + GROUP BY d.territory + ORDER BY value DESC + """, + {"from": from_date, "to": to_date}, + as_dict=True, + ) + + return { + "data": result or [], + "title": _("Deals by territory"), + "subtitle": _("Geographic distribution of deals and revenue"), + "xAxis": { + "title": _("Territory"), + "key": "territory", + "type": "category", + }, + "yAxis": { + "title": _("Number of deals"), + }, + "y2Axis": { + "title": _("Deal value") + f" ({get_base_currency_symbol()})", + }, + "series": [ + {"name": "deals", "type": "bar"}, + {"name": "value", "type": "line", "showDataPoints": True, "axis": "y2"}, + ], + } + + +def get_deals_by_salesperson(from_date="", to_date="", user=""): + """ + Get deal data by salesperson for the dashboard. + [ + { salesperson: 'John Smith', deals: 45, value: 2300000 }, + { salesperson: 'Jane Doe', deals: 30, value: 1500000 }, + ... + ] + """ + deal_conds = "" + + if not from_date or not to_date: + from_date = frappe.utils.get_first_day(from_date or frappe.utils.nowdate()) + to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate()) + + if user: + deal_conds += f" AND d.deal_owner = '{user}'" + + result = frappe.db.sql( + f""" + SELECT + IFNULL(u.full_name, d.deal_owner) AS salesperson, + COUNT(*) AS deals, + SUM(COALESCE(d.deal_value, 0) * IFNULL(d.exchange_rate, 1)) AS value + FROM `tabCRM Deal` AS d + LEFT JOIN `tabUser` AS u ON u.name = d.deal_owner + WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s + {deal_conds} + GROUP BY d.deal_owner + ORDER BY value DESC + """, + {"from": from_date, "to": to_date}, + as_dict=True, + ) + + return { + "data": result or [], + "title": _("Deals by salesperson"), + "subtitle": _("Number of deals and total value per salesperson"), + "xAxis": { + "title": _("Salesperson"), + "key": "salesperson", + "type": "category", + }, + "yAxis": { + "title": _("Number of deals"), + }, + "y2Axis": { + "title": _("Deal value") + f" ({get_base_currency_symbol()})", + }, + "series": [ + {"name": "deals", "type": "bar"}, + {"name": "value", "type": "line", "showDataPoints": True, "axis": "y2"}, + ], + } def get_base_currency_symbol(): diff --git a/frontend/src/components/Dashboard/DashboardItem.vue b/frontend/src/components/Dashboard/DashboardItem.vue index 23976d73..dfecbce5 100644 --- a/frontend/src/components/Dashboard/DashboardItem.vue +++ b/frontend/src/components/Dashboard/DashboardItem.vue @@ -1,7 +1,7 @@ -