From 53e4fee4db69b5a1d7ee90bd37d807737fc53607 Mon Sep 17 00:00:00 2001
From: Suraj Shetty <surajshetty3416@gmail.com>
Date: Sun, 8 May 2022 16:04:14 +0530
Subject: [PATCH 1/6] refactor: Remove exotel

Move it to separate app
---
 .../doctype/exotel_settings/__init__.py       |   0
 .../exotel_settings/exotel_settings.json      |  61 --------
 .../exotel_settings/exotel_settings.py        |  22 ---
 .../exotel_integration.py                     | 133 ------------------
 erpnext/tests/exotel_test_data.py             | 122 ----------------
 erpnext/tests/test_exotel.py                  |  69 ---------
 6 files changed, 407 deletions(-)
 delete mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/__init__.py
 delete mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
 delete mode 100644 erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.py
 delete mode 100644 erpnext/erpnext_integrations/exotel_integration.py
 delete mode 100644 erpnext/tests/exotel_test_data.py
 delete mode 100644 erpnext/tests/test_exotel.py

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 e69de29bb2..0000000000
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 72f47b53ec..0000000000
--- 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 4879cb5623..0000000000
--- 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 522de9ead8..0000000000
--- a/erpnext/erpnext_integrations/exotel_integration.py
+++ /dev/null
@@ -1,133 +0,0 @@
-import frappe
-import requests
-from frappe import _
-
-# 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()
-		frappe.log_error(title=_("Error in Exotel incoming call"))
-		frappe.db.commit()
-
-
-@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/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py
deleted file mode 100644
index 3ad2575c23..0000000000
--- 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 76bbb3e05a..0000000000
--- a/erpnext/tests/test_exotel.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import frappe
-from frappe.contacts.doctype.contact.test_contact import create_contact
-from frappe.tests.test_api import FrappeAPITestCase
-
-from erpnext.hr.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",
-			as_tuple=True,
-		)
-		# restart db connection to get latest data
-		frappe.connect()
-
-	@classmethod
-	def tearDownClass(cls):
-		frappe.db = cls.CURRENT_DB_CONNECTION

From e0bc437ddbb1bd490f9483d797d6221f709eafbd Mon Sep 17 00:00:00 2001
From: Suraj Shetty <surajshetty3416@gmail.com>
Date: Sun, 8 May 2022 16:05:04 +0530
Subject: [PATCH 2/6] refactor: Simplify call log code

---
 .../telephony/doctype/call_log/call_log.py    | 42 +++++++++----------
 1 file changed, 21 insertions(+), 21 deletions(-)

diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py
index 7725e71f19..1d6839c1e6 100644
--- a/erpnext/telephony/doctype/call_log/call_log.py
+++ b/erpnext/telephony/doctype/call_log/call_log.py
@@ -24,12 +24,10 @@ class CallLog(Document):
 		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 @@ class CallLog(Document):
 		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 @@ def link_existing_conversations(doc, state):
 						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 @@ def get_linked_call_logs(doctype, docname):
 		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]})
 

From cf9c065cf88ba706c036b4f199ae9c39b5bea836 Mon Sep 17 00:00:00 2001
From: Suraj Shetty <surajshetty3416@gmail.com>
Date: Fri, 22 Jul 2022 12:22:57 +0530
Subject: [PATCH 3/6] refactor: Add exotel deprecation warning

---
 .../v13_0/exotel_integration_deprecation_warning.py    | 10 ++++++++++
 1 file changed, 10 insertions(+)
 create mode 100644 erpnext/patches/v13_0/exotel_integration_deprecation_warning.py

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 0000000000..6e84ba9176
--- /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",
+	)

From 6349f29aedc2eec817786dffbe245db54eff0731 Mon Sep 17 00:00:00 2001
From: Suraj Shetty <surajshetty3416@gmail.com>
Date: Sat, 30 Jul 2022 14:26:37 +0530
Subject: [PATCH 4/6] fix: Remove option from Communication Medium

---
 .../communication_medium/communication_medium.json    |  2 +-
 .../erpnext_integrations/erpnext_integrations.json    | 11 -----------
 erpnext/www/lms/__init__.py                           |  0
 3 files changed, 1 insertion(+), 12 deletions(-)
 create mode 100644 erpnext/www/lms/__init__.py

diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.json b/erpnext/communication/doctype/communication_medium/communication_medium.json
index 1e1fe3bf49..b6b9c7e434 100644
--- a/erpnext/communication/doctype/communication_medium/communication_medium.json
+++ b/erpnext/communication/doctype/communication_medium/communication_medium.json
@@ -61,7 +61,7 @@
    "fieldname": "communication_channel",
    "fieldtype": "Select",
    "label": "Communication Channel",
