From f954dba824beb21f2f995a882cc379065a0f41c9 Mon Sep 17 00:00:00 2001
From: luka-03256 <42lukastefanovic@gmail.com>
Date: Tue, 18 Nov 2025 15:59:53 +0000
Subject: [PATCH 1/2] feat: add 'Accumulated Values in Group Company' checkbox
to Budget Variance Report (#50608)
---
.../budget_variance_report.js | 34 +++++++++++++
.../budget_variance_report.py | 50 +++++++++++++++++++
2 files changed, 84 insertions(+)
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
index c74450191aaf..f80e396261fd 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
@@ -17,6 +17,33 @@ frappe.query_reports["Budget Variance Report"] = {
return value;
},
};
+
+frappe.query_reports["Budget Variance Report"].onload = function(report) {
+ let company = frappe.query_report.get_fileter_value("company");
+ if (!company) return;
+
+ frappe.call({
+ method: "frappe.client.get_value",
+ args: {
+ doctype: "Company",
+ filters: { name: company },
+ fieldname: "is_group",
+ },
+ callback: function(r) {
+ const filter = frappe.query_report.get_filter("accumulated_in_group_company");
+ if (!filter) return;
+
+ if (r.message && r.message.is_group) {
+ filter.df.hidden = 0;
+ } else {
+ filter.df.hidden = 1;
+ frappe.query_report.set_filter_value("accumulated_in_group_company", 0);
+ }
+ filter.refresh();
+ },
+ });
+};
+
function get_filters() {
function get_dimensions() {
let result = [];
@@ -107,6 +134,13 @@ function get_filters() {
fieldtype: "Check",
default: 0,
},
+ {
+ fieldname: "accumulated_in_group_company",
+ label: __("Accumulated Values in Group Company"),
+ fieldtype: "Check",
+ default: 0,
+ hidden: 1,
+ },
];
return filters;
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index db42d23a8392..cca7065940f4 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -10,6 +10,26 @@
from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges
+def get_companies_for_report(filters):
+ """Return list of companies for consolidated reporting."""
+ company = filters.get("company")
+
+ # If not consolidating -> return only the selected company
+ if not filters.get("accumulated_in_group_company"):
+ return [company]
+
+ # Consolidated -> get all child companies + parent
+ companies = frappe.get_all(
+ "Company",
+ filters={"parent_company": company},
+ pluck="name"
+ )
+ # Always include parent company
+ if company not in companies:
+ companies.append(company)
+
+ return companies
+
def execute(filters=None):
if not filters:
@@ -24,6 +44,36 @@ def execute(filters=None):
period_month_ranges = get_period_month_ranges(filters["period"], filters["from_fiscal_year"])
cam_map = get_dimension_account_month_map(filters)
+ # consolidate dimension_account_month map accross companies
+ companies = get_companies_for_report(filters)
+ if filters.get("accumulated_in_group_company"):
+ consolidated_cam_map = {}
+ for comp in companies:
+ filters["company"] = comp
+ company_map = get_dimension_account_month_map(filters)
+
+ for dim, accounts in company_map.items():
+ if dim not in consolidated_cam_map:
+ consolidated_cam_map[dim] = {}
+ for acc, months in accounts.items():
+ if acc not in consolidated_cam_map[dim]:
+ consolidated_cam_map[dim][acc] = months
+ else:
+ # accumulate monthwise data
+ for year, mdata in months.items():
+ consolidated_cam_map[dim][acc].setdefault(year, {})
+ for month, vals in mdata.items():
+ consolidated_cam_map[dim][acc][year].setdefault(month, {})
+ for key in ("target", "actual"):
+ consolidated_cam_map[dim][acc][year][month][key] = \
+ consolidated_cam_map[dim][acc][year][month].get(key, 0) + vals.get(key, 0)
+ consolidated_cam_map[dim][acc][year][month]["variance"] = \
+ consolidated_cam_map[dim][acc][year][month]["target"] - consolidated_cam_map[dim][acc][year][month]["actual"]
+
+ cam_map = consolidated_cam_map
+ else:
+ cam_map = get_dimension_account_month_map(filters)
+
data = []
for dimension in dimensions:
dimension_items = cam_map.get(dimension)
From ada7858c22a33632489930fb227a2071a120d6a9 Mon Sep 17 00:00:00 2001
From: luka-03256 <42lukastefanovic@gmail.com>
Date: Wed, 19 Nov 2025 11:26:20 +0000
Subject: [PATCH 2/2] fix: resolve CodeRabbit issues, identation corrections,
filter logic, consolidation fixes
---
.../budget_variance_report.js | 81 ++++++++++---------
.../budget_variance_report.py | 68 +++++++++-------
2 files changed, 85 insertions(+), 64 deletions(-)
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
index f80e396261fd..c3e2487dbdc3 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
@@ -1,15 +1,16 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd.
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Budget Variance Report"] = {
filters: get_filters(),
+
formatter: function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
- if (column.fieldname.includes(__("variance"))) {
- if (data[column.fieldname] < 0) {
+ if (column.fieldname && column.fieldname.includes("variance")) {
+ if (data && data[column.fieldname] < 0) {
value = "" + value + "";
- } else if (data[column.fieldname] > 0) {
+ } else if (data && data[column.fieldname] > 0) {
value = "" + value + "";
}
}
@@ -18,32 +19,36 @@ frappe.query_reports["Budget Variance Report"] = {
},
};
-frappe.query_reports["Budget Variance Report"].onload = function(report) {
- let company = frappe.query_report.get_fileter_value("company");
- if (!company) return;
-
- frappe.call({
- method: "frappe.client.get_value",
- args: {
- doctype: "Company",
- filters: { name: company },
- fieldname: "is_group",
- },
- callback: function(r) {
- const filter = frappe.query_report.get_filter("accumulated_in_group_company");
- if (!filter) return;
+frappe.query_reports["Budget Variance Report"].onload = function (report) {
+ let company = frappe.query_report.get_filter_value("company");
+ if (!company) return;
- if (r.message && r.message.is_group) {
- filter.df.hidden = 0;
- } else {
- filter.df.hidden = 1;
- frappe.query_report.set_filter_value("accumulated_in_group_company", 0);
- }
- filter.refresh();
- },
- });
+ update_group_company_checkbox(company);
};
+function update_group_company_checkbox(company) {
+ frappe.call({
+ method: "frappe.client.get_value",
+ args: {
+ doctype: "Company",
+ filters: { name: company },
+ fieldname: "is_group",
+ },
+ callback: function (r) {
+ const filter = frappe.query_report.get_filter("accumulated_in_group_company");
+ if (!filter) return;
+
+ if (r.message && r.message.is_group) {
+ filter.df.hidden = 0;
+ } else {
+ filter.df.hidden = 1;
+ frappe.query_report.set_filter_value("accumulated_in_group_company", 0);
+ }
+ filter.refresh();
+ },
+ });
+}
+
function get_filters() {
function get_dimensions() {
let result = [];
@@ -101,6 +106,10 @@ function get_filters() {
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
+ on_change: function () {
+ let company = frappe.query_report.get_filter_value("company");
+ if (company) update_group_company_checkbox(company);
+ },
},
{
fieldname: "budget_against",
@@ -120,8 +129,6 @@ function get_filters() {
fieldtype: "MultiSelectList",
options: "budget_against",
get_data: function (txt) {
- if (!frappe.query_report.filters) return;
-
let budget_against = frappe.query_report.get_filter_value("budget_against");
if (!budget_against) return;
@@ -134,14 +141,14 @@ function get_filters() {
fieldtype: "Check",
default: 0,
},
- {
- fieldname: "accumulated_in_group_company",
- label: __("Accumulated Values in Group Company"),
- fieldtype: "Check",
- default: 0,
- hidden: 1,
- },
+ {
+ fieldname: "accumulated_in_group_company",
+ label: __("Accumulated Values in Group Company"),
+ fieldtype: "Check",
+ default: 0,
+ hidden: 1,
+ },
];
return filters;
-}
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index cca7065940f4..3006b6030782 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-
import datetime
import frappe
@@ -10,44 +9,47 @@
from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges
+
def get_companies_for_report(filters):
- """Return list of companies for consolidated reporting."""
- company = filters.get("company")
+ """Return list of companies for consolidated reporting."""
+ company = filters.get("company")
- # If not consolidating -> return only the selected company
- if not filters.get("accumulated_in_group_company"):
- return [company]
+ if not filters.get("accumulated_in_group_company"):
+ return [company]
- # Consolidated -> get all child companies + parent
- companies = frappe.get_all(
- "Company",
- filters={"parent_company": company},
- pluck="name"
- )
- # Always include parent company
- if company not in companies:
- companies.append(company)
+ companies = frappe.get_all(
+ "Company",
+ filters={"parent_company": company},
+ pluck="name",
+ )
+ if company not in companies:
+ companies.append(company)
- return companies
+ return companies
def execute(filters=None):
+ """Run Budget Variance Report with optional consolidation across group companies."""
if not filters:
filters = {}
columns = get_columns(filters)
+
if filters.get("budget_against_filter"):
dimensions = filters.get("budget_against_filter")
else:
dimensions = get_cost_centers(filters)
- period_month_ranges = get_period_month_ranges(filters["period"], filters["from_fiscal_year"])
- cam_map = get_dimension_account_month_map(filters)
+ period_month_ranges = get_period_month_ranges(
+ filters["period"], filters["from_fiscal_year"]
+ )
- # consolidate dimension_account_month map accross companies
- companies = get_companies_for_report(filters)
+ # consolidate dimension_account_month map across companies
+ companies = get_companies_for_report(filters)
if filters.get("accumulated_in_group_company"):
consolidated_cam_map = {}
+ original_company = filters.get("company")
+
for comp in companies:
filters["company"] = comp
company_map = get_dimension_account_month_map(filters)
@@ -55,6 +57,7 @@ def execute(filters=None):
for dim, accounts in company_map.items():
if dim not in consolidated_cam_map:
consolidated_cam_map[dim] = {}
+
for acc, months in accounts.items():
if acc not in consolidated_cam_map[dim]:
consolidated_cam_map[dim][acc] = months
@@ -65,11 +68,16 @@ def execute(filters=None):
for month, vals in mdata.items():
consolidated_cam_map[dim][acc][year].setdefault(month, {})
for key in ("target", "actual"):
- consolidated_cam_map[dim][acc][year][month][key] = \
- consolidated_cam_map[dim][acc][year][month].get(key, 0) + vals.get(key, 0)
- consolidated_cam_map[dim][acc][year][month]["variance"] = \
- consolidated_cam_map[dim][acc][year][month]["target"] - consolidated_cam_map[dim][acc][year][month]["actual"]
-
+ consolidated_cam_map[dim][acc][year][month][key] = (
+ consolidated_cam_map[dim][acc][year][month].get(key, 0)
+ + vals.get(key, 0)
+ )
+ consolidated_cam_map[dim][acc][year][month]["variance"] = (
+ consolidated_cam_map[dim][acc][year][month]["target"]
+ - consolidated_cam_map[dim][acc][year][month]["actual"]
+ )
+
+ filters["company"] = original_company
cam_map = consolidated_cam_map
else:
cam_map = get_dimension_account_month_map(filters)
@@ -78,13 +86,19 @@ def execute(filters=None):
for dimension in dimensions:
dimension_items = cam_map.get(dimension)
if dimension_items:
- data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0)
+ data = get_final_data(
+ dimension,
+ dimension_items,
+ filters,
+ period_month_ranges,
+ data,
+ 0,
+ )
chart = get_chart_data(filters, columns, data)
return columns, data, None, chart
-
def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation):
for account, monthwise_data in dimension_items.items():
row = [dimension, account]