mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-27 17:04:47 +00:00
Merge pull request #48100 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
"role_to_override_stop_action",
|
"role_to_override_stop_action",
|
||||||
"currency_exchange_section",
|
"currency_exchange_section",
|
||||||
"allow_stale",
|
"allow_stale",
|
||||||
|
"allow_pegged_currencies_exchange_rates",
|
||||||
"column_break_yuug",
|
"column_break_yuug",
|
||||||
"stale_days",
|
"stale_days",
|
||||||
"section_break_jpd0",
|
"section_break_jpd0",
|
||||||
@@ -592,6 +593,13 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_feyo",
|
"fieldname": "column_break_feyo",
|
||||||
"fieldtype": "Column Break"
|
"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"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -599,7 +607,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-05-05 12:29:38.302027",
|
"modified": "2025-06-16 16:40:54.871486",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AccountsSettings(Document):
|
|||||||
acc_frozen_upto: DF.Date | None
|
acc_frozen_upto: DF.Date | None
|
||||||
add_taxes_from_item_tax_template: DF.Check
|
add_taxes_from_item_tax_template: DF.Check
|
||||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||||
|
allow_pegged_currencies_exchange_rates: DF.Check
|
||||||
allow_stale: DF.Check
|
allow_stale: DF.Check
|
||||||
auto_reconcile_payments: DF.Check
|
auto_reconcile_payments: DF.Check
|
||||||
auto_reconciliation_job_trigger: DF.Int
|
auto_reconciliation_job_trigger: DF.Int
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
from rapidfuzz import fuzz, process
|
from rapidfuzz import fuzz, process
|
||||||
|
from rapidfuzz.utils import default_process
|
||||||
|
|
||||||
|
|
||||||
class AutoMatchParty:
|
class AutoMatchParty:
|
||||||
@@ -132,6 +133,7 @@ class AutoMatchbyPartyNameDescription:
|
|||||||
query=self.get(field),
|
query=self.get(field),
|
||||||
choices={row.get("name"): row.get("party_name") for row in names},
|
choices={row.get("name"): row.get("party_name") for row in names},
|
||||||
scorer=fuzz.token_set_ratio,
|
scorer=fuzz.token_set_ratio,
|
||||||
|
processor=default_process,
|
||||||
)
|
)
|
||||||
party_name, skip = self.process_fuzzy_result(result)
|
party_name, skip = self.process_fuzzy_result(result)
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"naming_series",
|
||||||
"budget_against",
|
"budget_against",
|
||||||
"company",
|
"company",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"naming_series",
|
|
||||||
"project",
|
"project",
|
||||||
"fiscal_year",
|
"fiscal_year",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
@@ -195,19 +195,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "naming_series",
|
"fieldname": "naming_series",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Select",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Series",
|
"label": "Series",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
|
"options": "BUDGET-.YYYY.-",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"reqd": 1,
|
||||||
"set_only_once": 1
|
"set_only_once": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-10-10 22:14:36.361509",
|
"modified": "2025-06-16 15:57:13.114981",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Budget",
|
"name": "Budget",
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Budget(Document):
|
|||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
fiscal_year: DF.Link
|
fiscal_year: DF.Link
|
||||||
monthly_distribution: DF.Link | None
|
monthly_distribution: DF.Link | None
|
||||||
naming_series: DF.Data | None
|
naming_series: DF.Literal["BUDGET-.YYYY.-"]
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
@@ -136,9 +136,6 @@ class Budget(Document):
|
|||||||
):
|
):
|
||||||
self.applicable_on_booking_actual_expenses = 1
|
self.applicable_on_booking_actual_expenses = 1
|
||||||
|
|
||||||
def before_naming(self):
|
|
||||||
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
|
|
||||||
|
|
||||||
|
|
||||||
def validate_expense_against_budget(args, expense_amount=0):
|
def validate_expense_against_budget(args, expense_amount=0):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -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,9 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestPeggedCurrencies(FrappeTestCase):
|
||||||
|
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
|
||||||
@@ -258,6 +258,7 @@ class POSInvoiceMergeLog(Document):
|
|||||||
if not found:
|
if not found:
|
||||||
tax.charge_type = "Actual"
|
tax.charge_type = "Actual"
|
||||||
tax.idx = idx
|
tax.idx = idx
|
||||||
|
tax.row_id = None
|
||||||
idx += 1
|
idx += 1
|
||||||
tax.included_in_print_rate = 0
|
tax.included_in_print_rate = 0
|
||||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||||
|
|||||||
@@ -424,6 +424,8 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
|
|||||||
Will first search in party (Customer / Supplier) record, if not found,
|
Will first search in party (Customer / Supplier) record, if not found,
|
||||||
will search in group (Customer Group / Supplier Group),
|
will search in group (Customer Group / Supplier Group),
|
||||||
finally will return default."""
|
finally will return default."""
|
||||||
|
if not party_type:
|
||||||
|
frappe.throw(_("Party Type is mandatory"))
|
||||||
if not company:
|
if not company:
|
||||||
frappe.throw(_("Please select a Company"))
|
frappe.throw(_("Please select a Company"))
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ class ReceivablePayableReport:
|
|||||||
self.filters.report_date = getdate(self.filters.report_date or nowdate())
|
self.filters.report_date = getdate(self.filters.report_date or nowdate())
|
||||||
self.age_as_on = (
|
self.age_as_on = (
|
||||||
getdate(nowdate())
|
getdate(nowdate())
|
||||||
if self.filters.calculate_ageing_with == "Today Date"
|
if "calculate_ageing_with" not in self.filters
|
||||||
|
or self.filters.calculate_ageing_with == "Today Date"
|
||||||
else self.filters.report_date
|
else self.filters.report_date
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,8 @@ def get_gl_entries(filters, accounting_dimensions):
|
|||||||
voucher_type, voucher_subtype, voucher_no, {dimension_fields}
|
voucher_type, voucher_subtype, voucher_no, {dimension_fields}
|
||||||
cost_center, project, {transaction_currency_fields}
|
cost_center, project, {transaction_currency_fields}
|
||||||
against_voucher_type, against_voucher, account_currency,
|
against_voucher_type, against_voucher, account_currency,
|
||||||
against, is_opening, creation {select_fields}
|
against, is_opening, creation {select_fields},
|
||||||
|
transaction_currency
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
where company=%(company)s {get_conditions(filters)}
|
where company=%(company)s {get_conditions(filters)}
|
||||||
{order_by_statement}
|
{order_by_statement}
|
||||||
|
|||||||
@@ -3,12 +3,15 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import flt, today
|
from frappe.utils import flt, today
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
|
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||||
|
|
||||||
|
|
||||||
class TestGeneralLedger(FrappeTestCase):
|
class TestGeneralLedger(FrappeTestCase):
|
||||||
@@ -168,6 +171,90 @@ class TestGeneralLedger(FrappeTestCase):
|
|||||||
self.assertEqual(data[3]["debit"], 100)
|
self.assertEqual(data[3]["debit"], 100)
|
||||||
self.assertEqual(data[3]["credit"], 100)
|
self.assertEqual(data[3]["credit"], 100)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
|
||||||
|
def test_debit_in_exchange_gain_loss_account(self):
|
||||||
|
company = "_Test Company"
|
||||||
|
|
||||||
|
exchange_gain_loss_account = frappe.db.get_value("Company", "exchange_gain_loss_account")
|
||||||
|
if not exchange_gain_loss_account:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company", company, "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||||
|
)
|
||||||
|
|
||||||
|
account_name = "_Test Receivable USD - _TC"
|
||||||
|
customer_name = "_Test Customer USD"
|
||||||
|
|
||||||
|
sales_invoice = create_sales_invoice(
|
||||||
|
company=company,
|
||||||
|
customer=customer_name,
|
||||||
|
currency="USD",
|
||||||
|
debit_to=account_name,
|
||||||
|
conversion_rate=85,
|
||||||
|
posting_date=today(),
|
||||||
|
)
|
||||||
|
|
||||||
|
payment_entry = create_payment_entry(
|
||||||
|
company=company,
|
||||||
|
party_type="Customer",
|
||||||
|
party=customer_name,
|
||||||
|
payment_type="Receive",
|
||||||
|
paid_from=account_name,
|
||||||
|
paid_from_account_currency="USD",
|
||||||
|
paid_to="Cash - _TC",
|
||||||
|
paid_to_account_currency="INR",
|
||||||
|
paid_amount=10,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
payment_entry.base_paid_amount = 800
|
||||||
|
payment_entry.received_amount = 800
|
||||||
|
payment_entry.currency = "USD"
|
||||||
|
payment_entry.source_exchange_rate = 80
|
||||||
|
payment_entry.append(
|
||||||
|
"references",
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"reference_doctype": "Sales Invoice",
|
||||||
|
"reference_name": sales_invoice.name,
|
||||||
|
"total_amount": 10,
|
||||||
|
"outstanding_amount": 10,
|
||||||
|
"exchange_rate": 85,
|
||||||
|
"allocated_amount": 10,
|
||||||
|
"exchange_gain_loss": -50,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
payment_entry.save()
|
||||||
|
payment_entry.submit()
|
||||||
|
|
||||||
|
journal_entry = frappe.get_all(
|
||||||
|
"Journal Entry Account", filters={"reference_name": sales_invoice.name}, fields=["parent"]
|
||||||
|
)
|
||||||
|
|
||||||
|
columns, data = execute(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"company": company,
|
||||||
|
"from_date": today(),
|
||||||
|
"to_date": today(),
|
||||||
|
"include_dimensions": 1,
|
||||||
|
"include_default_book_entries": 1,
|
||||||
|
"account": ["_Test Exchange Gain/Loss - _TC"],
|
||||||
|
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = data[1]
|
||||||
|
self.assertEqual(entry["debit"], 50)
|
||||||
|
self.assertEqual(entry["voucher_type"], "Journal Entry")
|
||||||
|
self.assertEqual(entry["voucher_no"], journal_entry[0]["parent"])
|
||||||
|
|
||||||
|
payment_entry.cancel()
|
||||||
|
payment_entry.delete()
|
||||||
|
sales_invoice.reload()
|
||||||
|
sales_invoice.cancel()
|
||||||
|
sales_invoice.delete()
|
||||||
|
|
||||||
def test_ignore_exchange_rate_journals_filter(self):
|
def test_ignore_exchange_rate_journals_filter(self):
|
||||||
# create a new account with USD currency
|
# create a new account with USD currency
|
||||||
account_name = "Test Debtors USD"
|
account_name = "Test Debtors USD"
|
||||||
|
|||||||
@@ -101,13 +101,18 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
|||||||
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
|
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
|
||||||
|
|
||||||
for entry in gl_entries:
|
for entry in gl_entries:
|
||||||
|
transaction_currency = entry.get("transaction_currency")
|
||||||
debit = flt(entry["debit"])
|
debit = flt(entry["debit"])
|
||||||
credit = flt(entry["credit"])
|
credit = flt(entry["credit"])
|
||||||
debit_in_account_currency = flt(entry["debit_in_account_currency"])
|
debit_in_account_currency = flt(entry["debit_in_account_currency"])
|
||||||
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
||||||
account_currency = entry["account_currency"]
|
account_currency = entry["account_currency"]
|
||||||
|
|
||||||
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
if (
|
||||||
|
len(account_currencies) == 1
|
||||||
|
and account_currency == presentation_currency
|
||||||
|
and (transaction_currency is None or account_currency == transaction_currency)
|
||||||
|
):
|
||||||
entry["debit"] = debit_in_account_currency
|
entry["debit"] = debit_in_account_currency
|
||||||
entry["credit"] = credit_in_account_currency
|
entry["credit"] = credit_in_account_currency
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ frappe.ui.form.on("Asset", {
|
|||||||
filters: { item_code: doc.item_code },
|
filters: { item_code: doc.item_code },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (frm.doc.docstatus == 1) {
|
||||||
|
frm.custom_make_buttons = {
|
||||||
|
"Asset Capitalization": "Asset Capitalization",
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ class AssetValueAdjustment(Document):
|
|||||||
)
|
)
|
||||||
asset.flags.ignore_validate_update_after_submit = True
|
asset.flags.ignore_validate_update_after_submit = True
|
||||||
asset.save()
|
asset.save()
|
||||||
|
asset.set_status()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
doctype = "Batch"
|
doctype = "Batch"
|
||||||
meta = frappe.get_meta(doctype, cached=True)
|
meta = frappe.get_meta(doctype, cached=True)
|
||||||
searchfields = meta.get_search_fields()
|
searchfields = meta.get_search_fields()
|
||||||
page_len = 30
|
page_len = 300
|
||||||
|
|
||||||
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
|
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
|
||||||
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))
|
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
"fieldname": "quantity",
|
"fieldname": "quantity",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Quantity",
|
"label": "Quantity",
|
||||||
|
"non_negative": 1,
|
||||||
"oldfieldname": "quantity",
|
"oldfieldname": "quantity",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
@@ -637,7 +638,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-06-03 16:24:47.518411",
|
"modified": "2025-06-16 16:13:22.497695",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM",
|
"name": "BOM",
|
||||||
@@ -670,6 +671,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"search_fields": "item, item_name",
|
"search_fields": "item, item_name",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -1184,6 +1184,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
|||||||
item.purchase_uom,
|
item.purchase_uom,
|
||||||
item_uom.conversion_factor,
|
item_uom.conversion_factor,
|
||||||
item.safety_stock,
|
item.safety_stock,
|
||||||
|
bom.item.as_("main_bom_item"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(bei.docstatus < 2)
|
(bei.docstatus < 2)
|
||||||
@@ -1927,6 +1928,7 @@ def get_raw_materials_of_sub_assembly_items(
|
|||||||
item.purchase_uom,
|
item.purchase_uom,
|
||||||
item_uom.conversion_factor,
|
item_uom.conversion_factor,
|
||||||
item.safety_stock,
|
item.safety_stock,
|
||||||
|
bom.item.as_("main_bom_item"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(bei.docstatus == 1)
|
(bei.docstatus == 1)
|
||||||
|
|||||||
@@ -409,3 +409,4 @@ erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
|
|||||||
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
|
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
|
||||||
erpnext.patches.v14_0.update_full_name_in_contract
|
erpnext.patches.v14_0.update_full_name_in_contract
|
||||||
erpnext.patches.v15_0.drop_sle_indexes
|
erpnext.patches.v15_0.drop_sle_indexes
|
||||||
|
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()
|
||||||
@@ -987,7 +987,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
var party = me.frm.doc[frappe.model.scrub(party_type)];
|
var party = me.frm.doc[frappe.model.scrub(party_type)];
|
||||||
if(party && me.frm.doc.company) {
|
if(party && me.frm.doc.company && (!me.frm.doc.__onload?.load_after_mapping || !me.frm.doc.get(party_account_field))) {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.party.get_party_account",
|
method: "erpnext.accounts.party.get_party_account",
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings
|
|||||||
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
test_dependencies = ["Product Bundle"]
|
test_dependencies = ["Product Bundle"]
|
||||||
|
|
||||||
@@ -861,6 +862,24 @@ class TestQuotation(FrappeTestCase):
|
|||||||
quotation.reload()
|
quotation.reload()
|
||||||
self.assertEqual(quotation.status, "Ordered")
|
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}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
test_records = frappe.get_test_records("Quotation")
|
test_records = frappe.get_test_records("Quotation")
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ def after_install():
|
|||||||
add_app_name()
|
add_app_name()
|
||||||
hide_workspaces()
|
hide_workspaces()
|
||||||
update_roles()
|
update_roles()
|
||||||
|
update_pegged_currencies()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -242,6 +243,27 @@ def create_default_role_profiles():
|
|||||||
role_profile.insert(ignore_permissions=True)
|
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 = {
|
DEFAULT_ROLE_PROFILES = {
|
||||||
"Inventory": [
|
"Inventory": [
|
||||||
"Stock User",
|
"Stock User",
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ from frappe.utils.nestedset import get_root_of
|
|||||||
|
|
||||||
from erpnext import get_default_company
|
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():
|
def before_tests():
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
@@ -49,11 +45,51 @@ def before_tests():
|
|||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
def get_pegged_rate(from_currency: str, to_currency: str, transaction_date) -> float | None:
|
def get_pegged_currencies():
|
||||||
if rate := PEGGED_CURRENCIES.get(from_currency, {}).get(to_currency):
|
pegged_currencies = frappe.get_all(
|
||||||
return rate
|
"Pegged Currency Details",
|
||||||
elif rate := PEGGED_CURRENCIES.get(to_currency, {}).get(from_currency):
|
filters={"parent": "Pegged Currencies"},
|
||||||
return 1 / rate
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -97,8 +133,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"):
|
if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"):
|
||||||
return 0.00
|
return 0.00
|
||||||
|
|
||||||
if rate := get_pegged_rate(from_currency, to_currency, transaction_date):
|
pegged_currencies = {}
|
||||||
return rate
|
|
||||||
|
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:
|
try:
|
||||||
cache = frappe.cache()
|
cache = frappe.cache()
|
||||||
@@ -111,8 +151,12 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
|||||||
settings = frappe.get_cached_doc("Currency Exchange Settings")
|
settings = frappe.get_cached_doc("Currency Exchange Settings")
|
||||||
req_params = {
|
req_params = {
|
||||||
"transaction_date": transaction_date,
|
"transaction_date": transaction_date,
|
||||||
"from_currency": from_currency if from_currency != "AED" else "USD",
|
"from_currency": from_currency
|
||||||
"to_currency": to_currency if to_currency != "AED" else "USD",
|
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 = {}
|
params = {}
|
||||||
for row in settings.req_params:
|
for row in settings.req_params:
|
||||||
@@ -125,12 +169,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)]
|
value = value[format_ces_api(str(res_key.key), req_params)]
|
||||||
cache.setex(name=key, time=21600, value=flt(value))
|
cache.setex(name=key, time=21600, value=flt(value))
|
||||||
|
|
||||||
# Support AED conversion through pegged USD
|
# Support multiple pegged currencies
|
||||||
value = flt(value)
|
value = flt(value)
|
||||||
if to_currency == "AED":
|
|
||||||
value *= 3.6725
|
if currency_settings.allow_pegged_currencies_exchange_rates and to_currency in pegged_currencies:
|
||||||
if from_currency == "AED":
|
value *= flt(pegged_currencies[to_currency]["ratio"])
|
||||||
value /= 3.6725
|
if currency_settings.allow_pegged_currencies_exchange_rates and from_currency in pegged_currencies:
|
||||||
|
value /= flt(pegged_currencies[from_currency]["ratio"])
|
||||||
|
|
||||||
return flt(value)
|
return flt(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -883,7 +883,10 @@ class update_entries_after:
|
|||||||
self.wh_data.valuation_rate
|
self.wh_data.valuation_rate
|
||||||
)
|
)
|
||||||
|
|
||||||
if sle.actual_qty < 0 and self.wh_data.qty_after_transaction != 0:
|
if (
|
||||||
|
sle.actual_qty < 0
|
||||||
|
and flt(self.wh_data.qty_after_transaction, self.flt_precision) != 0
|
||||||
|
):
|
||||||
self.wh_data.valuation_rate = flt(
|
self.wh_data.valuation_rate = flt(
|
||||||
self.wh_data.stock_value, self.currency_precision
|
self.wh_data.stock_value, self.currency_precision
|
||||||
) / flt(self.wh_data.qty_after_transaction, self.flt_precision)
|
) / flt(self.wh_data.qty_after_transaction, self.flt_precision)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ dependencies = [
|
|||||||
"pycountry~=22.3.5",
|
"pycountry~=22.3.5",
|
||||||
"Unidecode~=1.3.6",
|
"Unidecode~=1.3.6",
|
||||||
"barcodenumber~=0.5.0",
|
"barcodenumber~=0.5.0",
|
||||||
"rapidfuzz~=2.15.0",
|
"rapidfuzz~=3.12.2",
|
||||||
"holidays~=0.28",
|
"holidays~=0.28",
|
||||||
|
|
||||||
# integration dependencies
|
# integration dependencies
|
||||||
|
|||||||
Reference in New Issue
Block a user