-   "options": "\nExotel"
+   "options": ""
   }
  ],
  "links": [],
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
index 1f2619b9a6..c5faa2d59e 100644
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
@@ -77,17 +77,6 @@
    "link_type": "DocType",
    "onboard": 0,
    "type": "Link"
-  },
-  {
-   "dependencies": "",
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Exotel Settings",
-   "link_count": 0,
-   "link_to": "Exotel Settings",
-   "link_type": "DocType",
-   "onboard": 0,
-   "type": "Link"
   }
  ],
  "modified": "2022-01-13 17:35:35.508718",
diff --git a/erpnext/www/lms/__init__.py b/erpnext/www/lms/__init__.py
new file mode 100644
index 0000000000..e69de29bb2

From d95559a53cb5084cfe1a3291cbafc700d18445c6 Mon Sep 17 00:00:00 2001
From: Ankush Menat <ankush@frappe.io>
Date: Fri, 14 Jul 2023 17:32:39 +0530
Subject: [PATCH 5/6] fix: patch for exotel

---
 erpnext/patches.txt                           |  3 +-
 .../exotel_integration_deprecation_warning.py | 10 -----
 .../v15_0/remove_exotel_integration.py        | 37 +++++++++++++++++++
 3 files changed, 39 insertions(+), 11 deletions(-)
 delete mode 100644 erpnext/patches/v13_0/exotel_integration_deprecation_warning.py
 create mode 100644 erpnext/patches/v15_0/remove_exotel_integration.py

diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index f9d9ebbdb3..6fa4b5a85a 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -333,4 +333,5 @@ execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missin
 erpnext.patches.v14_0.cleanup_workspaces
 erpnext.patches.v15_0.remove_loan_management_module #2023-07-03
 erpnext.patches.v14_0.set_report_in_process_SOA
-erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
\ No newline at end of file
+erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
+erpnext.patches.v15_0.remove_exotel_integration
diff --git a/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py b/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py
deleted file mode 100644
index 6e84ba9176..0000000000
--- a/erpnext/patches/v13_0/exotel_integration_deprecation_warning.py
+++ /dev/null
@@ -1,10 +0,0 @@
-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/patches/v15_0/remove_exotel_integration.py b/erpnext/patches/v15_0/remove_exotel_integration.py
new file mode 100644
index 0000000000..a37773f337
--- /dev/null
+++ b/erpnext/patches/v15_0/remove_exotel_integration.py
@@ -0,0 +1,37 @@
+from contextlib import suppress
+
+import click
+import frappe
+from frappe import _
+from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
+from frappe.utils.user import get_system_managers
+
+SETTINGS_DOCTYPE = "Exotel Settings"
+
+
+def execute():
+	if "exotel_integration" in frappe.get_installed_apps():
+		return
+
+	with suppress(Exception):
+		exotel = frappe.get_doc(SETTINGS_DOCTYPE)
+		if exotel.enabled:
+			notify_existing_users()
+
+		frappe.delete_doc("DocType", SETTINGS_DOCTYPE)
+
+
+def notify_existing_users():
+	click.secho(
+		"Exotel integration is moved to a separate app and will be removed from ERPNext in version-15.\n"
+		"Please install the app to continue using the integration: https://github.com/frappe/exotel_integration",
+		fg="yellow",
+	)
+
+	notification = {
+		"subject": _(
+			"WARNING: Exotel app has been separated from ERPNext, please install the app to continue using Exotel integration."
+		),
+		"type": "Alert",
+	}
+	make_notification_logs(notification, get_system_managers(only_name=True))

From 41b6b739c0328954187c65091c464cd081bfb890 Mon Sep 17 00:00:00 2001
From: Ankush Menat <ankush@frappe.io>
Date: Fri, 14 Jul 2023 17:37:13 +0530
Subject: [PATCH 6/6] fix: touch modified to migrate

---
 .../workspace/erpnext_integrations/erpnext_integrations.json    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
index 6737713316..5c4be6ffaa 100644
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
@@ -241,7 +241,7 @@
    "type": "Link"
   }
  ],
- "modified": "2023-05-24 14:47:25.984717",
+ "modified": "2023-05-24 14:47:26.984717",
  "modified_by": "Administrator",
  "module": "ERPNext Integrations",
  "name": "ERPNext Integrations",