feat: improved call log doctype
* Added links and some more fields into Call Log Doctype
* Display call info in the call log link pages
diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py
index 885ef05..4ccd9bd 100644
--- a/erpnext/crm/doctype/utils.py
+++ b/erpnext/crm/doctype/utils.py
@@ -81,4 +81,4 @@
# strip 0 from the start of the number for proper number comparisions
# eg. 07888383332 should match with 7888383332
number = number.lstrip('0')
- return number
\ No newline at end of file
+ return number
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 5430221..c7efbba 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -272,12 +272,9 @@
},
"Contact": {
"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"
},
- "Lead": {
- "after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information"
- },
"Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
},
@@ -582,3 +579,7 @@
{'doctype': 'Hotel Room Type', 'index': 4}
]
}
+
+additional_timeline_content = {
+ '*': ['erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs']
+}
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index b4a1cf8..b289785 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -42,7 +42,8 @@
"public/js/hub/hub_factory.js",
"public/js/call_popup/call_popup.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": [
"stock/dashboard/item_dashboard.html",
diff --git a/erpnext/public/js/templates/call_link.html b/erpnext/public/js/templates/call_link.html
new file mode 100644
index 0000000..08bdf14
--- /dev/null
+++ b/erpnext/public/js/templates/call_link.html
@@ -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>
diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json
index 55ad2ba..1ecd884 100644
--- a/erpnext/telephony/doctype/call_log/call_log.json
+++ b/erpnext/telephony/doctype/call_log/call_log.json
@@ -8,20 +8,22 @@
"id",
"from",
"to",
- "column_break_3",
- "received_by",
"medium",
- "caller_information",
- "contact",
- "contact_name",
- "column_break_10",
+ "start_time",
+ "end_time",
+ "column_break_4",
+ "type",
"customer",
- "lead",
- "lead_name",
- "section_break_5",
"status",
"duration",
- "recording_url"
+ "recording_url",
+ "recording_html",
+ "section_break_11",
+ "summary",
+ "section_break_19",
+ "links",
+ "column_break_3",
+ "section_break_5"
],
"fields": [
{
@@ -50,6 +52,7 @@
{
"fieldname": "to",
"fieldtype": "Data",
+ "in_list_view": 1,
"label": "To",
"read_only": 1
},
@@ -58,13 +61,13 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
- "options": "Ringing\nIn Progress\nCompleted\nMissed",
+ "options": "Ringing\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled",
"read_only": 1
},
{
"description": "Call Duration in seconds",
"fieldname": "duration",
- "fieldtype": "Int",
+ "fieldtype": "Duration",
"in_list_view": 1,
"label": "Duration",
"read_only": 1
@@ -72,8 +75,7 @@
{
"fieldname": "recording_url",
"fieldtype": "Data",
- "label": "Recording URL",
- "read_only": 1
+ "label": "Recording URL"
},
{
"fieldname": "medium",
@@ -82,51 +84,52 @@
"read_only": 1
},
{
- "fieldname": "received_by",
- "fieldtype": "Link",
- "label": "Received By",
- "options": "Employee",
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "label": "Type",
+ "options": "Incoming\nOutgoing",
"read_only": 1
},
{
- "fieldname": "caller_information",
+ "fieldname": "recording_html",
+ "fieldtype": "HTML",
+ "label": "Recording HTML"
+ },
+ {
+ "fieldname": "section_break_19",
"fieldtype": "Section Break",
- "label": "Caller Information"
+ "label": "Reference"
},
{
- "fieldname": "contact",
- "fieldtype": "Link",
- "label": "Contact",
- "options": "Contact",
- "read_only": 1
+ "fieldname": "links",
+ "fieldtype": "Table",
+ "label": "Links",
+ "options": "Dynamic Link"
},
{
- "fieldname": "lead",
- "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",
+ "fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
- "fetch_from": "lead.lead_name",
- "fieldname": "lead_name",
- "fieldtype": "Data",
- "hidden": 1,
- "in_list_view": 1,
- "label": "Lead Name",
+ "fieldname": "summary",
+ "fieldtype": "Small Text",
+ "label": "Call Summary"
+ },
+ {
+ "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
},
{
@@ -137,9 +140,10 @@
"read_only": 1
}
],
+ "in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-25 14:32:44.407815",
+ "modified": "2021-01-13 12:28:20.288985",
"modified_by": "Administrator",
"module": "Telephony",
"name": "Call Log",
@@ -162,8 +166,8 @@
"role": "Employee"
}
],
- "sort_field": "modified",
- "sort_order": "ASC",
+ "sort_field": "creation",
+ "sort_order": "DESC",
"title_field": "from",
"track_changes": 1,
"track_views": 1
diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py
index 296473e..a277a5f 100644
--- a/erpnext/telephony/doctype/call_log/call_log.py
+++ b/erpnext/telephony/doctype/call_log/call_log.py
@@ -8,40 +8,83 @@
from frappe.model.document import Document
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.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
+
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):
+ def validate(self):
+ deduplicate_dynamic_links(self)
+
def before_insert(self):
- number = strip_number(self.get('from'))
- self.contact = get_contact_with_phone_number(number)
- self.lead = get_lead_with_phone_number(number)
- if self.contact:
- contact = frappe.get_doc("Contact", self.contact)
- self.customer = contact.get_link_for("Customer")
+ """Add lead(third party person) links to the document.
+ """
+ lead_number = self.get('from') if self.is_incoming_call() else self.get('to')
+ lead_number = strip_number(lead_number)
+
+ 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):
self.trigger_call_popup()
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()
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)
- elif doc_before_save.to != self.to:
+
+ if _is_call_missed(doc_before_save, self):
+ frappe.publish_realtime('call_{id}_missed'.format(id=self.id), self)
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):
- scheduled_employees = get_scheduled_employees_for_popup(self.medium)
- employee_emails = get_employees_with_number(self.to)
+ if self.is_incoming_call():
+ 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
- emails = set(scheduled_employees).intersection(employee_emails)
+ # check if employees with matched number are scheduled to receive popup
+ emails = set(scheduled_employees).intersection(employee_emails)
- # # if no employee found with matching phone number then show popup to scheduled employees
- # emails = emails or scheduled_employees if employee_emails
+ if frappe.conf.developer_mode:
+ self.add_comment(text=f"""
+ Scheduled Employees: {scheduled_employees}
+ Matching Employee: {employee_emails}
+ Show Popup To: {emails}
+ """)
- for email in emails:
- frappe.publish_realtime('show_call_popup', self, user=email)
+ 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)
+
@frappe.whitelist()
def add_call_summary(call_log, summary):
@@ -65,34 +108,67 @@
return employee_emails
-def set_caller_information(doc, state):
- '''Called from hooks on creation of Lead or Contact'''
- if doc.doctype not in ['Lead', 'Contact']: return
-
- numbers = [doc.get('phone'), doc.get('mobile_no')]
- # contact for Contact and lead for Lead
- 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':
+def link_existing_conversations(doc, state):
+ """
+ Called from hooks on creation of Contact or Lead to link all the existing conversations.
+ """
+ if doc.doctype != 'Contact': return
+ try:
numbers = [d.phone for d in doc.phone_nos]
- for number in numbers:
- number = strip_number(number)
- if not number: continue
+ for number in numbers:
+ number = strip_number(number)
+ 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({
- 'from': ['like', '%{}'.format(number)],
- fieldname: ''
+ for log in logs:
+ call_log = frappe.get_doc('Call Log', log)
+ 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)