refactor: update budget expense validation to align with new structure

This commit is contained in:
khushi8112
2025-10-28 12:41:05 +05:30
parent af9dc8e406
commit 64456af654
3 changed files with 60 additions and 118 deletions

View File

@@ -15,7 +15,6 @@
"account", "account",
"fiscal_year", "fiscal_year",
"column_break_3", "column_break_3",
"monthly_distribution",
"amended_from", "amended_from",
"distribution_type", "distribution_type",
"allocation_frequency", "allocation_frequency",
@@ -40,10 +39,6 @@
"action_if_accumulated_monthly_exceeded_on_cumulative_expense", "action_if_accumulated_monthly_exceeded_on_cumulative_expense",
"section_break_21", "section_break_21",
"accounts", "accounts",
"section_break_hqka",
"column_break_gnot",
"column_break_ybiq",
"total_budget_amount",
"section_break_fpdt", "section_break_fpdt",
"budget_distribution", "budget_distribution",
"section_break_kkan", "section_break_kkan",
@@ -99,13 +94,6 @@
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"depends_on": "eval:in_list([\"Stop\", \"Warn\"], doc.action_if_accumulated_monthly_budget_exceeded_on_po || doc.action_if_accumulated_monthly_budget_exceeded_on_mr || doc.action_if_accumulated_monthly_budget_exceeded_on_actual)",
"fieldname": "monthly_distribution",
"fieldtype": "Link",
"label": "Monthly Distribution",
"options": "Monthly Distribution"
},
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
@@ -248,24 +236,6 @@
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense", "label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
"options": "\nStop\nWarn\nIgnore" "options": "\nStop\nWarn\nIgnore"
}, },
{
"fieldname": "section_break_hqka",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_gnot",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ybiq",
"fieldtype": "Column Break"
},
{
"fieldname": "total_budget_amount",
"fieldtype": "Currency",
"label": "Total Budget Amount",
"read_only": 1
},
{ {
"fieldname": "section_break_fpdt", "fieldname": "section_break_fpdt",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@@ -331,7 +301,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-10-15 16:55:25.157976", "modified": "2025-10-26 01:09:56.367821",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Budget", "name": "Budget",

View File

@@ -7,6 +7,7 @@ from datetime import date
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate, month_diff from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate, month_diff
from frappe.utils.data import get_first_day from frappe.utils.data import get_first_day
@@ -33,11 +34,9 @@ class Budget(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
from erpnext.accounts.doctype.budget_account.budget_account import BudgetAccount
from erpnext.accounts.doctype.budget_distribution.budget_distribution import BudgetDistribution from erpnext.accounts.doctype.budget_distribution.budget_distribution import BudgetDistribution
account: DF.Link account: DF.Link
accounts: DF.Table[BudgetAccount]
action_if_accumulated_monthly_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"] action_if_accumulated_monthly_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
action_if_accumulated_monthly_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"] action_if_accumulated_monthly_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"] action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
@@ -61,73 +60,67 @@ class Budget(Document):
cost_center: DF.Link | None cost_center: DF.Link | None
distribution_type: DF.Literal["Amount", "Percent"] distribution_type: DF.Literal["Amount", "Percent"]
fiscal_year: DF.Link fiscal_year: DF.Link
monthly_distribution: DF.Link | None
naming_series: DF.Literal["BUDGET-.YYYY.-"] naming_series: DF.Literal["BUDGET-.YYYY.-"]
project: DF.Link | None project: DF.Link | None
revision_of: DF.Data | None revision_of: DF.Data | None
total_budget_amount: DF.Currency
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
if not self.get(frappe.scrub(self.budget_against)): if not self.get(frappe.scrub(self.budget_against)):
frappe.throw(_("{0} is mandatory").format(self.budget_against)) frappe.throw(_("{0} is mandatory").format(self.budget_against))
self.validate_duplicate() self.validate_duplicate()
self.validate_accounts() self.validate_account()
self.set_null_value() self.set_null_value()
self.validate_applicable_for() self.validate_applicable_for()
self.set_total_budget_amount()
def validate_duplicate(self): def validate_duplicate(self):
budget_against_field = frappe.scrub(self.budget_against) budget_against_field = frappe.scrub(self.budget_against)
budget_against = self.get(budget_against_field) budget_against = self.get(budget_against_field)
account = self.account
accounts = [d.account for d in self.accounts] or [] if not account:
existing_budget = frappe.db.sql( return
"""
select existing_budget = frappe.db.get_all(
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba "Budget",
where filters={
ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and "docstatus": ("<", 2),
b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format( "company": self.company,
"%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts)) budget_against_field: budget_against,
), "fiscal_year": self.fiscal_year,
(self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)), "account": account,
as_dict=1, "name": ("!=", self.name),
},
fields=["name", "account"],
) )
for d in existing_budget: if existing_budget:
d = existing_budget[0]
frappe.throw( frappe.throw(
_( _("Another Budget record '{0}' already exists against {1} '{2}' and account '{3}'").format(
"Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}" d.name, self.budget_against, budget_against, d.account
).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year), ),
DuplicateBudgetError, DuplicateBudgetError,
) )
def validate_accounts(self): def validate_account(self):
account_list = [] if not self.account:
for d in self.get("accounts"): frappe.throw(_("Account is mandatory"))
if d.account:
account_details = frappe.get_cached_value( account_details = frappe.get_cached_value(
"Account", d.account, ["is_group", "company", "report_type"], as_dict=1 "Account", self.account, ["is_group", "company", "report_type"], as_dict=1
)
if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(self.account))
elif account_details.company != self.company:
frappe.throw(_("Account {0} does not belong to company {1}").format(self.account, self.company))
elif account_details.report_type != "Profit and Loss":
frappe.throw(
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
self.account
) )
)
if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
elif account_details.company != self.company:
frappe.throw(
_("Account {0} does not belongs to company {1}").format(d.account, self.company)
)
elif account_details.report_type != "Profit and Loss":
frappe.throw(
_(
"Budget cannot be assigned against {0}, as it's not an Income or Expense account"
).format(d.account)
)
if d.account in account_list:
frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
else:
account_list.append(d.account)
def set_null_value(self): def set_null_value(self):
if self.budget_against == "Cost Center": if self.budget_against == "Cost Center":
@@ -153,9 +146,6 @@ class Budget(Document):
): ):
self.applicable_on_booking_actual_expenses = 1 self.applicable_on_booking_actual_expenses = 1
def set_total_budget_amount(self):
self.total_budget_amount = flt(sum(d.budget_amount for d in self.accounts))
def before_save(self): def before_save(self):
self.allocate_budget() self.allocate_budget()
@@ -295,7 +285,7 @@ def validate_expense_against_budget(args, expense_amount=0):
budget_records = frappe.db.sql( budget_records = frappe.db.sql(
f""" f"""
select select
b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution, b.name, b.{budget_against} as budget_against, b.budget_amount, b.monthly_distribution,
ifnull(b.applicable_on_material_request, 0) as for_material_request, ifnull(b.applicable_on_material_request, 0) as for_material_request,
ifnull(applicable_on_purchase_order, 0) as for_purchase_order, ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses, ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
@@ -303,10 +293,10 @@ def validate_expense_against_budget(args, expense_amount=0):
b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr, b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr,
b.action_if_annual_budget_exceeded_on_po, b.action_if_accumulated_monthly_budget_exceeded_on_po b.action_if_annual_budget_exceeded_on_po, b.action_if_accumulated_monthly_budget_exceeded_on_po
from from
`tabBudget` b, `tabBudget Account` ba `tabBudget` b
where where
b.name=ba.parent and b.fiscal_year=%s b.fiscal_year=%s
and ba.account=%s and b.docstatus=1 and b.account=%s and b.docstatus=1
{condition} {condition}
""", """,
(args.fiscal_year, args.account), (args.fiscal_year, args.account),
@@ -335,9 +325,7 @@ def validate_budget_records(args, budget_records, expense_amount):
) )
if monthly_action in ["Stop", "Warn"]: if monthly_action in ["Stop", "Warn"]:
budget_amount = get_accumulated_monthly_budget( budget_amount = get_accumulated_monthly_budget(budget.name, args.posting_date)
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
)
args["month_end_date"] = get_last_day(args.posting_date) args["month_end_date"] = get_last_day(args.posting_date)
@@ -581,37 +569,23 @@ def get_actual_expense(args):
return amount return amount
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget): def get_accumulated_monthly_budget(budget_name, posting_date):
distribution = {} posting_date = getdate(posting_date)
if monthly_distribution:
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
md = frappe.qb.DocType("Monthly Distribution")
res = ( bd = frappe.qb.DocType("Budget Distribution")
frappe.qb.from_(mdp) b = frappe.qb.DocType("Budget")
.join(md)
.on(mdp.parent == md.name)
.select(mdp.month, mdp.percentage_allocation)
.where(md.fiscal_year == fiscal_year)
.where(md.name == monthly_distribution)
.run(as_dict=True)
)
for d in res: result = (
distribution.setdefault(d.month, d.percentage_allocation) frappe.qb.from_(bd)
.join(b)
.on(bd.parent == b.name)
.select(Sum(bd.amount).as_("accumulated_amount"))
.where(b.name == budget_name)
.where(bd.end_date >= posting_date)
.run(as_dict=True)
)
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date") return flt(result[0]["accumulated_amount"]) if result else 0.0
accumulated_percentage = 0.0
while dt <= getdate(posting_date):
if monthly_distribution and distribution:
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
else:
accumulated_percentage += 100.0 / 12
dt = add_months(dt, 1)
return annual_budget * accumulated_percentage / 100
def get_item_details(args): def get_item_details(args):

View File

@@ -59,10 +59,8 @@ class BudgetValidation:
_obj.update( _obj.update(
{ {
"accumulated_monthly_budget": get_accumulated_monthly_budget( "accumulated_monthly_budget": get_accumulated_monthly_budget(
self.budget_map[key].monthly_distribution, self.budget_map[key].name,
self.doc_date, self.doc_date,
self.fiscal_year,
self.budget_map[key].budget_amount,
) )
} }
) )