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]