mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-28 02:58:30 +00:00
Merge pull request #47656 from ruthra-kumar/new_budget_controller_cumulative_breach
fix: handle cumulative breach for monthly and annual
This commit is contained in:
@@ -28,6 +28,10 @@
|
||||
"applicable_on_booking_actual_expenses",
|
||||
"action_if_annual_budget_exceeded",
|
||||
"action_if_accumulated_monthly_budget_exceeded",
|
||||
"control_action_for_cumulative_expense_section",
|
||||
"applicable_on_cumulative_expense",
|
||||
"action_if_annual_exceeded_on_cumulative_expense",
|
||||
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
||||
"section_break_21",
|
||||
"accounts"
|
||||
],
|
||||
@@ -202,12 +206,39 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "control_action_for_cumulative_expense_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Control Action for Cumulative Expense"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "(Purchase Order + Material Request + Actual Expense)",
|
||||
"fieldname": "applicable_on_cumulative_expense",
|
||||
"fieldtype": "Check",
|
||||
"label": "Applicable on Cumulative Expense"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.applicable_on_cumulative_expense == 1",
|
||||
"fieldname": "action_if_annual_exceeded_on_cumulative_expense",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Anual Budget Exceeded on Cumulative Expense",
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.applicable_on_cumulative_expense == 1",
|
||||
"fieldname": "action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:42.675933",
|
||||
"modified": "2025-05-22 13:46:28.510566",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
@@ -231,8 +262,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,14 @@ class Budget(Document):
|
||||
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_po: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_exceeded_on_cumulative_expense: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_annual_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
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"]
|
||||
amended_from: DF.Link | None
|
||||
applicable_on_booking_actual_expenses: DF.Check
|
||||
applicable_on_cumulative_expense: DF.Check
|
||||
applicable_on_material_request: DF.Check
|
||||
applicable_on_purchase_order: DF.Check
|
||||
budget_against: DF.Literal["", "Cost Center", "Project"]
|
||||
|
||||
@@ -380,6 +380,44 @@ class TestBudget(ERPNextTestSuite):
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
def test_action_for_cumulative_limit(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", applicable_on_cumulative_expense=True)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit - 1,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
jv.submit()
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_exceeded_on_cumulative_expense", "Stop"
|
||||
)
|
||||
po = create_purchase_order(
|
||||
transaction_date=nowdate(), qty=1, rate=accumulated_limit + 1, do_not_submit=True
|
||||
)
|
||||
po.set_missing_values()
|
||||
|
||||
self.assertRaises(BudgetError, po.submit)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_exceeded_on_cumulative_expense", "Ignore"
|
||||
)
|
||||
po.submit()
|
||||
|
||||
budget.load_from_db()
|
||||
budget.cancel()
|
||||
po.cancel()
|
||||
jv.cancel()
|
||||
|
||||
|
||||
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
|
||||
if budget_against_field == "project":
|
||||
@@ -479,6 +517,15 @@ def make_budget(**args):
|
||||
args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn"
|
||||
)
|
||||
|
||||
if args.applicable_on_cumulative_expense:
|
||||
budget.applicable_on_cumulative_expense = 1
|
||||
budget.action_if_annual_exceeded_on_cumulative_expense = (
|
||||
args.action_if_annual_exceeded_on_cumulative_expense or "Warn"
|
||||
)
|
||||
budget.action_if_accumulated_monthly_exceeded_on_cumulative_expense = (
|
||||
args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn"
|
||||
)
|
||||
|
||||
budget.insert()
|
||||
budget.submit()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form
|
||||
from frappe.utils import comma_and, flt, fmt_money, get_link_to_form
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import BudgetError, get_accumulated_monthly_budget
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -87,7 +87,8 @@ class BudgetValidation:
|
||||
self.get_ordered_amount(key)
|
||||
self.get_requested_amount(key)
|
||||
|
||||
self.handle_action(key, v)
|
||||
# Pre-emptive validation before hitting ledger
|
||||
self.handle_actions(key, v)
|
||||
|
||||
# Validation happens after submit for Purchase Order and
|
||||
# Material Request and so will be included in the query
|
||||
@@ -96,7 +97,7 @@ class BudgetValidation:
|
||||
v.current_actual_exp_amount = sum([x.debit - x.credit for x in v.get("gl_to_process", [])])
|
||||
|
||||
self.get_actual_expense(key)
|
||||
self.handle_action(key, v)
|
||||
self.handle_actions(key, v)
|
||||
|
||||
def get_child_nodes(self, budget_against, dimension):
|
||||
lft, rgt = frappe.db.get_all(
|
||||
@@ -182,6 +183,9 @@ class BudgetValidation:
|
||||
bud.applicable_on_booking_actual_expenses,
|
||||
bud.action_if_annual_budget_exceeded,
|
||||
bud.action_if_accumulated_monthly_budget_exceeded,
|
||||
bud.applicable_on_cumulative_expense,
|
||||
bud.action_if_annual_exceeded_on_cumulative_expense,
|
||||
bud.action_if_accumulated_monthly_exceeded_on_cumulative_expense,
|
||||
bud_acc.account,
|
||||
bud_acc.budget_amount,
|
||||
)
|
||||
@@ -328,7 +332,7 @@ class BudgetValidation:
|
||||
)
|
||||
self.execute_action(config.action_for_monthly, _msg)
|
||||
|
||||
def handle_action(self, key, v_map):
|
||||
def handle_purchase_order_overlimit(self, key, v_map):
|
||||
self.handle_individual_doctype_action(
|
||||
key,
|
||||
frappe._dict(
|
||||
@@ -344,6 +348,8 @@ class BudgetValidation:
|
||||
v_map.current_ordered_amount,
|
||||
v_map.accumulated_monthly_budget,
|
||||
)
|
||||
|
||||
def handle_material_request_overlimit(self, key, v_map):
|
||||
self.handle_individual_doctype_action(
|
||||
key,
|
||||
frappe._dict(
|
||||
@@ -359,6 +365,8 @@ class BudgetValidation:
|
||||
v_map.current_requested_amount,
|
||||
v_map.accumulated_monthly_budget,
|
||||
)
|
||||
|
||||
def handle_actual_expense_overlimit(self, key, v_map):
|
||||
self.handle_individual_doctype_action(
|
||||
key,
|
||||
frappe._dict(
|
||||
@@ -375,6 +383,53 @@ class BudgetValidation:
|
||||
v_map.accumulated_monthly_budget,
|
||||
)
|
||||
|
||||
def handle_actions(self, key, v_map):
|
||||
self.handle_purchase_order_overlimit(key, v_map)
|
||||
self.handle_material_request_overlimit(key, v_map)
|
||||
self.handle_actual_expense_overlimit(key, v_map)
|
||||
# PO + MR + Actual Expense
|
||||
self.handle_cumulative_overlimit(key, v_map)
|
||||
|
||||
def handle_cumulative_overlimit(self, key, v_map):
|
||||
if v_map.budget_doc.applicable_on_cumulative_expense:
|
||||
self.handle_cumulative_overlimit_for_monthly(key, v_map)
|
||||
self.handle_cumulative_overlimit_for_annual(key, v_map)
|
||||
|
||||
def budget_applicable_for(self, budget_doc) -> str:
|
||||
doctypes = []
|
||||
if budget_doc.applicable_on_purchase_order:
|
||||
doctypes.append("Purchase Order")
|
||||
if budget_doc.applicable_on_material_request:
|
||||
doctypes.append("Material Request")
|
||||
if budget_doc.applicable_on_booking_actual_expenses:
|
||||
doctypes.append("Actual Expense")
|
||||
return comma_and(doctypes)
|
||||
|
||||
def handle_cumulative_overlimit_for_monthly(self, key, v_map):
|
||||
current_amt = (
|
||||
v_map.current_ordered_amount + v_map.current_requested_amount + v_map.current_actual_exp_amount
|
||||
)
|
||||
monthly_diff = (
|
||||
v_map.ordered_amount + v_map.requested_amount + v_map.actual_expense + current_amt
|
||||
) - v_map.accumulated_monthly_budget
|
||||
if monthly_diff > 0:
|
||||
currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
_msg = _(
|
||||
"Accumulated Monthly Budget for Account {0} against {1} {2} is {3}. It will be collectively ({4}) exceeded by {5}"
|
||||
).format(
|
||||
frappe.bold(key[2]),
|
||||
frappe.bold(frappe.unscrub(key[0])),
|
||||
frappe.bold(key[1]),
|
||||
frappe.bold(fmt_money(v_map.accumulated_montly_budget, currency=currency)),
|
||||
self.budget_applicable_for(v_map.budget_doc),
|
||||
frappe.bold(fmt_money(monthly_diff, currency=currency)),
|
||||
)
|
||||
|
||||
self.execute_action(
|
||||
v_map.budget_doc.action_if_accumulated_monthly_exceeded_on_cumulative_expense, _msg
|
||||
)
|
||||
|
||||
def handle_cumulative_overlimit_for_annual(self, key, v_map):
|
||||
current_amt = (
|
||||
v_map.current_ordered_amount + v_map.current_requested_amount + v_map.current_actual_exp_amount
|
||||
)
|
||||
@@ -384,12 +439,13 @@ class BudgetValidation:
|
||||
if total_diff > 0:
|
||||
currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
_msg = _(
|
||||
"Annual Budget for Account {0} against {1} {2} is {3}. It will be exceeded by {4}"
|
||||
"Annual Budget for Account {0} against {1} {2} is {3}. It will be collectively ({4}) exceeded by {5}"
|
||||
).format(
|
||||
frappe.bold(key[2]),
|
||||
frappe.bold(frappe.unscrub(key[0])),
|
||||
frappe.bold(key[1]),
|
||||
frappe.bold(fmt_money(v_map.budget_amount, currency=currency)),
|
||||
self.budget_applicable_for(v_map.budget_doc),
|
||||
frappe.bold(fmt_money(total_diff, currency=currency)),
|
||||
)
|
||||
self.stop(_msg)
|
||||
self.execute_action(v_map.budget_doc.action_if_annual_exceeded_on_cumulative_expense, _msg)
|
||||
|
||||
Reference in New Issue
Block a user