Merge branch 'develop' into tds-jv

This commit is contained in:
ljain112
2025-11-24 16:47:30 +05:30
471 changed files with 200637 additions and 312508 deletions

View File

@@ -50,6 +50,15 @@ pull_request_rules:
- version-15-hotfix
assignees:
- "{{ author }}"
- name: backport to version-16-beta
conditions:
- label="backport version-16-beta"
actions:
backport:
branches:
- version-16-beta
assignees:
- "{{ author }}"
- name: Automatic merge on CI success and review
conditions:
- status-success=linters

View File

@@ -21,6 +21,7 @@
"account_currency",
"column_break1",
"parent_account",
"account_category",
"account_type",
"tax_rate",
"freeze_account",
@@ -189,13 +190,20 @@
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disable"
},
{
"description": "Used with Financial Report Template",
"fieldname": "account_category",
"fieldtype": "Link",
"label": "Account Category",
"options": "Account Category"
}
],
"icon": "fa fa-money",
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2025-01-22 10:40:35.766017",
"modified": "2025-08-02 06:26:44.657146",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -250,6 +258,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "account_number",
"show_name_in_global_search": 1,
"show_preview_popup": 1,
@@ -257,4 +266,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -31,6 +31,7 @@ class Account(NestedSet):
if TYPE_CHECKING:
from frappe.types import DF
account_category: DF.Link | None
account_currency: DF.Link | None
account_name: DF.Data
account_number: DF.Data | None
@@ -108,6 +109,7 @@ class Account(NestedSet):
self.validate_parent_child_account_type()
self.validate_root_details()
self.validate_account_number()
self.validate_disabled()
self.validate_group_or_ledger()
self.set_root_and_report_type()
self.validate_mandatory()
@@ -252,6 +254,14 @@ class Account(NestedSet):
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
def validate_disabled(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save or cint(doc_before_save.disabled) == cint(self.disabled):
return
if cint(self.disabled):
self.validate_default_accounts_in_company()
def validate_group_or_ledger(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save or cint(doc_before_save.is_group) == cint(self.is_group):
@@ -262,9 +272,32 @@ class Account(NestedSet):
elif cint(self.is_group):
if self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected."))
self.validate_default_accounts_in_company()
elif self.check_if_child_exists():
throw(_("Account with child nodes cannot be set as ledger"))
def validate_default_accounts_in_company(self):
default_account_fields = get_company_default_account_fields()
company_default_accounts = frappe.db.get_value(
"Company", self.company, list(default_account_fields.keys()), as_dict=1
)
msg = _("Account {0} cannot be disabled as it is already set as {1} for {2}.")
if not self.disabled:
msg = _("Account {0} cannot be converted to Group as it is already set as {1} for {2}.")
for d in default_account_fields:
if company_default_accounts.get(d) == self.name:
throw(
msg.format(
frappe.bold(self.name),
frappe.bold(default_account_fields.get(d)),
frappe.bold(self.company),
)
)
def validate_frozen_accounts_modifier(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
@@ -625,3 +658,27 @@ def _ensure_idle_system():
).format(pretty_date(last_gl_update)),
title=_("System In Use"),
)
def get_company_default_account_fields():
return {
"default_bank_account": "Default Bank Account",
"default_cash_account": "Default Cash Account",
"default_receivable_account": "Default Receivable Account",
"default_payable_account": "Default Payable Account",
"default_expense_account": "Default Expense Account",
"default_income_account": "Default Income Account",
"stock_received_but_not_billed": "Stock Received But Not Billed Account",
"stock_adjustment_account": "Stock Adjustment Account",
"write_off_account": "Write Off Account",
"default_discount_account": "Default Payment Discount Account",
"unrealized_profit_loss_account": "Unrealized Profit / Loss Account",
"exchange_gain_loss_account": "Exchange Gain / Loss Account",
"unrealized_exchange_gain_loss_account": "Unrealized Exchange Gain / Loss Account",
"round_off_account": "Round Off Account",
"default_deferred_revenue_account": "Default Deferred Revenue Account",
"default_deferred_expense_account": "Default Deferred Expense Account",
"accumulated_depreciation_account": "Accumulated Depreciation Account",
"depreciation_expense_account": "Depreciation Expense Account",
"disposal_account": "Gain/Loss Account on Asset Disposal",
}

View File

@@ -23,15 +23,7 @@ def create_charts(
if root_account:
root_type = child.get("root_type")
if account_name not in [
"account_name",
"account_number",
"account_type",
"root_type",
"is_group",
"tax_rate",
"account_currency",
]:
if account_name not in get_chart_metadata_fields():
account_number = cstr(child.get("account_number")).strip()
account_name, account_name_in_db = add_suffix_if_duplicate(
account_name, account_number, accounts
@@ -55,6 +47,7 @@ def create_charts(
"report_type": report_type,
"account_number": account_number,
"account_type": child.get("account_type"),
"account_category": child.get("account_category"),
"account_currency": child.get("account_currency")
if custom_chart
else frappe.get_cached_value("Company", company, "default_currency"),
@@ -97,20 +90,7 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
def identify_is_group(child):
if child.get("is_group"):
is_group = child.get("is_group")
elif len(
set(child.keys())
- set(
[
"account_name",
"account_type",
"root_type",
"is_group",
"tax_rate",
"account_number",
"account_currency",
]
)
):
elif len(set(child.keys()) - set(get_chart_metadata_fields())):
is_group = 1
else:
is_group = 0
@@ -253,13 +233,7 @@ def validate_bank_account(coa, bank_account):
def _get_account_names(account_master):
for account_name, child in account_master.items():
if account_name not in [
"account_number",
"account_type",
"root_type",
"is_group",
"tax_rate",
]:
if account_name not in get_chart_metadata_fields():
accounts.append(account_name)
_get_account_names(child)
@@ -284,15 +258,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
"""recursively called to form a parent-child based list of dict from chart template"""
for account_name, child in children.items():
account = {}
if account_name in [
"account_name",
"account_number",
"account_type",
"root_type",
"is_group",
"tax_rate",
"account_currency",
]:
if account_name in get_chart_metadata_fields():
continue
if from_coa_importer:
@@ -310,3 +276,16 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
_import_accounts(chart, None)
return accounts
def get_chart_metadata_fields():
return [
"account_name",
"account_number",
"account_type",
"account_category",
"root_type",
"is_group",
"tax_rate",
"account_currency",
]

View File

@@ -9,103 +9,192 @@ def get():
return {
_("Application of Funds (Assets)"): {
_("Current Assets"): {
_("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}},
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
_("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
_("Accounts Receivable"): {
_("Debtors"): {"account_type": "Receivable", "account_category": "Trade Receivables"}
},
_("Bank Accounts"): {
"account_type": "Bank",
"is_group": 1,
"account_category": "Cash and Cash Equivalents",
},
_("Cash In Hand"): {
_("Cash"): {"account_type": "Cash", "account_category": "Cash and Cash Equivalents"},
"account_type": "Cash",
"account_category": "Cash and Cash Equivalents",
},
_("Loans and Advances (Assets)"): {
_("Employee Advances"): {"account_type": "Payable"},
_("Employee Advances"): {
"account_type": "Payable",
"account_category": "Other Receivables",
},
},
_("Securities and Deposits"): {_("Earnest Money"): {}},
_("Securities and Deposits"): {
_("Earnest Money"): {"account_category": "Other Current Assets"}
},
_("Prepaid Expenses"): {"account_category": "Other Current Assets"},
_("Short-term Investments"): {"account_category": "Short-term Investments"},
_("Stock Assets"): {
_("Stock In Hand"): {"account_type": "Stock"},
_("Stock In Hand"): {"account_type": "Stock", "account_category": "Stock Assets"},
"account_type": "Stock",
"account_category": "Stock Assets",
},
_("Tax Assets"): {"is_group": 1},
_("Tax Assets"): {"is_group": 1, "account_category": "Other Current Assets"},
},
_("Fixed Assets"): {
_("Capital Equipment"): {"account_type": "Fixed Asset"},
_("Electronic Equipment"): {"account_type": "Fixed Asset"},
_("Furniture and Fixtures"): {"account_type": "Fixed Asset"},
_("Office Equipment"): {"account_type": "Fixed Asset"},
_("Plants and Machineries"): {"account_type": "Fixed Asset"},
_("Buildings"): {"account_type": "Fixed Asset"},
_("Software"): {"account_type": "Fixed Asset"},
_("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
_("Capital Equipment"): {
"account_type": "Fixed Asset",
"account_category": "Tangible Assets",
},
_("Electronic Equipment"): {
"account_type": "Fixed Asset",
"account_category": "Tangible Assets",
},
_("Furniture and Fixtures"): {
"account_type": "Fixed Asset",
"account_category": "Tangible Assets",
},
_("Office Equipment"): {"account_type": "Fixed Asset", "account_category": "Tangible Assets"},
_("Plants and Machineries"): {
"account_type": "Fixed Asset",
"account_category": "Tangible Assets",
},
_("Buildings"): {"account_type": "Fixed Asset", "account_category": "Tangible Assets"},
_("Software"): {"account_type": "Fixed Asset", "account_category": "Intangible Assets"},
_("Accumulated Depreciation"): {
"account_type": "Accumulated Depreciation",
"account_category": "Tangible Assets",
},
_("CWIP Account"): {
"account_type": "Capital Work in Progress",
"account_category": "Tangible Assets",
},
},
_("Investments"): {"is_group": 1},
_("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}},
_("Investments"): {"is_group": 1, "account_category": "Long-term Investments"},
_("Temporary Accounts"): {
_("Temporary Opening"): {
"account_type": "Temporary",
"account_category": "Other Non-current Assets",
}
},
"root_type": "Asset",
},
_("Expenses"): {
_("Direct Expenses"): {
_("Stock Expenses"): {
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"},
_("Expenses Included In Asset Valuation"): {
"account_type": "Expenses Included In Asset Valuation"
_("Cost of Goods Sold"): {
"account_type": "Cost of Goods Sold",
"account_category": "Cost of Goods Sold",
},
_("Expenses Included In Asset Valuation"): {
"account_type": "Expenses Included In Asset Valuation",
"account_category": "Other Direct Costs",
},
_("Expenses Included In Valuation"): {
"account_type": "Expenses Included In Valuation",
"account_category": "Other Direct Costs",
},
_("Stock Adjustment"): {
"account_type": "Stock Adjustment",
"account_category": "Other Direct Costs",
},
_("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"},
_("Stock Adjustment"): {"account_type": "Stock Adjustment"},
},
},
_("Indirect Expenses"): {
_("Administrative Expenses"): {},
_("Commission on Sales"): {},
_("Depreciation"): {"account_type": "Depreciation"},
_("Entertainment Expenses"): {},
_("Freight and Forwarding Charges"): {"account_type": "Chargeable"},
_("Legal Expenses"): {},
_("Marketing Expenses"): {"account_type": "Chargeable"},
_("Miscellaneous Expenses"): {"account_type": "Chargeable"},
_("Office Maintenance Expenses"): {},
_("Office Rent"): {},
_("Postal Expenses"): {},
_("Print and Stationery"): {},
_("Round Off"): {"account_type": "Round Off"},
_("Salary"): {},
_("Sales Expenses"): {},
_("Telephone Expenses"): {},
_("Travel Expenses"): {},
_("Utility Expenses"): {},
_("Write Off"): {},
_("Exchange Gain/Loss"): {},
_("Gain/Loss on Asset Disposal"): {},
_("Impairment"): {},
_("Administrative Expenses"): {"account_category": "Operating Expenses"},
_("Commission on Sales"): {"account_category": "Operating Expenses"},
_("Depreciation"): {"account_type": "Depreciation", "account_category": "Operating Expenses"},
_("Entertainment Expenses"): {"account_category": "Operating Expenses"},
_("Freight and Forwarding Charges"): {
"account_type": "Chargeable",
"account_category": "Operating Expenses",
},
_("Legal Expenses"): {"account_category": "Operating Expenses"},
_("Marketing Expenses"): {
"account_type": "Chargeable",
"account_category": "Operating Expenses",
},
_("Miscellaneous Expenses"): {
"account_type": "Chargeable",
"account_category": "Operating Expenses",
},
_("Office Maintenance Expenses"): {"account_category": "Operating Expenses"},
_("Office Rent"): {"account_category": "Operating Expenses"},
_("Postal Expenses"): {"account_category": "Operating Expenses"},
_("Print and Stationery"): {"account_category": "Operating Expenses"},
_("Round Off"): {"account_type": "Round Off", "account_category": "Operating Expenses"},
_("Salary"): {"account_category": "Operating Expenses"},
_("Sales Expenses"): {"account_category": "Operating Expenses"},
_("Telephone Expenses"): {"account_category": "Operating Expenses"},
_("Travel Expenses"): {"account_category": "Operating Expenses"},
_("Utility Expenses"): {"account_category": "Operating Expenses"},
_("Write Off"): {"account_category": "Operating Expenses"},
_("Exchange Gain/Loss"): {"account_category": "Operating Expenses"},
_("Interest Expense"): {"account_category": "Finance Costs"},
_("Bank Charges"): {"account_category": "Finance Costs"},
_("Gain/Loss on Asset Disposal"): {"account_category": "Other Operating Income"},
_("Impairment"): {"account_category": "Operating Expenses"},
_("Tax Expense"): {"account_category": "Tax Expense"},
},
"root_type": "Expense",
},
_("Income"): {
_("Direct Income"): {_("Sales"): {}, _("Service"): {}},
_("Indirect Income"): {"is_group": 1},
_("Direct Income"): {
_("Sales"): {"account_category": "Revenue from Operations"},
_("Service"): {"account_category": "Revenue from Operations"},
},
_("Indirect Income"): {
_("Interest Income"): {"account_category": "Investment Income"},
_("Interest on Fixed Deposits"): {"account_category": "Investment Income"},
"is_group": 1,
},
"root_type": "Income",
},
_("Source of Funds (Liabilities)"): {
_("Current Liabilities"): {
_("Accounts Payable"): {
_("Creditors"): {"account_type": "Payable"},
_("Payroll Payable"): {},
_("Creditors"): {"account_type": "Payable", "account_category": "Trade Payables"},
_("Payroll Payable"): {"account_category": "Other Payables"},
},
_("Accrued Expenses"): {"account_category": "Other Current Liabilities"},
_("Customer Advances"): {"account_category": "Other Current Liabilities"},
_("Stock Liabilities"): {
_("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"},
_("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"},
_("Stock Received But Not Billed"): {
"account_type": "Stock Received But Not Billed",
"account_category": "Trade Payables",
},
_("Asset Received But Not Billed"): {
"account_type": "Asset Received But Not Billed",
"account_category": "Trade Payables",
},
},
_("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
_("Duties and Taxes"): {
"account_type": "Tax",
"is_group": 1,
"account_category": "Current Tax Liabilities",
},
_("Short-term Provisions"): {"account_category": "Short-term Provisions"},
_("Loans (Liabilities)"): {
_("Secured Loans"): {},
_("Unsecured Loans"): {},
_("Bank Overdraft Account"): {},
_("Secured Loans"): {"account_category": "Long-term Borrowings"},
_("Unsecured Loans"): {"account_category": "Long-term Borrowings"},
_("Bank Overdraft Account"): {"account_category": "Short-term Borrowings"},
},
},
_("Non-Current Liabilities"): {
_("Long-term Provisions"): {"account_category": "Long-term Provisions"},
_("Employee Benefits Obligation"): {"account_category": "Other Non-current Liabilities"},
"is_group": 1,
},
"root_type": "Liability",
},
_("Equity"): {
_("Capital Stock"): {"account_type": "Equity"},
_("Dividends Paid"): {"account_type": "Equity"},
_("Opening Balance Equity"): {"account_type": "Equity"},
_("Retained Earnings"): {"account_type": "Equity"},
_("Revaluation Surplus"): {"account_type": "Equity"},
_("Capital Stock"): {"account_type": "Equity", "account_category": "Share Capital"},
_("Dividends Paid"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
_("Opening Balance Equity"): {
"account_type": "Equity",
"account_category": "Reserves and Surplus",
},
_("Retained Earnings"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
_("Revaluation Surplus"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
"root_type": "Equity",
},
}

View File

@@ -10,49 +10,128 @@ def get():
_("Application of Funds (Assets)"): {
_("Current Assets"): {
_("Accounts Receivable"): {
_("Debtors"): {"account_type": "Receivable", "account_number": "1310"},
_("Debtors"): {
"account_type": "Receivable",
"account_number": "1310",
"account_category": "Trade Receivables",
},
"account_number": "1300",
},
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"},
_("Bank Accounts"): {
"account_type": "Bank",
"is_group": 1,
"account_number": "1200",
"account_category": "Cash and Cash Equivalents",
},
_("Cash In Hand"): {
_("Cash"): {"account_type": "Cash", "account_number": "1110"},
_("Cash"): {
"account_type": "Cash",
"account_number": "1110",
"account_category": "Cash and Cash Equivalents",
},
"account_type": "Cash",
"account_number": "1100",
"account_category": "Cash and Cash Equivalents",
},
_("Loans and Advances (Assets)"): {
_("Employee Advances"): {"account_number": "1610", "account_type": "Payable"},
_("Employee Advances"): {
"account_number": "1610",
"account_type": "Payable",
"account_category": "Other Receivables",
},
"account_number": "1600",
},
_("Securities and Deposits"): {
_("Earnest Money"): {"account_number": "1651"},
_("Earnest Money"): {
"account_number": "1651",
"account_category": "Other Current Assets",
},
"account_number": "1650",
},
_("Prepaid Expenses"): {
"account_number": "1660",
"account_category": "Other Current Assets",
},
_("Short-term Investments"): {
"account_number": "1670",
"account_category": "Short-term Investments",
},
_("Stock Assets"): {
_("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"},
_("Stock In Hand"): {
"account_type": "Stock",
"account_number": "1410",
"account_category": "Stock Assets",
},
"account_type": "Stock",
"account_number": "1400",
"account_category": "Stock Assets",
},
_("Tax Assets"): {
"is_group": 1,
"account_number": "1500",
"account_category": "Other Current Assets",
},
_("Tax Assets"): {"is_group": 1, "account_number": "1500"},
"account_number": "1100-1600",
},
_("Fixed Assets"): {
_("Capital Equipment"): {"account_type": "Fixed Asset", "account_number": "1710"},
_("Electronic Equipment"): {"account_type": "Fixed Asset", "account_number": "1720"},
_("Furniture and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
_("Office Equipment"): {"account_type": "Fixed Asset", "account_number": "1740"},
_("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
_("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
_("Software"): {"account_type": "Fixed Asset", "account_number": "1770"},
_("Capital Equipment"): {
"account_type": "Fixed Asset",
"account_number": "1710",
"account_category": "Tangible Assets",
},
_("Electronic Equipment"): {
"account_type": "Fixed Asset",
"account_number": "1720",
"account_category": "Tangible Assets",
},
_("Furniture and Fixtures"): {
"account_type": "Fixed Asset",
"account_number": "1730",
"account_category": "Tangible Assets",
},
_("Office Equipment"): {
"account_type": "Fixed Asset",
"account_number": "1740",
"account_category": "Tangible Assets",
},
_("Plants and Machineries"): {
"account_type": "Fixed Asset",
"account_number": "1750",
"account_category": "Tangible Assets",
},
_("Buildings"): {
"account_type": "Fixed Asset",
"account_number": "1760",
"account_category": "Tangible Assets",
},
_("Software"): {
"account_type": "Fixed Asset",
"account_number": "1770",
"account_category": "Intangible Assets",
},
_("Accumulated Depreciation"): {
"account_type": "Accumulated Depreciation",
"account_number": "1780",
"account_category": "Tangible Assets",
},
_("CWIP Account"): {
"account_type": "Capital Work in Progress",
"account_number": "1790",
"account_category": "Tangible Assets",
},
_("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"},
"account_number": "1700",
},
_("Investments"): {"is_group": 1, "account_number": "1800"},
_("Investments"): {
"is_group": 1,
"account_number": "1800",
"account_category": "Long-term Investments",
},
_("Temporary Accounts"): {
_("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"},
_("Temporary Opening"): {
"account_type": "Temporary",
"account_number": "1910",
"account_category": "Other Non-current Assets",
},
"account_number": "1900",
},
"root_type": "Asset",
@@ -61,97 +140,220 @@ def get():
_("Expenses"): {
_("Direct Expenses"): {
_("Stock Expenses"): {
_("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"},
_("Cost of Goods Sold"): {
"account_type": "Cost of Goods Sold",
"account_number": "5111",
"account_category": "Cost of Goods Sold",
},
_("Expenses Included In Asset Valuation"): {
"account_type": "Expenses Included In Asset Valuation",
"account_number": "5112",
"account_category": "Other Direct Costs",
},
_("Expenses Included In Valuation"): {
"account_type": "Expenses Included In Valuation",
"account_number": "5118",
"account_category": "Other Direct Costs",
},
_("Stock Adjustment"): {
"account_type": "Stock Adjustment",
"account_number": "5119",
"account_category": "Other Direct Costs",
},
_("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"},
"account_number": "5110",
},
"account_number": "5100",
},
_("Indirect Expenses"): {
_("Administrative Expenses"): {"account_number": "5201"},
_("Commission on Sales"): {"account_number": "5202"},
_("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"},
_("Entertainment Expenses"): {"account_number": "5204"},
_("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"},
_("Legal Expenses"): {"account_number": "5206"},
_("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"},
_("Office Maintenance Expenses"): {"account_number": "5208"},
_("Office Rent"): {"account_number": "5209"},
_("Postal Expenses"): {"account_number": "5210"},
_("Print and Stationery"): {"account_number": "5211"},
_("Round Off"): {"account_type": "Round Off", "account_number": "5212"},
_("Salary"): {"account_number": "5213"},
_("Sales Expenses"): {"account_number": "5214"},
_("Telephone Expenses"): {"account_number": "5215"},
_("Travel Expenses"): {"account_number": "5216"},
_("Utility Expenses"): {"account_number": "5217"},
_("Write Off"): {"account_number": "5218"},
_("Exchange Gain/Loss"): {"account_number": "5219"},
_("Gain/Loss on Asset Disposal"): {"account_number": "5220"},
_("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"},
"account_number": "5200",
_("Administrative Expenses"): {
"account_number": "5201",
"account_category": "Operating Expenses",
},
_("Commission on Sales"): {
"account_number": "5202",
"account_category": "Operating Expenses",
},
_("Depreciation"): {
"account_type": "Depreciation",
"account_number": "5203",
"account_category": "Operating Expenses",
},
_("Entertainment Expenses"): {
"account_number": "5204",
"account_category": "Operating Expenses",
},
_("Freight and Forwarding Charges"): {
"account_type": "Chargeable",
"account_number": "5205",
"account_category": "Operating Expenses",
},
_("Legal Expenses"): {"account_number": "5206", "account_category": "Operating Expenses"},
_("Marketing Expenses"): {
"account_type": "Chargeable",
"account_number": "5207",
"account_category": "Operating Expenses",
},
_("Office Maintenance Expenses"): {
"account_number": "5208",
"account_category": "Operating Expenses",
},
_("Office Rent"): {"account_number": "5209", "account_category": "Operating Expenses"},
_("Postal Expenses"): {"account_number": "5210", "account_category": "Operating Expenses"},
_("Print and Stationery"): {
"account_number": "5211",
"account_category": "Operating Expenses",
},
_("Round Off"): {
"account_type": "Round Off",
"account_number": "5212",
"account_category": "Operating Expenses",
},
_("Salary"): {"account_number": "5213", "account_category": "Operating Expenses"},
_("Sales Expenses"): {"account_number": "5214", "account_category": "Operating Expenses"},
_("Telephone Expenses"): {"account_number": "5215", "account_category": "Operating Expenses"},
_("Travel Expenses"): {"account_number": "5216", "account_category": "Operating Expenses"},
_("Utility Expenses"): {"account_number": "5217", "account_category": "Operating Expenses"},
_("Write Off"): {"account_number": "5218", "account_category": "Operating Expenses"},
_("Exchange Gain/Loss"): {"account_number": "5219", "account_category": "Operating Expenses"},
_("Interest Expense"): {"account_number": "5220", "account_category": "Finance Costs"},
_("Bank Charges"): {"account_number": "5221", "account_category": "Finance Costs"},
_("Gain/Loss on Asset Disposal"): {
"account_number": "5222",
"account_category": "Other Operating Income",
},
_("Miscellaneous Expenses"): {
"account_type": "Chargeable",
"account_number": "5223",
"account_category": "Operating Expenses",
},
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
},
"root_type": "Expense",
"account_number": "5000",
},
_("Income"): {
_("Direct Income"): {
_("Sales"): {"account_number": "4110"},
_("Service"): {"account_number": "4120"},
_("Sales"): {"account_number": "4110", "account_category": "Revenue from Operations"},
_("Service"): {"account_number": "4120", "account_category": "Revenue from Operations"},
"account_number": "4100",
},
_("Indirect Income"): {"is_group": 1, "account_number": "4200"},
_("Indirect Income"): {
_("Interest Income"): {"account_number": "4210", "account_category": "Investment Income"},
_("Interest on Fixed Deposits"): {
"account_number": "4220",
"account_category": "Investment Income",
},
"is_group": 1,
"account_number": "4200",
},
"root_type": "Income",
"account_number": "4000",
},
_("Source of Funds (Liabilities)"): {
_("Current Liabilities"): {
_("Accounts Payable"): {
_("Creditors"): {"account_type": "Payable", "account_number": "2110"},
_("Payroll Payable"): {"account_number": "2120"},
_("Creditors"): {
"account_type": "Payable",
"account_number": "2110",
"account_category": "Trade Payables",
},
_("Payroll Payable"): {"account_number": "2120", "account_category": "Other Payables"},
"account_number": "2100",
},
_("Accrued Expenses"): {
"account_number": "2150",
"account_category": "Other Current Liabilities",
},
_("Customer Advances"): {
"account_number": "2160",
"account_category": "Other Current Liabilities",
},
_("Stock Liabilities"): {
_("Stock Received But Not Billed"): {
"account_type": "Stock Received But Not Billed",
"account_number": "2210",
"account_category": "Trade Payables",
},
_("Asset Received But Not Billed"): {
"account_type": "Asset Received But Not Billed",
"account_number": "2211",
"account_category": "Trade Payables",
},
"account_number": "2200",
},
_("Duties and Taxes"): {
_("TDS Payable"): {"account_number": "2310"},
_("TDS Payable"): {
"account_number": "2310",
"account_category": "Current Tax Liabilities",
},
"account_type": "Tax",
"is_group": 1,
"account_number": "2300",
"account_category": "Current Tax Liabilities",
},
_("Short-term Provisions"): {
"account_number": "2350",
"account_category": "Short-term Provisions",
},
_("Loans (Liabilities)"): {
_("Secured Loans"): {"account_number": "2410"},
_("Unsecured Loans"): {"account_number": "2420"},
_("Bank Overdraft Account"): {"account_number": "2430"},
_("Secured Loans"): {
"account_number": "2410",
"account_category": "Long-term Borrowings",
},
_("Unsecured Loans"): {
"account_number": "2420",
"account_category": "Long-term Borrowings",
},
_("Bank Overdraft Account"): {
"account_number": "2430",
"account_category": "Short-term Borrowings",
},
"account_number": "2400",
},
"account_number": "2100-2400",
},
_("Non-Current Liabilities"): {
_("Long-term Provisions"): {
"account_number": "2510",
"account_category": "Long-term Provisions",
},
_("Employee Benefits Obligation"): {
"account_number": "2520",
"account_category": "Other Non-current Liabilities",
},
"is_group": 1,
"account_number": "2500",
},
"root_type": "Liability",
"account_number": "2000",
},
_("Equity"): {
_("Capital Stock"): {"account_type": "Equity", "account_number": "3100"},
_("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"},
_("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"},
_("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"},
_("Capital Stock"): {
"account_type": "Equity",
"account_number": "3100",
"account_category": "Share Capital",
},
_("Dividends Paid"): {
"account_type": "Equity",
"account_number": "3200",
"account_category": "Reserves and Surplus",
},
_("Opening Balance Equity"): {
"account_type": "Equity",
"account_number": "3300",
"account_category": "Reserves and Surplus",
},
_("Retained Earnings"): {
"account_type": "Equity",
"account_number": "3400",
"account_category": "Reserves and Surplus",
},
_("Revaluation Surplus"): {
"account_type": "Equity",
"account_number": "3500",
"account_category": "Reserves and Surplus",
},
"root_type": "Equity",
"account_number": "3000",
},

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Account Category", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,71 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:account_category_name",
"creation": "2025-08-02 06:22:31.835063",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"account_category_name",
"description"
],
"fields": [
{
"fieldname": "account_category_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Account Category Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-10-15 03:19:47.171349",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Category",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "Auditor"
}
],
"row_format": "Dynamic",
"search_fields": "account_category_name, description",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,94 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import os
import frappe
from frappe import _
from frappe.model.document import Document, bulk_insert
DOCTYPE = "Account Category"
class AccountCategory(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
account_category_name: DF.Data
description: DF.SmallText | None
# end: auto-generated types
def after_rename(self, old_name, new_name, merge):
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FormulaFieldUpdater,
)
# get all template rows with this account category being used
row = frappe.qb.DocType("Financial Report Row")
rows = frappe._dict(
frappe.qb.from_(row)
.select(row.name, row.calculation_formula)
.where(row.calculation_formula.like(f"%{old_name}%"))
.run()
)
if not rows:
return
# Update formulas with new name
updater = FormulaFieldUpdater(
field_name="account_category",
value_mapping={old_name: new_name},
exclude_operators=["like", "not like"],
)
updated_formulas = updater.update_in_rows(rows)
if updated_formulas:
frappe.msgprint(
_("Updated {0} Financial Report Row(s) with new category name").format(len(updated_formulas))
)
def import_account_categories(template_path: str):
categories_file = os.path.join(template_path, "account_categories.json")
if not os.path.exists(categories_file):
return
with open(categories_file) as f:
categories = json.load(f, object_hook=frappe._dict)
create_account_categories(categories)
def create_account_categories(categories: list[dict]):
if not categories:
return
existing_categories = set(frappe.get_all(DOCTYPE, pluck="name"))
new_categories = []
for category_data in categories:
category_name = category_data.get("account_category_name")
if not category_name or category_name in existing_categories:
continue
doc = frappe.get_doc(
{
**category_data,
"doctype": DOCTYPE,
"name": category_name,
}
)
new_categories.append(doc)
existing_categories.add(category_name)
if new_categories:
bulk_insert(DOCTYPE, new_categories)

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# 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 IntegrationTestAccountCategory(IntegrationTestCase):
"""
Integration tests for AccountCategory.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -309,8 +309,8 @@ def get_dimensions(with_cost_center_and_project=False):
if with_cost_center_and_project:
dimension_filters.extend(
[
{"fieldname": "cost_center", "document_type": "Cost Center"},
{"fieldname": "project", "document_type": "Project"},
frappe._dict({"fieldname": "cost_center", "document_type": "Cost Center"}),
frappe._dict({"fieldname": "project", "document_type": "Project"}),
]
)

View File

@@ -77,6 +77,7 @@
"period_closing_settings_section",
"acc_frozen_upto",
"ignore_account_closing_balance",
"use_legacy_controller_for_pcv",
"column_break_25",
"frozen_accounts_modifier",
"tab_break_dpet",
@@ -651,6 +652,12 @@
"fieldname": "use_legacy_budget_controller",
"fieldtype": "Check",
"label": "Use Legacy Budget Controller"
},
{
"default": "1",
"fieldname": "use_legacy_controller_for_pcv",
"fieldtype": "Check",
"label": "Use Legacy Controller For Period Closing Voucher"
}
],
"grid_page_length": 50,
@@ -659,7 +666,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-09-24 16:08:08.515254",
"modified": "2025-10-20 14:06:08.870427",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -75,6 +75,7 @@ class AccountsSettings(Document):
unlink_advance_payment_on_cancelation_of_order: DF.Check
unlink_payment_on_cancellation_of_invoice: DF.Check
use_legacy_budget_controller: DF.Check
use_legacy_controller_for_pcv: DF.Check
# end: auto-generated types
def validate(self):

View File

@@ -1,8 +1,9 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Advance Payment Ledger Entry", {
// refresh(frm) {
// },
// });
frappe.ui.form.on("Advance Payment Ledger Entry", {
refresh(frm) {
frm.set_currency_labels(["amount"], frm.doc.currency);
frm.set_currency_labels(["base_amount"], erpnext.get_currency(frm.doc.company));
},
});

View File

@@ -10,8 +10,10 @@
"voucher_no",
"against_voucher_type",
"against_voucher_no",
"amount",
"currency",
"exchange_rate",
"amount",
"base_amount",
"event",
"delinked"
],
@@ -76,13 +78,29 @@
"fieldtype": "Check",
"label": "DeLinked",
"read_only": 1
},
{
"depends_on": "base_amount",
"fieldname": "base_amount",
"fieldtype": "Currency",
"label": "Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"depends_on": "exchange_rate",
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"precision": "9",
"read_only": 1
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-10-13 15:11:58.300836",
"modified": "2025-11-13 12:45:03.014555",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Payment Ledger Entry",

View File

@@ -19,10 +19,12 @@ class AdvancePaymentLedgerEntry(Document):
against_voucher_no: DF.DynamicLink | None
against_voucher_type: DF.Link | None
amount: DF.Currency
base_amount: DF.Currency
company: DF.Link | None
currency: DF.Link | None
delinked: DF.Check
event: DF.Data | None
exchange_rate: DF.Float
voucher_no: DF.DynamicLink | None
voucher_type: DF.Link | None
# end: auto-generated types

View File

@@ -9,13 +9,6 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
frappe.ui.form.on("Bank Guarantee", {
setup: function (frm) {
frm.set_query("bank", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_query("bank_account", function () {
return {
filters: {

View File

@@ -14,6 +14,7 @@ import openpyxl
from frappe import _
from frappe.core.doctype.data_import.data_import import DataImport
from frappe.core.doctype.data_import.importer import Importer, ImportFile
from frappe.query_builder.functions import Count
from frappe.utils.background_jobs import enqueue
from frappe.utils.file_manager import get_file, save_file
from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
@@ -371,7 +372,7 @@ def get_import_status(docname):
logs = frappe.get_all(
"Data Import Log",
fields=["count(*) as count", "success"],
fields=[{"COUNT": "*", "as": "count"}, "success"],
filters={"data_import": docname},
group_by="success",
)

View File

@@ -123,8 +123,7 @@
"fieldname": "transaction_id",
"fieldtype": "Data",
"label": "Transaction ID",
"read_only": 1,
"unique": 1
"read_only": 1
},
{
"allow_on_submit": 1,
@@ -239,7 +238,7 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-09-26 17:06:29.207673",
"modified": "2025-10-23 17:32:58.514807",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -4,16 +4,6 @@ frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Budget", {
onload: function (frm) {
frm.set_query("account", "accounts", function () {
return {
filters: {
company: frm.doc.company,
report_type: "Profit and Loss",
is_group: 0,
},
};
});
frm.set_query("monthly_distribution", function () {
return {
filters: {
@@ -30,8 +20,28 @@ frappe.ui.form.on("Budget", {
});
},
refresh: function (frm) {
refresh: async function (frm) {
frm.trigger("toggle_reqd_fields");
if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
let exception_role = await frappe.db.get_value(
"Company",
frm.doc.company,
"exception_budget_approver_role"
);
const role = exception_role.message.exception_budget_approver_role;
if (role && frappe.user.has_role(role)) {
frm.add_custom_button(
__("Revise Budget"),
function () {
frm.events.revise_budget_action(frm);
},
__("Actions")
);
}
}
},
budget_against: function (frm) {
@@ -39,6 +49,15 @@ frappe.ui.form.on("Budget", {
frm.trigger("toggle_reqd_fields");
},
budget_amount(frm) {
if (frm.doc.budget_distribution?.length) {
frm.doc.budget_distribution.forEach((row) => {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
});
frm.refresh_field("budget_distribution");
}
},
set_null_value: function (frm) {
if (frm.doc.budget_against == "Cost Center") {
frm.set_value("project", null);
@@ -51,4 +70,44 @@ frappe.ui.form.on("Budget", {
frm.toggle_reqd("cost_center", frm.doc.budget_against == "Cost Center");
frm.toggle_reqd("project", frm.doc.budget_against == "Project");
},
revise_budget_action: function (frm) {
frappe.confirm(
__(
"Are you sure you want to revise this budget? The current budget will be cancelled and a new draft will be created."
),
function () {
frappe.call({
method: "erpnext.accounts.doctype.budget.budget.revise_budget",
args: { budget_name: frm.doc.name },
callback: function (r) {
if (r.message) {
frappe.msgprint(__("New revised budget created successfully"));
frappe.set_route("Form", "Budget", r.message);
}
},
});
},
function () {
frappe.msgprint(__("Revision cancelled"));
}
);
},
});
frappe.ui.form.on("Budget Distribution", {
amount(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) {
row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2);
frm.refresh_field("budget_distribution");
}
},
percent(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
frm.refresh_field("budget_distribution");
}
},
});

View File

@@ -12,10 +12,19 @@
"company",
"cost_center",
"project",
"fiscal_year",
"account",
"column_break_3",
"monthly_distribution",
"amended_from",
"from_fiscal_year",
"to_fiscal_year",
"budget_start_date",
"budget_end_date",
"distribution_frequency",
"budget_amount",
"section_break_nwug",
"distribute_equally",
"section_break_fpdt",
"budget_distribution",
"section_break_6",
"applicable_on_material_request",
"action_if_annual_budget_exceeded_on_mr",
@@ -32,8 +41,8 @@
"applicable_on_cumulative_expense",
"action_if_annual_exceeded_on_cumulative_expense",
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
"section_break_21",
"accounts"
"section_break_kkan",
"revision_of"
],
"fields": [
{
@@ -44,6 +53,7 @@
"in_standard_filter": 1,
"label": "Budget Against",
"options": "\nCost Center\nProject",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
@@ -53,6 +63,7 @@
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
@@ -62,7 +73,8 @@
"in_global_search": 1,
"in_standard_filter": 1,
"label": "Cost Center",
"options": "Cost Center"
"options": "Cost Center",
"read_only_depends_on": "eval: doc.revision_of"
},
{
"depends_on": "eval:doc.budget_against == 'Project'",
@@ -70,28 +82,13 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Project",
"options": "Project"
},
{
"fieldname": "fiscal_year",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Fiscal Year",
"options": "Fiscal Year",
"reqd": 1
"options": "Project",
"read_only_depends_on": "eval: doc.revision_of"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:in_list([\"Stop\", \"Warn\"], doc.action_if_accumulated_monthly_budget_exceeded_on_po || doc.action_if_accumulated_monthly_budget_exceeded_on_mr || doc.action_if_accumulated_monthly_budget_exceeded_on_actual)",
"fieldname": "monthly_distribution",
"fieldtype": "Link",
"label": "Monthly Distribution",
"options": "Monthly Distribution"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
@@ -187,22 +184,12 @@
"options": "\nStop\nWarn\nIgnore"
},
{
"fieldname": "section_break_21",
"fieldtype": "Section Break"
},
{
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Budget Accounts",
"options": "Budget Account",
"reqd": 1
},
{
"default": "BUDGET-.########",
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"no_copy": 1,
"options": "BUDGET-.YYYY.-",
"options": "BUDGET-.########",
"print_hide": 1,
"reqd": 1,
"set_only_once": 1
@@ -232,13 +219,97 @@
"fieldtype": "Select",
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
"options": "\nStop\nWarn\nIgnore"
},
{
"fieldname": "section_break_fpdt",
"fieldtype": "Section Break"
},
{
"fieldname": "budget_distribution",
"fieldtype": "Table",
"label": "Budget Distribution",
"options": "Budget Distribution"
},
{
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"fieldname": "budget_amount",
"fieldtype": "Currency",
"label": "Budget Amount",
"reqd": 1
},
{
"fieldname": "section_break_kkan",
"fieldtype": "Section Break"
},
{
"fieldname": "revision_of",
"fieldtype": "Data",
"label": "Revision Of",
"no_copy": 1,
"read_only": 1
},
{
"default": "1",
"fieldname": "distribute_equally",
"fieldtype": "Check",
"label": "Distribute Equally"
},
{
"fieldname": "section_break_nwug",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "from_fiscal_year",
"fieldtype": "Link",
"label": "From Fiscal Year",
"options": "Fiscal Year",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"fieldname": "to_fiscal_year",
"fieldtype": "Link",
"label": "To Fiscal Year",
"options": "Fiscal Year",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"fieldname": "budget_start_date",
"fieldtype": "Date",
"hidden": 1,
"label": "Budget Start Date"
},
{
"fieldname": "budget_end_date",
"fieldtype": "Date",
"hidden": 1,
"label": "Budget End Date"
},
{
"default": "Monthly",
"fieldname": "distribution_frequency",
"fieldtype": "Select",
"label": "Distribution Frequency",
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-06-16 15:57:13.114981",
"modified": "2025-11-19 17:00:00.648224",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget",

View File

@@ -2,10 +2,14 @@
# For license information, please see license.txt
from datetime import date
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate
from frappe.query_builder.functions import Sum
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate, month_diff
from frappe.utils.data import get_first_day, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
@@ -30,9 +34,9 @@ class Budget(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.budget_account.budget_account import BudgetAccount
from erpnext.accounts.doctype.budget_distribution.budget_distribution import BudgetDistribution
accounts: DF.Table[BudgetAccount]
account: DF.Link
action_if_accumulated_monthly_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
action_if_accumulated_monthly_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
@@ -47,73 +51,117 @@ class Budget(Document):
applicable_on_material_request: DF.Check
applicable_on_purchase_order: DF.Check
budget_against: DF.Literal["", "Cost Center", "Project"]
budget_amount: DF.Currency
budget_distribution: DF.Table[BudgetDistribution]
budget_end_date: DF.Date | None
budget_start_date: DF.Date | None
company: DF.Link
cost_center: DF.Link | None
fiscal_year: DF.Link
monthly_distribution: DF.Link | None
naming_series: DF.Literal["BUDGET-.YYYY.-"]
distribute_equally: DF.Check
distribution_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"]
from_fiscal_year: DF.Link
naming_series: DF.Literal["BUDGET-.########"]
project: DF.Link | None
revision_of: DF.Data | None
to_fiscal_year: DF.Link
# end: auto-generated types
def validate(self):
if not self.get(frappe.scrub(self.budget_against)):
frappe.throw(_("{0} is mandatory").format(self.budget_against))
self.validate_budget_amount()
self.validate_fiscal_year()
self.set_fiscal_year_dates()
self.validate_duplicate()
self.validate_accounts()
self.validate_account()
self.set_null_value()
self.validate_applicable_for()
self.validate_existing_expenses()
def validate_budget_amount(self):
if self.budget_amount <= 0:
frappe.throw(_("Budget Amount can not be {0}.").format(self.budget_amount))
def validate_fiscal_year(self):
if self.from_fiscal_year:
self.validate_fiscal_year_company(self.from_fiscal_year, self.company)
if self.to_fiscal_year:
self.validate_fiscal_year_company(self.to_fiscal_year, self.company)
def validate_fiscal_year_company(self, fiscal_year, company):
linked_companies = frappe.get_all(
"Fiscal Year Company", filters={"parent": fiscal_year}, pluck="company"
)
if linked_companies and company not in linked_companies:
frappe.throw(_("Fiscal Year {0} is not available for Company {1}.").format(fiscal_year, company))
def set_fiscal_year_dates(self):
if self.from_fiscal_year:
self.budget_start_date = frappe.get_cached_value(
"Fiscal Year", self.from_fiscal_year, "year_start_date"
)
if self.to_fiscal_year:
self.budget_end_date = frappe.get_cached_value(
"Fiscal Year", self.to_fiscal_year, "year_end_date"
)
if self.budget_start_date > self.budget_end_date:
frappe.throw(_("From Fiscal Year cannot be greater than To Fiscal Year"))
def validate_duplicate(self):
budget_against_field = frappe.scrub(self.budget_against)
budget_against = self.get(budget_against_field)
account = self.account
if not account:
return
accounts = [d.account for d in self.accounts] or []
existing_budget = frappe.db.sql(
"""
select
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
where
ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and
b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format(
"%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))
),
(self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)),
as_dict=1,
f"""
SELECT name, account
FROM `tabBudget`
WHERE
docstatus < 2
AND company = %s
AND {budget_against_field} = %s
AND account = %s
AND name != %s
AND (
(SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
AND (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
)
""",
(self.company, budget_against, account, self.name, self.budget_end_date, self.budget_start_date),
as_dict=True,
)
for d in existing_budget:
if existing_budget:
d = existing_budget[0]
frappe.throw(
_(
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}"
).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year),
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' with overlapping fiscal years."
).format(d.name, self.budget_against, budget_against, d.account),
DuplicateBudgetError,
)
def validate_accounts(self):
account_list = []
for d in self.get("accounts"):
if d.account:
account_details = frappe.get_cached_value(
"Account", d.account, ["is_group", "company", "report_type"], as_dict=1
def validate_account(self):
if not self.account:
frappe.throw(_("Account is mandatory"))
account_details = frappe.get_cached_value(
"Account", self.account, ["is_group", "company", "report_type"], as_dict=1
)
if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(self.account))
elif account_details.company != self.company:
frappe.throw(_("Account {0} does not belong to company {1}").format(self.account, self.company))
elif account_details.report_type != "Profit and Loss":
frappe.throw(
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
self.account
)
if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
elif account_details.company != self.company:
frappe.throw(
_("Account {0} does not belongs to company {1}").format(d.account, self.company)
)
elif account_details.report_type != "Profit and Loss":
frappe.throw(
_(
"Budget cannot be assigned against {0}, as it's not an Income or Expense account"
).format(d.account)
)
if d.account in account_list:
frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
else:
account_list.append(d.account)
)
def set_null_value(self):
if self.budget_against == "Cost Center":
@@ -139,30 +187,201 @@ class Budget(Document):
):
self.applicable_on_booking_actual_expenses = 1
def validate_existing_expenses(self):
if self.is_new() and self.revision_of:
return
def validate_expense_against_budget(args, expense_amount=0):
args = frappe._dict(args)
params = frappe._dict(
{
"company": self.company,
"account": self.account,
"budget_start_date": self.budget_start_date,
"budget_end_date": self.budget_end_date,
"budget_against_field": frappe.scrub(self.budget_against),
"budget_against_doctype": frappe.unscrub(self.budget_against),
}
)
params[params.budget_against_field] = self.get(params.budget_against_field)
if frappe.get_cached_value("DocType", params.budget_against_doctype, "is_tree"):
params.is_tree = True
else:
params.is_tree = False
actual_spent = get_actual_expense(params)
if actual_spent > self.budget_amount:
frappe.throw(
_(
"Spending for Account {0} ({1}) between {2} and {3} "
"has already exceeded the new allocated budget. "
"Spent: {4}, Budget: {5}"
).format(
frappe.bold(self.account),
frappe.bold(self.company),
frappe.bold(self.budget_start_date),
frappe.bold(self.budget_end_date),
frappe.bold(frappe.utils.fmt_money(actual_spent)),
frappe.bold(frappe.utils.fmt_money(self.budget_amount)),
),
title=_("Budget Limit Exceeded"),
)
def before_save(self):
self.allocate_budget()
def on_update(self):
self.validate_distribution_totals()
def allocate_budget(self):
if self.revision_of:
return
if not self.should_regenerate_budget_distribution():
return
self.set("budget_distribution", [])
periods = self.get_budget_periods()
total_periods = len(periods)
row_percent = 100 / total_periods if total_periods else 0
for start_date, end_date in periods:
row = self.append("budget_distribution", {})
row.start_date = start_date
row.end_date = end_date
self.add_allocated_amount(row, row_percent)
def should_regenerate_budget_distribution(self):
"""Check whether budget distribution should be recalculated."""
old_doc = self.get_doc_before_save() if not self.is_new() else None
if not old_doc or not self.budget_distribution:
return True
if old_doc:
changed_fields = [
"from_fiscal_year",
"to_fiscal_year",
"budget_amount",
"distribution_frequency",
"distribute_equally",
]
for field in changed_fields:
if old_doc.get(field) != self.get(field):
return True
return bool(self.distribute_equally)
def get_budget_periods(self):
"""Return list of (start_date, end_date) tuples based on frequency."""
frequency = self.distribution_frequency
periods = []
start_date = getdate(self.budget_start_date)
end_date = getdate(self.budget_end_date)
while start_date <= end_date:
period_start = get_first_day(start_date)
period_end = self.get_period_end(period_start, frequency)
period_end = min(period_end, end_date)
periods.append((period_start, period_end))
start_date = add_months(period_start, self.get_month_increment(frequency))
return periods
def get_period_end(self, start_date, frequency):
"""Return the correct end date for a given frequency."""
if frequency == "Monthly":
return get_last_day(start_date)
elif frequency == "Quarterly":
return get_last_day(add_months(start_date, 2))
elif frequency == "Half-Yearly":
return get_last_day(add_months(start_date, 5))
else: # Yearly
return get_last_day(add_months(start_date, 11))
def get_month_increment(self, frequency):
"""Return how many months to move forward for the next period."""
return {
"Monthly": 1,
"Quarterly": 3,
"Half-Yearly": 6,
"Yearly": 12,
}.get(frequency, 1)
def add_allocated_amount(self, row, row_percent):
if not self.distribute_equally:
row.amount = 0
row.percent = 0
else:
row.amount = flt(self.budget_amount * row_percent / 100, 3)
row.percent = flt(row_percent, 3)
def validate_distribution_totals(self):
if self.should_regenerate_budget_distribution():
return
total_amount = sum(d.amount for d in self.budget_distribution)
total_percent = sum(d.percent for d in self.budget_distribution)
if flt(abs(total_amount - self.budget_amount), 2) > 0.10:
frappe.throw(
_("Total distributed amount {0} must be equal to Budget Amount {1}").format(
flt(total_amount, 2), self.budget_amount
)
)
if flt(abs(total_percent - 100), 2) > 0.10:
frappe.throw(
_("Total distribution percent must equal 100 (currently {0})").format(round(total_percent, 2))
)
def validate_expense_against_budget(params, expense_amount=0):
params = frappe._dict(params)
if not frappe.db.count("Budget", cache=True):
return
if not args.fiscal_year:
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
if not params.fiscal_year:
params.fiscal_year = get_fiscal_year(params.get("posting_date"), company=params.get("company"))[0]
if args.get("company"):
frappe.flags.exception_approver_role = frappe.get_cached_value(
"Company", args.get("company"), "exception_budget_approver_role"
)
posting_date = getdate(params.get("posting_date"))
posting_fiscal_year = get_fiscal_year(posting_date, company=params.get("company"))[0]
year_start_date, year_end_date = get_fiscal_year_date_range(posting_fiscal_year, posting_fiscal_year)
if not frappe.db.get_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}):
budget_exists = frappe.db.sql(
"""
select name
from `tabBudget`
where company = %s
and docstatus = 1
and (SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s
and (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s
limit 1
""",
(params.company, year_end_date, year_start_date),
)
if not budget_exists:
return
if not args.account:
args.account = args.get("expense_account")
if params.get("company"):
frappe.flags.exception_approver_role = frappe.get_cached_value(
"Company", params.get("company"), "exception_budget_approver_role"
)
if not (args.get("account") and args.get("cost_center")) and args.item_code:
args.cost_center, args.account = get_item_details(args)
if not params.account:
params.account = params.get("expense_account")
if not args.account:
if not params.get("expense_account") and params.get("account"):
params.expense_account = params.account
if not (params.get("account") and params.get("cost_center")) and params.item_code:
params.cost_center, params.account = get_item_details(params)
if not params.account:
return
default_dimensions = [
@@ -180,59 +399,78 @@ def validate_expense_against_budget(args, expense_amount=0):
budget_against = dimension.get("fieldname")
if (
args.get(budget_against)
and args.account
and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense")
params.get(budget_against)
and params.account
and (frappe.get_cached_value("Account", params.account, "root_type") == "Expense")
):
doctype = dimension.get("document_type")
if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"])
lft, rgt = frappe.get_cached_value(doctype, params.get(budget_against), ["lft", "rgt"])
condition = f"""and exists(select name from `tab{doctype}`
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
args.is_tree = True
params.is_tree = True
else:
condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}"
args.is_tree = False
condition = f"and b.{budget_against}={frappe.db.escape(params.get(budget_against))}"
params.is_tree = False
args.budget_against_field = budget_against
args.budget_against_doctype = doctype
params.budget_against_field = budget_against
params.budget_against_doctype = doctype
budget_records = frappe.db.sql(
f"""
select
b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution,
ifnull(b.applicable_on_material_request, 0) as for_material_request,
ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded,
b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr,
b.action_if_annual_budget_exceeded_on_po, b.action_if_accumulated_monthly_budget_exceeded_on_po
from
`tabBudget` b, `tabBudget Account` ba
where
b.name=ba.parent and b.fiscal_year=%s
and ba.account=%s and b.docstatus=1
SELECT
b.name,
b.{budget_against} AS budget_against,
b.budget_amount,
b.from_fiscal_year,
b.to_fiscal_year,
b.budget_start_date,
b.budget_end_date,
IFNULL(b.applicable_on_material_request, 0) AS for_material_request,
IFNULL(b.applicable_on_purchase_order, 0) AS for_purchase_order,
IFNULL(b.applicable_on_booking_actual_expenses, 0) AS for_actual_expenses,
b.action_if_annual_budget_exceeded,
b.action_if_accumulated_monthly_budget_exceeded,
b.action_if_annual_budget_exceeded_on_mr,
b.action_if_accumulated_monthly_budget_exceeded_on_mr,
b.action_if_annual_budget_exceeded_on_po,
b.action_if_accumulated_monthly_budget_exceeded_on_po
FROM
`tabBudget` b
WHERE
b.company = %s
AND b.docstatus = 1
AND %s BETWEEN b.budget_start_date AND b.budget_end_date
AND b.account = %s
{condition}
""",
(args.fiscal_year, args.account),
""",
(params.company, params.posting_date, params.account),
as_dict=True,
) # nosec
if budget_records:
validate_budget_records(args, budget_records, expense_amount)
validate_budget_records(params, budget_records, expense_amount)
def validate_budget_records(args, budget_records, expense_amount):
def validate_budget_records(params, budget_records, expense_amount):
for budget in budget_records:
if flt(budget.budget_amount):
yearly_action, monthly_action = get_actions(args, budget)
args["for_material_request"] = budget.for_material_request
args["for_purchase_order"] = budget.for_purchase_order
yearly_action, monthly_action = get_actions(params, budget)
params["for_material_request"] = budget.for_material_request
params["for_purchase_order"] = budget.for_purchase_order
params["from_fiscal_year"], params["to_fiscal_year"] = (
budget.from_fiscal_year,
budget.to_fiscal_year,
)
params["budget_start_date"], params["budget_end_date"] = (
budget.budget_start_date,
budget.budget_end_date,
)
if yearly_action in ("Stop", "Warn"):
compare_expense_with_budget(
args,
params,
flt(budget.budget_amount),
_("Annual"),
yearly_action,
@@ -241,14 +479,12 @@ def validate_budget_records(args, budget_records, expense_amount):
)
if monthly_action in ["Stop", "Warn"]:
budget_amount = get_accumulated_monthly_budget(
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
)
budget_amount = get_accumulated_monthly_budget(budget.name, params.posting_date)
args["month_end_date"] = get_last_day(args.posting_date)
params["month_end_date"] = get_last_day(params.posting_date)
compare_expense_with_budget(
args,
params,
budget_amount,
_("Accumulated Monthly"),
monthly_action,
@@ -257,40 +493,41 @@ def validate_budget_records(args, budget_records, expense_amount):
)
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0
def compare_expense_with_budget(params, budget_amount, action_for, action, budget_against, amount=0):
params.actual_expense, params.requested_amount, params.ordered_amount = get_actual_expense(params), 0, 0
if not amount:
args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args)
params.requested_amount, params.ordered_amount = (
get_requested_amount(params),
get_ordered_amount(params),
)
if args.get("doctype") == "Material Request" and args.for_material_request:
amount = args.requested_amount + args.ordered_amount
if params.get("doctype") == "Material Request" and params.for_material_request:
amount = params.requested_amount + params.ordered_amount
elif args.get("doctype") == "Purchase Order" and args.for_purchase_order:
amount = args.ordered_amount
elif params.get("doctype") == "Purchase Order" and params.for_purchase_order:
amount = params.ordered_amount
total_expense = args.actual_expense + amount
total_expense = params.actual_expense + amount
if total_expense > budget_amount:
if args.actual_expense > budget_amount:
error_tense = _("is already")
diff = args.actual_expense - budget_amount
if params.actual_expense > budget_amount:
diff = params.actual_expense - budget_amount
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It is already exceeded by {5}.")
else:
error_tense = _("will be")
diff = total_expense - budget_amount
_msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will be exceeded by {5}.")
currency = frappe.get_cached_value("Company", args.company, "default_currency")
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format(
currency = frappe.get_cached_value("Company", params.company, "default_currency")
msg = _msg.format(
_(action_for),
frappe.bold(args.account),
frappe.unscrub(args.budget_against_field),
frappe.bold(params.account),
frappe.unscrub(params.budget_against_field),
frappe.bold(budget_against),
frappe.bold(fmt_money(budget_amount, currency=currency)),
error_tense,
frappe.bold(fmt_money(diff, currency=currency)),
)
msg += get_expense_breakup(args, currency, budget_against)
msg += get_expense_breakup(params, currency, budget_against)
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
frappe.session.user
@@ -303,14 +540,25 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
def get_expense_breakup(args, currency, budget_against):
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
def get_expense_breakup(params, currency, budget_against):
msg = "<hr> {} - <ul>".format(_("Total Expenses booked through"))
common_filters = frappe._dict(
{
args.budget_against_field: budget_against,
"account": args.account,
"company": args.company,
params.budget_against_field: budget_against,
"account": params.account,
"company": params.company,
}
)
from_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
to_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
gl_filters = common_filters.copy()
gl_filters.update(
{
"from_date": from_date,
"to_date": to_date,
"is_cancelled": 0,
}
)
@@ -319,18 +567,23 @@ def get_expense_breakup(args, currency, budget_against):
+ frappe.utils.get_link_to_report(
"General Ledger",
label=_("Actual Expenses"),
filters=common_filters.copy().update(
{
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
"to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"),
"is_cancelled": 0,
}
),
filters=gl_filters,
)
+ " - "
+ frappe.bold(fmt_money(args.actual_expense, currency=currency))
+ frappe.bold(fmt_money(params.actual_expense, currency=currency))
+ "</li>"
)
mr_filters = common_filters.copy()
mr_filters.update(
{
"status": [["!=", "Stopped"]],
"docstatus": 1,
"material_request_type": "Purchase",
"schedule_date": [["between", [from_date, to_date]]],
"item_code": params.item_code,
"per_ordered": [["<", 100]],
}
)
msg += (
"<li>"
@@ -339,22 +592,24 @@ def get_expense_breakup(args, currency, budget_against):
label=_("Material Requests"),
report_type="Report Builder",
doctype="Material Request",
filters=common_filters.copy().update(
{
"status": [["!=", "Stopped"]],
"docstatus": 1,
"material_request_type": "Purchase",
"schedule_date": [["fiscal year", "2023-2024"]],
"item_code": args.item_code,
"per_ordered": [["<", 100]],
}
),
filters=mr_filters,
)
+ " - "
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
+ frappe.bold(fmt_money(params.requested_amount, currency=currency))
+ "</li>"
)
po_filters = common_filters.copy()
po_filters.update(
{
"status": [["!=", "Closed"]],
"docstatus": 1,
"transaction_date": [["between", [from_date, to_date]]],
"item_code": params.item_code,
"per_billed": [["<", 100]],
}
)
msg += (
"<li>"
+ frappe.utils.get_link_to_report(
@@ -362,42 +617,34 @@ def get_expense_breakup(args, currency, budget_against):
label=_("Unbilled Orders"),
report_type="Report Builder",
doctype="Purchase Order",
filters=common_filters.copy().update(
{
"status": [["!=", "Closed"]],
"docstatus": 1,
"transaction_date": [["fiscal year", "2023-2024"]],
"item_code": args.item_code,
"per_billed": [["<", 100]],
}
),
filters=po_filters,
)
+ " - "
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
+ frappe.bold(fmt_money(params.ordered_amount, currency=currency))
+ "</li></ul>"
)
return msg
def get_actions(args, budget):
def get_actions(params, budget):
yearly_action = budget.action_if_annual_budget_exceeded
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
if args.get("doctype") == "Material Request" and budget.for_material_request:
if params.get("doctype") == "Material Request" and budget.for_material_request:
yearly_action = budget.action_if_annual_budget_exceeded_on_mr
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr
elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
elif params.get("doctype") == "Purchase Order" and budget.for_purchase_order:
yearly_action = budget.action_if_annual_budget_exceeded_on_po
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
return yearly_action, monthly_action
def get_requested_amount(args):
item_code = args.get("item_code")
condition = get_other_condition(args, "Material Request")
def get_requested_amount(params):
item_code = params.get("item_code")
condition = get_other_condition(params, "Material Request")
data = frappe.db.sql(
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
@@ -411,9 +658,9 @@ def get_requested_amount(args):
return data[0][0] if data else 0
def get_ordered_amount(args):
item_code = args.get("item_code")
condition = get_other_condition(args, "Purchase Order")
def get_ordered_amount(params):
item_code = params.get("item_code")
condition = get_other_condition(params, "Purchase Order")
data = frappe.db.sql(
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
@@ -427,111 +674,102 @@ def get_ordered_amount(args):
return data[0][0] if data else 0
def get_other_condition(args, for_doc):
condition = "expense_account = '%s'" % (args.expense_account)
budget_against_field = args.get("budget_against_field")
def get_other_condition(params, for_doc):
condition = f"expense_account = '{params.expense_account}'"
budget_against_field = params.get("budget_against_field")
if budget_against_field and args.get(budget_against_field):
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
if budget_against_field and params.get(budget_against_field):
condition += f" and child.{budget_against_field} = '{params.get(budget_against_field)}'"
if args.get("fiscal_year"):
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
start_date, end_date = frappe.get_cached_value(
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
)
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
condition += f""" and parent.{date_field}
between '{start_date}' and '{end_date}' """
start_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
end_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
condition += f" and parent.{date_field} between '{start_date}' and '{end_date}'"
return condition
def get_actual_expense(args):
if not args.budget_against_doctype:
args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
def get_actual_expense(params):
if not params.budget_against_doctype:
params.budget_against_doctype = frappe.unscrub(params.budget_against_field)
budget_against_field = args.get("budget_against_field")
condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
budget_against_field = params.get("budget_against_field")
condition1 = " and gle.posting_date <= %(month_end_date)s" if params.get("month_end_date") else ""
if args.is_tree:
date_condition = (
f"and gle.posting_date between '{params.budget_start_date}' and '{params.budget_end_date}'"
)
if params.is_tree:
lft_rgt = frappe.db.get_value(
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
params.budget_against_doctype, params.get(budget_against_field), ["lft", "rgt"], as_dict=1
)
params.update(lft_rgt)
args.update(lft_rgt)
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
where lft>=%(lft)s and rgt<=%(rgt)s
and name=gle.{budget_against_field})"""
condition2 = f"""
and exists(
select name from `tab{params.budget_against_doctype}`
where lft >= %(lft)s and rgt <= %(rgt)s
and name = gle.{budget_against_field}
)
"""
else:
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
where name=gle.{budget_against_field} and
gle.{budget_against_field} = %({budget_against_field})s)"""
condition2 = f"""
and gle.{budget_against_field} = %({budget_against_field})s
"""
amount = flt(
frappe.db.sql(
f"""
select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle
where
is_cancelled = 0
and gle.account=%(account)s
{condition1}
and gle.fiscal_year=%(fiscal_year)s
and gle.company=%(company)s
and gle.docstatus=1
{condition2}
""",
(args),
select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle
where
is_cancelled = 0
and gle.account = %(account)s
{condition1}
{date_condition}
and gle.company = %(company)s
and gle.docstatus = 1
{condition2}
""",
params,
)[0][0]
) # nosec
return amount
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
distribution = {}
if monthly_distribution:
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
md = frappe.qb.DocType("Monthly Distribution")
def get_accumulated_monthly_budget(budget_name, posting_date):
posting_date = getdate(posting_date)
res = (
frappe.qb.from_(mdp)
.join(md)
.on(mdp.parent == md.name)
.select(mdp.month, mdp.percentage_allocation)
.where(md.fiscal_year == fiscal_year)
.where(md.name == monthly_distribution)
.run(as_dict=True)
)
bd = frappe.qb.DocType("Budget Distribution")
b = frappe.qb.DocType("Budget")
for d in res:
distribution.setdefault(d.month, d.percentage_allocation)
result = (
frappe.qb.from_(bd)
.join(b)
.on(bd.parent == b.name)
.select(Sum(bd.amount).as_("accumulated_amount"))
.where(b.name == budget_name)
.where(bd.start_date <= posting_date)
.run(as_dict=True)
)
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")
accumulated_percentage = 0.0
while dt <= getdate(posting_date):
if monthly_distribution and distribution:
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
else:
accumulated_percentage += 100.0 / 12
dt = add_months(dt, 1)
return annual_budget * accumulated_percentage / 100
return flt(result[0]["accumulated_amount"]) if result else 0.0
def get_item_details(args):
def get_item_details(params):
cost_center, expense_account = None, None
if not args.get("company"):
if not params.get("company"):
return cost_center, expense_account
if args.item_code:
if params.item_code:
item_defaults = frappe.db.get_value(
"Item Default",
{"parent": args.item_code, "company": args.get("company")},
{"parent": params.item_code, "company": params.get("company")},
["buying_cost_center", "expense_account"],
)
if item_defaults:
@@ -539,7 +777,7 @@ def get_item_details(args):
if not (cost_center and expense_account):
for doctype in ["Item Group", "Company"]:
data = get_expense_cost_center(doctype, args)
data = get_expense_cost_center(doctype, params)
if not cost_center and data:
cost_center = data[0]
@@ -553,14 +791,39 @@ def get_item_details(args):
return cost_center, expense_account
def get_expense_cost_center(doctype, args):
def get_expense_cost_center(doctype, params):
if doctype == "Item Group":
return frappe.db.get_value(
"Item Default",
{"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")},
{"parent": params.get(frappe.scrub(doctype)), "company": params.get("company")},
["buying_cost_center", "expense_account"],
)
else:
return frappe.db.get_value(
doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
doctype, params.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
)
def get_fiscal_year_date_range(from_fiscal_year, to_fiscal_year):
from_year = frappe.get_cached_value(
"Fiscal Year", from_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
)
to_year = frappe.get_cached_value(
"Fiscal Year", to_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
)
return from_year.year_start_date, to_year.year_end_date
@frappe.whitelist()
def revise_budget(budget_name):
old_budget = frappe.get_doc("Budget", budget_name)
if old_budget.docstatus == 1:
old_budget.cancel()
new_budget = frappe.copy_doc(old_budget)
new_budget.docstatus = 0
new_budget.revision_of = old_budget.name
new_budget.insert()
return new_budget.name

View File

@@ -3,12 +3,14 @@
import unittest
import frappe
from frappe.utils import now_datetime, nowdate
from frappe.client import submit
from frappe.utils import add_days, flt, get_first_day, get_last_day, getdate, now_datetime, nowdate
from erpnext.accounts.doctype.budget.budget import (
BudgetError,
get_accumulated_monthly_budget,
get_actual_expense,
revise_budget,
)
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.utils import get_fiscal_year
@@ -25,11 +27,15 @@ class TestBudget(ERPNextTestSuite):
def setUp(self):
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
self.company = "_Test Company"
self.fiscal_year = frappe.db.get_value("Fiscal Year", {}, "name")
self.account = "_Test Account Cost for Goods Sold - _TC"
self.cost_center = "_Test Cost Center - _TC"
def test_monthly_budget_crossed_ignore(self):
set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center")
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
@@ -50,12 +56,13 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_crossed_stop1(self):
set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center")
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
budget.name,
nowdate(),
)
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
@@ -73,13 +80,11 @@ class TestBudget(ERPNextTestSuite):
def test_exception_approver_role(self):
set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center")
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
)
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
@@ -107,16 +112,16 @@ class TestBudget(ERPNextTestSuite):
applicable_on_purchase_order=1,
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
budget_against="Cost Center",
do_not_save=False,
submit_budget=True,
)
fiscal_year = get_fiscal_year(nowdate())[0]
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
budget.name,
nowdate(),
)
mr = frappe.get_doc(
{
"doctype": "Material Request",
@@ -151,14 +156,15 @@ class TestBudget(ERPNextTestSuite):
applicable_on_purchase_order=1,
action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
budget_against="Cost Center",
do_not_save=False,
submit_budget=True,
)
fiscal_year = get_fiscal_year(nowdate())[0]
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
budget.name,
nowdate(),
)
po = create_purchase_order(
transaction_date=nowdate(), qty=1, rate=accumulated_limit + 1, do_not_submit=True
@@ -175,13 +181,14 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_crossed_stop2(self):
set_total_expense_zero(nowdate(), "project")
budget = make_budget(budget_against="Project")
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
budget.name,
nowdate(),
)
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
@@ -200,7 +207,7 @@ class TestBudget(ERPNextTestSuite):
def test_yearly_budget_crossed_stop1(self):
set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center")
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
@@ -217,7 +224,7 @@ class TestBudget(ERPNextTestSuite):
def test_yearly_budget_crossed_stop2(self):
set_total_expense_zero(nowdate(), "project")
budget = make_budget(budget_against="Project")
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
project = frappe.get_value("Project", {"project_name": "_Test Project"})
@@ -237,7 +244,7 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_on_cancellation1(self):
set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center")
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
month = now_datetime().month
if month > 9:
month = 9
@@ -266,7 +273,7 @@ class TestBudget(ERPNextTestSuite):
def test_monthly_budget_on_cancellation2(self):
set_total_expense_zero(nowdate(), "project")
budget = make_budget(budget_against="Project")
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
month = now_datetime().month
if month > 9:
month = 9
@@ -298,11 +305,17 @@ class TestBudget(ERPNextTestSuite):
set_total_expense_zero(nowdate(), "cost_center")
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
budget = make_budget(
budget_against="Cost Center",
cost_center="_Test Company - _TC",
do_not_save=False,
submit_budget=True,
)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
budget.name,
nowdate(),
)
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
@@ -331,11 +344,14 @@ class TestBudget(ERPNextTestSuite):
}
).insert(ignore_permissions=True)
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
budget = make_budget(
budget_against="Cost Center", cost_center=cost_center, do_not_save=False, submit_budget=True
)
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
budget.name,
nowdate(),
)
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
@@ -372,7 +388,12 @@ class TestBudget(ERPNextTestSuite):
{"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40},
)
make_budget(budget_against="Cost Center", cost_center="Main Budget Cost Center 1 - _TC")
make_budget(
budget_against="Cost Center",
cost_center="Main Budget Cost Center 1 - _TC",
do_not_save=False,
submit_budget=True,
)
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
@@ -387,12 +408,15 @@ class TestBudget(ERPNextTestSuite):
def test_action_for_cumulative_limit(self):
set_total_expense_zero(nowdate(), "cost_center")
budget = make_budget(budget_against="Cost Center", applicable_on_cumulative_expense=True)
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
budget = make_budget(
budget_against="Cost Center",
applicable_on_cumulative_expense=True,
do_not_save=False,
submit_budget=True,
)
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
@@ -422,6 +446,165 @@ class TestBudget(ERPNextTestSuite):
po.cancel()
jv.cancel()
def test_fiscal_year_validation(self):
frappe.get_doc(
{
"doctype": "Fiscal Year",
"year": "2100",
"year_start_date": "2100-04-01",
"year_end_date": "2101-03-31",
"companies": [{"company": "_Test Company"}],
}
).insert(ignore_permissions=True)
budget = make_budget(
budget_against="Cost Center",
from_fiscal_year="2100",
to_fiscal_year="2099",
do_not_save=True,
submit_budget=False,
)
with self.assertRaises(frappe.ValidationError):
budget.save()
def test_total_distribution_equals_budget(self):
budget = make_budget(
budget_against="Cost Center",
applicable_on_cumulative_expense=True,
distribute_equally=0,
budget_amount=12000,
do_not_save=False,
submit_budget=False,
)
for row in budget.budget_distribution:
row.amount = 2000
with self.assertRaises(frappe.ValidationError):
budget.save()
def test_evenly_distribute_budget(self):
budget = make_budget(
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
)
total = sum([d.amount for d in budget.budget_distribution])
self.assertEqual(flt(total), 120000)
self.assertTrue(all(d.amount == 10000 for d in budget.budget_distribution))
def test_create_revised_budget(self):
budget = make_budget(
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
)
revised_name = revise_budget(budget.name)
revised_budget = frappe.get_doc("Budget", revised_name)
self.assertNotEqual(budget.name, revised_budget.name)
self.assertEqual(revised_budget.budget_against, budget.budget_against)
self.assertEqual(revised_budget.budget_amount, budget.budget_amount)
old_budget = frappe.get_doc("Budget", budget.name)
self.assertEqual(old_budget.docstatus, 2)
def test_revision_preserves_distribution(self):
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center - _TC")
budget = make_budget(
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
)
revised_name = revise_budget(budget.name)
revised_budget = frappe.get_doc("Budget", revised_name)
self.assertGreater(len(revised_budget.budget_distribution), 0)
total = sum(row.amount for row in revised_budget.budget_distribution)
self.assertEqual(total, revised_budget.budget_amount)
def test_manual_budget_amount_total(self):
budget = make_budget(
budget_against="Cost Center",
distribute_equally=0,
budget_amount=30000,
budget_start_date="2025-04-01",
budget_end_date="2025-06-30",
do_not_save=False,
submit_budget=False,
)
budget.budget_distribution = []
for row in [
{"start_date": "2025-04-01", "end_date": "2025-04-30", "amount": 10000, "percent": 33.33},
{"start_date": "2025-05-01", "end_date": "2025-05-31", "amount": 15000, "percent": 50.00},
{"start_date": "2025-06-01", "end_date": "2025-06-30", "amount": 5000, "percent": 16.67},
]:
budget.append("budget_distribution", row)
budget.save()
total_child_amount = sum(row.amount for row in budget.budget_distribution)
self.assertEqual(total_child_amount, budget.budget_amount)
def test_fiscal_year_company_mismatch(self):
budget = make_budget(budget_against="Cost Center", do_not_save=True, submit_budget=False)
fy = frappe.get_doc(
{
"doctype": "Fiscal Year",
"year": "2099",
"year_start_date": "2099-04-01",
"year_end_date": "2100-03-31",
"companies": [{"company": "_Test Company 2"}],
}
).insert(ignore_permissions=True)
budget.from_fiscal_year = fy.name
budget.to_fiscal_year = fy.name
budget.company = "_Test Company"
with self.assertRaises(frappe.ValidationError):
budget.save()
def test_manual_distribution_total_equals_budget_amount(self):
budget = make_budget(
budget_against="Cost Center",
cost_center="_Test Cost Center - _TC",
distribute_equally=0,
budget_amount=12000,
do_not_save=False,
submit_budget=False,
)
for d in budget.budget_distribution:
d.amount = 2000
with self.assertRaises(frappe.ValidationError):
budget.save()
def test_duplicate_budget_validation(self):
budget = make_budget(
budget_against="Cost Center",
distribute_equally=1,
budget_amount=15000,
do_not_save=False,
submit_budget=True,
)
new_budget = frappe.new_doc("Budget")
new_budget.company = "_Test Company"
new_budget.from_fiscal_year = budget.from_fiscal_year
new_budget.to_fiscal_year = new_budget.from_fiscal_year
new_budget.budget_against = "Cost Center"
new_budget.cost_center = "_Test Cost Center - _TC"
new_budget.account = "_Test Account Cost for Goods Sold - _TC"
new_budget.budget_amount = 10000
with self.assertRaises(frappe.ValidationError):
new_budget.insert()
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
if budget_against_field == "project":
@@ -430,21 +613,32 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
budget_against = budget_against_CC or "_Test Cost Center - _TC"
fiscal_year = get_fiscal_year(nowdate())[0]
fiscal_year_start_date, fiscal_year_end_date = get_fiscal_year(nowdate())[1:3]
args = frappe._dict(
{
"account": "_Test Account Cost for Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
"monthly_end_date": posting_date,
"month_end_date": posting_date,
"company": "_Test Company",
"fiscal_year": fiscal_year,
"from_fiscal_year": fiscal_year,
"to_fiscal_year": fiscal_year,
"budget_against_field": budget_against_field,
"budget_start_date": fiscal_year_start_date,
"budget_end_date": fiscal_year_end_date,
}
)
if not args.get(budget_against_field):
args[budget_against_field] = budget_against
args.budget_against_doctype = frappe.unscrub(budget_against_field)
if frappe.get_cached_value("DocType", args.budget_against_doctype, "is_tree"):
args.is_tree = True
else:
args.is_tree = False
existing_expense = get_actual_expense(args)
if existing_expense:
@@ -474,18 +668,33 @@ def make_budget(**args):
budget_against = args.budget_against
cost_center = args.cost_center
fiscal_year = get_fiscal_year(nowdate())[0]
if budget_against == "Project":
project_name = "{}%".format("_Test Project/" + fiscal_year)
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
project = frappe.get_value("Project", {"project_name": "_Test Project"})
budget_list = frappe.get_all(
"Budget",
filters={
"project": project,
"account": "_Test Account Cost for Goods Sold - _TC",
},
pluck="name",
)
else:
cost_center_name = "{}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", cost_center_name)})
for d in budget_list:
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
budget_list = frappe.get_all(
"Budget",
filters={
"cost_center": cost_center or "_Test Cost Center - _TC",
"account": "_Test Account Cost for Goods Sold - _TC",
},
pluck="name",
)
for name in budget_list:
doc = frappe.get_doc("Budget", name)
if doc.docstatus == 1:
doc.cancel()
frappe.delete_doc("Budget", name, force=True, ignore_missing=True)
budget = frappe.new_doc("Budget")
@@ -494,18 +703,18 @@ def make_budget(**args):
else:
budget.cost_center = cost_center or "_Test Cost Center - _TC"
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution")
monthly_distribution.fiscal_year = fiscal_year
monthly_distribution.save()
budget.fiscal_year = fiscal_year
budget.monthly_distribution = "_Test Distribution"
budget.from_fiscal_year = args.from_fiscal_year or fiscal_year
budget.to_fiscal_year = args.to_fiscal_year or fiscal_year
budget.company = "_Test Company"
budget.account = "_Test Account Cost for Goods Sold - _TC"
budget.budget_amount = args.budget_amount or 200000
budget.applicable_on_booking_actual_expenses = 1
budget.action_if_annual_budget_exceeded = "Stop"
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
budget.budget_against = budget_against
budget.append("accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000})
budget.distribution_frequency = "Monthly"
budget.distribute_equally = args.get("distribute_equally", 1)
if args.applicable_on_material_request:
budget.applicable_on_material_request = 1
@@ -530,7 +739,13 @@ def make_budget(**args):
args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn"
)
budget.insert()
budget.submit()
if not args.do_not_save:
try:
budget.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
if args.submit_budget:
budget.submit()
return budget

View File

@@ -0,0 +1,58 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-10-12 23:31:03.841996",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"start_date",
"end_date",
"amount",
"percent"
],
"fields": [
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date",
"read_only": 1,
"search_index": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "End Date",
"read_only": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount"
},
{
"fieldname": "percent",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Percent"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-11-03 13:18:28.398198",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget Distribution",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,26 @@
# 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 BudgetDistribution(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
amount: DF.Currency
end_date: DF.Date | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
percent: DF.Percent
start_date: DF.Date | None
# end: auto-generated types
pass

View File

@@ -3,6 +3,8 @@
import frappe
from frappe.query_builder import functions
from frappe.query_builder.utils import DocType
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, flt, today
@@ -81,10 +83,11 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(je.total_debit, 8500.0)
self.assertEqual(je.total_credit, 8500.0)
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=["sum(debit)-sum(credit) as balance"],
fields=[(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance")],
)[0]
self.assertEqual(acc_balance.balance, 8500.0)
@@ -146,12 +149,15 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(je.total_debit, 500.0)
self.assertEqual(je.total_credit, 500.0)
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
(
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
).as_("balance_in_account_currency"),
],
)[0]
# account shouldn't have balance in base and account currency
@@ -193,12 +199,15 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
pe.references = []
pe.save().submit()
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
(
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
).as_("balance_in_account_currency"),
],
)[0]
# account should have balance only in account currency
@@ -235,12 +244,15 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(flt(je.total_debit, precision), 0.0)
self.assertEqual(flt(je.total_credit, precision), 0.0)
gl = DocType("GL Entry")
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
(functions.Sum(gl.debit) - functions.Sum(gl.credit)).as_("balance"),
(
functions.Sum(gl.debit_in_account_currency) - functions.Sum(gl.credit_in_account_currency)
).as_("balance_in_account_currency"),
],
)[0]
# account shouldn't have balance in base and account currency post revaluation

View File

@@ -0,0 +1,187 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-09-06 09:39:46.503678",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_code",
"display_name",
"indentation_level",
"data_source",
"balance_type",
"column_break_hxqu",
"fieldtype",
"color",
"bold_text",
"italic_text",
"hidden_calculation",
"hide_when_empty",
"reverse_sign",
"include_in_charts",
"section_break_ornw",
"column_break_asfe",
"advanced_filtering",
"filters_editor",
"calculation_formula",
"section_break_pvro",
"formula_description"
],
"fields": [
{
"columns": 1,
"description": "Code to reference this line in formulas (e.g., REV100, EXP200, ASSET100)",
"fieldname": "reference_code",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Line Reference"
},
{
"description": "Text displayed on the financial statement (e.g., 'Total Revenue', 'Cash and Cash Equivalents')",
"fieldname": "display_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Display Name"
},
{
"columns": 1,
"description": "Indentation level: 0 = Main heading, 1 = Sub-category, 2 = Individual accounts, etc.",
"fieldname": "indentation_level",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Indent Level"
},
{
"description": "How this line gets its data",
"fieldname": "data_source",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Data Source",
"options": "\nAccount Data\nCalculated Amount\nCustom API\nBlank Line\nColumn Break\nSection Break"
},
{
"depends_on": "eval:doc.data_source == 'Account Data'",
"description": "Opening Balance = Start of period, Closing Balance = End of period, Period Movement = Net change during period",
"fieldname": "balance_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Balance Type",
"mandatory_depends_on": "eval:doc.data_source == 'Account Data'",
"options": "\nOpening Balance\nClosing Balance\nPeriod Movement (Debits - Credits)"
},
{
"fieldname": "column_break_hxqu",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Bold text for emphasis (totals, major headings)",
"fieldname": "bold_text",
"fieldtype": "Check",
"label": "Bold Text"
},
{
"default": "0",
"description": "Italic text for subtotals or notes",
"fieldname": "italic_text",
"fieldtype": "Check",
"label": "Italic Text"
},
{
"default": "0",
"description": "Calculate but don't show on final report",
"fieldname": "hidden_calculation",
"fieldtype": "Check",
"label": "Hidden Line (Internal Use Only)"
},
{
"default": "0",
"description": "Hide this line if amount is zero",
"fieldname": "hide_when_empty",
"fieldtype": "Check",
"label": "Hide If Zero"
},
{
"columns": 1,
"default": "0",
"description": "Show negative values as positive (for expenses in P&L)",
"fieldname": "reverse_sign",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Reverse Sign"
},
{
"fieldname": "section_break_ornw",
"fieldtype": "Section Break"
},
{
"depends_on": "eval: (doc.data_source === \"Account Data\" && doc.advanced_filtering) || [\"Calculated Amount\", \"Custom API\"].includes(doc.data_source);\n",
"fieldname": "calculation_formula",
"fieldtype": "Code",
"label": "Formula or Account Filter",
"mandatory_depends_on": "eval:doc.data_source != 'Blank Line' && doc.data_source != 'Column Break' && doc.data_source != 'Section Break'"
},
{
"fieldname": "formula_description",
"fieldtype": "HTML"
},
{
"default": "0",
"description": "If enabled, this row's values will be displayed on financial charts",
"fieldname": "include_in_charts",
"fieldtype": "Check",
"label": "Include in Charts"
},
{
"description": "Color to highlight values (e.g., red for exceptions)",
"fieldname": "color",
"fieldtype": "Color",
"label": "Color"
},
{
"description": "How to format and present values in the financial report (only if different from column fieldtype)",
"fieldname": "fieldtype",
"fieldtype": "Select",
"label": "Value Type",
"options": "\nCurrency\nFloat\nInt\nPercent"
},
{
"depends_on": "eval: doc.data_source === \"Account Data\" && !doc.advanced_filtering",
"fieldname": "filters_editor",
"fieldtype": "HTML"
},
{
"depends_on": "eval: ![\"Blank Line\", \"Column Break\", \"Section Break\"].includes(doc.data_source);",
"fieldname": "column_break_asfe",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval: doc.data_source === \"Account Data\"",
"description": "Use <strong>Python</strong> filters to get Accounts",
"fieldname": "advanced_filtering",
"fieldtype": "Check",
"label": "Advanced Filtering",
"print_hide": 1
},
{
"fieldname": "section_break_pvro",
"fieldtype": "Section Break"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-10-14 09:23:27.208072",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Financial Report Row",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,47 @@
# 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 FinancialReportRow(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
advanced_filtering: DF.Check
balance_type: DF.Literal[
"", "Opening Balance", "Closing Balance", "Period Movement (Debits - Credits)"
]
bold_text: DF.Check
calculation_formula: DF.Code | None
color: DF.Color | None
data_source: DF.Literal[
"",
"Account Data",
"Calculated Amount",
"Custom API",
"Blank Line",
"Column Break",
"Section Break",
]
display_name: DF.Data | None
fieldtype: DF.Literal["", "Currency", "Float", "Int", "Percent"]
hidden_calculation: DF.Check
hide_when_empty: DF.Check
include_in_charts: DF.Check
indentation_level: DF.Int
italic_text: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
reference_code: DF.Data | None
reverse_sign: DF.Check
# end: auto-generated types
pass

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,433 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Financial Report Template", {
refresh(frm) {
// add custom button to view missed accounts
frm.add_custom_button(__("View Account Coverage"), function () {
let selected_rows = frm.get_field("rows").grid.get_selected_children();
const has_selection = selected_rows.length > 0;
if (selected_rows.length === 0) selected_rows = frm.doc.rows;
show_accounts_tree(selected_rows, has_selection);
});
// add custom button to open the financial report
frm.add_custom_button(__("View Report"), function () {
frappe.set_route("query-report", frm.doc.report_type, {
report_template: frm.doc.name,
});
});
},
validate(frm) {
if (!frm.doc.rows || frm.doc.rows.length === 0) {
frappe.msgprint(__("At least one row is required for a financial report template"));
}
},
});
frappe.ui.form.on("Financial Report Row", {
data_source(frm, cdt, cdn) {
const row = locals[cdt][cdn];
update_formula_label(frm, row.data_source);
update_formula_description(frm, row.data_source);
if (row.data_source !== "Account Data") {
frappe.model.set_value(cdt, cdn, "balance_type", "");
}
if (["Blank Line", "Column Break", "Section Break"].includes(row.data_source)) {
frappe.model.set_value(cdt, cdn, "calculation_formula", "");
}
set_up_filters_editor(frm, cdt, cdn);
},
form_render(frm, cdt, cdn) {
const row = locals[cdt][cdn];
update_formula_label(frm, row.data_source);
update_advanced_formula_property(frm, cdt, cdn);
set_up_filters_editor(frm, cdt, cdn);
update_formula_description(frm, row.data_source);
},
calculation_formula(frm, cdt, cdn) {
update_advanced_formula_property(frm, cdt, cdn);
},
advanced_filtering(frm, cdt, cdn) {
set_up_filters_editor(frm, cdt, cdn);
},
});
// FILTERS EDITOR
function set_up_filters_editor(frm, cdt, cdn) {
const row = locals[cdt][cdn];
if (row.data_source !== "Account Data" || row.advanced_filtering) return;
const grid_row = frm.fields_dict["rows"].grid.get_row(cdn);
const wrapper = grid_row.get_field("filters_editor").$wrapper;
wrapper.empty();
const ACCOUNT = "Account";
const FIELD_IDX = 1;
const OPERATOR_IDX = 2;
const VALUE_IDX = 3;
// Parse saved filters
let saved_filters = [];
if (row.calculation_formula) {
try {
const parsed = JSON.parse(row.calculation_formula);
if (Array.isArray(parsed)) saved_filters = [parsed];
else if (parsed.and) saved_filters = parsed.and;
} catch (e) {
frappe.show_alert({
message: __("Invalid filter formula. Please check the syntax."),
indicator: "red",
});
}
}
if (saved_filters.length)
// Ensure every filter starts with "Account"
saved_filters = saved_filters.map((f) => [ACCOUNT, ...f]);
frappe.model.with_doctype(ACCOUNT, () => {
const filter_group = new frappe.ui.FilterGroup({
parent: wrapper,
doctype: ACCOUNT,
on_change: () => {
// only need [[field, operator, value]]
const filters = filter_group
.get_filters()
.map((f) => [f[FIELD_IDX], f[OPERATOR_IDX], f[VALUE_IDX]]);
const current = filters.length > 1 ? { and: filters } : filters[0];
frappe.model.set_value(cdt, cdn, "calculation_formula", JSON.stringify(current));
},
});
filter_group.add_filters_to_filter_group(saved_filters);
});
}
function update_advanced_formula_property(frm, cdt, cdn) {
const row = locals[cdt][cdn];
const is_advanced = is_advanced_formula(row);
frm.set_df_property("rows", "read_only", is_advanced, frm.doc.name, "advanced_filtering", cdn);
if (is_advanced && !row.advanced_filtering) {
row.advanced_filtering = 1;
frm.refresh_field("rows");
}
}
function is_advanced_formula(row) {
if (!row || row.data_source !== "Account Data") return false;
let parsed = null;
if (row.calculation_formula) {
try {
parsed = JSON.parse(row.calculation_formula);
} catch (e) {
console.warn("Invalid JSON in calculation_formula:", e);
return false;
}
}
if (Array.isArray(parsed)) return false;
if (parsed?.or) return true;
if (parsed?.and) return parsed.and.some((cond) => !Array.isArray(cond));
return false;
}
// ACCOUNTS TREE VIEW
function show_accounts_tree(template_rows, has_selection) {
// filtered rows
const account_rows = template_rows.filter((row) => row.data_source === "Account Data");
if (account_rows.length === 0) {
frappe.show_alert(__("No <strong>Account Data</strong> row found"));
return;
}
const dialog = new frappe.ui.Dialog({
title: __("Accounts Missing from Report"),
fields: [
{
fieldname: "company",
fieldtype: "Link",
options: "Company",
label: "Company",
reqd: 1,
default: frappe.defaults.get_user_default("Company"),
onchange: () => {
const company_field = dialog.get_field("company");
if (!company_field.value || company_field.value === company_field.last_value) return;
refresh_tree_view(dialog, account_rows);
},
},
{
fieldname: "view_type",
fieldtype: "Select",
options: ["Missing Accounts", "Filtered Accounts"],
label: "View",
default: has_selection ? "Filtered Accounts" : "Missing Accounts",
reqd: 1,
onchange: () => {
dialog.set_title(
dialog.get_value("view_type") === "Missing Accounts"
? __("Accounts Missing from Report")
: __("Accounts Included in Report")
);
refresh_tree_view(dialog, account_rows);
},
},
{
fieldname: "tip",
fieldtype: "HTML",
label: "Tip",
options: `
<div class="alert alert-success" role="alert">
Tip: Select report lines to view their accounts
</div>
`,
depends_on: has_selection ? "eval: false" : "eval: true",
},
{
fieldname: "tree_area",
fieldtype: "HTML",
label: "Chart of Accounts",
read_only: 1,
depends_on: "eval: doc.company",
},
],
primary_action_label: __("Done"),
primary_action() {
dialog.hide();
},
});
dialog.show();
refresh_tree_view(dialog, account_rows);
}
async function refresh_tree_view(dialog, account_rows) {
const missed = dialog.get_value("view_type") === "Missing Accounts";
const company = dialog.get_value("company");
const wrapper = dialog.get_field("tree_area").$wrapper;
wrapper.empty();
// get filtered accounts
const { message: filtered_accounts } = await frappe.call({
method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_filtered_accounts",
args: { company: company, account_rows: account_rows },
});
// render tree
const tree = new FilteredTree({
parent: wrapper,
label: company,
root_value: company,
method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_children_accounts",
args: { doctype: "Account", company: company, filtered_accounts: filtered_accounts, missed: missed },
toolbar: [],
});
tree.load_children(tree.root_node, true);
}
class FilteredTree extends frappe.ui.Tree {
render_children_of_all_nodes(data_list) {
data_list = this.get_filtered_data_list(data_list);
super.render_children_of_all_nodes(data_list);
}
get_filtered_data_list(data_list) {
let removed_nodes = new Set();
// Filter nodes with no data
data_list = data_list.filter((d) => {
if (d.data.length === 0) {
removed_nodes.add(d.parent);
return false;
}
return true;
});
// Remove references to removed nodes and iteratively remove empty parents
while (removed_nodes.size > 0) {
const current_removed = [...removed_nodes];
removed_nodes.clear();
data_list = data_list.filter((d) => {
d.data = d.data.filter((a) => !current_removed.includes(a.value));
if (d.data.length === 0) {
removed_nodes.add(d.parent);
return false;
}
return true;
});
}
return data_list;
}
}
function update_formula_label(frm, data_source) {
const grid = frm.fields_dict.rows.grid;
const field = grid.fields_map.calculation_formula;
if (!field) return;
const labels = {
"Account Data": "Account Filter",
"Custom API": "API Method Path",
};
grid.update_docfield_property(
"calculation_formula",
"label",
labels[data_source] || "Calculation Formula"
);
}
// FORMULA DESCRIPTION
function update_formula_description(frm, data_source) {
if (!data_source) return;
let grid = frm.fields_dict.rows.grid;
let field = grid.fields_map.formula_description;
if (!field) return;
// Common CSS styles and elements
const container_style = `style="padding: var(--padding-md); border: 1px solid var(--border-color); border-radius: var(--border-radius); margin-top: var(--margin-sm);"`;
const title_style = `style="margin-top: 0; color: var(--text-color);"`;
const subtitle_style = `style="color: var(--text-color); margin-bottom: var(--margin-xs);"`;
const text_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted);"`;
const list_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted); font-size: 0.9em;"`;
const note_style = `style="margin-bottom: 0; color: var(--text-muted); font-size: 0.9em;"`;
const tip_style = `style="margin-bottom: 0; color: var(--text-color); font-size: 0.85em;"`;
let description_html = "";
if (data_source === "Account Data") {
description_html = `
<div ${container_style}>
<h5 ${title_style}>Account Filter Guide</h5>
<p ${text_style}>Specify which accounts to include in this line.</p>
<h6 ${subtitle_style}>Basic Examples:</h6>
<ul ${list_style}>
<li><code>["account_type", "=", "Cash"]</code> - All Cash accounts</li>
<li><code>["root_type", "in", ["Asset", "Liability"]]</code> - All Asset and Liability accounts</li>
<li><code>["account_category", "like", "Revenue"]</code> - Revenue accounts</li>
</ul>
<h6 ${subtitle_style}>Multiple Conditions (AND/OR):</h6>
<ul ${list_style}>
<li><code>{"and": [["root_type", "=", "Asset"], ["account_type", "=", "Cash"]]}</code></li>
<li><code>{"or": [["account_category", "like", "Revenue"], ["account_category", "like", "Income"]]}</code></li>
</ul>
<p ${note_style}><strong>Available operators:</strong> <code>=, !=, in, not in, like, not like, is</code></p>
<p ${tip_style}><strong>Multi-Company Tip:</strong> Use fields like <code>account_type</code>, <code>root_type</code>, and <code>account_category</code> for templates that work across multiple companies.</p>
</div>`;
} else if (data_source === "Calculated Amount") {
description_html = `
<div ${container_style}>
<h5 ${title_style}>Formula Guide</h5>
<p ${text_style}>Create calculations using reference codes from other lines.</p>
<h6 ${subtitle_style}>Basic Examples:</h6>
<ul ${list_style}>
<li><code>REV100 + REV200</code> - Add two revenue lines</li>
<li><code>ASSETS - LIABILITIES</code> - Calculate equity</li>
<li><code>REVENUE * 0.1</code> - 10% of revenue</li>
</ul>
<h6 ${subtitle_style}>Common Functions:</h6>
<ul ${list_style}>
<li><code>abs(value)</code> - Remove negative sign</li>
<li><code>round(value)</code> - Round to whole number</li>
<li><code>max(val1, val2)</code> - Larger of two values</li>
<li><code>min(val1, val2)</code> - Smaller of two values</li>
</ul>
<p ${note_style}><strong>Required:</strong> Use "Reference Code" from other rows in your formulas.</p>
</div>`;
} else if (data_source === "Custom API") {
description_html = `
<div ${container_style}>
<h5 ${title_style}>Custom API Setup</h5>
<p ${text_style}>Path to your custom method that returns financial data.</p>
<h6 ${subtitle_style}>Format:</h6>
<ul ${list_style}>
<li><code>erpnext.custom.financial_apis.get_custom_revenue</code></li>
<li><code>my_app.financial_reports.get_kpi_data</code></li>
</ul>
<h6 ${subtitle_style}>Return Format:</h6>
<p ${text_style}>Numbers for each period: <code>[1000.0, 1200.0, 1150.0]</code></p>
</div>`;
} else if (data_source === "Blank Line") {
description_html = `
<div ${container_style}>
<h5 ${title_style}>Blank Line</h5>
<p ${text_style}>Adds empty space for better visual separation.</p>
<h6 ${subtitle_style}>Use For:</h6>
<ul ${list_style}>
<li>Separating major sections</li>
<li>Adding space before totals</li>
</ul>
<p ${note_style}><strong>Note:</strong> No formula needed - creates visual spacing only.</p>
</div>`;
} else if (data_source === "Column Break") {
description_html = `
<div ${container_style}>
<h5 ${title_style}>Column Break</h5>
<p ${text_style}>Creates a visual break for side-by-side layout.</p>
<h6 ${subtitle_style}>Use For:</h6>
<ul ${list_style}>
<li>Horizontal P&L statements</li>
<li>Side-by-side Balance Sheet sections</li>
</ul>
<p ${note_style}><strong>Note:</strong> No formula needed - this is for formatting only.</p>
</div>`;
} else if (data_source === "Section Break") {
description_html = `
<div ${container_style}>
<h5 ${title_style}>Section Break</h5>
<p ${text_style}>Creates a visual break for separating different sections.</p>
<h6 ${subtitle_style}>Use For:</h6>
<ul ${list_style}>
<li>Separating major sections in a report - say trading & profit and loss</li>
<li>Improving readability by adding space</li>
</ul>
<p ${note_style}><strong>Note:</strong> No formula needed - this is for formatting only.</p>
</div>`;
}
grid.update_docfield_property("formula_description", "options", description_html);
}

View File

@@ -0,0 +1,102 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:template_name",
"creation": "2025-08-02 04:44:15.184541",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"template_name",
"report_type",
"module",
"column_break_lvnq",
"disabled",
"section_break_fvlw",
"rows"
],
"fields": [
{
"description": "Descriptive name for your template (e.g., 'Standard P&L', 'Detailed Balance Sheet')",
"fieldname": "template_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Template Name",
"reqd": 1,
"unique": 1
},
{
"description": "Type of financial statement this template generates",
"fieldname": "report_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Report Type",
"options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement"
},
{
"depends_on": "eval:frappe.boot.developer_mode",
"fieldname": "module",
"fieldtype": "Link",
"label": "Module (for Export)",
"options": "Module Def"
},
{
"fieldname": "column_break_lvnq",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_fvlw",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 1,
"fieldname": "rows",
"fieldtype": "Table",
"label": "Report Line Items",
"options": "Financial Report Row"
},
{
"default": "0",
"description": "Disable template to prevent use in reports",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-11-14 00:11:03.508139",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Financial Report Template",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "Accounts User"
},
{
"read": 1,
"role": "Auditor"
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "template_name"
}

View File

@@ -0,0 +1,179 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import os
import shutil
import frappe
from frappe import _
from frappe.model.document import Document
from erpnext.accounts.doctype.account_category.account_category import import_account_categories
from erpnext.accounts.doctype.financial_report_template.financial_report_validation import TemplateValidator
class FinancialReportTemplate(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.financial_report_row.financial_report_row import FinancialReportRow
disabled: DF.Check
module: DF.Link | None
report_type: DF.Literal[
"", "Profit and Loss Statement", "Balance Sheet", "Cash Flow", "Custom Financial Statement"
]
rows: DF.Table[FinancialReportRow]
template_name: DF.Data
# end: auto-generated types
def validate(self):
validator = TemplateValidator(self)
result = validator.validate()
result.notify_user()
def on_update(self):
self._export_template()
def on_trash(self):
self._delete_template()
def _export_template(self):
from frappe.modules.utils import export_module_json
if not self.module:
return
export_module_json(self, True, self.module)
self._export_account_categories()
def _delete_template(self):
if not self.module or not frappe.conf.developer_mode:
return
module_path = frappe.get_module_path(self.module)
dir_path = os.path.join(module_path, "financial_report_template", frappe.scrub(self.name))
shutil.rmtree(dir_path, ignore_errors=True)
def _export_account_categories(self):
import json
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FormulaFieldExtractor,
)
if not self.module or not frappe.conf.developer_mode or frappe.flags.in_import:
return
# Extract category from rows
extractor = FormulaFieldExtractor(
field_name="account_category", exclude_operators=["like", "not like"]
)
account_data_rows = [row for row in self.rows if row.data_source == "Account Data"]
category_names = extractor.extract_from_rows(account_data_rows)
if not category_names:
return
# Get path
module_path = frappe.get_module_path(self.module)
categories_file = os.path.join(module_path, "financial_report_template", "account_categories.json")
# Load existing categories
existing_categories = {}
if os.path.exists(categories_file):
try:
with open(categories_file) as f:
existing_data = json.load(f)
existing_categories = {cat["account_category_name"]: cat for cat in existing_data}
except (json.JSONDecodeError, KeyError):
pass # Create new file
# Fetch categories from database
if category_names:
db_categories = frappe.get_all(
"Account Category",
filters={"account_category_name": ["in", list(category_names)]},
fields=["account_category_name", "description"],
)
for cat in db_categories:
existing_categories[cat["account_category_name"]] = cat
# Sort by category name
sorted_categories = sorted(existing_categories.values(), key=lambda x: x["account_category_name"])
# Write to file
os.makedirs(os.path.dirname(categories_file), exist_ok=True)
with open(categories_file, "w") as f:
json.dump(sorted_categories, f, indent=2)
def sync_financial_report_templates(chart_of_accounts=None, existing_company=None):
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import get_chart
# If COA is being created for an existing company,
# skip syncing templates as they are likely already present
if existing_company:
return
# Allow regional templates to completely override ERPNext
# templates based on the chart of accounts selected
disable_default_financial_report_template = False
if chart_of_accounts:
coa = get_chart(chart_of_accounts)
if coa.get("disable_default_financial_report_template", False):
disable_default_financial_report_template = True
installed_apps = frappe.get_installed_apps()
for app in installed_apps:
if disable_default_financial_report_template and app == "erpnext":
continue
_sync_templates_for(app)
def _sync_templates_for(app_name):
templates = []
for module_name in frappe.local.app_modules.get(app_name) or []:
module_path = frappe.get_module_path(module_name)
template_path = os.path.join(module_path, "financial_report_template")
if not os.path.isdir(template_path):
continue
import_account_categories(template_path)
for template_dir in os.listdir(template_path):
json_file = os.path.join(template_path, template_dir, f"{template_dir}.json")
if os.path.isfile(json_file):
templates.append(json_file)
if not templates:
return
# ensure files are not exported
frappe.flags.in_import = True
for template_path in templates:
with open(template_path) as f:
template_data = frappe._dict(frappe.parse_json(f.read()))
template_name = template_data.get("name")
if not frappe.db.exists("Financial Report Template", template_name):
doc = frappe.get_doc(template_data)
doc.flags.ignore_mandatory = True
doc.flags.ignore_permissions = True
doc.flags.ignore_validate = True
doc.insert()
frappe.flags.in_import = False

View File

@@ -0,0 +1,545 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import ast
import json
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, ClassVar
import frappe
from frappe import _
from frappe.database.operator_map import OPERATOR_MAP
from frappe.database.query import SQLFunctionParser
@dataclass
class ValidationIssue:
"""Represents a single validation issue"""
message: str
row_idx: int | None = None
field: str | None = None
details: dict[str, Any] = None
def __post_init__(self):
if self.details is None:
self.details = {}
def __str__(self) -> str:
prefix = f"Row {self.row_idx}: " if self.row_idx else ""
field_info = f"[{self.field}] " if self.field else ""
message = f"{prefix}{field_info}{self.message}"
return _(message)
@dataclass
class ValidationResult:
issues: list[ValidationIssue] = field(default_factory=list)
warnings: list[ValidationIssue] = field(default_factory=list)
@property
def is_valid(self) -> bool:
return len(self.issues) == 0
@property
def has_warnings(self) -> bool:
return len(self.warnings) > 0
@property
def error_count(self) -> int:
return len(self.issues)
@property
def warning_count(self) -> int:
return len(self.warnings)
def merge(self, other: "ValidationResult") -> "ValidationResult":
self.issues.extend(other.issues)
self.warnings.extend(other.warnings)
return self
def add_error(self, issue: ValidationIssue) -> None:
"""Add a critical error that prevents functionality"""
self.issues.append(issue)
def add_warning(self, issue: ValidationIssue) -> None:
"""Add a warning for recommendatory validation"""
self.warnings.append(issue)
def notify_user(self) -> None:
warnings = "<br><br>".join(str(w) for w in self.warnings)
errors = "<br><br>".join(str(e) for e in self.issues)
if warnings:
frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
if errors:
frappe.throw(errors, title=_("Errors"))
class TemplateValidator:
"""Main validator that orchestrates all validations"""
def __init__(self, template):
self.template = template
self.validators = [
TemplateStructureValidator(),
DependencyValidator(template),
]
self.formula_validator = FormulaValidator(template)
def validate(self) -> ValidationResult:
result = ValidationResult([])
# Run template-level validators
for validator in self.validators:
result.merge(validator.validate(self.template))
# Run row-level validations
account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
for row in self.template.rows:
result.merge(self.formula_validator.validate(row, account_fields))
return result
class Validator(ABC):
@abstractmethod
def validate(self, context: Any) -> ValidationResult:
pass
class TemplateStructureValidator(Validator):
def validate(self, template) -> ValidationResult:
result = ValidationResult()
result.merge(self._validate_reference_codes(template))
result.merge(self._validate_required_fields(template))
return result
def _validate_reference_codes(self, template) -> ValidationResult:
result = ValidationResult()
used_codes = set()
for row in template.rows:
if not row.reference_code:
continue
ref_code = row.reference_code.strip()
# Check format
if not re.match(r"^[A-Za-z][A-Za-z0-9_-]*$", ref_code):
result.add_error(
ValidationIssue(
message=f"Invalid line reference format: '{ref_code}'. Must start with letter and contain only letters, numbers, underscores, and hyphens",
row_idx=row.idx,
)
)
# Check uniqueness
if ref_code in used_codes:
result.add_error(
ValidationIssue(
message=f"Duplicate line reference: '{ref_code}'",
row_idx=row.idx,
)
)
used_codes.add(ref_code)
return result
def _validate_required_fields(self, template) -> ValidationResult:
result = ValidationResult()
for row in template.rows:
# Balance type required
if row.data_source == "Account Data" and not row.balance_type:
result.add_error(
ValidationIssue(
message="Balance Type is required for Account Data",
row_idx=row.idx,
)
)
# Calculation formula required
if row.data_source in ["Account Data", "Calculated Amount", "Custom API"]:
if not row.calculation_formula:
result.add_error(
ValidationIssue(
message=f"Formula is required for {row.data_source}",
row_idx=row.idx,
)
)
return result
class DependencyValidator(Validator):
def __init__(self, template):
self.template = template
self.dependencies = self._build_dependency_graph()
def validate(self, context=None) -> ValidationResult:
result = ValidationResult()
result.merge(self._validate_circular_dependencies())
result.merge(self._validate_missing_dependencies())
return result
def _build_dependency_graph(self) -> dict[str, list[str]]:
graph = {}
available_codes = {row.reference_code for row in self.template.rows if row.reference_code}
for row in self.template.rows:
if row.reference_code and row.data_source == "Calculated Amount" and row.calculation_formula:
deps = extract_reference_codes_from_formula(row.calculation_formula, list(available_codes))
if deps:
graph[row.reference_code] = deps
return graph
def _validate_circular_dependencies(self) -> ValidationResult:
"""
Efficient cycle detection using DFS (Depth-First Search) with three-color algorithm:
- WHITE (0): unvisited node
- GRAY (1): currently being processed (on recursion stack)
- BLACK (2): fully processed
Example cycle detection:
A → B → C → A (cycle detected when A is GRAY and visited again)
"""
result = ValidationResult()
WHITE, GRAY, BLACK = 0, 1, 2
colors = {node: WHITE for node in self.dependencies}
def dfs(node, path):
if node not in colors:
return # External dependency
if colors[node] == GRAY:
# Found cycle
cycle_start = path.index(node)
cycle = [*path[cycle_start:], node]
result.add_error(
ValidationIssue(
message=f"Circular dependency detected: {''.join(cycle)}",
)
)
return
if colors[node] == BLACK:
return # Already processed
colors[node] = GRAY
path.append(node)
for neighbor in self.dependencies.get(node, []):
dfs(neighbor, path.copy())
colors[node] = BLACK
for node in self.dependencies:
if colors[node] == WHITE:
dfs(node, [])
return result
def _validate_missing_dependencies(self) -> ValidationResult:
available = {row.reference_code for row in self.template.rows if row.reference_code}
result = ValidationResult()
for ref_code, deps in self.dependencies.items():
undefined = [d for d in deps if d not in available]
if undefined:
row_idx = self._get_row_idx(ref_code)
result.add_error(
ValidationIssue(
message=f"Line References undefined in Formula: {', '.join(undefined)}",
row_idx=row_idx,
)
)
return result
def _get_row_idx(self, reference_code: str) -> int | None:
for row in self.template.rows:
if row.reference_code == reference_code:
return row.idx
return None
class CalculationFormulaValidator(Validator):
"""Validates calculation formulas used in Calculated Amount rows"""
def __init__(self, reference_codes: set[str]):
self.reference_codes = reference_codes
def validate(self, row) -> ValidationResult:
"""Validate calculation formula for a single row"""
result = ValidationResult()
if row.data_source != "Calculated Amount":
return result
if not row.calculation_formula:
result.add_error(
ValidationIssue(
message="Formula is required for Calculated Amount",
row_idx=row.idx,
field="Formula",
)
)
return result
formula = self._preprocess_formula(row.calculation_formula)
row.calculation_formula = formula
# Check parentheses
if not self._are_parentheses_balanced(formula):
result.add_error(
ValidationIssue(
message="Formula has unbalanced parentheses",
row_idx=row.idx,
)
)
return result
# Check self-reference
available_codes = list(self.reference_codes)
refs = extract_reference_codes_from_formula(formula, available_codes)
if row.reference_code and row.reference_code in refs:
result.add_error(
ValidationIssue(
message=f"Formula references itself ('{row.reference_code}')",
row_idx=row.idx,
)
)
# Check undefined references
undefined = set(refs) - set(available_codes)
if undefined:
result.add_error(
ValidationIssue(
message=f"Formula references undefined codes: {', '.join(undefined)}",
row_idx=row.idx,
)
)
# Try to evaluate with dummy values
eval_error = self._test_formula_evaluation(formula, available_codes)
if eval_error:
result.add_error(
ValidationIssue(
message=f"Formula evaluation error: {eval_error}",
row_idx=row.idx,
)
)
return result
def _preprocess_formula(self, formula: str) -> str:
if not formula or not isinstance(formula, str):
return ""
return formula.strip()
@staticmethod
def _are_parentheses_balanced(formula: str) -> bool:
return formula.count("(") == formula.count(")")
def _test_formula_evaluation(self, formula: str, available_codes: list[str]) -> str | None:
try:
context = {code: 1.0 for code in available_codes}
context.update(
{
"abs": abs,
"round": round,
"min": min,
"max": max,
"sum": sum,
"sqrt": lambda x: x**0.5,
"pow": pow,
"ceil": lambda x: int(x) + (1 if x % 1 else 0),
"floor": lambda x: int(x),
}
)
result = frappe.safe_eval(formula, eval_globals=None, eval_locals=context)
if not isinstance(result, (int, float)): # noqa: UP038
return f"Formula must return a numeric value, got {type(result).__name__}"
return None
except Exception as e:
return str(e)
class AccountFilterValidator(Validator):
"""Validates account filter expressions used in Account Data rows"""
def __init__(self, account_fields: set | None = None):
self.account_fields = account_fields or set(frappe.get_meta("Account")._valid_columns)
def validate(self, row) -> ValidationResult:
result = ValidationResult()
if row.data_source != "Account Data":
return result
if not row.calculation_formula:
result.add_error(
ValidationIssue(
message="Account filter is required for Account Data",
row_idx=row.idx,
field="Formula",
)
)
return result
try:
filter_config = json.loads(row.calculation_formula)
error = self._validate_filter_structure(filter_config, self.account_fields)
if error:
result.add_error(
ValidationIssue(
message=error,
row_idx=row.idx,
field="Account Filter",
)
)
except json.JSONDecodeError as e:
result.add_error(
ValidationIssue(
message=f"Invalid JSON format: {e!s}",
row_idx=row.idx,
field="Account Filter",
)
)
return result
def _validate_filter_structure(self, filter_config, account_fields: set) -> str | None:
# simple condition: [field, operator, value]
if isinstance(filter_config, list):
if len(filter_config) != 3:
return "Filter must be [field, operator, value]"
field, operator, value = filter_config
if not isinstance(field, str) or not isinstance(operator, str):
return "Field and operator must be strings"
if field not in account_fields:
return f"Field '{field}' is not a valid account field"
if operator.casefold() not in OPERATOR_MAP:
return f"Invalid operator '{operator}'"
if operator in ["in", "not in"] and not isinstance(value, list):
return f"Operator '{operator}' requires a list value"
# logical condition: {"and": [condition1, condition2]}
elif isinstance(filter_config, dict):
if len(filter_config) != 1:
return "Logical condition must have exactly one operator"
op = next(iter(filter_config.keys())).lower()
if op not in ["and", "or"]:
return "Logical operators must be 'and' or 'or'"
conditions = filter_config[next(iter(filter_config.keys()))]
if not isinstance(conditions, list) or len(conditions) < 1:
return "Logical conditions need at least 1 sub-condition"
# recursive
for condition in conditions:
error = self._validate_filter_structure(condition, account_fields)
if error:
return error
else:
return "Filter must be a list or dict"
return None
class FormulaValidator(Validator):
def __init__(self, template):
self.template = template
reference_codes = {row.reference_code for row in template.rows if row.reference_code}
self.calculation_validator = CalculationFormulaValidator(reference_codes)
self.account_filter_validator = AccountFilterValidator()
def validate(self, row, account_fields: set) -> ValidationResult:
result = ValidationResult()
if not row.calculation_formula:
return result
if row.data_source == "Calculated Amount":
return self.calculation_validator.validate(row)
elif row.data_source == "Account Data":
# Update account fields if provided
if account_fields:
self.account_filter_validator.account_fields = account_fields
return self.account_filter_validator.validate(row)
elif row.data_source == "Custom API":
result.merge(self._validate_custom_api(row))
return result
def _validate_custom_api(self, row) -> ValidationResult:
result = ValidationResult()
api_path = row.calculation_formula
if "." not in api_path:
result.add_error(
ValidationIssue(
message="Custom API path should be in format: app.module.method",
row_idx=row.idx,
field="Formula",
)
)
return result
# Method exists?
try:
module_path, method_name = api_path.rsplit(".", 1)
module = frappe.get_module(module_path)
if not hasattr(module, method_name):
result.add_error(
ValidationIssue(
message=f"Method '{method_name}' not found in module '{module_path}' (might be environment-specific)",
row_idx=row.idx,
field="Formula",
)
)
except Exception as e:
result.add_error(
ValidationIssue(
message=f"Could not validate API path: {e!s}",
row_idx=row.idx,
field="Formula",
)
)
return result
def extract_reference_codes_from_formula(formula: str, available_codes: list[str]) -> list[str]:
found_codes = []
for code in available_codes:
# Match complete words only to avoid partial matches
pattern = r"\b" + re.escape(code) + r"\b"
if re.search(pattern, formula):
found_codes.append(code)
return found_codes

View File

@@ -0,0 +1,79 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.tests import IntegrationTestCase
from frappe.tests.utils import make_test_records
# 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 TestFinancialReportTemplate(IntegrationTestCase):
pass
class FinancialReportTemplateTestCase(IntegrationTestCase):
"""Utility class with common setup and helper methods for all test classes"""
@classmethod
def setUpClass(cls):
"""Set up test data"""
make_test_records("Company")
make_test_records("Fiscal Year")
cls.create_test_template()
@classmethod
def create_test_template(cls):
"""Create a test financial report template"""
if not frappe.db.exists("Financial Report Template", "Test P&L Template"):
template = frappe.get_doc(
{
"doctype": "Financial Report Template",
"template_name": "Test P&L Template",
"report_type": "Profit and Loss Statement",
"rows": [
{
"reference_code": "INC001",
"display_name": "Income",
"indentation_level": 0,
"data_source": "Account Data",
"balance_type": "Closing Balance",
"bold_text": 1,
"calculation_formula": '["root_type", "=", "Income"]',
},
{
"reference_code": "EXP001",
"display_name": "Expenses",
"indentation_level": 0,
"data_source": "Account Data",
"balance_type": "Closing Balance",
"bold_text": 1,
"calculation_formula": '["root_type", "=", "Expense"]',
},
{
"reference_code": "NET001",
"display_name": "Net Profit/Loss",
"indentation_level": 0,
"data_source": "Calculated Amount",
"bold_text": 1,
"calculation_formula": "INC001 - EXP001",
},
],
}
)
template.insert()
cls.test_template = frappe.get_doc("Financial Report Template", "Test P&L Template")
@staticmethod
def create_test_template_with_rows(rows_data):
"""Helper method to create test template with specific rows"""
template_name = f"Test Template {frappe.generate_hash()[:8]}"
template = frappe.get_doc(
{"doctype": "Financial Report Template", "template_name": template_name, "rows": rows_data}
)
return template

View File

@@ -99,7 +99,7 @@ class FiscalYear(Document):
)
overlap = False
if not self.get("companies") or not company_for_existing:
if not self.get("companies") and not company_for_existing:
overlap = True
for d in self.get("companies"):

View File

@@ -25,6 +25,27 @@ class TestFiscalYear(IntegrationTestCase):
self.assertRaises(frappe.exceptions.InvalidDates, fy.insert)
def test_company_fiscal_year_overlap(self):
for name in ["_Test Global FY 2001", "_Test Company FY 2001"]:
if frappe.db.exists("Fiscal Year", name):
frappe.delete_doc("Fiscal Year", name)
global_fy = frappe.new_doc("Fiscal Year")
global_fy.year = "_Test Global FY 2001"
global_fy.year_start_date = "2001-04-01"
global_fy.year_end_date = "2002-03-31"
global_fy.insert()
company_fy = frappe.new_doc("Fiscal Year")
company_fy.year = "_Test Company FY 2001"
company_fy.year_start_date = "2001-01-01"
company_fy.year_end_date = "2001-12-31"
company_fy.append("companies", {"company": "_Test Company"})
company_fy.insert()
self.assertTrue(frappe.db.exists("Fiscal Year", global_fy.name))
self.assertTrue(frappe.db.exists("Fiscal Year", company_fy.name))
def test_record_generator():
test_records = [

View File

@@ -0,0 +1,63 @@
{
"actions": [],
"creation": "2025-07-17 12:24:05.609186",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"item_row",
"tax_row",
"rate",
"amount",
"taxable_amount"
],
"fields": [
{
"fieldname": "item_row",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Item Row",
"reqd": 1
},
{
"fieldname": "tax_row",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Tax Row",
"reqd": 1
},
{
"fieldname": "rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Tax Rate"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Tax Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "taxable_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Taxable Amount",
"options": "Company:company:default_currency"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-09-26 15:54:19.750714",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Wise Tax Detail",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,27 @@
# 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 ItemWiseTaxDetail(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
amount: DF.Currency
item_row: DF.Data
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
rate: DF.Float
tax_row: DF.Data
taxable_amount: DF.Currency
# end: auto-generated types
pass

View File

@@ -111,6 +111,10 @@ frappe.ui.form.on("Journal Entry", {
}
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
$.each(frm.doc.accounts || [], function (i, row) {
erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name);
});
},
before_save: function (frm) {
if (frm.doc.docstatus == 0 && !frm.doc.is_system_generated) {

View File

@@ -512,7 +512,7 @@ class JournalEntry(AccountsController):
if (
d.reference_type == "Asset"
and d.reference_name
and d.account_type == "Depreciation"
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
and d.debit
):
asset = frappe.get_cached_doc("Asset", d.reference_name)
@@ -1831,8 +1831,8 @@ def get_exchange_rate(
# The date used to retreive the exchange rate here is the date passed
# in as an argument to this function.
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
else:
exchange_rate = 1

View File

@@ -106,7 +106,6 @@
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"no_copy": 1,
"options": "Currency",
"print_hide": 1,
"read_only": 1
@@ -271,7 +270,8 @@
"label": "Advance Voucher Type",
"no_copy": 1,
"options": "DocType",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "advance_voucher_no",
@@ -279,13 +279,14 @@
"label": "Advance Voucher No",
"no_copy": 1,
"options": "advance_voucher_type",
"read_only": 1
"read_only": 1,
"search_index": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-09-29 13:01:48.916517",
"modified": "2025-10-27 13:48:32.805100",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -71,8 +71,8 @@ class OpeningInvoiceCreationTool(Document):
max_count = {}
fields = [
"company",
"count(name) as total_invoices",
"sum(outstanding_amount) as outstanding_amount",
{"COUNT": "*", "as": "total_invoices"},
{"SUM": "outstanding_amount", "as": "outstanding_amount"},
]
companies = frappe.get_all("Company", fields=["name as company", "default_currency as currency"])
if not companies:

View File

@@ -577,6 +577,8 @@ frappe.ui.form.on("Payment Entry", {
paid_from: function (frm) {
if (frm.set_party_account_based_on_party) return;
frm.events.set_company_bank_account(frm);
frm.events.set_account_currency_and_balance(
frm,
frm.doc.paid_from,
@@ -593,6 +595,8 @@ frappe.ui.form.on("Payment Entry", {
paid_to: function (frm) {
if (frm.set_party_account_based_on_party) return;
frm.events.set_company_bank_account(frm);
frm.events.set_account_currency_and_balance(
frm,
frm.doc.paid_to,
@@ -1325,6 +1329,8 @@ frappe.ui.form.on("Payment Entry", {
},
bank_account: function (frm) {
if (frm.set_company_bank_account_based_on_coa) return;
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) {
frappe.call({
@@ -1363,6 +1369,34 @@ frappe.ui.form.on("Payment Entry", {
}
},
set_company_bank_account: function (frm) {
if (!["Pay", "Receive"].includes(frm.doc.payment_type)) return;
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
if (!frm.doc.company || !frm.doc[field]) return;
frm.set_company_bank_account_based_on_coa = true;
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: "Bank Account",
filters: {
company: frm.doc.company,
account: frm.doc[field],
disabled: 0,
},
fieldname: ["name"],
},
callback: async function (r) {
if (r.message) await frm.set_value("bank_account", r.message.name);
frm.set_company_bank_account_based_on_coa = false;
},
});
},
sales_taxes_and_charges_template: function (frm) {
frm.trigger("fetch_taxes_from_template");
},
@@ -1421,7 +1455,6 @@ frappe.ui.form.on("Payment Entry", {
$.each(frm.doc["taxes"] || [], function (i, tax) {
frm.events.validate_taxes_and_charges(tax);
frm.events.validate_inclusive_tax(tax);
tax.item_wise_tax_detail = {};
let tax_fields = [
"total",
"tax_fraction_for_current_item",

View File

@@ -1437,6 +1437,7 @@ class PaymentEntry(AccountsController):
else allocated_amount_in_company_currency / self.transaction_exchange_rate,
"advance_voucher_type": d.advance_voucher_type,
"advance_voucher_no": d.advance_voucher_no,
"transaction_exchange_rate": self.target_exchange_rate,
},
item=self,
)
@@ -1873,7 +1874,7 @@ class PaymentEntry(AccountsController):
else:
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += tax.base_tax_amount
self.base_total_taxes_and_charges += current_tax_amount
if self.get("taxes"):
self.paid_amount_after_tax = self.get("taxes")[-1].base_total

View File

@@ -61,6 +61,22 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
},
};
});
this.frm.set_query("cost_center", "payments", () => {
return {
filters: {
company: this.frm.doc.company,
is_group: 0,
},
};
});
this.frm.set_query("cost_center", "allocation", () => {
return {
filters: {
company: this.frm.doc.company,
is_group: 0,
},
};
});
}
refresh() {
@@ -385,6 +401,16 @@ frappe.ui.form.on("Payment Reconciliation Allocation", {
// filter payment
let payment = frm.doc.payments.filter((x) => x.reference_name == row.reference_name);
let amount = payment[0].amount;
for (const d of frm.doc.allocation) {
if (row.reference_name == d.reference_name && amount) {
if (d.allocated_amount <= amount) {
d.amount = amount;
amount -= d.allocated_amount;
}
}
}
frm.call({
doc: frm.doc,
method: "calculate_difference_on_allocation_change",

View File

@@ -72,7 +72,7 @@ class PaymentReconciliation(Document):
self.common_filter_conditions = []
self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = []
self.dimensions = get_dimensions()[0]
self.dimensions = get_dimensions(with_cost_center_and_project=True)[0]
def load_from_db(self):
# 'modified' attribute is required for `run_doc_method` to work properly.
@@ -669,7 +669,7 @@ class PaymentReconciliation(Document):
"party": self.party,
},
fields=[
"parent as `name`",
"parent as name",
"exchange_rate",
],
as_list=1,

View File

@@ -975,7 +975,7 @@ class TestPaymentReconciliation(IntegrationTestCase):
total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(credit) as amount",
[{"SUM": "credit", "as": "amount"}],
group_by="reference_name",
)[0].amount
@@ -1069,7 +1069,7 @@ class TestPaymentReconciliation(IntegrationTestCase):
total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(credit) as amount",
[{"SUM": "credit", "as": "amount"}],
group_by="reference_name",
)[0].amount

View File

@@ -4,6 +4,8 @@
frappe.ui.form.on("Period Closing Voucher", {
onload: function (frm) {
if (!frm.doc.transaction_date) frm.doc.transaction_date = frappe.datetime.obj_to_str(new Date());
frm.ignore_doctypes_on_cancel_all = ["Process Period Closing Voucher"];
},
setup: function (frm) {

View File

@@ -132,7 +132,11 @@ class PeriodClosingVoucher(AccountsController):
def on_submit(self):
self.db_set("gle_processing_status", "In Progress")
self.make_gl_entries()
if frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"):
self.make_gl_entries()
else:
ppcv = frappe.get_doc({"doctype": "Process Period Closing Voucher", "parent_pcv": self.name})
ppcv.save().submit()
def on_cancel(self):
self.ignore_linked_doctypes = (
@@ -140,11 +144,29 @@ class PeriodClosingVoucher(AccountsController):
"Stock Ledger Entry",
"Payment Ledger Entry",
"Account Closing Balance",
"Process Period Closing Voucher",
)
self.block_if_future_closing_voucher_exists()
if not frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"):
self.cancel_process_pcv_docs()
self.db_set("gle_processing_status", "In Progress")
self.cancel_gl_entries()
def cancel_process_pcv_docs(self):
ppcvs = frappe.db.get_all("Process Period Closing Voucher", {"parent_pcv": self.name, "docstatus": 1})
for x in ppcvs:
frappe.get_doc("Process Period Closing Voucher", x.name).cancel()
def on_trash(self):
super().on_trash()
ppcvs = frappe.db.get_all(
"Process Period Closing Voucher", {"parent_pcv": self.name, "docstatus": ["in", [1, 2]]}
)
for x in ppcvs:
frappe.delete_doc("Process Period Closing Voucher", x.name, force=True, ignore_permissions=True)
def make_gl_entries(self):
if frappe.db.estimate_count("GL Entry") > 100_000:
frappe.enqueue(
@@ -453,8 +475,15 @@ def process_gl_and_closing_entries(doc):
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Failed")
frappe.log_error(title=_("Period Closing Voucher {0} GL Entry Processing Failed").format(doc.name))
frappe.db.set_value(
doc.doctype,
doc.name,
{
"error_message": str(e),
"gle_processing_status": "Failed",
},
)
def process_cancellation(voucher_type, voucher_no):
@@ -466,8 +495,17 @@ def process_cancellation(voucher_type, voucher_no):
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
frappe.log_error(
title=_("Period Closing Voucher {0} GL Entry Cancellation Failed").format(voucher_no)
)
frappe.db.set_value(
voucher_type,
voucher_no,
{
"error_message": str(e),
"gle_processing_status": "Failed",
},
)
def delete_closing_entries(voucher_no):

View File

@@ -13,6 +13,10 @@ from erpnext.accounts.utils import get_fiscal_year
class TestPeriodClosingVoucher(IntegrationTestCase):
def setUp(self):
super().setUp()
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
def test_closing_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")

View File

@@ -70,6 +70,7 @@
"taxes",
"sec_tax_breakup",
"other_charges_calculation",
"item_wise_tax_details",
"section_break_43",
"base_total_taxes_and_charges",
"column_break_47",
@@ -1602,6 +1603,14 @@
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
},
{
"fieldname": "item_wise_tax_details",
"fieldtype": "Table",
"hidden": 1,
"label": "Item Wise Tax Details",
"no_copy": 1,
"options": "Item Wise Tax Detail"
}
],
"icon": "fa fa-file-text",

View File

@@ -31,6 +31,7 @@ class POSInvoice(SalesInvoice):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.item_wise_tax_detail.item_wise_tax_detail import ItemWiseTaxDetail
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
@@ -99,6 +100,7 @@ class POSInvoice(SalesInvoice):
is_opening: DF.Literal["No", "Yes"]
is_pos: DF.Check
is_return: DF.Check
item_wise_tax_details: DF.Table[ItemWiseTaxDetail]
items: DF.Table[POSInvoiceItem]
language: DF.Data | None
letter_head: DF.Link | None
@@ -189,6 +191,9 @@ class POSInvoice(SalesInvoice):
super().__init__(*args, **kwargs)
def validate(self):
if not self.customer:
frappe.throw(_("Please select Customer first"))
if not cint(self.is_pos):
frappe.throw(
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
@@ -388,14 +393,14 @@ class POSInvoice(SalesInvoice):
):
return
from erpnext.stock.stock_ledger import is_negative_stock_allowed
for d in self.get("items"):
if not d.serial_and_batch_bundle:
if is_negative_stock_allowed(item_code=d.item_code):
return
available_stock, is_stock_item, is_negative_stock_allowed = get_stock_availability(
d.item_code, d.warehouse
)
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
if is_negative_stock_allowed:
continue
item_code, warehouse, _qty = (
frappe.bold(d.item_code),
@@ -853,20 +858,22 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
from erpnext.stock.stock_ledger import is_negative_stock_allowed
if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty, is_stock_item
return bin_qty - pos_sales_qty, is_stock_item, is_negative_stock_allowed(item_code=item_code)
else:
is_stock_item = True
if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
return get_bundle_availability(item_code, warehouse), is_stock_item
return get_bundle_availability(item_code, warehouse), is_stock_item, False
else:
is_stock_item = False
# Is a service item or non_stock item
return 0, is_stock_item
return 0, is_stock_item, False
def get_bundle_availability(bundle_item_code, warehouse):

View File

@@ -160,7 +160,6 @@
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "200px",
"reqd": 1,
"width": "200px"
},
{
@@ -858,14 +857,15 @@
],
"istable": 1,
"links": [],
"modified": "2024-05-07 15:56:53.343317",
"modified": "2025-11-12 18:11:11.818015",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -36,7 +36,7 @@ class POSInvoiceItem(SalesInvoiceItem):
delivered_by_supplier: DF.Check
delivered_qty: DF.Float
delivery_note: DF.Link | None
description: DF.TextEditor
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency

View File

@@ -17,7 +17,6 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
from erpnext.controllers.taxes_and_totals import ItemWiseTaxDetail
class POSInvoiceMergeLog(Document):
@@ -156,7 +155,6 @@ class POSInvoiceMergeLog(Document):
sales_invoice.save()
sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name
return sales_invoice
@@ -207,7 +205,7 @@ class POSInvoiceMergeLog(Document):
return return_invoices
def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], []
items, payments, taxes, item_tax_details = [], [], [], []
loyalty_amount_sum, loyalty_points_sum = 0, 0
@@ -217,6 +215,8 @@ class POSInvoiceMergeLog(Document):
loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1
for doc in data:
old_new_item_map = frappe._dict()
old_new_tax_map = frappe._dict()
map_doc(doc, invoice, table_map={"doctype": invoice.doctype})
if doc.get("posting_date"):
@@ -244,6 +244,7 @@ class POSInvoiceMergeLog(Document):
if item.serial_and_batch_bundle:
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
items.append(si_item)
old_new_item_map[item.name] = si_item
for tax in doc.get("taxes"):
found = False
@@ -253,7 +254,7 @@ class POSInvoiceMergeLog(Document):
t.base_tax_amount = flt(t.base_tax_amount) + flt(
tax.base_tax_amount_after_discount_amount
)
update_item_wise_tax_detail(t, tax)
old_new_tax_map[tax.name] = t
found = True
if not found:
tax.charge_type = "Actual"
@@ -263,8 +264,9 @@ class POSInvoiceMergeLog(Document):
tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
tax.item_wise_tax_detail = tax.item_wise_tax_detail
tax.dont_recompute_tax = 1
taxes.append(tax)
old_new_tax_map[tax.name] = tax
for payment in doc.get("payments"):
found = False
@@ -281,6 +283,16 @@ class POSInvoiceMergeLog(Document):
base_rounding_adjustment += doc.base_rounding_adjustment
base_rounded_total += doc.base_rounded_total
for d in doc.get("item_wise_tax_details"):
row = frappe._dict(
item=old_new_item_map[d.item_row],
tax=old_new_tax_map[d.tax_row],
amount=d.amount,
rate=d.rate,
taxable_amount=d.taxable_amount,
)
item_tax_details.append(row)
if loyalty_points_sum:
invoice.redeem_loyalty_points = 1
invoice.loyalty_points = loyalty_points_sum
@@ -342,6 +354,7 @@ class POSInvoiceMergeLog(Document):
invoice.set("sales_partner", None)
invoice.set("commission_rate", 0)
invoice.set("total_commission", 0)
invoice._item_wise_tax_details = item_tax_details
return invoice
@@ -419,24 +432,6 @@ class POSInvoiceMergeLog(Document):
si.cancel()
def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail)
tax_row_detail = json.loads(tax_row.item_wise_tax_detail)
if not consolidated_tax_detail:
consolidated_tax_detail = {}
for item_code, tax_data in tax_row_detail.items():
tax_data = ItemWiseTaxDetail(**tax_data)
if consolidated_tax_detail.get(item_code):
consolidated_tax_detail[item_code]["tax_amount"] += tax_data.tax_amount
consolidated_tax_detail[item_code]["net_amount"] += tax_data.net_amount
else:
consolidated_tax_detail.update({item_code: tax_data})
consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail)
def get_all_unconsolidated_invoices():
filters = {
"consolidated_invoice": ["in", ["", None]],

View File

@@ -164,20 +164,36 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
inv.load_from_db()
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
item_wise_tax_detail = json.loads(consolidated_invoice.get("taxes")[0].item_wise_tax_detail)
expected_item_wise_tax_detail = {
"_Test Item": {
"tax_rate": 9,
"tax_amount": 9,
"net_amount": 100,
expected_item_wise_tax_details = [
{
"item_row": consolidated_invoice.items[0].name,
"tax_row": consolidated_invoice.taxes[0].name,
"rate": 9.0,
"amount": 9.0,
"taxable_amount": 100.0,
},
"_Test Item 2": {
"tax_rate": 5,
"tax_amount": 5,
"net_amount": 100,
{
"item_row": consolidated_invoice.items[1].name,
"tax_row": consolidated_invoice.taxes[0].name,
"rate": 5.0,
"amount": 5.0,
"taxable_amount": 100.0,
},
}
self.assertEqual(item_wise_tax_detail, expected_item_wise_tax_detail)
]
actual = [
{
"item_row": d.item_row,
"tax_row": d.tax_row,
"rate": d.rate,
"amount": d.amount,
"taxable_amount": d.taxable_amount,
}
for d in consolidated_invoice.get("item_wise_tax_details")
]
self.assertEqual(actual, expected_item_wise_tax_details)
def test_consolidation_round_off_error_1(self):
"""

View File

@@ -43,9 +43,19 @@ class POSOpeningEntry(StatusUpdater):
self.set_status()
def validate_pos_profile_and_cashier(self):
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
if not frappe.db.exists("POS Profile", self.pos_profile):
frappe.throw(_("POS Profile {} does not exist.").format(self.pos_profile))
pos_profile_company, pos_profile_disabled = frappe.db.get_value(
"POS Profile", self.pos_profile, ["company", "disabled"]
)
if pos_profile_disabled:
frappe.throw(_("POS Profile {} is disabled.").format(frappe.bold(self.pos_profile)))
if self.company != pos_profile_company:
frappe.throw(
_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company)
_("POS Profile {} does not belong to company {}").format(self.pos_profile, self.company)
)
if not cint(frappe.db.get_value("User", self.user, "enabled")):

View File

@@ -40,6 +40,12 @@ class TestPOSOpeningEntry(IntegrationTestCase):
self.assertEqual(opening_entry.status, "Open")
self.assertNotEqual(opening_entry.docstatus, 0)
def test_pos_opening_entry_on_disabled_pos(self):
test_user, pos_profile = self.init_user_and_profile(disabled=1)
with self.assertRaises(frappe.ValidationError):
create_opening_entry(pos_profile, test_user.name)
def test_multiple_pos_opening_entries_for_same_pos_profile(self):
test_user, pos_profile = self.init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)

View File

@@ -75,6 +75,7 @@ class POSProfile(Document):
# end: auto-generated types
def validate(self):
self.validate_disabled()
self.validate_default_profile()
self.validate_all_link_fields()
self.validate_duplicate_groups()
@@ -99,6 +100,21 @@ class POSProfile(Document):
title=_("Mandatory Accounting Dimension"),
)
def validate_disabled(self):
old_doc = self.get_doc_before_save()
if (
old_doc
and self.disabled
and old_doc.disabled != self.disabled
and frappe.db.exists("POS Opening Entry", {"pos_profile": self.name, "status": "Open"})
):
frappe.throw(
_("POS Profile {0} cannot be disabled as there are ongoing POS sessions.").format(
frappe.bold(self.name)
)
)
def validate_default_profile(self):
for row in self.applicable_for_users:
res = frappe.db.sql(

View File

@@ -4,6 +4,7 @@ import unittest
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import cint
from erpnext.accounts.doctype.pos_profile.pos_profile import (
get_child_nodes,
@@ -38,6 +39,50 @@ class TestPOSProfile(IntegrationTestCase):
frappe.db.sql("delete from `tabPOS Profile`")
def test_disabled_pos_profile_creation(self):
make_pos_profile(name="_Test POS Profile 001", disabled=1)
pos_profile = frappe.get_doc("POS Profile", "_Test POS Profile 001")
if pos_profile:
self.assertEqual(pos_profile.disabled, 1)
def test_disabled_pos_profile_after_opening(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
test_user, pos_profile = init_user_and_profile()
if pos_profile:
create_opening_entry(pos_profile, test_user.name)
self.assertEqual(pos_profile.disabled, 0)
pos_profile.disabled = 1
self.assertRaises(frappe.ValidationError, pos_profile.save)
def test_disabled_pos_profile_after_completing_session(self):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import (
create_opening_entry,
)
test_user, pos_profile = init_user_and_profile()
if pos_profile:
opening_entry = create_opening_entry(pos_profile, test_user.name)
closing_entry = make_closing_entry_from_opening(opening_entry)
closing_entry.submit()
pos_profile.disabled = 1
pos_profile.save()
pos_profile.reload()
self.assertEqual(pos_profile.disabled, 1)
def get_customers_list(pos_profile=None):
if pos_profile is None:
@@ -117,6 +162,7 @@ def make_pos_profile(**args):
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC",
"location": "Block 1" if not args.do_not_set_accounting_dimension else None,
"disabled": cint(args.disabled) or 0,
}
)

View File

@@ -713,6 +713,7 @@ def get_item_uoms(doctype, txt, searchfield, start, page_len, filters):
return frappe.get_all(
"UOM Conversion Detail",
filters={"parent": ("in", items), "uom": ("like", f"{txt}%")},
fields=["distinct uom"],
fields=["uom"],
as_list=1,
distinct=True,
)

View File

@@ -243,10 +243,13 @@ def get_other_conditions(conditions, values, args):
if group_condition:
conditions += " and " + group_condition
if args.get("transaction_date"):
date = args.get("transaction_date") or frappe.get_value(
args.get("doctype"), args.get("name"), "posting_date", ignore=True
)
if date:
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = args.get("transaction_date")
values["transaction_date"] = date
if args.get("doctype") in [
"Quotation",

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Process Period Closing Voucher", {
refresh(frm) {
if (frm.doc.docstatus == 1 && ["Queued"].find((x) => x == frm.doc.status)) {
let execute_btn = __("Start");
frm.add_custom_button(execute_btn, () => {
frm.call({
method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.start_pcv_processing",
args: {
docname: frm.doc.name,
},
}).then((r) => {
if (!r.exc) {
frappe.show_alert(__("Job Started"));
frm.reload_doc();
}
});
});
}
if (frm.doc.docstatus == 1 && ["Running"].find((x) => x == frm.doc.status)) {
let execute_btn = __("Pause");
frm.add_custom_button(execute_btn, () => {
frm.call({
method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.pause_pcv_processing",
args: {
docname: frm.doc.name,
},
}).then((r) => {
if (!r.exc) {
frappe.show_alert(__("PCV Paused"));
frm.reload_doc();
}
});
});
}
if (frm.doc.docstatus == 1 && ["Paused"].find((x) => x == frm.doc.status)) {
let execute_btn = __("Resume");
frm.add_custom_button(execute_btn, () => {
frm.call({
method: "erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.resume_pcv_processing",
args: {
docname: frm.doc.name,
},
}).then((r) => {
if (!r.exc) {
frappe.show_alert(__("PCV Resumed"));
frm.reload_doc();
}
});
});
}
// progress bar
let progress = 0;
let normal_finished = frm.doc.normal_balances.filter((x) => x.status == "Completed").length;
let opening_finished = frm.doc.z_opening_balances.filter((x) => x.status == "Completed").length;
progress =
((normal_finished + opening_finished) /
(frm.doc.normal_balances.length + frm.doc.z_opening_balances.length)) *
100;
frm.dashboard.add_progress("Books closure progress", progress, "");
},
});

View File

@@ -0,0 +1,113 @@
{
"actions": [],
"autoname": "format:Process-PCV-{###}",
"creation": "2025-09-25 15:44:03.534699",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"parent_pcv",
"status",
"p_l_closing_balance",
"normal_balances",
"bs_closing_balance",
"z_opening_balances",
"amended_from"
],
"fields": [
{
"fieldname": "parent_pcv",
"fieldtype": "Link",
"in_list_view": 1,
"label": "PCV",
"options": "Period Closing Voucher",
"reqd": 1
},
{
"default": "Queued",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
"options": "Queued\nRunning\nPaused\nCompleted\nCancelled"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Process Period Closing Voucher",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "p_l_closing_balance",
"fieldtype": "JSON",
"label": "P&L Closing Balance",
"no_copy": 1
},
{
"fieldname": "normal_balances",
"fieldtype": "Table",
"label": "Dates to Process",
"no_copy": 1,
"options": "Process Period Closing Voucher Detail"
},
{
"fieldname": "z_opening_balances",
"fieldtype": "Table",
"label": "Opening Balances",
"no_copy": 1,
"options": "Process Period Closing Voucher Detail"
},
{
"fieldname": "bs_closing_balance",
"fieldtype": "JSON",
"label": "Balance Sheet Closing Balance"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-11-05 11:40:24.996403",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Period Closing Voucher",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,559 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import copy
from datetime import timedelta
import frappe
from frappe import qb
from frappe.model.document import Document
from frappe.query_builder.functions import Count, Max, Min, Sum
from frappe.utils import add_days, flt, get_datetime
from frappe.utils.scheduler import is_scheduler_inactive
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
class ProcessPeriodClosingVoucher(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.process_period_closing_voucher_detail.process_period_closing_voucher_detail import (
ProcessPeriodClosingVoucherDetail,
)
amended_from: DF.Link | None
bs_closing_balance: DF.JSON | None
normal_balances: DF.Table[ProcessPeriodClosingVoucherDetail]
p_l_closing_balance: DF.JSON | None
parent_pcv: DF.Link
status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"]
z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail]
# end: auto-generated types
def validate(self):
self.status = "Queued"
self.populate_processing_tables()
def populate_processing_tables(self):
self.generate_pcv_dates()
self.generate_opening_balances_dates()
def get_dates(self, start, end):
return [start + timedelta(days=x) for x in range((end - start).days + 1)]
def generate_pcv_dates(self):
self.normal_balances = []
pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv)
dates = self.get_dates(get_datetime(pcv.period_start_date), get_datetime(pcv.period_end_date))
for x in dates:
self.append(
"normal_balances",
{"processing_date": x, "status": "Queued", "report_type": "Profit and Loss"},
)
self.append(
"normal_balances", {"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"}
)
def generate_opening_balances_dates(self):
self.z_opening_balances = []
pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv)
if pcv.is_first_period_closing_voucher():
gl = qb.DocType("GL Entry")
min = qb.from_(gl).select(Min(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0]
max = qb.from_(gl).select(Max(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0]
dates = self.get_dates(get_datetime(min), get_datetime(max))
for x in dates:
self.append(
"z_opening_balances",
{"processing_date": x, "status": "Queued", "report_type": "Balance Sheet"},
)
def on_submit(self):
start_pcv_processing(self.name)
def on_cancel(self):
cancel_pcv_processing(self.name)
@frappe.whitelist()
def start_pcv_processing(docname: str):
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
if normal_balances := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
fields=["processing_date", "report_type", "parentfield"],
order_by="parentfield, idx, processing_date",
limit=4,
):
if not is_scheduler_inactive():
for x in normal_balances:
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{
"processing_date": x.processing_date,
"parent": docname,
"report_type": x.report_type,
"parentfield": x.parentfield,
},
"status",
"Running",
)
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout="3600",
is_async=True,
enqueue_after_commit=True,
docname=docname,
date=x.processing_date,
report_type=x.report_type,
parentfield=x.parentfield,
)
else:
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
@frappe.whitelist()
def pause_pcv_processing(docname: str):
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
if queued_dates := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
pluck="name",
):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
qb.update(ppcvd).set(ppcvd.status, "Paused").where(ppcvd.name.isin(queued_dates)).run()
@frappe.whitelist()
def cancel_pcv_processing(docname: str):
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Cancelled").where(ppcv.name.eq(docname)).run()
if queued_dates := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
pluck="name",
):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
qb.update(ppcvd).set(ppcvd.status, "Cancelled").where(ppcvd.name.isin(queued_dates)).run()
@frappe.whitelist()
def resume_pcv_processing(docname: str):
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Running").where(ppcv.name.eq(docname)).run()
if paused_dates := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Paused"},
pluck="name",
):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
qb.update(ppcvd).set(ppcvd.status, "Queued").where(ppcvd.name.isin(paused_dates)).run()
start_pcv_processing(docname)
def update_default_dimensions(dimension_fields, gl_entry, dimension_values):
for i, dimension in enumerate(dimension_fields):
gl_entry[dimension] = dimension_values[i]
def get_gle_for_pl_account(pcv, acc, balances, dimensions):
balance_in_account_currency = flt(balances.debit_in_account_currency) - flt(
balances.credit_in_account_currency
)
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
gl_entry = frappe._dict(
{
"company": pcv.company,
"posting_date": pcv.period_end_date,
"account": acc,
"account_currency": balances.account_currency,
"debit_in_account_currency": abs(balance_in_account_currency)
if balance_in_account_currency < 0
else 0,
"debit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
"credit_in_account_currency": abs(balance_in_account_currency)
if balance_in_account_currency > 0
else 0,
"credit": abs(balance_in_company_currency) if balance_in_company_currency > 0 else 0,
"is_period_closing_voucher_entry": 1,
"voucher_type": "Period Closing Voucher",
"voucher_no": pcv.name,
"fiscal_year": pcv.fiscal_year,
"remarks": pcv.remarks,
"is_opening": "No",
}
)
# update dimensions
update_default_dimensions(get_dimensions(), gl_entry, dimensions)
return gl_entry
def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
balance_in_company_currency = flt(dimension_balance.balance_in_company_currency)
debit = balance_in_company_currency if balance_in_company_currency > 0 else 0
credit = abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0
gl_entry = frappe._dict(
{
"company": pcv.company,
"posting_date": pcv.period_end_date,
"account": pcv.closing_account_head,
"account_currency": frappe.db.get_value("Account", pcv.closing_account_head, "account_currency"),
"debit_in_account_currency": debit,
"debit": debit,
"credit_in_account_currency": credit,
"credit": credit,
"is_period_closing_voucher_entry": 1,
"voucher_type": "Period Closing Voucher",
"voucher_no": pcv.name,
"fiscal_year": pcv.fiscal_year,
"remarks": pcv.remarks,
"is_opening": "No",
}
)
# update dimensions
update_default_dimensions(get_dimensions(), gl_entry, dimensions)
return gl_entry
@frappe.whitelist()
def schedule_next_date(docname: str):
if to_process := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
fields=["processing_date", "report_type", "parentfield"],
order_by="parentfield, idx, processing_date",
limit=1,
):
if not is_scheduler_inactive():
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{
"processing_date": to_process[0].processing_date,
"parent": docname,
"report_type": to_process[0].report_type,
"parentfield": to_process[0].parentfield,
},
"status",
"Running",
)
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout="3600",
is_async=True,
enqueue_after_commit=True,
docname=docname,
date=to_process[0].processing_date,
report_type=to_process[0].report_type,
parentfield=to_process[0].parentfield,
)
else:
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
total_no_of_dates = (
qb.from_(ppcvd).select(Count(ppcvd.star)).where(ppcvd.parent.eq(docname)).run()[0][0]
)
completed = (
qb.from_(ppcvd)
.select(Count(ppcvd.star))
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Completed"))
.run()[0][0]
)
# Ensure both normal and opening balances are processed for all dates
if total_no_of_dates == completed:
summarize_and_post_ledger_entries(docname)
def make_dict_json_compliant(dimension_wise_balance) -> dict:
"""
convert tuple -> str
JSON doesn't support dictionary with tuple keys
"""
converted_dict = {}
for k, v in dimension_wise_balance.items():
str_key = [str(x) for x in k]
str_key = ",".join(str_key)
converted_dict[str_key] = v
return converted_dict
def get_consolidated_gles(balances, report_type) -> list:
gl_entries = []
for x in balances:
if x.report_type == report_type:
closing_balances = [frappe._dict(gle) for gle in frappe.json.loads(x.closing_balance)]
gl_entries.extend(closing_balances)
return gl_entries
def get_gl_entries(docname):
"""
Calculate total closing balance of all P&L accounts across PCV start and end date
"""
ppcv = frappe.get_doc("Process Period Closing Voucher", docname)
# calculate balance
gl_entries = get_consolidated_gles(ppcv.normal_balances, "Profit and Loss")
pl_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries)
# save
json_dict = make_dict_json_compliant(pl_dimension_wise_acc_balance)
frappe.db.set_value(
"Process Period Closing Voucher", docname, "p_l_closing_balance", frappe.json.dumps(json_dict)
)
# build gl map
pcv = frappe.get_doc("Period Closing Voucher", ppcv.parent_pcv)
pl_accounts_reverse_gle = []
closing_account_gle = []
for dimensions, account_balances in pl_dimension_wise_acc_balance.items():
for acc, balances in account_balances.items():
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
if balance_in_company_currency:
pl_accounts_reverse_gle.append(get_gle_for_pl_account(pcv, acc, balances, dimensions))
closing_account_gle.append(get_gle_for_closing_account(pcv, account_balances["balances"], dimensions))
return pl_accounts_reverse_gle, closing_account_gle
def calculate_balance_sheet_balance(docname):
"""
Calculate total closing balance of all P&L accounts across PCV start and end date.
If it is first PCV, opening entries are also considered
"""
ppcv = frappe.get_doc("Process Period Closing Voucher", docname)
gl_entries = get_consolidated_gles(ppcv.normal_balances + ppcv.z_opening_balances, "Balance Sheet")
# build dimension wise dictionary from all GLE's
bs_dimension_wise_acc_balance = build_dimension_wise_balance_dict(gl_entries)
# save
json_dict = make_dict_json_compliant(bs_dimension_wise_acc_balance)
frappe.db.set_value(
"Process Period Closing Voucher", docname, "bs_closing_balance", frappe.json.dumps(json_dict)
)
return bs_dimension_wise_acc_balance
def get_p_l_closing_entries(pl_gles, pcv):
pl_closing_entries = copy.deepcopy(pl_gles)
for d in pl_gles:
# reverse debit and credit
gle_copy = copy.deepcopy(d)
gle_copy.debit = d.credit
gle_copy.credit = d.debit
gle_copy.debit_in_account_currency = d.credit_in_account_currency
gle_copy.credit_in_account_currency = d.debit_in_account_currency
gle_copy.is_period_closing_voucher_entry = 0
gle_copy.period_closing_voucher = pcv.name
pl_closing_entries.append(gle_copy)
return pl_closing_entries
def get_bs_closing_entries(dimension_wise_balance, pcv):
closing_entries = []
for dimensions, account_balances in dimension_wise_balance.items():
for acc, balances in account_balances.items():
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
if acc != "balances" and balance_in_company_currency:
closing_entries.append(get_closing_entry(pcv, acc, balances, dimensions))
return closing_entries
def get_closing_account_closing_entry(closing_account_gle, pcv):
closing_entries_for_closing_account = copy.deepcopy(closing_account_gle)
for d in closing_entries_for_closing_account:
d.period_closing_voucher = pcv.name
return closing_entries_for_closing_account
def summarize_and_post_ledger_entries(docname):
# P&L accounts
pl_accounts_reverse_gle, closing_account_gle = get_gl_entries(docname)
gl_entries = pl_accounts_reverse_gle + closing_account_gle
from erpnext.accounts.general_ledger import make_gl_entries
if gl_entries:
make_gl_entries(gl_entries, merge_entries=False)
pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv")
pcv = frappe.get_doc("Period Closing Voucher", pcv_name)
# Balance sheet accounts
bs_dimension_wise_acc_balance = calculate_balance_sheet_balance(docname)
pl_closing_entries = get_p_l_closing_entries(pl_accounts_reverse_gle, pcv)
bs_closing_entries = get_bs_closing_entries(bs_dimension_wise_acc_balance, pcv)
closing_entries_for_closing_account = get_closing_account_closing_entry(closing_account_gle, pcv)
closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account
make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date)
frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed")
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
def get_closing_entry(pcv, account, balances, dimensions):
closing_entry = frappe._dict(
{
"company": pcv.company,
"closing_date": pcv.period_end_date,
"period_closing_voucher": pcv.name,
"account": account,
"account_currency": balances.account_currency,
"debit_in_account_currency": flt(balances.debit_in_account_currency),
"debit": flt(balances.debit),
"credit_in_account_currency": flt(balances.credit_in_account_currency),
"credit": flt(balances.credit),
"is_period_closing_voucher_entry": 0,
}
)
# update dimensions
update_default_dimensions(get_dimensions(), closing_entry, dimensions)
return closing_entry
def get_dimensions():
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
default_dimensions = ["cost_center", "finance_book", "project"]
dimensions = default_dimensions + get_accounting_dimensions()
return dimensions
def get_dimension_key(res):
return tuple([res.get(dimension) for dimension in get_dimensions()])
def build_dimension_wise_balance_dict(gl_entries):
dimension_balances = frappe._dict()
for x in gl_entries:
dimension_key = get_dimension_key(x)
dimension_balances.setdefault(dimension_key, frappe._dict()).setdefault(
x.account,
frappe._dict(
{
"debit_in_account_currency": 0,
"credit_in_account_currency": 0,
"debit": 0,
"credit": 0,
"account_currency": x.account_currency,
}
),
)
dimension_balances[dimension_key][x.account].debit_in_account_currency += flt(
x.debit_in_account_currency
)
dimension_balances[dimension_key][x.account].credit_in_account_currency += flt(
x.credit_in_account_currency
)
dimension_balances[dimension_key][x.account].debit += flt(x.debit)
dimension_balances[dimension_key][x.account].credit += flt(x.credit)
# dimension-wise total balances
dimension_balances[dimension_key].setdefault(
"balances",
frappe._dict(
{
"balance_in_account_currency": 0,
"balance_in_company_currency": 0,
}
),
)
balance_in_account_currency = flt(x.debit_in_account_currency) - flt(x.credit_in_account_currency)
balance_in_company_currency = flt(x.debit) - flt(x.credit)
dimension_balances[dimension_key][
"balances"
].balance_in_account_currency += balance_in_account_currency
dimension_balances[dimension_key][
"balances"
].balance_in_company_currency += balance_in_company_currency
return dimension_balances
def process_individual_date(docname: str, date, report_type, parentfield):
current_date_status = frappe.db.get_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "report_type": report_type, "parentfield": parentfield},
"status",
)
if current_date_status != "Running":
return
pcv_name = frappe.db.get_value("Process Period Closing Voucher", docname, "parent_pcv")
company = frappe.db.get_value("Period Closing Voucher", pcv_name, "company")
dimensions = get_dimensions()
accounts = frappe.db.get_all(
"Account", filters={"company": company, "report_type": report_type}, pluck="name"
)
# summarize
gle = qb.DocType("GL Entry")
query = qb.from_(gle).select(gle.account)
for dim in dimensions:
query = query.select(gle[dim])
query = query.select(
Sum(gle.debit).as_("debit"),
Sum(gle.credit).as_("credit"),
Sum(gle.debit_in_account_currency).as_("debit_in_account_currency"),
Sum(gle.credit_in_account_currency).as_("credit_in_account_currency"),
gle.account_currency,
).where(
(gle.company.eq(company))
& (gle.is_cancelled.eq(0))
& (gle.posting_date.eq(date))
& (gle.account.isin(accounts))
)
if parentfield == "z_opening_balances":
query = query.where(gle.is_opening.eq("Yes"))
query = query.groupby(gle.account)
for dim in dimensions:
query = query.groupby(gle[dim])
res = query.run(as_dict=True)
# save results
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
"closing_balance",
frappe.json.dumps(res),
)
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
"status",
"Completed",
)
# chain call
schedule_next_date(docname)

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# 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 IntegrationTestProcessPeriodClosingVoucher(IntegrationTestCase):
"""
Integration tests for ProcessPeriodClosingVoucher.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -0,0 +1,58 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-10-01 15:58:17.544153",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"processing_date",
"report_type",
"status",
"closing_balance"
],
"fields": [
{
"fieldname": "processing_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Processing Date"
},
{
"default": "Queued",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Queued\nRunning\nPaused\nCompleted\nCancelled"
},
{
"fieldname": "closing_balance",
"fieldtype": "JSON",
"in_list_view": 1,
"label": "Closing Balance"
},
{
"default": "Profit and Loss",
"fieldname": "report_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Report Type",
"options": "Profit and Loss\nBalance Sheet"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-10-20 12:03:59.106931",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Period Closing Voucher Detail",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,26 @@
# 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 ProcessPeriodClosingVoucherDetail(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
closing_balance: DF.JSON | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
processing_date: DF.Date | None
report_type: DF.Literal["Profit and Loss", "Balance Sheet"]
status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"]
# end: auto-generated types
pass

View File

@@ -71,14 +71,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger("supplier");
}
this.frm.set_query("supplier", function () {
return {
filters: {
is_transporter: 0,
},
};
});
}
refresh(doc) {

View File

@@ -112,6 +112,7 @@
"tax_withheld_vouchers",
"sec_tax_breakup",
"other_charges_calculation",
"item_wise_tax_details",
"pricing_rule_details",
"pricing_rules",
"raw_materials_supplied",
@@ -1670,6 +1671,14 @@
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "item_wise_tax_details",
"fieldtype": "Table",
"hidden": 1,
"label": "Item Wise Tax Details",
"no_copy": 1,
"options": "Item Wise Tax Detail"
}
],
"grid_page_length": 50,

View File

@@ -40,7 +40,6 @@ from erpnext.assets.doctype.asset_category.asset_category import get_asset_categ
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
update_billed_amount_based_on_po,
)
@@ -63,6 +62,7 @@ class PurchaseInvoice(BuyingController):
from frappe.types import DF
from erpnext.accounts.doctype.advance_tax.advance_tax import AdvanceTax
from erpnext.accounts.doctype.item_wise_tax_detail.item_wise_tax_detail import ItemWiseTaxDetail
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import (
@@ -137,6 +137,7 @@ class PurchaseInvoice(BuyingController):
is_paid: DF.Check
is_return: DF.Check
is_subcontracted: DF.Check
item_wise_tax_details: DF.Table[ItemWiseTaxDetail]
items: DF.Table[PurchaseInvoiceItem]
language: DF.Data | None
letter_head: DF.Link | None
@@ -460,11 +461,12 @@ class PurchaseInvoice(BuyingController):
self.asset_received_but_not_billed = None
inventory_account_map = {}
if self.update_stock:
self.validate_item_code()
self.validate_warehouse(for_validate)
if auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
inventory_account_map = self.get_inventory_account_map()
for item in self.get("items"):
# in case of auto inventory accounting,
@@ -481,21 +483,19 @@ class PurchaseInvoice(BuyingController):
)
):
if self.update_stock and item.warehouse and (not item.from_warehouse):
if (
for_validate
and item.expense_account
and item.expense_account != warehouse_account[item.warehouse]["account"]
):
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map)
if for_validate and item.expense_account and item.expense_account != _inv_dict["account"]:
msg = _(
"Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account"
).format(
item.idx,
frappe.bold(warehouse_account[item.warehouse]["account"]),
frappe.bold(_inv_dict["account"]),
frappe.bold(item.expense_account),
frappe.bold(item.warehouse),
)
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = warehouse_account[item.warehouse]["account"]
item.expense_account = _inv_dict["account"]
else:
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not
if item.purchase_receipt:
@@ -857,7 +857,7 @@ class PurchaseInvoice(BuyingController):
party=self.supplier,
)
def get_gl_entries(self, warehouse_account=None):
def get_gl_entries(self, inventory_account_map=None):
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if self.auto_accounting_for_stock:
@@ -947,7 +947,7 @@ class PurchaseInvoice(BuyingController):
# item gl entries
stock_items = self.get_stock_items()
if self.update_stock and self.auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
inventory_account_map = self.get_inventory_account_map()
landed_cost_entries = self.get_item_account_wise_lcv_entries()
@@ -997,18 +997,24 @@ class PurchaseInvoice(BuyingController):
)
if item.from_warehouse:
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map)
_inv_dict_from_warehouse = self.get_inventory_account_dict(
item, inventory_account_map, "from_warehouse"
)
gl_entries.append(
self.get_gl_dict(
{
"account": warehouse_account[item.warehouse]["account"],
"against": warehouse_account[item.from_warehouse]["account"],
"account": _inv_dict["account"],
"against": _inv_dict_from_warehouse["account"],
"cost_center": item.cost_center,
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": warehouse_debit_amount,
"debit_in_transaction_currency": item.net_amount,
},
warehouse_account[item.warehouse]["account_currency"],
_inv_dict["account_currency"],
item=item,
)
)
@@ -1021,15 +1027,15 @@ class PurchaseInvoice(BuyingController):
gl_entries.append(
self.get_gl_dict(
{
"account": warehouse_account[item.from_warehouse]["account"],
"against": warehouse_account[item.warehouse]["account"],
"account": _inv_dict_from_warehouse["account"],
"against": _inv_dict["account"],
"cost_center": item.cost_center,
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
"debit_in_transaction_currency": item.net_amount,
},
warehouse_account[item.from_warehouse]["account_currency"],
_inv_dict_from_warehouse["account_currency"],
item=item,
)
)
@@ -1097,15 +1103,19 @@ class PurchaseInvoice(BuyingController):
# sub-contracting warehouse
if flt(item.rm_supp_cost):
supplier_warehouse_account = warehouse_account[self.supplier_warehouse]["account"]
if not supplier_warehouse_account:
supplier_wh_dict = self.get_inventory_account_dict(
item, inventory_account_map, "supplier_warehouse"
)
supplier_inventory_account = supplier_wh_dict["account"]
if not supplier_inventory_account:
frappe.throw(
_("Please set account in Warehouse {0}").format(self.supplier_warehouse)
)
gl_entries.append(
self.get_gl_dict(
{
"account": supplier_warehouse_account,
"account": supplier_inventory_account,
"against": item.expense_account,
"cost_center": item.cost_center,
"project": item.project or self.project,
@@ -1113,7 +1123,7 @@ class PurchaseInvoice(BuyingController):
"credit": flt(item.rm_supp_cost),
"credit_in_transaction_currency": item.net_amount,
},
warehouse_account[self.supplier_warehouse]["account_currency"],
supplier_wh_dict["account_currency"],
item=item,
)
)

View File

@@ -1374,7 +1374,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
total_debit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": creditors_account, "docstatus": 1, "reference_name": pi.name},
"sum(debit) as amount",
[{"SUM": "debit", "as": "amount"}],
group_by="reference_name",
)[0].amount
self.assertEqual(flt(total_debit_amount, 2), 2500)
@@ -1456,7 +1456,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
total_debit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": creditors_account, "docstatus": 1, "reference_name": pi_2.name},
"sum(debit) as amount",
[{"SUM": "debit", "as": "amount"}],
group_by="reference_name",
)[0].amount
self.assertEqual(flt(total_debit_amount, 2), 1500)

View File

@@ -34,8 +34,7 @@
"base_net_amount",
"base_tax_amount",
"base_total",
"base_tax_amount_after_discount_amount",
"item_wise_tax_detail"
"base_tax_amount_after_discount_amount"
],
"fields": [
{
@@ -196,16 +195,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "item_wise_tax_detail",
"fieldtype": "Code",
"hidden": 1,
"label": "Item Wise Tax Detail",
"oldfieldname": "item_wise_tax_detail",
"oldfieldtype": "Small Text",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
@@ -279,7 +268,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-04-15 13:14:48.936047",
"modified": "2025-07-24 15:08:44.433022",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",

View File

@@ -35,7 +35,6 @@ class PurchaseTaxesandCharges(Document):
included_in_paid_amount: DF.Check
included_in_print_rate: DF.Check
is_tax_withholding_account: DF.Check
item_wise_tax_detail: DF.Code | None
net_amount: DF.Currency
parent: DF.Data
parentfield: DF.Data

View File

@@ -101,8 +101,8 @@ class RepostAccountingLedger(Document):
if doc.doctype in ["Payment Entry", "Journal Entry"]:
gle_map = doc.build_gl_map()
elif doc.doctype == "Purchase Receipt":
warehouse_account_map = get_warehouse_account_map(doc.company)
gle_map = doc.get_gl_entries(warehouse_account_map)
inventory_account_map = doc.get_inventory_account_map()
gle_map = doc.get_gl_entries(inventory_account_map)
else:
gle_map = doc.get_gl_entries()
@@ -213,7 +213,10 @@ def get_allowed_types_from_settings(child_doc: bool = False):
repost_docs = [
x.document_type
for x in frappe.db.get_all(
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
"Repost Allowed Types",
filters={"allowed": True},
fields=["document_type"],
distinct=True,
)
]
result = repost_docs
@@ -287,7 +290,11 @@ def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters
filters.update({"document_type": ("like", f"%{txt}%")})
if allowed_types := frappe.db.get_all(
"Repost Allowed Types", filters=filters, fields=["distinct(document_type)"], as_list=1
"Repost Allowed Types",
filters=filters,
fields=["document_type"],
as_list=1,
distinct=True,
):
return allowed_types
return []

View File

@@ -101,6 +101,7 @@
"discount_amount",
"sec_tax_breakup",
"other_charges_calculation",
"item_wise_tax_details",
"pricing_rule_details",
"pricing_rules",
"packing_list",
@@ -2238,6 +2239,14 @@
"hidden": 1,
"label": "Has Subcontracted",
"read_only": 1
},
{
"fieldname": "item_wise_tax_details",
"fieldtype": "Table",
"hidden": 1,
"label": "Item Wise Tax Details",
"no_copy": 1,
"options": "Item Wise Tax Detail"
}
],
"grid_page_length": 50,

View File

@@ -65,6 +65,7 @@ class SalesInvoice(SellingController):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.item_wise_tax_detail.item_wise_tax_detail import ItemWiseTaxDetail
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
@@ -146,6 +147,7 @@ class SalesInvoice(SellingController):
is_opening: DF.Literal["No", "Yes"]
is_pos: DF.Check
is_return: DF.Check
item_wise_tax_details: DF.Table[ItemWiseTaxDetail]
items: DF.Table[SalesInvoiceItem]
language: DF.Link | None
letter_head: DF.Link | None
@@ -847,9 +849,10 @@ class SalesInvoice(SellingController):
timesheet.db_update_all()
def update_billed_qty_in_scio(self):
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
fieldname = table.returned_qty if self.is_return else table.billed_qty
if self.is_return:
return
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
data = frappe._dict(
{
item.scio_detail: item.stock_qty if self._action == "submit" else -item.stock_qty
@@ -861,8 +864,8 @@ class SalesInvoice(SellingController):
if data:
case_expr = Case()
for name, qty in data.items():
case_expr = case_expr.when(table.name == name, fieldname + qty)
frappe.qb.update(table).set(fieldname, case_expr).where(
case_expr = case_expr.when(table.name == name, table.billed_qty + qty)
frappe.qb.update(table).set(table.billed_qty, case_expr).where(
(table.name.isin(list(data.keys()))) & (table.docstatus == 1)
).run()
@@ -1281,23 +1284,21 @@ class SalesInvoice(SellingController):
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
query = (
frappe.qb.from_(table)
.select(
table.required_qty, table.consumed_qty, table.billed_qty, table.returned_qty, table.name
)
.select(table.required_qty, table.consumed_qty, table.billed_qty, table.name)
.where((table.docstatus == 1) & (table.name.isin([item.scio_detail for item in self_rms])))
)
result = query.run(as_dict=True)
data = {item.name: item for item in result}
for item in self_rms:
row = data.get(item.scio_detail)
max_qty = max(row.required_qty, row.consumed_qty) - row.billed_qty - row.returned_qty
max_qty = max(row.required_qty, row.consumed_qty) - row.billed_qty
if item.stock_qty > max_qty:
frappe.throw(
_("Row #{0}: Stock quantity {1} ({2}) for item {3} cannot exceed {4}").format(
item.idx,
item.stock_qty,
item.stock_uom,
frappe.bold(item.item_code),
get_link_to_form("Item", item.item_code),
frappe.bold(max_qty),
)
)
@@ -1551,7 +1552,7 @@ class SalesInvoice(SellingController):
elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock):
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def get_gl_entries(self, warehouse_account=None):
def get_gl_entries(self, inventory_account_map=None):
from erpnext.accounts.general_ledger import merge_similar_entries
gl_entries = []
@@ -2701,6 +2702,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
target.purchase_order = source.purchase_order
target.po_detail = source.purchase_order_item
if (source.get("serial_no") or source.get("batch_no")) and not source.get("serial_and_batch_bundle"):
target.use_serial_batch_fields = 1
item_field_map = {
"doctype": target_doctype + " Item",
"field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"],

View File

@@ -2077,12 +2077,12 @@ class TestSalesInvoice(ERPNextTestSuite):
{
"item": "_Test Item",
"taxable_amount": 10000.0,
"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0, "net_amount": 10000.0},
"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0, "taxable_amount": 10000.0},
},
{
"item": "_Test Item 2",
"taxable_amount": 5000.0,
"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0, "net_amount": 5000.0},
"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0, "taxable_amount": 5000.0},
},
]
@@ -3612,7 +3612,7 @@ class TestSalesInvoice(ERPNextTestSuite):
frappe.db.get_all(
"Payment Ledger Entry",
filters={"against_voucher_no": si.name, "delinked": 0},
fields=["sum(amount), sum(amount_in_account_currency)"],
fields=[{"SUM": "amount"}, {"SUM": "amount_in_account_currency"}],
as_list=1,
)
@@ -3980,29 +3980,29 @@ class TestSalesInvoice(ERPNextTestSuite):
target_doc=si,
args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
)
si.save().submit()
si.save()
expected = [
{
"charge_type": "Actual",
"account_head": "Freight and Forwarding Charges - _TC",
"tax_amount": 120.0,
"total": 1520.0,
"base_total": 1520.0,
"total": 1620.0,
"base_total": 1620.0,
},
{
"charge_type": "Actual",
"account_head": "Marketing Expenses - _TC",
"tax_amount": 150.0,
"total": 1670.0,
"base_total": 1670.0,
"total": 1770.0,
"base_total": 1770.0,
},
{
"charge_type": "Actual",
"account_head": "Miscellaneous Expenses - _TC",
"tax_amount": 60.0,
"total": 1610.0,
"base_total": 1610.0,
"total": 1830.0,
"base_total": 1830.0,
},
]
actual = [

View File

@@ -31,7 +31,6 @@
"base_tax_amount",
"base_total",
"base_tax_amount_after_discount_amount",
"item_wise_tax_detail",
"dont_recompute_tax"
],
"fields": [
@@ -174,15 +173,6 @@
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "item_wise_tax_detail",
"fieldtype": "Code",
"hidden": 1,
"label": "Item Wise Tax Detail",
"oldfieldname": "item_wise_tax_detail",
"oldfieldtype": "Small Text",
"read_only": 1
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
@@ -257,13 +247,14 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-11-22 19:17:31.898467",
"modified": "2025-07-24 15:08:34.381704",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": []
}
}

View File

@@ -33,7 +33,6 @@ class SalesTaxesandCharges(Document):
dont_recompute_tax: DF.Check
included_in_paid_amount: DF.Check
included_in_print_rate: DF.Check
item_wise_tax_detail: DF.Code | None
net_amount: DF.Currency
parent: DF.Data
parentfield: DF.Data

View File

@@ -121,7 +121,7 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
gl_entries = frappe.db.get_all(
"GL Entry",
filters={"voucher_no": pi.name},
fields=["account", "sum(debit) as debit", "sum(credit) as credit"],
fields=["account", {"SUM": "debit", "as": "debit"}, {"SUM": "credit", "as": "credit"}],
group_by="account",
)
self.assertEqual(len(gl_entries), 3)

View File

@@ -0,0 +1,118 @@
[
{
"account_category_name": "Cash and Cash Equivalents",
"description": "Cash on hand, demand deposits, and short-term highly liquid investments readily convertible to cash with original maturities of three months or less. Examples: Cash in hand, bank current accounts, money market funds, treasury bills \u22643 months."
},
{
"account_category_name": "Cost of Goods Sold",
"description": "Direct costs attributable to cost of goods sold. Examples: Raw materials, stock in trade."
},
{
"account_category_name": "Current Tax Liabilities",
"description": "Income tax obligations for current and prior periods. Examples: Provision for income tax, advance tax paid, tax deducted at source."
},
{
"account_category_name": "Finance Costs",
"description": "Interest and financing-related expenses. Examples: Interest on borrowings, bank charges, lease interest, foreign exchange losses."
},
{
"account_category_name": "Intangible Assets",
"description": "Identifiable non-monetary assets without physical substance. Examples: Software, patents, trademarks, licenses, development costs."
},
{
"account_category_name": "Investment Income",
"description": "Returns generated from financial investments and cash management. Examples: Interest income, dividend income, rental income, fair value gains."
},
{
"account_category_name": "Long-term Borrowings",
"description": "Interest-bearing debt obligations with maturity beyond one year. Examples: Term loans, bonds, debentures, mortgages."
},
{
"account_category_name": "Long-term Investments",
"description": "Investments held for strategic purposes or extended periods. Examples: Equity investments, bonds, associates, joint ventures, deposits."
},
{
"account_category_name": "Long-term Provisions",
"description": "Present obligations beyond one year with uncertain timing/amount. Examples: Asset retirement obligations, environmental remediation, legal settlements."
},
{
"account_category_name": "Operating Expenses",
"description": "Costs incurred in ordinary business operations excluding direct costs. Examples: Selling expenses, administrative costs, marketing, utilities, rent."
},
{
"account_category_name": "Other Current Assets",
"description": "Current assets not classified elsewhere including prepaid expenses and advances. Examples: Prepaid insurance, prepaid rent, advance to suppliers, security deposits recoverable within one year."
},
{
"account_category_name": "Other Current Liabilities",
"description": "Short-term obligations not classified elsewhere. Examples: Accrued expenses, statutory liabilities, employee payables."
},
{
"account_category_name": "Other Direct Costs",
"description": "Direct costs excluding cost of goods sold. Examples: Direct labor, manufacturing overhead, freight inward."
},
{
"account_category_name": "Other Non-current Assets",
"description": "Long-term assets not classified elsewhere. Examples: Security deposits, long-term prepayments, advances for capital goods."
},
{
"account_category_name": "Other Non-current Liabilities",
"description": "Long-term obligations not classified elsewhere. Examples: Long-term deposits, deferred income, government grants."
},
{
"account_category_name": "Other Operating Income",
"description": "Incidental income related to business operations but not core revenue. Examples: Scrap sales, government grants, insurance claims, foreign exchange gains."
},
{
"account_category_name": "Other Payables",
"description": "Non-trade payables and obligations to parties other than suppliers. Examples: Employee payables, accrued expenses, customer advances, security deposits received."
},
{
"account_category_name": "Other Receivables",
"description": "Non-trade amounts due to the entity excluding financing arrangements. Examples: Employee advances, insurance claims, tax refunds, deposits recoverable."
},
{
"account_category_name": "Reserves and Surplus",
"description": "Accumulated profits and other reserves created from profits or share premium. Examples: General reserves, retained earnings, statutory reserves, share premium."
},
{
"account_category_name": "Revenue from Operations",
"description": "Income from primary business activities in ordinary course. Examples: Sales of goods, service revenue, commission income, royalty income."
},
{
"account_category_name": "Share Capital",
"description": "Nominal value of issued and paid-up equity shares. Examples: Common stock, ordinary shares, preference shares."
},
{
"account_category_name": "Short-term Borrowings",
"description": "Interest-bearing debt obligations due within one year. Examples: Bank overdrafts, short-term loans, current portion of long-term debt."
},
{
"account_category_name": "Short-term Investments",
"description": "Financial instruments held for short-term investment purposes, readily convertible to cash. Examples: Marketable securities, fixed deposits >3 months, mutual funds."
},
{
"account_category_name": "Short-term Provisions",
"description": "Present obligations due within one year with uncertain timing or amount. Examples: Warranty provisions, legal claims, restructuring costs."
},
{
"account_category_name": "Stock Assets",
"description": "Inventory and stock-related assets including raw materials, work in progress, finished goods, and stock in trade. Examples: Raw materials, finished goods, trading merchandise, consumables."
},
{
"account_category_name": "Tangible Assets",
"description": "Physical assets used in business operations including property, plant, and equipment. Examples: Land, buildings, machinery, equipment, vehicles, furniture, capital work in progress."
},
{
"account_category_name": "Tax Expense",
"description": "Current and deferred income tax obligations. Examples: Current tax provision, deferred tax expense, withholding taxes."
},
{
"account_category_name": "Trade Payables",
"description": "Amounts owed to suppliers. Examples: Supplier invoices, accrued purchases, bills payable."
},
{
"account_category_name": "Trade Receivables",
"description": "Amounts due from customers for goods sold or services provided in ordinary course of business. Examples: Accounts receivable, notes receivable from customers, unbilled revenue."
}
]

View File

@@ -0,0 +1,993 @@
{
"creation": "2025-09-07 07:24:40.762641",
"disabled": 0,
"docstatus": 0,
"doctype": "Financial Report Template",
"idx": 0,
"modified": "2025-10-15 03:12:19.165699",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Horizontal Balance Sheet (Columnar)",
"owner": "Administrator",
"report_type": "Balance Sheet",
"rows": [
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Section Break",
"display_name": "",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Column Break",
"display_name": "Equity & Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "Capital & Reserves",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "CAPITAL_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Share Capital\"]",
"data_source": "Account Data",
"display_name": "Share Capital",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_SHARE_CAPITAL",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "{\"and\":[[\"account_category\",\"=\",\"Reserves and Surplus\"],[\"account_name\",\"not like\",\"%Retained Earnings%\"]]}",
"data_source": "Account Data",
"display_name": "Reserves & Surplus",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_RESERVES",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "{\"and\":[[\"account_name\",\"like\",\"%Retained Earnings%\"],[\"account_category\",\"=\",\"Reserves and Surplus\"]]}",
"data_source": "Account Data",
"display_name": "Retained Earnings",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_RETAINED_EARNINGS",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "L_SHARE_CAPITAL + L_RESERVES + L_RETAINED_EARNINGS",
"data_source": "Calculated Amount",
"display_name": "Total Capital & Reserves",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 1,
"reference_code": "L_TOTAL_EQUITY",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "Non-Current Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "NCL_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Long-term Borrowings\"], [\"account_name\", \"not like\", \"Unsecured\"]]}",
"data_source": "Account Data",
"display_name": "Secured Loans",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_SECURED_LOANS",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Long-term Borrowings\"], [\"account_name\", \"like\", \"Unsecured\"]]}",
"data_source": "Account Data",
"display_name": "Unsecured Loans",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_UNSECURED_LOANS",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Long-term Provisions\"]",
"data_source": "Account Data",
"display_name": "Long-term Provisions",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_LT_PROVISIONS",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Non-current Liabilities\"]",
"data_source": "Account Data",
"display_name": "Other Non-current Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_OTHER_NCL",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "L_SECURED_LOANS + L_UNSECURED_LOANS + L_LT_PROVISIONS + L_OTHER_NCL",
"data_source": "Calculated Amount",
"display_name": "Total Non-current Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 1,
"reference_code": "L_TOTAL_NCL",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "Current Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "CL_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Short-term Borrowings\"]",
"data_source": "Account Data",
"display_name": "Short-term Borrowings",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_ST_BORROWINGS",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Trade Payables\"]",
"data_source": "Account Data",
"display_name": "Trade Payables",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_TRADE_PAYABLES",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Payables\"]",
"data_source": "Account Data",
"display_name": "Other Payables",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_OTHER_PAYABLES",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Current Tax Liabilities\"]",
"data_source": "Account Data",
"display_name": "Current Tax Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_TAX_LIABILITIES",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Short-term Provisions\"]",
"data_source": "Account Data",
"display_name": "Short-term Provisions",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_PROVISIONS",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Current Liabilities\"]",
"data_source": "Account Data",
"display_name": "Other Current Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_OTHER_CL",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "L_ST_BORROWINGS + L_TRADE_PAYABLES + L_OTHER_PAYABLES + L_TAX_LIABILITIES + L_PROVISIONS + L_OTHER_CL",
"data_source": "Calculated Amount",
"display_name": "Total Current Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 1,
"reference_code": "L_TOTAL_CL",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "Current Year Earnings",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "PL_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Period Movement (Debits - Credits)",
"bold_text": 1,
"calculation_formula": "[\"root_type\", \"in\", [\"Income\", \"Expense\"]]",
"color": "#28a745",
"data_source": "Account Data",
"display_name": "Net Profit/(Loss) for the Year",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "L_CURRENT_PL",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Column Break",
"display_name": "Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "Non-Current Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "NCA_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Tangible Assets\"], [\"account_type\", \"!=\", \"Accumulated Depreciation\"]]}",
"data_source": "Account Data",
"display_name": "Property, Plant & Equipment",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_TANGIBLE",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_type\", \"like\", \"Accumulated Depreciation\"]",
"data_source": "Account Data",
"display_name": "Less: Accumulated Depreciation",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 2,
"italic_text": 1,
"reference_code": "A_ACC_DEPRECIATION",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "A_TANGIBLE - A_ACC_DEPRECIATION",
"data_source": "Calculated Amount",
"display_name": "Net Tangible Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_NET_TANGIBLE",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Intangible Assets\"]",
"data_source": "Account Data",
"display_name": "Intangible Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_INTANGIBLE",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Long-term Investments\"]",
"data_source": "Account Data",
"display_name": "Long-term Investments",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_LT_INVESTMENTS",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Non-current Assets\"]",
"data_source": "Account Data",
"display_name": "Other Non-current Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_OTHER_NCA",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "(A_NET_TANGIBLE if A_ACC_DEPRECIATION else A_TANGIBLE) + A_INTANGIBLE + A_LT_INVESTMENTS + A_OTHER_NCA",
"data_source": "Calculated Amount",
"display_name": "Total Non-current Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 1,
"reference_code": "A_TOTAL_NCA",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "Current Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "CA_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Stock Assets\"]",
"data_source": "Account Data",
"display_name": "Inventories",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_STOCK",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Trade Receivables\"]",
"data_source": "Account Data",
"display_name": "Trade Receivables",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_TRADE_RECEIVABLES",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Receivables\"]",
"data_source": "Account Data",
"display_name": "Other Receivables",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_OTHER_RECEIVABLES",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Short-term Investments\"]",
"data_source": "Account Data",
"display_name": "Short-term Investments",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_ST_INVESTMENTS",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Cash and Cash Equivalents\"]",
"data_source": "Account Data",
"display_name": "Cash & Bank Balances",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_CASH_BANK",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Current Assets\"]",
"data_source": "Account Data",
"display_name": "Other Current Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "A_OTHER_CA",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "A_STOCK + A_TRADE_RECEIVABLES + A_OTHER_RECEIVABLES + A_ST_INVESTMENTS + A_CASH_BANK + A_OTHER_CA",
"data_source": "Calculated Amount",
"display_name": "Total Current Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 1,
"reference_code": "A_TOTAL_CA",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Section Break",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "L_TOTAL_EQUITY + L_TOTAL_NCL + L_TOTAL_CL + L_CURRENT_PL",
"data_source": "Calculated Amount",
"display_name": "TOTAL",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "L_GRAND_TOTAL",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Column Break",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "A_TOTAL_NCA + A_TOTAL_CA",
"data_source": "Calculated Amount",
"display_name": "TOTAL",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "A_GRAND_TOTAL",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Section Break",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "L_GRAND_TOTAL - A_GRAND_TOTAL",
"color": "#EC864B",
"data_source": "Calculated Amount",
"display_name": "Balance Check (should be zero)",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 1,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 1,
"reference_code": "BALANCE_CHECK",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "A_TOTAL_NCA + A_TOTAL_CA",
"data_source": "Calculated Amount",
"display_name": "Total Assets",
"fieldtype": "",
"hidden_calculation": 1,
"hide_when_empty": 0,
"include_in_charts": 1,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "TOTAL_ASSETS",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "L_TOTAL_NCL + L_TOTAL_CL",
"data_source": "Calculated Amount",
"display_name": "Total Liabilities",
"fieldtype": "",
"hidden_calculation": 1,
"hide_when_empty": 0,
"include_in_charts": 1,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "TOTAL_LIABILITIES",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "L_TOTAL_EQUITY + L_CURRENT_PL",
"data_source": "Calculated Amount",
"display_name": "Total Equity",
"fieldtype": "",
"hidden_calculation": 1,
"hide_when_empty": 0,
"include_in_charts": 1,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "TOTAL_EQUITY",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "KEY FINANCIAL RATIOS",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "METRICS_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "(A_TOTAL_CA / L_TOTAL_CL) if L_TOTAL_CL != 0 else 0",
"data_source": "Calculated Amount",
"display_name": "Current Ratio",
"fieldtype": "Float",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 1,
"reference_code": "CURRENT_RATIO",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "((A_TRADE_RECEIVABLES + A_ST_INVESTMENTS + A_CASH_BANK) / L_TOTAL_CL) if L_TOTAL_CL != 0 else 0",
"data_source": "Calculated Amount",
"display_name": "Quick Ratio (Acid Test)",
"fieldtype": "Float",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 1,
"reference_code": "QUICK_RATIO",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "((L_SECURED_LOANS + L_UNSECURED_LOANS + L_ST_BORROWINGS) / TOTAL_EQUITY) if TOTAL_EQUITY != 0 else 0",
"data_source": "Calculated Amount",
"display_name": "Debt to Equity Ratio",
"fieldtype": "Float",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 1,
"reference_code": "DEBT_EQUITY_RATIO",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "A_TOTAL_CA - L_TOTAL_CL",
"data_source": "Calculated Amount",
"display_name": "Net Working Capital",
"fieldtype": "Currency",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 1,
"reference_code": "WORKING_CAPITAL",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "(TOTAL_EQUITY / TOTAL_ASSETS * 100) if TOTAL_ASSETS != 0 else 0",
"data_source": "Calculated Amount",
"display_name": "Equity Ratio %",
"fieldtype": "Percent",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 1,
"reference_code": "EQUITY_RATIO",
"reverse_sign": 0
}
],
"template_name": "Horizontal Balance Sheet (Columnar)"
}

View File

@@ -0,0 +1,824 @@
{
"creation": "2025-09-06 21:47:56.970556",
"disabled": 0,
"docstatus": 0,
"doctype": "Financial Report Template",
"idx": 0,
"modified": "2025-10-15 03:13:38.485684",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Standard Balance Sheet (IFRS)",
"owner": "Administrator",
"report_type": "Balance Sheet",
"rows": [
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "ASSETS",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "ASSETS_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "CURRENT ASSETS",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "CA_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Cash and Cash Equivalents\"]",
"data_source": "Account Data",
"display_name": "Cash and Cash Equivalents",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CA100",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Trade Receivables\"]",
"data_source": "Account Data",
"display_name": "Trade Receivables",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CA200",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Receivables\"]",
"data_source": "Account Data",
"display_name": "Other Receivables",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CA300",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Stock Assets\"]",
"data_source": "Account Data",
"display_name": "Inventories",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CA400",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Short-term Investments\"]",
"data_source": "Account Data",
"display_name": "Short-term Investments",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CA500",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Current Assets\"]",
"data_source": "Account Data",
"display_name": "Other Current Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CA600",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "CA100 + CA200 + CA300 + CA400 + CA500 + CA600",
"data_source": "Calculated Amount",
"display_name": "Total Current Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 1,
"reference_code": "CA_TOTAL",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "NON-CURRENT ASSETS",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "NCA_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Tangible Assets\"]",
"data_source": "Account Data",
"display_name": "Property, Plant and Equipment",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "NCA100",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Intangible Assets\"]",
"data_source": "Account Data",
"display_name": "Intangible Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "NCA200",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Long-term Investments\"]",
"data_source": "Account Data",
"display_name": "Long-term Investments",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "NCA300",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Non-current Assets\"]",
"data_source": "Account Data",
"display_name": "Other Non-current Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "NCA400",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "NCA100 + NCA200 + NCA300 + NCA400",
"data_source": "Calculated Amount",
"display_name": "Total Non-current Assets",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 1,
"reference_code": "NCA_TOTAL",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "CA_TOTAL + NCA_TOTAL",
"data_source": "Calculated Amount",
"display_name": "TOTAL ASSETS",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 1,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "TOTAL_ASSETS",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "LIABILITIES AND EQUITY",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "LIAB_EQUITY_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "CURRENT LIABILITIES",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "CL_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Trade Payables\"]",
"data_source": "Account Data",
"display_name": "Trade Payables",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CL100",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Payables\"]",
"data_source": "Account Data",
"display_name": "Other Payables",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CL200",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Short-term Borrowings\"]",
"data_source": "Account Data",
"display_name": "Short-term Borrowings",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CL300",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Current Tax Liabilities\"]",
"data_source": "Account Data",
"display_name": "Current Tax Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CL400",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Short-term Provisions\"]",
"data_source": "Account Data",
"display_name": "Short-term Provisions",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CL500",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Current Liabilities\"]",
"data_source": "Account Data",
"display_name": "Other Current Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "CL600",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "CL100 + CL200 + CL300 + CL400 + CL500 + CL600",
"data_source": "Calculated Amount",
"display_name": "Total Current Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 1,
"reference_code": "CL_TOTAL",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "NON-CURRENT LIABILITIES",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "NCL_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Long-term Borrowings\"]",
"data_source": "Account Data",
"display_name": "Long-term Borrowings",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "NCL100",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Long-term Provisions\"]",
"data_source": "Account Data",
"display_name": "Long-term Provisions",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "NCL200",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Other Non-current Liabilities\"]",
"data_source": "Account Data",
"display_name": "Other Non-current Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "NCL300",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "NCL100 + NCL200 + NCL300",
"data_source": "Calculated Amount",
"display_name": "Total Non-current Liabilities",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 1,
"reference_code": "NCL_TOTAL",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "CL_TOTAL + NCL_TOTAL",
"data_source": "Calculated Amount",
"display_name": "TOTAL LIABILITIES",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 1,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "TOTAL_LIABILITIES",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"data_source": "Blank Line",
"display_name": "EQUITY",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "EQ_HEADER",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Share Capital\"]",
"data_source": "Account Data",
"display_name": "Share Capital",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "EQ100",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"account_category\", \"=\", \"Reserves and Surplus\"]",
"data_source": "Account Data",
"display_name": "Reserves and Surplus",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "EQ200",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "Closing Balance",
"bold_text": 0,
"calculation_formula": "[\"root_type\", \"in\", [\"Income\", \"Expense\"]]",
"data_source": "Account Data",
"display_name": "Provisional Profit and Loss",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 1,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 0,
"reference_code": "EQ300",
"reverse_sign": 1
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "EQ100 + EQ200 + EQ300",
"data_source": "Calculated Amount",
"display_name": "Total Equity",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 1,
"italic_text": 1,
"reference_code": "TOTAL_EQUITY",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "TOTAL_EQUITY",
"data_source": "Calculated Amount",
"display_name": "TOTAL EQUITY",
"fieldtype": "",
"hidden_calculation": 1,
"hide_when_empty": 0,
"include_in_charts": 1,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "TOTAL_LIABILITIES + TOTAL_EQUITY",
"data_source": "Calculated Amount",
"display_name": "TOTAL LIABILITIES AND EQUITY",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "TOTAL_LIAB_EQUITY",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 1,
"calculation_formula": "TOTAL_ASSETS - TOTAL_LIAB_EQUITY",
"color": "#CB2929",
"data_source": "Calculated Amount",
"display_name": "Balance Check (should be zero)",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 1,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reference_code": "BALANCE_CHECK",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"data_source": "Blank Line",
"fieldtype": "",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 0,
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "CA_TOTAL / CL_TOTAL if CL_TOTAL != 0 else 0",
"data_source": "Calculated Amount",
"display_name": "Current Ratio",
"fieldtype": "Float",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 1,
"reference_code": "CURRENT_RATIO",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "(CL300 + NCL100) / TOTAL_EQUITY if TOTAL_EQUITY != 0 else 0",
"data_source": "Calculated Amount",
"display_name": "Debt to Equity Ratio",
"fieldtype": "Float",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 1,
"reference_code": "DEBT_EQUITY_RATIO",
"reverse_sign": 0
},
{
"advanced_filtering": 0,
"balance_type": "",
"bold_text": 0,
"calculation_formula": "CA_TOTAL - CL_TOTAL",
"data_source": "Calculated Amount",
"display_name": "Working Capital",
"fieldtype": "Currency",
"hidden_calculation": 0,
"hide_when_empty": 0,
"include_in_charts": 0,
"indentation_level": 0,
"italic_text": 1,
"reference_code": "WORKING_CAPITAL",
"reverse_sign": 0
}
],
"template_name": "Standard Balance Sheet (IFRS)"
}

Some files were not shown because too many files have changed in this diff Show More