Merge branch 'develop' into call-summary-dialog
diff --git a/erpnext/communication/__init__.py b/erpnext/communication/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/communication/__init__.py
diff --git a/erpnext/communication/doctype/__init__.py b/erpnext/communication/doctype/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/communication/doctype/__init__.py
diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/communication/doctype/call_log/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/communication/doctype/call_log/__init__.py
diff --git a/erpnext/communication/doctype/call_log/call_log.js b/erpnext/communication/doctype/call_log/call_log.js
new file mode 100644
index 0000000..0018516
--- /dev/null
+++ b/erpnext/communication/doctype/call_log/call_log.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Call Log', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json
new file mode 100644
index 0000000..110030d
--- /dev/null
+++ b/erpnext/communication/doctype/call_log/call_log.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py
new file mode 100644
index 0000000..66f1064
--- /dev/null
+++ b/erpnext/communication/doctype/call_log/call_log.py
@@ -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)
diff --git a/erpnext/communication/doctype/communication_medium/__init__.py b/erpnext/communication/doctype/communication_medium/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium/__init__.py
diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.js b/erpnext/communication/doctype/communication_medium/communication_medium.js
new file mode 100644
index 0000000..e37cd5b
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium/communication_medium.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Communication Medium', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.json b/erpnext/communication/doctype/communication_medium/communication_medium.json
new file mode 100644
index 0000000..f009b38
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium/communication_medium.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.py b/erpnext/communication/doctype/communication_medium/communication_medium.py
new file mode 100644
index 0000000..f233da0
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium/communication_medium.py
@@ -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
diff --git a/erpnext/communication/doctype/communication_medium/test_communication_medium.py b/erpnext/communication/doctype/communication_medium/test_communication_medium.py
new file mode 100644
index 0000000..fc5754f
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium/test_communication_medium.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestCommunicationMedium(unittest.TestCase):
+ pass
diff --git a/erpnext/communication/doctype/communication_medium_timeslot/__init__.py b/erpnext/communication/doctype/communication_medium_timeslot/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium_timeslot/__init__.py
diff --git a/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.json b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.json
new file mode 100644
index 0000000..b278ca0
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.py b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.py
new file mode 100644
index 0000000..d68d2d6
--- /dev/null
+++ b/erpnext/communication/doctype/communication_medium_timeslot/communication_medium_timeslot.py
@@ -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
diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py
new file mode 100644
index 0000000..bd8b678
--- /dev/null
+++ b/erpnext/crm/doctype/utils.py
@@ -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
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py b/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
new file mode 100644
index 0000000..72f47b5
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py
new file mode 100644
index 0000000..77de84c
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py
@@ -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"))
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py
new file mode 100644
index 0000000..c04cedc
--- /dev/null
+++ b/erpnext/erpnext_integrations/exotel_integration.py
@@ -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
+ )
\ No newline at end of file
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 6ce75bb..756f6ba 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -169,6 +169,11 @@
{'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",
diff --git a/erpnext/hr/doctype/employee_group_table/employee_group_table.json b/erpnext/hr/doctype/employee_group_table/employee_group_table.json
index f2e7770..4e0045c 100644
--- a/erpnext/hr/doctype/employee_group_table/employee_group_table.json
+++ b/erpnext/hr/doctype/employee_group_table/employee_group_table.json
@@ -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
}
\ No newline at end of file
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index 9ef8937..316d6de 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -22,4 +22,5 @@
Non Profit
Hotels
Hub Node
-Quality Management
\ No newline at end of file
+Quality Management
+Communication
\ No newline at end of file
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index be7189b..f6137d5 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -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": [
diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js
new file mode 100644
index 0000000..91dfe80
--- /dev/null
+++ b/erpnext/public/js/call_popup/call_popup.js
@@ -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();
+ }
+ });
+});
diff --git a/erpnext/public/less/call_popup.less b/erpnext/public/less/call_popup.less
new file mode 100644
index 0000000..32e85ce
--- /dev/null
+++ b/erpnext/public/less/call_popup.less
@@ -0,0 +1,9 @@
+.call-popup {
+ a:hover {
+ text-decoration: underline;
+ }
+ .for-description {
+ max-height: 250px;
+ overflow: scroll;
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/sounds/call-disconnect.mp3 b/erpnext/public/sounds/call-disconnect.mp3
new file mode 100644
index 0000000..1202273
--- /dev/null
+++ b/erpnext/public/sounds/call-disconnect.mp3
Binary files differ
diff --git a/erpnext/public/sounds/incoming-call.mp3 b/erpnext/public/sounds/incoming-call.mp3
new file mode 100644
index 0000000..60431e3
--- /dev/null
+++ b/erpnext/public/sounds/incoming-call.mp3
Binary files differ