From a98a2d8f2acbb6ad556b164a80908d9c4e28d5dc Mon Sep 17 00:00:00 2001 From: Jagadeesh Date: Wed, 28 May 2025 11:09:32 +0000 Subject: [PATCH 1/4] [feature] : (#433) report view for account group mapping --- .../report/account_group_mapping/__init__.py | 0 .../account_group_mapping.js | 48 +++++ .../account_group_mapping.json | 33 ++++ .../account_group_mapping.py | 164 ++++++++++++++++++ .../workspace/accounting/accounting.json | 14 +- 5 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 erpnext/accounts/report/account_group_mapping/__init__.py create mode 100644 erpnext/accounts/report/account_group_mapping/account_group_mapping.js create mode 100644 erpnext/accounts/report/account_group_mapping/account_group_mapping.json create mode 100644 erpnext/accounts/report/account_group_mapping/account_group_mapping.py diff --git a/erpnext/accounts/report/account_group_mapping/__init__.py b/erpnext/accounts/report/account_group_mapping/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/report/account_group_mapping/account_group_mapping.js b/erpnext/accounts/report/account_group_mapping/account_group_mapping.js new file mode 100644 index 000000000000..e1c3e1e9cdff --- /dev/null +++ b/erpnext/accounts/report/account_group_mapping/account_group_mapping.js @@ -0,0 +1,48 @@ +// 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"].join("\n"), + 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"].join("\n"), + 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 (r) { + if (r.message) frappe.query_report.refresh(); + } + }); + }, +}; diff --git a/erpnext/accounts/report/account_group_mapping/account_group_mapping.json b/erpnext/accounts/report/account_group_mapping/account_group_mapping.json new file mode 100644 index 000000000000..8941f74f5189 --- /dev/null +++ b/erpnext/accounts/report/account_group_mapping/account_group_mapping.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/accounts/report/account_group_mapping/account_group_mapping.py b/erpnext/accounts/report/account_group_mapping/account_group_mapping.py new file mode 100644 index 000000000000..b81166f111ae --- /dev/null +++ b/erpnext/accounts/report/account_group_mapping/account_group_mapping.py @@ -0,0 +1,164 @@ +# 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): + conditions = ["company=%s", "rgt - lft = 1", "report_type=%s"] + args = [self.company, self.report_type] + + if self.root_type: + conditions.append("root_type=%s") + args.append(self.root_type) + where = " AND ".join(conditions) + 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 + }) + + 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 {"message": "no change"} + + 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)] + 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() + + return {"message": "success"} diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index 96b9ab024c51..824083e79f8a 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -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" @@ -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, @@ -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", From 68ca30205841feb48d4ecc4108f493918b402652 Mon Sep 17 00:00:00 2001 From: Jagadeesh Date: Mon, 2 Jun 2025 11:44:28 +0000 Subject: [PATCH 2/4] [feature] : (#433) PR suggested changes --- .../account_group_mapping.js | 8 ++-- .../account_group_mapping.py | 45 ++++++++++--------- .../summarized_profit_and_loss.py | 12 +++++ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/report/account_group_mapping/account_group_mapping.js b/erpnext/accounts/report/account_group_mapping/account_group_mapping.js index e1c3e1e9cdff..25b4f7cedebd 100644 --- a/erpnext/accounts/report/account_group_mapping/account_group_mapping.js +++ b/erpnext/accounts/report/account_group_mapping/account_group_mapping.js @@ -15,7 +15,7 @@ frappe.query_reports["Account Group Mapping"] = { fieldname: "report_type", label: __("Report Type"), fieldtype: "Select", - options: ["Profit and Loss", "Balance Sheet"].join("\n"), + options: ["Profit and Loss", "Balance Sheet"], default: "Profit and Loss", description: __("Filter by report type for Account Groups and Accounts"), reqd: 1 @@ -24,7 +24,7 @@ frappe.query_reports["Account Group Mapping"] = { fieldname: "root_type", label: __("Root Type"), fieldtype: "Select", - options: ["", "Income", "Expense", "Asset", "Liability", "Equity"].join("\n"), + options: ["", "Income", "Expense", "Asset", "Liability", "Equity"], default: "", description: __("Filter by root type for Accounts only") } @@ -40,8 +40,8 @@ frappe.query_reports["Account Group Mapping"] = { old_group: old_value, new_group: new_value }, - callback: function (r) { - if (r.message) frappe.query_report.refresh(); + callback: function () { + frappe.query_report.refresh(); } }); }, diff --git a/erpnext/accounts/report/account_group_mapping/account_group_mapping.py b/erpnext/accounts/report/account_group_mapping/account_group_mapping.py index b81166f111ae..0d8d8bf9cbda 100644 --- a/erpnext/accounts/report/account_group_mapping/account_group_mapping.py +++ b/erpnext/accounts/report/account_group_mapping/account_group_mapping.py @@ -32,13 +32,15 @@ def get_account_groups(self): ) def get_leaf_accounts(self): - conditions = ["company=%s", "rgt - lft = 1", "report_type=%s"] - args = [self.company, self.report_type] + filters = {**self.filters, "is_group": 0} - if self.root_type: - conditions.append("root_type=%s") - args.append(self.root_type) - where = " AND ".join(conditions) + 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 @@ -64,22 +66,22 @@ def get_account_group_mappings(self, group_names): def get_columns(self): columns = [ { - "label": _("Account Number"), - "fieldname": "account_number", - "fieldtype": "Data", - "width": 100 + "label": _("Account Number"), + "fieldname": "account_number", + "fieldtype": "Data", + "width": 100 }, { - "label": _("Account Name"), - "fieldname": "account_name", - "fieldtype": "Data", - "width": 250, + "label": _("Account Name"), + "fieldname": "account_name", + "fieldtype": "Data", + "width": 250 }, { - "label": _("Unmapped & Recent"), - "fieldname": "recent_unmapped", - "fieldtype": "Data", - "width": 130, + "label": _("Unmapped & Recent"), + "fieldname": "recent_unmapped", + "fieldtype": "Data", + "width": 130 }, ] @@ -145,11 +147,14 @@ def update_account_group_mapping(account, old_group, new_group): frappe.throw(_("Account is required.")) if old_group == new_group: - return {"message": "no change"} + 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: @@ -160,5 +165,3 @@ def update_account_group_mapping(account, old_group, new_group): "account": account }) new_group_doc.save() - - return {"message": "success"} diff --git a/erpnext/accounts/report/summarized_profit_and_loss/summarized_profit_and_loss.py b/erpnext/accounts/report/summarized_profit_and_loss/summarized_profit_and_loss.py index 8dcc1a94e3ee..a54175fa22cf 100644 --- a/erpnext/accounts/report/summarized_profit_and_loss/summarized_profit_and_loss.py +++ b/erpnext/accounts/report/summarized_profit_and_loss/summarized_profit_and_loss.py @@ -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(b.get("mtd_budget", 0) for b in budget_totals.values()) + result["ytd_budget"] = sum(b.get("ytd_budget", 0) for b in budget_totals.values()) + return result def get_net_profit_loss_for_period(self, accounts, from_date, to_date): From 273681c679d3ce532a57a3ef7e7eea184b8b1f2c Mon Sep 17 00:00:00 2001 From: Jagadeesh Date: Tue, 3 Jun 2025 09:15:46 +0000 Subject: [PATCH 3/4] [feature] : (#433) Convert budget values to float before summing totals --- .../summarized_profit_and_loss/summarized_profit_and_loss.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/summarized_profit_and_loss/summarized_profit_and_loss.py b/erpnext/accounts/report/summarized_profit_and_loss/summarized_profit_and_loss.py index a54175fa22cf..0d5043ad11b3 100644 --- a/erpnext/accounts/report/summarized_profit_and_loss/summarized_profit_and_loss.py +++ b/erpnext/accounts/report/summarized_profit_and_loss/summarized_profit_and_loss.py @@ -101,8 +101,8 @@ def get_net_profit_loss(self): ) # Sum budget for all accounts - result["mtd_budget"] = sum(b.get("mtd_budget", 0) for b in budget_totals.values()) - result["ytd_budget"] = sum(b.get("ytd_budget", 0) for b in budget_totals.values()) + 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 From 1494a72828b45ec0c795e7e603e4003e4d9b0bb2 Mon Sep 17 00:00:00 2001 From: Jagadeesh Date: Tue, 3 Jun 2025 09:51:56 +0000 Subject: [PATCH 4/4] [feature] : (#433) Z index UI issue temporary workaround fix --- .../account_group_mapping.js | 27 +++++++++++++++++++ .../account_group_mapping.py | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/account_group_mapping/account_group_mapping.js b/erpnext/accounts/report/account_group_mapping/account_group_mapping.js index 25b4f7cedebd..1872323d503c 100644 --- a/erpnext/accounts/report/account_group_mapping/account_group_mapping.js +++ b/erpnext/accounts/report/account_group_mapping/account_group_mapping.js @@ -45,4 +45,31 @@ frappe.query_reports["Account Group Mapping"] = { } }); }, + 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); + } + }; diff --git a/erpnext/accounts/report/account_group_mapping/account_group_mapping.py b/erpnext/accounts/report/account_group_mapping/account_group_mapping.py index 0d8d8bf9cbda..186f0a5759a8 100644 --- a/erpnext/accounts/report/account_group_mapping/account_group_mapping.py +++ b/erpnext/accounts/report/account_group_mapping/account_group_mapping.py @@ -103,7 +103,8 @@ def get_columns(self): } }, "width": 160, - "editable": 1 + "editable": 1, + "align": "left" }) return columns