From e804bf33ba388debd6a601220aa6709cbf19daee Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Tue, 9 Jun 2026 11:58:30 +0530 Subject: [PATCH 1/5] feat(currency exchange settings): frankfurter v2 support (cherry picked from commit 56bfe6b6a695b2d93ceedf8d50082aa732789b2a) --- .../currency_exchange_settings.js | 18 ++++++++++++------ .../currency_exchange_settings.json | 6 +++--- .../currency_exchange_settings.py | 19 +++++++++++++++++-- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js index 40f0938ee1c..950092a2382 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js @@ -11,22 +11,28 @@ frappe.ui.form.on("Currency Exchange Settings", { }, callback: function (r) { if (r && r.message) { + let result = [], + params = {}; if (frm.doc.service_provider == "exchangerate.host") { - let result = ["result"]; - let params = { + result = ["result"]; + params = { date: "{transaction_date}", from: "{from_currency}", to: "{to_currency}", }; - add_param(frm, r.message, params, result); } else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) { - let result = ["rates", "{to_currency}"]; - let params = { + result = ["rates", "{to_currency}"]; + params = { base: "{from_currency}", symbols: "{to_currency}", }; - add_param(frm, r.message, params, result); + } else if (frm.doc.service_provider == "frankfurter.dev - v2") { + result = ["rate"]; + params = { + date: "{transaction_date}", + }; } + add_param(frm, r.message, params, result); } }, }); diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 9f0852bb686..2fbb0086245 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_bulk_edit": 1, "creation": "2022-01-10 13:03:26.237081", "doctype": "DocType", "editable_grid": 1, @@ -78,7 +79,7 @@ "fieldname": "service_provider", "fieldtype": "Select", "label": "Service Provider", - "options": "frankfurter.dev\nexchangerate.host\nCustom", + "options": "frankfurter.dev\nexchangerate.host\nfrankfurter.dev - v2\nCustom", "reqd": 1 }, { @@ -101,11 +102,10 @@ "label": "Use HTTP Protocol" } ], - "hide_toolbar": 0, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-03-16 13:28:21.075743", + "modified": "2026-06-09 11:34:10.432378", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py index 55f967fc788..3d7651cb485 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document): disabled: DF.Check req_params: DF.Table[CurrencyExchangeSettingsDetails] result_key: DF.Table[CurrencyExchangeSettingsResult] - service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"] + service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "frankfurter.dev - v2", "Custom"] url: DF.Data | None use_http: DF.Check # end: auto-generated types @@ -70,6 +70,14 @@ class CurrencyExchangeSettings(Document): self.append("req_params", {"key": "base", "value": "{from_currency}"}) self.append("req_params", {"key": "symbols", "value": "{to_currency}"}) + elif self.service_provider == "frankfurter.dev - v2": + self.set("result_key", []) + self.set("req_params", []) + + self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http) + self.append("result_key", {"key": "rate"}) + self.append("req_params", {"key": "date", "value": "{transaction_date}"}) + def validate_parameters(self): params = {} for row in self.req_params: @@ -105,13 +113,20 @@ class CurrencyExchangeSettings(Document): @frappe.whitelist() def get_api_endpoint(service_provider: str | None = None, use_http: bool = False): - if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]: + if service_provider and service_provider in [ + "exchangerate.host", + "frankfurter.dev", + "frankfurter.app", + "frankfurter.dev - v2", + ]: if service_provider == "exchangerate.host": api = "api.exchangerate.host/convert" elif service_provider == "frankfurter.app": api = "api.frankfurter.app/{transaction_date}" elif service_provider == "frankfurter.dev": api = "api.frankfurter.dev/v1/{transaction_date}" + elif service_provider == "frankfurter.dev - v2": + api = "api.frankfurter.dev/v2/rate/{from_currency}/{to_currency}" protocol = "https://" if use_http: From 471ab662f6ffac1bfcc2e96d2131ca778a58c763 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Tue, 9 Jun 2026 12:17:18 +0530 Subject: [PATCH 2/5] fix: use frankfurter v2 by default for new install (cherry picked from commit 479f9f63c9b3e26410e67190b1e8cf4e47832b19) --- erpnext/setup/install.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index b8162bb648c..08ce8d98a28 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -87,14 +87,7 @@ def setup_repost_defaults(): def setup_currency_exchange(): ces = frappe.get_single("Currency Exchange Settings") try: - ces.set("result_key", []) - ces.set("req_params", []) - - ces.api_endpoint = "https://api.frankfurter.dev/v1/{transaction_date}" - ces.append("result_key", {"key": "rates"}) - ces.append("result_key", {"key": "{to_currency}"}) - ces.append("req_params", {"key": "base", "value": "{from_currency}"}) - ces.append("req_params", {"key": "symbols", "value": "{to_currency}"}) + ces.service_provider = "frankfurter.dev - v2" ces.save() except frappe.ValidationError: pass From 6f25d915c77b26285fcd727543a8073b2b4619e5 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Tue, 9 Jun 2026 23:00:41 +0530 Subject: [PATCH 3/5] test: fixed currency exchange test for frankfurter v2 api (cherry picked from commit 138f683a68acb6c698beb26821299264cc1e4e73) --- .../doctype/currency_exchange/test_currency_exchange.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index 79df2a7ab32..ba8b685ebb6 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -66,13 +66,16 @@ def patched_requests_get(*args, **kwargs): if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"): if test_exchange_values.get(kwargs["params"]["date"]): return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200) - elif args[0].startswith("https://api.frankfurter.dev") and kwargs.get("params"): + elif args[0].startswith("https://api.frankfurter.dev/v1") and kwargs.get("params"): if kwargs["params"].get("base") and kwargs["params"].get("symbols"): date = args[0].replace("https://api.frankfurter.dev/v1/", "") if test_exchange_values.get(date): return PatchResponse( {"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200 ) + elif args[0].startswith("https://api.frankfurter.dev/v2") and kwargs.get("params"): + if kwargs["params"].get("date") and test_exchange_values.get(kwargs["params"]["date"]): + return PatchResponse({"rate": test_exchange_values.get(kwargs["params"]["date"])}, 200) return PatchResponse({"rates": None}, 404) From 79fd176a8e13674428ce144556ce0cb1a0876ff4 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Mon, 15 Jun 2026 11:27:26 +0530 Subject: [PATCH 4/5] fix: restricting currency_exchange_settings write permission only to system manager (cherry picked from commit 0c2d5488a64325724afe4b3f925a9be92e000a4f) --- .../currency_exchange_settings.json | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 2fbb0086245..a3aea6016b9 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_bulk_edit": 1, "creation": "2022-01-10 13:03:26.237081", "doctype": "DocType", "editable_grid": 1, @@ -105,7 +104,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-06-09 11:34:10.432378", + "modified": "2026-06-15 11:25:55.873110", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", @@ -122,24 +121,11 @@ "write": 1 }, { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "Accounts Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, "email": 1, "print": 1, "read": 1, "role": "Accounts User", - "share": 1, - "write": 1 + "share": 1 } ], "row_format": "Dynamic", From 2606d660af5480b9675eb9c741faf372587824f7 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Mon, 15 Jun 2026 13:50:38 +0530 Subject: [PATCH 5/5] fix(get_exchange_rate): using get_single_value to fetch `disabled` value from `currency_exchange_settings` (cherry picked from commit abb579e2db7ac7705b859f8fb037fac7a0de96be) --- erpnext/setup/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 03938ebb94e..d5d3081bac4 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -95,7 +95,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if entries: return flt(entries[0].exchange_rate) - if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"): + if frappe.get_single_value("Currency Exchange Settings", "disabled"): return 0.00 pegged_currencies = {}