Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 272 additions & 0 deletions crm/api/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
from crm.fcrm.doctype.crm_dashboard.crm_dashboard import create_default_manager_dashboard
from crm.utils import sales_user_only

CALL_CONNECTED_STATUSES = ("Completed", "In Progress")
CLOSED_TASK_STATUSES = ("Done", "Completed", "Closed", "Cancelled", "Canceled")


@frappe.whitelist()
def reset_to_default():
Expand Down Expand Up @@ -41,6 +44,12 @@ def get_dashboard(from_date="", to_date="", user=""):
else:
layout = json.loads(frappe.db.get_value("CRM Dashboard", "Manager Dashboard", "layout") or "[]")

default_layout = json.loads(create_default_manager_dashboard())
existing_names = {l.get("name") for l in layout}
for default_item in default_layout:
if default_item.get("name") not in existing_names:
layout.append(default_item)

for l in layout:
method_name = f"get_{l['name']}"
if hasattr(frappe.get_attr("crm.api.dashboard"), method_name):
Expand Down Expand Up @@ -134,6 +143,269 @@ def get_total_leads(from_date, to_date, user=""):
}


def get_total_site_visits(from_date, to_date, user=""):
"""
Count leads marked as visited in the selected period.
"""
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())

diff = frappe.utils.date_diff(to_date, from_date) or 1
params = {
"from_date": from_date,
"to_date": to_date,
"prev_from_date": frappe.utils.add_days(from_date, -diff),
}

conds = "custom_site_visit_status = 'Visited'"
if user:
conds += " AND lead_owner = %(user)s"
params["user"] = 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 {conds}
THEN name END) as current_visits,
COUNT(CASE
WHEN creation >= %(prev_from_date)s AND creation < %(from_date)s
AND {conds}
THEN name END) as prev_visits
FROM `tabCRM Lead`
""",
params,
as_dict=1,
)

current = result[0].current_visits or 0
prev = result[0].prev_visits or 0
delta = ((current - prev) / prev * 100) if prev else 0

return {
"title": _("Total site visits"),
"tooltip": _("Leads marked as 'Visited' in the selected period"),
"value": current,
"delta": delta,
"deltaSuffix": "%",
}


def get_total_outgoing_calls(from_date, to_date, user=""):
"""
Total outgoing calls with ability to focus on connected calls.
"""
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())

diff = frappe.utils.date_diff(to_date, from_date) or 1
params = {
"from_date": from_date,
"to_date": to_date,
"prev_from_date": frappe.utils.add_days(from_date, -diff),
"type": "Outgoing",
"connected_statuses": CALL_CONNECTED_STATUSES,
}

conds = "type = %(type)s"
prev_conds = conds
if user:
conds += " AND caller = %(user)s"
prev_conds = conds
params["user"] = 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 {conds}
THEN name END) AS current_total,
COUNT(CASE
WHEN creation >= %(prev_from_date)s AND creation < %(from_date)s
AND {prev_conds}
THEN name END) AS prev_total,
COUNT(CASE
WHEN creation >= %(from_date)s AND creation < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
AND {conds} AND status IN %(connected_statuses)s
THEN name END) AS current_connected
FROM `tabCRM Call Log`
""",
params,
as_dict=1,
)

current_total = result[0].current_total or 0
prev_total = result[0].prev_total or 0
current_connected = result[0].current_connected or 0
delta = ((current_total - prev_total) / prev_total * 100) if prev_total else 0

return {
"title": _("Total outgoing calls"),
"tooltip": _("Outgoing calls in the selected period"),
"value": current_total,
"delta": delta,
"deltaSuffix": "%",
"values": {
"all": current_total,
"connected": current_connected,
},
"filters": [
{"label": _("All"), "value": "all", "key": "all"},
{"label": _("Connected"), "value": "connected", "key": "connected"},
],
}


def get_total_incoming_calls(from_date, to_date, user=""):
"""
Total incoming calls with ability to focus on received calls.
"""
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())

diff = frappe.utils.date_diff(to_date, from_date) or 1
params = {
"from_date": from_date,
"to_date": to_date,
"prev_from_date": frappe.utils.add_days(from_date, -diff),
"type": "Incoming",
"received_statuses": CALL_CONNECTED_STATUSES,
}

