diff --git a/crm/api/dashboard.py b/crm/api/dashboard.py index 2fe1dfef..da3eaada 100644 --- a/crm/api/dashboard.py +++ b/crm/api/dashboard.py @@ -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 [] diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.json b/crm/fcrm/doctype/crm_deal/crm_deal.json index 14880fbd..4ce81ec4 100644 --- a/crm/fcrm/doctype/crm_deal/crm_deal.json +++ b/crm/fcrm/doctype/crm_deal/crm_deal.json @@ -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", diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.py b/crm/fcrm/doctype/crm_deal/crm_deal.py index b82f16db..f46f6475 100644 --- a/crm/fcrm/doctype/crm_deal/crm_deal.py +++ b/crm/fcrm/doctype/crm_deal/crm_deal.py @@ -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: 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_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..9e910e5c 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,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, - }) \ No newline at end of file + 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, + }, + ) diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json index 00907793..635c02d3 100644 --- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json +++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json @@ -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", diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py index 0b4e2932..7897bb1a 100644 --- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py +++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py @@ -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", diff --git a/crm/install.py b/crm/install.py index ae0a437c..1b401db2 100644 --- a/crm/install.py +++ b/crm/install.py @@ -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() 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..230f3776 --- /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 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) diff --git a/frontend/src/pages/Dashboard.vue b/frontend/src/pages/Dashboard.vue index 541bec75..e118a694 100644 --- a/frontend/src/pages/Dashboard.vue +++ b/frontend/src/pages/Dashboard.vue @@ -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" >