Merge pull request #50999 from khushi8112/budget-fixes

fix: better manual budget distribution
This commit is contained in:
Khushi Rawat
2025-12-10 11:57:13 +05:30
committed by GitHub
5 changed files with 134 additions and 36 deletions

View File

@@ -12,6 +12,15 @@ frappe.ui.form.on("Budget", {
}; };
}); });
frm.set_query("account", function () {
return {
filters: {
is_group: 0,
company: frm.doc.company,
},
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => { frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
if (value) { if (value) {
@@ -24,24 +33,16 @@ frappe.ui.form.on("Budget", {
frm.trigger("toggle_reqd_fields"); frm.trigger("toggle_reqd_fields");
if (!frm.doc.__islocal && frm.doc.docstatus == 1) { if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
let exception_role = await frappe.db.get_value( frm.add_custom_button(
"Company", __("Revise Budget"),
frm.doc.company, function () {
"exception_budget_approver_role" frm.events.revise_budget_action(frm);
},
__("Actions")
); );
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")
);
}
} }
toggle_distribution_fields(frm);
}, },
budget_against: function (frm) { budget_against: function (frm) {
@@ -54,10 +55,15 @@ frappe.ui.form.on("Budget", {
frm.doc.budget_distribution.forEach((row) => { frm.doc.budget_distribution.forEach((row) => {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2); row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
}); });
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution"); frm.refresh_field("budget_distribution");
} }
}, },
distribute_equally: function (frm) {
toggle_distribution_fields(frm);
},
set_null_value: function (frm) { set_null_value: function (frm) {
if (frm.doc.budget_against == "Cost Center") { if (frm.doc.budget_against == "Cost Center") {
frm.set_value("project", null); frm.set_value("project", null);
@@ -100,6 +106,8 @@ frappe.ui.form.on("Budget Distribution", {
let row = frappe.get_doc(cdt, cdn); let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) { if (frm.doc.budget_amount) {
row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2); row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2);
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution"); frm.refresh_field("budget_distribution");
} }
}, },
@@ -107,7 +115,29 @@ frappe.ui.form.on("Budget Distribution", {
let row = frappe.get_doc(cdt, cdn); let row = frappe.get_doc(cdt, cdn);
if (frm.doc.budget_amount) { if (frm.doc.budget_amount) {
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2); row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
set_total_budget_amount(frm);
frm.refresh_field("budget_distribution"); frm.refresh_field("budget_distribution");
} }
}, },
}); });
function set_total_budget_amount(frm) {
let total = 0;
(frm.doc.budget_distribution || []).forEach((row) => {
total += flt(row.amount);
});
frm.set_value("budget_distribution_total", total);
}
function toggle_distribution_fields(frm) {
const grid = frm.fields_dict.budget_distribution.grid;
["amount", "percent"].forEach((field) => {
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
});
grid.refresh();
}

View File

