mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-01 19:29:10 +00:00
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:
@@ -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']
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
43
erpnext/public/js/templates/call_link.html
Normal file
43
erpnext/public/js/templates/call_link.html
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user