mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-15 19:19:17 +00:00
Merge pull request #50999 from khushi8112/budget-fixes
fix: better manual budget distribution
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -450,4 +450,5 @@ erpnext.patches.v16_0.set_valuation_method_on_companies
|
|||||||
erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_to_table
|
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
|
||||||
11
erpnext/patches/v16_0/populate_budget_distribution_total.py
Normal file
11
erpnext/patches/v16_0/populate_budget_distribution_total.py
Normal 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)
|
||||||
Reference in New Issue
Block a user