feat: improved call log doctype

* Added links and some more fields into Call Log Doctype
* Display call info in the call log link pages
This commit is contained in:
leela
2021-01-12 23:31:40 +05:30
committed by Leela vadlamudi
parent 74feaf85dc
commit 3234df5581
6 changed files with 224 additions and 99 deletions

View File

@@ -81,4 +81,4 @@ def strip_number(number):
# strip 0 from the start of the number for proper number comparisions # strip 0 from the start of the number for proper number comparisions
# eg. 07888383332 should match with 7888383332 # eg. 07888383332 should match with 7888383332
number = number.lstrip('0') number = number.lstrip('0')
return number return number

View File

@@ -272,12 +272,9 @@ doc_events = {
}, },
"Contact": { "Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue", "on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information", "after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
"validate": "erpnext.crm.utils.update_lead_phone_numbers" "validate": "erpnext.crm.utils.update_lead_phone_numbers"
}, },
"Lead": {
"after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information"
},
"Email Unsubscribe": { "Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
}, },
@@ -582,3 +579,7 @@ global_search_doctypes = {
{'doctype': 'Hotel Room Type', 'index': 4} {'doctype': 'Hotel Room Type', 'index': 4}
] ]
} }
additional_timeline_content = {
'*': ['erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs']
}

View File

@@ -42,7 +42,8 @@
"public/js/hub/hub_factory.js", "public/js/hub/hub_factory.js",
"public/js/call_popup/call_popup.js", "public/js/call_popup/call_popup.js",
"public/js/utils/dimension_tree_filter.js", "public/js/utils/dimension_tree_filter.js",
"public/js/telephony.js" "public/js/telephony.js",
"public/js/templates/call_link.html"
], ],
"js/item-dashboard.min.js": [ "js/item-dashboard.min.js": [
"stock/dashboard/item_dashboard.html", "stock/dashboard/item_dashboard.html",

View File

@@ -0,0 +1,43 @@
<div class="call-detail-wrapper">
<div class="left-arrow"></div>
<div class="head text-muted">
<span>
<i class="fa fa-phone"> </i>
<span>
<span> {{ type }} Call</span>
-
<span> {{ frappe.format(duration, { fieldtype: "Duration" }) }}</span>
-
<span> {{ comment_when(creation) }}</span>
-
<!-- <span> {{ status }}</span>
- -->
<a class="text-muted" href="#Form/Call Log/{{name}}">Details</a>
{% if (show_call_button) { %}
<a class="pull-right">Callback</a>
{% } %}
</div>
<div class="body padding">
{% if (type === "Incoming") { %}
<span> Incoming call from {{ from }}, received by {{ to }}</span>
{% } else { %}
<span> Outgoing Call made by {{ from }} to {{ to }}</span>
{% } %}
<hr>
<div class="summary">
{% if (summary) { %}
<span>{{ summary }}</span>
{% } else { %}
<i class="text-muted">{{ __("No Summary") }}</i>
{% } %}
</div>
{% if (recording_url) { %}
<div class="margin-top">
<audio
controls
src="{{ recording_url }}">
</audio>
</div>
{% } %}
</div>
</div>

View File

@@ -8,20 +8,22 @@
"id", "id",
"from", "from",
"to", "to",
"column_break_3",
"received_by",
"medium", "medium",
"caller_information", "start_time",
"contact", "end_time",
"contact_name", "column_break_4",
"column_break_10", "type",
"customer", "customer",
"lead",
"lead_name",
"section_break_5",
"status", "status",
"duration", "duration",
"recording_url" "recording_url",
"recording_html",
"section_break_11",
"summary",
"section_break_19",
"links",
"column_break_3",
"section_break_5"
], ],
"fields": [ "fields": [
{ {
@@ -50,6 +52,7 @@
{ {
"fieldname": "to", "fieldname": "to",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1,
"label": "To", "label": "To",
"read_only": 1 "read_only": 1
}, },
@@ -58,13 +61,13 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1, "in_list_view": 1,
"label": "Status", "label": "Status",
"options": "Ringing\nIn Progress\nCompleted\nMissed", "options": "Ringing\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled",
"read_only": 1 "read_only": 1
}, },
{ {
"description": "Call Duration in seconds", "description": "Call Duration in seconds",
"fieldname": "duration", "fieldname": "duration",
"fieldtype": "Int", "fieldtype": "Duration",
"in_list_view": 1, "in_list_view": 1,
"label": "Duration", "label": "Duration",
"read_only": 1 "read_only": 1
@@ -72,8 +75,7 @@
{ {
"fieldname": "recording_url", "fieldname": "recording_url",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Recording URL", "label": "Recording URL"
"read_only": 1
}, },
{ {
"fieldname": "medium", "fieldname": "medium",
@@ -82,51 +84,52 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "received_by", "fieldname": "type",
"fieldtype": "Link", "fieldtype": "Select",
"label": "Received By", "label": "Type",
"options": "Employee", "options": "Incoming\nOutgoing",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "caller_information", "fieldname": "recording_html",
"fieldtype": "HTML",
"label": "Recording HTML"
},
{
"fieldname": "section_break_19",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Caller Information" "label": "Reference"
}, },
{ {
"fieldname": "contact", "fieldname": "links",
"fieldtype": "Link", "fieldtype": "Table",
"label": "Contact", "label": "Links",
"options": "Contact", "options": "Dynamic Link"
"read_only": 1
}, },
{ {
"fieldname": "lead", "fieldname": "column_break_4",
"fieldtype": "Link",
"label": "Lead ",
"options": "Lead",
"read_only": 1
},
{
"fetch_from": "contact.name",
"fieldname": "contact_name",
"fieldtype": "Data",
"hidden": 1,
"in_list_view": 1,
"label": "Contact Name",
"read_only": 1
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fetch_from": "lead.lead_name", "fieldname": "summary",
"fieldname": "lead_name", "fieldtype": "Small Text",
"fieldtype": "Data", "label": "Call Summary"
"hidden": 1, },
"in_list_view": 1, {
"label": "Lead Name", "fieldname": "section_break_11",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "start_time",
"fieldtype": "Datetime",
"label": "Start Time",
"read_only": 1
},
{
"fieldname": "end_time",
"fieldtype": "Datetime",
"label": "End Time",
"read_only": 1 "read_only": 1
}, },
{ {
@@ -137,9 +140,10 @@
"read_only": 1 "read_only": 1
} }
], ],
"in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-11-25 14:32:44.407815", "modified": "2021-01-13 12:28:20.288985",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Telephony", "module": "Telephony",
"name": "Call Log", "name": "Call Log",
@@ -162,8 +166,8 @@
"role": "Employee" "role": "Employee"
} }
], ],
"sort_field": "modified", "sort_field": "creation",
"sort_order": "ASC", "sort_order": "DESC",
"title_field": "from", "title_field": "from",
"track_changes": 1, "track_changes": 1,
"track_views": 1 "track_views": 1

