@@ -46,6 +46,7 @@
|
||||
"role_to_override_stop_action",
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"allow_pegged_currencies_exchange_rates",
|
||||
"column_break_yuug",
|
||||
"stale_days",
|
||||
"section_break_jpd0",
|
||||
@@ -614,6 +615,13 @@
|
||||
{
|
||||
"fieldname": "column_break_feyo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable this field to fetch the exchange rates for Pegged Currencies.\n\n",
|
||||
"fieldname": "allow_pegged_currencies_exchange_rates",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Pegged Currencies Exchange Rates"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -622,7 +630,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-06 11:03:28.095723",
|
||||
"modified": "2025-06-16 16:40:54.871486",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -26,6 +26,7 @@ class AccountsSettings(Document):
|
||||
acc_frozen_upto: DF.Date | None
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
allow_pegged_currencies_exchange_rates: DF.Check
|
||||
allow_stale: DF.Check
|
||||
auto_reconcile_payments: DF.Check
|
||||
auto_reconciliation_job_trigger: DF.Int
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Pegged Currencies", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-05-30 11:47:03.670913",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"pegged_currencies_item_section",
|
||||
"pegged_currency_item"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "pegged_currencies_item_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_currency_item",
|
||||
"fieldtype": "Table",
|
||||
"options": "Pegged Currency Details"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-02 11:46:31.936714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pegged Currencies",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PeggedCurrencies(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pegged_currencies.pegged_currencies import PeggedCurrencies
|
||||
|
||||
pegged_currency_item: DF.Table[PeggedCurrencies]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestPeggedCurrencies(UnitTestCase):
|
||||
"""
|
||||
Unit tests for PeggedCurrencies.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestPeggedCurrencies(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for PeggedCurrencies.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-05-30 11:59:28.219277",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"source_currency",
|
||||
"pegged_against",
|
||||
"pegged_exchange_rate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "source_currency",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_exchange_rate",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Exchange Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "pegged_against",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Pegged Against",
|
||||
"options": "Currency"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-17 14:11:16.521193",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pegged Currency Details",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PeggedCurrencyDetails(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
pegged_against: DF.Link | None
|
||||
pegged_exchange_rate: DF.Data | None
|
||||
source_currency: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -421,3 +421,4 @@ erpnext.patches.v14_0.update_full_name_in_contract
|
||||
erpnext.patches.v15_0.drop_sle_indexes
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "confirm_before_resetting_posting_date", 1)
|
||||
erpnext.patches.v15_0.rename_pos_closing_entry_fields #2025-06-13
|
||||
erpnext.patches.v15_0.update_pegged_currencies
|
||||
|
||||
7
erpnext/patches/v15_0/update_pegged_currencies.py
Normal file
7
erpnext/patches/v15_0/update_pegged_currencies.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.setup.install import update_pegged_currencies
|
||||
|
||||
|
||||
def execute():
|
||||
update_pegged_currencies()
|
||||
@@ -6,6 +6,7 @@ from frappe.tests import IntegrationTestCase, change_settings
|
||||
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
||||
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = ["Product Bundle"]
|
||||
|
||||
@@ -863,6 +864,24 @@ class TestQuotation(IntegrationTestCase):
|
||||
quotation.reload()
|
||||
self.assertEqual(quotation.status, "Ordered")
|
||||
|
||||
@change_settings("Accounts Settings", {"allow_pegged_currencies_exchange_rates": True})
|
||||
def test_make_quotation_qar_to_inr(self):
|
||||
quotation = make_quotation(
|
||||
currency="QAR",
|
||||
transaction_date="2026-06-04",
|
||||
)
|
||||
|
||||
cache = frappe.cache()
|
||||
key = "currency_exchange_rate_{}:{}:{}".format("2026-06-04", "QAR", "INR")
|
||||
value = cache.get(key)
|
||||
expected_rate = flt(value) / 3.64
|
||||
|
||||
self.assertEqual(
|
||||
quotation.conversion_rate,
|
||||
expected_rate,
|
||||
f"Expected conversion rate {expected_rate}, got {quotation.conversion_rate}",
|
||||
)
|
||||
|
||||
|
||||
def enable_calculate_bundle_price(enable=1):
|
||||
selling_settings = frappe.get_doc("Selling Settings")
|
||||
|
||||
@@ -32,6 +32,7 @@ def after_install():
|
||||
add_app_name()
|
||||
update_roles()
|
||||
make_default_operations()
|
||||
update_pegged_currencies()
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@@ -223,6 +224,27 @@ def create_default_role_profiles():
|
||||
role_profile.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def update_pegged_currencies():
|
||||
doc = frappe.get_doc("Pegged Currencies", "Pegged Currencies")
|
||||
|
||||
existing_sources = {item.source_currency for item in doc.pegged_currency_item}
|
||||
|
||||
currencies_to_add = [
|
||||
{"source_currency": "AED", "pegged_against": "USD", "pegged_exchange_rate": 3.6725},
|
||||
{"source_currency": "BHD", "pegged_against": "USD", "pegged_exchange_rate": 0.376},
|
||||
{"source_currency": "JOD", "pegged_against": "USD", "pegged_exchange_rate": 0.709},
|
||||
{"source_currency": "OMR", "pegged_against": "USD", "pegged_exchange_rate": 0.3845},
|
||||
{"source_currency": "QAR", "pegged_against": "USD", "pegged_exchange_rate": 3.64},
|
||||
{"source_currency": "SAR", "pegged_against": "USD", "pegged_exchange_rate": 3.75},
|
||||
]
|
||||
|
||||
for currency in currencies_to_add:
|
||||
if currency["source_currency"] not in existing_sources:
|
||||
doc.append("pegged_currency_item", currency)
|
||||
|
||||
doc.save()
|
||||
|
||||
|
||||
DEFAULT_ROLE_PROFILES = {
|
||||
"Inventory": [
|
||||
"Stock User",
|
||||
|
||||
@@ -9,10 +9,6 @@ from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext import get_default_company
|
||||
|
||||
PEGGED_CURRENCIES = {
|
||||
"USD": {"AED": 3.6725}, # AED is pegged to USD at a rate of 3.6725 since 1997
|
||||
}
|
||||
|
||||
|
||||
def before_tests():
|
||||
frappe.clear_cache()
|
||||
@@ -47,11 +43,51 @@ def before_tests():
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_pegged_rate(from_currency: str, to_currency: str, transaction_date) -> float | None:
|
||||
if rate := PEGGED_CURRENCIES.get(from_currency, {}).get(to_currency):
|
||||
return rate
|
||||
elif rate := PEGGED_CURRENCIES.get(to_currency, {}).get(from_currency):
|
||||
return 1 / rate
|
||||
def get_pegged_currencies():
|
||||
pegged_currencies = frappe.get_all(
|
||||
"Pegged Currency Details",
|
||||
filters={"parent": "Pegged Currencies"},
|
||||
fields=["source_currency", "pegged_against", "pegged_exchange_rate"],
|
||||
)
|
||||
|
||||
pegged_map = {
|
||||
currency.source_currency: {
|
||||
"pegged_against": currency.pegged_against,
|
||||
"ratio": flt(currency.pegged_exchange_rate),
|
||||
}
|
||||
for currency in pegged_currencies
|
||||
}
|
||||
return pegged_map
|
||||
|
||||
|
||||
def get_pegged_rate(pegged_map, from_currency, to_currency, transaction_date=None):
|
||||
from_entry = pegged_map.get(from_currency)
|
||||
to_entry = pegged_map.get(to_currency)
|
||||
|
||||
if from_currency in pegged_map and to_currency in pegged_map:
|
||||
# Case 1: Both are present and pegged to same bases
|
||||
if from_entry["pegged_against"] == to_entry["pegged_against"]:
|
||||
return (1 / from_entry["ratio"]) * to_entry["ratio"]
|
||||
|
||||
# Case 2: Both are present but pegged to different bases
|
||||
base_from = from_entry["pegged_against"]
|
||||
base_to = to_entry["pegged_against"]
|
||||
base_rate = get_exchange_rate(base_from, base_to, transaction_date)
|
||||
|
||||
if not base_rate:
|
||||
return None
|
||||
|
||||
return (1 / from_entry["ratio"]) * base_rate * to_entry["ratio"]
|
||||
|
||||
# Case 3: from_currency is pegged to to_currency
|
||||
if from_entry and from_entry["pegged_against"] == to_currency:
|
||||
return flt(from_entry["ratio"])
|
||||
|
||||
# Case 4: to_currency is pegged to from_currency
|
||||
if to_entry and to_entry["pegged_against"] == from_currency:
|
||||
return 1 / flt(to_entry["ratio"])
|
||||
|
||||
""" If only one entry exists but doesn’t match pegged currency logic, return None """
|
||||
return None
|
||||
|
||||
|
||||
@@ -95,8 +131,12 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"):
|
||||
return 0.00
|
||||
|
||||
if rate := get_pegged_rate(from_currency, to_currency, transaction_date):
|
||||
return rate
|
||||
pegged_currencies = {}
|
||||
|
||||
if currency_settings.allow_pegged_currencies_exchange_rates:
|
||||
pegged_currencies = get_pegged_currencies()
|
||||
if rate := get_pegged_rate(pegged_currencies, from_currency, to_currency, transaction_date):
|
||||
return rate
|
||||
|
||||
try:
|
||||
cache = frappe.cache()
|
||||
@@ -109,8 +149,12 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
settings = frappe.get_cached_doc("Currency Exchange Settings")
|
||||
req_params = {
|
||||
"transaction_date": transaction_date,
|
||||
"from_currency": from_currency if from_currency != "AED" else "USD",
|
||||
"to_currency": to_currency if to_currency != "AED" else "USD",
|
||||
"from_currency": from_currency
|
||||
if from_currency not in pegged_currencies
|
||||
else pegged_currencies[from_currency]["pegged_against"],
|
||||
"to_currency": to_currency
|
||||
if to_currency not in pegged_currencies
|
||||
else pegged_currencies[to_currency]["pegged_against"],
|
||||
}
|
||||
params = {}
|
||||
for row in settings.req_params:
|
||||
@@ -123,12 +167,13 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
value = value[format_ces_api(str(res_key.key), req_params)]
|
||||
cache.setex(name=key, time=21600, value=flt(value))
|
||||
|
||||
# Support AED conversion through pegged USD
|
||||
# Support multiple pegged currencies
|
||||
value = flt(value)
|
||||
if to_currency == "AED":
|
||||
value *= 3.6725
|
||||
if from_currency == "AED":
|
||||
value /= 3.6725
|
||||
|
||||
if currency_settings.allow_pegged_currencies_exchange_rates and to_currency in pegged_currencies:
|
||||
value *= flt(pegged_currencies[to_currency]["ratio"])
|
||||
if currency_settings.allow_pegged_currencies_exchange_rates and from_currency in pegged_currencies:
|
||||
value /= flt(pegged_currencies[from_currency]["ratio"])
|
||||
|
||||
return flt(value)
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user