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/GeneralSettings.vue b/frontend/src/components/Settings/General/GeneralSettings.vue index 0c35e65b..551b05bd 100644 --- a/frontend/src/components/Settings/General/GeneralSettings.vue +++ b/frontend/src/components/Settings/General/GeneralSettings.vue @@ -21,7 +21,7 @@