View File

@@ -8,40 +8,83 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number
from frappe.contacts.doctype.contact.contact import get_contact_with_phone_number from frappe.contacts.doctype.contact.contact import get_contact_with_phone_number
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number
END_CALL_STATUSES = ['No Answer', 'Completed', 'Busy', 'Failed']
ONGOING_CALL_STATUSES = ['Ringing', 'In Progress']
class CallLog(Document): class CallLog(Document):
def validate(self):
deduplicate_dynamic_links(self)
def before_insert(self): def before_insert(self):
number = strip_number(self.get('from')) """Add lead(third party person) links to the document.
self.contact = get_contact_with_phone_number(number) """
self.lead = get_lead_with_phone_number(number) lead_number = self.get('from') if self.is_incoming_call() else self.get('to')
if self.contact: lead_number = strip_number(lead_number)
contact = frappe.get_doc("Contact", self.contact)
self.customer = contact.get_link_for("Customer") contact = get_contact_with_phone_number(strip_number(lead_number))
if contact:
self.add_link(link_type='Contact', link_name=contact)
lead = get_lead_with_phone_number(lead_number)
if lead:
self.add_link(link_type='Lead', link_name=lead)
def after_insert(self): def after_insert(self):
self.trigger_call_popup() self.trigger_call_popup()
def on_update(self): def on_update(self):
def _is_call_missed(doc_before_save, doc_after_save):
# FIXME: This works for Exotel but not for all telepony providers
return doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES
def _is_call_ended(doc_before_save, doc_after_save):
return doc_before_save.status not in END_CALL_STATUSES and self.status in END_CALL_STATUSES
doc_before_save = self.get_doc_before_save() doc_before_save = self.get_doc_before_save()
if not doc_before_save: return if not doc_before_save: return
if doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']:
frappe.publish_realtime('call_{id}_disconnected'.format(id=self.id), self) if _is_call_missed(doc_before_save, self):
elif doc_before_save.to != self.to: frappe.publish_realtime('call_{id}_missed'.format(id=self.id), self)
self.trigger_call_popup() self.trigger_call_popup()
if _is_call_ended(doc_before_save, self):
frappe.publish_realtime('call_{id}_ended'.format(id=self.id), self)
def is_incoming_call(self):
return self.type == 'Incoming'
def add_link(self, link_type, link_name):
self.append('links', {
'link_doctype': link_type,
'link_name': link_name
})
def trigger_call_popup(self): def trigger_call_popup(self):
scheduled_employees = get_scheduled_employees_for_popup(self.medium) if self.is_incoming_call():
employee_emails = get_employees_with_number(self.to) scheduled_employees = get_scheduled_employees_for_popup(self.medium)
employee_emails = get_employees_with_number(self.to)
# check if employees with matched number are scheduled to receive popup # check if employees with matched number are scheduled to receive popup
emails = set(scheduled_employees).intersection(employee_emails) emails = set(scheduled_employees).intersection(employee_emails)
# # if no employee found with matching phone number then show popup to scheduled employees if frappe.conf.developer_mode:
# emails = emails or scheduled_employees if employee_emails self.add_comment(text=f"""
Scheduled Employees: {scheduled_employees}
Matching Employee: {employee_emails}
Show Popup To: {emails}
""")
if employee_emails and not emails:
self.add_comment(text=_("No employee was scheduled for call popup"))
for email in emails:
frappe.publish_realtime('show_call_popup', self, user=email)
for email in emails:
frappe.publish_realtime('show_call_popup', self, user=email)
@frappe.whitelist() @frappe.whitelist()
def add_call_summary(call_log, summary): def add_call_summary(call_log, summary):
@@ -65,34 +108,67 @@ def get_employees_with_number(number):
return employee_emails return employee_emails
def set_caller_information(doc, state): def link_existing_conversations(doc, state):
'''Called from hooks on creation of Lead or Contact''' """
if doc.doctype not in ['Lead', 'Contact']: return Called from hooks on creation of Contact or Lead to link all the existing conversations.
"""
numbers = [doc.get('phone'), doc.get('mobile_no')] if doc.doctype != 'Contact': return
# contact for Contact and lead for Lead try:
fieldname = doc.doctype.lower()
# contact_name or lead_name
display_name_field = '{}_name'.format(fieldname)
# Contact now has all the nos saved in child table
if doc.doctype == 'Contact':
numbers = [d.phone for d in doc.phone_nos] numbers = [d.phone for d in doc.phone_nos]
for number in numbers: for number in numbers:
number = strip_number(number) number = strip_number(number)
if not number: continue if not number: continue
logs = frappe.db.sql_list("""
SELECT cl.name FROM `tabCall Log` cl
LEFT JOIN `tabDynamic Link` dl
ON cl.name = dl.parent
WHERE (cl.`from` like %(phone_number)s or cl.`to` like %(phone_number)s)
GROUP BY cl.name
HAVING SUM(
CASE
WHEN dl.link_doctype = %(doctype)s AND dl.link_name = %(docname)s
THEN 1
ELSE 0
END
)=0
""", dict(
phone_number='%{}'.format(number),
docname=doc.name,
doctype = doc.doctype
)
)
filters = frappe._dict({ for log in logs:
'from': ['like', '%{}'.format(number)], call_log = frappe.get_doc('Call Log', log)
fieldname: '' call_log.add_link(link_type=doc.doctype, link_name=doc.name)
call_log.save()
frappe.db.commit()
except Exception:
frappe.log_error(title=_('Error during caller information update'))
def get_linked_call_logs(doctype, docname):
# content will be shown in timeline
logs = frappe.get_all('Dynamic Link', fields=['parent'], filters={
'parenttype': 'Call Log',
'link_doctype': doctype,
'link_name': docname
})
logs = set([log.parent for log in logs])
logs = frappe.get_all('Call Log', fields=['*'], filters={
'name': ['in', logs]
})
timeline_contents = []
for log in logs:
log.show_call_button = 0
timeline_contents.append({
'creation': log.creation,
'template': 'call_link',
'template_data': log
}) })
logs = frappe.get_all('Call Log', filters=filters) return timeline_contents
for log in logs:
frappe.db.set_value('Call Log', log.name, {
fieldname: doc.name,
display_name_field: doc.get_title()
}, update_modified=False)