mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 16:34:46 +00:00
feat: budget for multiple fiscal year
This commit is contained in:
@@ -13,14 +13,17 @@
|
|||||||
"cost_center",
|
"cost_center",
|
||||||
"project",
|
"project",
|
||||||
"account",
|
"account",
|
||||||
"fiscal_year",
|
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
|
"from_fiscal_year",
|
||||||
|
"to_fiscal_year",
|
||||||
"distribution_type",
|
"distribution_type",
|
||||||
"allocation_frequency",
|
"allocation_frequency",
|
||||||
"budget_start_date",
|
|
||||||
"budget_end_date",
|
|
||||||
"budget_amount",
|
"budget_amount",
|
||||||
|
"section_break_nwug",
|
||||||
|
"distribute_equally",
|
||||||
|
"section_break_fpdt",
|
||||||
|
"budget_distribution",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"applicable_on_material_request",
|
"applicable_on_material_request",
|
||||||
"action_if_annual_budget_exceeded_on_mr",
|
"action_if_annual_budget_exceeded_on_mr",
|
||||||
@@ -37,8 +40,6 @@
|
|||||||
"applicable_on_cumulative_expense",
|
"applicable_on_cumulative_expense",
|
||||||
"action_if_annual_exceeded_on_cumulative_expense",
|
"action_if_annual_exceeded_on_cumulative_expense",
|
||||||
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
||||||
"section_break_fpdt",
|
|
||||||
"budget_distribution",
|
|
||||||
"section_break_kkan",
|
"section_break_kkan",
|
||||||
"revision_of"
|
"revision_of"
|
||||||
],
|
],
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Budget Against",
|
"label": "Budget Against",
|
||||||
"options": "\nCost Center\nProject",
|
"options": "\nCost Center\nProject",
|
||||||
|
"read_only_depends_on": "eval: doc.revision_of",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -60,6 +62,7 @@
|
|||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
|
"read_only_depends_on": "eval: doc.revision_of",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -69,7 +72,8 @@
|
|||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center",
|
||||||
|
"read_only_depends_on": "eval: doc.revision_of"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.budget_against == 'Project'",
|
"depends_on": "eval:doc.budget_against == 'Project'",
|
||||||
@@ -77,16 +81,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
"options": "Project"
|
"options": "Project",
|
||||||
},
|
"read_only_depends_on": "eval: doc.revision_of"
|
||||||
{
|
|
||||||
"fieldname": "fiscal_year",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "Fiscal Year",
|
|
||||||
"options": "Fiscal Year",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
@@ -237,6 +233,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Account",
|
"label": "Account",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
|
"read_only_depends_on": "eval: doc.revision_of",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -245,18 +242,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Distribution Type",
|
"label": "Distribution Type",
|
||||||
"options": "Amount\nPercent",
|
"options": "Amount\nPercent",
|
||||||
"reqd": 1
|
"read_only_depends_on": "eval: doc.revision_of",
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "budget_start_date",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"label": "Budget Start Date",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "budget_end_date",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"label": "Budget End Date",
|
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -264,14 +250,15 @@
|
|||||||
"fieldname": "allocation_frequency",
|
"fieldname": "allocation_frequency",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Allocation Frequency",
|
"label": "Allocation Frequency",
|
||||||
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly\nDate Range",
|
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
|
||||||
|
"read_only_depends_on": "eval: doc.revision_of",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "budget_amount",
|
"fieldname": "budget_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Budget Amount",
|
"label": "Budget Amount",
|
||||||
"options": "Company:company:default_currency",
|
"read_only_depends_on": "eval: doc.revision_of",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -284,13 +271,40 @@
|
|||||||
"label": "Revision Of",
|
"label": "Revision Of",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 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
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-29 03:06:52.730795",
|
"modified": "2025-10-30 19:07:51.022844",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Budget",
|
"name": "Budget",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.functions import Sum
|
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, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
@@ -45,7 +45,7 @@ class Budget(Document):
|
|||||||
action_if_annual_budget_exceeded_on_mr: 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_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||||
action_if_annual_exceeded_on_cumulative_expense: 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", "Date Range"]
|
allocation_frequency: DF.Literal["Monthly", "Quarterly", "Half-Yearly", "Yearly"]
|
||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
applicable_on_booking_actual_expenses: DF.Check
|
applicable_on_booking_actual_expenses: DF.Check
|
||||||
applicable_on_cumulative_expense: DF.Check
|
applicable_on_cumulative_expense: DF.Check
|
||||||
@@ -54,15 +54,15 @@ class Budget(Document):
|
|||||||
budget_against: DF.Literal["", "Cost Center", "Project"]
|
budget_against: DF.Literal["", "Cost Center", "Project"]
|
||||||
budget_amount: DF.Currency
|
budget_amount: DF.Currency
|
||||||
budget_distribution: DF.Table[BudgetDistribution]
|
budget_distribution: DF.Table[BudgetDistribution]
|
||||||
budget_end_date: DF.Date
|
|
||||||
budget_start_date: DF.Date
|
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
|
distribute_equally: DF.Check
|
||||||
distribution_type: DF.Literal["Amount", "Percent"]
|
distribution_type: DF.Literal["Amount", "Percent"]
|
||||||
fiscal_year: DF.Link
|
from_fiscal_year: DF.Link
|
||||||
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
|
||||||
|
to_fiscal_year: DF.Link
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@@ -81,25 +81,38 @@ class Budget(Document):
|
|||||||
if not account:
|
if not account:
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_budget = frappe.db.get_all(
|
from_start, _ = frappe.get_cached_value(
|
||||||
"Budget",
|
"Fiscal Year", self.from_fiscal_year, ["year_start_date", "year_end_date"]
|
||||||
filters={
|
)
|
||||||
"docstatus": ("<", 2),
|
_, to_end = frappe.get_cached_value(
|
||||||
"company": self.company,
|
"Fiscal Year", self.to_fiscal_year, ["year_start_date", "year_end_date"]
|
||||||
budget_against_field: budget_against,
|
)
|
||||||
"fiscal_year": self.fiscal_year,
|
|
||||||
"account": account,
|
existing_budget = frappe.db.sql(
|
||||||
"name": ("!=", self.name),
|
f"""
|
||||||
},
|
SELECT name, account
|
||||||
fields=["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, to_end, from_start),
|
||||||
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing_budget:
|
if existing_budget:
|
||||||
d = existing_budget[0]
|
d = existing_budget[0]
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Another Budget record '{0}' already exists against {1} '{2}' and account '{3}'").format(
|
_(
|
||||||
d.name, self.budget_against, budget_against, d.account
|
"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,
|
DuplicateBudgetError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -153,75 +166,99 @@ class Budget(Document):
|
|||||||
if self.revision_of:
|
if self.revision_of:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.set("budget_distribution", [])
|
if not self.should_regenerate_budget_distribution():
|
||||||
if not (self.budget_start_date and self.budget_end_date and self.allocation_frequency):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
start = getdate(self.budget_start_date)
|
self.set("budget_distribution", [])
|
||||||
end = getdate(self.budget_end_date)
|
|
||||||
freq = self.allocation_frequency
|
|
||||||
|
|
||||||
months = month_diff(end, start)
|
self.set_budget_date_range()
|
||||||
if freq == "Monthly":
|
periods = self.get_budget_periods()
|
||||||
total_periods = months
|
total_periods = len(periods)
|
||||||
elif freq == "Quarterly":
|
row_percent = 100 / total_periods if total_periods else 0
|
||||||
total_periods = months // 3 + (1 if months % 3 else 0)
|
|
||||||
elif freq == "Half-Yearly":
|
|
||||||
total_periods = months // 6 + (1 if months % 6 else 0)
|
|
||||||
else:
|
|
||||||
total_periods = end.year - start.year + 1
|
|
||||||
|
|
||||||
if self.distribution_type == "Amount":
|
for start_date, end_date in periods:
|
||||||
per_row = flt(self.budget_amount / total_periods, 2)
|
|
||||||
else:
|
|
||||||
per_row = flt(100 / total_periods, 2)
|
|
||||||
|
|
||||||
assigned = 0
|
|
||||||
current = start
|
|
||||||
|
|
||||||
while current <= end:
|
|
||||||
row = self.append("budget_distribution", {})
|
row = self.append("budget_distribution", {})
|
||||||
|
row.start_date = start_date
|
||||||
|
row.end_date = end_date
|
||||||
|
self.add_allocated_amount(row, row_percent)
|
||||||
|
|
||||||
if freq == "Monthly":
|
def should_regenerate_budget_distribution(self):
|
||||||
row.start_date = get_first_day(current)
|
"""Check whether budget distribution should be recalculated."""
|
||||||
row.end_date = get_last_day(current)
|
old_doc = self.get_doc_before_save() if not self.is_new() else None
|
||||||
current = add_months(current, 1)
|
|
||||||
elif freq == "Quarterly":
|
|
||||||
month = ((current.month - 1) // 3) * 3 + 1
|
|
||||||
quarter_start = date(current.year, month, 1)
|
|
||||||
quarter_end = get_last_day(add_months(quarter_start, 2))
|
|
||||||
row.start_date = quarter_start
|
|
||||||
row.end_date = min(quarter_end, end)
|
|
||||||
current = add_months(quarter_start, 3)
|
|
||||||
elif freq == "Half-Yearly":
|
|
||||||
half = 1 if current.month <= 6 else 2
|
|
||||||
half_start = date(current.year, 1, 1) if half == 1 else date(current.year, 7, 1)
|
|
||||||
half_end = date(current.year, 6, 30) if half == 1 else date(current.year, 12, 31)
|
|
||||||
row.start_date = half_start
|
|
||||||
row.end_date = min(half_end, end)
|
|
||||||
current = add_months(half_start, 6)
|
|
||||||
else: # Yearly
|
|
||||||
year_start = date(current.year, 1, 1)
|
|
||||||
year_end = date(current.year, 12, 31)
|
|
||||||
row.start_date = year_start
|
|
||||||
row.end_date = min(year_end, end)
|
|
||||||
current = date(current.year + 1, 1, 1)
|
|
||||||
|
|
||||||
if self.distribution_type == "Amount":
|
if not self.budget_distribution:
|
||||||
if len(self.budget_distribution) == total_periods:
|
return True
|
||||||
row.amount = flt(self.budget_amount - assigned)
|
|
||||||
|
|
||||||
else:
|
if old_doc:
|
||||||
row.amount = per_row
|
changed_fields = [
|
||||||
assigned += per_row
|
"from_fiscal_year",
|
||||||
row.percent = flt(row.amount * 100 / self.budget_amount)
|
"to_fiscal_year",
|
||||||
else:
|
"allocation_frequency",
|
||||||
if len(self.budget_distribution) == total_periods:
|
"distribute_equally",
|
||||||
row.percent = flt(100 - assigned)
|
]
|
||||||
else:
|
for field in changed_fields:
|
||||||
row.percent = per_row
|
if old_doc.get(field) != self.get(field):
|
||||||
assigned += per_row
|
return True
|
||||||
row.amount = flt(row.percent * self.budget_amount / 100)
|
|
||||||
|
return bool(self.distribute_equally)
|
||||||
|
|
||||||
|
def set_budget_date_range(self):
|
||||||
|
"""Set budget start and end dates based on selected fiscal years."""
|
||||||
|
from_fiscal_year = frappe.db.get_value(
|
||||||
|
"Fiscal Year", self.from_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||||
|
)
|
||||||
|
to_fiscal_year = frappe.db.get_value(
|
||||||
|
"Fiscal Year", self.to_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.budget_start_date = from_fiscal_year.year_start_date
|
||||||
|
self.budget_end_date = to_fiscal_year.year_end_date
|
||||||
|
|
||||||
|
def get_budget_periods(self):
|
||||||
|
"""Return list of (start_date, end_date) tuples based on frequency."""
|
||||||
|
frequency = self.allocation_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, 2)
|
||||||
|
row.percent = flt(row_percent, 3)
|
||||||
|
|
||||||
|
|
||||||
def validate_expense_against_budget(args, expense_amount=0):
|
def validate_expense_against_budget(args, expense_amount=0):
|
||||||
@@ -229,17 +266,31 @@ def validate_expense_against_budget(args, expense_amount=0):
|
|||||||
if not frappe.db.count("Budget", cache=True):
|
if not frappe.db.count("Budget", cache=True):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not args.fiscal_year:
|
posting_date = getdate(args.get("posting_date"))
|
||||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
posting_fiscal_year = get_fiscal_year(posting_date, company=args.get("company"))[0]
|
||||||
|
year_start_date, year_end_date = get_fiscal_year_date_range(posting_fiscal_year, posting_fiscal_year)
|
||||||
|
|
||||||
|
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
|
||||||
|
""",
|
||||||
|
(args.company, year_end_date, year_start_date),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not budget_exists:
|
||||||
|
return
|
||||||
|
|
||||||
if args.get("company"):
|
if args.get("company"):
|
||||||
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
||||||
"Company", args.get("company"), "exception_budget_approver_role"
|
"Company", args.get("company"), "exception_budget_approver_role"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not frappe.db.get_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not args.account:
|
if not args.account:
|
||||||
args.account = args.get("expense_account")
|
args.account = args.get("expense_account")
|
||||||
|
|
||||||
@@ -284,22 +335,36 @@ def validate_expense_against_budget(args, expense_amount=0):
|
|||||||
|
|
||||||
budget_records = frappe.db.sql(
|
budget_records = frappe.db.sql(
|
||||||
f"""
|
f"""
|
||||||
select
|
SELECT
|
||||||
b.name, b.{budget_against} as budget_against, b.budget_amount,
|
b.name,
|
||||||
ifnull(b.applicable_on_material_request, 0) as for_material_request,
|
b.{budget_against} AS budget_against,
|
||||||
ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
|
b.budget_amount,
|
||||||
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
|
b.from_fiscal_year,
|
||||||
b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded,
|
b.to_fiscal_year,
|
||||||
b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr,
|
IFNULL(b.applicable_on_material_request, 0) AS for_material_request,
|
||||||
b.action_if_annual_budget_exceeded_on_po, b.action_if_accumulated_monthly_budget_exceeded_on_po
|
IFNULL(b.applicable_on_purchase_order, 0) AS for_purchase_order,
|
||||||
from
|
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
|
`tabBudget` b
|
||||||
where
|
WHERE
|
||||||
b.fiscal_year=%s
|
b.company = %s
|
||||||
and b.account=%s and b.docstatus=1
|
AND b.docstatus = 1
|
||||||
|
AND (
|
||||||
|
%s BETWEEN
|
||||||
|
(SELECT year_start_date FROM `tabFiscal Year` WHERE name = b.from_fiscal_year)
|
||||||
|
AND
|
||||||
|
(SELECT year_end_date FROM `tabFiscal Year` WHERE name = b.to_fiscal_year)
|
||||||
|
)
|
||||||
|
AND b.account = %s
|
||||||
{condition}
|
{condition}
|
||||||
""",
|
""",
|
||||||
(args.fiscal_year, args.account),
|
(args.company, args.posting_date, args.account),
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
) # nosec
|
) # nosec
|
||||||
|
|
||||||
@@ -313,6 +378,7 @@ def validate_budget_records(args, budget_records, expense_amount):
|
|||||||
yearly_action, monthly_action = get_actions(args, budget)
|
yearly_action, monthly_action = get_actions(args, budget)
|
||||||
args["for_material_request"] = budget.for_material_request
|
args["for_material_request"] = budget.for_material_request
|
||||||
args["for_purchase_order"] = budget.for_purchase_order
|
args["for_purchase_order"] = budget.for_purchase_order
|
||||||
|
args["from_fiscal_year"], args["to_fiscal_year"] = budget.from_fiscal_year, budget.to_fiscal_year
|
||||||
|
|
||||||
if yearly_action in ("Stop", "Warn"):
|
if yearly_action in ("Stop", "Warn"):
|
||||||
compare_expense_with_budget(
|
compare_expense_with_budget(
|
||||||
@@ -384,7 +450,7 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
|||||||
|
|
||||||
|
|
||||||
def get_expense_breakup(args, currency, budget_against):
|
def get_expense_breakup(args, currency, budget_against):
|
||||||
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
|
msg = "<hr> {} - <ul>".format(_("Total Expenses booked through"))
|
||||||
|
|
||||||
common_filters = frappe._dict(
|
common_filters = frappe._dict(
|
||||||
{
|
{
|
||||||
@@ -394,23 +460,39 @@ def get_expense_breakup(args, currency, budget_against):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from_date = frappe.get_cached_value("Fiscal Year", args.from_fiscal_year, "year_start_date")
|
||||||
|
to_date = frappe.get_cached_value("Fiscal Year", args.to_fiscal_year, "year_end_date")
|
||||||
|
gl_filters = common_filters.copy()
|
||||||
|
gl_filters.update(
|
||||||
|
{
|
||||||
|
"from_date": from_date,
|
||||||
|
"to_date": to_date,
|
||||||
|
"is_cancelled": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
msg += (
|
msg += (
|
||||||
"<li>"
|
"<li>"
|
||||||
+ frappe.utils.get_link_to_report(
|
+ frappe.utils.get_link_to_report(
|
||||||
"General Ledger",
|
"General Ledger",
|
||||||
label=_("Actual Expenses"),
|
label=_("Actual Expenses"),
|
||||||
filters=common_filters.copy().update(
|
filters=gl_filters,
|
||||||
{
|
|
||||||
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
|
|
||||||
"to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"),
|
|
||||||
"is_cancelled": 0,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
+ " - "
|
+ " - "
|
||||||
+ frappe.bold(fmt_money(args.actual_expense, currency=currency))
|
+ frappe.bold(fmt_money(args.actual_expense, currency=currency))
|
||||||
+ "</li>"
|
+ "</li>"
|
||||||
)
|
)
|
||||||
|
mr_filters = common_filters.copy()
|
||||||
|
mr_filters.update(
|
||||||
|
{
|
||||||
|
"status": [["!=", "Stopped"]],
|
||||||
|
"docstatus": 1,
|
||||||
|
"material_request_type": "Purchase",
|
||||||
|
"schedule_date": [["between", from_date, to_date]],
|
||||||
|
"item_code": args.item_code,
|
||||||
|
"per_ordered": [["<", 100]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
msg += (
|
msg += (
|
||||||
"<li>"
|
"<li>"
|
||||||
@@ -419,22 +501,24 @@ def get_expense_breakup(args, currency, budget_against):
|
|||||||
label=_("Material Requests"),
|
label=_("Material Requests"),
|
||||||
report_type="Report Builder",
|
report_type="Report Builder",
|
||||||
doctype="Material Request",
|
doctype="Material Request",
|
||||||
filters=common_filters.copy().update(
|
filters=mr_filters,
|
||||||
{
|
|
||||||
"status": [["!=", "Stopped"]],
|
|
||||||
"docstatus": 1,
|
|
||||||
"material_request_type": "Purchase",
|
|
||||||
"schedule_date": [["fiscal year", "2023-2024"]],
|
|
||||||
"item_code": args.item_code,
|
|
||||||
"per_ordered": [["<", 100]],
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
+ " - "
|
+ " - "
|
||||||
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
|
+ frappe.bold(fmt_money(args.requested_amount, currency=currency))
|
||||||
+ "</li>"
|
+ "</li>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
po_filters = common_filters.copy()
|
||||||
|
po_filters.update(
|
||||||
|
{
|
||||||
|
"status": [["!=", "Closed"]],
|
||||||
|
"docstatus": 1,
|
||||||
|
"transaction_date": [["between", from_date, to_date]],
|
||||||
|
"item_code": args.item_code,
|
||||||
|
"per_billed": [["<", 100]],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
msg += (
|
msg += (
|
||||||
"<li>"
|
"<li>"
|
||||||
+ frappe.utils.get_link_to_report(
|
+ frappe.utils.get_link_to_report(
|
||||||
@@ -442,15 +526,7 @@ def get_expense_breakup(args, currency, budget_against):
|
|||||||
label=_("Unbilled Orders"),
|
label=_("Unbilled Orders"),
|
||||||
report_type="Report Builder",
|
report_type="Report Builder",
|
||||||
doctype="Purchase Order",
|
doctype="Purchase Order",
|
||||||
filters=common_filters.copy().update(
|
filters=po_filters,
|
||||||
{
|
|
||||||
"status": [["!=", "Closed"]],
|
|
||||||
"docstatus": 1,
|
|
||||||
"transaction_date": [["fiscal year", "2023-2024"]],
|
|
||||||
"item_code": args.item_code,
|
|
||||||
"per_billed": [["<", 100]],
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
+ " - "
|
+ " - "
|
||||||
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
|
+ frappe.bold(fmt_money(args.ordered_amount, currency=currency))
|
||||||
@@ -508,20 +584,18 @@ def get_ordered_amount(args):
|
|||||||
|
|
||||||
|
|
||||||
def get_other_condition(args, for_doc):
|
def get_other_condition(args, for_doc):
|
||||||
condition = "expense_account = '%s'" % (args.expense_account)
|
condition = f"expense_account = '{args.expense_account}'"
|
||||||
budget_against_field = args.get("budget_against_field")
|
budget_against_field = args.get("budget_against_field")
|
||||||
|
|
||||||
if budget_against_field and args.get(budget_against_field):
|
if budget_against_field and args.get(budget_against_field):
|
||||||
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
||||||
|
|
||||||
if args.get("fiscal_year"):
|
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
|
||||||
start_date, end_date = frappe.get_cached_value(
|
|
||||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
|
||||||
)
|
|
||||||
|
|
||||||
condition += f""" and parent.{date_field}
|
start_date = frappe.get_cached_value("Fiscal Year", args.from_fiscal_year, "year_start_date")
|
||||||
between '{start_date}' and '{end_date}' """
|
end_date = frappe.get_cached_value("Fiscal Year", args.to_fiscal_year, "year_end_date")
|
||||||
|
|
||||||
|
condition += f" and parent.{date_field} between '{start_date}' and '{end_date}'"
|
||||||
|
|
||||||
return condition
|
return condition
|
||||||
|
|
||||||
@@ -533,36 +607,48 @@ def get_actual_expense(args):
|
|||||||
budget_against_field = args.get("budget_against_field")
|
budget_against_field = args.get("budget_against_field")
|
||||||
condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
|
condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
|
||||||
|
|
||||||
|
from_start, _ = frappe.get_cached_value(
|
||||||
|
"Fiscal Year", args.from_fiscal_year, ["year_start_date", "year_end_date"]
|
||||||
|
)
|
||||||
|
_, to_end = frappe.get_cached_value(
|
||||||
|
"Fiscal Year", args.to_fiscal_year, ["year_start_date", "year_end_date"]
|
||||||
|
)
|
||||||
|
|
||||||
|
date_condition = f"and gle.posting_date between '{from_start}' and '{to_end}'"
|
||||||
|
|
||||||
if args.is_tree:
|
if args.is_tree:
|
||||||
lft_rgt = frappe.db.get_value(
|
lft_rgt = frappe.db.get_value(
|
||||||
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
|
||||||
)
|
)
|
||||||
|
|
||||||
args.update(lft_rgt)
|
args.update(lft_rgt)
|
||||||
|
|
||||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
condition2 = f"""
|
||||||
where lft>=%(lft)s and rgt<=%(rgt)s
|
and exists(
|
||||||
and name=gle.{budget_against_field})"""
|
select name from `tab{args.budget_against_doctype}`
|
||||||
|
where lft >= %(lft)s and rgt <= %(rgt)s
|
||||||
|
and name = gle.{budget_against_field}
|
||||||
|
)
|
||||||
|
"""
|
||||||
else:
|
else:
|
||||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
condition2 = f"""
|
||||||
where name=gle.{budget_against_field} and
|
and gle.{budget_against_field} = %({budget_against_field})s
|
||||||
gle.{budget_against_field} = %({budget_against_field})s)"""
|
"""
|
||||||
|
|
||||||
amount = flt(
|
amount = flt(
|
||||||
frappe.db.sql(
|
frappe.db.sql(
|
||||||
f"""
|
f"""
|
||||||
select sum(gle.debit) - sum(gle.credit)
|
select sum(gle.debit) - sum(gle.credit)
|
||||||
from `tabGL Entry` gle
|
from `tabGL Entry` gle
|
||||||
where
|
where
|
||||||
is_cancelled = 0
|
is_cancelled = 0
|
||||||
and gle.account=%(account)s
|
and gle.account = %(account)s
|
||||||
{condition1}
|
{condition1}
|
||||||
and gle.fiscal_year=%(fiscal_year)s
|
{date_condition}
|
||||||
and gle.company=%(company)s
|
and gle.company = %(company)s
|
||||||
and gle.docstatus=1
|
and gle.docstatus = 1
|
||||||
{condition2}
|
{condition2}
|
||||||
""",
|
""",
|
||||||
(args),
|
args,
|
||||||
)[0][0]
|
)[0][0]
|
||||||
) # nosec
|
) # nosec
|
||||||
|
|
||||||
@@ -632,6 +718,16 @@ def get_expense_cost_center(doctype, args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_fiscal_year_date_range(from_fiscal_year, to_fiscal_year):
|
||||||
|
from_year = frappe.get_cached_value(
|
||||||
|
"Fiscal Year", from_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||||
|
)
|
||||||
|
to_year = frappe.get_cached_value(
|
||||||
|
"Fiscal Year", to_fiscal_year, ["year_start_date", "year_end_date"], as_dict=True
|
||||||
|
)
|
||||||
|
return from_year.year_start_date, to_year.year_end_date
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def revise_budget(budget_name):
|
def revise_budget(budget_name):
|
||||||
old_budget = frappe.get_doc("Budget", budget_name)
|
old_budget = frappe.get_doc("Budget", budget_name)
|
||||||
@@ -643,7 +739,6 @@ def revise_budget(budget_name):
|
|||||||
new_budget = frappe.copy_doc(old_budget)
|
new_budget = frappe.copy_doc(old_budget)
|
||||||
new_budget.docstatus = 0
|
new_budget.docstatus = 0
|
||||||
new_budget.revision_of = old_budget.name
|
new_budget.revision_of = old_budget.name
|
||||||
new_budget.posting_date = frappe.utils.nowdate()
|
|
||||||
new_budget.insert()
|
new_budget.insert()
|
||||||
|
|
||||||
return new_budget.name
|
return new_budget.name
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_days, getdate, now_datetime, nowdate
|
from frappe.client import submit
|
||||||
|
from frappe.utils import add_days, flt, get_first_day, get_last_day, getdate, now_datetime, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.budget.budget import (
|
from erpnext.accounts.doctype.budget.budget import (
|
||||||
BudgetError,
|
BudgetError,
|
||||||
@@ -34,7 +35,7 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_monthly_budget_crossed_ignore(self):
|
def test_monthly_budget_crossed_ignore(self):
|
||||||
set_total_expense_zero(nowdate(), "cost_center")
|
set_total_expense_zero(nowdate(), "cost_center")
|
||||||
|
|
||||||
budget = make_budget(budget_against="Cost Center")
|
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||||
|
|
||||||
jv = make_journal_entry(
|
jv = make_journal_entry(
|
||||||
"_Test Account Cost for Goods Sold - _TC",
|
"_Test Account Cost for Goods Sold - _TC",
|
||||||
@@ -55,7 +56,7 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_monthly_budget_crossed_stop1(self):
|
def test_monthly_budget_crossed_stop1(self):
|
||||||
set_total_expense_zero(nowdate(), "cost_center")
|
set_total_expense_zero(nowdate(), "cost_center")
|
||||||
|
|
||||||
budget = make_budget(budget_against="Cost Center")
|
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||||
|
|
||||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_exception_approver_role(self):
|
def test_exception_approver_role(self):
|
||||||
set_total_expense_zero(nowdate(), "cost_center")
|
set_total_expense_zero(nowdate(), "cost_center")
|
||||||
|
|
||||||
budget = make_budget(budget_against="Cost Center")
|
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||||
|
|
||||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||||
|
|
||||||
@@ -111,11 +112,11 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
applicable_on_purchase_order=1,
|
applicable_on_purchase_order=1,
|
||||||
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
|
action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
|
||||||
budget_against="Cost Center",
|
budget_against="Cost Center",
|
||||||
|
do_not_save=False,
|
||||||
|
subimit_budget=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
|
||||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
|
||||||
|
|
||||||
accumulated_limit = get_accumulated_monthly_budget(
|
accumulated_limit = get_accumulated_monthly_budget(
|
||||||
budget.name,
|
budget.name,
|
||||||
@@ -156,11 +157,11 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
applicable_on_purchase_order=1,
|
applicable_on_purchase_order=1,
|
||||||
action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
|
action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
|
||||||
budget_against="Cost Center",
|
budget_against="Cost Center",
|
||||||
|
do_not_save=False,
|
||||||
|
submit_budget=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
|
||||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
|
||||||
|
|
||||||
accumulated_limit = get_accumulated_monthly_budget(
|
accumulated_limit = get_accumulated_monthly_budget(
|
||||||
budget.name,
|
budget.name,
|
||||||
@@ -181,7 +182,7 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_monthly_budget_crossed_stop2(self):
|
def test_monthly_budget_crossed_stop2(self):
|
||||||
set_total_expense_zero(nowdate(), "project")
|
set_total_expense_zero(nowdate(), "project")
|
||||||
|
|
||||||
budget = make_budget(budget_against="Project")
|
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||||
|
|
||||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_yearly_budget_crossed_stop1(self):
|
def test_yearly_budget_crossed_stop1(self):
|
||||||
set_total_expense_zero(nowdate(), "cost_center")
|
set_total_expense_zero(nowdate(), "cost_center")
|
||||||
|
|
||||||
budget = make_budget(budget_against="Cost Center")
|
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||||
|
|
||||||
jv = make_journal_entry(
|
jv = make_journal_entry(
|
||||||
"_Test Account Cost for Goods Sold - _TC",
|
"_Test Account Cost for Goods Sold - _TC",
|
||||||
@@ -224,7 +225,7 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_yearly_budget_crossed_stop2(self):
|
def test_yearly_budget_crossed_stop2(self):
|
||||||
set_total_expense_zero(nowdate(), "project")
|
set_total_expense_zero(nowdate(), "project")
|
||||||
|
|
||||||
budget = make_budget(budget_against="Project")
|
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||||
|
|
||||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||||
|
|
||||||
@@ -244,7 +245,7 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_monthly_budget_on_cancellation1(self):
|
def test_monthly_budget_on_cancellation1(self):
|
||||||
set_total_expense_zero(nowdate(), "cost_center")
|
set_total_expense_zero(nowdate(), "cost_center")
|
||||||
|
|
||||||
budget = make_budget(budget_against="Cost Center")
|
budget = make_budget(budget_against="Cost Center", do_not_save=False, submit_budget=True)
|
||||||
month = now_datetime().month
|
month = now_datetime().month
|
||||||
if month > 9:
|
if month > 9:
|
||||||
month = 9
|
month = 9
|
||||||
@@ -273,7 +274,7 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_monthly_budget_on_cancellation2(self):
|
def test_monthly_budget_on_cancellation2(self):
|
||||||
set_total_expense_zero(nowdate(), "project")
|
set_total_expense_zero(nowdate(), "project")
|
||||||
|
|
||||||
budget = make_budget(budget_against="Project")
|
budget = make_budget(budget_against="Project", do_not_save=False, submit_budget=True)
|
||||||
month = now_datetime().month
|
month = now_datetime().month
|
||||||
if month > 9:
|
if month > 9:
|
||||||
month = 9
|
month = 9
|
||||||
@@ -305,7 +306,12 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
set_total_expense_zero(nowdate(), "cost_center")
|
set_total_expense_zero(nowdate(), "cost_center")
|
||||||
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
|
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
|
||||||
|
|
||||||
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
|
budget = make_budget(
|
||||||
|
budget_against="Cost Center",
|
||||||
|
cost_center="_Test Company - _TC",
|
||||||
|
do_not_save=False,
|
||||||
|
submit_budget=True,
|
||||||
|
)
|
||||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||||
|
|
||||||
accumulated_limit = get_accumulated_monthly_budget(
|
accumulated_limit = get_accumulated_monthly_budget(
|
||||||
@@ -339,7 +345,9 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
}
|
}
|
||||||
).insert(ignore_permissions=True)
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
|
budget = make_budget(
|
||||||
|
budget_against="Cost Center", cost_center=cost_center, do_not_save=False, submit_budget=True
|
||||||
|
)
|
||||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||||
|
|
||||||
accumulated_limit = get_accumulated_monthly_budget(
|
accumulated_limit = get_accumulated_monthly_budget(
|
||||||
@@ -381,7 +389,12 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
{"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40},
|
{"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40},
|
||||||
)
|
)
|
||||||
|
|
||||||
make_budget(budget_against="Cost Center", cost_center="Main Budget Cost Center 1 - _TC")
|
make_budget(
|
||||||
|
budget_against="Cost Center",
|
||||||
|
cost_center="Main Budget Cost Center 1 - _TC",
|
||||||
|
do_not_save=False,
|
||||||
|
submit_budget=True,
|
||||||
|
)
|
||||||
|
|
||||||
jv = make_journal_entry(
|
jv = make_journal_entry(
|
||||||
"_Test Account Cost for Goods Sold - _TC",
|
"_Test Account Cost for Goods Sold - _TC",
|
||||||
@@ -396,7 +409,12 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_action_for_cumulative_limit(self):
|
def test_action_for_cumulative_limit(self):
|
||||||
set_total_expense_zero(nowdate(), "cost_center")
|
set_total_expense_zero(nowdate(), "cost_center")
|
||||||
|
|
||||||
budget = make_budget(budget_against="Cost Center", applicable_on_cumulative_expense=True)
|
budget = make_budget(
|
||||||
|
budget_against="Cost Center",
|
||||||
|
applicable_on_cumulative_expense=True,
|
||||||
|
do_not_save=False,
|
||||||
|
submit_budget=True,
|
||||||
|
)
|
||||||
|
|
||||||
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
|
accumulated_limit = get_accumulated_monthly_budget(budget.name, nowdate())
|
||||||
|
|
||||||
@@ -417,8 +435,6 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
)
|
)
|
||||||
po.set_missing_values()
|
po.set_missing_values()
|
||||||
|
|
||||||
print(">>>>>>>>>>>>>>>>>>>>>>>>")
|
|
||||||
|
|
||||||
self.assertRaises(BudgetError, po.submit)
|
self.assertRaises(BudgetError, po.submit)
|
||||||
|
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
@@ -434,7 +450,6 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_distribution_date_validation(self):
|
def test_distribution_date_validation(self):
|
||||||
budget = frappe.new_doc("Budget")
|
budget = frappe.new_doc("Budget")
|
||||||
budget.company = self.company
|
budget.company = self.company
|
||||||
budget.fiscal_year = self.fiscal_year
|
|
||||||
budget.budget_against = "Cost Center"
|
budget.budget_against = "Cost Center"
|
||||||
budget.cost_center = self.cost_center
|
budget.cost_center = self.cost_center
|
||||||
budget.account = self.account
|
budget.account = self.account
|
||||||
@@ -456,9 +471,14 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
budget.save()
|
budget.save()
|
||||||
|
|
||||||
def test_total_distribution_equals_budget(self):
|
def test_total_distribution_equals_budget(self):
|
||||||
|
budget = make_budget(
|
||||||
|
budget_against="Cost Center",
|
||||||
|
applicable_on_cumulative_expense=True,
|
||||||
|
do_not_save=False,
|
||||||
|
submit_budget=True,
|
||||||
|
)
|
||||||
budget = frappe.new_doc("Budget")
|
budget = frappe.new_doc("Budget")
|
||||||
budget.company = self.company
|
budget.company = self.company
|
||||||
budget.fiscal_year = self.fiscal_year
|
|
||||||
budget.budget_against = "Cost Center"
|
budget.budget_against = "Cost Center"
|
||||||
budget.cost_center = self.cost_center
|
budget.cost_center = self.cost_center
|
||||||
budget.account = ("_Test Account Cost for Goods Sold - _TC",)
|
budget.account = ("_Test Account Cost for Goods Sold - _TC",)
|
||||||
@@ -488,14 +508,18 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
budget.save()
|
budget.save()
|
||||||
|
|
||||||
def test_evenly_distribute_budget(self):
|
def test_evenly_distribute_budget(self):
|
||||||
budget = make_budget(budget_against="Cost Center", budget_amount=120000)
|
budget = make_budget(
|
||||||
|
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||||
|
)
|
||||||
|
|
||||||
total = sum([d.amount for d in budget.budget_distribution])
|
total = sum([d.amount for d in budget.budget_distribution])
|
||||||
self.assertEqual(total, 120000)
|
self.assertEqual(flt(total), 120000)
|
||||||
self.assertTrue(all(d.amount == 10000 for d in budget.budget_distribution))
|
self.assertTrue(all(d.amount == 10000 for d in budget.budget_distribution))
|
||||||
|
|
||||||
def test_create_revised_budget(self):
|
def test_create_revised_budget(self):
|
||||||
budget = make_budget(budget_against="Cost Center", budget_amount=120000)
|
budget = make_budget(
|
||||||
|
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||||
|
)
|
||||||
|
|
||||||
revised_name = revise_budget(budget.name)
|
revised_name = revise_budget(budget.name)
|
||||||
|
|
||||||
@@ -508,7 +532,9 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
self.assertEqual(old_budget.docstatus, 2)
|
self.assertEqual(old_budget.docstatus, 2)
|
||||||
|
|
||||||
def test_revision_preserves_distribution(self):
|
def test_revision_preserves_distribution(self):
|
||||||
budget = make_budget(budget_against="Cost Center", budget_amount=120000)
|
budget = make_budget(
|
||||||
|
budget_against="Cost Center", budget_amount=120000, do_not_save=False, submit_budget=True
|
||||||
|
)
|
||||||
|
|
||||||
revised_name = revise_budget(budget.name)
|
revised_name = revise_budget(budget.name)
|
||||||
revised_budget = frappe.get_doc("Budget", revised_name)
|
revised_budget = frappe.get_doc("Budget", revised_name)
|
||||||
@@ -518,6 +544,32 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
total = sum(row.amount for row in revised_budget.budget_distribution)
|
total = sum(row.amount for row in revised_budget.budget_distribution)
|
||||||
self.assertEqual(total, revised_budget.budget_amount)
|
self.assertEqual(total, revised_budget.budget_amount)
|
||||||
|
|
||||||
|
def test_manual_budget_amount_total(self):
|
||||||
|
budget = make_budget(
|
||||||
|
budget_against="Cost Center",
|
||||||
|
distribute_equally=0,
|
||||||
|
budget_amount=30000,
|
||||||
|
budget_start_date="2025-04-01",
|
||||||
|
budget_end_date="2025-06-30",
|
||||||
|
do_not_save=True,
|
||||||
|
submit_budget=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
budget.budget_distribution = []
|
||||||
|
|
||||||
|
for row in [
|
||||||
|
{"start_date": "2025-04-01", "end_date": "2025-04-30", "amount": 10000},
|
||||||
|
{"start_date": "2025-05-01", "end_date": "2025-05-31", "amount": 15000},
|
||||||
|
{"start_date": "2025-06-01", "end_date": "2025-06-30", "amount": 5000},
|
||||||
|
]:
|
||||||
|
budget.append("budget_distribution", row)
|
||||||
|
|
||||||
|
budget.save()
|
||||||
|
|
||||||
|
total_child_amount = sum(row.amount for row in budget.budget_distribution)
|
||||||
|
|
||||||
|
self.assertEqual(total_child_amount, budget.budget_amount)
|
||||||
|
|
||||||
|
|
||||||
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
|
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
|
||||||
if budget_against_field == "project":
|
if budget_against_field == "project":
|
||||||
@@ -533,7 +585,8 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
|
|||||||
"cost_center": "_Test Cost Center - _TC",
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
"monthly_end_date": posting_date,
|
"monthly_end_date": posting_date,
|
||||||
"company": "_Test Company",
|
"company": "_Test Company",
|
||||||
"fiscal_year": fiscal_year,
|
"from_fiscal_year": fiscal_year,
|
||||||
|
"to_fiscal_year": fiscal_year,
|
||||||
"budget_against_field": budget_against_field,
|
"budget_against_field": budget_against_field,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -605,7 +658,8 @@ def make_budget(**args):
|
|||||||
else:
|
else:
|
||||||
budget.cost_center = cost_center or "_Test Cost Center - _TC"
|
budget.cost_center = cost_center or "_Test Cost Center - _TC"
|
||||||
|
|
||||||
budget.fiscal_year = fiscal_year
|
budget.from_fiscal_year = fiscal_year
|
||||||
|
budget.to_fiscal_year = fiscal_year
|
||||||
budget.company = "_Test Company"
|
budget.company = "_Test Company"
|
||||||
budget.account = "_Test Account Cost for Goods Sold - _TC"
|
budget.account = "_Test Account Cost for Goods Sold - _TC"
|
||||||
budget.budget_amount = args.budget_amount or 200000
|
budget.budget_amount = args.budget_amount or 200000
|
||||||
@@ -614,10 +668,9 @@ def make_budget(**args):
|
|||||||
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
|
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
|
||||||
budget.budget_against = budget_against
|
budget.budget_against = budget_against
|
||||||
|
|
||||||
budget.budget_start_date = "2025-04-01"
|
|
||||||
budget.budget_end_date = "2026-03-31"
|
|
||||||
budget.allocation_frequency = "Monthly"
|
budget.allocation_frequency = "Monthly"
|
||||||
budget.distribution_type = "Amount"
|
budget.distribution_type = "Amount"
|
||||||
|
budget.distribute_equally = args.get("distribute_equally", 1)
|
||||||
|
|
||||||
if args.applicable_on_material_request:
|
if args.applicable_on_material_request:
|
||||||
budget.applicable_on_material_request = 1
|
budget.applicable_on_material_request = 1
|
||||||
@@ -642,7 +695,13 @@ def make_budget(**args):
|
|||||||
args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn"
|
args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn"
|
||||||
)
|
)
|
||||||
|
|
||||||
budget.insert()
|
if not args.do_not_save:
|
||||||
budget.submit()
|
try:
|
||||||
|
budget.insert(ignore_if_duplicate=True)
|
||||||
|
except frappe.DuplicateEntryError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if args.submit_budget:
|
||||||
|
budget.submit()
|
||||||
|
|
||||||
return budget
|
return budget
|
||||||
|
|||||||
@@ -17,13 +17,15 @@
|
|||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Start Date",
|
"label": "Start Date",
|
||||||
|
"read_only": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "end_date",
|
"fieldname": "end_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "End Date"
|
"label": "End Date",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
@@ -44,7 +46,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-15 16:53:23.462653",
|
"modified": "2025-10-30 12:35:31.310931",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Budget Distribution",
|
"name": "Budget Distribution",
|
||||||
|
|||||||
Reference in New Issue
Block a user