Merge pull request #36133 from s-aga-r/FIX-ISS-23-24-02011

perf: index in `Item` and `Item Variant Attribute`
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 81ff6a5..15c84d4 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -271,6 +271,12 @@
 		as_dict=1,
 	)
 
+	if isinstance(with_cost_center_and_project, str):
+		if with_cost_center_and_project.lower().strip() == "true":
+			with_cost_center_and_project = True
+		else:
+			with_cost_center_and_project = False
+
 	if with_cost_center_and_project:
 		dimension_filters.extend(
 			[
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index f3a892b..db9609d 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -416,6 +416,7 @@
 	filters,
 	gl_entries_by_account,
 	ignore_closing_entries=False,
+	ignore_opening_entries=False,
 ):
 	"""Returns a dict like { "account": [gl entries], ... }"""
 	gl_entries = []
@@ -426,7 +427,6 @@
 		pluck="name",
 	)
 
-	ignore_opening_entries = False
 	if accounts_list:
 		# For balance sheet
 		if not from_date:
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index 7a8b7dc..5176c31 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -117,6 +117,7 @@
 		filters,
 		gl_entries_by_account,
 		ignore_closing_entries=not flt(filters.with_period_closing_entry),
+		ignore_opening_entries=True,
 	)
 
 	calculate_values(accounts, gl_entries_by_account, opening_balances)
diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.json b/erpnext/communication/doctype/communication_medium/communication_medium.json
index 1e1fe3b..b6b9c7e 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/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 0d42ca8..0000000
--- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
+++ /dev/null
@@ -1,89 +0,0 @@
-{
- "actions": [],
- "creation": "2019-05-21 07:41:53.536536",
- "doctype": "DocType",
- "engine": "InnoDB",
- "field_order": [
-  "enabled",
-  "section_break_2",
-  "account_sid",
-  "api_key",
-  "api_token",
-  "section_break_6",
-  "map_custom_field_to_doctype",
-  "target_doctype"
- ],
- "fields": [
-  {
-   "default": "0",
-   "fieldname": "enabled",
-   "fieldtype": "Check",
-   "label": "Enabled"
-  },
-  {
-   "depends_on": "enabled",
-   "fieldname": "section_break_2",
-   "fieldtype": "Section Break",
-   "label": "Credentials"
-  },
-  {
-   "fieldname": "account_sid",
-   "fieldtype": "Data",
-   "label": "Account SID"
-  },
-  {
-   "fieldname": "api_token",
-   "fieldtype": "Data",
-   "label": "API Token"
-  },
-  {
-   "fieldname": "api_key",
-   "fieldtype": "Data",
-   "label": "API Key"
-  },
-  {
-   "depends_on": "enabled",
-   "fieldname": "section_break_6",
-   "fieldtype": "Section Break",
-   "label": "Custom Field"
-  },
-  {
-   "default": "0",
-   "fieldname": "map_custom_field_to_doctype",
-   "fieldtype": "Check",
-   "label": "Map Custom Field to DocType"
-  },
-  {
-   "depends_on": "map_custom_field_to_doctype",
-   "fieldname": "target_doctype",
-   "fieldtype": "Link",
-   "label": "Target DocType",
-   "mandatory_depends_on": "map_custom_field_to_doctype",
-   "options": "DocType"
-  }
- ],
- "issingle": 1,
- "links": [],
- "modified": "2022-12-14 17:24:50.176107",
- "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",
- "states": [],
- "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 0d40667..0000000
--- a/erpnext/erpnext_integrations/exotel_integration.py
+++ /dev/null
@@ -1,151 +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.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 map_custom_field(call_payload, call_log):
-	field_value = call_payload.get("CustomField")
-
-	if not field_value:
-		return call_log
-
-	settings = get_exotel_settings()
-	target_doctype = settings.target_doctype
-	mapping_enabled = settings.map_custom_field_to_doctype
-
-	if not mapping_enabled or not target_doctype:
-		return call_log
-
-	call_log.append("links", {"link_doctype": target_doctype, "link_name": field_value})
-
-	return call_log
-
-
-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"))
-	map_custom_field(call_payload, call_log)
-	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, **kwargs):
-	endpoint = get_exotel_endpoint("Calls/connect.json?details=true")
-	response = requests.post(
-		endpoint, data={"From": from_number, "To": to_number, "CallerId": caller_id, **kwargs}
-	)
-
-	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/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
index ccc46b7..5c4be6f 100644
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
@@ -231,17 +231,6 @@
    "type": "Card Break"
   },
   {
-   "dependencies": "",
-   "hidden": 0,
-   "is_query_report": 0,
-   "label": "Exotel Settings",
-   "link_count": 0,
-   "link_to": "Exotel Settings",
-   "link_type": "DocType",
-   "onboard": 0,
-   "type": "Link"
-  },
-  {
    "hidden": 0,
    "is_query_report": 0,
    "label": "Woocommerce Settings",
@@ -252,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",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 5f957a5..a988bad 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -621,7 +621,7 @@
 	def create_work_order(self, item):
 		from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
 
-		if item.get("qty") <= 0:
+		if flt(item.get("qty")) <= 0:
 			return
 
 		wo = frappe.new_doc("Work Order")
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index f9d9ebb..6fa4b5a 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -333,4 +333,5 @@
 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/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py
index 8849c11..2947b98 100644
--- a/erpnext/patches/v14_0/update_closing_balances.py
+++ b/erpnext/patches/v14_0/update_closing_balances.py
@@ -50,6 +50,7 @@
 							"voucher_no": ["!=", pcv.name],
 							"posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]],
 							"is_opening": "No",
+							"company": company,
 						},
 						fields=["*"],
 					)
