fix: create call logs with recording_url
This commit is contained in:
parent
3c0f5289d1
commit
b13fa6a503
0
crm/crm/doctype/crm_call_log/__init__.py
Normal file
0
crm/crm/doctype/crm_call_log/__init__.py
Normal file
8
crm/crm/doctype/crm_call_log/crm_call_log.js
Normal file
8
crm/crm/doctype/crm_call_log/crm_call_log.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("CRM Call Log", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
129
crm/crm/doctype/crm_call_log/crm_call_log.json
Normal file
129
crm/crm/doctype/crm_call_log/crm_call_log.json
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "field:id",
|
||||||
|
"creation": "2023-08-28 00:23:36.229137",
|
||||||
|
"default_view": "List",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"id",
|
||||||
|
"from",
|
||||||
|
"status",
|
||||||
|
"call_received_by",
|
||||||
|
"medium",
|
||||||
|
"start_time",
|
||||||
|
"column_break_ufnp",
|
||||||
|
"type",
|
||||||
|
"to",
|
||||||
|
"lead",
|
||||||
|
"duration",
|
||||||
|
"recording_url",
|
||||||
|
"end_time"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "ID",
|
||||||
|
"unique": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "From"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Ringing\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "to",
|
||||||
|
"fieldname": "call_received_by",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Call Received By",
|
||||||
|
"options": "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "start_time",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Start Time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "medium",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Medium"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ufnp",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Type",
|
||||||
|
"options": "Incoming\nOutgoing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "to",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "To"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "duration",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Duration",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "recording_url",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Recording URL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "end_time",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "End Time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "lead",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Lead/Deal",
|
||||||
|
"options": "CRM Lead"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2023-08-28 01:34:24.864624",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "CRM",
|
||||||
|
"name": "CRM Call Log",
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
9
crm/crm/doctype/crm_call_log/crm_call_log.py
Normal file
9
crm/crm/doctype/crm_call_log/crm_call_log.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CRMCallLog(Document):
|
||||||
|
pass
|
||||||
9
crm/crm/doctype/crm_call_log/test_crm_call_log.py
Normal file
9
crm/crm/doctype/crm_call_log/test_crm_call_log.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestCRMCallLog(FrappeTestCase):
|
||||||
|
pass
|
||||||
@ -12,7 +12,10 @@
|
|||||||
"api_secret",
|
"api_secret",
|
||||||
"column_break_idds",
|
"column_break_idds",
|
||||||
"auth_token",
|
"auth_token",
|
||||||
"twiml_sid"
|
"twiml_sid",
|
||||||
|
"section_break_ssqj",
|
||||||
|
"record_calls",
|
||||||
|
"column_break_avmt"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -47,12 +50,26 @@
|
|||||||
"fieldname": "twiml_sid",
|
"fieldname": "twiml_sid",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "TwiML SID"
|
"label": "TwiML SID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ssqj",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "record_calls",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Record Calls"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_avmt",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-08-17 18:38:02.655918",
|
"modified": "2023-08-28 00:44:24.942914",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Twilio Settings",
|
"name": "Twilio Settings",
|
||||||
|
|||||||
@ -2,7 +2,8 @@ from werkzeug.wrappers import Response
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from .twilio_handler import Twilio, IncomingCall
|
from frappe import _
|
||||||
|
from .twilio_handler import Twilio, IncomingCall, TwilioCallDetails
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def generate_access_token():
|
def generate_access_token():
|
||||||
@ -46,19 +47,57 @@ def voice(**kwargs):
|
|||||||
from_number = _get_caller_number(args.Caller)
|
from_number = _get_caller_number(args.Caller)
|
||||||
resp = twilio.generate_twilio_dial_response(from_number, args.To)
|
resp = twilio.generate_twilio_dial_response(from_number, args.To)
|
||||||
|
|
||||||
# call_details = TwilioCallDetails(args, call_from=from_number)
|
call_details = TwilioCallDetails(args, call_from=from_number)
|
||||||
# create_call_log(call_details)
|
create_call_log(call_details)
|
||||||
return Response(resp.to_xml(), mimetype='text/xml')
|
return Response(resp.to_xml(), mimetype='text/xml')
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def twilio_incoming_call_handler(**kwargs):
|
def twilio_incoming_call_handler(**kwargs):
|
||||||
args = frappe._dict(kwargs)
|
args = frappe._dict(kwargs)
|
||||||
# call_details = TwilioCallDetails(args)
|
call_details = TwilioCallDetails(args)
|
||||||
# create_call_log(call_details)
|
create_call_log(call_details)
|
||||||
|
|
||||||
resp = IncomingCall(args.From, args.To).process()
|
resp = IncomingCall(args.From, args.To).process()
|
||||||
return Response(resp.to_xml(), mimetype='text/xml')
|
return Response(resp.to_xml(), mimetype='text/xml')
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def create_call_log(call_details: TwilioCallDetails):
|
||||||
|
call_log = frappe.get_doc({**call_details.to_dict(),
|
||||||
|
'doctype': 'CRM Call Log',
|
||||||
|
'medium': 'Twilio'
|
||||||
|
})
|
||||||
|
|
||||||
|
call_log.flags.ignore_permissions = True
|
||||||
|
call_log.save()
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def update_call_log(call_sid, status=None):
|
||||||
|
"""Update call log status.
|
||||||
|
"""
|
||||||
|
twilio = Twilio.connect()
|
||||||
|
if not (twilio and frappe.db.exists("CRM Call Log", call_sid)): return
|
||||||
|
|
||||||
|
call_details = twilio.get_call_info(call_sid)
|
||||||
|
call_log = frappe.get_doc("CRM Call Log", call_sid)
|
||||||
|
call_log.status = status or TwilioCallDetails.get_call_status(call_details.status)
|
||||||
|
call_log.duration = call_details.duration
|
||||||
|
call_log.start_time = get_datetime_from_timestamp(call_details.start_time)
|
||||||
|
call_log.end_time = get_datetime_from_timestamp(call_details.end_time)
|
||||||
|
call_log.flags.ignore_permissions = True
|
||||||
|
call_log.save()
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def update_recording_info(**kwargs):
|
||||||
|
try:
|
||||||
|
args = frappe._dict(kwargs)
|
||||||
|
recording_url = args.RecordingUrl
|
||||||
|
call_sid = args.CallSid
|
||||||
|
update_call_log(call_sid)
|
||||||
|
frappe.db.set_value("CRM Call Log", call_sid, "recording_url", recording_url)
|
||||||
|
except:
|
||||||
|
frappe.log_error(title=_("Failed to capture Twilio recording"))
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_call_info(**kwargs):
|
def get_call_info(**kwargs):
|
||||||
@ -78,4 +117,14 @@ def get_call_info(**kwargs):
|
|||||||
client = Twilio.get_twilio_client()
|
client = Twilio.get_twilio_client()
|
||||||
client.calls(args.ParentCallSid).user_defined_messages.create(
|
client.calls(args.ParentCallSid).user_defined_messages.create(
|
||||||
content=json.dumps(call_info)
|
content=json.dumps(call_info)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_datetime_from_timestamp(timestamp):
|
||||||
|
from datetime import datetime
|
||||||
|
from pytz import timezone
|
||||||
|
|
||||||
|
datetime_utc_tz_str = timestamp.strftime('%Y-%m-%d %H:%M:%S%z')
|
||||||
|
datetime_utc_tz = datetime.strptime(datetime_utc_tz_str, '%Y-%m-%d %H:%M:%S%z')
|
||||||
|
system_timezone = frappe.utils.get_system_timezone()
|
||||||
|
converted_datetime = datetime_utc_tz.astimezone(timezone(system_timezone))
|
||||||
|
return frappe.utils.format_datetime(converted_datetime, 'yyyy-MM-dd HH:mm:ss')
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import re
|
|
||||||
import json
|
|
||||||
from twilio.rest import Client as TwilioClient
|
from twilio.rest import Client as TwilioClient
|
||||||
from twilio.jwt.access_token import AccessToken
|
from twilio.jwt.access_token import AccessToken
|
||||||
from twilio.jwt.access_token.grants import VoiceGrant
|
from twilio.jwt.access_token.grants import VoiceGrant
|
||||||
@ -69,6 +67,10 @@ class Twilio:
|
|||||||
"""Convert safe identity string into emailID.
|
"""Convert safe identity string into emailID.
|
||||||
"""
|
"""
|
||||||
return identity.replace('(at)', '@')
|
return identity.replace('(at)', '@')
|
||||||
|
|
||||||
|
def get_recording_status_callback_url(self):
|
||||||
|
url_path = "/api/method/crm.twilio.api.update_recording_info"
|
||||||
|
return get_public_url(url_path)
|
||||||
|
|
||||||
def get_call_status_callback_url(self):
|
def get_call_status_callback_url(self):
|
||||||
url_path = "/api/method/crm.twilio.api.get_call_info"
|
url_path = "/api/method/crm.twilio.api.get_call_info"
|
||||||
@ -80,9 +82,9 @@ class Twilio:
|
|||||||
resp = VoiceResponse()
|
resp = VoiceResponse()
|
||||||
dial = Dial(
|
dial = Dial(
|
||||||
caller_id=from_number,
|
caller_id=from_number,
|
||||||
# record=self.settings.record_calls,
|
record=self.settings.record_calls,
|
||||||
# recording_status_callback=self.get_recording_status_callback_url(),
|
recording_status_callback=self.get_recording_status_callback_url(),
|
||||||
# recording_status_callback_event='completed'
|
recording_status_callback_event='completed'
|
||||||
)
|
)
|
||||||
dial.number(
|
dial.number(
|
||||||
to_number,
|
to_number,
|
||||||
@ -102,9 +104,9 @@ class Twilio:
|
|||||||
resp = VoiceResponse()
|
resp = VoiceResponse()
|
||||||
dial = Dial(
|
dial = Dial(
|
||||||
ring_tone=ring_tone,
|
ring_tone=ring_tone,
|
||||||
# record=self.settings.record_calls,
|
record=self.settings.record_calls,
|
||||||
# recording_status_callback=self.get_recording_status_callback_url(),
|
recording_status_callback=self.get_recording_status_callback_url(),
|
||||||
# recording_status_callback_event='completed'
|
recording_status_callback_event='completed'
|
||||||
)
|
)
|
||||||
dial.client(client)
|
dial.client(client)
|
||||||
resp.append(dial)
|
resp.append(dial)
|
||||||
@ -187,3 +189,41 @@ def get_the_call_attender(owners):
|
|||||||
if ((details['call_receiving_device'] == 'Phone' and details['mobile_no']) or
|
if ((details['call_receiving_device'] == 'Phone' and details['mobile_no']) or
|
||||||
(details['call_receiving_device'] == 'Computer' and name in current_loggedin_users)):
|
(details['call_receiving_device'] == 'Computer' and name in current_loggedin_users)):
|
||||||
return details
|
return details
|
||||||
|
|
||||||
|
|
||||||
|
class TwilioCallDetails:
|
||||||
|
def __init__(self, call_info, call_from = None, call_to = None):
|
||||||
|
self.call_info = call_info
|
||||||
|
self.account_sid = call_info.get('AccountSid')
|
||||||
|
self.application_sid = call_info.get('ApplicationSid')
|
||||||
|
self.call_sid = call_info.get('CallSid')
|
||||||
|
self.call_status = self.get_call_status(call_info.get('CallStatus'))
|
||||||
|
self._call_from = call_from
|
||||||
|
self._call_to = call_to
|
||||||
|
|
||||||
|
def get_direction(self):
|
||||||
|
if self.call_info.get('Caller').lower().startswith('client'):
|
||||||
|
return 'Outgoing'
|
||||||
|
return 'Incoming'
|
||||||
|
|
||||||
|
def get_from_number(self):
|
||||||
|
return self._call_from or self.call_info.get('From')
|
||||||
|
|
||||||
|
def get_to_number(self):
|
||||||
|
return self._call_to or self.call_info.get('To')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_call_status(cls, twilio_status):
|
||||||
|
"""Convert Twilio given status into system status.
|
||||||
|
"""
|
||||||
|
twilio_status = twilio_status or ''
|
||||||
|
return ' '.join(twilio_status.split('-')).title()
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'type': self.get_direction(),
|
||||||
|
'status': self.call_status,
|
||||||
|
'id': self.call_sid,
|
||||||
|
'from': self.get_from_number(),
|
||||||
|
'to': self.get_to_number()
|
||||||
|
}
|
||||||
@ -247,6 +247,20 @@ function addDeviceListeners() {
|
|||||||
device.on('connect', (conn) => {
|
device.on('connect', (conn) => {
|
||||||
log.value = 'Successfully established call!'
|
log.value = 'Successfully established call!'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
device.on('disconnect', (conn) => {
|
||||||
|
log.value = 'Call ended disconnect.'
|
||||||
|
update_call_log(conn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_call_log(conn, status = 'Completed') {
|
||||||
|
console.log('connection', conn)
|
||||||
|
if (!conn.parameters.CallSid) return
|
||||||
|
call('crm.twilio.api.update_call_log', {
|
||||||
|
call_sid: conn.parameters.CallSid,
|
||||||
|
status: status,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMute() {
|
function toggleMute() {
|
||||||
@ -303,7 +317,7 @@ function hangUpCall() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleDisconnectedIncomingCall() {
|
function handleDisconnectedIncomingCall() {
|
||||||
log.value = `Call ended.`
|
log.value = `Call ended from handle disconnected Incoming call.`
|
||||||
showCallPopup.value = false
|
showCallPopup.value = false
|
||||||
if (showSmallCallWindow.value == undefined) {
|
if (showSmallCallWindow.value == undefined) {
|
||||||
showSmallCallWindow = false
|
showSmallCallWindow = false
|
||||||
@ -346,8 +360,8 @@ async function makeOutgoingCall(number) {
|
|||||||
calling.value = true
|
calling.value = true
|
||||||
onCall.value = false
|
onCall.value = false
|
||||||
})
|
})
|
||||||
_call.value.on('disconnect', () => {
|
_call.value.on('disconnect', (conn) => {
|
||||||
log.value = `Call ended.`
|
log.value = `Call ended from makeOutgoing call disconnect.`
|
||||||
calling.value = false
|
calling.value = false
|
||||||
onCall.value = false
|
onCall.value = false
|
||||||
showCallPopup.value = false
|
showCallPopup.value = false
|
||||||
@ -356,9 +370,10 @@ async function makeOutgoingCall(number) {
|
|||||||
callStatus.value = ''
|
callStatus.value = ''
|
||||||
muted.value = false
|
muted.value = false
|
||||||
counterUp.value.stop()
|
counterUp.value.stop()
|
||||||
|
update_call_log(conn)
|
||||||
})
|
})
|
||||||
_call.value.on('cancel', () => {
|
_call.value.on('cancel', () => {
|
||||||
log.value = `Call ended.`
|
log.value = `Call ended from makeOutgoing call cancel.`
|
||||||
calling.value = false
|
calling.value = false
|
||||||
onCall.value = false
|
onCall.value = false
|
||||||
showCallPopup.value = false
|
showCallPopup.value = false
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user