Merge pull request #1031 from frappe/mergify/bp/main-hotfix/pr-1030
This commit is contained in:
commit
eaf61005cd
@ -19,18 +19,24 @@ def get_number_card_data(from_date="", to_date="", user="", lead_conds="", deal_
|
||||
if is_sales_user and not user:
|
||||
user = frappe.session.user
|
||||
|
||||
lead_chart_data = get_lead_count(from_date, to_date, user, lead_conds)
|
||||
deal_chart_data = get_deal_count(from_date, to_date, user, deal_conds)
|
||||
get_won_deal_count_data = get_won_deal_count(from_date, to_date, user, deal_conds)
|
||||
get_average_deal_value_data = get_average_deal_value(from_date, to_date, user, deal_conds)
|
||||
get_average_time_to_close_data = get_average_time_to_close(from_date, to_date, user, deal_conds)
|
||||
lead_count = get_lead_count(from_date, to_date, user, lead_conds)
|
||||
ongoing_deal_count = get_ongoing_deal_count(from_date, to_date, user, deal_conds)["count"]
|
||||
average_ongoing_deal_value = get_ongoing_deal_count(from_date, to_date, user, deal_conds)["average"]
|
||||
won_deal_count = get_won_deal_count(from_date, to_date, user, deal_conds)["count"]
|
||||
average_won_deal_value = get_won_deal_count(from_date, to_date, user, deal_conds)["average"]
|
||||
average_deal_value = get_average_deal_value(from_date, to_date, user, deal_conds)
|
||||
average_time_to_close_a_lead = get_average_time_to_close(from_date, to_date, user, deal_conds)["lead"]
|
||||
average_time_to_close_a_deal = get_average_time_to_close(from_date, to_date, user, deal_conds)["deal"]
|
||||
|
||||
return [
|
||||
lead_chart_data,
|
||||
deal_chart_data,
|
||||
get_won_deal_count_data,
|
||||
get_average_deal_value_data,
|
||||
get_average_time_to_close_data,
|
||||
lead_count,
|
||||
ongoing_deal_count,
|
||||
average_ongoing_deal_value,
|
||||
won_deal_count,
|
||||
average_won_deal_value,
|
||||
average_deal_value,
|
||||
average_time_to_close_a_lead,
|
||||
average_time_to_close_a_deal,
|
||||
]
|
||||
|
||||
|
||||
@ -83,18 +89,17 @@ def get_lead_count(from_date, to_date, user="", conds="", return_result=False):
|
||||
)
|
||||
|
||||
return {
|
||||
"title": _("Total Leads"),
|
||||
"title": _("Total leads"),
|
||||
"value": current_month_leads,
|
||||
"delta": delta_in_percentage,
|
||||
"deltaSuffix": "%",
|
||||
"negativeIsBetter": False,
|
||||
"tooltip": _("Total number of leads"),
|
||||
}
|
||||
|
||||
|
||||
def get_deal_count(from_date, to_date, user="", conds="", return_result=False):
|
||||
def get_ongoing_deal_count(from_date, to_date, user="", conds="", return_result=False):
|
||||
"""
|
||||
Get deal count for the dashboard.
|
||||
Get ongoing deal count for the dashboard, and also calculate average deal value for ongoing deals.
|
||||
"""
|
||||
|
||||
diff = frappe.utils.date_diff(to_date, from_date)
|
||||
@ -102,25 +107,44 @@ def get_deal_count(from_date, to_date, user="", conds="", return_result=False):
|
||||
diff = 1
|
||||
|
||||
if user:
|
||||
conds += f" AND deal_owner = '{user}'"
|
||||
conds += f" AND d.deal_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_deals,
|
||||
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 creation >= %(prev_from_date)s AND creation < %(from_date)s
|
||||
{conds}
|
||||
THEN name
|
||||
ELSE NULL
|
||||
END) as prev_month_deals
|
||||
FROM `tabCRM Deal`
|
||||
COUNT(CASE
|
||||
WHEN d.creation >= %(prev_from_date)s AND d.creation < %(from_date)s
|
||||
AND s.type NOT IN ('Won', 'Lost')
|
||||
{conds}
|
||||
THEN d.name
|
||||
ELSE NULL
|
||||
END) as prev_month_deals,
|
||||
|
||||
AVG(CASE
|
||||
WHEN d.creation >= %(from_date)s AND d.creation < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
|
||||
AND s.type NOT IN ('Won', 'Lost')
|
||||
{conds}
|
||||
THEN d.deal_value * IFNULL(d.exchange_rate, 1)
|
||||
ELSE NULL
|
||||
END) as current_month_avg_value,
|
||||
|
||||
AVG(CASE
|
||||
WHEN d.creation >= %(prev_from_date)s AND d.creation < %(from_date)s
|
||||
AND s.type NOT IN ('Won', 'Lost')
|
||||
{conds}
|
||||
THEN d.deal_value * IFNULL(d.exchange_rate, 1)
|
||||
ELSE NULL
|
||||
END) as prev_month_avg_value
|
||||
FROM `tabCRM Deal` d
|
||||
JOIN `tabCRM Deal Status` s ON d.status = s.name
|
||||
""",
|
||||
{
|
||||
"from_date": from_date,
|
||||
@ -135,24 +159,36 @@ def get_deal_count(from_date, to_date, user="", conds="", return_result=False):
|
||||
|
||||
current_month_deals = result[0].current_month_deals or 0
|
||||
prev_month_deals = result[0].prev_month_deals or 0
|
||||
current_month_avg_value = result[0].current_month_avg_value or 0
|
||||
prev_month_avg_value = result[0].prev_month_avg_value or 0
|
||||
|
||||
delta_in_percentage = (
|
||||
(current_month_deals - prev_month_deals) / prev_month_deals * 100 if prev_month_deals else 0
|
||||
)
|
||||
avg_value_delta = current_month_avg_value - prev_month_avg_value if prev_month_avg_value else 0
|
||||
|
||||
return {
|
||||
"title": _("Total Deals"),
|
||||
"value": current_month_deals,
|
||||
"delta": delta_in_percentage,
|
||||
"deltaSuffix": "%",
|
||||
"negativeIsBetter": False,
|
||||
"tooltip": _("Total number of deals"),
|
||||
"count": {
|
||||
"title": _("Ongoing deals"),
|
||||
"value": current_month_deals,
|
||||
"delta": delta_in_percentage,
|
||||
"deltaSuffix": "%",
|
||||
"tooltip": _("Total number of ongoing deals"),
|
||||
},
|
||||
"average": {
|
||||
"title": _("Avg ongoing deal value"),
|
||||
"value": current_month_avg_value,
|
||||
"delta": avg_value_delta,
|
||||
"prefix": get_base_currency_symbol(),
|
||||
# "suffix": "K",
|
||||
"tooltip": _("Average deal value of ongoing deals"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_won_deal_count(from_date, to_date, user="", conds="", return_result=False):
|
||||
"""
|
||||
Get won deal count for the dashboard.
|
||||
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)
|
||||
@ -160,26 +196,45 @@ def get_won_deal_count(from_date, to_date, user="", conds="", return_result=Fals
|
||||
diff = 1
|
||||
|
||||
if user:
|
||||
conds += f" AND deal_owner = '{user}'"
|
||||
conds += f" AND d.deal_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) AND status = 'Won'
|
||||
{conds}
|
||||
THEN name
|
||||
ELSE NULL
|
||||
END) as current_month_deals,
|
||||
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 creation >= %(prev_from_date)s AND creation < %(from_date)s AND status = 'Won'
|
||||
{conds}
|
||||
THEN name
|
||||
ELSE NULL
|
||||
END) as prev_month_deals
|
||||
FROM `tabCRM Deal`
|
||||
""",
|
||||
COUNT(CASE
|
||||
WHEN d.closed_date >= %(prev_from_date)s AND d.closed_date < %(from_date)s
|
||||
AND s.type = 'Won'
|
||||
{conds}
|
||||
THEN d.name
|
||||
ELSE NULL
|
||||
END) as prev_month_deals,
|
||||
|
||||
AVG(CASE
|
||||
WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
|
||||
AND s.type = 'Won'
|
||||
{conds}
|
||||
THEN d.deal_value * IFNULL(d.exchange_rate, 1)
|
||||
ELSE NULL
|
||||
END) as current_month_avg_value,
|
||||
|
||||
AVG(CASE
|
||||
WHEN d.closed_date >= %(prev_from_date)s AND d.closed_date < %(from_date)s
|
||||
AND s.type = 'Won'
|
||||
{conds}
|
||||
THEN d.deal_value * IFNULL(d.exchange_rate, 1)
|
||||
ELSE NULL
|
||||
END) as prev_month_avg_value
|
||||
FROM `tabCRM Deal` d
|
||||
JOIN `tabCRM Deal Status` s ON d.status = s.name
|
||||
""",
|
||||
{
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
@ -193,18 +248,30 @@ def get_won_deal_count(from_date, to_date, user="", conds="", return_result=Fals
|
||||
|
||||
current_month_deals = result[0].current_month_deals or 0
|
||||
prev_month_deals = result[0].prev_month_deals or 0
|
||||
current_month_avg_value = result[0].current_month_avg_value or 0
|
||||
prev_month_avg_value = result[0].prev_month_avg_value or 0
|
||||
|
||||
delta_in_percentage = (
|
||||
(current_month_deals - prev_month_deals) / prev_month_deals * 100 if prev_month_deals else 0
|
||||
)
|
||||
avg_value_delta = current_month_avg_value - prev_month_avg_value if prev_month_avg_value else 0
|
||||
|
||||
return {
|
||||
"title": _("Won Deals"),
|
||||
"value": current_month_deals,
|
||||
"delta": delta_in_percentage,
|
||||
"deltaSuffix": "%",
|
||||
"negativeIsBetter": False,
|
||||
"tooltip": _("Total number of won deals"),
|
||||
"count": {
|
||||
"title": _("Won deals"),
|
||||
"value": current_month_deals,
|
||||
"delta": delta_in_percentage,
|
||||
"deltaSuffix": "%",
|
||||
"tooltip": _("Total number of won deals based on its closure date"),
|
||||
},
|
||||
"average": {
|
||||
"title": _("Avg won deal value"),
|
||||
"value": current_month_avg_value,
|
||||
"delta": avg_value_delta,
|
||||
"prefix": get_base_currency_symbol(),
|
||||
# "suffix": "K",
|
||||
"tooltip": _("Average deal value of won deals"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -218,25 +285,28 @@ def get_average_deal_value(from_date, to_date, user="", conds="", return_result=
|
||||
diff = 1
|
||||
|
||||
if user:
|
||||
conds += f" AND deal_owner = '{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 d.status != 'Lost'
|
||||
{conds}
|
||||
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 d.status != 'Lost'
|
||||
{conds}
|
||||
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,
|
||||
@ -252,7 +322,7 @@ def get_average_deal_value(from_date, to_date, user="", conds="", return_result=
|
||||
delta = current_month_avg - prev_month_avg if prev_month_avg else 0
|
||||
|
||||
return {
|
||||
"title": _("Avg Deal Value"),
|
||||
"title": _("Avg deal value"),
|
||||
"value": current_month_avg,
|
||||
"tooltip": _("Average deal value of ongoing & won deals"),
|
||||
"prefix": get_base_currency_symbol(),
|
||||
@ -265,6 +335,9 @@ def get_average_deal_value(from_date, to_date, user="", conds="", return_result=
|
||||
def get_average_time_to_close(from_date, to_date, user="", conds="", return_result=False):
|
||||
"""
|
||||
Get average time to close deals for the dashboard.
|
||||
Returns both:
|
||||
- Average time from lead creation to deal closure
|
||||
- Average time from deal creation to deal closure
|
||||
"""
|
||||
|
||||
diff = frappe.utils.date_diff(to_date, from_date)
|
||||
@ -280,13 +353,18 @@ def get_average_time_to_close(from_date, to_date, user="", conds="", return_resu
|
||||
result = frappe.db.sql(
|
||||
f"""
|
||||
SELECT
|
||||
AVG(CASE WHEN d.closed_on >= %(from_date)s AND d.closed_on < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
|
||||
THEN TIMESTAMPDIFF(DAY, COALESCE(l.creation, d.creation), d.closed_on) END) as current_avg,
|
||||
AVG(CASE WHEN d.closed_on >= %(prev_from_date)s AND d.closed_on < %(prev_to_date)s
|
||||
THEN TIMESTAMPDIFF(DAY, COALESCE(l.creation, d.creation), d.closed_on) END) as prev_avg
|
||||
AVG(CASE WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
|
||||
THEN TIMESTAMPDIFF(DAY, COALESCE(l.creation, d.creation), d.closed_date) END) as current_avg_lead,
|
||||
AVG(CASE WHEN d.closed_date >= %(prev_from_date)s AND d.closed_date < %(prev_to_date)s
|
||||
THEN TIMESTAMPDIFF(DAY, COALESCE(l.creation, d.creation), d.closed_date) END) as prev_avg_lead,
|
||||
AVG(CASE WHEN d.closed_date >= %(from_date)s AND d.closed_date < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
|
||||
THEN TIMESTAMPDIFF(DAY, d.creation, d.closed_date) END) as current_avg_deal,
|
||||
AVG(CASE WHEN d.closed_date >= %(prev_from_date)s AND d.closed_date < %(prev_to_date)s
|
||||
THEN TIMESTAMPDIFF(DAY, d.creation, d.closed_date) END) as prev_avg_deal
|
||||
FROM `tabCRM Deal` AS d
|
||||
JOIN `tabCRM Deal Status` s ON d.status = s.name
|
||||
LEFT JOIN `tabCRM Lead` l ON d.lead = l.name
|
||||
WHERE d.status = 'Won' AND d.closed_on IS NOT NULL
|
||||
WHERE d.closed_date IS NOT NULL AND s.type = 'Won'
|
||||
{conds}
|
||||
""",
|
||||
{
|
||||
@ -301,18 +379,33 @@ def get_average_time_to_close(from_date, to_date, user="", conds="", return_resu
|
||||
if return_result:
|
||||
return result
|
||||
|
||||
current_avg = result[0].current_avg or 0
|
||||
prev_avg = result[0].prev_avg or 0
|
||||
delta = current_avg - prev_avg if prev_avg else 0
|
||||
current_avg_lead = result[0].current_avg_lead or 0
|
||||
prev_avg_lead = result[0].prev_avg_lead or 0
|
||||
delta_lead = current_avg_lead - prev_avg_lead if prev_avg_lead else 0
|
||||
|
||||
current_avg_deal = result[0].current_avg_deal or 0
|
||||
prev_avg_deal = result[0].prev_avg_deal or 0
|
||||
delta_deal = current_avg_deal - prev_avg_deal if prev_avg_deal else 0
|
||||
|
||||
return {
|
||||
"title": _("Avg Time to Close"),
|
||||
"value": current_avg,
|
||||
"tooltip": _("Average time taken from lead creation to deal closure"),
|
||||
"suffix": " days",
|
||||
"delta": delta,
|
||||
"deltaSuffix": " days",
|
||||
"negativeIsBetter": True,
|
||||
"lead": {
|
||||
"title": _("Avg time to close a lead"),
|
||||
"value": current_avg_lead,
|
||||
"tooltip": _("Average time taken from lead creation to deal closure"),
|
||||
"suffix": " days",
|
||||
"delta": delta_lead,
|
||||
"deltaSuffix": " days",
|
||||
"negativeIsBetter": True,
|
||||
},
|
||||
"deal": {
|
||||
"title": _("Avg time to close a deal"),
|
||||
"value": current_avg_deal,
|
||||
"tooltip": _("Average time taken from deal creation to deal closure"),
|
||||
"suffix": " days",
|
||||
"delta": delta_deal,
|
||||
"deltaSuffix": " days",
|
||||
"negativeIsBetter": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -356,14 +449,15 @@ def get_sales_trend_data(from_date="", to_date="", user="", lead_conds="", deal_
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
DATE(creation) AS date,
|
||||
DATE(d.creation) AS date,
|
||||
0 AS leads,
|
||||
COUNT(*) AS deals,
|
||||
SUM(CASE WHEN status = 'Won' THEN 1 ELSE 0 END) AS won_deals
|
||||
FROM `tabCRM Deal`
|
||||
WHERE DATE(creation) BETWEEN %(from)s AND %(to)s
|
||||
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(creation)
|
||||
GROUP BY DATE(d.creation)
|
||||
) AS daily
|
||||
GROUP BY date
|
||||
ORDER BY date
|
||||
@ -488,7 +582,8 @@ def get_lost_deal_reasons(from_date="", to_date="", user="", deal_conds=""):
|
||||
d.lost_reason AS reason,
|
||||
COUNT(*) AS count
|
||||
FROM `tabCRM Deal` AS d
|
||||
WHERE DATE(d.creation) BETWEEN %(from)s AND %(to)s AND d.status = 'Lost'
|
||||
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 != ''
|
||||
@ -520,23 +615,24 @@ def get_forecasted_revenue(user="", deal_conds=""):
|
||||
result = frappe.db.sql(
|
||||
f"""
|
||||
SELECT
|
||||
DATE_FORMAT(d.close_date, '%Y-%m') AS month,
|
||||
DATE_FORMAT(d.expected_closure_date, '%Y-%m') AS month,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN d.status = 'Lost' THEN d.deal_value * IFNULL(d.exchange_rate, 1)
|
||||
ELSE d.deal_value * IFNULL(d.probability, 0) / 100 * IFNULL(d.exchange_rate, 1) -- forecasted
|
||||
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 d.status = 'Won' THEN d.deal_value * IFNULL(d.exchange_rate, 1) -- actual
|
||||
WHEN s.type = 'Won' THEN d.deal_value * IFNULL(d.exchange_rate, 1) -- actual
|
||||
ELSE 0
|
||||
END
|
||||
) AS actual
|
||||
FROM `tabCRM Deal` AS d
|
||||
WHERE d.close_date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
|
||||
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.close_date, '%Y-%m')
|
||||
GROUP BY DATE_FORMAT(d.expected_closure_date, '%Y-%m')
|
||||
ORDER BY month
|
||||
""",
|
||||
as_dict=True,
|
||||
@ -581,7 +677,8 @@ def get_funnel_conversion_data(from_date="", to_date="", user="", lead_conds="",
|
||||
|
||||
# Get total leads
|
||||
total_leads = frappe.db.sql(
|
||||
f""" SELECT COUNT(*) AS count
|
||||
f"""
|
||||
SELECT COUNT(*) AS count
|
||||
FROM `tabCRM Lead`
|
||||
WHERE DATE(creation) BETWEEN %(from)s AND %(to)s
|
||||
{lead_conds}
|
||||
@ -593,25 +690,7 @@ def get_funnel_conversion_data(from_date="", to_date="", user="", lead_conds="",
|
||||
|
||||
result.append({"stage": "Leads", "count": total_leads_count})
|
||||
|
||||
# Get deal stages
|
||||
all_deal_stages = frappe.get_all(
|
||||
"CRM Deal Status", filters={"name": ["!=", "Lost"]}, order_by="position", pluck="name"
|
||||
)
|
||||
|
||||
# Get deal counts for each stage
|
||||
for i, stage in enumerate(all_deal_stages):
|
||||
stages_to_count = all_deal_stages[i:]
|
||||
placeholders = ", ".join(["%s"] * len(stages_to_count))
|
||||
query = f"""
|
||||
SELECT COUNT(*) as count
|
||||
FROM `tabCRM Deal`
|
||||
WHERE DATE(creation) BETWEEN %s AND %s
|
||||
AND status IN ({placeholders})
|
||||
{deal_conds}
|
||||
"""
|
||||
params = [from_date, to_date, *stages_to_count]
|
||||
row = frappe.db.sql(query, params, as_dict=True)
|
||||
result.append({"stage": stage, "count": row[0]["count"] if row else 0})
|
||||
result += get_deal_status_change_counts(from_date, to_date, deal_conds)
|
||||
|
||||
return result or []
|
||||
|
||||
@ -638,8 +717,10 @@ def get_deals_by_stage(from_date="", to_date="", user="", deal_conds=""):
|
||||
f"""
|
||||
SELECT
|
||||
d.status AS stage,
|
||||
COUNT(*) AS count
|
||||
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
|
||||
@ -694,3 +775,44 @@ def get_base_currency_symbol():
|
||||
"""
|
||||
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 []
|
||||
|
||||
@ -18,10 +18,11 @@
|
||||
"lost_notes",
|
||||
"section_break_jgpm",
|
||||
"probability",
|
||||
"expected_deal_value",
|
||||
"deal_value",
|
||||
"column_break_kpxa",
|
||||
"close_date",
|
||||
"closed_on",
|
||||
"expected_closure_date",
|
||||
"closed_date",
|
||||
"contacts_tab",
|
||||
"contacts",
|
||||
"contact",
|
||||
@ -95,11 +96,6 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Website"
|
||||
},
|
||||
{
|
||||
"fieldname": "close_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Close Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "next_step",
|
||||
"fieldtype": "Data",
|
||||
@ -412,23 +408,34 @@
|
||||
"label": "Lost Notes",
|
||||
"mandatory_depends_on": "eval: doc.lost_reason == \"Other\""
|
||||
},
|
||||
{
|
||||
"fieldname": "closed_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Closed On"
|
||||
},
|
||||
{
|
||||
"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-09 17:58:55.956639",
|
||||
"modified": "2025-07-13 11:54:20.608489",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "CRM Deal",
|
||||
|
||||
@ -25,8 +25,8 @@ class CRMDeal(Document):
|
||||
self.assign_agent(self.deal_owner)
|
||||
if self.has_value_changed("status"):
|
||||
add_status_change_log(self)
|
||||
if self.status == "Won":
|
||||
self.closed_on = frappe.utils.now_datetime()
|
||||
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()
|
||||
@ -166,7 +166,7 @@ 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:
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"],
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,44 @@ def get_duration(from_date, to_date):
|
||||
duration = to_date - from_date
|
||||
return duration.total_seconds()
|
||||
|
||||
|
||||
def add_status_change_log(doc):
|
||||
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,
|
||||
"to": "",
|
||||
"to_type": "",
|
||||
"from_date": now_minus_one_minute,
|
||||
"to_date": "",
|
||||
"log_owner": frappe.session.user,
|
||||
},
|
||||
)
|
||||
to_status_type = frappe.db.get_value("CRM Deal Status", doc.status, "type")
|
||||
last_status_change = doc.status_change_log[-1]
|
||||
last_status_change.to = doc.status
|
||||
last_status_change.to_type = to_status_type
|
||||
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,
|
||||
})
|
||||
doc.append(
|
||||
"status_change_log",
|
||||
{
|
||||
"from": doc.status,
|
||||
"from_type": to_status_type,
|
||||
"to": "",
|
||||
"to_type": "",
|
||||
"from_date": datetime.now(),
|
||||
"to_date": "",
|
||||
"log_owner": frappe.session.user,
|
||||
},
|
||||
)
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
},
|
||||
{
|
||||
"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"
|
||||
@ -77,7 +77,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-10 16:35:25.030011",
|
||||
"modified": "2025-07-13 11:58:34.857638",
|
||||
"modified_by": "Administrator",
|
||||
"module": "FCRM",
|
||||
"name": "FCRM Settings",
|
||||
|
||||
@ -38,24 +38,24 @@ 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",
|
||||
|
||||
@ -69,36 +69,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 +118,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()
|
||||
|
||||
@ -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
|
||||
crm.patches.v1_0.update_deal_status_probabilities
|
||||
crm.patches.v1_0.update_deal_status_type
|
||||
44
crm/patches/v1_0/update_deal_status_type.py
Normal file
44
crm/patches/v1_0/update_deal_status_type.py
Normal file
@ -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 status.type is None or status.type == "":
|
||||
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)
|
||||
@ -59,7 +59,7 @@
|
||||
doctype="User"
|
||||
:filters="{ name: ['in', users.data.crmUsers?.map((u) => u.name)] }"
|
||||
@change="(v) => updateFilter('user', v)"
|
||||
:placeholder="__('Sales User')"
|
||||
:placeholder="__('Sales user')"
|
||||
:hideMe="true"
|
||||
>
|
||||
<template #prefix>
|
||||
@ -110,8 +110,14 @@
|
||||
<div v-if="funnelConversion.data" class="border rounded-md min-h-80">
|
||||
<AxisChart :config="funnelConversion.data" />
|
||||
</div>
|
||||
<div v-if="lostDealReasons.data" class="border rounded-md">
|
||||
<AxisChart :config="lostDealReasons.data" />
|
||||
<div v-if="dealsByStage.data" class="border rounded-md">
|
||||
<AxisChart :config="dealsByStage.data.bar" />
|
||||
</div>
|
||||
<div v-if="dealsByStage.data" class="border rounded-md">
|
||||
<DonutChart :config="dealsByStage.data.donut" />
|
||||
</div>
|
||||
<div v-if="leadsBySource.data" class="border rounded-md">
|
||||
<DonutChart :config="leadsBySource.data" />
|
||||
</div>
|
||||
<div v-if="dealsByTerritory.data" class="border rounded-md">
|
||||
<AxisChart :config="dealsByTerritory.data" />
|
||||
@ -119,11 +125,8 @@
|
||||
<div v-if="dealsBySalesperson.data" class="border rounded-md">
|
||||
<AxisChart :config="dealsBySalesperson.data" />
|
||||
</div>
|
||||
<div v-if="dealsByStage.data" class="border rounded-md">
|
||||
<DonutChart :config="dealsByStage.data" />
|
||||
</div>
|
||||
<div v-if="leadsBySource.data" class="border rounded-md">
|
||||
<DonutChart :config="leadsBySource.data" />
|
||||
<div v-if="lostDealReasons.data" class="border rounded-md">
|
||||
<AxisChart :config="lostDealReasons.data" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -266,7 +269,7 @@ const salesTrend = createResource({
|
||||
transform(data = []) {
|
||||
return {
|
||||
data: data,
|
||||
title: __('Sales Trend'),
|
||||
title: __('Sales trend'),
|
||||
subtitle: __('Daily performance of leads, deals, and wins'),
|
||||
xAxis: {
|
||||
title: __('Date'),
|
||||
@ -300,7 +303,7 @@ const funnelConversion = createResource({
|
||||
transform(data = []) {
|
||||
return {
|
||||
data: data,
|
||||
title: __('Funnel Conversion'),
|
||||
title: __('Funnel conversion'),
|
||||
subtitle: __('Lead to deal conversion pipeline'),
|
||||
xAxis: {
|
||||
title: __('Stage'),
|
||||
@ -338,18 +341,18 @@ const dealsBySalesperson = createResource({
|
||||
transform(r = { data: [], currency_symbol: '$' }) {
|
||||
return {
|
||||
data: r.data || [],
|
||||
title: __('Deals by Salesperson'),
|
||||
subtitle: 'Number of deals and total value per salesperson',
|
||||
title: __('Deals by salesperson'),
|
||||
subtitle: __('Number of deals and total value per salesperson'),
|
||||
xAxis: {
|
||||
title: __('Salesperson'),
|
||||
key: 'salesperson',
|
||||
type: 'category' as const,
|
||||
},
|
||||
yAxis: {
|
||||
title: __('Number of Deals'),
|
||||
title: __('Number of deals'),
|
||||
},
|
||||
y2Axis: {
|
||||
title: __('Deal Value') + ` (${r.currency_symbol})`,
|
||||
title: __('Deal value') + ` (${r.currency_symbol})`,
|
||||
},
|
||||
series: [
|
||||
{ name: 'deals', type: 'bar' as const },
|
||||
@ -378,7 +381,7 @@ const dealsByTerritory = createResource({
|
||||
transform(r = { data: [], currency_symbol: '$' }) {
|
||||
return {
|
||||
data: r.data || [],
|
||||
title: __('Deals by Territory'),
|
||||
title: __('Deals by territory'),
|
||||
subtitle: __('Geographic distribution of deals and revenue'),
|
||||
xAxis: {
|
||||
title: __('Territory'),
|
||||
@ -386,10 +389,10 @@ const dealsByTerritory = createResource({
|
||||
type: 'category' as const,
|
||||
},
|
||||
yAxis: {
|
||||
title: __('Number of Deals'),
|
||||
title: __('Number of deals'),
|
||||
},
|
||||
y2Axis: {
|
||||
title: __('Deal Value') + ` (${r.currency_symbol})`,
|
||||
title: __('Deal value') + ` (${r.currency_symbol})`,
|
||||
},
|
||||
series: [
|
||||
{ name: 'deals', type: 'bar' as const },
|
||||
@ -418,7 +421,7 @@ const lostDealReasons = createResource({
|
||||
transform(data = []) {
|
||||
return {
|
||||
data: data,
|
||||
title: __('Lost Deal Reasons'),
|
||||
title: __('Lost deal reasons'),
|
||||
subtitle: __('Common reasons for losing deals'),
|
||||
xAxis: {
|
||||
title: __('Reason'),
|
||||
@ -444,7 +447,7 @@ const forecastedRevenue = createResource({
|
||||
transform(r = { data: [], currency_symbol: '$' }) {
|
||||
return {
|
||||
data: r.data || [],
|
||||
title: __('Revenue Forecast'),
|
||||
title: __('Revenue forecast'),
|
||||
subtitle: __('Projected vs actual revenue based on deal probability'),
|
||||
xAxis: {
|
||||
title: __('Month'),
|
||||
@ -476,11 +479,26 @@ const dealsByStage = createResource({
|
||||
auto: true,
|
||||
transform(data = []) {
|
||||
return {
|
||||
data: data,
|
||||
title: __('Deals by Stage'),
|
||||
subtitle: __('Current pipeline distribution'),
|
||||
categoryColumn: 'stage',
|
||||
valueColumn: 'count',
|
||||
donut: {
|
||||
data: data,
|
||||
title: __('Deals by stage'),
|
||||
subtitle: __('Current pipeline distribution'),
|
||||
categoryColumn: 'stage',
|
||||
valueColumn: 'count',
|
||||
},
|
||||
bar: {
|
||||
data: data.filter((d) => d.status_type != 'Lost'),
|
||||
title: __('Deals by ongoing & won stage'),
|
||||
xAxis: {
|
||||
title: __('Stage'),
|
||||
key: 'stage',
|
||||
type: 'category' as const,
|
||||
},
|
||||
yAxis: {
|
||||
title: __('Count'),
|
||||
},
|
||||
series: [{ name: 'count', type: 'bar' as const }],
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -499,7 +517,7 @@ const leadsBySource = createResource({
|
||||
transform(data = []) {
|
||||
return {
|
||||
data: data,
|
||||
title: __('Leads by Source'),
|
||||
title: __('Leads by source'),
|
||||
subtitle: __('Lead generation channel analysis'),
|
||||
categoryColumn: 'source',
|
||||
valueColumn: 'count',
|
||||
|
||||
@ -773,7 +773,7 @@ const showLostReasonModal = ref(false)
|
||||
|
||||
function setLostReason() {
|
||||
if (
|
||||
document.doc.status !== 'Lost' ||
|
||||
getDealStatus(document.doc.status).type !== 'Lost' ||
|
||||
(document.doc.lost_reason && document.doc.lost_reason !== 'Other') ||
|
||||
(document.doc.lost_reason === 'Other' && document.doc.lost_notes)
|
||||
) {
|
||||
@ -785,7 +785,7 @@ function setLostReason() {
|
||||
}
|
||||
|
||||
function beforeStatusChange(data) {
|
||||
if (data?.hasOwnProperty('status') && data.status == 'Lost') {
|
||||
if (data?.hasOwnProperty('status') && getDealStatus(data.status).type == 'Lost') {
|
||||
setLostReason()
|
||||
} else {
|
||||
document.save.submit(null, {
|
||||
|
||||
@ -643,7 +643,7 @@ const showLostReasonModal = ref(false)
|
||||
|
||||
function setLostReason() {
|
||||
if (
|
||||
document.doc.status !== 'Lost' ||
|
||||
getDealStatus(document.doc.status).type !== 'Lost' ||
|
||||
(document.doc.lost_reason && document.doc.lost_reason !== 'Other') ||
|
||||
(document.doc.lost_reason === 'Other' && document.doc.lost_notes)
|
||||
) {
|
||||
@ -655,7 +655,7 @@ function setLostReason() {
|
||||
}
|
||||
|
||||
function beforeStatusChange(data) {
|
||||
if (data?.hasOwnProperty('status') && data.status == 'Lost') {
|
||||
if (data?.hasOwnProperty('status') && getDealStatus(data.status).type == 'Lost') {
|
||||
setLostReason()
|
||||
} else {
|
||||
document.save.submit(null, {
|
||||
|
||||
@ -28,7 +28,7 @@ export const statusesStore = defineStore('crm-statuses', () => {
|
||||
|
||||
const dealStatuses = createListResource({
|
||||
doctype: 'CRM Deal Status',
|
||||
fields: ['name', 'color', 'position'],
|
||||
fields: ['name', 'color', 'position', 'type'],
|
||||
orderBy: 'position asc',
|
||||
cache: 'deal-statuses',
|
||||
initialData: [],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user