@@ -58,7 +59,7 @@
 					# add opening entries only for the first pcv
 					closing_entries += frappe.db.get_all(
 						"GL Entry",
-						filters={"is_cancelled": 0, "is_opening": "Yes"},
+						filters={"is_cancelled": 0, "is_opening": "Yes", "company": company},
 						fields=["*"],
 					)
 
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 0000000..a37773f
--- /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))
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index a859a67..8633be8 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -632,7 +632,6 @@
 		fields.splice(3, 0, {
 			fieldtype: 'Float',
 			fieldname: "conversion_factor",
-			in_list_view: 1,
 			label: __("Conversion Factor"),
 			precision: get_precision('conversion_factor')
 		})
@@ -640,6 +639,7 @@
 
 	new frappe.ui.Dialog({
 		title: __("Update Items"),
+		size: "extra-large",
 		fields: [
 			{
 				fieldname: "trans_items",
diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.js b/erpnext/setup/doctype/holiday_list/holiday_list.js
index ea033c7..90d9f1b 100644
--- a/erpnext/setup/doctype/holiday_list/holiday_list.js
+++ b/erpnext/setup/doctype/holiday_list/holiday_list.js
@@ -6,13 +6,41 @@
 		if (frm.doc.holidays) {
 			frm.set_value("total_holidays", frm.doc.holidays.length);
 		}
+
+		frm.call("get_supported_countries").then(r => {
+			frm.subdivisions_by_country = r.message.subdivisions_by_country;
+			frm.fields_dict.country.set_data(
+				r.message.countries.sort((a, b) => a.label.localeCompare(b.label))
+			);
+
+			if (frm.doc.country) {
+				frm.trigger("set_subdivisions");
+			}
+		});
 	},
 	from_date: function(frm) {
 		if (frm.doc.from_date && !frm.doc.to_date) {
 			var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12);
 			frm.set_value("to_date", frappe.datetime.add_days(a_year_from_start, -1));
 		}
-	}
+	},
+	country: function(frm) {
+		frm.set_value("subdivision", "");
+
+		if (frm.doc.country) {
+			frm.trigger("set_subdivisions");
+		}
+	},
+	set_subdivisions: function(frm) {
+		const subdivisions = [...frm.subdivisions_by_country[frm.doc.country]];
+		if (subdivisions && subdivisions.length > 0) {
+			frm.fields_dict.subdivision.set_data(subdivisions);
+			frm.set_df_property("subdivision", "hidden", 0);
+		} else {
+			frm.fields_dict.subdivision.set_data([]);
+			frm.set_df_property("subdivision", "hidden", 1);
+		}
+	},
 });
 
 frappe.tour["Holiday List"] = [
diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.json b/erpnext/setup/doctype/holiday_list/holiday_list.json
index 4bbe6a6..45671d1 100644
--- a/erpnext/setup/doctype/holiday_list/holiday_list.json
+++ b/erpnext/setup/doctype/holiday_list/holiday_list.json
@@ -1,480 +1,166 @@
 {
- "allow_copy": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
  "allow_import": 1,
  "allow_rename": 1,
  "autoname": "field:holiday_list_name",
- "beta": 0,
  "creation": "2013-01-10 16:34:14",
- "custom": 0,
- "docstatus": 0,
  "doctype": "DocType",
  "document_type": "Setup",
- "editable_grid": 0,
  "engine": "InnoDB",
+ "field_order": [
+  "holiday_list_name",
+  "from_date",
+  "to_date",
+  "column_break_4",
+  "total_holidays",
+  "add_weekly_holidays",
+  "weekly_off",
+  "get_weekly_off_dates",
+  "add_local_holidays",
+  "country",
+  "subdivision",
+  "get_local_holidays",
+  "holidays_section",
+  "holidays",
+  "clear_table",
+  "section_break_9",
+  "color"
+ ],
  "fields": [
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "holiday_list_name",
    "fieldtype": "Data",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
-   "in_standard_filter": 0,
    "label": "Holiday List Name",
-   "length": 0,
-   "no_copy": 0,
    "oldfieldname": "holiday_list_name",
    "oldfieldtype": "Data",
-   "permlevel": 0,
-   "print_hide": 0,
-   "print_hide_if_no_value": 0,
-   "read_only": 0,
-   "remember_last_selected_value": 0,
-   "report_hide": 0,
    "reqd": 1,
-   "search_index": 0,
-   "set_only_once": 0,
-   "translatable": 0,
    "unique": 1
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "from_date",
    "fieldtype": "Date",
-   "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": "From Date",
-   "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": 1,
-   "search_index": 0,
-   "set_only_once": 0,
-   "translatable": 0,
-   "unique": 0
+   "reqd": 1
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "to_date",
    "fieldtype": "Date",
-   "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": "To Date",
-   "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": 1,
-   "search_index": 0,
-   "set_only_once": 0,
-   "translatable": 0,
-   "unique": 0
+   "reqd": 1
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "column_break_4",
-   "fieldtype": "Column Break",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
-   "in_standard_filter": 0,
-   "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
+   "fieldtype": "Column Break"
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "total_holidays",
    "fieldtype": "Int",
-   "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": "Total Holidays",
-   "length": 0,
-   "no_copy": 0,
-   "permlevel": 0,
-   "precision": "",
-   "print_hide": 0,
-   "print_hide_if_no_value": 0,
-   "read_only": 1,
-   "remember_last_selected_value": 0,
-   "report_hide": 0,
-   "reqd": 0,
-   "search_index": 0,
-   "set_only_once": 0,
-   "translatable": 0,
-   "unique": 0
+   "read_only": 1
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
    "collapsible": 1,
-   "columns": 0,
+   "depends_on": "eval: doc.from_date && doc.to_date",
    "fieldname": "add_weekly_holidays",
    "fieldtype": "Section Break",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
-   "in_standard_filter": 0,
-   "label": "Add Weekly Holidays",
-   "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
+   "label": "Add Weekly Holidays"
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "weekly_off",
    "fieldtype": "Select",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
    "in_standard_filter": 1,
    "label": "Weekly Off",
-   "length": 0,
    "no_copy": 1,
    "options": "\nSunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday",
-   "permlevel": 0,
    "print_hide": 1,
-   "print_hide_if_no_value": 0,
-   "read_only": 0,
-   "remember_last_selected_value": 0,
-   "report_hide": 1,
-   "reqd": 0,
-   "search_index": 0,
-   "set_only_once": 0,
-   "translatable": 0,
-   "unique": 0
+   "report_hide": 1
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "get_weekly_off_dates",
    "fieldtype": "Button",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
-   "in_standard_filter": 0,
    "label": "Add to Holidays",
-   "length": 0,
-   "no_copy": 0,
-   "options": "get_weekly_off_dates",
-   "permlevel": 0,
-   "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
+   "options": "get_weekly_off_dates"
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "holidays_section",
    "fieldtype": "Section Break",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
-   "in_standard_filter": 0,
-   "label": "Holidays",
-   "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
+   "label": "Holidays"
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "holidays",
    "fieldtype": "Table",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
-   "in_standard_filter": 0,
    "label": "Holidays",
-   "length": 0,
-   "no_copy": 0,
    "oldfieldname": "holiday_list_details",
    "oldfieldtype": "Table",
-   "options": "Holiday",
-   "permlevel": 0,
-   "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
+   "options": "Holiday"
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "clear_table",
    "fieldtype": "Button",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
-   "in_standard_filter": 0,
    "label": "Clear Table",
-   "length": 0,
-   "no_copy": 0,
-   "options": "clear_table",
-   "permlevel": 0,
-   "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
+   "options": "clear_table"
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "section_break_9",
-   "fieldtype": "Section Break",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
-   "in_standard_filter": 0,
-   "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
+   "fieldtype": "Section Break"
   },
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
    "fieldname": "color",
    "fieldtype": "Color",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
-   "in_standard_filter": 0,
    "label": "Color",
-   "length": 0,
-   "no_copy": 0,
-   "permlevel": 0,
-   "precision": "",
-   "print_hide": 1,
-   "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
+   "print_hide": 1
+  },
+  {
+   "fieldname": "country",
+   "fieldtype": "Autocomplete",
+   "label": "Country"
+  },
+  {
+   "depends_on": "country",
+   "fieldname": "subdivision",
+   "fieldtype": "Autocomplete",
+   "label": "Subdivision"
+  },
+  {
+   "collapsible": 1,
+   "depends_on": "eval: doc.from_date && doc.to_date",
+   "fieldname": "add_local_holidays",
+   "fieldtype": "Section Break",
+   "label": "Add Local Holidays"
+  },
+  {
+   "fieldname": "get_local_holidays",
+   "fieldtype": "Button",
+   "label": "Add to Holidays",
+   "options": "get_local_holidays"
   }
  ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
  "icon": "fa fa-calendar",
  "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-07-03 07:22:46.474096",
+ "links": [],
+ "modified": "2023-07-14 13:28:53.156421",
  "modified_by": "Administrator",
  "module": "Setup",
  "name": "Holiday List",
+ "naming_rule": "By fieldname",
  "owner": "Administrator",
  "permissions": [
   {
-   "amend": 0,
-   "cancel": 0,
    "create": 1,
    "delete": 1,
    "email": 1,
-   "export": 0,
-   "if_owner": 0,
-   "import": 0,
-   "permlevel": 0,
    "print": 1,
    "read": 1,
    "report": 1,
    "role": "HR Manager",
-   "set_user_permissions": 0,
    "share": 1,
-   "submit": 0,
    "write": 1
   }
  ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
  "sort_field": "modified",
  "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py
index 84d0d35..2ef4e65 100644
--- a/erpnext/setup/doctype/holiday_list/holiday_list.py
+++ b/erpnext/setup/doctype/holiday_list/holiday_list.py
@@ -3,11 +3,15 @@
 
 
 import json
+from datetime import date
 
 import frappe
+from babel import Locale
 from frappe import _, throw
 from frappe.model.document import Document
-from frappe.utils import cint, formatdate, getdate, today
+from frappe.utils import formatdate, getdate, today
+from holidays import country_holidays
+from holidays.utils import list_supported_countries
 
 
 class OverlapError(frappe.ValidationError):
@@ -21,25 +25,66 @@
 
 	@frappe.whitelist()
 	def get_weekly_off_dates(self):
-		self.validate_values()
-		date_list = self.get_weekly_off_date_list(self.from_date, self.to_date)
-		last_idx = max(
-			[cint(d.idx) for d in self.get("holidays")]
-			or [
-				0,
-			]
-		)
-		for i, d in enumerate(date_list):
-			ch = self.append("holidays", {})
-			ch.description = _(self.weekly_off)
-			ch.holiday_date = d
-			ch.weekly_off = 1
-			ch.idx = last_idx + i + 1
-
-	def validate_values(self):
 		if not self.weekly_off:
 			throw(_("Please select weekly off day"))
 
+		existing_holidays = self.get_holidays()
+
+		for d in self.get_weekly_off_date_list(self.from_date, self.to_date):
+			if d in existing_holidays:
+				continue
+
+			self.append("holidays", {"description": _(self.weekly_off), "holiday_date": d, "weekly_off": 1})
+
+		self.sort_holidays()
+
+	@frappe.whitelist()
+	def get_supported_countries(self):
+		subdivisions_by_country = list_supported_countries()
+		countries = [
+			{"value": country, "label": local_country_name(country)}
+			for country in subdivisions_by_country.keys()
+		]
+		return {
+			"countries": countries,
+			"subdivisions_by_country": subdivisions_by_country,
+		}
+
+	@frappe.whitelist()
+	def get_local_holidays(self):
+		if not self.country:
+			throw(_("Please select a country"))
+
+		existing_holidays = self.get_holidays()
+		from_date = getdate(self.from_date)
+		to_date = getdate(self.to_date)
+
+		for holiday_date, holiday_name in country_holidays(
+			self.country,
+			subdiv=self.subdivision,
+			years=[from_date.year, to_date.year],
+			language=frappe.local.lang,
+		).items():
+			if holiday_date in existing_holidays:
+				continue
+
+			if holiday_date < from_date or holiday_date > to_date:
+				continue
+
+			self.append(
+				"holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0}
+			)
+
+		self.sort_holidays()
+
+	def sort_holidays(self):
+		self.holidays.sort(key=lambda x: getdate(x.holiday_date))
+		for i in range(len(self.holidays)):
+			self.holidays[i].idx = i + 1
+
+	def get_holidays(self) -> list[date]:
+		return [getdate(holiday.holiday_date) for holiday in self.holidays]
+
 	def validate_days(self):
 		if getdate(self.from_date) > getdate(self.to_date):
 			throw(_("To Date cannot be before From Date"))
@@ -120,3 +165,8 @@
 		)
 	else:
 		return False
+
+
+def local_country_name(country_code: str) -> str:
+	"""Return the localized country name for the given country code."""
+	return Locale.parse(frappe.local.lang).territories.get(country_code, country_code)
diff --git a/erpnext/setup/doctype/holiday_list/test_holiday_list.py b/erpnext/setup/doctype/holiday_list/test_holiday_list.py
index d32cfe8..23b08fd 100644
--- a/erpnext/setup/doctype/holiday_list/test_holiday_list.py
+++ b/erpnext/setup/doctype/holiday_list/test_holiday_list.py
@@ -3,7 +3,7 @@
 
 import unittest
 from contextlib import contextmanager
-from datetime import timedelta
+from datetime import date, timedelta
 
 import frappe
 from frappe.utils import getdate
@@ -23,6 +23,41 @@
 		fetched_holiday_list = frappe.get_value("Holiday List", holiday_list.name)
 		self.assertEqual(holiday_list.name, fetched_holiday_list)
 
+	def test_weekly_off(self):
+		holiday_list = frappe.new_doc("Holiday List")
+		holiday_list.from_date = "2023-01-01"
+		holiday_list.to_date = "2023-02-28"
+		holiday_list.weekly_off = "Sunday"
+		holiday_list.get_weekly_off_dates()
+
+		holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
+
+		self.assertNotIn(date(2022, 12, 25), holidays)
+		self.assertIn(date(2023, 1, 1), holidays)
+		self.assertIn(date(2023, 1, 8), holidays)
+		self.assertIn(date(2023, 1, 15), holidays)
+		self.assertIn(date(2023, 1, 22), holidays)
+		self.assertIn(date(2023, 1, 29), holidays)
+		self.assertIn(date(2023, 2, 5), holidays)
+		self.assertIn(date(2023, 2, 12), holidays)
+		self.assertIn(date(2023, 2, 19), holidays)
+		self.assertIn(date(2023, 2, 26), holidays)
+		self.assertNotIn(date(2023, 3, 5), holidays)
+
+	def test_local_holidays(self):
+		holiday_list = frappe.new_doc("Holiday List")
+		holiday_list.from_date = "2023-04-01"
+		holiday_list.to_date = "2023-04-30"
+		holiday_list.country = "DE"
+		holiday_list.subdivision = "SN"
+		holiday_list.get_local_holidays()
+
+		holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
+		self.assertNotIn(date(2023, 1, 1), holidays)
+		self.assertIn(date(2023, 4, 7), holidays)
+		self.assertIn(date(2023, 4, 10), holidays)
+		self.assertNotIn(date(2023, 5, 1), holidays)
+
 
 def make_holiday_list(
 	name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None
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 9b91414..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_single_value("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
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 5f0a8dc..e30a5d0 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -1219,7 +1219,7 @@
 Hold,Anhalten,
 Hold Invoice,Rechnung zurückhalten,
 Holiday,Urlaub,
-Holiday List,Urlaubsübersicht,
+Holiday List,Feiertagsliste,
 Hotel Rooms of type {0} are unavailable on {1},Hotelzimmer vom Typ {0} sind auf {1} nicht verfügbar,
 Hotels,Hotels,
 Hourly,Stündlich,
@@ -3317,7 +3317,7 @@
 Working,In Bearbeitung,
 Working Hours,Arbeitszeit,
 Workstation,Arbeitsplatz,
-Workstation is closed on the following dates as per Holiday List: {0},Arbeitsplatz ist an folgenden Tagen gemäß der Urlaubsliste geschlossen: {0},
+Workstation is closed on the following dates as per Holiday List: {0},Arbeitsplatz ist an folgenden Tagen gemäß der Feiertagsliste geschlossen: {0},
 Wrapping up,Aufwickeln,
 Wrong Password,Falsches Passwort,
 Year start date or end date is overlapping with {0}. To avoid please set company,"Jahresbeginn oder Enddatum überlappt mit {0}. Bitte ein Unternehmen wählen, um dies zu verhindern",
@@ -3583,6 +3583,7 @@
 Activity,Aktivität,
 Add / Manage Email Accounts.,Hinzufügen/Verwalten von E-Mail-Konten,
 Add Child,Unterpunkt hinzufügen,
+Add Local Holidays,Lokale Feiertage hinzufügen,
 Add Multiple,Mehrere hinzufügen,
 Add Participants,Teilnehmer hinzufügen,
 Add to Featured Item,Zum empfohlenen Artikel hinzufügen,
@@ -4046,6 +4047,7 @@
 Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.,Der Bestandswert ({0}) und der Kontostand ({1}) sind für das Konto {2} und die verknüpften Lager nicht synchron.,
 Stores - {0},Stores - {0},
 Student with email {0} does not exist,Der Student mit der E-Mail-Adresse {0} existiert nicht,
+Subdivision,Teilgebiet,
 Submit Review,Bewertung abschicken,
 Submitted,Gebucht,
 Supplier Addresses And Contacts,Lieferanten-Adressen und Kontaktdaten,
@@ -4192,6 +4194,7 @@
 No students Found,Keine Schüler gefunden,
 Not in Stock,Nicht lagernd,
 Please select a Customer,Bitte wählen Sie einen Kunden aus,
+Please select a country,Bitte wählen Sie ein Land aus,
 Printed On,Gedruckt auf,
 Received From,Erhalten von,
 Sales Person,Verkäufer,
@@ -6497,7 +6500,7 @@
 Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
 Leave Policy,Urlaubsrichtlinie,
 Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
-Applicable Holiday List,Geltende Urlaubsliste,
+Applicable Holiday List,Geltende Feiertagsliste,
 Default Shift,Standardverschiebung,
 Salary Details,Gehaltsdetails,
 Salary Mode,Gehaltsmodus,
@@ -6662,12 +6665,12 @@
 Expense Claim Detail,Auslage,
 Expense Date,Datum der Auslage,
 Expense Claim Type,Art der Auslagenabrechnung,
-Holiday List Name,Urlaubslistenname,
-Total Holidays,Insgesamt Feiertage,
-Add Weekly Holidays,Wöchentliche Feiertage hinzufügen,
+Holiday List Name,Name der Feiertagsliste,
+Total Holidays,Insgesamt freie Tage,
+Add Weekly Holidays,Wöchentlich freie Tage hinzufügen,
 Weekly Off,Wöchentlich frei,
-Add to Holidays,Zu Feiertagen hinzufügen,
-Holidays,Ferien,
+Add to Holidays,Zu freien Tagen hinzufügen,
+Holidays,Arbeitsfreie Tage,
 Clear Table,Tabelle leeren,
 HR Settings,Einstellungen zum Modul Personalwesen,
 Employee Settings,Mitarbeitereinstellungen,
@@ -6777,7 +6780,7 @@
 Is Carry Forward,Ist Übertrag,
 Is Expired,Ist abgelaufen,
 Is Leave Without Pay,Ist unbezahlter Urlaub,
-Holiday List for Optional Leave,Urlaubsliste für optionalen Urlaub,
+Holiday List for Optional Leave,Feiertagsliste für optionalen Urlaub,
 Leave Allocations,Zuteilungen verlassen,
 Leave Policy Details,Urlaubsrichtliniendetails,
 Leave Policy Detail,Urlaubsrichtliniendetail,
@@ -7646,7 +7649,7 @@
 Change Abbreviation,Abkürzung ändern,
 Parent Company,Muttergesellschaft,
 Default Values,Standardwerte,
-Default Holiday List,Standard-Urlaubsliste,
+Default Holiday List,Standard Feiertagsliste,
 Default Selling Terms,Standardverkaufsbedingungen,
 Default Buying Terms,Standard-Einkaufsbedingungen,
 Create Chart Of Accounts Based On,"Kontenplan erstellen, basierend auf",
diff --git a/pyproject.toml b/pyproject.toml
index 012ffb1..3e0dfb2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,6 +13,7 @@
     "Unidecode~=1.3.6",
     "barcodenumber~=0.5.0",
     "rapidfuzz~=2.15.0",
+    "holidays~=0.28",
 
     # integration dependencies
     "gocardless-pro~=1.22.0",