import frappe from frappe import _ 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=""): """ Get dashboard items for the CRM dashboard. Returns a list of number cards with lead and deal statistics. """ 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()) roles = frappe.get_roles(frappe.session.user) is_sales_user = "Sales User" in roles and "Sales Manager" not in roles and "System Manager" not in roles 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"], } def get_lead_count(from_date, to_date, user="", conds="", return_result=False): """ Get lead count for the dashboard. """ diff = frappe.utils.date_diff(to_date, from_date) if diff == 0: diff = 1 if user: conds += f" AND lead_owner = '{user}'" result = frappe.db.sql( f""" SELECT COUNT(CASE WHEN creation >= %(from_date)s AND creation < DATE_ADD(%(to_date)s, INTERVAL 1 DAY) {conds} THEN name ELSE NULL END) as current_month_leads, COUNT(CASE WHEN creation >= %(prev_from_date)s AND creation < %(from_date)s {conds} THEN name ELSE NULL END) as prev_month_leads FROM `tabCRM Lead` """, { "from_date": from_date, "to_date": to_date, "prev_from_date": frappe.utils.add_days(from_date, -diff), }, 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 delta_in_percentage = ( (current_month_leads - prev_month_leads) / prev_month_leads * 100 if prev_month_leads else 0 ) return { "title": _("Total leads"), "value": current_month_leads, "delta": delta_in_percentage, "deltaSuffix": "%", } def get_ongoing_deal_count(from_date, to_date, user="", conds="", return_result=False): """ Get ongoing deal count for the dashboard, and also calculate average deal value for ongoing deals. """ 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 COUNT(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') {conds} THEN d.name ELSE NULL END) as current_month_deals, COUNT(CASE WHEN d.creation >= %(prev_from_date)s AND d.creation < %(from_date)s AND s.type NOT IN ('Won', 'Lost') {conds} THEN d.name ELSE NULL END) as prev_month_deals, 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') {conds} THEN d.deal_value * IFNULL(d.exchange_rate, 1) ELSE NULL END) as current_month_avg_value, AVG(CASE WHEN d.creation >= %(prev_from_date)s AND d.creation < %(from_date)s AND s.type NOT IN ('Won', 'Lost') {conds} THEN d.deal_value * IFNULL(d.exchange_rate, 1) ELSE NULL END) as prev_month_avg_value 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, ) 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"), }, } def get_won_deal_count(from_date, to_date, user="", conds="", return_result=False): """ 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 if user: conds += f" AND d.deal_owner = '{user}'" result = frappe.db.sql( f""" SELECT COUNT(CASE WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY) AND s.type = 'Won' {conds} THEN d.name ELSE NULL END) as current_month_deals, COUNT(CASE WHEN d.closed_date >= %(prev_from_date)s AND d.closed_date < %(from_date)s AND s.type = 'Won' {conds} THEN d.name ELSE NULL END) as prev_month_deals, 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' {conds} THEN d.deal_value * IFNULL(d.exchange_rate, 1) ELSE NULL END) as current_month_avg_value, AVG(CASE WHEN d.closed_date >= %(prev_from_date)s AND d.closed_date < %(from_date)s AND s.type = 'Won' {conds} THEN d.deal_value * IFNULL(d.exchange_rate, 1) ELSE NULL END) as prev_month_avg_value 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, ) 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"), }, } def get_average_deal_value(from_date, to_date, user="", conds="", return_result=False): """ Get average deal value for the dashboard. """ 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 != 'Lost' {conds} THEN d.deal_value * IFNULL(d.exchange_rate, 1) ELSE NULL END) as current_month_avg, AVG(CASE WHEN d.creation >= %(prev_from_date)s AND d.creation < %(from_date)s AND s.type != 'Lost' {conds} THEN d.deal_value * IFNULL(d.exchange_rate, 1) ELSE NULL END) as prev_month_avg FROM `tabCRM Deal` AS 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_avg = result[0].current_month_avg or 0 prev_month_avg = result[0].prev_month_avg or 0 delta = current_month_avg - prev_month_avg if prev_month_avg else 0 return { "title": _("Avg deal value"), "value": current_month_avg, "tooltip": _("Average deal value of ongoing & won deals"), "prefix": get_base_currency_symbol(), # "suffix": "K", "delta": delta, "deltaSuffix": "%", } def get_average_time_to_close(from_date, to_date, user="", conds="", return_result=False): """ 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 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, 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, 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 THEN TIMESTAMPDIFF(DAY, d.creation, d.closed_date) END) as prev_avg_deal 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, ) 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, }, } def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""): """ Get sales trend data for the dashboard. [ { date: new Date('2024-05-01'), leads: 45, deals: 23, won_deals: 12 }, { date: new Date('2024-05-02'), leads: 50, deals: 30, won_deals: 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: lead_conds += f" AND lead_owner = '{user}'" deal_conds += f" AND deal_owner = '{user}'" result = frappe.db.sql( f""" SELECT DATE_FORMAT(date, '%%Y-%%m-%%d') AS date, SUM(leads) AS leads, SUM(deals) AS deals, SUM(won_deals) AS won_deals FROM ( SELECT DATE(creation) AS date, COUNT(*) AS leads, 0 AS deals, 0 AS won_deals FROM `tabCRM Lead` WHERE DATE(creation) BETWEEN %(from)s AND %(to)s {lead_conds} GROUP BY DATE(creation) UNION ALL SELECT DATE(d.creation) AS date, 0 AS leads, COUNT(*) AS deals, SUM(CASE WHEN s.type = 'Won' THEN 1 ELSE 0 END) AS won_deals FROM `tabCRM Deal` d JOIN `tabCRM Deal Status` s ON d.status = s.name WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s {deal_conds} GROUP BY DATE(d.creation) ) AS daily GROUP BY date ORDER BY date """, {"from": from_date, "to": to_date}, as_dict=True, ) return [ { "date": frappe.utils.get_datetime(row.date).strftime("%Y-%m-%d"), "leads": row.leads or 0, "deals": row.deals or 0, "won_deals": row.won_deals or 0, } 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 [] 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=""): """ Get forecasted revenue for the dashboard. [ { date: new Date('2024-05-01'), forecasted: 1200000, actual: 980000 }, { date: new Date('2024-06-01'), forecasted: 1350000, actual: 1120000 }, { date: new Date('2024-07-01'), forecasted: 1600000, actual: "" }, { date: new Date('2024-08-01'), forecasted: 1500000, actual: "" }, ... ] """ if user: deal_conds += f" AND d.deal_owner = '{user}'" result = frappe.db.sql( f""" SELECT DATE_FORMAT(d.expected_closure_date, '%Y-%m') AS month, SUM( CASE WHEN s.type = 'Lost' THEN d.expected_deal_value * IFNULL(d.exchange_rate, 1) ELSE d.expected_deal_value * IFNULL(d.probability, 0) / 100 * IFNULL(d.exchange_rate, 1) -- forecasted END ) AS forecasted, SUM( CASE WHEN s.type = 'Won' THEN d.deal_value * IFNULL(d.exchange_rate, 1) -- actual ELSE 0 END ) AS actual FROM `tabCRM Deal` AS d JOIN `tabCRM Deal Status` s ON d.status = s.name WHERE d.expected_closure_date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH) {deal_conds} GROUP BY DATE_FORMAT(d.expected_closure_date, '%Y-%m') ORDER BY month """, as_dict=True, ) if not result: return [] for row in result: row["month"] = frappe.utils.get_datetime(row["month"]).strftime("%Y-%m-01") row["forecasted"] = row["forecasted"] or "" row["actual"] = row["actual"] or "" return result or [] def get_funnel_conversion_data(from_date="", to_date="", user="", lead_conds="", deal_conds=""): """ Get funnel conversion data for the dashboard. [ { stage: 'Leads', count: 120 }, { stage: 'Qualification', count: 100 }, { stage: 'Negotiation', count: 80 }, { stage: 'Ready to Close', count: 60 }, { stage: 'Won', count: 30 }, ... ] """ 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: lead_conds += f" AND lead_owner = '{user}'" deal_conds += f" AND deal_owner = '{user}'" result = [] # Get total leads total_leads = frappe.db.sql( f""" SELECT COUNT(*) AS count FROM `tabCRM Lead` WHERE DATE(creation) BETWEEN %(from)s AND %(to)s {lead_conds} """, {"from": from_date, "to": to_date}, as_dict=True, ) total_leads_count = total_leads[0].count if total_leads else 0 result.append({"stage": "Leads", "count": total_leads_count}) result += get_deal_status_change_counts(from_date, to_date, deal_conds) return result or [] def get_deals_by_stage(from_date="", to_date="", user="", deal_conds=""): """ Get deal data by stage for the dashboard. [ { stage: 'Prospecting', count: 120 }, { stage: 'Negotiation', count: 45 }, ... ] """ 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 {deal_conds} GROUP BY d.status ORDER BY count DESC """, {"from": from_date, "to": to_date}, as_dict=True, ) return result or [] def get_leads_by_source(from_date="", to_date="", user="", lead_conds=""): """ Get lead data by source for the dashboard. [ { source: 'Website', count: 120 }, { source: 'Referral', count: 45 }, ... ] """ 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: lead_conds += f" AND lead_owner = '{user}'" result = frappe.db.sql( f""" SELECT IFNULL(source, 'Empty') AS source, COUNT(*) AS count FROM `tabCRM Lead` WHERE DATE(creation) BETWEEN %(from)s AND %(to)s {lead_conds} GROUP BY source ORDER BY count DESC """, {"from": from_date, "to": to_date}, as_dict=True, ) return result or [] def get_deals_by_source(from_date="", to_date="", user="", deal_conds=""): """ Get deal data by source for the dashboard. [ { source: 'Website', count: 120 }, { source: 'Referral', count: 45 }, ... ] """ 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 lead_owner = '{user}'" result = frappe.db.sql( f""" SELECT IFNULL(source, 'Empty') AS source, COUNT(*) AS count FROM `tabCRM Deal` WHERE DATE(creation) BETWEEN %(from)s AND %(to)s {deal_conds} GROUP BY source ORDER BY count DESC """, {"from": from_date, "to": to_date}, as_dict=True, ) return result or [] def get_base_currency_symbol(): """ Get the base currency symbol from the system settings. """ base_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD" return frappe.db.get_value("Currency", base_currency, "symbol") or "" def get_deal_status_change_counts(from_date, to_date, deal_conds=""): """ Get count of each status change (to) for each deal, excluding deals with current status type 'Lost'. Order results by status position. Returns: [ {"status": "Qualification", "count": 120}, {"status": "Negotiation", "count": 85}, ... ] """ result = frappe.db.sql( f""" SELECT scl.to AS stage, COUNT(*) AS count FROM `tabCRM Status Change Log` scl JOIN `tabCRM Deal` d ON scl.parent = d.name JOIN `tabCRM Deal Status` s ON d.status = s.name JOIN `tabCRM Deal Status` st ON scl.to = st.name WHERE scl.to IS NOT NULL AND scl.to != '' AND s.type != 'Lost' AND DATE(d.creation) BETWEEN %(from)s AND %(to)s {deal_conds} GROUP BY scl.to, st.position ORDER BY st.position ASC """, {"from": from_date, "to": to_date}, as_dict=True, ) return result or []