mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-25 16:04:46 +00:00
refactor: better manual budget distribution ux
This commit is contained in:
@@ -49,6 +49,15 @@ frappe.ui.form.on("Budget", {
|
|||||||
frm.trigger("toggle_reqd_fields");
|
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) {
|
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);
|
||||||
@@ -85,3 +94,20 @@ frappe.ui.form.on("Budget", {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -231,6 +231,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "account",
|
"fieldname": "account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Account",
|
"label": "Account",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"read_only_depends_on": "eval: doc.revision_of",
|
"read_only_depends_on": "eval: doc.revision_of",
|
||||||
@@ -258,7 +259,6 @@
|
|||||||
"fieldname": "budget_amount",
|
"fieldname": "budget_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Budget Amount",
|
"label": "Budget Amount",
|
||||||
"read_only_depends_on": "eval: doc.revision_of",
|
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -304,7 +304,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-30 19:07:51.022844",
|
"modified": "2025-10-31 01:13:15.114440",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Budget",
|
"name": "Budget",
|
||||||
|
|||||||
@@ -160,6 +160,9 @@ class Budget(Document):
|
|||||||
def before_save(self):
|
def before_save(self):
|
||||||
self.allocate_budget()
|
self.allocate_budget()
|
||||||
|
|
||||||
|
def on_update(self):
|
||||||
|
self.validate_distribution_totals()
|
||||||
|
|
||||||
def allocate_budget(self):
|
def allocate_budget(self):
|
||||||
if self.revision_of:
|
if self.revision_of:
|
||||||
return
|
return
|
||||||
@@ -183,14 +186,14 @@ class Budget(Document):
|
|||||||
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."""
|
||||||
old_doc = self.get_doc_before_save() if not self.is_new() else None
|
old_doc = self.get_doc_before_save() if not self.is_new() else None
|
||||||
|
if not old_doc or not self.budget_distribution:
|
||||||
if not self.budget_distribution:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if old_doc:
|
if old_doc:
|
||||||
changed_fields = [
|
changed_fields = [
|
||||||
"from_fiscal_year",
|
"from_fiscal_year",
|
||||||
"to_fiscal_year",
|
"to_fiscal_year",
|
||||||
|
"budget_amount",
|
||||||
"allocation_frequency",
|
"allocation_frequency",
|
||||||
"distribute_equally",
|
"distribute_equally",
|
||||||
]
|
]
|
||||||
@@ -255,9 +258,28 @@ class Budget(Document):
|
|||||||
row.amount = 0
|
row.amount = 0
|
||||||
row.percent = 0
|
row.percent = 0
|
||||||
else:
|
else:
|
||||||
row.amount = flt(self.budget_amount * row_percent / 100, 2)
|
row.amount = flt(self.budget_amount * row_percent / 100, 3)
|
||||||
row.percent = flt(row_percent, 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 equal Budget Amount {1}").format(
|
||||||
|
flt(total_amount, 2), self.budget_amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if round(total_percent, 2) != 100:
|
||||||
|
frappe.throw(
|
||||||
|
_("Total distribution percent must equal 100 (currently {0})").format(round(total_percent, 2))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_expense_against_budget(args, expense_amount=0):
|
def validate_expense_against_budget(args, expense_amount=0):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -551,16 +551,16 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
budget_amount=30000,
|
budget_amount=30000,
|
||||||
budget_start_date="2025-04-01",
|
budget_start_date="2025-04-01",
|
||||||
budget_end_date="2025-06-30",
|
budget_end_date="2025-06-30",
|
||||||
do_not_save=True,
|
do_not_save=False,
|
||||||
submit_budget=False,
|
submit_budget=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
budget.budget_distribution = []
|
budget.budget_distribution = []
|
||||||
|
|
||||||
for row in [
|
for row in [
|
||||||
{"start_date": "2025-04-01", "end_date": "2025-04-30", "amount": 10000},
|
{"start_date": "2025-04-01", "end_date": "2025-04-30", "amount": 10000, "percent": 33.33},
|
||||||
{"start_date": "2025-05-01", "end_date": "2025-05-31", "amount": 15000},
|
{"start_date": "2025-05-01", "end_date": "2025-05-31", "amount": 15000, "percent": 50.00},
|
||||||
{"start_date": "2025-06-01", "end_date": "2025-06-30", "amount": 5000},
|
{"start_date": "2025-06-01", "end_date": "2025-06-30", "amount": 5000, "percent": 16.67},
|
||||||
]:
|
]:
|
||||||
budget.append("budget_distribution", row)
|
budget.append("budget_distribution", row)
|
||||||
|
|
||||||
@@ -608,10 +608,10 @@ class TestBudget(ERPNextTestSuite):
|
|||||||
def test_duplicate_budget_validation(self):
|
def test_duplicate_budget_validation(self):
|
||||||
make_budget(
|
make_budget(
|
||||||
budget_against="Cost Center",
|
budget_against="Cost Center",
|
||||||
distribute_equally=0,
|
distribute_equally=1,
|
||||||
budget_amount=15000,
|
budget_amount=15000,
|
||||||
do_not_save=False,
|
do_not_save=False,
|
||||||
submit_budget=False,
|
submit_budget=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
budget = frappe.new_doc("Budget")
|
budget = frappe.new_doc("Budget")
|
||||||
|
|||||||
Reference in New Issue
Block a user