From 3234df55818556088faf8ee8d449c32ac7da5783 Mon Sep 17 00:00:00 2001 From: leela Date: Tue, 12 Jan 2021 23:31:40 +0530 Subject: [PATCH] feat: improved call log doctype * Added links and some more fields into Call Log Doctype * Display call info in the call log link pages --- erpnext/crm/doctype/utils.py | 2 +- erpnext/hooks.py | 9 +- erpnext/public/build.json | 3 +- erpnext/public/js/templates/call_link.html | 43 +++++ .../telephony/doctype/call_log/call_log.json | 106 ++++++------ .../telephony/doctype/call_log/call_log.py | 160 +++++++++++++----- 6 files changed, 224 insertions(+), 99 deletions(-) create mode 100644 erpnext/public/js/templates/call_link.html diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py index 885ef0584d..4ccd9bd73b 100644 --- a/erpnext/crm/doctype/utils.py +++ b/erpnext/crm/doctype/utils.py @@ -81,4 +81,4 @@ def strip_number(number): # 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 5430221d66..c7efbbad8d 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -272,12 +272,9 @@ doc_events = { }, "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 @@ global_search_doctypes = { {'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 b4a1cf81be..b2897852f0 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 0000000000..08bdf142a7 --- /dev/null +++ b/erpnext/public/js/templates/call_link.html @@ -0,0 +1,43 @@ +
+
+
+ + + + {{ type }} Call + - + {{ frappe.format(duration, { fieldtype: "Duration" }) }} + - + {{ comment_when(creation) }} + - + + Details + {% if (show_call_button) { %} + Callback + {% } %} +
+
+ {% if (type === "Incoming") { %} + Incoming call from {{ from }}, received by {{ to }} + {% } else { %} + Outgoing Call made by {{ from }} to {{ to }} + {% } %} +
+
+ {% if (summary) { %} + {{ summary }} + {% } else { %} + {{ __("No Summary") }} + {% } %} +
+ {% if (recording_url) { %} +
+ +
+ {% } %} +
+
diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json index 55ad2baefd..1ecd884bbd 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 296473efe1..a277a5f956 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 import _ 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} + """) + + 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() def add_call_summary(call_log, summary): @@ -65,34 +108,67 @@ def get_employees_with_number(number): 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)