@@ -25,6 +25,10 @@
"distribute_equally", "distribute_equally",
"section_break_fpdt", "section_break_fpdt",
"budget_distribution", "budget_distribution",
"section_break_wkqb",
"column_break_paum",
"column_break_nwor",
"budget_distribution_total",
"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",
@@ -222,7 +226,8 @@
}, },
{ {
"fieldname": "section_break_fpdt", "fieldname": "section_break_fpdt",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"hide_border": 1
}, },
{ {
"fieldname": "budget_distribution", "fieldname": "budget_distribution",
@@ -303,13 +308,32 @@
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly", "options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
"read_only_depends_on": "eval: doc.revision_of", "read_only_depends_on": "eval: doc.revision_of",
"reqd": 1 "reqd": 1
},
{
"fieldname": "section_break_wkqb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_paum",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_nwor",
"fieldtype": "Column Break"
},
{
"fieldname": "budget_distribution_total",
"fieldtype": "Currency",
"label": "Budget Distribution Total",
"no_copy": 1,
"read_only": 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-11-19 17:00:00.648224", "modified": "2025-12-10 02:35:01.197613",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Budget", "name": "Budget",

View File

@@ -53,6 +53,7 @@ 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_distribution_total: DF.Currency
budget_end_date: DF.Date | None budget_end_date: DF.Date | None
budget_start_date: DF.Date | None budget_start_date: DF.Date | None
company: DF.Link company: DF.Link
@@ -230,28 +231,49 @@ class Budget(Document):
def before_save(self): def before_save(self):
self.allocate_budget() self.allocate_budget()
self.budget_distribution_total = sum(flt(row.amount) for row in self.budget_distribution)
def on_update(self): def on_update(self):
self.validate_distribution_totals() self.validate_distribution_totals()
def allocate_budget(self): def allocate_budget(self):
if self.revision_of: if self._should_skip_allocation():
return
if self._should_recalculate_manual_distribution():
self._recalculate_manual_distribution()
return return
if not self.should_regenerate_budget_distribution(): if not self.should_regenerate_budget_distribution():
return return
self.set("budget_distribution", []) self._regenerate_distribution()
periods = self.get_budget_periods() def _should_skip_allocation(self):
total_periods = len(periods) return self.revision_of and not self.distribute_equally
row_percent = 100 / total_periods if total_periods else 0
for start_date, end_date in periods: def _should_recalculate_manual_distribution(self):
row = self.append("budget_distribution", {}) return (
row.start_date = start_date not self.distribute_equally
row.end_date = end_date and bool(self.budget_distribution)
self.add_allocated_amount(row, row_percent) and self._is_only_budget_amount_changed()
)
def _is_only_budget_amount_changed(self):
old = self.get_doc_before_save()
if not old:
return False
return (
old.budget_amount != self.budget_amount
and old.distribution_frequency == self.distribution_frequency
and old.budget_start_date == self.budget_start_date
and old.budget_end_date == self.budget_end_date
)
def _recalculate_manual_distribution(self):
for row in self.budget_distribution:
row.amount = flt((row.percent / 100) * self.budget_amount, 3)
def should_regenerate_budget_distribution(self): def should_regenerate_budget_distribution(self):
"""Check whether budget distribution should be recalculated.""" """Check whether budget distribution should be recalculated."""
@@ -265,7 +287,6 @@ class Budget(Document):
"to_fiscal_year", "to_fiscal_year",
"budget_amount", "budget_amount",
"distribution_frequency", "distribution_frequency",
"distribute_equally",
] ]
for field in changed_fields: for field in changed_fields:
if old_doc.get(field) != self.get(field): if old_doc.get(field) != self.get(field):
@@ -273,6 +294,21 @@ class Budget(Document):
return bool(self.distribute_equally) return bool(self.distribute_equally)
def _regenerate_distribution(self):
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)
self.budget_distribution_total = self.budget_amount
def get_budget_periods(self): def get_budget_periods(self):
"""Return list of (start_date, end_date) tuples based on frequency.""" """Return list of (start_date, end_date) tuples based on frequency."""
frequency = self.distribution_frequency frequency = self.distribution_frequency
@@ -312,12 +348,8 @@ class Budget(Document):
}.get(frequency, 1) }.get(frequency, 1)
def add_allocated_amount(self, row, row_percent): def add_allocated_amount(self, row, row_percent):
if not self.distribute_equally: row.amount = flt(self.budget_amount * row_percent / 100, 3)
row.amount = 0 row.percent = flt(row_percent, 3)
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): def validate_distribution_totals(self):
if self.should_regenerate_budget_distribution(): if self.should_regenerate_budget_distribution():

View File

@@ -451,3 +451,4 @@ erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_to_table
erpnext.patches.v16_0.migrate_budget_records_to_new_structure erpnext.patches.v16_0.migrate_budget_records_to_new_structure
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter
erpnext.patches.v16_0.migrate_account_freezing_settings_to_company erpnext.patches.v16_0.migrate_account_freezing_settings_to_company
erpnext.patches.v16_0.populate_budget_distribution_total

View File

@@ -0,0 +1,11 @@
import frappe
from frappe.utils import flt
def execute():
budgets = frappe.get_all("Budget", filters={"docstatus": ["in", [0, 1]]}, fields=["name"])
for b in budgets:
doc = frappe.get_doc("Budget", b.name)
total = sum(flt(row.amount) for row in doc.budget_distribution)
doc.db_set("budget_distribution_total", total, update_modified=False)