Merge pull request #55566 from frappe/mergify/bp/version-16-hotfix/pr-55562

fix: aggregate child cost center data in Budget Variance Report (backport #55562)
This commit is contained in:
Khushi Rawat
2026-06-03 03:37:27 +05:30
committed by GitHub

View File

@@ -3,6 +3,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder import CustomFunction
from frappe.utils import add_months, flt, formatdate from frappe.utils import add_months, flt, formatdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
@@ -19,6 +20,8 @@ def execute(filters=None):
columns = get_columns(filters) columns = get_columns(filters)
if filters.get("budget_against_filter"): if filters.get("budget_against_filter"):
dimensions = filters.get("budget_against_filter") dimensions = filters.get("budget_against_filter")
if filters.get("budget_against") == "Cost Center":
dimensions = get_cost_center_with_children(dimensions)
else: else:
dimensions = get_budget_dimensions(filters) dimensions = get_budget_dimensions(filters)
if not dimensions: if not dimensions:
@@ -40,39 +43,29 @@ def validate_filters(filters):
def get_budget_records(filters, dimensions): def get_budget_records(filters, dimensions):
budget_against_field = frappe.scrub(filters["budget_against"]) budget_against_field = frappe.scrub(filters["budget_against"])
budget = frappe.qb.DocType("Budget")
return frappe.db.sql( return (
f""" frappe.qb.from_(budget)
SELECT .select(
b.name, budget.name,
b.account, budget.account,
b.{budget_against_field} AS dimension, budget[budget_against_field].as_("dimension"),
b.budget_amount, budget.budget_amount,
b.from_fiscal_year, budget.from_fiscal_year,
b.to_fiscal_year, budget.to_fiscal_year,
b.budget_start_date, budget.budget_start_date,
b.budget_end_date budget.budget_end_date,
FROM )
`tabBudget` b .where(
WHERE (budget.company == filters.company)
b.company = %s & (budget.docstatus == 1)
AND b.docstatus = 1 & (budget.budget_against == filters.budget_against)
AND b.budget_against = %s & (budget[budget_against_field].isin(dimensions))
AND b.{budget_against_field} IN ({", ".join(["%s"] * len(dimensions))}) & (budget.from_fiscal_year <= filters.to_fiscal_year)
AND ( & (budget.to_fiscal_year >= filters.from_fiscal_year)
b.from_fiscal_year <= %s )
AND b.to_fiscal_year >= %s ).run(as_dict=True)
)
""",
(
filters.company,
filters.budget_against,
*dimensions,
filters.to_fiscal_year,
filters.from_fiscal_year,
),
as_dict=True,
)
def build_budget_map(budget_records, filters): def build_budget_map(budget_records, filters):
@@ -120,50 +113,41 @@ def build_budget_map(budget_records, filters):
def get_actual_transactions(dimension_name, filters): def get_actual_transactions(dimension_name, filters):
budget_against = frappe.scrub(filters.get("budget_against")) budget_against = frappe.scrub(filters.get("budget_against"))
cost_center_filter = "" monthname = CustomFunction("MONTHNAME", ["date"])
gle = frappe.qb.DocType("GL Entry")
budget = frappe.qb.DocType("Budget")
query = (
frappe.qb.from_(gle)
.from_(budget)
.select(
gle.account,
gle.debit,
gle.credit,
gle.fiscal_year,
monthname(gle.posting_date).as_("month_name"),
budget[budget_against].as_("budget_against"),
)
.where(
(budget.docstatus == 1)
& (budget.account == gle.account)
& (gle.fiscal_year >= filters.from_fiscal_year)
& (gle.fiscal_year <= filters.to_fiscal_year)
& (gle.is_cancelled == 0)
& (budget[budget_against] == dimension_name)
)
.groupby(gle.name)
.orderby(gle.fiscal_year)
)
if filters.get("budget_against") == "Cost Center" and dimension_name: if filters.get("budget_against") == "Cost Center" and dimension_name:
cc_lft, cc_rgt = frappe.db.get_value("Cost Center", dimension_name, ["lft", "rgt"]) cost_centers = get_cost_center_with_children([dimension_name])
cost_center_filter = f""" query = query.where(gle.cost_center.isin(cost_centers))
and lft >= "{cc_lft}" else:
and rgt <= "{cc_rgt}" query = query.where(budget[budget_against] == gle[budget_against])
"""
actual_transactions = frappe.db.sql( actual_transactions = query.run(as_dict=True)
f"""
select
gl.account,
gl.debit,
gl.credit,
gl.fiscal_year,
MONTHNAME(gl.posting_date) as month_name,
b.{budget_against} as budget_against
from
`tabGL Entry` gl,
`tabBudget` b
where
b.docstatus = 1
and b.account=gl.account
and b.{budget_against} = gl.{budget_against}
and gl.fiscal_year between %s and %s
and gl.is_cancelled = 0
and b.{budget_against} = %s
and exists(
select
name
from
`tab{filters.budget_against}`
where
name = gl.{budget_against}
{cost_center_filter}
)
group by
gl.name
order by gl.fiscal_year
""",
(filters.from_fiscal_year, filters.to_fiscal_year, dimension_name),
as_dict=1,
)
actual_transactions_map = {} actual_transactions_map = {}
for transaction in actual_transactions: for transaction in actual_transactions:
@@ -382,33 +366,37 @@ def get_fiscal_years(filters):
return fiscal_year return fiscal_year
def get_budget_dimensions(filters): def get_cost_center_with_children(cost_centers):
order_by = "" """Expand each cost center to include itself and all its descendants."""
if filters.get("budget_against") == "Cost Center": cc = frappe.qb.DocType("Cost Center")
order_by = "order by lft" all_cost_centers = set()
for cost_center in cost_centers:
if filters.get("budget_against") in ["Cost Center", "Project"]: result = frappe.db.get_value("Cost Center", cost_center, ["lft", "rgt"])
return frappe.db.sql_list( if not result:
""" continue
select lft, rgt = result
name children = (
from frappe.qb.from_(cc).select(cc.name).where((cc.lft >= lft) & (cc.rgt <= rgt)).run(pluck="name")
`tab{tab}`
where
company = %s
{order_by}
""".format(tab=filters.get("budget_against"), order_by=order_by),
filters.get("company"),
) )
all_cost_centers.update(children)
return list(all_cost_centers)
def get_budget_dimensions(filters):
budget_against = filters.get("budget_against")
dimension = frappe.qb.DocType(budget_against)
if budget_against in ["Cost Center", "Project"]:
query = (
frappe.qb.from_(dimension)
.select(dimension.name)
.where(dimension.company == filters.get("company"))
)
if budget_against == "Cost Center":
query = query.orderby(dimension.lft)
return query.run(pluck="name")
else: else:
return frappe.db.sql_list( return frappe.qb.from_(dimension).select(dimension.name).run(pluck="name")
"""
select
name
from
`tab{tab}`
""".format(tab=filters.get("budget_against"))
) # nosec
def validate_budget_dimensions(filters): def validate_budget_dimensions(filters):