fix: validate existing expenses when revising or modifying budget amounts

This commit is contained in:
khushi8112
2025-11-06 14:03:09 +05:30
parent 09ed3066d8
commit 57f9faa15a
3 changed files with 95 additions and 62 deletions

View File

@@ -19,7 +19,7 @@
"to_fiscal_year",
"budget_start_date",
"budget_end_date",
"allocation_frequency",
"distribution_frequency",
"budget_amount",
"section_break_nwug",
"distribute_equally",
@@ -238,15 +238,6 @@
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"default": "Monthly",
"fieldname": "allocation_frequency",
"fieldtype": "Select",
"label": "Allocation Frequency",
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
},
{
"fieldname": "budget_amount",
"fieldtype": "Currency",
@@ -302,13 +293,22 @@
"fieldtype": "Date",
"hidden": 1,
"label": "Budget End Date"
},
{
"default": "Monthly",
"fieldname": "distribution_frequency",
"fieldtype": "Select",
"label": "Distribution Frequency",
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
"read_only_depends_on": "eval: doc.revision_of",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-11-05 01:00:46.470251",
"modified": "2025-11-06 10:36:35.565701",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget",

View File

@@ -45,7 +45,6 @@ class Budget(Document):
action_if_annual_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
action_if_annual_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
action_if_annual_exceeded_on_cumulative_expense: DF.Literal["", "Stop", "Warn", "Ignore"]
allocation_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"]
amended_from: DF.Link | None
applicable_on_booking_actual_expenses: DF.Check
applicable_on_cumulative_expense: DF.Check
@@ -59,6 +58,7 @@ class Budget(Document):
company: DF.Link
cost_center: DF.Link | None
distribute_equally: DF.Check
distribution_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"]
from_fiscal_year: DF.Link
naming_series: DF.Literal["BUDGET-.YYYY.-"]
project: DF.Link | None
@@ -75,6 +75,7 @@ class Budget(Document):
self.validate_account()
self.set_null_value()
self.validate_applicable_for()
self.validate_existing_expenses()
def validate_fiscal_year(self):
if self.from_fiscal_year:
@@ -94,12 +95,14 @@ class Budget(Document):
self.budget_start_date = frappe.get_cached_value(
"Fiscal Year", self.from_fiscal_year, "year_start_date"
)
if self.to_fiscal_year:
self.budget_end_date = frappe.get_cached_value(
"Fiscal Year", self.to_fiscal_year, "year_end_date"
)
if self.budget_start_date > self.budget_end_date:
frappe.throw(_("From Fiscal Year cannot be greater than To Fiscal Year"))
def validate_duplicate(self):
budget_against_field = frappe.scrub(self.budget_against)
budget_against = self.get(budget_against_field)
@@ -179,6 +182,47 @@ class Budget(Document):
):
self.applicable_on_booking_actual_expenses = 1
def validate_existing_expenses(self):
if self.is_new() and self.revision_of:
return
args = frappe._dict(
{
"company": self.company,
"account": self.account,
"budget_start_date": self.budget_start_date,
"budget_end_date": self.budget_end_date,
"budget_against_field": frappe.scrub(self.budget_against),
"budget_against_doctype": frappe.unscrub(self.budget_against),
}
)
args[args.budget_against_field] = self.get(args.budget_against_field)
if frappe.get_cached_value("DocType", args.budget_against_doctype, "is_tree"):
args.is_tree = True
else:
args.is_tree = False
actual_spent = get_actual_expense(args)
if actual_spent > self.budget_amount:
frappe.throw(
_(
"Spending for Account {0} ({1}) between {2} and {3} "
"has already exceeded the new allocated budget. "
"Spent: {4}, Budget: {5}"
).format(
frappe.bold(self.account),
frappe.bold(self.company),
frappe.bold(self.budget_start_date),
frappe.bold(self.budget_end_date),
frappe.bold(frappe.utils.fmt_money(actual_spent)),
frappe.bold(frappe.utils.fmt_money(self.budget_amount)),
),
title=_("Budget Limit Exceeded"),
)
def before_save(self):
self.allocate_budget()
@@ -215,7 +259,7 @@ class Budget(Document):
"from_fiscal_year",
"to_fiscal_year",
"budget_amount",
"allocation_frequency",
"distribution_frequency",
"distribute_equally",
]
for field in changed_fields:
@@ -226,7 +270,7 @@ class Budget(Document):
def get_budget_periods(self):
"""Return list of (start_date, end_date) tuples based on frequency."""
frequency = self.allocation_frequency
frequency = self.distribution_frequency
periods = []
start_date = getdate(self.budget_start_date)
@@ -279,7 +323,7 @@ class Budget(Document):
if flt(abs(total_amount - self.budget_amount), 2) > 0.10:
frappe.throw(
_("Total distributed amount {0} must equal Budget Amount {1}").format(
_("Total distributed amount {0} must be equal to Budget Amount {1}").format(
flt(total_amount, 2), self.budget_amount
)
)

View File

@@ -446,24 +446,23 @@ class TestBudget(ERPNextTestSuite):
po.cancel()
jv.cancel()
def test_distribution_date_validation(self):
budget = frappe.new_doc("Budget")
budget.company = self.company
budget.budget_against = "Cost Center"
budget.cost_center = self.cost_center
budget.account = self.account
budget.budget_amount = 100000
start = getdate("2025-04-10")
end = getdate("2025-04-05")
budget.append(
"budget_distribution",
def test_fiscal_year_validation(self):
frappe.get_doc(
{
"start_date": start,
"end_date": end,
"amount": 50000,
},
"doctype": "Fiscal Year",
"year": "2100",
"year_start_date": "2100-04-01",
"year_end_date": "2101-03-31",
"companies": [{"company": "_Test Company"}],
}
).insert(ignore_permissions=True)
budget = make_budget(
budget_against="Cost Center",
from_fiscal_year="2100",
to_fiscal_year="2099",
do_not_save=True,
submit_budget=False,
)
with self.assertRaises(frappe.ValidationError):
@@ -473,35 +472,14 @@ class TestBudget(ERPNextTestSuite):
budget = make_budget(
budget_against="Cost Center",
applicable_on_cumulative_expense=True,
distribute_equally=0,
budget_amount=12000,
do_not_save=False,
submit_budget=True,
submit_budget=False,
)
budget = frappe.new_doc("Budget")
budget.company = self.company
budget.budget_against = "Cost Center"
budget.cost_center = self.cost_center
budget.account = ("_Test Account Cost for Goods Sold - _TC",)
budget.budget_amount = 12000
budget.start_date = getdate("2025-04-01")
budget.end_date = getdate("2025-06-30")
budget.append(
"budget_distribution",
{
"start_date": getdate("2025-04-01"),
"end_date": getdate("2025-04-30"),
"amount": 6000,
},
)
budget.append(
"budget_distribution",
{
"start_date": getdate("2025-05-01"),
"end_date": getdate("2025-06-30"),
"amount": 5000,
},
)
for row in budget.budget_distribution:
row.amount = 2000
with self.assertRaises(frappe.ValidationError):
budget.save()
@@ -531,6 +509,7 @@ class TestBudget(ERPNextTestSuite):
self.assertEqual(old_budget.docstatus, 2)
def test_revision_preserves_distribution(self):
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center - _TC")
budget = make_budget(
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
)
@@ -634,6 +613,7 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
budget_against = budget_against_CC or "_Test Cost Center - _TC"
fiscal_year = get_fiscal_year(nowdate())[0]
fiscal_year_start_date, fiscal_year_end_date = get_fiscal_year(nowdate())[1:3]
args = frappe._dict(
{
@@ -644,12 +624,21 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
"from_fiscal_year": fiscal_year,
"to_fiscal_year": fiscal_year,
"budget_against_field": budget_against_field,
"budget_start_date": fiscal_year_start_date,
"budget_end_date": fiscal_year_end_date,
}
)
if not args.get(budget_against_field):
args[budget_against_field] = budget_against
args.budget_against_doctype = frappe.unscrub(budget_against_field)
if frappe.get_cached_value("DocType", args.budget_against_doctype, "is_tree"):
args.is_tree = True
else:
args.is_tree = False
existing_expense = get_actual_expense(args)
if existing_expense:
@@ -714,8 +703,8 @@ def make_budget(**args):
else:
budget.cost_center = cost_center or "_Test Cost Center - _TC"
budget.from_fiscal_year = fiscal_year
budget.to_fiscal_year = fiscal_year
budget.from_fiscal_year = args.from_fiscal_year or fiscal_year
budget.to_fiscal_year = args.to_fiscal_year or fiscal_year
budget.company = "_Test Company"
budget.account = "_Test Account Cost for Goods Sold - _TC"
budget.budget_amount = args.budget_amount or 200000
@@ -724,7 +713,7 @@ def make_budget(**args):
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
budget.budget_against = budget_against
budget.allocation_frequency = "Monthly"
budget.distribution_frequency = "Monthly"
budget.distribute_equally = args.get("distribute_equally", 1)
if args.applicable_on_material_request: