Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt

frappe.query_reports["Account Group Mapping"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
fieldname: "report_type",
label: __("Report Type"),
fieldtype: "Select",
options: ["Profit and Loss", "Balance Sheet"],
default: "Profit and Loss",
description: __("Filter by report type for Account Groups and Accounts"),
reqd: 1
},
{
fieldname: "root_type",
label: __("Root Type"),
fieldtype: "Select",
options: ["", "Income", "Expense", "Asset", "Liability", "Equity"],
default: "",
description: __("Filter by root type for Accounts only")
}
],

onChange: function (new_value, column, data) {
const old_value = data[column.fieldname];

frappe.call({
method: "erpnext.accounts.report.account_group_mapping.account_group_mapping.update_account_group_mapping",
args: {
account: data.account,
old_group: old_value,
new_group: new_value
},
callback: function () {
frappe.query_report.refresh();
}
});
},
onload: function () {
const style = document.createElement("style");
style.innerHTML = `
.awesomplete {
z-index: 10010 !important;
}

.awesomplete > ul {
z-index: 10010 !important;
position: absolute !important;
background: white;
border: 1px solid #ddd;
max-height: 200px;
overflow-y: auto;
}

.dt-scrollable {
overflow: visible !important;
}

.dataTable-cell > .link-field {
overflow: visible !important;
}
`;
document.head.appendChild(style);
}

};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2025-05-27 10:01:53.573544",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2025-05-27 10:01:53.573544",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Group Mapping",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"reference_report": "",
"report_name": "Account Group Mapping",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
],
"timeout": 0
}
168 changes: 168 additions & 0 deletions erpnext/accounts/report/account_group_mapping/account_group_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt

import frappe
from frappe import _

def execute(filters=None):
return AccountGroupMappingReport(filters).run()

class AccountGroupMappingReport:
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
self.company = self.filters.get("company")
self.report_type = self.filters.get("report_type")
self.root_type = self.filters.get("root_type")

def run(self):
self.validate_filters()
return self.get_columns(), self.get_data()

def validate_filters(self):
if not self.company:
frappe.throw(_("Company filter is required"))

def get_account_groups(self):
filters = {"company": self.company, "report_type": self.report_type}
return frappe.get_all(
"Account Group",
filters=filters,
fields=["name", "group_name", "report_type", "root_type"],
order_by="group_name"
)

def get_leaf_accounts(self):
filters = {**self.filters, "is_group": 0}

where_clauses = []
args = []
for key, value in filters.items():
where_clauses.append(f"{key}=%s")
args.append(value)

where = " AND ".join(where_clauses)
return frappe.db.sql(
f"""
SELECT name, account_number, account_name, root_type, lft, rgt
FROM `tabAccount`
WHERE {where}
ORDER BY lft
""", tuple(args), as_dict=True
)

def get_account_group_mappings(self, group_names):
mapping = {}

for group in group_names:
rows = frappe.get_all(
"Account Group Row",
filters={"parent": group, "row_type": "Account"},
fields=["account"]
)
for r in rows:
mapping.setdefault(r["account"], []).append(group)
return mapping

def get_columns(self):
columns = [
{
"label": _("Account Number"),
"fieldname": "account_number",
"fieldtype": "Data",
"width": 100
},
{
"label": _("Account Name"),
"fieldname": "account_name",
"fieldtype": "Data",
"width": 250
},
{
"label": _("Unmapped & Recent"),
"fieldname": "recent_unmapped",
"fieldtype": "Data",
"width": 130
},
]

self.account_groups = self.get_account_groups()
self.group_names = [g.name for g in self.account_groups]
self.account_group_mappings = self.get_account_group_mappings(self.group_names)
max_groups = max((len(groups) for groups in self.account_group_mappings.values()), default=1)

for i in range(1, max_groups + 2):
columns.append({
"label": _(f"Account Group {i}"),
"fieldname": f"account_group_{i}",
"fieldtype": "Link",
"options": "Account Group",
"get_query": {
"filters": {
"company": self.company,
"report_type": self.report_type,
}
},
"width": 160,
"editable": 1,
"align": "left"
})

return columns

def get_data(self):
accounts = self.get_leaf_accounts()
data = []
max_groups = len([c for c in self.get_columns() if c["fieldname"].startswith("account_group_")])

for acc in accounts:
row = {
"account_number": acc["account_number"],
"account_name": acc["account_name"],
"account": acc["name"]
}

groups = self.account_group_mappings.get(acc["name"], [])

for i in range(1, max_groups + 1):
row[f"account_group_{i}"] = groups[i-1] if i-1 < len(groups) else ""

# Check for recent GL Entry if unmapped
row["recent_unmapped"] = ""

if not groups:
recent_entry = frappe.db.exists(
"GL Entry",
{
"account": acc["name"],
"posting_date": [">=", frappe.utils.add_days(frappe.utils.nowdate(), -90)]
}
)
row["recent_unmapped"] = _("Needs Mapping") if recent_entry else ""
data.append(row)

return data

@frappe.whitelist()
def update_account_group_mapping(account, old_group, new_group):
if not account:
frappe.throw(_("Account is required."))

if old_group == new_group:
return

if old_group and old_group != new_group:
old_group_doc = frappe.get_doc("Account Group", old_group)
old_group_doc.rows = [r for r in old_group_doc.rows if not (r.row_type == "Account" and r.account == account)]
# Reset idx for all rows
for i, r in enumerate(old_group_doc.rows):
r.idx = i + 1
old_group_doc.save()

if new_group:
new_group_doc = frappe.get_doc("Account Group", new_group)
if not any(r.row_type == "Account" and r.account == account for r in new_group_doc.rows):
new_group_doc.append("rows", {
"row_type": "Account",
"account": account
})
new_group_doc.save()
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ def get_net_profit_loss(self):
for key, (from_date, to_date) in periods.items():
result[key] = self.get_net_profit_loss_for_period(accounts, from_date, to_date)

# --- Budget Calculation ---
budget_data = self.get_budget_data(accounts, self.filters.year_start_date, self.filters.report_date)
budget_totals = self.calculate_budget_totals(
budget_data,
self.filters.month_start_date, self.filters.report_date,
self.filters.year_start_date, self.filters.report_date
)

# Sum budget for all accounts
result["mtd_budget"] = sum(flt(b.get("mtd_budget")) for b in budget_totals.values())
result["ytd_budget"] = sum(flt(b.get("ytd_budget")) for b in budget_totals.values())

return result

def get_net_profit_loss_for_period(self, accounts, from_date, to_date):
Expand Down
14 changes: 12 additions & 2 deletions erpnext/accounts/workspace/accounting/accounting.json
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "More Reports",
"link_count": 6,
"link_count": 7,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
Expand All @@ -1111,6 +1111,16 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Account Group Mapping",
"link_count": 0,
"link_to": "Account Group Mapping",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
Expand Down Expand Up @@ -1155,7 +1165,7 @@
"type": "Link"
}
],
"modified": "2025-05-21 12:17:11.323572",
"modified": "2025-05-27 15:51:21.425993",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
Expand Down