mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-04 12:49:10 +00:00
refactor: update budget expense validation to align with new structure
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user