Merge pull request #29962 from nemesis189/exotel-fixes

fix: Exotel Call Log fixes
This commit is contained in:
Suraj Shetty 2022-04-13 20:36:03 +05:30 committed by GitHub
commit d890ee9495
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 363 additions and 30 deletions

View File

@ -65,7 +65,6 @@ class TestRequestforQuotation(FrappeTestCase):
)
sq.submit()
frappe.form_dict = frappe.local("form_dict")
frappe.form_dict.name = rfq.name
self.assertEqual(check_supplier_has_docname_access(supplier_wt_appos[0].get("supplier")), True)

View File

@ -37,11 +37,26 @@ def handle_end_call(**kwargs):
@frappe.whitelist(allow_guest=True)
def handle_missed_call(**kwargs):
update_call_log(kwargs, "Missed")
status = ""
call_type = kwargs.get("CallType")
dial_call_status = kwargs.get("DialCallStatus")
if call_type == "incomplete" and dial_call_status == "no-answer":
status = "No Answer"
elif call_type == "client-hangup" and dial_call_status == "canceled":
status = "Canceled"
elif call_type == "incomplete" and dial_call_status == "failed":
status = "Failed"
update_call_log(kwargs, status)
def update_call_log(call_payload, status="Ringing", call_log=None):
call_log = call_log or get_call_log(call_payload)
# for a new sid, call_log and get_call_log will be empty so create a new log
if not call_log:
call_log = create_call_log(call_payload)
if call_log:
call_log.status = status
call_log.to = call_payload.get("DialWhomNumber")
@ -53,16 +68,9 @@ def update_call_log(call_payload, status="Ringing", call_log=None):
def get_call_log(call_payload):
call_log = frappe.get_all(
"Call Log",
{
"id": call_payload.get("CallSid"),
},
limit=1,
)
if call_log:
return frappe.get_doc("Call Log", call_log[0].name)
call_log_id = call_payload.get("CallSid")
if frappe.db.exists("Call Log", call_log_id):
return frappe.get_doc("Call Log", call_log_id)
def create_call_log(call_payload):

View File

@ -4,7 +4,7 @@
"allow_import": 1,
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2013-03-07 09:04:18",
"creation": "2022-02-21 11:54:09.632218",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
@ -813,11 +813,12 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2021-06-17 11:31:37.730760",
"modified": "2022-03-22 13:44:37.088519",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@ -857,7 +858,9 @@
],
"search_fields": "employee_name",
"show_name_in_global_search": 1,
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "employee_name"
}

View File

