diff --git a/erpnext/communication/__init__.py b/erpnext/communication/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/communication/doctype/__init__.py b/erpnext/communication/doctype/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/communication/doctype/call_log/__init__.py b/erpnext/communication/doctype/call_log/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 00000000000..110030d3de6 --- /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 00000000000..66f1064e584 --- /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 00000000000..e69de29bb2d 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 00000000000..f009b388771 --- /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 00000000000..f233da07d5d --- /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_timeslot/__init__.py b/erpnext/communication/doctype/communication_medium_timeslot/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 00000000000..b278ca08f5c --- /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 00000000000..d68d2d67a76 --- /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 00000000000..bd8b678d3b7 --- /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 += '
' + 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 00000000000..e69de29bb2d 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 00000000000..72f47b53ec2 --- /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 00000000000..77de84ce5c9 --- /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 00000000000..c04cedce31f --- /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 6ce75bbac36..756f6ba8ca2 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -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", 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 f2e77700b83..4e0045cdeb8 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 9ef8937ee57..316d6de20e3 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -22,4 +22,5 @@ ERPNext Integrations 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 be7189bb96b..f6137d58717 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 00000000000..91dfe809a43 --- /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': `${__('No communication found.')}` + }, { + 'fieldtype': 'Small Text', + 'label': __('Last Issue'), + 'fieldname': 'last_issue', + 'read_only': true, + 'default': `${__('No issue raised by the customer.')}` + }, { + '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')}
${__('View call log')}`, + 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(`
${__("Loading...")}
`); + 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(` +
+ ${__('Unknown Number')}: ${this.caller_number} + +
+ `).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 += `
${link.link_doctype}: ${frappe.utils.get_form_link(link.link_doctype, link.link_name, true)}
`; + }); + wrapper.append(` +
+ ${frappe.avatar(null, 'avatar-xl', contact.name, contact.image)} +
+
${contact_name}
+
${contact.mobile_no || ''}
+
${contact.phone_no || ''}
+ ${contact_links} +
+
+ `); + } + + 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(` + ${__('View all issues from {0}', [issue.customer])} + `); + } + }); + } + 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 00000000000..32e85ce16d9 --- /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 00000000000..1202273dfe5 Binary files /dev/null and b/erpnext/public/sounds/call-disconnect.mp3 differ diff --git a/erpnext/public/sounds/incoming-call.mp3 b/erpnext/public/sounds/incoming-call.mp3 new file mode 100644 index 00000000000..60431e30884 Binary files /dev/null and b/erpnext/public/sounds/incoming-call.mp3 differ