conds = "type = %(type)s"
prev_conds = conds
if user:
conds += " AND receiver = %(user)s"
prev_conds = conds
params["user"] = 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 {conds}
THEN name END) AS current_total,
COUNT(CASE
WHEN creation >= %(prev_from_date)s AND creation < %(from_date)s
AND {prev_conds}
THEN name END) AS prev_total,
COUNT(CASE
WHEN creation >= %(from_date)s AND creation < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
AND {conds} AND status IN %(received_statuses)s
THEN name END) AS current_received
FROM `tabCRM Call Log`
""",
params,
as_dict=1,
)

current_total = result[0].current_total or 0
prev_total = result[0].prev_total or 0
current_received = result[0].current_received or 0
delta = ((current_total - prev_total) / prev_total * 100) if prev_total else 0

return {
"title": _("Total incoming calls"),
"tooltip": _("Incoming calls in the selected period"),
"value": current_total,
"delta": delta,
"deltaSuffix": "%",
"values": {
"all": current_total,
"received": current_received,
},
"filters": [
{"label": _("All"), "value": "all", "key": "all"},
{"label": _("Received"), "value": "received", "key": "received"},
],
}


def get_todays_tasks(from_date=None, to_date=None, user=""):
"""
Count tasks due today and highlight how many have notifications.
"""
today = frappe.utils.getdate(frappe.utils.nowdate())
start = frappe.utils.get_datetime(today)
end = frappe.utils.add_days(start, 1)

task_filters = {
"due_date": today,
"status": ["not in", CLOSED_TASK_STATUSES],
}
if user:
task_filters["assigned_to"] = user

task_names = frappe.get_all("CRM Task", filters=task_filters, pluck="name")
task_count = len(task_names)

notification_filters = {
"notification_type_doctype": "CRM Task",
"creation": ["between", [start, end]],
}
if task_names:
notification_filters["notification_type_doc"] = ["in", task_names]
if user:
notification_filters["to_user"] = user

notification_count = (
frappe.db.count("CRM Notification", filters=notification_filters) if task_names else 0
)

return {
"title": _("Today's tasks"),
"tooltip": _("Tasks due today"),
"value": task_count,
"values": {
"all": task_count,
"notified": notification_count,
},
"filters": [
{"label": _("All"), "value": "all", "key": "all"},
{"label": _("With notifications"), "value": "notified", "key": "notified"},
],
}


def get_todays_closed_tasks(from_date=None, to_date=None, user=""):
"""
Count tasks closed today.
"""
today = frappe.utils.getdate(frappe.utils.nowdate())
start = frappe.utils.get_datetime(today)
end = frappe.utils.add_days(start, 1)

filters = {
"status": ["in", CLOSED_TASK_STATUSES],
"modified": ["between", [start, end]],
}
if user:
filters["assigned_to"] = user

current_closed = frappe.db.count("CRM Task", filters=filters)

prev_start = frappe.utils.add_days(start, -1)
prev_end = start
prev_filters = filters.copy()
prev_filters["modified"] = ["between", [prev_start, prev_end]]

previous_closed = frappe.db.count("CRM Task", filters=prev_filters)
delta = current_closed - previous_closed

return {
"title": _("Today's closed tasks"),
"tooltip": _("Tasks marked closed today"),
"value": current_closed,
"delta": delta,
}


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.
Expand Down
2 changes: 1 addition & 1 deletion crm/fcrm/doctype/crm_dashboard/crm_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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"}}]'
return '[{"name":"total_site_visits","type":"number_chart","tooltip":"Total number of site visits","layout":{"x":0,"y":0,"w":4,"h":3,"i":"total_site_visits"}},{"name":"total_outgoing_calls","type":"number_chart","tooltip":"Total outgoing calls","layout":{"x":4,"y":0,"w":4,"h":3,"i":"total_outgoing_calls"}},{"name":"total_incoming_calls","type":"number_chart","tooltip":"Total incoming calls","layout":{"x":8,"y":0,"w":4,"h":3,"i":"total_incoming_calls"}},{"name":"todays_tasks","type":"number_chart","tooltip":"Tasks due today","layout":{"x":12,"y":0,"w":4,"h":3,"i":"todays_tasks"}},{"name":"todays_closed_tasks","type":"number_chart","tooltip":"Tasks closed today","layout":{"x":16,"y":0,"w":4,"h":3,"i":"todays_closed_tasks"}},{"name":"total_leads","type":"number_chart","tooltip":"Total number of leads","layout":{"x":0,"y":3,"w":4,"h":3,"i":"total_leads"}},{"name":"ongoing_deals","type":"number_chart","tooltip":"Total number of ongoing deals","layout":{"x":4,"y":3,"w":4,"h":3,"i":"ongoing_deals"}},{"name":"won_deals","type":"number_chart","tooltip":"Total number of won deals","layout":{"x":8,"y":3,"w":4,"h":3,"i":"won_deals"}},{"name":"average_won_deal_value","type":"number_chart","tooltip":"Average value of won deals","layout":{"x":12,"y":3,"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":16,"y":3,"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":0,"y":6,"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":6,"w":4,"h":3,"i":"average_time_to_close_a_deal"}},{"name":"spacer","type":"spacer","layout":{"x":8,"y":6,"w":12,"h":3,"i":"spacer"}},{"name":"sales_trend","type":"axis_chart","layout":{"x":0,"y":9,"w":10,"h":9,"i":"sales_trend"}},{"name":"forecasted_revenue","type":"axis_chart","layout":{"x":10,"y":9,"w":10,"h":9,"i":"forecasted_revenue"}},{"name":"funnel_conversion","type":"axis_chart","layout":{"x":0,"y":18,"w":10,"h":9,"i":"funnel_conversion"}},{"name":"deals_by_stage_donut","type":"donut_chart","layout":{"x":10,"y":18,"w":10,"h":9,"i":"deals_by_stage_donut"}},{"name":"lost_deal_reasons","type":"axis_chart","layout":{"x":0,"y":27,"w":20,"h":9,"i":"lost_deal_reasons"}},{"name":"leads_by_source","type":"donut_chart","layout":{"x":0,"y":36,"w":10,"h":9,"i":"leads_by_source"}},{"name":"deals_by_source","type":"donut_chart","layout":{"x":10,"y":36,"w":10,"h":9,"i":"deals_by_source"}},{"name":"deals_by_territory","type":"axis_chart","layout":{"x":0,"y":45,"w":10,"h":9,"i":"deals_by_territory"}},{"name":"deals_by_salesperson","type":"axis_chart","layout":{"x":10,"y":45,"w":10,"h":9,"i":"deals_by_salesperson"}}]'


def create_default_manager_dashboard(force=False):
Expand Down
13 changes: 13 additions & 0 deletions crm/fcrm/doctype/crm_deal/crm_deal.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def before_validate(self):
def validate(self):
self.set_primary_contact()
self.set_primary_email_mobile_no()
self.validate_mobile_no_permissions()
if not self.is_new() and self.has_value_changed("deal_owner") and self.deal_owner:
self.share_with_agent(self.deal_owner)
self.assign_agent(self.deal_owner)
Expand Down Expand Up @@ -134,6 +135,18 @@ def set_primary_email_mobile_no(self):
self.mobile_no = ""
self.phone = ""

def validate_mobile_no_permissions(self):
if self.is_new() or not self.has_value_changed("mobile_no"):
return

allowed_roles = {"Sales Manager", "Sales Master Manager"}
user_roles = set(frappe.get_roles(frappe.session.user))
if not user_roles.intersection(allowed_roles):
frappe.throw(
_("Only Sales Manager/Sales Master Manager can change the mobile number."),
exc=frappe.PermissionError,
)

def assign_agent(self, agent):
if not agent:
return
Expand Down
13 changes: 13 additions & 0 deletions crm/fcrm/doctype/crm_lead/crm_lead.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def validate(self):
self.set_lead_name()
self.set_title()
self.validate_email()
self.validate_mobile_no_permissions()
if not self.is_new() and self.has_value_changed("lead_owner") and self.lead_owner:
self.share_with_agent(self.lead_owner)
self.assign_agent(self.lead_owner)
Expand Down Expand Up @@ -126,6 +127,18 @@ def validate_email(self):
if self.is_new() or not self.image:
self.image = has_gravatar(self.email)

def validate_mobile_no_permissions(self):
if self.is_new() or not self.has_value_changed("mobile_no"):
return

allowed_roles = {"Sales Manager", "Sales Master Manager"}
user_roles = set(frappe.get_roles(frappe.session.user))
if not user_roles.intersection(allowed_roles):
frappe.throw(
_("Only Sales Manager/Sales Master Manager can change the mobile number."),
exc=frappe.PermissionError,
)

def assign_agent(self, agent):
if not agent:
return
Expand Down
3 changes: 2 additions & 1 deletion crm/modules.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
FCRM
Lead Syncing
Lead Syncing
vv-custom
Empty file added crm/vv_custom/__init__.py
Empty file.
Loading