@ -140,6 +140,14 @@ class CallPopup {
}, {
'fieldtype': 'Section Break',
'hide_border': 1,
}, {
'fieldname': 'call_type',
'label': 'Call Type',
'fieldtype': 'Link',
'options': 'Telephony Call Type',
}, {
'fieldtype': 'Section Break',
'hide_border': 1,
}, {
'fieldtype': 'Small Text',
'label': __('Call Summary'),
@ -149,10 +157,12 @@ class CallPopup {
'label': __('Save'),
'click': () => {
const call_summary = this.call_details.get_value('call_summary');
const call_type = this.call_details.get_value('call_type');
if (!call_summary) return;
frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', {
frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary_and_call_type', {
'call_log': this.call_log.name,
'summary': call_summary,
'call_type': call_type,
}).then(() => {
this.close_modal();
frappe.show_alert({

View File

@ -19,7 +19,7 @@ class TestQualityProcedure(unittest.TestCase):
)
).insert()
frappe.form_dict = dict(
frappe.local.form_dict = frappe._dict(
doctype="Quality Procedure",
quality_procedure_name="Test Child 1",
parent_quality_procedure=procedure.name,

View File

@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "field:id",
"creation": "2019-06-05 12:07:02.634534",
"creation": "2022-02-21 11:54:58.414784",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@ -9,6 +9,8 @@
"id",
"from",
"to",
"call_received_by",
"employee_user_id",
"medium",
"start_time",
"end_time",
@ -20,6 +22,7 @@
"recording_url",
"recording_html",
"section_break_11",
"type_of_call",
"summary",
"section_break_19",
"links"
@ -103,7 +106,8 @@
},
{
"fieldname": "summary",
"fieldtype": "Small Text"
"fieldtype": "Small Text",
"label": "Summary"
},
{
"fieldname": "section_break_11",
@ -134,15 +138,34 @@
"fieldname": "call_details_section",
"fieldtype": "Section Break",
"label": "Call Details"
},
{
"fieldname": "employee_user_id",
"fieldtype": "Data",
"hidden": 1,
"label": "Employee User Id"
},
{
"fieldname": "type_of_call",
"fieldtype": "Data",
"label": "Type Of Call"
},
{
"depends_on": "to",
"fieldname": "call_received_by",
"fieldtype": "Data",
"label": "Call Received By",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-08 14:23:28.744844",
"modified": "2022-02-25 14:37:48.575230",
"modified_by": "Administrator",
"module": "Telephony",
"name": "Call Log",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@ -164,6 +187,7 @@
],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "from",
"track_changes": 1,
"track_views": 1

View File

@ -32,6 +32,10 @@ class CallLog(Document):
if lead:
self.add_link(link_type="Lead", link_name=lead)
# Add Employee Name
if self.is_incoming_call():
self.update_received_by()
def after_insert(self):
self.trigger_call_popup()
@ -49,6 +53,9 @@ class CallLog(Document):
if not doc_before_save:
return
if self.is_incoming_call() and self.has_value_changed("to"):
self.update_received_by()
if _is_call_missed(doc_before_save, self):
frappe.publish_realtime("call_{id}_missed".format(id=self.id), self)
self.trigger_call_popup()
@ -65,7 +72,8 @@ class CallLog(Document):
def trigger_call_popup(self):
if self.is_incoming_call():
scheduled_employees = get_scheduled_employees_for_popup(self.medium)
employee_emails = get_employees_with_number(self.to)
employees = get_employees_with_number(self.to)
employee_emails = [employee.get("user_id") for employee in employees]
# check if employees with matched number are scheduled to receive popup
emails = set(scheduled_employees).intersection(employee_emails)
@ -85,10 +93,17 @@ class CallLog(Document):
for email in emails:
frappe.publish_realtime("show_call_popup", self, user=email)
def update_received_by(self):
if employees := get_employees_with_number(self.get("to")):
self.call_received_by = employees[0].get("name")
self.employee_user_id = employees[0].get("user_id")
@frappe.whitelist()
def add_call_summary(call_log, summary):
def add_call_summary_and_call_type(call_log, summary, call_type):
doc = frappe.get_doc("Call Log", call_log)
doc.type_of_call = call_type
doc.save()
doc.add_comment("Comment", frappe.bold(_("Call Summary")) + "<br><br>" + summary)
@ -97,20 +112,19 @@ def get_employees_with_number(number):
if not number:
return []
employee_emails = frappe.cache().hget("employees_with_number", number)
if employee_emails:
return employee_emails
employee_doc_name_and_emails = frappe.cache().hget("employees_with_number", number)
if employee_doc_name_and_emails:
return employee_doc_name_and_emails
employees = frappe.get_all(
employee_doc_name_and_emails = frappe.get_all(
"Employee",
filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]},
fields=["user_id"],
filters={"cell_number": ["like", f"%{number}%"], "user_id": ["!=", ""]},
fields=["name", "user_id"],
)
employee_emails = [employee.user_id for employee in employees]
frappe.cache().hset("employees_with_number", number, employee_emails)
frappe.cache().hset("employees_with_number", number, employee_doc_name_and_emails)
return employee_emails
return employee_doc_name_and_emails
def link_existing_conversations(doc, state):

View File

@ -0,0 +1,8 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Telephony Call Type', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,58 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:call_type",
"creation": "2022-02-25 16:13:37.321312",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"call_type",
"amended_from"
],
"fields": [
{
"fieldname": "call_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Call Type",
"reqd": 1,
"unique": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Telephony Call Type",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-02-25 16:14:07.087461",
"modified_by": "Administrator",
"module": "Telephony",
"name": "Telephony Call Type",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class TelephonyCallType(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestTelephonyCallType(unittest.TestCase):
pass

View File

@ -0,0 +1,122 @@
import frappe
call_initiation_data = frappe._dict(
{
"CallSid": "23c162077629863c1a2d7f29263a162m",
"CallFrom": "09999999991",
"CallTo": "09999999980",
"Direction": "incoming",
"Created": "Wed, 23 Feb 2022 12:31:59",
"From": "09999999991",
"To": "09999999988",
"CurrentTime": "2022-02-23 12:32:02",
"DialWhomNumber": "09999999999",
"Status": "busy",
"EventType": "Dial",
"AgentEmail": "test_employee_exotel@company.com",
}
)
call_end_data = frappe._dict(
{
"CallSid": "23c162077629863c1a2d7f29263a162m",
"CallFrom": "09999999991",
"CallTo": "09999999980",
"Direction": "incoming",
"ForwardedFrom": "null",
"Created": "Wed, 23 Feb 2022 12:31:59",
"DialCallDuration": "17",
"RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3",
"StartTime": "2022-02-23 12:31:58",
"EndTime": "1970-01-01 05:30:00",
"DialCallStatus": "completed",
"CallType": "completed",
"DialWhomNumber": "09999999999",
"ProcessStatus": "null",
"flow_id": "228040",
"tenant_id": "67291",
"From": "09999999991",
"To": "09999999988",
"RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25",
"CurrentTime": "2022-02-23 12:32:25",
"OutgoingPhoneNumber": "09999999988",
"Legs": [
{
"Number": "09999999999",
"Type": "single",
"OnCallDuration": "10",
"CallerId": "09999999980",
"CauseCode": "NORMAL_CLEARING",
"Cause": "16",
}
],
}
)
call_disconnected_data = frappe._dict(
{
"CallSid": "d96421addce69e24bdc7ce5880d1162l",
"CallFrom": "09999999991",
"CallTo": "09999999980",
"Direction": "incoming",
"ForwardedFrom": "null",
"Created": "Mon, 21 Feb 2022 15:58:12",
"DialCallDuration": "0",
"StartTime": "2022-02-21 15:58:12",
"EndTime": "1970-01-01 05:30:00",
"DialCallStatus": "canceled",
"CallType": "client-hangup",
"DialWhomNumber": "09999999999",
"ProcessStatus": "null",
"flow_id": "228040",
"tenant_id": "67291",
"From": "09999999991",
"To": "09999999988",
"CurrentTime": "2022-02-21 15:58:47",
"OutgoingPhoneNumber": "09999999988",
"Legs": [
{
"Number": "09999999999",
"Type": "single",
"OnCallDuration": "0",
"CallerId": "09999999980",
"CauseCode": "RING_TIMEOUT",
"Cause": "1003",
}
],
}
)
call_not_answered_data = frappe._dict(
{
"CallSid": "fdb67a2b4b2d057b610a52ef43f81622",
"CallFrom": "09999999991",
"CallTo": "09999999980",
"Direction": "incoming",
"ForwardedFrom": "null",
"Created": "Mon, 21 Feb 2022 15:47:02",
"DialCallDuration": "0",
"StartTime": "2022-02-21 15:47:02",
"EndTime": "1970-01-01 05:30:00",
"DialCallStatus": "no-answer",
"CallType": "incomplete",
"DialWhomNumber": "09999999999",
"ProcessStatus": "null",
"flow_id": "228040",
"tenant_id": "67291",
"From": "09999999991",
"To": "09999999988",
"CurrentTime": "2022-02-21 15:47:40",
"OutgoingPhoneNumber": "09999999988",
"Legs": [
{
"Number": "09999999999",
"Type": "single",
"OnCallDuration": "0",
"CallerId": "09999999980",
"CauseCode": "RING_TIMEOUT",
"Cause": "1003",
}
],
}
)

View File

@ -0,0 +1,69 @@
import frappe
from frappe.contacts.doctype.contact.test_contact import create_contact
from frappe.tests.test_api import FrappeAPITestCase
from erpnext.hr.doctype.employee.test_employee import make_employee
class TestExotel(FrappeAPITestCase):
@classmethod
def setUpClass(cls):
cls.CURRENT_DB_CONNECTION = frappe.db
cls.test_employee_name = make_employee(
user="test_employee_exotel@company.com", cell_number="9999999999"
)
frappe.db.set_value("Exotel Settings", "Exotel Settings", "enabled", 1)
phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}]
create_contact(name="Test Contact", salutation="Mr", phones=phones)
frappe.db.commit()
def test_for_successful_call(self):
from .exotel_test_data import call_end_data, call_initiation_data
api_method = "handle_incoming_call"
end_call_api_method = "handle_end_call"
self.emulate_api_call_from_exotel(api_method, call_initiation_data)
self.emulate_api_call_from_exotel(end_call_api_method, call_end_data)
call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid)
self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom)
self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber)
self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
self.assertEqual(call_log.get("status"), "Completed")
def test_for_disconnected_call(self):
from .exotel_test_data import call_disconnected_data
api_method = "handle_missed_call"
self.emulate_api_call_from_exotel(api_method, call_disconnected_data)
call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid)
self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom)
self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber)
self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
self.assertEqual(call_log.get("status"), "Canceled")
def test_for_call_not_answered(self):
from .exotel_test_data import call_not_answered_data
api_method = "handle_missed_call"
self.emulate_api_call_from_exotel(api_method, call_not_answered_data)
call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid)
self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom)
self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber)
self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
self.assertEqual(call_log.get("status"), "No Answer")
def emulate_api_call_from_exotel(self, api_method, data):
self.post(
f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}",
data=frappe.as_json(data),
content_type="application/json",
as_tuple=True,
)
# restart db connection to get latest data
frappe.connect()
@classmethod
def tearDownClass(cls):
frappe.db = cls.CURRENT_DB_CONNECTION