Merge branch 'develop' of https://github.com/frappe/erpnext into move-exotel-to-separate-app
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py b/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py
+++ /dev/null
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
deleted file mode 100644
index 72f47b5..0000000
--- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
+++ /dev/null
@@ -1,61 +0,0 @@
-{
- "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
deleted file mode 100644
index 4879cb5..0000000
--- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-import frappe
-import requests
-from frappe import _
-from frappe.model.document import Document
-
-
-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"))
diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py
deleted file mode 100644
index fd9f74e..0000000
--- a/erpnext/erpnext_integrations/exotel_integration.py
+++ /dev/null
@@ -1,131 +0,0 @@
-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):
- try:
- 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)
- else:
- update_call_log(call_payload, call_log=call_log)
- except Exception as e:
- frappe.db.rollback()
- exotel_settings.log_error("Error in Exotel incoming call")
-
-
-@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):
- status = ""
- call_type = kwargs.get("CallType")
- dial_call_status = kwargs.get("DialCallStatus")
-
- if call_type == "incomplete" and dial_call_status == "no-answer":
- status = "No Answer"
- elif call_type == "client-hangup" and dial_call_status == "canceled":
- status = "Canceled"
- elif call_type == "incomplete" and dial_call_status == "failed":
- status = "Failed"
-
- update_call_log(kwargs, status)
-
-
-def update_call_log(call_payload, status="Ringing", call_log=None):
- call_log = call_log or get_call_log(call_payload)
-
- # for a new sid, call_log and get_call_log will be empty so create a new log
- if not call_log:
- call_log = create_call_log(call_payload)
- if call_log:
- call_log.status = status
- call_log.to = call_payload.get("DialWhomNumber")
- 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_id = call_payload.get("CallSid")
- if frappe.db.exists("Call Log", call_log_id):
- return frappe.get_doc("Call Log", call_log_id)
-
-
-def create_call_log(call_payload):
- call_log = frappe.new_doc("Call Log")
- call_log.id = call_payload.get("CallSid")
- call_log.to = call_payload.get("DialWhomNumber")
- 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
- )
diff --git a/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py b/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py
new file mode 100644
index 0000000..6e84ba9
--- /dev/null
+++ b/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py
@@ -0,0 +1,10 @@
+import click
+
+
+def execute():
+
+ click.secho(
+ "Exotel integration is moved to a separate app and will be removed from ERPNext in version-14.\n"
+ "Please install the app to continue using the integration: https://github.com/frappe/exotel_integration",
+ fg="yellow",
+ )
diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py
index 7725e71..1d6839c 100644
--- a/erpnext/telephony/doctype/call_log/call_log.py
+++ b/erpnext/telephony/doctype/call_log/call_log.py
@@ -24,12 +24,10 @@
lead_number = self.get("from") if self.is_incoming_call() else self.get("to")
lead_number = strip_number(lead_number)
- contact = get_contact_with_phone_number(strip_number(lead_number))
- if contact:
+ if contact := get_contact_with_phone_number(strip_number(lead_number)):
self.add_link(link_type="Contact", link_name=contact)
- lead = get_lead_with_phone_number(lead_number)
- if lead:
+ if lead := get_lead_with_phone_number(lead_number):
self.add_link(link_type="Lead", link_name=lead)
# Add Employee Name
@@ -70,28 +68,30 @@
self.append("links", {"link_doctype": link_type, "link_name": link_name})
def trigger_call_popup(self):
- if self.is_incoming_call():
- scheduled_employees = get_scheduled_employees_for_popup(self.medium)
- employees = get_employees_with_number(self.to)
- employee_emails = [employee.get("user_id") for employee in employees]
+ if not self.is_incoming_call():
+ return
- # check if employees with matched number are scheduled to receive popup
- emails = set(scheduled_employees).intersection(employee_emails)
+ scheduled_employees = get_scheduled_employees_for_popup(self.medium)
+ employees = get_employees_with_number(self.to)
+ employee_emails = [employee.get("user_id") for employee in employees]
- if frappe.conf.developer_mode:
- self.add_comment(
- text=f"""
+ # check if employees with matched number are scheduled to receive popup
+ emails = set(scheduled_employees).intersection(employee_emails)
+
+ if frappe.conf.developer_mode:
+ self.add_comment(
+ text=f"""
Scheduled Employees: {scheduled_employees}
Matching Employee: {employee_emails}
Show Popup To: {emails}
"""
- )
+ )
- if employee_emails and not emails:
- self.add_comment(text=_("No employee was scheduled for call popup"))
+ if employee_emails and not emails:
+ self.add_comment(text=_("No employee was scheduled for call popup"))
- for email in emails:
- frappe.publish_realtime("show_call_popup", self, user=email)
+ for email in emails:
+ frappe.publish_realtime("show_call_popup", self, user=email)
def update_received_by(self):
if employees := get_employees_with_number(self.get("to")):
@@ -154,8 +154,8 @@
ELSE 0
END
)=0
- """,
- dict(phone_number="%{}".format(number), docname=doc.name, doctype=doc.doctype),
+ """,
+ dict(phone_number=f"%{number}", docname=doc.name, doctype=doc.doctype),
)
for log in logs:
@@ -175,7 +175,7 @@
filters={"parenttype": "Call Log", "link_doctype": doctype, "link_name": docname},
)
- logs = set([log.parent for log in logs])
+ logs = {log.parent for log in logs}
logs = frappe.get_all("Call Log", fields=["*"], filters={"name": ["in", logs]})
diff --git a/erpnext/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py
deleted file mode 100644
index 3ad2575..0000000
--- a/erpnext/tests/exotel_test_data.py
+++ /dev/null
@@ -1,122 +0,0 @@
-import frappe
-
-call_initiation_data = frappe._dict(
- {
- "CallSid": "23c162077629863c1a2d7f29263a162m",
- "CallFrom": "09999999991",
- "CallTo": "09999999980",
- "Direction": "incoming",
- "Created": "Wed, 23 Feb 2022 12:31:59",
- "From": "09999999991",
- "To": "09999999988",
- "CurrentTime": "2022-02-23 12:32:02",
- "DialWhomNumber": "09999999999",
- "Status": "busy",
- "EventType": "Dial",
- "AgentEmail": "test_employee_exotel@company.com",
- }
-)
-
-call_end_data = frappe._dict(
- {
- "CallSid": "23c162077629863c1a2d7f29263a162m",
- "CallFrom": "09999999991",
- "CallTo": "09999999980",
- "Direction": "incoming",
- "ForwardedFrom": "null",
- "Created": "Wed, 23 Feb 2022 12:31:59",
- "DialCallDuration": "17",
- "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3",
- "StartTime": "2022-02-23 12:31:58",
- "EndTime": "1970-01-01 05:30:00",
- "DialCallStatus": "completed",
- "CallType": "completed",
- "DialWhomNumber": "09999999999",
- "ProcessStatus": "null",
- "flow_id": "228040",
- "tenant_id": "67291",
- "From": "09999999991",
- "To": "09999999988",
- "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25",
- "CurrentTime": "2022-02-23 12:32:25",
- "OutgoingPhoneNumber": "09999999988",
- "Legs": [
- {
- "Number": "09999999999",
- "Type": "single",
- "OnCallDuration": "10",
- "CallerId": "09999999980",
- "CauseCode": "NORMAL_CLEARING",
- "Cause": "16",
- }
- ],
- }
-)
-
-call_disconnected_data = frappe._dict(
- {
- "CallSid": "d96421addce69e24bdc7ce5880d1162l",
- "CallFrom": "09999999991",
- "CallTo": "09999999980",
- "Direction": "incoming",
- "ForwardedFrom": "null",
- "Created": "Mon, 21 Feb 2022 15:58:12",
- "DialCallDuration": "0",
- "StartTime": "2022-02-21 15:58:12",
- "EndTime": "1970-01-01 05:30:00",
- "DialCallStatus": "canceled",
- "CallType": "client-hangup",
- "DialWhomNumber": "09999999999",
- "ProcessStatus": "null",
- "flow_id": "228040",
- "tenant_id": "67291",
- "From": "09999999991",
- "To": "09999999988",
- "CurrentTime": "2022-02-21 15:58:47",
- "OutgoingPhoneNumber": "09999999988",
- "Legs": [
- {
- "Number": "09999999999",
- "Type": "single",
- "OnCallDuration": "0",
- "CallerId": "09999999980",
- "CauseCode": "RING_TIMEOUT",
- "Cause": "1003",
- }
- ],
- }
-)
-
-call_not_answered_data = frappe._dict(
- {
- "CallSid": "fdb67a2b4b2d057b610a52ef43f81622",
- "CallFrom": "09999999991",
- "CallTo": "09999999980",
- "Direction": "incoming",
- "ForwardedFrom": "null",
- "Created": "Mon, 21 Feb 2022 15:47:02",
- "DialCallDuration": "0",
- "StartTime": "2022-02-21 15:47:02",
- "EndTime": "1970-01-01 05:30:00",
- "DialCallStatus": "no-answer",
- "CallType": "incomplete",
- "DialWhomNumber": "09999999999",
- "ProcessStatus": "null",
- "flow_id": "228040",
- "tenant_id": "67291",
- "From": "09999999991",
- "To": "09999999988",
- "CurrentTime": "2022-02-21 15:47:40",
- "OutgoingPhoneNumber": "09999999988",
- "Legs": [
- {
- "Number": "09999999999",
- "Type": "single",
- "OnCallDuration": "0",
- "CallerId": "09999999980",
- "CauseCode": "RING_TIMEOUT",
- "Cause": "1003",
- }
- ],
- }
-)
diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py
deleted file mode 100644
index f5cca72..0000000
--- a/erpnext/tests/test_exotel.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import frappe
-from frappe.contacts.doctype.contact.test_contact import create_contact
-from frappe.tests.test_api import FrappeAPITestCase
-
-from erpnext.setup.doctype.employee.test_employee import make_employee
-
-
-class TestExotel(FrappeAPITestCase):
- @classmethod
- def setUpClass(cls):
- cls.CURRENT_DB_CONNECTION = frappe.db
- cls.test_employee_name = make_employee(
- user="test_employee_exotel@company.com", cell_number="9999999999"
- )
- frappe.db.set_value("Exotel Settings", "Exotel Settings", "enabled", 1)
- phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}]
- create_contact(name="Test Contact", salutation="Mr", phones=phones)
- frappe.db.commit()
-
- def test_for_successful_call(self):
- from .exotel_test_data import call_end_data, call_initiation_data
-
- api_method = "handle_incoming_call"
- end_call_api_method = "handle_end_call"
-
- self.emulate_api_call_from_exotel(api_method, call_initiation_data)
- self.emulate_api_call_from_exotel(end_call_api_method, call_end_data)
- call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid)
-
- self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom)
- self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber)
- self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
- self.assertEqual(call_log.get("status"), "Completed")
-
- def test_for_disconnected_call(self):
- from .exotel_test_data import call_disconnected_data
-
- api_method = "handle_missed_call"
- self.emulate_api_call_from_exotel(api_method, call_disconnected_data)
- call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid)
- self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom)
- self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber)
- self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
- self.assertEqual(call_log.get("status"), "Canceled")
-
- def test_for_call_not_answered(self):
- from .exotel_test_data import call_not_answered_data
-
- api_method = "handle_missed_call"
- self.emulate_api_call_from_exotel(api_method, call_not_answered_data)
- call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid)
- self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom)
- self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber)
- self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
- self.assertEqual(call_log.get("status"), "No Answer")
-
- def emulate_api_call_from_exotel(self, api_method, data):
- self.post(
- f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}",
- data=frappe.as_json(data),
- content_type="application/json",
- )
- # restart db connection to get latest data
- frappe.connect()
-
- @classmethod
- def tearDownClass(cls):
- frappe.db = cls.CURRENT_DB_CONNECTION