mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-05 06:28:29 +00:00
Merge branch 'develop' into tds-jv
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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"}),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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"):
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]],
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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")):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, "");
|
||||
},
|
||||
});
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
Reference in New Issue
Block a user