diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index 1186b60ab11..4b2aefffb57 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -49,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); @@ -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"); + } + }, +}); diff --git a/erpnext/accounts/doctype/budget/budget.json b/erpnext/accounts/doctype/budget/budget.json index 849d4a5e927..e88daf00565 100644 --- a/erpnext/accounts/doctype/budget/budget.json +++ b/erpnext/accounts/doctype/budget/budget.json @@ -231,6 +231,7 @@ { "fieldname": "account", "fieldtype": "Link", + "in_list_view": 1, "label": "Account", "options": "Account", "read_only_depends_on": "eval: doc.revision_of", @@ -258,7 +259,6 @@ "fieldname": "budget_amount", "fieldtype": "Currency", "label": "Budget Amount", - "read_only_depends_on": "eval: doc.revision_of", "reqd": 1 }, { @@ -304,7 +304,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-30 19:07:51.022844", + "modified": "2025-10-31 01:13:15.114440", "modified_by": "Administrator", "module": "Accounts", "name": "Budget", diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 120d78b7430..b9a0909ef45 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -160,6 +160,9 @@ class Budget(Document): def before_save(self): self.allocate_budget() + def on_update(self): + self.validate_distribution_totals() + def allocate_budget(self): if self.revision_of: return @@ -183,14 +186,14 @@ class Budget(Document): 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 self.budget_distribution: + if not old_doc or not self.budget_distribution: return True if old_doc: changed_fields = [ "from_fiscal_year", "to_fiscal_year", + "budget_amount", "allocation_frequency", "distribute_equally", ] @@ -255,9 +258,28 @@ class Budget(Document): row.amount = 0 row.percent = 0 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) + 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): args = frappe._dict(args) diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 6f1e751993b..1768d69349a 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -551,16 +551,16 @@ class TestBudget(ERPNextTestSuite): budget_amount=30000, budget_start_date="2025-04-01", budget_end_date="2025-06-30", - do_not_save=True, + do_not_save=False, 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}, + {"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, "percent": 50.00}, + {"start_date": "2025-06-01", "end_date": "2025-06-30", "amount": 5000, "percent": 16.67}, ]: budget.append("budget_distribution", row) @@ -608,10 +608,10 @@ class TestBudget(ERPNextTestSuite): def test_duplicate_budget_validation(self): make_budget( budget_against="Cost Center", - distribute_equally=0, + distribute_equally=1, budget_amount=15000, do_not_save=False, - submit_budget=False, + submit_budget=True, ) budget = frappe.new_doc("Budget")