feat: Call Popup and Exotel Integration (#17763)
feat: Call Popup and Exotel Integration
This commit is contained in:
commit
8d2c996b50
0
erpnext/communication/__init__.py
Normal file
0
erpnext/communication/__init__.py
Normal file
0
erpnext/communication/doctype/__init__.py
Normal file
0
erpnext/communication/doctype/__init__.py
Normal file
0
erpnext/communication/doctype/call_log/__init__.py
Normal file
0
erpnext/communication/doctype/call_log/__init__.py
Normal file
106
erpnext/communication/doctype/call_log/call_log.json
Normal file
106
erpnext/communication/doctype/call_log/call_log.json
Normal file
@ -0,0 +1,106 @@
|
||||
{
|
||||
"autoname": "field:id",
|
||||
"creation": "2019-06-05 12:07:02.634534",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"id",
|
||||
"from",
|
||||
"to",
|
||||
"column_break_3",
|
||||
"medium",
|
||||
"section_break_5",
|
||||
"status",
|
||||
"duration",
|
||||
"recording_url",
|
||||
"summary"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "id",
|
||||
"fieldtype": "Data",
|
||||
"label": "ID",
|
||||
"read_only": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "From",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to",
|
||||
"fieldtype": "Data",
|
||||
"label": "To",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Ringing\nIn Progress\nCompleted\nMissed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Call Duration in seconds",
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Duration",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "summary",
|
||||
"fieldtype": "Data",
|
||||
"label": "Summary",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "recording_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Recording URL",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "medium",
|
||||
"fieldtype": "Data",
|
||||
"label": "Medium",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"modified": "2019-07-01 09:09:48.516722",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Communication",
|
||||
"name": "Call Log",
|
||||
"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": "ASC",
|
||||
"title_field": "from",
|
||||
"track_changes": 1
|
||||
}
|
19
erpnext/communication/doctype/call_log/call_log.py
Normal file
19
erpnext/communication/doctype/call_log/call_log.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from erpnext.crm.doctype.utils import get_employee_emails_for_popup
|
||||
|
||||
class CallLog(Document):
|
||||
def after_insert(self):
|
||||
employee_emails = get_employee_emails_for_popup(self.medium)
|
||||
for email in employee_emails:
|
||||
frappe.publish_realtime('show_call_popup', self, user=email)
|
||||
|
||||
def on_update(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if doc_before_save and doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']:
|
||||
frappe.publish_realtime('call_{id}_disconnected'.format(id=self.id), self)
|
@ -0,0 +1,81 @@
|
||||
{
|
||||
"autoname": "Prompt",
|
||||
"creation": "2019-06-05 11:48:30.572795",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"communication_medium_type",
|
||||
"catch_all",
|
||||
"column_break_3",
|
||||
"provider",
|
||||
"disabled",
|
||||
"timeslots_section",
|
||||
"timeslots"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "communication_medium_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Communication Medium Type",
|
||||
"options": "Voice\nEmail\nChat",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "If there is no assigned timeslot, then communication will be handled by this group",
|
||||
"fieldname": "catch_all",
|
||||
"fieldtype": "Link",
|
||||
"label": "Catch All",
|
||||
"options": "Employee Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "provider",
|
||||
"fieldtype": "Link",
|
||||
"label": "Provider",
|
||||
"options": "Supplier"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "timeslots_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Timeslots"
|
||||
},
|
||||
{
|
||||
"fieldname": "timeslots",
|
||||
"fieldtype": "Table",
|
||||
"label": "Timeslots",
|
||||
"options": "Communication Medium Timeslot"
|
||||
}
|
||||
],
|
||||
"modified": "2019-06-05 11:49:30.769006",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Communication",
|
||||
"name": "Communication Medium",
|
||||
"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": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class CommunicationMedium(Document):
|
||||
pass
|
@ -0,0 +1,56 @@
|
||||
{
|
||||
"creation": "2019-06-05 11:43:38.897272",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"day_of_week",
|
||||
"from_time",
|
||||
"to_time",
|
||||
"employee_group"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "day_of_week",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Day of Week",
|
||||
"options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "from_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "From Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "to_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "To Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "employee_group",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Employee Group",
|
||||
"options": "Employee Group",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-06-05 12:19:59.994979",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Communication",
|
||||
"name": "Communication Medium Timeslot",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class CommunicationMediumTimeslot(Document):
|
||||
pass
|
100
erpnext/crm/doctype/utils.py
Normal file
100
erpnext/crm/doctype/utils.py
Normal file
@ -0,0 +1,100 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
import json
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_document_with_phone_number(number):
|
||||
# finds contacts and leads
|
||||
if not number: return
|
||||
number = number.lstrip('0')
|
||||
number_filter = {
|
||||
'phone': ['like', '%{}'.format(number)],
|
||||
'mobile_no': ['like', '%{}'.format(number)]
|
||||
}
|
||||
contacts = frappe.get_all('Contact', or_filters=number_filter, limit=1)
|
||||
|
||||
if contacts:
|
||||
return frappe.get_doc('Contact', contacts[0].name)
|
||||
|
||||
leads = frappe.get_all('Lead', or_filters=number_filter, limit=1)
|
||||
|
||||
if leads:
|
||||
return frappe.get_doc('Lead', leads[0].name)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_last_interaction(number, reference_doc):
|
||||
reference_doc = json.loads(reference_doc) if reference_doc else get_document_with_phone_number(number)
|
||||
|
||||
if not reference_doc: return
|
||||
|
||||
reference_doc = frappe._dict(reference_doc)
|
||||
|
||||
last_communication = {}
|
||||
last_issue = {}
|
||||
if reference_doc.doctype == 'Contact':
|
||||
customer_name = ''
|
||||
query_condition = ''
|
||||
for link in reference_doc.links:
|
||||
link = frappe._dict(link)
|
||||
if link.link_doctype == 'Customer':
|
||||
customer_name = link.link_name
|
||||
query_condition += "(`reference_doctype`='{}' AND `reference_name`='{}') OR".format(link.link_doctype, link.link_name)
|
||||
|
||||
if query_condition:
|
||||
query_condition = query_condition[:-2]
|
||||
last_communication = frappe.db.sql("""
|
||||
SELECT `name`, `content`
|
||||
FROM `tabCommunication`
|
||||
WHERE {}
|
||||
ORDER BY `modified`
|
||||
LIMIT 1
|
||||
""".format(query_condition)) # nosec
|
||||
|
||||
if customer_name:
|
||||
last_issue = frappe.get_all('Issue', {
|
||||
'customer': customer_name
|
||||
}, ['name', 'subject', 'customer'], limit=1)
|
||||
|
||||
elif reference_doc.doctype == 'Lead':
|
||||
last_communication = frappe.get_all('Communication', filters={
|
||||
'reference_doctype': reference_doc.doctype,
|
||||
'reference_name': reference_doc.name,
|
||||
'sent_or_received': 'Received'
|
||||
}, fields=['name', 'content'], limit=1)
|
||||
|
||||
return {
|
||||
'last_communication': last_communication[0] if last_communication else None,
|
||||
'last_issue': last_issue[0] if last_issue else None
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_call_summary(docname, summary):
|
||||
call_log = frappe.get_doc('Call Log', docname)
|
||||
summary = _('Call Summary by {0}: {1}').format(
|
||||
frappe.utils.get_fullname(frappe.session.user), summary)
|
||||
if not call_log.summary:
|
||||
call_log.summary = summary
|
||||
else:
|
||||
call_log.summary += '<br>' + summary
|
||||
call_log.save(ignore_permissions=True)
|
||||
|
||||
def get_employee_emails_for_popup(communication_medium):
|
||||
now_time = frappe.utils.nowtime()
|
||||
weekday = frappe.utils.get_weekday()
|
||||
|
||||
available_employee_groups = frappe.get_all("Communication Medium Timeslot", filters={
|
||||
'day_of_week': weekday,
|
||||
'parent': communication_medium,
|
||||
'from_time': ['<=', now_time],
|
||||
'to_time': ['>=', now_time],
|
||||
}, fields=['employee_group'], debug=1)
|
||||
|
||||
available_employee_groups = tuple([emp.employee_group for emp in available_employee_groups])
|
||||
|
||||
employees = frappe.get_all('Employee Group Table', filters={
|
||||
'parent': ['in', available_employee_groups]
|
||||
}, fields=['user_id'])
|
||||
|
||||
employee_emails = set([employee.user_id for employee in employees])
|
||||
|
||||
return employee_emails
|
@ -0,0 +1,61 @@
|
||||
{
|
||||
"creation": "2019-05-21 07:41:53.536536",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enabled",
|
||||
"section_break_2",
|
||||
"account_sid",
|
||||
"api_key",
|
||||
"api_token"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "section_break_2",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_sid",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account SID"
|
||||
},
|
||||
{
|
||||
"fieldname": "api_token",
|
||||
"fieldtype": "Data",
|
||||
"label": "API Token"
|
||||
},
|
||||
{
|
||||
"fieldname": "api_key",
|
||||
"fieldtype": "Data",
|
||||
"label": "API Key"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"modified": "2019-05-22 06:25:18.026997",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "Exotel Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
import requests
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
class ExotelSettings(Document):
|
||||
def validate(self):
|
||||
self.verify_credentials()
|
||||
|
||||
def verify_credentials(self):
|
||||
if self.enabled:
|
||||
response = requests.get('https://api.exotel.com/v1/Accounts/{sid}'
|
||||
.format(sid = self.account_sid), auth=(self.api_key, self.api_token))
|
||||
if response.status_code != 200:
|
||||
frappe.throw(_("Invalid credentials"))
|
101
erpnext/erpnext_integrations/exotel_integration.py
Normal file
101
erpnext/erpnext_integrations/exotel_integration.py
Normal file
@ -0,0 +1,101 @@
|
||||
import frappe
|
||||
import requests
|
||||
|
||||
# api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call
|
||||
# api/method/erpnext.erpnext_integrations.exotel_integration.handle_end_call
|
||||
# api/method/erpnext.erpnext_integrations.exotel_integration.handle_missed_call
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def handle_incoming_call(**kwargs):
|
||||
exotel_settings = get_exotel_settings()
|
||||
if not exotel_settings.enabled: return
|
||||
|
||||
call_payload = kwargs
|
||||
status = call_payload.get('Status')
|
||||
if status == 'free':
|
||||
return
|
||||
|
||||
call_log = get_call_log(call_payload)
|
||||
if not call_log:
|
||||
create_call_log(call_payload)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def handle_end_call(**kwargs):
|
||||
update_call_log(kwargs, 'Completed')
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def handle_missed_call(**kwargs):
|
||||
update_call_log(kwargs, 'Missed')
|
||||
|
||||
def update_call_log(call_payload, status):
|
||||
call_log = get_call_log(call_payload)
|
||||
if call_log:
|
||||
call_log.status = status
|
||||
call_log.duration = call_payload.get('DialCallDuration') or 0
|
||||
call_log.recording_url = call_payload.get('RecordingUrl')
|
||||
call_log.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
return call_log
|
||||
|
||||
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)
|
||||
|
||||
def create_call_log(call_payload):
|
||||
call_log = frappe.new_doc('Call Log')
|
||||
call_log.id = call_payload.get('CallSid')
|
||||
call_log.to = call_payload.get('CallTo')
|
||||
call_log.medium = call_payload.get('To')
|
||||
call_log.status = 'Ringing'
|
||||
setattr(call_log, 'from', call_payload.get('CallFrom'))
|
||||
call_log.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
return call_log
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_call_status(call_id):
|
||||
endpoint = get_exotel_endpoint('Calls/{call_id}.json'.format(call_id=call_id))
|
||||
response = requests.get(endpoint)
|
||||
status = response.json().get('Call', {}).get('Status')
|
||||
return status
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_a_call(from_number, to_number, caller_id):
|
||||
endpoint = get_exotel_endpoint('Calls/connect.json?details=true')
|
||||
response = requests.post(endpoint, data={
|
||||
'From': from_number,
|
||||
'To': to_number,
|
||||
'CallerId': caller_id
|
||||
})
|
||||
|
||||
return response.json()
|
||||
|
||||
def get_exotel_settings():
|
||||
return frappe.get_single('Exotel Settings')
|
||||
|
||||
def whitelist_numbers(numbers, caller_id):
|
||||
endpoint = get_exotel_endpoint('CustomerWhitelist')
|
||||
response = requests.post(endpoint, data={
|
||||
'VirtualNumber': caller_id,
|
||||
'Number': numbers,
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
def get_all_exophones():
|
||||
endpoint = get_exotel_endpoint('IncomingPhoneNumbers')
|
||||
response = requests.post(endpoint)
|
||||
return response
|
||||
|
||||
def get_exotel_endpoint(action):
|
||||
settings = get_exotel_settings()
|
||||
return 'https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/{action}'.format(
|
||||
api_key=settings.api_key,
|
||||
api_token=settings.api_token,
|
||||
sid=settings.account_sid,
|
||||
action=action
|
||||
)
|
@ -169,6 +169,11 @@ default_roles = [
|
||||
{'role': 'Student', 'doctype':'Student', 'email_field': 'student_email_id'},
|
||||
]
|
||||
|
||||
sounds = [
|
||||
{"name": "incoming-call", "src": "/assets/erpnext/sounds/incoming-call.mp3", "volume": 0.2},
|
||||
{"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2},
|
||||
]
|
||||
|
||||
has_website_permission = {
|
||||
"Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission",
|
||||
"Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission",
|
||||
|
@ -1,109 +1,45 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-11-19 12:39:46.153061",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"creation": "2018-11-19 12:39:46.153061",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"employee",
|
||||
"employee_name",
|
||||
"user_id"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Employee",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Employee",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Employee",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "employee.first_name",
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Employee Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fetch_from": "employee.first_name",
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Employee Name"
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.user_id",
|
||||
"fieldname": "user_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "ERPNext User ID",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-11-19 13:18:17.281656",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Group Table",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-06-06 10:41:20.313756",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Group Table",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -22,4 +22,5 @@ ERPNext Integrations
|
||||
Non Profit
|
||||
Hotels
|
||||
Hub Node
|
||||
Quality Management
|
||||
Quality Management
|
||||
Communication
|
@ -1,7 +1,8 @@
|
||||
{
|
||||
"css/erpnext.css": [
|
||||
"public/less/erpnext.less",
|
||||
"public/less/hub.less"
|
||||
"public/less/hub.less",
|
||||
"public/less/call_popup.less"
|
||||
],
|
||||
"css/marketplace.css": [
|
||||
"public/less/hub.less"
|
||||
@ -49,6 +50,7 @@
|
||||
"public/js/education/student_button.html",
|
||||
"public/js/education/assessment_result_tool.html",
|
||||
"public/js/hub/hub_factory.js",
|
||||
"public/js/call_popup/call_popup.js",
|
||||
"public/js/utils/dimension_tree_filter.js"
|
||||
],
|
||||
"js/item-dashboard.min.js": [
|
||||
|
212
erpnext/public/js/call_popup/call_popup.js
Normal file
212
erpnext/public/js/call_popup/call_popup.js
Normal file
@ -0,0 +1,212 @@
|
||||
class CallPopup {
|
||||
constructor(call_log) {
|
||||
this.caller_number = call_log.from;
|
||||
this.call_log = call_log;
|
||||
this.setup_listener();
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
'static': true,
|
||||
'minimizable': true,
|
||||
'fields': [{
|
||||
'fieldname': 'caller_info',
|
||||
'fieldtype': 'HTML'
|
||||
}, {
|
||||
'fielname': 'last_interaction',
|
||||
'fieldtype': 'Section Break',
|
||||
'label': __('Activity'),
|
||||
}, {
|
||||
'fieldtype': 'Small Text',
|
||||
'label': __('Last Communication'),
|
||||
'fieldname': 'last_communication',
|
||||
'read_only': true,
|
||||
'default': `<i class="text-muted">${__('No communication found.')}<i>`
|
||||
}, {
|
||||
'fieldtype': 'Small Text',
|
||||
'label': __('Last Issue'),
|
||||
'fieldname': 'last_issue',
|
||||
'read_only': true,
|
||||
'default': `<i class="text-muted">${__('No issue raised by the customer.')}<i>`
|
||||
}, {
|
||||
'fieldtype': 'Column Break',
|
||||
}, {
|
||||
'fieldtype': 'Small Text',
|
||||
'label': __('Call Summary'),
|
||||
'fieldname': 'call_summary',
|
||||
}, {
|
||||
'fieldtype': 'Button',
|
||||
'label': __('Save'),
|
||||
'click': () => {
|
||||
const call_summary = this.dialog.get_value('call_summary');
|
||||
if (!call_summary) return;
|
||||
frappe.xcall('erpnext.crm.doctype.utils.add_call_summary', {
|
||||
'docname': this.call_log.id,
|
||||
'summary': call_summary,
|
||||
}).then(() => {
|
||||
this.close_modal();
|
||||
frappe.show_alert({
|
||||
message: `${__('Call Summary Saved')}<br><a class="text-small text-muted" href="#Form/Call Log/${this.call_log.name}">${__('View call log')}</a>`,
|
||||
indicator: 'green'
|
||||
});
|
||||
});
|
||||
}
|
||||
}],
|
||||
});
|
||||
this.set_call_status();
|
||||
this.make_caller_info_section();
|
||||
this.dialog.get_close_btn().show();
|
||||
this.dialog.$body.addClass('call-popup');
|
||||
this.dialog.set_secondary_action(this.close_modal.bind(this));
|
||||
frappe.utils.play_sound('incoming-call');
|
||||
this.dialog.show();
|
||||
}
|
||||
|
||||
make_caller_info_section() {
|
||||
const wrapper = this.dialog.get_field('caller_info').$wrapper;
|
||||
wrapper.append(`<div class="text-muted"> ${__("Loading...")} </div>`);
|
||||
frappe.xcall('erpnext.crm.doctype.utils.get_document_with_phone_number', {
|
||||
'number': this.caller_number
|
||||
}).then(contact_doc => {
|
||||
wrapper.empty();
|
||||
const contact = this.contact = contact_doc;
|
||||
if (!contact) {
|
||||
this.setup_unknown_caller(wrapper);
|
||||
} else {
|
||||
this.setup_known_caller(wrapper);
|
||||
this.set_call_status();
|
||||
this.make_last_interaction_section();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setup_unknown_caller(wrapper) {
|
||||
wrapper.append(`
|
||||
<div class="caller-info">
|
||||
<b>${__('Unknown Number')}:</b> ${this.caller_number}
|
||||
<button
|
||||
class="margin-left btn btn-new btn-default btn-xs"
|
||||
data-doctype="Contact"
|
||||
title=${__("Make New Contact")}>
|
||||
<i class="octicon octicon-plus text-medium"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).find('button').click(
|
||||
() => frappe.set_route(`Form/Contact/New Contact?phone=${this.caller_number}`)
|
||||
);
|
||||
}
|
||||
|
||||
setup_known_caller(wrapper) {
|
||||
const contact = this.contact;
|
||||
const contact_name = frappe.utils.get_form_link(contact.doctype, contact.name, true, this.get_caller_name());
|
||||
const links = contact.links ? contact.links : [];
|
||||
|
||||
let contact_links = '';
|
||||
|
||||
links.forEach(link => {
|
||||
contact_links += `<div>${link.link_doctype}: ${frappe.utils.get_form_link(link.link_doctype, link.link_name, true)}</div>`;
|
||||
});
|
||||
wrapper.append(`
|
||||
<div class="caller-info flex">
|
||||
${frappe.avatar(null, 'avatar-xl', contact.name, contact.image)}
|
||||
<div>
|
||||
<h5>${contact_name}</h5>
|
||||
<div>${contact.mobile_no || ''}</div>
|
||||
<div>${contact.phone_no || ''}</div>
|
||||
${contact_links}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
set_indicator(color, blink=false) {
|
||||
let classes = `indicator ${color} ${blink ? 'blink': ''}`;
|
||||
this.dialog.header.find('.indicator').attr('class', classes);
|
||||
}
|
||||
|
||||
set_call_status(call_status) {
|
||||
let title = '';
|
||||
call_status = call_status || this.call_log.status;
|
||||
if (['Ringing'].includes(call_status) || !call_status) {
|
||||
title = __('Incoming call from {0}', [this.get_caller_name()]);
|
||||
this.set_indicator('blue', true);
|
||||
} else if (call_status === 'In Progress') {
|
||||
title = __('Call Connected');
|
||||
this.set_indicator('yellow');
|
||||
} else if (call_status === 'Missed') {
|
||||
this.set_indicator('red');
|
||||
title = __('Call Missed');
|
||||
} else if (['Completed', 'Disconnected'].includes(call_status)) {
|
||||
this.set_indicator('red');
|
||||
title = __('Call Disconnected');
|
||||
} else {
|
||||
this.set_indicator('blue');
|
||||
title = call_status;
|
||||
}
|
||||
this.dialog.set_title(title);
|
||||
}
|
||||
|
||||
update_call_log(call_log) {
|
||||
this.call_log = call_log;
|
||||
this.set_call_status();
|
||||
}
|
||||
|
||||
close_modal() {
|
||||
this.dialog.hide();
|
||||
delete erpnext.call_popup;
|
||||
}
|
||||
|
||||
call_disconnected(call_log) {
|
||||
frappe.utils.play_sound('call-disconnect');
|
||||
this.update_call_log(call_log);
|
||||
setTimeout(() => {
|
||||
if (!this.dialog.get_value('call_summary')) {
|
||||
this.close_modal();
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
make_last_interaction_section() {
|
||||
frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', {
|
||||
'number': this.caller_number,
|
||||
'reference_doc': this.contact
|
||||
}).then(data => {
|
||||
const comm_field = this.dialog.get_field('last_communication');
|
||||
if (data.last_communication) {
|
||||
const comm = data.last_communication;
|
||||
comm_field.set_value(comm.content);
|
||||
}
|
||||
|
||||
if (data.last_issue) {
|
||||
const issue = data.last_issue;
|
||||
const issue_field = this.dialog.get_field("last_issue");
|
||||
issue_field.set_value(issue.subject);
|
||||
issue_field.$wrapper.append(`<a class="text-medium" href="#List/Issue?customer=${issue.customer}">
|
||||
${__('View all issues from {0}', [issue.customer])}
|
||||
</a>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
get_caller_name() {
|
||||
return this.contact ? this.contact.lead_name || this.contact.name || '' : this.caller_number;
|
||||
}
|
||||
setup_listener() {
|
||||
frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => {
|
||||
this.call_disconnected(call_log);
|
||||
// Remove call disconnect listener after the call is disconnected
|
||||
frappe.realtime.off(`call_${this.call_log.id}_disconnected`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(document).on('app_ready', function () {
|
||||
frappe.realtime.on('show_call_popup', call_log => {
|
||||
if (!erpnext.call_popup) {
|
||||
erpnext.call_popup = new CallPopup(call_log);
|
||||
} else {
|
||||
erpnext.call_popup.update_call_log(call_log);
|
||||
erpnext.call_popup.dialog.show();
|
||||
}
|
||||
});
|
||||
});
|
9
erpnext/public/less/call_popup.less
Normal file
9
erpnext/public/less/call_popup.less
Normal file
@ -0,0 +1,9 @@
|
||||
.call-popup {
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.for-description {
|
||||
max-height: 250px;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
BIN
erpnext/public/sounds/call-disconnect.mp3
Normal file
BIN
erpnext/public/sounds/call-disconnect.mp3
Normal file
Binary file not shown.
BIN
erpnext/public/sounds/incoming-call.mp3
Normal file
BIN
erpnext/public/sounds/incoming-call.mp3
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user