diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index d3931dec3db..3ac7b8fe8f8 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -4,16 +4,6 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Budget", { onload: function (frm) { - frm.set_query("account", "accounts", function () { - return { - filters: { - company: frm.doc.company, - report_type: "Profit and Loss", - is_group: 0, - }, - }; - }); - frm.set_query("monthly_distribution", function () { return { filters: { @@ -30,8 +20,28 @@ frappe.ui.form.on("Budget", { }); }, - refresh: function (frm) { + refresh: async function (frm) { frm.trigger("toggle_reqd_fields"); + + if (!frm.doc.__islocal && frm.doc.docstatus == 1) { + let exception_role = await frappe.db.get_value( + "Company", + frm.doc.company, + "exception_budget_approver_role" + ); + + const role = exception_role.message.exception_budget_approver_role; + + if (role && frappe.user.has_role(role)) { + frm.add_custom_button( + __("Revise Budget"), + function () { + frm.events.revise_budget_action(frm); + }, + __("Actions") + ); + } + } }, budget_against: function (frm) { @@ -39,6 +49,15 @@ frappe.ui.form.on("Budget", { frm.trigger("toggle_reqd_fields"); }, + budget_amount(frm) { + if (frm.doc.budget_distribution?.length) { + frm.doc.budget_distribution.forEach((row) => { + row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2); + }); + frm.refresh_field("budget_distribution"); + } + }, + set_null_value: function (frm) { if (frm.doc.budget_against == "Cost Center") { frm.set_value("project", null); @@ -51,4 +70,44 @@ frappe.ui.form.on("Budget", { frm.toggle_reqd("cost_center", frm.doc.budget_against == "Cost Center"); frm.toggle_reqd("project", frm.doc.budget_against == "Project"); }, + + revise_budget_action: function (frm) { + frappe.confirm( + __( + "Are you sure you want to revise this budget? The current budget will be cancelled and a new draft will be created." + ), + function () { + frappe.call({ + method: "erpnext.accounts.doctype.budget.budget.revise_budget", + args: { budget_name: frm.doc.name }, + callback: function (r) { + if (r.message) { + frappe.msgprint(__("New revised budget created successfully")); + frappe.set_route("Form", "Budget", r.message); + } + }, + }); + }, + function () { + frappe.msgprint(__("Revision cancelled")); + } + ); + }, +}); + +frappe.ui.form.on("Budget Distribution", { + amount(frm, cdt, cdn) { + let row = frappe.get_doc(cdt, cdn); + if (frm.doc.budget_amount) { + row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2); + frm.refresh_field("budget_distribution"); + } + }, + percent(frm, cdt, cdn) { + let row = frappe.get_doc(cdt, cdn); + if (frm.doc.budget_amount) { + row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2); + frm.refresh_field("budget_distribution"); + } + }, }); diff --git a/erpnext/accounts/doctype/budget/budget.json b/erpnext/accounts/doctype/budget/budget.json index fcd78691a03..8476a2831f0 100644 --- a/erpnext/accounts/doctype/budget/budget.json +++ b/erpnext/accounts/doctype/budget/budget.json @@ -12,10 +12,19 @@ "company", "cost_center", "project", - "fiscal_year", + "account", "column_break_3", - "monthly_distribution", "amended_from", + "from_fiscal_year", + "to_fiscal_year", + "budget_start_date", + "budget_end_date", + "distribution_frequency", + "budget_amount", + "section_break_nwug", + "distribute_equally", + "section_break_fpdt", + "budget_distribution", "section_break_6", "applicable_on_material_request", "action_if_annual_budget_exceeded_on_mr", @@ -32,8 +41,8 @@ "applicable_on_cumulative_expense", "action_if_annual_exceeded_on_cumulative_expense", "action_if_accumulated_monthly_exceeded_on_cumulative_expense", - "section_break_21", - "accounts" + "section_break_kkan", + "revision_of" ], "fields": [ { @@ -44,6 +53,7 @@ "in_standard_filter": 1, "label": "Budget Against", "options": "\nCost Center\nProject", + "read_only_depends_on": "eval: doc.revision_of", "reqd": 1 }, { @@ -53,6 +63,7 @@ "in_standard_filter": 1, "label": "Company", "options": "Company", + "read_only_depends_on": "eval: doc.revision_of", "reqd": 1 }, { @@ -62,7 +73,8 @@ "in_global_search": 1, "in_standard_filter": 1, "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "read_only_depends_on": "eval: doc.revision_of" }, { "depends_on": "eval:doc.budget_against == 'Project'", @@ -70,28 +82,13 @@ "fieldtype": "Link", "in_standard_filter": 1, "label": "Project", - "options": "Project" - }, - { - "fieldname": "fiscal_year", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Fiscal Year", - "options": "Fiscal Year", - "reqd": 1 + "options": "Project", + "read_only_depends_on": "eval: doc.revision_of" }, { "fieldname": "column_break_3", "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", "fieldtype": "Link", @@ -187,22 +184,12 @@ "options": "\nStop\nWarn\nIgnore" }, { - "fieldname": "section_break_21", - "fieldtype": "Section Break" - }, - { - "fieldname": "accounts", - "fieldtype": "Table", - "label": "Budget Accounts", - "options": "Budget Account", - "reqd": 1 - }, - { + "default": "BUDGET-.########", "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", "no_copy": 1, - "options": "BUDGET-.YYYY.-", + "options": "BUDGET-.########", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -232,13 +219,97 @@ "fieldtype": "Select", "label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense", "options": "\nStop\nWarn\nIgnore" + }, + { + "fieldname": "section_break_fpdt", + "fieldtype": "Section Break" + }, + { + "fieldname": "budget_distribution", + "fieldtype": "Table", + "label": "Budget Distribution", + "options": "Budget Distribution" + }, + { + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Account", + "options": "Account", + "read_only_depends_on": "eval: doc.revision_of", + "reqd": 1 + }, + { + "fieldname": "budget_amount", + "fieldtype": "Currency", + "label": "Budget Amount", + "reqd": 1 + }, + { + "fieldname": "section_break_kkan", + "fieldtype": "Section Break" + }, + { + "fieldname": "revision_of", + "fieldtype": "Data", + "label": "Revision Of", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "1", + "fieldname": "distribute_equally", + "fieldtype": "Check", + "label": "Distribute Equally" + }, + { + "fieldname": "section_break_nwug", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "from_fiscal_year", + "fieldtype": "Link", + "label": "From Fiscal Year", + "options": "Fiscal Year", + "read_only_depends_on": "eval: doc.revision_of", + "reqd": 1 + }, + { + "fieldname": "to_fiscal_year", + "fieldtype": "Link", + "label": "To Fiscal Year", + "options": "Fiscal Year", + "read_only_depends_on": "eval: doc.revision_of", + "reqd": 1 + }, + { + "fieldname": "budget_start_date", + "fieldtype": "Date", + "hidden": 1, + "label": "Budget Start Date" + }, + { + "fieldname": "budget_end_date", + "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-06-16 15:57:13.114981", + "modified": "2025-11-19 17:00:00.648224", "modified_by": "Administrator", "module": "Accounts", "name": "Budget", diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index a55c189f783..d798da5b589 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -2,10 +2,14 @@ # For license information, please see license.txt +from datetime import date + import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate +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.data import get_first_day, nowdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, @@ -30,9 +34,9 @@ class Budget(Document): if TYPE_CHECKING: 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 - accounts: DF.Table[BudgetAccount] + account: DF.Link 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"] @@ -47,73 +51,117 @@ class Budget(Document): applicable_on_material_request: DF.Check applicable_on_purchase_order: DF.Check budget_against: DF.Literal["", "Cost Center", "Project"] + budget_amount: DF.Currency + budget_distribution: DF.Table[BudgetDistribution] + budget_end_date: DF.Date | None + budget_start_date: DF.Date | None company: DF.Link cost_center: DF.Link | None - fiscal_year: DF.Link - monthly_distribution: DF.Link | None - naming_series: DF.Literal["BUDGET-.YYYY.-"] + distribute_equally: DF.Check + distribution_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"] + from_fiscal_year: DF.Link + naming_series: DF.Literal["BUDGET-.########"] project: DF.Link | None + revision_of: DF.Data | None + to_fiscal_year: DF.Link # end: auto-generated types def validate(self): if not self.get(frappe.scrub(self.budget_against)): frappe.throw(_("{0} is mandatory").format(self.budget_against)) + self.validate_budget_amount() + self.validate_fiscal_year() + self.set_fiscal_year_dates() self.validate_duplicate() - self.validate_accounts() + self.validate_account() self.set_null_value() self.validate_applicable_for() + self.validate_existing_expenses() + + def validate_budget_amount(self): + if self.budget_amount <= 0: + frappe.throw(_("Budget Amount can not be {0}.").format(self.budget_amount)) + + def validate_fiscal_year(self): + if self.from_fiscal_year: + self.validate_fiscal_year_company(self.from_fiscal_year, self.company) + if self.to_fiscal_year: + self.validate_fiscal_year_company(self.to_fiscal_year, self.company) + + def validate_fiscal_year_company(self, fiscal_year, company): + linked_companies = frappe.get_all( + "Fiscal Year Company", filters={"parent": fiscal_year}, pluck="company" + ) + if linked_companies and company not in linked_companies: + frappe.throw(_("Fiscal Year {0} is not available for Company {1}.").format(fiscal_year, company)) + + def set_fiscal_year_dates(self): + if self.from_fiscal_year: + 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) + account = self.account + + if not account: + return - accounts = [d.account for d in self.accounts] or [] existing_budget = frappe.db.sql( - """ - select - b.name, ba.account from `tabBudget` b, `tabBudget Account` ba - where - ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and - b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format( - "%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts)) - ), - (self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)), - as_dict=1, + f""" + SELECT name, account + FROM `tabBudget` + WHERE + docstatus < 2 + AND company = %s + AND {budget_against_field} = %s + AND account = %s + AND name != %s + AND ( + (SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s + AND (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s + ) + """, + (self.company, budget_against, account, self.name, self.budget_end_date, self.budget_start_date), + as_dict=True, ) - for d in existing_budget: + if existing_budget: + d = existing_budget[0] frappe.throw( _( - "Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}" - ).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year), + "Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' with overlapping fiscal years." + ).format(d.name, self.budget_against, budget_against, d.account), DuplicateBudgetError, ) - def validate_accounts(self): - account_list = [] - for d in self.get("accounts"): - if d.account: - account_details = frappe.get_cached_value( - "Account", d.account, ["is_group", "company", "report_type"], as_dict=1 + def validate_account(self): + if not self.account: + frappe.throw(_("Account is mandatory")) + + account_details = frappe.get_cached_value( + "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): if self.budget_against == "Cost Center": @@ -139,30 +187,201 @@ class Budget(Document): ): self.applicable_on_booking_actual_expenses = 1 + def validate_existing_expenses(self): + if self.is_new() and self.revision_of: + return -def validate_expense_against_budget(args, expense_amount=0): - args = frappe._dict(args) + params = 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), + } + ) + + params[params.budget_against_field] = self.get(params.budget_against_field) + + if frappe.get_cached_value("DocType", params.budget_against_doctype, "is_tree"): + params.is_tree = True + else: + params.is_tree = False + + actual_spent = get_actual_expense(params) + + 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() + + def on_update(self): + self.validate_distribution_totals() + + def allocate_budget(self): + if self.revision_of: + return + + if not self.should_regenerate_budget_distribution(): + return + + self.set("budget_distribution", []) + + periods = self.get_budget_periods() + total_periods = len(periods) + row_percent = 100 / total_periods if total_periods else 0 + + for start_date, end_date in periods: + row = self.append("budget_distribution", {}) + row.start_date = start_date + row.end_date = end_date + self.add_allocated_amount(row, row_percent) + + def should_regenerate_budget_distribution(self): + """Check whether budget distribution should be recalculated.""" + old_doc = self.get_doc_before_save() if not self.is_new() else None + if not old_doc or not self.budget_distribution: + return True + + if old_doc: + changed_fields = [ + "from_fiscal_year", + "to_fiscal_year", + "budget_amount", + "distribution_frequency", + "distribute_equally", + ] + for field in changed_fields: + if old_doc.get(field) != self.get(field): + return True + + return bool(self.distribute_equally) + + def get_budget_periods(self): + """Return list of (start_date, end_date) tuples based on frequency.""" + frequency = self.distribution_frequency + periods = [] + + start_date = getdate(self.budget_start_date) + end_date = getdate(self.budget_end_date) + + while start_date <= end_date: + period_start = get_first_day(start_date) + period_end = self.get_period_end(period_start, frequency) + period_end = min(period_end, end_date) + + periods.append((period_start, period_end)) + start_date = add_months(period_start, self.get_month_increment(frequency)) + + return periods + + def get_period_end(self, start_date, frequency): + """Return the correct end date for a given frequency.""" + if frequency == "Monthly": + return get_last_day(start_date) + elif frequency == "Quarterly": + return get_last_day(add_months(start_date, 2)) + elif frequency == "Half-Yearly": + return get_last_day(add_months(start_date, 5)) + else: # Yearly + return get_last_day(add_months(start_date, 11)) + + def get_month_increment(self, frequency): + """Return how many months to move forward for the next period.""" + return { + "Monthly": 1, + "Quarterly": 3, + "Half-Yearly": 6, + "Yearly": 12, + }.get(frequency, 1) + + def add_allocated_amount(self, row, row_percent): + if not self.distribute_equally: + row.amount = 0 + row.percent = 0 + else: + row.amount = flt(self.budget_amount * row_percent / 100, 3) + row.percent = flt(row_percent, 3) + + def validate_distribution_totals(self): + if self.should_regenerate_budget_distribution(): + return + + total_amount = sum(d.amount for d in self.budget_distribution) + total_percent = sum(d.percent for d in self.budget_distribution) + + if flt(abs(total_amount - self.budget_amount), 2) > 0.10: + frappe.throw( + _("Total distributed amount {0} must be equal to Budget Amount {1}").format( + flt(total_amount, 2), self.budget_amount + ) + ) + + if flt(abs(total_percent - 100), 2) > 0.10: + frappe.throw( + _("Total distribution percent must equal 100 (currently {0})").format(round(total_percent, 2)) + ) + + +def validate_expense_against_budget(params, expense_amount=0): + params = frappe._dict(params) if not frappe.db.count("Budget", cache=True): return - if not args.fiscal_year: - args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0] + if not params.fiscal_year: + params.fiscal_year = get_fiscal_year(params.get("posting_date"), company=params.get("company"))[0] - if args.get("company"): - frappe.flags.exception_approver_role = frappe.get_cached_value( - "Company", args.get("company"), "exception_budget_approver_role" - ) + posting_date = getdate(params.get("posting_date")) + posting_fiscal_year = get_fiscal_year(posting_date, company=params.get("company"))[0] + year_start_date, year_end_date = get_fiscal_year_date_range(posting_fiscal_year, posting_fiscal_year) - if not frappe.db.get_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): + budget_exists = frappe.db.sql( + """ + select name + from `tabBudget` + where company = %s + and docstatus = 1 + and (SELECT year_start_date FROM `tabFiscal Year` WHERE name = from_fiscal_year) <= %s + and (SELECT year_end_date FROM `tabFiscal Year` WHERE name = to_fiscal_year) >= %s + limit 1 + """, + (params.company, year_end_date, year_start_date), + ) + + if not budget_exists: return - if not args.account: - args.account = args.get("expense_account") + if params.get("company"): + frappe.flags.exception_approver_role = frappe.get_cached_value( + "Company", params.get("company"), "exception_budget_approver_role" + ) - if not (args.get("account") and args.get("cost_center")) and args.item_code: - args.cost_center, args.account = get_item_details(args) + if not params.account: + params.account = params.get("expense_account") - if not args.account: + if not params.get("expense_account") and params.get("account"): + params.expense_account = params.account + + if not (params.get("account") and params.get("cost_center")) and params.item_code: + params.cost_center, params.account = get_item_details(params) + + if not params.account: return default_dimensions = [ @@ -180,59 +399,78 @@ def validate_expense_against_budget(args, expense_amount=0): budget_against = dimension.get("fieldname") if ( - args.get(budget_against) - and args.account - and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense") + params.get(budget_against) + and params.account + and (frappe.get_cached_value("Account", params.account, "root_type") == "Expense") ): doctype = dimension.get("document_type") if frappe.get_cached_value("DocType", doctype, "is_tree"): - lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"]) + lft, rgt = frappe.get_cached_value(doctype, params.get(budget_against), ["lft", "rgt"]) condition = f"""and exists(select name from `tab{doctype}` where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec - args.is_tree = True + params.is_tree = True else: - condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}" - args.is_tree = False + condition = f"and b.{budget_against}={frappe.db.escape(params.get(budget_against))}" + params.is_tree = False - args.budget_against_field = budget_against - args.budget_against_doctype = doctype + params.budget_against_field = budget_against + params.budget_against_doctype = doctype budget_records = frappe.db.sql( f""" - select - b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution, - ifnull(b.applicable_on_material_request, 0) as for_material_request, - ifnull(applicable_on_purchase_order, 0) as for_purchase_order, - ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses, - b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded, - 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 - from - `tabBudget` b, `tabBudget Account` ba - where - b.name=ba.parent and b.fiscal_year=%s - and ba.account=%s and b.docstatus=1 + SELECT + b.name, + b.{budget_against} AS budget_against, + b.budget_amount, + b.from_fiscal_year, + b.to_fiscal_year, + b.budget_start_date, + b.budget_end_date, + IFNULL(b.applicable_on_material_request, 0) AS for_material_request, + IFNULL(b.applicable_on_purchase_order, 0) AS for_purchase_order, + IFNULL(b.applicable_on_booking_actual_expenses, 0) AS for_actual_expenses, + b.action_if_annual_budget_exceeded, + b.action_if_accumulated_monthly_budget_exceeded, + 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 + FROM + `tabBudget` b + WHERE + b.company = %s + AND b.docstatus = 1 + AND %s BETWEEN b.budget_start_date AND b.budget_end_date + AND b.account = %s {condition} - """, - (args.fiscal_year, args.account), + """, + (params.company, params.posting_date, params.account), as_dict=True, ) # nosec if budget_records: - validate_budget_records(args, budget_records, expense_amount) + validate_budget_records(params, budget_records, expense_amount) -def validate_budget_records(args, budget_records, expense_amount): +def validate_budget_records(params, budget_records, expense_amount): for budget in budget_records: if flt(budget.budget_amount): - yearly_action, monthly_action = get_actions(args, budget) - args["for_material_request"] = budget.for_material_request - args["for_purchase_order"] = budget.for_purchase_order + yearly_action, monthly_action = get_actions(params, budget) + params["for_material_request"] = budget.for_material_request + params["for_purchase_order"] = budget.for_purchase_order + params["from_fiscal_year"], params["to_fiscal_year"] = ( + budget.from_fiscal_year, + budget.to_fiscal_year, + ) + params["budget_start_date"], params["budget_end_date"] = ( + budget.budget_start_date, + budget.budget_end_date, + ) if yearly_action in ("Stop", "Warn"): compare_expense_with_budget( - args, + params, flt(budget.budget_amount), _("Annual"), yearly_action, @@ -241,14 +479,12 @@ def validate_budget_records(args, budget_records, expense_amount): ) if monthly_action in ["Stop", "Warn"]: - budget_amount = get_accumulated_monthly_budget( - budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount - ) + budget_amount = get_accumulated_monthly_budget(budget.name, params.posting_date) - args["month_end_date"] = get_last_day(args.posting_date) + params["month_end_date"] = get_last_day(params.posting_date) compare_expense_with_budget( - args, + params, budget_amount, _("Accumulated Monthly"), monthly_action, @@ -257,38 +493,41 @@ def validate_budget_records(args, budget_records, expense_amount): ) -def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0): - args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0 +def compare_expense_with_budget(params, budget_amount, action_for, action, budget_against, amount=0): + params.actual_expense, params.requested_amount, params.ordered_amount = get_actual_expense(params), 0, 0 if not amount: - args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args) + params.requested_amount, params.ordered_amount = ( + get_requested_amount(params), + get_ordered_amount(params), + ) - if args.get("doctype") == "Material Request" and args.for_material_request: - amount = args.requested_amount + args.ordered_amount + if params.get("doctype") == "Material Request" and params.for_material_request: + amount = params.requested_amount + params.ordered_amount - elif args.get("doctype") == "Purchase Order" and args.for_purchase_order: - amount = args.ordered_amount + elif params.get("doctype") == "Purchase Order" and params.for_purchase_order: + amount = params.ordered_amount - total_expense = args.actual_expense + amount + total_expense = params.actual_expense + amount if total_expense > budget_amount: - if args.actual_expense > budget_amount: - diff = args.actual_expense - budget_amount + if params.actual_expense > budget_amount: + diff = params.actual_expense - budget_amount _msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It is already exceeded by {5}.") else: diff = total_expense - budget_amount _msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will be exceeded by {5}.") - currency = frappe.get_cached_value("Company", args.company, "default_currency") + currency = frappe.get_cached_value("Company", params.company, "default_currency") msg = _msg.format( _(action_for), - frappe.bold(args.account), - frappe.unscrub(args.budget_against_field), + frappe.bold(params.account), + frappe.unscrub(params.budget_against_field), frappe.bold(budget_against), frappe.bold(fmt_money(budget_amount, currency=currency)), frappe.bold(fmt_money(diff, currency=currency)), ) - msg += get_expense_breakup(args, currency, budget_against) + msg += get_expense_breakup(params, currency, budget_against) if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles( frappe.session.user @@ -301,14 +540,25 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_ frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded")) -def get_expense_breakup(args, currency, budget_against): - msg = "
{{ _('Total Expenses booked through') }} -