diff --git a/crm/api/dashboard.py b/crm/api/dashboard.py
new file mode 100644
index 00000000..4bed4218
--- /dev/null
+++ b/crm/api/dashboard.py
@@ -0,0 +1,1142 @@
+import json
+
+import frappe
+from frappe import _
+
+from crm.fcrm.doctype.crm_dashboard.crm_dashboard import create_default_manager_dashboard
+from crm.utils import sales_user_only
+
+
+@frappe.whitelist()
+def reset_to_default():
+ frappe.only_for("System Manager")
+ create_default_manager_dashboard(force=True)
+
+
+@frappe.whitelist()
+@sales_user_only
+def get_dashboard(from_date="", to_date="", user=""):
+ """
+ Get the dashboard data for the CRM 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
+
+ dashboard = frappe.db.exists("CRM Dashboard", "Manager Dashboard")
+
+ layout = []
+
+ if not dashboard:
+ layout = json.loads(create_default_manager_dashboard())
+ frappe.db.commit()
+ else:
+ layout = json.loads(frappe.db.get_value("CRM Dashboard", "Manager Dashboard", "layout") or "[]")
+
+ for l in layout:
+ method_name = f"get_{l['name']}"
+ 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
+
+
+@frappe.whitelist()
+@sales_user_only
+def get_chart(name, type, from_date="", to_date="", user=""):
+ """
+ Get number chart 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
+
+ method_name = f"get_{name}"
+ if hasattr(frappe.get_attr("crm.api.dashboard"), method_name):
+ method = getattr(frappe.get_attr("crm.api.dashboard"), method_name)
+ return method(from_date, to_date, user)
+ else:
+ return {"error": _("Invalid chart name")}
+
+
+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:
+ 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,
+ )
+
+ 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"),
+ "tooltip": _("Total number of leads"),
+ "value": current_month_leads,
+ "delta": delta_in_percentage,
+ "deltaSuffix": "%",
+ }
+
+
+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:
+ 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
+ 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"),
+ "tooltip": _("Total number of non won/lost 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')
+ {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,
+ )
+
+ current_month_avg_value = result[0].current_month_avg_value or 0
+ prev_month_avg_value = result[0].prev_month_avg_value or 0
+
+ avg_value_delta = current_month_avg_value - prev_month_avg_value if prev_month_avg_value else 0
+
+ return {
+ "title": _("Avg. ongoing deal value"),
+ "tooltip": _("Average deal value of non won/lost deals"),
+ "value": current_month_avg_value,
+ "delta": avg_value_delta,
+ "prefix": get_base_currency_symbol(),
+ }
+
+
+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.
+ """
+
+ 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
+ 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
+ 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"),
+ "tooltip": _("Total number of won deals based on its closure date"),
+ "value": current_month_deals,
+ "delta": delta_in_percentage,
+ "deltaSuffix": "%",
+ }
+
+
+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'
+ {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,
+ )
+
+ current_month_avg_value = result[0].current_month_avg_value or 0
+ prev_month_avg_value = result[0].prev_month_avg_value or 0
+
+ avg_value_delta = current_month_avg_value - prev_month_avg_value if prev_month_avg_value else 0
+
+ return {
+ "title": _("Avg. won deal value"),
+ "tooltip": _("Average deal value of won deals"),
+ "value": current_month_avg_value,
+ "delta": avg_value_delta,
+ "prefix": get_base_currency_symbol(),
+ }
+
+
+def get_average_deal_value(from_date, to_date, user=""):
+ """
+ Get average deal value 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}'"
+
+ 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"),
+ "tooltip": _("Average deal value of ongoing & won deals"),
+ "value": current_month_avg,
+ "prefix": get_base_currency_symbol(),
+ "delta": delta,
+ "deltaSuffix": "%",
+ }
+
+
+def get_average_time_to_close_a_lead(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, 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
+ 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"),
+ "tooltip": _("Average time taken from lead creation to deal closure"),
+ "value": current_avg_lead,
+ "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
+ 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,
+ )
+
+ 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 {
+ "title": _("Avg. time to close a deal"),
+ "tooltip": _("Average time taken from deal creation to deal closure"),
+ "value": current_avg_deal,
+ "suffix": " days",
+ "delta": delta_deal,
+ "deltaSuffix": " days",
+ "negativeIsBetter": True,
+ }
+
+
+def get_sales_trend(from_date="", to_date="", user=""):
+ """
+ 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 },
+ ...
+ ]
+ """
+
+ 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())
+
+ 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,
+ )
+
+ sales_trend = [
+ {
+ "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
+ ]
+
+ 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_forecasted_revenue(from_date="", to_date="", user=""):
+ """
+ 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: "" },
+ ...
+ ]
+ """
+ deal_conds = ""
+
+ 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,
+ )
+
+ 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 {
+ "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(from_date="", to_date="", user=""):
+ """
+ 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 },
+ ...
+ ]
+ """
+ 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())
+
+ 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 {
+ "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_axis(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())
+ 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())
+ 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 {
+ "data": result or [],
+ "title": _("Deals by stage"),
+ "subtitle": _("Current pipeline distribution"),
+ "categoryColumn": "stage",
+ "valueColumn": "count",
+ }
+
+
+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.
+ [
+ { source: 'Website', count: 120 },
+ { source: 'Referral', count: 45 },
+ ...
+ ]
+ """
+ lead_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:
+ 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 {
+ "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=""):
+ """
+ Get deal data by source for the dashboard.
+ [
+ { source: 'Website', count: 120 },
+ { source: 'Referral', 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())
+ to_date = frappe.utils.get_last_day(to_date or frappe.utils.nowdate())
+
+ if user:
+ deal_conds += f" AND deal_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 {
+ "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():
+ """
+ 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 []
diff --git a/crm/api/session.py b/crm/api/session.py
index add14c37..b45ac43d 100644
--- a/crm/api/session.py
+++ b/crm/api/session.py
@@ -23,9 +23,6 @@ def get_users():
if frappe.session.user == user.name:
user.session_user = True
- user.is_manager = "Sales Manager" in frappe.get_roles(user.name)
- user.is_admin = user.name == "Administrator"
-
user.roles = frappe.get_roles(user.name)
user.role = ""
@@ -42,7 +39,7 @@ def get_users():
if frappe.session.user == user.name:
user.session_user = True
- user.is_agent = frappe.db.exists("CRM Telephony Agent", {"user": user.name})
+ user.is_telephony_agent = frappe.db.exists("CRM Telephony Agent", {"user": user.name})
crm_users = []
diff --git a/crm/fcrm/doctype/crm_dashboard/__init__.py b/crm/fcrm/doctype/crm_dashboard/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/crm/fcrm/doctype/crm_dashboard/crm_dashboard.js b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.js
new file mode 100644
index 00000000..9aee84c7
--- /dev/null
+++ b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("CRM Dashboard", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/crm/fcrm/doctype/crm_dashboard/crm_dashboard.json b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.json
new file mode 100644
index 00000000..11dcdec4
--- /dev/null
+++ b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.json
@@ -0,0 +1,105 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:title",
+ "creation": "2025-07-14 12:19:49.725022",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "private",
+ "column_break_exbw",
+ "user",
+ "section_break_hfza",
+ "layout"
+ ],
+ "fields": [
+ {
+ "fieldname": "column_break_exbw",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_hfza",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "[]",
+ "fieldname": "layout",
+ "fieldtype": "Code",
+ "label": "Layout",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Name",
+ "unique": 1
+ },
+ {
+ "depends_on": "private",
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "mandatory_depends_on": "private",
+ "options": "User"
+ },
+ {
+ "default": "0",
+ "fieldname": "private",
+ "fieldtype": "Check",
+ "label": "Private"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2025-07-14 12:36:10.831351",
+ "modified_by": "Administrator",
+ "module": "FCRM",
+ "name": "CRM Dashboard",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "title"
+}
diff --git a/crm/fcrm/doctype/crm_dashboard/crm_dashboard.py b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.py
new file mode 100644
index 00000000..0f6ffaa3
--- /dev/null
+++ b/crm/fcrm/doctype/crm_dashboard/crm_dashboard.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+
+class CRMDashboard(Document):
+ pass
+
+
+def default_manager_dashboard_layout():
+ """
+ Returns the default layout for the CRM Manager Dashboard.
+ """
+ return '[{"name":"total_leads","type":"number_chart","tooltip":"Total number of leads","layout":{"x":0,"y":0,"w":4,"h":3,"i":"total_leads"}},{"name":"ongoing_deals","type":"number_chart","tooltip":"Total number of ongoing deals","layout":{"x":8,"y":0,"w":4,"h":3,"i":"ongoing_deals"}},{"name":"won_deals","type":"number_chart","tooltip":"Total number of won deals","layout":{"x":12,"y":0,"w":4,"h":3,"i":"won_deals"}},{"name":"average_won_deal_value","type":"number_chart","tooltip":"Average value of won deals","layout":{"x":16,"y":0,"w":4,"h":3,"i":"average_won_deal_value"}},{"name":"average_deal_value","type":"number_chart","tooltip":"Average deal value of ongoing and won deals","layout":{"x":0,"y":2,"w":4,"h":3,"i":"average_deal_value"}},{"name":"average_time_to_close_a_lead","type":"number_chart","tooltip":"Average time taken to close a lead","layout":{"x":4,"y":0,"w":4,"h":3,"i":"average_time_to_close_a_lead"}},{"name":"average_time_to_close_a_deal","type":"number_chart","layout":{"x":4,"y":2,"w":4,"h":3,"i":"average_time_to_close_a_deal"}},{"name":"spacer","type":"spacer","layout":{"x":8,"y":2,"w":12,"h":3,"i":"spacer"}},{"name":"sales_trend","type":"axis_chart","layout":{"x":0,"y":4,"w":10,"h":9,"i":"sales_trend"}},{"name":"forecasted_revenue","type":"axis_chart","layout":{"x":10,"y":4,"w":10,"h":9,"i":"forecasted_revenue"}},{"name":"funnel_conversion","type":"axis_chart","layout":{"x":0,"y":11,"w":10,"h":9,"i":"funnel_conversion"}},{"name":"deals_by_stage_donut","type":"donut_chart","layout":{"x":10,"y":11,"w":10,"h":9,"i":"deals_by_stage_donut"}},{"name":"lost_deal_reasons","type":"axis_chart","layout":{"x":0,"y":32,"w":20,"h":9,"i":"lost_deal_reasons"}},{"name":"leads_by_source","type":"donut_chart","layout":{"x":0,"y":18,"w":10,"h":9,"i":"leads_by_source"}},{"name":"deals_by_source","type":"donut_chart","layout":{"x":10,"y":18,"w":10,"h":9,"i":"deals_by_source"}},{"name":"deals_by_territory","type":"axis_chart","layout":{"x":0,"y":25,"w":10,"h":9,"i":"deals_by_territory"}},{"name":"deals_by_salesperson","type":"axis_chart","layout":{"x":10,"y":25,"w":10,"h":9,"i":"deals_by_salesperson"}}]'
+
+
+def create_default_manager_dashboard(force=False):
+ """
+ Creates the default CRM Manager Dashboard if it does not exist.
+ """
+ if not frappe.db.exists("CRM Dashboard", "Manager Dashboard"):
+ doc = frappe.new_doc("CRM Dashboard")
+ doc.title = "Manager Dashboard"
+ doc.layout = default_manager_dashboard_layout()
+ doc.insert(ignore_permissions=True)
+ elif force:
+ doc = frappe.get_doc("CRM Dashboard", "Manager Dashboard")
+ doc.layout = default_manager_dashboard_layout()
+ doc.save(ignore_permissions=True)
+ return doc.layout
diff --git a/crm/fcrm/doctype/crm_dashboard/test_crm_dashboard.py b/crm/fcrm/doctype/crm_dashboard/test_crm_dashboard.py
new file mode 100644
index 00000000..7e915613
--- /dev/null
+++ b/crm/fcrm/doctype/crm_dashboard/test_crm_dashboard.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests import IntegrationTestCase, UnitTestCase
+
+
+# On IntegrationTestCase, the doctype test records and all
+# link-field test record dependencies are recursively loaded
+# Use these module variables to add/remove to/from that list
+EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+
+
+class UnitTestCRMDashboard(UnitTestCase):
+ """
+ Unit tests for CRMDashboard.
+ Use this class for testing individual functions and methods.
+ """
+
+ pass
+
+
+class IntegrationTestCRMDashboard(IntegrationTestCase):
+ """
+ Integration tests for CRMDashboard.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.json b/crm/fcrm/doctype/crm_deal/crm_deal.json
index 1f9f604f..4ce81ec4 100644
--- a/crm/fcrm/doctype/crm_deal/crm_deal.json
+++ b/crm/fcrm/doctype/crm_deal/crm_deal.json
@@ -18,9 +18,11 @@
"lost_notes",
"section_break_jgpm",
"probability",
+ "expected_deal_value",
"deal_value",
"column_break_kpxa",
- "close_date",
+ "expected_closure_date",
+ "closed_date",
"contacts_tab",
"contacts",
"contact",
@@ -37,6 +39,7 @@
"column_break_xbyf",
"territory",
"currency",
+ "exchange_rate",
"annual_revenue",
"industry",
"person_section",
@@ -93,11 +96,6 @@
"fieldtype": "Data",
"label": "Website"
},
- {
- "fieldname": "close_date",
- "fieldtype": "Date",
- "label": "Close Date"
- },
{
"fieldname": "next_step",
"fieldtype": "Data",
@@ -409,12 +407,35 @@
"fieldtype": "Text",
"label": "Lost Notes",
"mandatory_depends_on": "eval: doc.lost_reason == \"Other\""
+ },
+ {
+ "default": "1",
+ "description": "The rate used to convert the deal\u2019s currency to your crm's base currency (set in CRM Settings). It is set once when the currency is first added and doesn't change automatically.",
+ "fieldname": "exchange_rate",
+ "fieldtype": "Float",
+ "label": "Exchange Rate"
+ },
+ {
+ "fieldname": "expected_deal_value",
+ "fieldtype": "Currency",
+ "label": "Expected Deal Value",
+ "options": "currency"
+ },
+ {
+ "fieldname": "expected_closure_date",
+ "fieldtype": "Date",
+ "label": "Expected Closure Date"
+ },
+ {
+ "fieldname": "closed_date",
+ "fieldtype": "Date",
+ "label": "Closed Date"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2025-07-05 12:25:05.927806",
+ "modified": "2025-07-13 11:54:20.608489",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Deal",
diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.py b/crm/fcrm/doctype/crm_deal/crm_deal.py
index 6e056ffe..f46f6475 100644
--- a/crm/fcrm/doctype/crm_deal/crm_deal.py
+++ b/crm/fcrm/doctype/crm_deal/crm_deal.py
@@ -10,6 +10,7 @@ from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
add_status_change_log,
)
+from crm.utils import get_exchange_rate
class CRMDeal(Document):
@@ -24,8 +25,11 @@ class CRMDeal(Document):
self.assign_agent(self.deal_owner)
if self.has_value_changed("status"):
add_status_change_log(self)
+ if frappe.db.get_value("CRM Deal Status", self.status, "type") == "Won":
+ self.closed_date = frappe.utils.nowdate()
self.validate_forcasting_fields()
self.validate_lost_reason()
+ self.update_exchange_rate()
def after_insert(self):
if self.deal_owner:
@@ -162,12 +166,21 @@ class CRMDeal(Document):
"""
Validate the lost reason if the status is set to "Lost".
"""
- if self.status == "Lost":
+ if self.status and frappe.get_cached_value("CRM Deal Status", self.status, "type") == "Lost":
if not self.lost_reason:
frappe.throw(_("Please specify a reason for losing the deal."), frappe.ValidationError)
elif self.lost_reason == "Other" and not self.lost_notes:
frappe.throw(_("Please specify the reason for losing the deal."), frappe.ValidationError)
+ def update_exchange_rate(self):
+ if self.has_value_changed("currency") or not self.exchange_rate:
+ system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
+ exchange_rate = 1
+ if self.currency and self.currency != system_currency:
+ exchange_rate = get_exchange_rate(self.currency, system_currency, frappe.utils.nowdate())
+
+ self.db_set("exchange_rate", exchange_rate)
+
@staticmethod
def default_list_data():
columns = [
diff --git a/crm/fcrm/doctype/crm_deal_status/crm_deal_status.json b/crm/fcrm/doctype/crm_deal_status/crm_deal_status.json
index d9b5f203..2af130de 100644
--- a/crm/fcrm/doctype/crm_deal_status/crm_deal_status.json
+++ b/crm/fcrm/doctype/crm_deal_status/crm_deal_status.json
@@ -7,9 +7,11 @@
"engine": "InnoDB",
"field_order": [
"deal_status",
- "color",
+ "type",
"position",
- "probability"
+ "column_break_ojiu",
+ "probability",
+ "color"
],
"fields": [
{
@@ -39,12 +41,24 @@
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Probability"
+ },
+ {
+ "default": "Open",
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Type",
+ "options": "Open\nOngoing\nOn Hold\nWon\nLost"
+ },
+ {
+ "fieldname": "column_break_ojiu",
+ "fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2025-07-01 12:06:42.937440",
+ "modified": "2025-07-11 16:03:28.077955",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Deal Status",
diff --git a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py
index 34a361e9..8a714d38 100644
--- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py
+++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py
@@ -160,7 +160,7 @@ def add_forecasting_section(layout, doctype):
"columns": [
{
"name": "column_" + str(random_string(4)),
- "fields": ["close_date", "probability", "deal_value"],
+ "fields": ["expected_closure_date", "probability", "expected_deal_value"],
}
],
},
diff --git a/crm/fcrm/doctype/crm_organization/crm_organization.json b/crm/fcrm/doctype/crm_organization/crm_organization.json
index 34252d1c..9486a0b4 100644
--- a/crm/fcrm/doctype/crm_organization/crm_organization.json
+++ b/crm/fcrm/doctype/crm_organization/crm_organization.json
@@ -10,6 +10,7 @@
"organization_name",
"no_of_employees",
"currency",
+ "exchange_rate",
"annual_revenue",
"organization_logo",
"column_break_pnpp",
@@ -74,12 +75,18 @@
"fieldtype": "Link",
"label": "Address",
"options": "Address"
+ },
+ {
+ "description": "The rate used to convert the organization\u2019s currency to your crm's base currency (set in CRM Settings). It is set once when the currency is first added and doesn't change automatically.",
+ "fieldname": "exchange_rate",
+ "fieldtype": "Float",
+ "label": "Exchange Rate"
}
],
"image_field": "organization_logo",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2024-09-17 18:37:10.341062",
+ "modified": "2025-07-15 11:40:12.175598",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Organization",
@@ -111,7 +118,8 @@
"write": 1
}
],
+ "row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/crm/fcrm/doctype/crm_organization/crm_organization.py b/crm/fcrm/doctype/crm_organization/crm_organization.py
index 471d0f51..1cdf186c 100644
--- a/crm/fcrm/doctype/crm_organization/crm_organization.py
+++ b/crm/fcrm/doctype/crm_organization/crm_organization.py
@@ -4,51 +4,65 @@
import frappe
from frappe.model.document import Document
+from crm.utils import get_exchange_rate
+
class CRMOrganization(Document):
- @staticmethod
- def default_list_data():
- columns = [
- {
- 'label': 'Organization',
- 'type': 'Data',
- 'key': 'organization_name',
- 'width': '16rem',
- },
- {
- 'label': 'Website',
- 'type': 'Data',
- 'key': 'website',
- 'width': '14rem',
- },
- {
- 'label': 'Industry',
- 'type': 'Link',
- 'key': 'industry',
- 'options': 'CRM Industry',
- 'width': '14rem',
- },
- {
- 'label': 'Annual Revenue',
- 'type': 'Currency',
- 'key': 'annual_revenue',
- 'width': '14rem',
- },
- {
- 'label': 'Last Modified',
- 'type': 'Datetime',
- 'key': 'modified',
- 'width': '8rem',
- },
- ]
- rows = [
- "name",
- "organization_name",
- "organization_logo",
- "website",
- "industry",
- "currency",
- "annual_revenue",
- "modified",
- ]
- return {'columns': columns, 'rows': rows}
+ def validate(self):
+ self.update_exchange_rate()
+
+ def update_exchange_rate(self):
+ if self.has_value_changed("currency") or not self.exchange_rate:
+ system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
+ exchange_rate = 1
+ if self.currency and self.currency != system_currency:
+ exchange_rate = get_exchange_rate(self.currency, system_currency, frappe.utils.nowdate())
+
+ self.db_set("exchange_rate", exchange_rate)
+
+ @staticmethod
+ def default_list_data():
+ columns = [
+ {
+ "label": "Organization",
+ "type": "Data",
+ "key": "organization_name",
+ "width": "16rem",
+ },
+ {
+ "label": "Website",
+ "type": "Data",
+ "key": "website",
+ "width": "14rem",
+ },
+ {
+ "label": "Industry",
+ "type": "Link",
+ "key": "industry",
+ "options": "CRM Industry",
+ "width": "14rem",
+ },
+ {
+ "label": "Annual Revenue",
+ "type": "Currency",
+ "key": "annual_revenue",
+ "width": "14rem",
+ },
+ {
+ "label": "Last Modified",
+ "type": "Datetime",
+ "key": "modified",
+ "width": "8rem",
+ },
+ ]
+ rows = [
+ "name",
+ "organization_name",
+ "organization_logo",
+ "website",
+ "industry",
+ "currency",
+ "annual_revenue",
+ "modified",
+ ]
+ return {"columns": columns, "rows": rows}
diff --git a/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.json b/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.json
index 36da12b1..15f55abd 100644
--- a/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.json
+++ b/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.json
@@ -13,6 +13,8 @@
"column_break_mwmz",
"duration",
"last_status_change_log",
+ "from_type",
+ "to_type",
"log_owner"
],
"fields": [
@@ -61,18 +63,31 @@
"fieldtype": "Link",
"label": "Owner",
"options": "User"
+ },
+ {
+ "fieldname": "from_type",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "From Type"
+ },
+ {
+ "fieldname": "to_type",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "To Type"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-01-06 13:26:40.597277",
+ "modified": "2025-07-13 12:37:41.278584",
"modified_by": "Administrator",
"module": "FCRM",
"name": "CRM Status Change Log",
"owner": "Administrator",
"permissions": [],
+ "row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.py b/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.py
index 2a3691e4..5ca24949 100644
--- a/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.py
+++ b/crm/fcrm/doctype/crm_status_change_log/crm_status_change_log.py
@@ -1,15 +1,17 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-import frappe
from datetime import datetime
-from frappe.utils import add_to_date, get_datetime
+
+import frappe
from frappe.model.document import Document
+from frappe.utils import add_to_date, get_datetime
class CRMStatusChangeLog(Document):
pass
+
def get_duration(from_date, to_date):
if not isinstance(from_date, datetime):
from_date = get_datetime(from_date)
@@ -18,28 +20,45 @@ def get_duration(from_date, to_date):
duration = to_date - from_date
return duration.total_seconds()
+
def add_status_change_log(doc):
+ to_status_type = frappe.db.get_value("CRM Deal Status", doc.status, "type") if doc.status else None
+
if not doc.is_new():
previous_status = doc.get_doc_before_save().status if doc.get_doc_before_save() else None
+ previous_status_type = (
+ frappe.db.get_value("CRM Deal Status", previous_status, "type") if previous_status else None
+ )
if not doc.status_change_log and previous_status:
now_minus_one_minute = add_to_date(datetime.now(), minutes=-1)
- doc.append("status_change_log", {
- "from": previous_status,
- "to": "",
- "from_date": now_minus_one_minute,
- "to_date": "",
- "log_owner": frappe.session.user,
- })
+ doc.append(
+ "status_change_log",
+ {
+ "from": previous_status,
+ "from_type": previous_status_type or "",
+ "to": "",
+ "to_type": "",
+ "from_date": now_minus_one_minute,
+ "to_date": "",
+ "log_owner": frappe.session.user,
+ },
+ )
last_status_change = doc.status_change_log[-1]
last_status_change.to = doc.status
+ last_status_change.to_type = to_status_type or ""
last_status_change.to_date = datetime.now()
last_status_change.log_owner = frappe.session.user
last_status_change.duration = get_duration(last_status_change.from_date, last_status_change.to_date)
- doc.append("status_change_log", {
- "from": doc.status,
- "to": "",
- "from_date": datetime.now(),
- "to_date": "",
- "log_owner": frappe.session.user,
- })
\ No newline at end of file
+ doc.append(
+ "status_change_log",
+ {
+ "from": doc.status,
+ "from_type": to_status_type or "",
+ "to": "",
+ "to_type": "",
+ "from_date": datetime.now(),
+ "to_date": "",
+ "log_owner": frappe.session.user,
+ },
+ )
diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
index e679b023..635c02d3 100644
--- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
+++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
@@ -8,6 +8,7 @@
"defaults_tab",
"restore_defaults",
"enable_forecasting",
+ "currency",
"branding_tab",
"brand_name",
"brand_logo",
@@ -60,16 +61,23 @@
},
{
"default": "0",
- "description": "It will make deal's \"Close Date\" & \"Deal Value\" mandatory to get accurate forecasting insights",
+ "description": "It will make deal's \"Expected Closure Date\" & \"Expected Deal Value\" mandatory to get accurate forecasting insights",
"fieldname": "enable_forecasting",
"fieldtype": "Check",
"label": "Enable Forecasting"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Currency",
+ "options": "Currency"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2025-07-01 13:20:48.757603",
+ "modified": "2025-07-13 11:58:34.857638",
"modified_by": "Administrator",
"module": "FCRM",
"name": "FCRM Settings",
diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py
index 1460c265..7897bb1a 100644
--- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py
+++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py
@@ -17,6 +17,7 @@ class FCRMSettings(Document):
def validate(self):
self.do_not_allow_to_delete_if_standard()
self.setup_forecasting()
+ self.make_currency_read_only()
def do_not_allow_to_delete_if_standard(self):
if not self.has_value_changed("dropdown_items"):
@@ -37,29 +38,39 @@ class FCRMSettings(Document):
delete_property_setter(
"CRM Deal",
"reqd",
- "close_date",
+ "expected_closure_date",
)
delete_property_setter(
"CRM Deal",
"reqd",
- "deal_value",
+ "expected_deal_value",
)
else:
make_property_setter(
"CRM Deal",
- "close_date",
+ "expected_closure_date",
"reqd",
1 if self.enable_forecasting else 0,
"Check",
)
make_property_setter(
"CRM Deal",
- "deal_value",
+ "expected_deal_value",
"reqd",
1 if self.enable_forecasting else 0,
"Check",
)
+ def make_currency_read_only(self):
+ if self.currency and self.has_value_changed("currency"):
+ make_property_setter(
+ "FCRM Settings",
+ "currency",
+ "read_only",
+ 1,
+ "Check",
+ )
+
def get_standard_dropdown_items():
return [item.get("name1") for item in frappe.get_hooks("standard_dropdown_items")]
diff --git a/crm/install.py b/crm/install.py
index ae0a437c..473ba5f6 100644
--- a/crm/install.py
+++ b/crm/install.py
@@ -4,6 +4,7 @@ import click
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from crm.fcrm.doctype.crm_dashboard.crm_dashboard import create_default_manager_dashboard
from crm.fcrm.doctype.crm_products.crm_products import create_product_details_script
@@ -23,6 +24,7 @@ def after_install(force=False):
add_default_lost_reasons()
add_standard_dropdown_items()
add_default_scripts()
+ create_default_manager_dashboard(force)
frappe.db.commit()
@@ -69,36 +71,43 @@ def add_default_deal_statuses():
statuses = {
"Qualification": {
"color": "gray",
+ "type": "Open",
"probability": 10,
"position": 1,
},
"Demo/Making": {
"color": "orange",
+ "type": "Ongoing",
"probability": 25,
"position": 2,
},
"Proposal/Quotation": {
"color": "blue",
+ "type": "Ongoing",
"probability": 50,
"position": 3,
},
"Negotiation": {
"color": "yellow",
+ "type": "Ongoing",
"probability": 70,
"position": 4,
},
"Ready to Close": {
"color": "purple",
+ "type": "Ongoing",
"probability": 90,
"position": 5,
},
"Won": {
"color": "green",
+ "type": "Won",
"probability": 100,
"position": 6,
},
"Lost": {
"color": "red",
+ "type": "Lost",
"probability": 0,
"position": 7,
},
@@ -111,6 +120,7 @@ def add_default_deal_statuses():
doc = frappe.new_doc("CRM Deal Status")
doc.deal_status = status
doc.color = statuses[status]["color"]
+ doc.type = statuses[status]["type"]
doc.probability = statuses[status]["probability"]
doc.position = statuses[status]["position"]
doc.insert()
diff --git a/crm/integrations/exotel/handler.py b/crm/integrations/exotel/handler.py
index 8809be6a..e070cbce 100644
--- a/crm/integrations/exotel/handler.py
+++ b/crm/integrations/exotel/handler.py
@@ -242,19 +242,18 @@ def get_call_log_status(call_payload, direction="inbound"):
elif status == "failed":
return "Failed"
- status = call_payload.get("DialCallStatus")
call_type = call_payload.get("CallType")
- dial_call_status = call_payload.get("DialCallStatus")
+ status = call_payload.get("DialCallStatus") or call_payload.get("Status")
- if call_type == "incomplete" and dial_call_status == "no-answer":
+ if call_type == "incomplete" and status == "no-answer":
status = "No Answer"
- elif call_type == "client-hangup" and dial_call_status == "canceled":
+ elif call_type == "client-hangup" and status == "canceled":
status = "Canceled"
- elif call_type == "incomplete" and dial_call_status == "failed":
+ elif call_type == "incomplete" and status == "failed":
status = "Failed"
elif call_type == "completed":
status = "Completed"
- elif dial_call_status == "busy":
+ elif status == "busy":
status = "Ringing"
return status
diff --git a/crm/locale/main.pot b/crm/locale/main.pot
index 82140d28..9c6a914f 100644
--- a/crm/locale/main.pot
+++ b/crm/locale/main.pot
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Frappe CRM VERSION\n"
"Report-Msgid-Bugs-To: shariq@frappe.io\n"
-"POT-Creation-Date: 2025-07-06 09:36+0000\n"
-"PO-Revision-Date: 2025-07-06 09:36+0000\n"
+"POT-Creation-Date: 2025-07-13 09:38+0000\n"
+"PO-Revision-Date: 2025-07-13 09:38+0000\n"
"Last-Translator: shariq@frappe.io\n"
"Language-Team: shariq@frappe.io\n"
"MIME-Version: 1.0\n"
@@ -197,8 +197,8 @@ msgstr ""
msgid "Actions"
msgstr ""
-#: frontend/src/pages/Deal.vue:572 frontend/src/pages/Lead.vue:453
-#: frontend/src/pages/MobileDeal.vue:453 frontend/src/pages/MobileLead.vue:345
+#: frontend/src/pages/Deal.vue:572 frontend/src/pages/Lead.vue:454
+#: frontend/src/pages/MobileDeal.vue:453 frontend/src/pages/MobileLead.vue:347
msgid "Activity"
msgstr ""
@@ -292,7 +292,7 @@ msgstr ""
msgid "Add to Holidays"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:422
+#: frontend/src/components/Layouts/AppSidebar.vue:434
msgid "Add your first comment"
msgstr ""
@@ -379,11 +379,11 @@ msgstr ""
msgid "Annual Revenue should be a number"
msgstr ""
-#: frontend/src/components/Settings/GeneralSettings.vue:64
+#: frontend/src/components/Settings/General/BrandSettings.vue:69
msgid "Appears in the left sidebar. Recommended size is 32x32 px in PNG or SVG"
msgstr ""
-#: frontend/src/components/Settings/GeneralSettings.vue:99
+#: frontend/src/components/Settings/General/BrandSettings.vue:103
msgid "Appears next to the title in your browser tab. Recommended size is 32x32 px in PNG or ICO"
msgstr ""
@@ -434,6 +434,10 @@ msgstr ""
msgid "Are you sure you want to reset 'Create Quotation from CRM Deal' Form Script?"
msgstr ""
+#: frontend/src/components/Settings/General/GeneralSettings.vue:137
+msgid "Are you sure you want to set the currency as {0}? This cannot be changed later."
+msgstr ""
+
#: frontend/src/components/DeleteLinkedDocModal.vue:243
msgid "Are you sure you want to unlink {0} linked item(s)?"
msgstr ""
@@ -466,7 +470,7 @@ msgstr ""
msgid "Assignment cleared successfully"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:565
+#: frontend/src/components/Layouts/AppSidebar.vue:577
msgid "Assignment rule"
msgstr ""
@@ -479,12 +483,12 @@ msgstr ""
msgid "Attach"
msgstr ""
-#: frontend/src/pages/Deal.vue:127 frontend/src/pages/Lead.vue:181
+#: frontend/src/pages/Deal.vue:127 frontend/src/pages/Lead.vue:182
msgid "Attach a file"
msgstr ""
-#: frontend/src/pages/Deal.vue:607 frontend/src/pages/Lead.vue:488
-#: frontend/src/pages/MobileDeal.vue:489 frontend/src/pages/MobileLead.vue:381
+#: frontend/src/pages/Deal.vue:607 frontend/src/pages/Lead.vue:489
+#: frontend/src/pages/MobileDeal.vue:489 frontend/src/pages/MobileLead.vue:383
msgid "Attachments"
msgstr ""
@@ -493,6 +497,22 @@ msgstr ""
msgid "Auth Token"
msgstr ""
+#: crm/api/dashboard.py:257
+msgid "Average deal value of ongoing & won deals"
+msgstr ""
+
+#: crm/api/dashboard.py:311
+msgid "Average time taken from lead creation to deal closure"
+msgstr ""
+
+#: crm/api/dashboard.py:255
+msgid "Avg Deal Value"
+msgstr ""
+
+#: crm/api/dashboard.py:309
+msgid "Avg Time to Close"
+msgstr ""
+
#: frontend/src/components/Activities/EmailArea.vue:72
#: frontend/src/components/EmailEditor.vue:44
#: frontend/src/components/EmailEditor.vue:69
@@ -518,10 +538,14 @@ msgstr ""
msgid "Between"
msgstr ""
-#: frontend/src/components/Settings/GeneralSettings.vue:34
+#: frontend/src/components/Settings/General/BrandSettings.vue:40
msgid "Brand name"
msgstr ""
+#: frontend/src/components/Settings/General/BrandSettings.vue:9
+msgid "Brand settings"
+msgstr ""
+
#. Label of the branding_tab (Tab Break) field in DocType 'FCRM Settings'
#: crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
msgid "Branding"
@@ -557,6 +581,10 @@ msgstr ""
msgid "CRM Contacts"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:511
+msgid "CRM Dashboard"
+msgstr ""
+
#. Name of a DocType
#: crm/fcrm/doctype/crm_deal/crm_deal.json
msgid "CRM Deal"
@@ -707,6 +735,10 @@ msgstr ""
msgid "CRM View Settings"
msgstr ""
+#: frontend/src/components/Settings/General/GeneralSettings.vue:47
+msgid "CRM currency for all monetary values. Once set, cannot be edited."
+msgstr ""
+
#: frontend/src/components/ViewControls.vue:272
msgid "CSV"
msgstr ""
@@ -725,7 +757,7 @@ msgstr ""
msgid "Call duration in seconds"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:539
+#: frontend/src/components/Layouts/AppSidebar.vue:551
msgid "Call log"
msgstr ""
@@ -752,8 +784,8 @@ msgstr ""
msgid "Calling..."
msgstr ""
-#: frontend/src/pages/Deal.vue:592 frontend/src/pages/Lead.vue:473
-#: frontend/src/pages/MobileDeal.vue:473 frontend/src/pages/MobileLead.vue:365
+#: frontend/src/pages/Deal.vue:592 frontend/src/pages/Lead.vue:474
+#: frontend/src/pages/MobileDeal.vue:473 frontend/src/pages/MobileLead.vue:367
msgid "Calls"
msgstr ""
@@ -783,7 +815,7 @@ msgstr ""
msgid "Cannot change role of user with Admin access"
msgstr ""
-#: crm/fcrm/doctype/fcrm_settings/fcrm_settings.py:32
+#: crm/fcrm/doctype/fcrm_settings/fcrm_settings.py:33
msgid "Cannot delete standard items {0}"
msgstr ""
@@ -791,11 +823,11 @@ msgstr ""
msgid "Capture"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:544
+#: frontend/src/components/Layouts/AppSidebar.vue:556
msgid "Capturing leads"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:473
+#: frontend/src/components/Layouts/AppSidebar.vue:485
msgid "Change"
msgstr ""
@@ -808,13 +840,13 @@ msgstr ""
msgid "Change Status"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:464
-#: frontend/src/components/Layouts/AppSidebar.vue:472
+#: frontend/src/components/Layouts/AppSidebar.vue:476
+#: frontend/src/components/Layouts/AppSidebar.vue:484
msgid "Change deal status"
msgstr ""
#: frontend/src/components/Settings/ProfileSettings.vue:26
-#: frontend/src/pages/Contact.vue:41 frontend/src/pages/Lead.vue:102
+#: frontend/src/pages/Contact.vue:41 frontend/src/pages/Lead.vue:103
#: frontend/src/pages/MobileContact.vue:37
#: frontend/src/pages/MobileOrganization.vue:37
#: frontend/src/pages/Organization.vue:41
@@ -823,7 +855,7 @@ msgstr ""
#: frontend/src/components/Modals/ConvertToDealModal.vue:43
#: frontend/src/components/Modals/ConvertToDealModal.vue:69
-#: frontend/src/pages/MobileLead.vue:123 frontend/src/pages/MobileLead.vue:150
+#: frontend/src/pages/MobileLead.vue:125 frontend/src/pages/MobileLead.vue:152
msgid "Choose Existing"
msgstr ""
@@ -839,7 +871,7 @@ msgstr ""
msgid "Choose the email service provider you want to configure."
msgstr ""
-#: frontend/src/components/Controls/Link.vue:60
+#: frontend/src/components/Controls/Link.vue:62
msgid "Clear"
msgstr ""
@@ -871,10 +903,15 @@ msgstr ""
msgid "Close Date"
msgstr ""
-#: crm/fcrm/doctype/crm_deal/crm_deal.py:159
+#: crm/fcrm/doctype/crm_deal/crm_deal.py:163
msgid "Close Date is required."
msgstr ""
+#. Label of the closed_on (Datetime) field in DocType 'CRM Deal'
+#: crm/fcrm/doctype/crm_deal/crm_deal.json
+msgid "Closed On"
+msgstr ""
+
#: frontend/src/components/Layouts/AppSidebar.vue:107
msgid "Collapse"
msgstr ""
@@ -910,15 +947,19 @@ msgstr ""
#: crm/fcrm/doctype/crm_notification/crm_notification.json
#: frontend/src/components/CommentBox.vue:80
#: frontend/src/components/CommunicationArea.vue:19
-#: frontend/src/components/Layouts/AppSidebar.vue:562
+#: frontend/src/components/Layouts/AppSidebar.vue:574
msgid "Comment"
msgstr ""
-#: frontend/src/pages/Deal.vue:582 frontend/src/pages/Lead.vue:463
-#: frontend/src/pages/MobileDeal.vue:463 frontend/src/pages/MobileLead.vue:355
+#: frontend/src/pages/Deal.vue:582 frontend/src/pages/Lead.vue:464
+#: frontend/src/pages/MobileDeal.vue:463 frontend/src/pages/MobileLead.vue:357
msgid "Comments"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:422
+msgid "Common reasons for losing deals"
+msgstr ""
+
#. Label of the communication_status (Link) field in DocType 'CRM Deal'
#. Label of the communication_status (Link) field in DocType 'CRM Lead'
#: crm/fcrm/doctype/crm_deal/crm_deal.json
@@ -951,7 +992,7 @@ msgstr ""
msgid "Condition"
msgstr ""
-#: frontend/src/components/Settings/GeneralSettings.vue:15
+#: frontend/src/components/Settings/General/GeneralSettings.vue:8
msgid "Configure general settings for your CRM"
msgstr ""
@@ -985,9 +1026,9 @@ msgstr ""
#. Label of the contact (Link) field in DocType 'CRM Deal'
#: crm/fcrm/doctype/crm_contacts/crm_contacts.json
#: crm/fcrm/doctype/crm_deal/crm_deal.json
-#: frontend/src/components/Layouts/AppSidebar.vue:535
+#: frontend/src/components/Layouts/AppSidebar.vue:547
#: frontend/src/components/Modals/ConvertToDealModal.vue:65
-#: frontend/src/pages/MobileLead.vue:146
+#: frontend/src/pages/MobileLead.vue:148
msgid "Contact"
msgstr ""
@@ -1064,22 +1105,22 @@ msgstr ""
msgid "Content is required"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:363
+#: frontend/src/components/Layouts/AppSidebar.vue:375
#: frontend/src/components/ListBulkActions.vue:88
#: frontend/src/components/Modals/ConvertToDealModal.vue:8
-#: frontend/src/pages/MobileLead.vue:62 frontend/src/pages/MobileLead.vue:109
+#: frontend/src/pages/MobileLead.vue:62 frontend/src/pages/MobileLead.vue:111
msgid "Convert"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:354
-#: frontend/src/components/Layouts/AppSidebar.vue:362
+#: frontend/src/components/Layouts/AppSidebar.vue:366
+#: frontend/src/components/Layouts/AppSidebar.vue:374
msgid "Convert lead to deal"
msgstr ""
#: frontend/src/components/ListBulkActions.vue:80
#: frontend/src/components/ListBulkActions.vue:195
#: frontend/src/components/Modals/ConvertToDealModal.vue:19
-#: frontend/src/pages/Lead.vue:53 frontend/src/pages/MobileLead.vue:105
+#: frontend/src/pages/Lead.vue:53 frontend/src/pages/MobileLead.vue:107
msgid "Convert to Deal"
msgstr ""
@@ -1096,6 +1137,11 @@ msgstr ""
msgid "Copied to clipboard"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:278 frontend/src/pages/Dashboard.vue:311
+#: frontend/src/pages/Dashboard.vue:429
+msgid "Count"
+msgstr ""
+
#: frontend/src/components/Modals/AddressModal.vue:99
#: frontend/src/components/Modals/CallLogModal.vue:102
#: frontend/src/components/Modals/ContactModal.vue:41
@@ -1133,7 +1179,7 @@ msgstr ""
msgid "Create Lead"
msgstr ""
-#: frontend/src/components/Controls/Link.vue:48
+#: frontend/src/components/Controls/Link.vue:50
#: frontend/src/components/Modals/EmailTemplateSelectorModal.vue:69
#: frontend/src/components/Modals/WhatsappTemplateSelectorModal.vue:45
#: frontend/src/components/SidePanelLayout.vue:140
@@ -1165,42 +1211,58 @@ msgstr ""
msgid "Create lead"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:332
+#: frontend/src/components/Layouts/AppSidebar.vue:344
msgid "Create your first lead"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:402
+#: frontend/src/components/Layouts/AppSidebar.vue:414
msgid "Create your first note"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:382
+#: frontend/src/components/Layouts/AppSidebar.vue:394
msgid "Create your first task"
msgstr ""
#. Label of the currency (Link) field in DocType 'CRM Deal'
#. Label of the currency (Link) field in DocType 'CRM Organization'
+#. Label of the currency (Link) field in DocType 'FCRM Settings'
#: crm/fcrm/doctype/crm_deal/crm_deal.json
#: crm/fcrm/doctype/crm_organization/crm_organization.json
+#: crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
+#: frontend/src/components/Settings/General/GeneralSettings.vue:43
msgid "Currency"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:574
+#. Label of the currency_exchange (Link) field in DocType 'CRM Organization'
+#: crm/fcrm/doctype/crm_organization/crm_organization.json
+msgid "Currency Exchange"
+msgstr ""
+
+#: frontend/src/components/Settings/General/GeneralSettings.vue:151
+msgid "Currency set as {0} successfully"
+msgstr ""
+
+#: frontend/src/pages/Dashboard.vue:481
+msgid "Current pipeline distribution"
+msgstr ""
+
+#: frontend/src/components/Layouts/AppSidebar.vue:586
msgid "Custom actions"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:524
+#: frontend/src/components/Layouts/AppSidebar.vue:536
msgid "Custom branding"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:573
+#: frontend/src/components/Layouts/AppSidebar.vue:585
msgid "Custom fields"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:576
+#: frontend/src/components/Layouts/AppSidebar.vue:588
msgid "Custom list actions"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:575
+#: frontend/src/components/Layouts/AppSidebar.vue:587
msgid "Custom statuses"
msgstr ""
@@ -1208,7 +1270,7 @@ msgstr ""
msgid "Customer created successfully"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:570
+#: frontend/src/components/Layouts/AppSidebar.vue:582
msgid "Customization"
msgstr ""
@@ -1216,10 +1278,14 @@ msgstr ""
msgid "Customize quick filters"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:270
+msgid "Daily performance of leads, deals, and wins"
+msgstr ""
+
#: frontend/src/components/Activities/DataFields.vue:6
-#: frontend/src/components/Layouts/AppSidebar.vue:563
-#: frontend/src/pages/Deal.vue:587 frontend/src/pages/Lead.vue:468
-#: frontend/src/pages/MobileDeal.vue:468 frontend/src/pages/MobileLead.vue:360
+#: frontend/src/components/Layouts/AppSidebar.vue:575
+#: frontend/src/pages/Deal.vue:587 frontend/src/pages/Lead.vue:469
+#: frontend/src/pages/MobileDeal.vue:468 frontend/src/pages/MobileLead.vue:362
msgid "Data"
msgstr ""
@@ -1230,10 +1296,11 @@ msgstr ""
#. Label of the date (Date) field in DocType 'CRM Holiday'
#: crm/fcrm/doctype/crm_holiday/crm_holiday.json
+#: frontend/src/pages/Dashboard.vue:272
msgid "Date"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:534
+#: frontend/src/components/Layouts/AppSidebar.vue:546
#: frontend/src/components/Settings/EmailTemplate/EditEmailTemplate.vue:54
#: frontend/src/components/Settings/EmailTemplate/EditEmailTemplate.vue:62
#: frontend/src/components/Settings/EmailTemplate/EmailTemplates.vue:78
@@ -1260,11 +1327,12 @@ msgid "Deal Statuses"
msgstr ""
#. Label of the deal_value (Currency) field in DocType 'CRM Deal'
-#: crm/fcrm/doctype/crm_deal/crm_deal.json
+#: crm/fcrm/doctype/crm_deal/crm_deal.json frontend/src/pages/Dashboard.vue:352
+#: frontend/src/pages/Dashboard.vue:392
msgid "Deal Value"
msgstr ""
-#: crm/fcrm/doctype/crm_deal/crm_deal.py:157
+#: crm/fcrm/doctype/crm_deal/crm_deal.py:161
msgid "Deal Value is required."
msgstr ""
@@ -1286,6 +1354,18 @@ msgstr ""
msgid "Deals"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:341
+msgid "Deals by Salesperson"
+msgstr ""
+
+#: frontend/src/pages/Dashboard.vue:480
+msgid "Deals by Stage"
+msgstr ""
+
+#: frontend/src/pages/Dashboard.vue:381
+msgid "Deals by Territory"
+msgstr ""
+
#: frontend/src/components/Settings/EmailTemplate/EditEmailTemplate.vue:115
#: frontend/src/components/Settings/EmailTemplate/NewEmailTemplate.vue:115
msgid "Dear {{ lead_name }}, \\n\\nThis is a reminder for the payment of {{ grand_total }}. \\n\\nThanks, \\nFrappé"
@@ -1370,7 +1450,7 @@ msgstr ""
#: frontend/src/components/ViewControls.vue:1161
#: frontend/src/components/ViewControls.vue:1172
#: frontend/src/pages/Contact.vue:103 frontend/src/pages/Deal.vue:134
-#: frontend/src/pages/Lead.vue:190 frontend/src/pages/MobileContact.vue:82
+#: frontend/src/pages/Lead.vue:191 frontend/src/pages/MobileContact.vue:82
#: frontend/src/pages/MobileContact.vue:266
#: frontend/src/pages/MobileDeal.vue:538
#: frontend/src/pages/MobileOrganization.vue:72
@@ -1451,7 +1531,7 @@ msgstr ""
#: crm/fcrm/doctype/crm_lead/crm_lead.json
#: crm/fcrm/doctype/crm_lead_source/crm_lead_source.json
#: frontend/src/pages/MobileContact.vue:286
-#: frontend/src/pages/MobileDeal.vue:447 frontend/src/pages/MobileLead.vue:339
+#: frontend/src/pages/MobileDeal.vue:447 frontend/src/pages/MobileLead.vue:341
#: frontend/src/pages/MobileOrganization.vue:323
msgid "Details"
msgstr ""
@@ -1564,7 +1644,7 @@ msgstr ""
msgid "Duration"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:587
+#: frontend/src/components/Layouts/AppSidebar.vue:599
#: frontend/src/components/Settings/Settings.vue:135
msgid "ERPNext"
msgstr ""
@@ -1706,7 +1786,7 @@ msgstr ""
msgid "Email accounts"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:561
+#: frontend/src/components/Layouts/AppSidebar.vue:573
msgid "Email communication"
msgstr ""
@@ -1714,7 +1794,7 @@ msgstr ""
msgid "Email from Lead"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:540
+#: frontend/src/components/Layouts/AppSidebar.vue:552
msgid "Email template"
msgstr ""
@@ -1722,8 +1802,8 @@ msgstr ""
msgid "Email templates"
msgstr ""
-#: frontend/src/pages/Deal.vue:577 frontend/src/pages/Lead.vue:458
-#: frontend/src/pages/MobileDeal.vue:458 frontend/src/pages/MobileLead.vue:350
+#: frontend/src/pages/Deal.vue:577 frontend/src/pages/Lead.vue:459
+#: frontend/src/pages/MobileDeal.vue:458 frontend/src/pages/MobileLead.vue:352
msgid "Emails"
msgstr ""
@@ -1756,6 +1836,10 @@ msgstr ""
msgid "Enable Outgoing"
msgstr ""
+#: frontend/src/components/Settings/General/GeneralSettings.vue:19
+msgid "Enable forecasting"
+msgstr ""
+
#. Label of the enabled (Check) field in DocType 'CRM Exotel Settings'
#. Label of the enabled (Check) field in DocType 'CRM Form Script'
#. Label of the enabled (Check) field in DocType 'CRM Service Level Agreement'
@@ -1814,7 +1898,7 @@ msgstr ""
msgid "Error updating document"
msgstr ""
-#: frontend/src/pages/Lead.vue:398
+#: frontend/src/pages/Lead.vue:399
msgid "Error updating lead"
msgstr ""
@@ -1835,13 +1919,18 @@ msgstr ""
msgid "Excel"
msgstr ""
+#. Label of the exchange_rate (Float) field in DocType 'CRM Deal'
+#: crm/fcrm/doctype/crm_deal/crm_deal.json
+msgid "Exchange Rate"
+msgstr ""
+
#. Option for the 'Telephony Medium' (Select) field in DocType 'CRM Call Log'
#. Label of the exotel (Check) field in DocType 'CRM Telephony Agent'
#. Option for the 'Default Medium' (Select) field in DocType 'CRM Telephony
#. Agent'
#: crm/fcrm/doctype/crm_call_log/crm_call_log.json
#: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json
-#: frontend/src/components/Layouts/AppSidebar.vue:585
+#: frontend/src/components/Layouts/AppSidebar.vue:597
#: frontend/src/components/Settings/TelephonySettings.vue:41
#: frontend/src/components/Settings/TelephonySettings.vue:63
msgid "Exotel"
@@ -1933,6 +2022,10 @@ msgstr ""
msgid "Failed to delete template"
msgstr ""
+#: crm/utils/__init__.py:285
+msgid "Failed to fetch historical exchange rate from external API. Please try again later."
+msgstr ""
+
#: frontend/src/data/script.js:110
msgid "Failed to load form controller: {0}"
msgstr ""
@@ -1964,7 +2057,7 @@ msgstr ""
#. Label of the favicon (Attach) field in DocType 'FCRM Settings'
#: crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
-#: frontend/src/components/Settings/GeneralSettings.vue:77
+#: frontend/src/components/Settings/General/BrandSettings.vue:81
msgid "Favicon"
msgstr ""
@@ -2052,6 +2145,14 @@ msgstr ""
msgid "For"
msgstr ""
+#: frontend/src/components/Settings/General/GeneralSettings.vue:128
+msgid "Forecasting disabled successfully"
+msgstr ""
+
+#: frontend/src/components/Settings/General/GeneralSettings.vue:127
+msgid "Forecasting enabled successfully"
+msgstr ""
+
#. Option for the 'Apply To' (Select) field in DocType 'CRM Form Script'
#: crm/fcrm/doctype/crm_form_script/crm_form_script.json
msgid "Form"
@@ -2066,7 +2167,7 @@ msgstr ""
msgid "Frappe CRM"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:591
+#: frontend/src/components/Layouts/AppSidebar.vue:603
msgid "Frappe CRM mobile"
msgstr ""
@@ -2110,6 +2211,10 @@ msgstr ""
msgid "Full Name"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:303
+msgid "Funnel Conversion"
+msgstr ""
+
#. Label of the gender (Link) field in DocType 'CRM Contacts'
#. Label of the gender (Link) field in DocType 'CRM Deal'
#. Label of the gender (Link) field in DocType 'CRM Lead'
@@ -2119,16 +2224,20 @@ msgstr ""
msgid "Gender"
msgstr ""
-#: frontend/src/components/Settings/GeneralSettings.vue:6
+#: frontend/src/components/Settings/General/GeneralSettings.vue:5
#: frontend/src/components/Settings/Settings.vue:89
msgid "General"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:382
+msgid "Geographic distribution of deals and revenue"
+msgstr ""
+
#: frontend/src/components/Modals/AboutModal.vue:57
msgid "GitHub Repository"
msgstr ""
-#: frontend/src/pages/Deal.vue:114 frontend/src/pages/Lead.vue:166
+#: frontend/src/pages/Deal.vue:114 frontend/src/pages/Lead.vue:167
msgid "Go to website"
msgstr ""
@@ -2216,8 +2325,8 @@ msgstr ""
msgid "Holidays"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:525
-#: frontend/src/components/Settings/GeneralSettings.vue:112
+#: frontend/src/components/Layouts/AppSidebar.vue:537
+#: frontend/src/components/Settings/General/HomeActions.vue:9
msgid "Home actions"
msgstr ""
@@ -2317,7 +2426,7 @@ msgstr ""
msgid "Initiating call..."
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:581
+#: frontend/src/components/Layouts/AppSidebar.vue:593
msgid "Integration"
msgstr ""
@@ -2330,8 +2439,8 @@ msgctxt "FCRM"
msgid "Integrations"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:512
-#: frontend/src/components/Layouts/AppSidebar.vue:515
+#: frontend/src/components/Layouts/AppSidebar.vue:524
+#: frontend/src/components/Layouts/AppSidebar.vue:527
msgid "Introduction"
msgstr ""
@@ -2372,7 +2481,7 @@ msgstr ""
msgid "Invite by email"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:526
+#: frontend/src/components/Layouts/AppSidebar.vue:538
msgid "Invite users"
msgstr ""
@@ -2380,7 +2489,7 @@ msgstr ""
msgid "Invite users to access CRM. Specify their roles to control access and permissions"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:342
+#: frontend/src/components/Layouts/AppSidebar.vue:354
msgid "Invite your team"
msgstr ""
@@ -2543,7 +2652,7 @@ msgstr ""
#. Label of the lead (Link) field in DocType 'CRM Deal'
#: crm/fcrm/doctype/crm_deal/crm_deal.json
-#: frontend/src/components/Layouts/AppSidebar.vue:533
+#: frontend/src/components/Layouts/AppSidebar.vue:545
#: frontend/src/components/Settings/EmailTemplate/EditEmailTemplate.vue:58
#: frontend/src/components/Settings/EmailTemplate/EmailTemplates.vue:77
#: frontend/src/components/Settings/EmailTemplate/NewEmailTemplate.vue:58
@@ -2581,16 +2690,28 @@ msgstr ""
msgid "Lead Statuses"
msgstr ""
-#: frontend/src/pages/Lead.vue:394 frontend/src/pages/MobileLead.vue:280
+#: frontend/src/pages/Dashboard.vue:503
+msgid "Lead generation channel analysis"
+msgstr ""
+
+#: frontend/src/pages/Dashboard.vue:304
+msgid "Lead to deal conversion pipeline"
+msgstr ""
+
+#: frontend/src/pages/Lead.vue:395 frontend/src/pages/MobileLead.vue:282
msgid "Lead updated successfully"
msgstr ""
#. Label of a shortcut in the Frappe CRM Workspace
#: crm/fcrm/workspace/frappe_crm/frappe_crm.json
-#: frontend/src/pages/Lead.vue:413 frontend/src/pages/MobileLead.vue:299
+#: frontend/src/pages/Lead.vue:414 frontend/src/pages/MobileLead.vue:301
msgid "Leads"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:502
+msgid "Leads by Source"
+msgstr ""
+
#. Label of the lft (Int) field in DocType 'CRM Territory'
#: crm/fcrm/doctype/crm_territory/crm_territory.json
msgid "Left"
@@ -2661,10 +2782,14 @@ msgstr ""
#. Label of the brand_logo (Attach) field in DocType 'FCRM Settings'
#: crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
-#: frontend/src/components/Settings/GeneralSettings.vue:42
+#: frontend/src/components/Settings/General/BrandSettings.vue:47
msgid "Logo"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:421
+msgid "Lost Deal Reasons"
+msgstr ""
+
#. Label of the lost_notes (Text) field in DocType 'CRM Deal'
#: crm/fcrm/doctype/crm_deal/crm_deal.json
msgid "Lost Notes"
@@ -2718,7 +2843,7 @@ msgstr ""
msgid "Make a Call"
msgstr ""
-#: frontend/src/pages/Deal.vue:94 frontend/src/pages/Lead.vue:135
+#: frontend/src/pages/Deal.vue:94 frontend/src/pages/Lead.vue:136
msgid "Make a call"
msgstr ""
@@ -2746,6 +2871,10 @@ msgstr ""
msgid "Make {0} as default calling medium"
msgstr ""
+#: frontend/src/components/Settings/General/GeneralSettings.vue:23
+msgid "Makes \"Close Date\" and \"Deal Value\" mandatory for deal value forecasting"
+msgstr ""
+
#: frontend/src/components/Settings/Users.vue:11
msgid "Manage CRM users by adding or inviting them, and assign roles to control their access and permissions"
msgstr ""
@@ -2779,7 +2908,7 @@ msgstr ""
msgid "Mark all as read"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:530
+#: frontend/src/components/Layouts/AppSidebar.vue:542
msgid "Masters"
msgstr ""
@@ -2830,7 +2959,7 @@ msgstr ""
msgid "Mobile Number Missing"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:594
+#: frontend/src/components/Layouts/AppSidebar.vue:606
msgid "Mobile app installation"
msgstr ""
@@ -2847,6 +2976,10 @@ msgstr ""
msgid "Monday"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:450
+msgid "Month"
+msgstr ""
+
#: frontend/src/components/FieldLayoutEditor.vue:454
msgid "Move to next section"
msgstr ""
@@ -2972,12 +3105,12 @@ msgid "New WhatsApp Message"
msgstr ""
#: frontend/src/components/Modals/ConvertToDealModal.vue:81
-#: frontend/src/pages/MobileLead.vue:163
+#: frontend/src/pages/MobileLead.vue:165
msgid "New contact will be created based on the person's details"
msgstr ""
#: frontend/src/components/Modals/ConvertToDealModal.vue:56
-#: frontend/src/pages/MobileLead.vue:137
+#: frontend/src/pages/MobileLead.vue:139
msgid "New organization will be created based on the data in details section"
msgstr ""
@@ -3042,7 +3175,7 @@ msgstr ""
msgid "No contacts added"
msgstr ""
-#: frontend/src/pages/Deal.vue:107 frontend/src/pages/Lead.vue:157
+#: frontend/src/pages/Deal.vue:107 frontend/src/pages/Lead.vue:158
msgid "No email set"
msgstr ""
@@ -3063,7 +3196,7 @@ msgstr ""
msgid "No new notifications"
msgstr ""
-#: frontend/src/pages/Lead.vue:142
+#: frontend/src/pages/Lead.vue:143
msgid "No phone number set"
msgstr ""
@@ -3090,7 +3223,7 @@ msgstr ""
msgid "No website found"
msgstr ""
-#: frontend/src/pages/Deal.vue:120 frontend/src/pages/Lead.vue:172
+#: frontend/src/pages/Deal.vue:120 frontend/src/pages/Lead.vue:173
msgid "No website set"
msgstr ""
@@ -3120,6 +3253,10 @@ msgstr ""
msgid "Normal"
msgstr ""
+#: crm/utils/__init__.py:263
+msgid "Not Allowed"
+msgstr ""
+
#: frontend/src/components/Filter.vue:273
#: frontend/src/components/Filter.vue:294
#: frontend/src/components/Filter.vue:311
@@ -3149,13 +3286,13 @@ msgstr ""
#: frontend/src/components/Modals/DataFieldsModal.vue:10
#: frontend/src/components/Modals/QuickEntryModal.vue:10
#: frontend/src/components/Modals/SidePanelModal.vue:10
-#: frontend/src/components/Settings/GeneralSettings.vue:9
+#: frontend/src/components/Settings/General/BrandSettings.vue:16
#: frontend/src/components/Settings/SettingsPage.vue:11
#: frontend/src/components/Settings/TelephonySettings.vue:11
msgid "Not Saved"
msgstr ""
-#: crm/fcrm/doctype/crm_deal/crm_deal.py:249
+#: crm/fcrm/doctype/crm_deal/crm_deal.py:262
msgid "Not allowed to add contact to Deal"
msgstr ""
@@ -3163,26 +3300,26 @@ msgstr ""
msgid "Not allowed to convert Lead to Deal"
msgstr ""
-#: crm/fcrm/doctype/crm_deal/crm_deal.py:260
+#: crm/fcrm/doctype/crm_deal/crm_deal.py:273
msgid "Not allowed to remove contact from Deal"
msgstr ""
-#: crm/fcrm/doctype/crm_deal/crm_deal.py:271
+#: crm/fcrm/doctype/crm_deal/crm_deal.py:284
msgid "Not allowed to set primary contact for Deal"
msgstr ""
-#: frontend/src/pages/Deal.vue:458 frontend/src/pages/Lead.vue:361
+#: frontend/src/pages/Deal.vue:458 frontend/src/pages/Lead.vue:362
msgid "Not permitted"
msgstr ""
#. Label of the note (Link) field in DocType 'CRM Call Log'
#: crm/fcrm/doctype/crm_call_log/crm_call_log.json
-#: frontend/src/components/Layouts/AppSidebar.vue:537
+#: frontend/src/components/Layouts/AppSidebar.vue:549
msgid "Note"
msgstr ""
-#: frontend/src/pages/Deal.vue:602 frontend/src/pages/Lead.vue:483
-#: frontend/src/pages/MobileDeal.vue:484 frontend/src/pages/MobileLead.vue:376
+#: frontend/src/pages/Deal.vue:602 frontend/src/pages/Lead.vue:484
+#: frontend/src/pages/MobileDeal.vue:484 frontend/src/pages/MobileLead.vue:378
msgid "Notes"
msgstr ""
@@ -3191,7 +3328,7 @@ msgid "Notes View"
msgstr ""
#: frontend/src/components/Activities/EmailArea.vue:13
-#: frontend/src/components/Layouts/AppSidebar.vue:566
+#: frontend/src/components/Layouts/AppSidebar.vue:578
msgid "Notification"
msgstr ""
@@ -3224,6 +3361,10 @@ msgstr ""
msgid "Number"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:349 frontend/src/pages/Dashboard.vue:389
+msgid "Number of Deals"
+msgstr ""
+
#. Label of the old_parent (Link) field in DocType 'CRM Territory'
#: crm/fcrm/doctype/crm_territory/crm_territory.json
msgid "Old Parent"
@@ -3233,7 +3374,7 @@ msgstr ""
msgid "Only image files are allowed"
msgstr ""
-#: crm/fcrm/doctype/crm_deal/crm_deal.py:58
+#: crm/fcrm/doctype/crm_deal/crm_deal.py:62
#: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.py:23
msgid "Only one {0} can be set as primary."
msgstr ""
@@ -3283,10 +3424,10 @@ msgstr ""
#. Label of the organization (Data) field in DocType 'CRM Lead'
#: crm/fcrm/doctype/crm_deal/crm_deal.json
#: crm/fcrm/doctype/crm_lead/crm_lead.json
-#: frontend/src/components/Layouts/AppSidebar.vue:536
+#: frontend/src/components/Layouts/AppSidebar.vue:548
#: frontend/src/components/Modals/ConvertToDealModal.vue:39
#: frontend/src/pages/Contact.vue:507 frontend/src/pages/MobileContact.vue:505
-#: frontend/src/pages/MobileLead.vue:119
+#: frontend/src/pages/MobileLead.vue:121
#: frontend/src/pages/MobileOrganization.vue:449
#: frontend/src/pages/MobileOrganization.vue:503
#: frontend/src/pages/Organization.vue:458
@@ -3324,7 +3465,7 @@ msgstr ""
msgid "Organizations"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:558
+#: frontend/src/components/Layouts/AppSidebar.vue:570
msgid "Other features"
msgstr ""
@@ -3388,6 +3529,10 @@ msgstr ""
msgid "Pending Invites"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:35
+msgid "Period"
+msgstr ""
+
#. Label of the person_section (Section Break) field in DocType 'CRM Deal'
#. Label of the person_tab (Tab Break) field in DocType 'CRM Lead'
#: crm/fcrm/doctype/crm_deal/crm_deal.json
@@ -3424,7 +3569,7 @@ msgstr ""
msgid "Pinned Views"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:554
+#: frontend/src/components/Layouts/AppSidebar.vue:566
msgid "Pinned view"
msgstr ""
@@ -3432,7 +3577,7 @@ msgstr ""
msgid "Playback speed"
msgstr ""
-#: frontend/src/components/Settings/EmailAccountList.vue:42
+#: frontend/src/components/Settings/EmailAccountList.vue:49
msgid "Please add an email account to continue."
msgstr ""
@@ -3449,12 +3594,12 @@ msgid "Please provide a reason for marking this deal as lost"
msgstr ""
#: frontend/src/components/Modals/ConvertToDealModal.vue:145
-#: frontend/src/pages/MobileLead.vue:438
+#: frontend/src/pages/MobileLead.vue:440
msgid "Please select an existing contact"
msgstr ""
#: frontend/src/components/Modals/ConvertToDealModal.vue:150
-#: frontend/src/pages/MobileLead.vue:443
+#: frontend/src/pages/MobileLead.vue:445
msgid "Please select an existing organization"
msgstr ""
@@ -3462,11 +3607,11 @@ msgstr ""
msgid "Please setup Exotel intergration"
msgstr ""
-#: crm/fcrm/doctype/crm_deal/crm_deal.py:167
+#: crm/fcrm/doctype/crm_deal/crm_deal.py:171
msgid "Please specify a reason for losing the deal."
msgstr ""
-#: crm/fcrm/doctype/crm_deal/crm_deal.py:169
+#: crm/fcrm/doctype/crm_deal/crm_deal.py:173
msgid "Please specify the reason for losing the deal."
msgstr ""
@@ -3550,7 +3695,7 @@ msgstr ""
msgid "Products"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:523
+#: frontend/src/components/Layouts/AppSidebar.vue:535
#: frontend/src/components/Settings/Settings.vue:79
msgid "Profile"
msgstr ""
@@ -3559,6 +3704,10 @@ msgstr ""
msgid "Profile updated successfully"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:448
+msgid "Projected vs actual revenue based on deal probability"
+msgstr ""
+
#. Label of the public (Check) field in DocType 'CRM View Settings'
#: crm/fcrm/doctype/crm_view_settings/crm_view_settings.json
msgid "Public"
@@ -3568,7 +3717,7 @@ msgstr ""
msgid "Public Views"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:553
+#: frontend/src/components/Layouts/AppSidebar.vue:565
msgid "Public view"
msgstr ""
@@ -3596,7 +3745,7 @@ msgstr ""
msgid "Quick Filters updated successfully"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:577
+#: frontend/src/components/Layouts/AppSidebar.vue:589
msgid "Quick entry layout"
msgstr ""
@@ -3610,6 +3759,10 @@ msgstr ""
msgid "Read"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:424
+msgid "Reason"
+msgstr ""
+
#. Label of the record_calls (Check) field in DocType 'CRM Twilio Settings'
#: crm/fcrm/doctype/crm_twilio_settings/crm_twilio_settings.json
msgid "Record Calls"
@@ -3683,7 +3836,7 @@ msgid "Remove column"
msgstr ""
#: frontend/src/components/Settings/ProfileSettings.vue:32
-#: frontend/src/pages/Contact.vue:47 frontend/src/pages/Lead.vue:108
+#: frontend/src/pages/Contact.vue:47 frontend/src/pages/Lead.vue:109
#: frontend/src/pages/MobileContact.vue:43
#: frontend/src/pages/MobileOrganization.vue:43
#: frontend/src/pages/Organization.vue:47
@@ -3772,6 +3925,14 @@ msgstr ""
msgid "Retake"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:456
+msgid "Revenue"
+msgstr ""
+
+#: frontend/src/pages/Dashboard.vue:447
+msgid "Revenue Forecast"
+msgstr ""
+
#: frontend/src/components/Settings/EmailTemplate/EditEmailTemplate.vue:84
#: frontend/src/components/Settings/EmailTemplate/NewEmailTemplate.vue:84
msgid "Rich Text"
@@ -3877,6 +4038,10 @@ msgstr ""
msgid "Sales Manager"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:269
+msgid "Sales Trend"
+msgstr ""
+
#. Name of a role
#. Option for the 'Role' (Select) field in DocType 'CRM Invitation'
#: crm/fcrm/doctype/crm_call_log/crm_call_log.json
@@ -3904,9 +4069,14 @@ msgstr ""
#: frontend/src/components/Settings/Users.vue:186
#: frontend/src/components/Settings/Users.vue:268
#: frontend/src/components/Settings/Users.vue:271
+#: frontend/src/pages/Dashboard.vue:62
msgid "Sales User"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:344
+msgid "Salesperson"
+msgstr ""
+
#. Label of the salutation (Link) field in DocType 'CRM Deal'
#. Label of the salutation (Link) field in DocType 'CRM Lead'
#: crm/fcrm/doctype/crm_deal/crm_deal.json
@@ -3930,6 +4100,7 @@ msgstr ""
#: frontend/src/components/Modals/LostReasonModal.vue:44
#: frontend/src/components/Modals/QuickEntryModal.vue:26
#: frontend/src/components/Modals/SidePanelModal.vue:26
+#: frontend/src/components/Settings/General/GeneralSettings.vue:145
#: frontend/src/components/Telephony/ExotelCallUI.vue:231
#: frontend/src/components/ViewControls.vue:123
msgid "Save"
@@ -3945,7 +4116,7 @@ msgstr ""
msgid "Saved Views"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:552
+#: frontend/src/components/Layouts/AppSidebar.vue:564
msgid "Saved view"
msgstr ""
@@ -3970,6 +4141,14 @@ msgstr ""
msgid "Section"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:15
+msgid "Select Range"
+msgstr ""
+
+#: frontend/src/components/Settings/General/GeneralSettings.vue:63
+msgid "Select currency"
+msgstr ""
+
#: frontend/src/components/FieldLayout/Field.vue:332
msgid "Select {0}"
msgstr ""
@@ -3986,11 +4165,11 @@ msgstr ""
msgid "Send Template"
msgstr ""
-#: frontend/src/pages/Deal.vue:101 frontend/src/pages/Lead.vue:151
+#: frontend/src/pages/Deal.vue:101 frontend/src/pages/Lead.vue:152
msgid "Send an email"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:443
+#: frontend/src/components/Layouts/AppSidebar.vue:455
msgid "Send email"
msgstr ""
@@ -4008,7 +4187,7 @@ msgstr ""
msgid "Series"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:564
+#: frontend/src/components/Layouts/AppSidebar.vue:576
msgid "Service level agreement"
msgstr ""
@@ -4032,11 +4211,15 @@ msgstr ""
msgid "Set as default"
msgstr ""
-#: frontend/src/pages/Lead.vue:129
+#: frontend/src/components/Settings/General/GeneralSettings.vue:136
+msgid "Set currency"
+msgstr ""
+
+#: frontend/src/pages/Lead.vue:130
msgid "Set first name"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:516
+#: frontend/src/components/Layouts/AppSidebar.vue:528
msgid "Setting up"
msgstr ""
@@ -4082,7 +4265,7 @@ msgstr ""
#. Label of the defaults_tab (Tab Break) field in DocType 'FCRM Settings'
#: crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
-#: frontend/src/components/Layouts/AppSidebar.vue:520
+#: frontend/src/components/Layouts/AppSidebar.vue:532
#: frontend/src/components/Settings/Settings.vue:11
#: frontend/src/components/Settings/Settings.vue:75
msgid "Settings"
@@ -4092,7 +4275,7 @@ msgstr ""
msgid "Setup Email"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:322
+#: frontend/src/components/Layouts/AppSidebar.vue:334
msgid "Setup your password"
msgstr ""
@@ -4153,6 +4336,10 @@ msgstr ""
msgid "Source Name"
msgstr ""
+#: frontend/src/pages/Dashboard.vue:306
+msgid "Stage"
+msgstr ""
+
#: crm/fcrm/doctype/crm_form_script/crm_form_script.js:15
msgid "Standard Form Scripts can not be modified, duplicate the Form Script instead."
msgstr ""
@@ -4284,12 +4471,12 @@ msgstr ""
#. Option for the 'Type' (Select) field in DocType 'CRM Notification'
#: crm/fcrm/doctype/crm_notification/crm_notification.json
-#: frontend/src/components/Layouts/AppSidebar.vue:538
+#: frontend/src/components/Layouts/AppSidebar.vue:550
msgid "Task"
msgstr ""
-#: frontend/src/pages/Deal.vue:597 frontend/src/pages/Lead.vue:478
-#: frontend/src/pages/MobileDeal.vue:479 frontend/src/pages/MobileLead.vue:371
+#: frontend/src/pages/Deal.vue:597 frontend/src/pages/Lead.vue:479
+#: frontend/src/pages/MobileDeal.vue:479 frontend/src/pages/MobileLead.vue:373
msgid "Tasks"
msgstr ""
@@ -4349,6 +4536,7 @@ msgstr ""
#: crm/fcrm/doctype/crm_deal/crm_deal.json
#: crm/fcrm/doctype/crm_lead/crm_lead.json
#: crm/fcrm/doctype/crm_organization/crm_organization.json
+#: frontend/src/pages/Dashboard.vue:384
msgid "Territory"
msgstr ""
@@ -4366,6 +4554,11 @@ msgstr ""
msgid "The Condition '{0}' is invalid: {1}"
msgstr ""
+#. Description of the 'Exchange Rate' (Float) field in DocType 'CRM Deal'
+#: crm/fcrm/doctype/crm_deal/crm_deal.json
+msgid "The rate used to convert the deal’s currency to your crm's base currency (set in CRM Settings). It is set once when the currency is first added and doesn't change automatically."
+msgstr ""
+
#: crm/fcrm/doctype/crm_service_level_agreement/crm_service_level_agreement.js:14
msgid "There can only be one default priority in Priorities table"
msgstr ""
@@ -4486,11 +4679,19 @@ msgstr ""
msgid "Total"
msgstr ""
+#: crm/api/dashboard.py:144
+msgid "Total Deals"
+msgstr ""
+
#. Label of the total_holidays (Int) field in DocType 'CRM Holiday List'
#: crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.json
msgid "Total Holidays"
msgstr ""
+#: crm/api/dashboard.py:86
+msgid "Total Leads"
+msgstr ""
+
#. Description of the 'Net Total' (Currency) field in DocType 'CRM Deal'
#. Description of the 'Net Total' (Currency) field in DocType 'CRM Lead'
#: crm/fcrm/doctype/crm_deal/crm_deal.json
@@ -4498,6 +4699,18 @@ msgstr ""
msgid "Total after discount"
msgstr ""
+#: crm/api/dashboard.py:149
+msgid "Total number of deals"
+msgstr ""
+
+#: crm/api/dashboard.py:91
+msgid "Total number of leads"
+msgstr ""
+
+#: crm/api/dashboard.py:207
+msgid "Total number of won deals"
+msgstr ""
+
#. Option for the 'Weekly Off' (Select) field in DocType 'CRM Holiday List'
#. Option for the 'Workday' (Select) field in DocType 'CRM Service Day'
#: crm/fcrm/doctype/crm_holiday_list/crm_holiday_list.json
@@ -4516,7 +4729,7 @@ msgstr ""
#. Agent'
#: crm/fcrm/doctype/crm_call_log/crm_call_log.json
#: crm/fcrm/doctype/crm_telephony_agent/crm_telephony_agent.json
-#: frontend/src/components/Layouts/AppSidebar.vue:584
+#: frontend/src/components/Layouts/AppSidebar.vue:596
#: frontend/src/components/Settings/TelephonySettings.vue:40
#: frontend/src/components/Settings/TelephonySettings.vue:50
msgid "Twilio"
@@ -4624,7 +4837,8 @@ msgstr ""
#: frontend/src/components/Modals/NoteModal.vue:6
#: frontend/src/components/Modals/TaskModal.vue:8
#: frontend/src/components/Settings/EmailTemplate/EditEmailTemplate.vue:17
-#: frontend/src/components/Settings/GeneralSettings.vue:21
+#: frontend/src/components/Settings/General/BrandSettings.vue:23
+#: frontend/src/components/Settings/General/HomeActions.vue:17
#: frontend/src/components/Settings/ProfileSettings.vue:95
#: frontend/src/components/Settings/SettingsPage.vue:20
#: frontend/src/components/Settings/TelephonySettings.vue:23
@@ -4664,7 +4878,7 @@ msgid "Upload Video"
msgstr ""
#: frontend/src/components/Settings/ProfileSettings.vue:27
-#: frontend/src/pages/Contact.vue:42 frontend/src/pages/Lead.vue:103
+#: frontend/src/pages/Contact.vue:42 frontend/src/pages/Lead.vue:104
#: frontend/src/pages/MobileContact.vue:38
#: frontend/src/pages/MobileOrganization.vue:38
#: frontend/src/pages/Organization.vue:42
@@ -4711,11 +4925,11 @@ msgstr ""
msgid "View Name"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:549
+#: frontend/src/components/Layouts/AppSidebar.vue:561
msgid "Views"
msgstr ""
-#: frontend/src/components/Layouts/AppSidebar.vue:546
+#: frontend/src/components/Layouts/AppSidebar.vue:558
msgid "Web form"
msgstr ""
@@ -4759,10 +4973,10 @@ msgstr ""
#. Option for the 'Type' (Select) field in DocType 'CRM Notification'
#: crm/fcrm/doctype/crm_notification/crm_notification.json
-#: frontend/src/components/Layouts/AppSidebar.vue:586
+#: frontend/src/components/Layouts/AppSidebar.vue:598
#: frontend/src/components/Settings/Settings.vue:129
-#: frontend/src/pages/Deal.vue:612 frontend/src/pages/Lead.vue:493
-#: frontend/src/pages/MobileDeal.vue:494 frontend/src/pages/MobileLead.vue:386
+#: frontend/src/pages/Deal.vue:612 frontend/src/pages/Lead.vue:494
+#: frontend/src/pages/MobileDeal.vue:494 frontend/src/pages/MobileLead.vue:388
msgid "WhatsApp"
msgstr ""
@@ -4782,6 +4996,10 @@ msgstr ""
msgid "Width can be in number, pixel or rem (eg. 3, 30px, 10rem)"
msgstr ""
+#: crm/api/dashboard.py:202
+msgid "Won Deals"
+msgstr ""
+
#. Label of the workday (Select) field in DocType 'CRM Service Day'
#: crm/fcrm/doctype/crm_service_day/crm_service_day.json
msgid "Workday"
@@ -4805,6 +5023,10 @@ msgstr ""
msgid "You"
msgstr ""
+#: crm/utils/__init__.py:262
+msgid "You are not permitted to access this resource."
+msgstr ""
+
#: frontend/src/components/Telephony/CallUI.vue:39
msgid "You can change the default calling medium from the settings"
msgstr ""
@@ -5120,8 +5342,8 @@ msgstr ""
msgid "{0} hours ago"
msgstr ""
-#: frontend/src/pages/Deal.vue:525 frontend/src/pages/Lead.vue:406
-#: frontend/src/pages/MobileDeal.vue:400 frontend/src/pages/MobileLead.vue:292
+#: frontend/src/pages/Deal.vue:525 frontend/src/pages/Lead.vue:407
+#: frontend/src/pages/MobileDeal.vue:400 frontend/src/pages/MobileLead.vue:294
msgid "{0} is a required field"
msgstr ""
diff --git a/crm/patches.txt b/crm/patches.txt
index 484c73dc..910f0fe1 100644
--- a/crm/patches.txt
+++ b/crm/patches.txt
@@ -13,4 +13,5 @@ crm.patches.v1_0.update_deal_quick_entry_layout
crm.patches.v1_0.update_layouts_to_new_format
crm.patches.v1_0.move_twilio_agent_to_telephony_agent
crm.patches.v1_0.create_default_scripts # 13-06-2025
-crm.patches.v1_0.update_deal_status_probabilities
\ No newline at end of file
+crm.patches.v1_0.update_deal_status_probabilities
+crm.patches.v1_0.update_deal_status_type
\ No newline at end of file
diff --git a/crm/patches/v1_0/update_deal_status_type.py b/crm/patches/v1_0/update_deal_status_type.py
new file mode 100644
index 00000000..d3e4084c
--- /dev/null
+++ b/crm/patches/v1_0/update_deal_status_type.py
@@ -0,0 +1,44 @@
+import frappe
+
+
+def execute():
+ deal_statuses = frappe.get_all("CRM Deal Status", fields=["name", "type", "deal_status"])
+
+ openStatuses = ["New", "Open", "Unassigned", "Qualification"]
+ ongoingStatuses = [
+ "Demo/Making",
+ "Proposal/Quotation",
+ "Negotiation",
+ "Ready to Close",
+ "Demo Scheduled",
+ "Follow Up",
+ ]
+ onHoldStatuses = ["On Hold", "Paused", "Stalled", "Awaiting Reply"]
+ wonStatuses = ["Won", "Closed Won", "Successful", "Completed"]
+ lostStatuses = [
+ "Lost",
+ "Closed",
+ "Closed Lost",
+ "Junk",
+ "Unqualified",
+ "Disqualified",
+ "Cancelled",
+ "No Response",
+ ]
+
+ for status in deal_statuses:
+ if not status.type or status.type is None or status.type == "Open":
+ if status.deal_status in openStatuses:
+ type = "Open"
+ elif status.deal_status in ongoingStatuses:
+ type = "Ongoing"
+ elif status.deal_status in onHoldStatuses:
+ type = "On Hold"
+ elif status.deal_status in wonStatuses:
+ type = "Won"
+ elif status.deal_status in lostStatuses:
+ type = "Lost"
+ else:
+ type = "Ongoing"
+
+ frappe.db.set_value("CRM Deal Status", status.name, "type", type)
diff --git a/crm/utils/__init__.py b/crm/utils/__init__.py
index 9b954859..6f7bbad0 100644
--- a/crm/utils/__init__.py
+++ b/crm/utils/__init__.py
@@ -1,10 +1,14 @@
-from frappe import frappe
+import functools
+
+import frappe
import phonenumbers
+import requests
+from frappe import _
+from frappe.model.docstatus import DocStatus
+from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.utils import floor
from phonenumbers import NumberParseException
from phonenumbers import PhoneNumberFormat as PNF
-from frappe.model.docstatus import DocStatus
-from frappe.model.dynamic_links import get_dynamic_link_map
def parse_phone_number(phone_number, default_country="IN"):
@@ -97,6 +101,7 @@ def seconds_to_duration(seconds):
else:
return "0s"
+
# Extracted from frappe core frappe/model/delete_doc.py/check_if_doc_is_linked
def get_linked_docs(doc, method="Delete"):
from frappe.model.rename_doc import get_link_fields
@@ -161,6 +166,7 @@ def get_linked_docs(doc, method="Delete"):
)
return docs
+
# Extracted from frappe core frappe/model/delete_doc.py/check_if_doc_is_dynamically_linked
def get_dynamic_linked_docs(doc, method="Delete"):
docs = []
@@ -222,3 +228,59 @@ def get_dynamic_linked_docs(doc, method="Delete"):
}
)
return docs
+
+
+def is_admin(user: str | None = None) -> bool:
+ """
+ Check whether `user` is an admin
+
+ :param user: User to check against, defaults to current user
+ :return: Whether `user` is an admin
+ """
+ user = user or frappe.session.user
+ return user == "Administrator"
+
+
+def is_sales_user(user: str | None = None) -> bool:
+ """
+ Check whether `user` is an agent
+
+ :param user: User to check against, defaults to current user
+ :return: Whether `user` is an agent
+ """
+ user = user or frappe.session.user
+ return is_admin() or "Sales Manager" in frappe.get_roles(user) or "Sales User" in frappe.get_roles(user)
+
+
+def sales_user_only(fn):
+ """Decorator to validate if user is an agent."""
+
+ @functools.wraps(fn)
+ def wrapper(*args, **kwargs):
+ if not is_sales_user():
+ frappe.throw(
+ msg=_("You are not permitted to access this resource."),
+ title=_("Not Allowed"),
+ exc=frappe.PermissionError,
+ )
+
+ return fn(*args, **kwargs)
+
+ return wrapper
+
+
+def get_exchange_rate(from_currency, to_currency, date=None):
+ if not date:
+ date = "latest"
+
+ url = f"https://api.frankfurter.app/{date}?from={from_currency}&to={to_currency}"
+
+ response = requests.get(url)
+
+ if response.status_code == 200:
+ data = response.json()
+ rate = data["rates"].get(to_currency)
+ return rate
+ else:
+ frappe.throw(_("Failed to fetch historical exchange rate from external API. Please try again later."))
+ return None
diff --git a/frappe-ui b/frappe-ui
index 424288f7..b295b54a 160000
--- a/frappe-ui
+++ b/frappe-ui
@@ -1 +1 @@
-Subproject commit 424288f77af4779dd3bb71dc3d278fc627f95179
+Subproject commit b295b54aaa3a2a22f455df819d87433dc6b9ff6a
diff --git a/frontend/components.d.ts b/frontend/components.d.ts
index 63df320c..c1dd22b6 100644
--- a/frontend/components.d.ts
+++ b/frontend/components.d.ts
@@ -12,6 +12,7 @@ declare module 'vue' {
Activities: typeof import('./src/components/Activities/Activities.vue')['default']
ActivityHeader: typeof import('./src/components/Activities/ActivityHeader.vue')['default']
ActivityIcon: typeof import('./src/components/Icons/ActivityIcon.vue')['default']
+ AddChartModal: typeof import('./src/components/Dashboard/AddChartModal.vue')['default']
AddExistingUserModal: typeof import('./src/components/Modals/AddExistingUserModal.vue')['default']
AddressIcon: typeof import('./src/components/Icons/AddressIcon.vue')['default']
AddressModal: typeof import('./src/components/Modals/AddressModal.vue')['default']
@@ -31,6 +32,7 @@ declare module 'vue' {
Autocomplete: typeof import('./src/components/frappe-ui/Autocomplete.vue')['default']
AvatarIcon: typeof import('./src/components/Icons/AvatarIcon.vue')['default']
BrandLogo: typeof import('./src/components/BrandLogo.vue')['default']
+ BrandSettings: typeof import('./src/components/Settings/General/BrandSettings.vue')['default']
BulkDeleteLinkedDocModal: typeof import('./src/components/BulkDeleteLinkedDocModal.vue')['default']
CalendarIcon: typeof import('./src/components/Icons/CalendarIcon.vue')['default']
CallArea: typeof import('./src/components/Activities/CallArea.vue')['default']
@@ -61,7 +63,9 @@ declare module 'vue' {
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
+ DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
+ DashboardItem: typeof import('./src/components/Dashboard/DashboardItem.vue')['default']
DataFields: typeof import('./src/components/Activities/DataFields.vue')['default']
DataFieldsModal: typeof import('./src/components/Modals/DataFieldsModal.vue')['default']
DealModal: typeof import('./src/components/Modals/DealModal.vue')['default']
@@ -98,11 +102,9 @@ declare module 'vue' {
EmailIcon: typeof import('./src/components/Icons/EmailIcon.vue')['default']
EmailProviderIcon: typeof import('./src/components/Settings/EmailProviderIcon.vue')['default']
EmailTemplateIcon: typeof import('./src/components/Icons/EmailTemplateIcon.vue')['default']
- EmailTemplateModal: typeof import('./src/components/Modals/EmailTemplateModal.vue')['default']
EmailTemplatePage: typeof import('./src/components/Settings/EmailTemplate/EmailTemplatePage.vue')['default']
EmailTemplates: typeof import('./src/components/Settings/EmailTemplate/EmailTemplates.vue')['default']
EmailTemplateSelectorModal: typeof import('./src/components/Modals/EmailTemplateSelectorModal.vue')['default']
- EmailTemplatesListView: typeof import('./src/components/ListViews/EmailTemplatesListView.vue')['default']
ERPNextIcon: typeof import('./src/components/Icons/ERPNextIcon.vue')['default']
ERPNextSettings: typeof import('./src/components/Settings/ERPNextSettings.vue')['default']
ErrorPage: typeof import('./src/components/ErrorPage.vue')['default']
@@ -127,7 +129,8 @@ declare module 'vue' {
FormattedInput: typeof import('./src/components/Controls/FormattedInput.vue')['default']
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
GenderIcon: typeof import('./src/components/Icons/GenderIcon.vue')['default']
- GeneralSettings: typeof import('./src/components/Settings/GeneralSettings.vue')['default']
+ GeneralSettings: typeof import('./src/components/Settings/General/GeneralSettings.vue')['default']
+ GeneralSettingsPage: typeof import('./src/components/Settings/General/GeneralSettingsPage.vue')['default']
GlobalModals: typeof import('./src/components/Modals/GlobalModals.vue')['default']
GoogleIcon: typeof import('./src/components/Icons/GoogleIcon.vue')['default']
Grid: typeof import('./src/components/Controls/Grid.vue')['default']
@@ -138,6 +141,7 @@ declare module 'vue' {
GroupByIcon: typeof import('./src/components/Icons/GroupByIcon.vue')['default']
HeartIcon: typeof import('./src/components/Icons/HeartIcon.vue')['default']
HelpIcon: typeof import('./src/components/Icons/HelpIcon.vue')['default']
+ HomeActions: typeof import('./src/components/Settings/General/HomeActions.vue')['default']
Icon: typeof import('./src/components/Icon.vue')['default']
IconPicker: typeof import('./src/components/IconPicker.vue')['default']
ImageUploader: typeof import('./src/components/Controls/ImageUploader.vue')['default']
@@ -163,11 +167,10 @@ declare module 'vue' {
ListRows: typeof import('./src/components/ListViews/ListRows.vue')['default']
LoadingIndicator: typeof import('./src/components/Icons/LoadingIndicator.vue')['default']
LostReasonModal: typeof import('./src/components/Modals/LostReasonModal.vue')['default']
- LucideInfo: typeof import('~icons/lucide/info')['default']
- LucideMoreHorizontal: typeof import('~icons/lucide/more-horizontal')['default']
- LucidePlus: typeof import('~icons/lucide/plus')['default']
- LucideSearch: typeof import('~icons/lucide/search')['default']
- LucideX: typeof import('~icons/lucide/x')['default']
+ LucideCalendar: typeof import('~icons/lucide/calendar')['default']
+ LucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
+ LucidePenLine: typeof import('~icons/lucide/pen-line')['default']
+ LucideRefreshCcw: typeof import('~icons/lucide/refresh-ccw')['default']
MarkAsDoneIcon: typeof import('./src/components/Icons/MarkAsDoneIcon.vue')['default']
MaximizeIcon: typeof import('./src/components/Icons/MaximizeIcon.vue')['default']
MenuIcon: typeof import('./src/components/Icons/MenuIcon.vue')['default']
@@ -201,7 +204,6 @@ declare module 'vue' {
PlaybackSpeedOption: typeof import('./src/components/Activities/PlaybackSpeedOption.vue')['default']
PlayIcon: typeof import('./src/components/Icons/PlayIcon.vue')['default']
Popover: typeof import('./src/components/frappe-ui/Popover.vue')['default']
- ProfileImageEditor: typeof import('./src/components/Settings/ProfileImageEditor.vue')['default']
ProfileSettings: typeof import('./src/components/Settings/ProfileSettings.vue')['default']
QuickEntryModal: typeof import('./src/components/Modals/QuickEntryModal.vue')['default']
QuickFilterField: typeof import('./src/components/QuickFilterField.vue')['default']
diff --git a/frontend/package.json b/frontend/package.json
index 7672e8df..ec932138 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,7 +13,7 @@
"@tiptap/extension-paragraph": "^2.12.0",
"@twilio/voice-sdk": "^2.10.2",
"@vueuse/integrations": "^10.3.0",
- "frappe-ui": "^0.1.166",
+ "frappe-ui": "^0.1.171",
"gemoji": "^8.1.0",
"lodash": "^4.17.21",
"mime": "^4.0.1",
diff --git a/frontend/src/components/Controls/Link.vue b/frontend/src/components/Controls/Link.vue
index 3cb2d6cd..633f2b2e 100644
--- a/frontend/src/components/Controls/Link.vue
+++ b/frontend/src/components/Controls/Link.vue
@@ -10,6 +10,8 @@
:size="attrs.size || 'sm'"
:variant="attrs.variant"
:placeholder="attrs.placeholder"
+ :disabled="attrs.disabled"
+ :placement="attrs.placement"
:filterable="false"
>
diff --git a/frontend/src/components/Dashboard/AddChartModal.vue b/frontend/src/components/Dashboard/AddChartModal.vue
new file mode 100644
index 00000000..31c2931c
--- /dev/null
+++ b/frontend/src/components/Dashboard/AddChartModal.vue
@@ -0,0 +1,165 @@
+
+
+
+
+
diff --git a/frontend/src/components/Dashboard/DashboardGrid.vue b/frontend/src/components/Dashboard/DashboardGrid.vue
new file mode 100644
index 00000000..ca984a0c
--- /dev/null
+++ b/frontend/src/components/Dashboard/DashboardGrid.vue
@@ -0,0 +1,62 @@
+
+
+ {{ __('Configure general settings for your CRM') }}
+
+ {{ __('General') }}
+
+