diff --git a/crm/fcrm/doctype/crm_deal/crm_deal.py b/crm/fcrm/doctype/crm_deal/crm_deal.py
index bd4198a3..6bc2e020 100644
--- a/crm/fcrm/doctype/crm_deal/crm_deal.py
+++ b/crm/fcrm/doctype/crm_deal/crm_deal.py
@@ -7,10 +7,8 @@ from frappe.desk.form.assign_to import add as assign
from frappe.model.document import Document
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
-from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
- add_status_change_log,
-)
-from crm.utils import get_exchange_rate
+from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log
+from crm.fcrm.doctype.fcrm_settings.fcrm_settings import get_exchange_rate
class CRMDeal(Document):
@@ -177,7 +175,7 @@ class CRMDeal(Document):
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
exchange_rate = 1
if self.currency and self.currency != system_currency:
- exchange_rate = get_exchange_rate(self.currency, system_currency, frappe.utils.nowdate())
+ exchange_rate = get_exchange_rate(self.currency, system_currency)
self.db_set("exchange_rate", exchange_rate)
diff --git a/crm/fcrm/doctype/crm_organization/crm_organization.py b/crm/fcrm/doctype/crm_organization/crm_organization.py
index 1cdf186c..a8247627 100644
--- a/crm/fcrm/doctype/crm_organization/crm_organization.py
+++ b/crm/fcrm/doctype/crm_organization/crm_organization.py
@@ -4,7 +4,7 @@
import frappe
from frappe.model.document import Document
-from crm.utils import get_exchange_rate
+from crm.fcrm.doctype.fcrm_settings.fcrm_settings import get_exchange_rate
class CRMOrganization(Document):
@@ -16,7 +16,7 @@ class CRMOrganization(Document):
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
exchange_rate = 1
if self.currency and self.currency != system_currency:
- exchange_rate = get_exchange_rate(self.currency, system_currency, frappe.utils.nowdate())
+ exchange_rate = get_exchange_rate(self.currency, system_currency)
self.db_set("exchange_rate", exchange_rate)
diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
index 635c02d3..0e04b4d1 100644
--- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
+++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.json
@@ -8,7 +8,12 @@
"defaults_tab",
"restore_defaults",
"enable_forecasting",
+ "currency_tab",
"currency",
+ "exchange_rate_provider_section",
+ "service_provider",
+ "column_break_vqck",
+ "access_key",
"branding_tab",
"brand_name",
"brand_logo",
@@ -72,13 +77,42 @@
"in_list_view": 1,
"label": "Currency",
"options": "Currency"
+ },
+ {
+ "fieldname": "currency_tab",
+ "fieldtype": "Tab Break",
+ "label": "Currency"
+ },
+ {
+ "fieldname": "exchange_rate_provider_section",
+ "fieldtype": "Section Break",
+ "label": "Exchange Rate Provider"
+ },
+ {
+ "default": "frankfurter.app",
+ "fieldname": "service_provider",
+ "fieldtype": "Select",
+ "label": "Service Provider",
+ "options": "frankfurter.app\nexchangerate.host",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.service_provider == 'exchangerate.host';",
+ "fieldname": "access_key",
+ "fieldtype": "Data",
+ "label": "Access Key",
+ "mandatory_depends_on": "eval:doc.service_provider == 'exchangerate.host';"
+ },
+ {
+ "fieldname": "column_break_vqck",
+ "fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2025-07-13 11:58:34.857638",
- "modified_by": "Administrator",
+ "modified": "2025-07-28 17:04:24.585768",
+ "modified_by": "shariq@frappe.io",
"module": "FCRM",
"name": "FCRM Settings",
"owner": "Administrator",
diff --git a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py
index 7897bb1a..3c27695f 100644
--- a/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py
+++ b/crm/fcrm/doctype/fcrm_settings/fcrm_settings.py
@@ -2,6 +2,7 @@
# For license information, please see license.txt
import frappe
+import requests
from frappe import _
from frappe.custom.doctype.property_setter.property_setter import delete_property_setter, make_property_setter
from frappe.model.document import Document
@@ -132,3 +133,76 @@ def get_forecasting_script():
this.doc.probability = status.probability
}
}"""
+
+
+def get_exchange_rate(from_currency, to_currency, date=None):
+ if not date:
+ date = "latest"
+
+ api_used = "frankfurter"
+
+ api_endpoint = f"https://api.frankfurter.app/{date}?from={from_currency}&to={to_currency}"
+ res = requests.get(api_endpoint, timeout=5)
+ if res.ok:
+ data = res.json()
+ return data["rates"][to_currency]
+
+ # Fallback to exchangerate.host if Frankfurter API fails
+ settings = FCRMSettings("FCRM Settings")
+ if settings and settings.service_provider == "exchangerate.host":
+ api_used = "exchangerate.host"
+ if not settings.access_key:
+ frappe.throw(
+ _("Access Key is required for Service Provider: {0}").format(
+ frappe.bold(settings.service_provider)
+ )
+ )
+
+ params = {
+ "access_key": settings.access_key,
+ "from": from_currency,
+ "to": to_currency,
+ "amount": 1,
+ }
+
+ if date != "latest":
+ params["date"] = date
+
+ api_endpoint = "https://api.exchangerate.host/convert"
+
+ res = requests.get(api_endpoint, params=params, timeout=5)
+ if res.ok:
+ data = res.json()
+ return data["result"]
+
+ frappe.log_error(
+ title="Exchange Rate Fetch Error",
+ message=f"Failed to fetch exchange rate from {from_currency} to {to_currency} using {api_used} API.",
+ )
+
+ if api_used == "frankfurter":
+ user = frappe.session.user
+ is_manager = (
+ "System Manager" in frappe.get_roles(user)
+ or "Sales Manager" in frappe.get_roles(user)
+ or user == "Administrator"
+ )
+
+ if not is_manager:
+ frappe.throw(
+ _(
+ "Ask your manager to set up the Exchange Rate Provider, as default provider does not support currency conversion for {0} to {1}."
+ ).format(from_currency, to_currency)
+ )
+ else:
+ frappe.throw(
+ _(
+ "Setup the Exchange Rate Provider as 'Exchangerate Host' in settings, as default provider does not support currency conversion for {0} to {1}."
+ ).format(from_currency, to_currency)
+ )
+
+ frappe.throw(
+ _(
+ "Failed to fetch exchange rate from {0} to {1} on {2}. Please check your internet connection or try again later."
+ ).format(from_currency, to_currency, date)
+ )
diff --git a/crm/utils/__init__.py b/crm/utils/__init__.py
index eaf076d5..92ee37d7 100644
--- a/crm/utils/__init__.py
+++ b/crm/utils/__init__.py
@@ -267,24 +267,3 @@ def sales_user_only(fn):
return fn(*args, **kwargs)
return wrapper
-
-
-def get_exchange_rate(from_currency, to_currency, date=None):
- if not date:
- date = "latest"
-
- url = f"https://api.frankfurter.app/{date}?from={from_currency}&to={to_currency}"
-
- for _i in range(3):
- response = requests.get(url)
- if response.status_code == 200:
- data = response.json()
- rate = data["rates"].get(to_currency)
- if rate:
- return rate
-
- frappe.log_error(
- f"Failed to fetch exchange rate from {from_currency} to {to_currency} on {date}",
- title="Exchange Rate Fetch Error",
- )
- return 1.0 # Default exchange rate if API call fails or no rate found
diff --git a/frontend/components.d.ts b/frontend/components.d.ts
index c1dd22b6..afedd3ed 100644
--- a/frontend/components.d.ts
+++ b/frontend/components.d.ts
@@ -62,6 +62,7 @@ declare module 'vue' {
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
+ CurrencySettings: typeof import('./src/components/Settings/General/CurrencySettings.vue')['default']
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']
diff --git a/frontend/src/components/Settings/General/CurrencySettings.vue b/frontend/src/components/Settings/General/CurrencySettings.vue
new file mode 100644
index 00000000..63699bd9
--- /dev/null
+++ b/frontend/src/components/Settings/General/CurrencySettings.vue
@@ -0,0 +1,197 @@
+
+