diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js index 3ac7b8fe8f8..5128fdc9c92 100644 --- a/erpnext/accounts/doctype/budget/budget.js +++ b/erpnext/accounts/doctype/budget/budget.js @@ -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); frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => { if (value) { @@ -24,24 +33,16 @@ frappe.ui.form.on("Budget", { frm.trigger("toggle_reqd_fields"); if (!frm.doc.__islocal && frm.doc.docstatus == 1) { - let exception_role = await frappe.db.get_value( - "Company", - frm.doc.company, - "exception_budget_approver_role" + frm.add_custom_button( + __("Revise Budget"), + function () { + 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) { @@ -54,10 +55,15 @@ frappe.ui.form.on("Budget", { frm.doc.budget_distribution.forEach((row) => { row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2); }); + set_total_budget_amount(frm); frm.refresh_field("budget_distribution"); } }, + distribute_equally: function (frm) { + toggle_distribution_fields(frm); + }, + set_null_value: function (frm) { if (frm.doc.budget_against == "Cost Center") { frm.set_value("project", null); @@ -100,6 +106,8 @@ frappe.ui.form.on("Budget Distribution", { let row = frappe.get_doc(cdt, cdn); if (frm.doc.budget_amount) { row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2); + + set_total_budget_amount(frm); frm.refresh_field("budget_distribution"); } }, @@ -107,7 +115,29 @@ frappe.ui.form.on("Budget Distribution", { let row = frappe.get_doc(cdt, cdn); if (frm.doc.budget_amount) { row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2); + + set_total_budget_amount(frm); 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(); +} diff --git a/erpnext/accounts/doctype/budget/budget.json b/erpnext/accounts/doctype/budget/budget.json index 8476a2831f0..960d62e4c99 100644 --- a/erpnext/accounts/doctype/budget/budget.json +++ b/erpnext/accounts/doctype/budget/budget.json @@ -25,6 +25,10 @@ "distribute_equally", "section_break_fpdt", "budget_distribution", + "section_break_wkqb", + "column_break_paum", + "column_break_nwor", + "budget_distribution_total", "section_break_6", "applicable_on_material_request", "action_if_annual_budget_exceeded_on_mr", @@ -222,7 +226,8 @@ }, { "fieldname": "section_break_fpdt", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { "fieldname": "budget_distribution", @@ -303,13 +308,32 @@ "options": "Monthly\nQuarterly\nHalf-Yearly\nYearly", "read_only_depends_on": "eval: doc.revision_of", "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, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-11-19 17:00:00.648224", + "modified": "2025-12-10 02:35:01.197613", "modified_by": "Administrator", "module": "Accounts", "name": "Budget", diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index d798da5b589..39528da99db 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -53,6 +53,7 @@ class Budget(Document): budget_against: DF.Literal["", "Cost Center", "Project"] budget_amount: DF.Currency budget_distribution: DF.Table[BudgetDistribution] + budget_distribution_total: DF.Currency budget_end_date: DF.Date | None budget_start_date: DF.Date | None company: DF.Link @@ -230,28 +231,49 @@ class Budget(Document): def before_save(self): self.allocate_budget() + self.budget_distribution_total = sum(flt(row.amount) for row in self.budget_distribution) def on_update(self): self.validate_distribution_totals() 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 if not self.should_regenerate_budget_distribution(): return - self.set("budget_distribution", []) + self._regenerate_distribution() - periods = self.get_budget_periods() - total_periods = len(periods) - row_percent = 100 / total_periods if total_periods else 0 + def _should_skip_allocation(self): + return self.revision_of and not self.distribute_equally - 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) + def _should_recalculate_manual_distribution(self): + return ( + not self.distribute_equally + and bool(self.budget_distribution) + 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): """Check whether budget distribution should be recalculated.""" @@ -265,7 +287,6 @@ class Budget(Document): "to_fiscal_year", "budget_amount", "distribution_frequency", - "distribute_equally", ] for field in changed_fields: if old_doc.get(field) != self.get(field): @@ -273,6 +294,21 @@ class Budget(Document): 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): """Return list of (start_date, end_date) tuples based on frequency.""" frequency = self.distribution_frequency @@ -312,12 +348,8 @@ class Budget(Document): }.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, 3) - row.percent = flt(row_percent, 3) + 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(): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3e640e7bf0c..c0f8ff6e2e2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -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.v16_0.migrate_budget_records_to_new_structure erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter -erpnext.patches.v16_0.migrate_account_freezing_settings_to_company \ No newline at end of file +erpnext.patches.v16_0.migrate_account_freezing_settings_to_company +erpnext.patches.v16_0.populate_budget_distribution_total \ No newline at end of file diff --git a/erpnext/patches/v16_0/populate_budget_distribution_total.py b/erpnext/patches/v16_0/populate_budget_distribution_total.py new file mode 100644 index 00000000000..033fb968b4f --- /dev/null +++ b/erpnext/patches/v16_0/populate_budget_distribution_total.py @@ -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)