From 8d9d006c98d7409c766e9d6ece0bebdacf03d4e0 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Tue, 30 Dec 2025 14:30:56 +0530 Subject: [PATCH 01/50] refactor: rename parent bureau head to regional bureau head --- beams/beams/doctype/bureau/bureau.json | 32 +++++++++++--------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/beams/beams/doctype/bureau/bureau.json b/beams/beams/doctype/bureau/bureau.json index 6dccea586..f90905578 100644 --- a/beams/beams/doctype/bureau/bureau.json +++ b/beams/beams/doctype/bureau/bureau.json @@ -11,12 +11,13 @@ "cost_center", "amended_from", "mode_of_payment", + "bureau_head", "column_break_xpwx", "company", "location", "is_parent_bureau", "regional_bureau", - "parent_bureau_head" + "regional_bureau_head" ], "fields": [ { @@ -83,36 +84,29 @@ "label": "Is Parent Bureau" }, { - "fetch_from": "regional_bureau.parent_bureau_head", - "fieldname": "parent_bureau_head", + "fetch_from": "regional_bureau.bureau_head", + "fieldname": "regional_bureau_head", + "fieldtype": "Link", + "label": "Regional Bureau Head", + "options": "Employee", + "in_list_view": 1 + }, + { + "fieldname": "bureau_head", "fieldtype": "Link", - "in_list_view": 1, "label": "Bureau Head", "options": "Employee" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-12-01 14:26:58.708333", + "modified": "2025-12-30 14:21:11.929685", "modified_by": "Administrator", "module": "BEAMS", "name": "Bureau", "naming_rule": "By fieldname", "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], + "permissions": [], "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", From a62edccdbf9506510d58222df583f8317a9e30ca Mon Sep 17 00:00:00 2001 From: NaseeraVP Date: Tue, 30 Dec 2025 16:14:00 +0530 Subject: [PATCH 02/50] fix:Reflect manual batta values in total batta for actual policy --- .../beams/doctype/batta_claim/batta_claim.js | 15 +++++++++ .../beams/doctype/batta_claim/batta_claim.py | 31 ++++++++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/beams/beams/doctype/batta_claim/batta_claim.js b/beams/beams/doctype/batta_claim/batta_claim.js index 036758956..9e101909d 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.js +++ b/beams/beams/doctype/batta_claim/batta_claim.js @@ -58,6 +58,7 @@ frappe.ui.form.on('Batta Claim', { }) }, is_avail_room_rent: function(frm) { + toggle_room_rent_batta_field(frm); update_all_daily_batta(frm); calculate_allowance(frm); }, @@ -68,6 +69,7 @@ frappe.ui.form.on('Batta Claim', { }) }, refresh: function(frm) { + toggle_room_rent_batta_field(frm); clear_checkbox_exceed(frm); frappe.call({ method: "beams.beams.doctype.batta_claim.batta_claim.get_batta_policy_values", @@ -214,6 +216,19 @@ frappe.ui.form.on('Work Detail', { } }); +/* + Toggle room_rent_batta field visibility based on is_avail_room_rent checkbox +*/ +function toggle_room_rent_batta_field(frm) { + if (frm.doc.is_avail_room_rent) { + frm.set_df_property('room_rent_batta', 'hidden', 0); + } else { + frm.set_df_property('room_rent_batta', 'hidden', 1); + frm.set_value('room_rent_batta', 0); + } + frm.refresh_field('room_rent_batta'); +} + /* Calculates the total distance traveled based on all work detail entries. */ diff --git a/beams/beams/doctype/batta_claim/batta_claim.py b/beams/beams/doctype/batta_claim/batta_claim.py index b180316ea..57a67e9bc 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.py +++ b/beams/beams/doctype/batta_claim/batta_claim.py @@ -139,7 +139,9 @@ def calculate_total_hours(self): def calculate_total_daily_batta(self): ''' - Calculation of Total Daily Batta + Calculation of Total Daily Batta + - If policy is ACTUAL → take manual parent values directly + - Else → calculate from rows ''' rows_total = 0.0 sum_food = 0.0 @@ -148,10 +150,31 @@ def calculate_total_daily_batta(self): rows_total += flt(row.daily_batta or 0) sum_food += flt(row.total_food_allowance or 0) - parent_components = flt(self.room_rent_batta or 0) \ - + flt(self.daily_batta_with_overnight_stay or 0) + is_actual_with = 0 + is_actual_without = 0 - self.total_daily_batta = flt(rows_total + parent_components + sum_food) + if self.designation: + batta_policy = frappe.get_all( + 'Batta Policy', + filters={'designation': self.designation}, + fields=['is_actual_', 'is_actual__'], + limit=1 + ) + if batta_policy: + is_actual_with = cint(batta_policy[0].get('is_actual_', 0)) + is_actual_without = cint(batta_policy[0].get('is_actual__', 0)) + + parent_components = 0.0 + + if is_actual_with: + parent_components += flt(self.daily_batta_with_overnight_stay or 0) + + if is_actual_without: + parent_components += flt(self.daily_batta_without_overnight_stay or 0) + + parent_components += flt(self.room_rent_batta or 0) + + self.total_daily_batta = flt(rows_total + sum_food + parent_components) def calculate_batta(self): ''' From 0f43ac520cd0bfec10824d5409fde100b1f24ab6 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Wed, 31 Dec 2025 10:49:30 +0530 Subject: [PATCH 03/50] feat: set MOP as bureau's MOP --- .../substitute_booking/substitute_booking.js | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/beams/beams/doctype/substitute_booking/substitute_booking.js b/beams/beams/doctype/substitute_booking/substitute_booking.js index eca7d978d..c18e033f5 100644 --- a/beams/beams/doctype/substitute_booking/substitute_booking.js +++ b/beams/beams/doctype/substitute_booking/substitute_booking.js @@ -124,7 +124,10 @@ frappe.ui.form.on("Substitute Booking", { }, is_budgeted(frm) { update_budget_exceeded_visibility(frm); - } + }, + bureau(frm) { + fetch_mode_of_payment_from_bureau(frm, frm.doc.bureau); + } }); @@ -202,3 +205,23 @@ function set_bureau_and_account(frm) { }); } + +/* + Fetch Mode of Payment from Bureau and set it +*/ +function fetch_mode_of_payment_from_bureau(frm, bureau) { + if (!bureau) { + frm.set_value("mode_of_payment", null); + return; + } + + frappe.db + .get_value("Bureau", bureau, "mode_of_payment") + .then(r => { + if (r.message?.mode_of_payment) { + frm.set_value("mode_of_payment", r.message.mode_of_payment); + } else { + frm.set_value("mode_of_payment", null); + } + }); +} From 006afa5f6b9d337402cfbdcd9a91c7b12652f8ad Mon Sep 17 00:00:00 2001 From: NaseeraVP Date: Wed, 31 Dec 2025 12:45:42 +0530 Subject: [PATCH 04/50] feat: Total Amount field in Material Request doctype --- .../material_request/material_request.js | 30 +++++++++++++++++++ beams/setup.py | 8 +++++ 2 files changed, 38 insertions(+) diff --git a/beams/beams/custom_scripts/material_request/material_request.js b/beams/beams/custom_scripts/material_request/material_request.js index 7d71d3e99..a7ea037de 100644 --- a/beams/beams/custom_scripts/material_request/material_request.js +++ b/beams/beams/custom_scripts/material_request/material_request.js @@ -12,12 +12,42 @@ frappe.ui.form.on('Material Request', { }, refresh(frm) { clear_checkbox_exceed(frm); + calculate_total_amount(frm); + }, is_budgeted: function(frm){ clear_checkbox_exceed(frm); } }); +frappe.ui.form.on('Material Request Item', { + amount: function(frm, cdt, cdn) { + calculate_total_amount(frm); + }, + qty: function(frm, cdt, cdn) { + calculate_total_amount(frm); + }, + rate: function(frm, cdt, cdn) { + calculate_total_amount(frm); + }, + items_remove: function(frm, cdt, cdn) { + calculate_total_amount(frm); + } +}); + +/** + * Calculates total amount from qty × rate for all items + */ +function calculate_total_amount(frm) { + let total = 0.0; + if (frm.doc.items && frm.doc.items.length) { + frm.doc.items.forEach(function(item) { + total += (item.amount || 0); + }); + } + frm.set_value('total_amount', total); +} + /** * Clears the "budget_exceeded" checkbox if "is_budgeted" is unchecked. */ diff --git a/beams/setup.py b/beams/setup.py index 079a20d66..b5e3c4404 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -5682,6 +5682,14 @@ def get_material_request_custom_fields(): "insert_after": "technical", "read_only": 1, }, + { + "fieldname": "total_amount", + "fieldtype": "Currency", + "label": "Total Amount", + "insert_after": "items", + "read_only": 1, + "description": "Auto calculated from item amounts" + }, ] } From db14faa705973bf57afe1fd4d70fbdd8d1ce5e26 Mon Sep 17 00:00:00 2001 From: NaseeraVP Date: Wed, 31 Dec 2025 16:10:50 +0530 Subject: [PATCH 05/50] feat:Implement role-based HOD notification for Batta Claim --- .../doctype/batta_claim/batta_claim.json | 11 +++++++- .../beams/doctype/batta_claim/batta_claim.py | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/beams/beams/doctype/batta_claim/batta_claim.json b/beams/beams/doctype/batta_claim/batta_claim.json index 60d896d8b..61a39c587 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.json +++ b/beams/beams/doctype/batta_claim/batta_claim.json @@ -33,6 +33,7 @@ "attach", "trip_sheet", "travel_request", + "hod_user", "section_break_bkxb", "room_rent_batta", "batta_based_on", @@ -325,6 +326,14 @@ "fieldtype": "Check", "hidden": 1, "label": "From Bureau" + }, + { + "fieldname": "hod_user", + "fieldtype": "Link", + "hidden": 1, + "label": "HOD User", + "options": "User", + "read_only": 1 } ], "index_web_pages_for_search": 1, @@ -339,7 +348,7 @@ "link_fieldname": "batta_claim_reference" } ], - "modified": "2025-12-06 11:55:57.725516", + "modified": "2025-12-31 14:38:53.705321", "modified_by": "Administrator", "module": "BEAMS", "name": "Batta Claim", diff --git a/beams/beams/doctype/batta_claim/batta_claim.py b/beams/beams/doctype/batta_claim/batta_claim.py index 57a67e9bc..d6fe87761 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.py +++ b/beams/beams/doctype/batta_claim/batta_claim.py @@ -36,6 +36,7 @@ def on_submit(self): self.create_journal_entry_from_batta_claim() def validate(self): + self.assign_hod_role() self.calculate_total_hours() self.calculate_total_distance_travelled() self.calculate_daily_batta() @@ -290,6 +291,31 @@ def calculate_total_batta(self): food_allowance = row.total_food_allowance or 0 row.total_batta = daily_batta + food_allowance + def assign_hod_role(self): + if not self.employee: + return + + department = frappe.db.get_value("Employee", self.employee, "department") + if not department: + return + + hod = frappe.db.get_value("Department", department, "head_of_department") + if not hod: + return + + hod_user = frappe.db.get_value("Employee", hod, "user_id") + if not hod_user: + return + + if not frappe.db.exists( + "Has Role", + {"parent": hod_user, "role": "HOD"} + ): + user = frappe.get_doc("User", hod_user) + user.append("roles", {"role": "HOD"}) + user.save(ignore_permissions=True) + + @frappe.whitelist() def calculate_batta_allowance(designation=None, is_travelling_outside_kerala=0, is_overnight_stay=0, is_avail_room_rent=0, total_distance_travelled_km=0, total_hours=0): ''' From 456ac959281855537534c51a424c3511f09f2126 Mon Sep 17 00:00:00 2001 From: NaseeraVP Date: Thu, 1 Jan 2026 11:29:06 +0530 Subject: [PATCH 06/50] feat:Relocate Payment Section to Details Tab in Purchase Invoice --- beams/setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/beams/setup.py b/beams/setup.py index b5e3c4404..cec301dbb 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -5614,6 +5614,13 @@ def get_property_setters(): "property_type": "Data", "value": "employee_name, designation" }, + { + "doctype_or_field": "DocType", + "doc_type": "Purchase Invoice", + "property": "field_order", + "property_type": "Data", + "value": '["workflow_state", "title", "naming_series", "invoice_type", "purchase_order_id", "stringer_bill_reference", "batta_claim_reference", "supplier", "bureau", "barter_invoice", "quotation", "supplier_name", "ewaybill", "tally_masterid", "tally_voucherno", "tax_id", "company", "column_break_6", "posting_date", "posting_time", "set_posting_time", "due_date", "column_break1", "is_paid", "is_return", "return_against", "update_outstanding_for_self", "update_billed_amount_in_purchase_order", "update_billed_amount_in_purchase_receipt", "apply_tds", "is_reverse_charge", "is_budgeted", "budget_exceeded", "from_bureau", "tax_withholding_category", "amended_from", "payments_section", "mode_of_payment", "base_paid_amount", "clearance_date", "col_br_payments", "cash_bank_account", "paid_amount", "supplier_invoice_details", "bill_no", "column_break_15", "bill_date", "accounting_dimensions_section", "cost_center", "dimension_col_break", "project", "currency_and_price_list", "currency", "conversion_rate", "use_transaction_date_exchange_rate", "column_break2", "buying_price_list", "price_list_currency", "plc_conversion_rate", "ignore_pricing_rule", "sec_warehouse", "scan_barcode", "col_break_warehouse", "update_stock", "set_warehouse", "set_from_warehouse", "is_subcontracted", "rejected_warehouse", "supplier_warehouse", "items_section", "items", "section_break_26", "total_qty", "total_net_weight", "column_break_50", "base_total", "base_net_total", "attach", "column_break_28", "total", "net_total", "tax_withholding_net_total", "base_tax_withholding_net_total", "taxes_section", "tax_category", "taxes_and_charges", "column_break_58", "shipping_rule", "column_break_49", "incoterm", "named_place", "section_break_51", "taxes", "totals", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted", "base_total_taxes_and_charges", "column_break_40", "taxes_and_charges_added", "taxes_and_charges_deducted", "total_taxes_and_charges", "section_break_49", "base_grand_total", "base_rounding_adjustment", "base_rounded_total", "base_in_words", "column_break8", "grand_total", "rounding_adjustment", "use_company_roundoff_cost_center", "rounded_total", "in_words", "total_advance", "outstanding_amount", "disable_rounded_total", "section_break_44", "apply_discount_on", "base_discount_amount", "column_break_46", "additional_discount_percentage", "discount_amount", "tax_withheld_vouchers_section", "tax_withheld_vouchers", "sec_tax_breakup", "other_charges_calculation", "section_gst_breakup", "gst_breakup_table", "pricing_rule_details", "pricing_rules", "raw_materials_supplied", "supplied_items", "payments_tab", "advances_section", "allocate_advances_automatically", "only_include_allocated_payments", "get_advances", "advances", "advance_tax", "write_off", "write_off_amount", "base_write_off_amount", "column_break_61", "write_off_account", "write_off_cost_center", "address_and_contact_tab", "section_addresses", "supplier_address", "address_display", "supplier_gstin", "gst_category", "col_break_address", "contact_person", "contact_display", "contact_mobile", "contact_email", "company_shipping_address_section", "dispatch_address", "dispatch_address_display", "column_break_126", "shipping_address", "shipping_address_display", "company_billing_address_section", "billing_address", "column_break_130", "billing_address_display", "company_gstin", "place_of_supply", "terms_tab", "payment_schedule_section", "payment_terms_template", "ignore_default_payment_terms_template", "payment_schedule", "terms_section_break", "tc_name", "terms", "more_info_tab", "status_section", "status", "column_break_177", "per_received", "accounting_details_section", "credit_to", "party_account_currency", "is_opening", "against_expense_account", "column_break_63", "unrealized_profit_loss_account", "subscription_section", "subscription", "auto_repeat", "update_auto_repeat_reference", "column_break_114", "from_date", "to_date", "printing_settings", "letter_head", "group_same_items", "column_break_112", "select_print_heading", "language", "transporter_info", "transporter", "gst_transporter_id", "driver", "lr_no", "vehicle_no", "distance", "transporter_col_break", "transporter_name", "mode_of_transport", "driver_name", "lr_date", "gst_vehicle_type", "gst_section", "itc_classification", "ineligibility_reason", "reconciliation_status", "sb_14", "on_hold", "release_date", "cb_17", "hold_comment", "additional_info_section", "is_internal_supplier", "represents_company", "supplier_group", "column_break_147", "inter_company_invoice_reference", "is_old_subcontracting_flow", "remarks", "connections_tab"]' + }, ] def get_material_request_custom_fields(): From e49e719b57c424df5e7d3f22a2b408517ce19258 Mon Sep 17 00:00:00 2001 From: anjusha Date: Thu, 1 Jan 2026 13:18:03 +0530 Subject: [PATCH 07/50] fix: resolve invalid depends_on expression in Budget form --- beams/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beams/setup.py b/beams/setup.py index eb8960ff3..a102b6fa5 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -1103,7 +1103,7 @@ def get_budget_custom_fields(): "label": "Rejection Feedback", "options":"Rejection Feedback", "insert_after": "december", - "depends_on": "eval: doc.workflow_state.includes('Rejected')" + "depends_on": "eval: doc.workflow_state == 'Rejected'" }, { "fieldname": "total_amount", From d85aef3865940608ee311f092151d0385a969bd8 Mon Sep 17 00:00:00 2001 From: anjusha Date: Thu, 1 Jan 2026 16:17:15 +0530 Subject: [PATCH 08/50] fix(Bureau): set regional bureau head based on selected bureau --- beams/beams/doctype/bureau/bureau.js | 14 ++++++++++++++ beams/beams/doctype/bureau/bureau.json | 15 +++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/beams/beams/doctype/bureau/bureau.js b/beams/beams/doctype/bureau/bureau.js index c533665cc..0f79aac61 100644 --- a/beams/beams/doctype/bureau/bureau.js +++ b/beams/beams/doctype/bureau/bureau.js @@ -4,6 +4,20 @@ frappe.ui.form.on("Bureau", { setup(frm) { set_filters(frm); + }, + regional_bureau(frm) { + if (frm.doc.regional_bureau) { + frappe.db.get_value( + 'Bureau', + frm.doc.regional_bureau, + 'regional_bureau_head', + (r) => { + if (r && r.regional_bureau_head) { + frm.set_value('regional_bureau_head', r.regional_bureau_head); + } + } + ); + } } }); diff --git a/beams/beams/doctype/bureau/bureau.json b/beams/beams/doctype/bureau/bureau.json index f90905578..f9b694847 100644 --- a/beams/beams/doctype/bureau/bureau.json +++ b/beams/beams/doctype/bureau/bureau.json @@ -84,13 +84,12 @@ "label": "Is Parent Bureau" }, { - "fetch_from": "regional_bureau.bureau_head", - "fieldname": "regional_bureau_head", - "fieldtype": "Link", - "label": "Regional Bureau Head", - "options": "Employee", - "in_list_view": 1 - }, + "fieldname": "regional_bureau_head", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Regional Bureau Head", + "options": "Employee" + }, { "fieldname": "bureau_head", "fieldtype": "Link", @@ -100,7 +99,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-12-30 14:21:11.929685", + "modified": "2026-01-01 15:25:28.045824", "modified_by": "Administrator", "module": "BEAMS", "name": "Bureau", From 50370cf7783df5320f8accd6c6d45a88b9a6ee6d Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Fri, 2 Jan 2026 10:10:35 +0530 Subject: [PATCH 09/50] refactor: fix indent --- beams/beams/doctype/substitute_booking/substitute_booking.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beams/beams/doctype/substitute_booking/substitute_booking.js b/beams/beams/doctype/substitute_booking/substitute_booking.js index c18e033f5..9b14cd84c 100644 --- a/beams/beams/doctype/substitute_booking/substitute_booking.js +++ b/beams/beams/doctype/substitute_booking/substitute_booking.js @@ -125,7 +125,7 @@ frappe.ui.form.on("Substitute Booking", { is_budgeted(frm) { update_budget_exceeded_visibility(frm); }, - bureau(frm) { + bureau(frm) { fetch_mode_of_payment_from_bureau(frm, frm.doc.bureau); } }); From 1df629efe940bf22895c51b4e098c73cf7f3612f Mon Sep 17 00:00:00 2001 From: anjusha Date: Fri, 2 Jan 2026 10:44:33 +0530 Subject: [PATCH 10/50] refactor(bureau): extract regional bureau head logic into helper function --- beams/beams/doctype/bureau/bureau.js | 32 +++++++++++++++++----------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/beams/beams/doctype/bureau/bureau.js b/beams/beams/doctype/bureau/bureau.js index 0f79aac61..b8540a8d1 100644 --- a/beams/beams/doctype/bureau/bureau.js +++ b/beams/beams/doctype/bureau/bureau.js @@ -6,18 +6,7 @@ frappe.ui.form.on("Bureau", { set_filters(frm); }, regional_bureau(frm) { - if (frm.doc.regional_bureau) { - frappe.db.get_value( - 'Bureau', - frm.doc.regional_bureau, - 'regional_bureau_head', - (r) => { - if (r && r.regional_bureau_head) { - frm.set_value('regional_bureau_head', r.regional_bureau_head); - } - } - ); - } + set_regional_bureau_head(frm); } }); @@ -33,3 +22,22 @@ let set_filters = function (frm) { } }); } + +/** + * Fetches and sets the Regional Bureau Head based on the selected Regional Bureau. + */ +function set_regional_bureau_head(frm) { + if (frm.doc.regional_bureau) { + frappe.db.get_value( + 'Bureau', + frm.doc.regional_bureau, + 'regional_bureau_head', + (r) => { + if (r && r.regional_bureau_head) { + frm.set_value('regional_bureau_head', r.regional_bureau_head); + } + } + ); + } +} + From 6b609e7d8325833f2f1fb0fa3afd25eeacde66f0 Mon Sep 17 00:00:00 2001 From: anjusha Date: Sat, 3 Jan 2026 15:30:38 +0530 Subject: [PATCH 11/50] fix(Stringer Bill): resolve invalid fetch_from field for bureau head --- beams/beams/doctype/stringer_bill/stringer_bill.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beams/beams/doctype/stringer_bill/stringer_bill.json b/beams/beams/doctype/stringer_bill/stringer_bill.json index 9c5dc61d0..090b39892 100644 --- a/beams/beams/doctype/stringer_bill/stringer_bill.json +++ b/beams/beams/doctype/stringer_bill/stringer_bill.json @@ -130,7 +130,7 @@ "label": "Is Budget Exceed" }, { - "fetch_from": "bureau.parent_bureau_head", + "fetch_from": "bureau.regional_bureau_head", "fieldname": "bureau_head", "fieldtype": "Link", "hidden": 1, @@ -146,7 +146,7 @@ "link_fieldname": "stringer_bill_reference" } ], - "modified": "2025-12-24 11:01:47.212630", + "modified": "2026-01-03 15:22:45.560448", "modified_by": "Administrator", "module": "BEAMS", "name": "Stringer Bill", From ef7fa58d2255f42353989433b4313ee4422b72c2 Mon Sep 17 00:00:00 2001 From: NaseeraVP Date: Mon, 5 Jan 2026 10:32:39 +0530 Subject: [PATCH 12/50] fix: Implement role-based HOD notification for Batta Claim --- .../doctype/batta_claim/batta_claim.json | 12 +++---- .../beams/doctype/batta_claim/batta_claim.py | 33 ++++++++++++------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/beams/beams/doctype/batta_claim/batta_claim.json b/beams/beams/doctype/batta_claim/batta_claim.json index 61a39c587..bf78578ac 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.json +++ b/beams/beams/doctype/batta_claim/batta_claim.json @@ -33,7 +33,7 @@ "attach", "trip_sheet", "travel_request", - "hod_user", + "hod_email", "section_break_bkxb", "room_rent_batta", "batta_based_on", @@ -328,11 +328,11 @@ "label": "From Bureau" }, { - "fieldname": "hod_user", - "fieldtype": "Link", + "fieldname": "hod_email", + "fieldtype": "Data", "hidden": 1, - "label": "HOD User", - "options": "User", + "label": "HOD Email", + "options": "Email", "read_only": 1 } ], @@ -348,7 +348,7 @@ "link_fieldname": "batta_claim_reference" } ], - "modified": "2025-12-31 14:38:53.705321", + "modified": "2026-01-05 14:51:52.507275", "modified_by": "Administrator", "module": "BEAMS", "name": "Batta Claim", diff --git a/beams/beams/doctype/batta_claim/batta_claim.py b/beams/beams/doctype/batta_claim/batta_claim.py index d6fe87761..7ee0174a2 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.py +++ b/beams/beams/doctype/batta_claim/batta_claim.py @@ -292,28 +292,37 @@ def calculate_total_batta(self): row.total_batta = daily_batta + food_allowance def assign_hod_role(self): - if not self.employee: + ''' + Set the Head of Department (HOD) for the Leave Application based on the Employee's department. + ''' + if not self.employee or self.hod_email: return - department = frappe.db.get_value("Employee", self.employee, "department") + department = frappe.db.get_value( + "Employee", + self.employee, + "department" + ) if not department: return - hod = frappe.db.get_value("Department", department, "head_of_department") - if not hod: + hod_employee = frappe.db.get_value( + "Department", + department, + "head_of_department" + ) + if not hod_employee: return - hod_user = frappe.db.get_value("Employee", hod, "user_id") + hod_user = frappe.db.get_value( + "Employee", + hod_employee, + "user_id" + ) if not hod_user: return - if not frappe.db.exists( - "Has Role", - {"parent": hod_user, "role": "HOD"} - ): - user = frappe.get_doc("User", hod_user) - user.append("roles", {"role": "HOD"}) - user.save(ignore_permissions=True) + self.hod_email = hod_user @frappe.whitelist() From 8575d3b3714a478ba1610cb8f31cb451d59f968a Mon Sep 17 00:00:00 2001 From: Neha Fathima Date: Mon, 12 Jan 2026 12:54:53 +0530 Subject: [PATCH 13/50] fix: Duplicate error in Employee creation in employee doctype --- .../beams/custom_scripts/employee/employee.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/beams/beams/custom_scripts/employee/employee.py b/beams/beams/custom_scripts/employee/employee.py index 7a3821367..89efc63bf 100644 --- a/beams/beams/custom_scripts/employee/employee.py +++ b/beams/beams/custom_scripts/employee/employee.py @@ -118,20 +118,21 @@ def autoname(doc, method): doc.name = get_next_employee_id(department_abbr) doc.employee = doc.name -def get_next_employee_id(department_abbr): - ''' - Method to get next Employee ID - ''' - series_prefix = "MB/{0}/".format(department_abbr) - next_employee_id = '{0}1'.format(series_prefix) - employees = frappe.db.get_all('Employee', { 'name': ['like', '%{0}%'.format(series_prefix)] }, order_by='name desc', pluck='name') - if employees: - employee_id = employees[0] - employee_id = employee_id.replace(series_prefix, "") - employee_count = int(employee_id) - next_employee_id = '{0}{1}'.format(series_prefix, str(employee_count+1)) - return next_employee_id +def get_next_employee_id(department_abbr): + series_prefix = f"MB/{department_abbr}/" + employees = frappe.db.get_all( + 'Employee', + filters={'name': ['like', f'{series_prefix}%']}, + pluck='name' + ) + max_no = 0 + for emp in employees: + num = int(emp.replace(series_prefix, "")) + if num > max_no: + max_no = num + next_no = max_no + 1 + return f"{series_prefix}{str(next_no).zfill(3)}" def validate_offer_dates(doc, method): """Validate Employee fields before saving/submitting.""" From ad11f920200b095fdf290a85fd83b8b33749d360 Mon Sep 17 00:00:00 2001 From: NaseeraVP Date: Tue, 13 Jan 2026 13:31:25 +0530 Subject: [PATCH 14/50] fix:Fix reason for rejection field handling across workflow --- .../material_request/material_request.py | 12 ++++++++---- beams/setup.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/beams/beams/custom_scripts/material_request/material_request.py b/beams/beams/custom_scripts/material_request/material_request.py index 1772eaae8..52900bdae 100644 --- a/beams/beams/custom_scripts/material_request/material_request.py +++ b/beams/beams/custom_scripts/material_request/material_request.py @@ -6,9 +6,13 @@ def validate(doc,method): - # Validate that "Reason for Rejection" is filled if the status is "Rejected" - if doc.workflow_state == "Rejected" and not doc.reason_for_rejection: - frappe.throw("Please provide a Reason for Rejection before rejecting this request.") + rejected_states = [ + "Rejected", + "Rejected by Non-Technical User", + "Rejected by Technical User" + ] + if doc.workflow_state in rejected_states and not doc.reason_for_rejection: + frappe.throw("Please provide a Reason for Rejection before rejecting this request.") @frappe.whitelist() def notify_stock_managers(doc=None, method=None): @@ -194,7 +198,7 @@ def create_todo_for_hod(doc, method): def set_checkbox_for_item_type(doc, method): """ Sets Technical or Non-Technical checkboxes on Material Request - based on the Item Type of items. + based on the Item Type of items. """ doc.technical = 0 diff --git a/beams/setup.py b/beams/setup.py index 7a2d5b7fd..fcff6eb68 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -5648,9 +5648,9 @@ def get_material_request_custom_fields(): "fieldtype": "Small Text", "label": "Reason for Rejection", "insert_after": "items", - "depends_on":"eval:doc.workflow_state == 'Rejected' || doc.workflow_state == 'Informed Admin' || doc.workflow_state == 'Informed HR' || doc.workflow_state == 'Informed HOD'", + "depends_on": "eval:doc.workflow_state != 'Draft' && doc.workflow_state != 'Approved'", "allow_on_submit": 1, - "read_only_depends_on": "eval:doc.workflow_state == 'Rejected'" + "read_only_depends_on": "eval:doc.workflow_state && doc.workflow_state.includes('Rejected')" }, { "fieldname": "employee_name", From 7f0afc762474a4d362a8ab4594bade6d3fa6d6b8 Mon Sep 17 00:00:00 2001 From: Neha Fathima Date: Tue, 13 Jan 2026 10:51:03 +0530 Subject: [PATCH 15/50] fix: simplify the logic to fix duplicate error --- .../beams/custom_scripts/employee/employee.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/beams/beams/custom_scripts/employee/employee.py b/beams/beams/custom_scripts/employee/employee.py index 89efc63bf..5560c908d 100644 --- a/beams/beams/custom_scripts/employee/employee.py +++ b/beams/beams/custom_scripts/employee/employee.py @@ -118,21 +118,20 @@ def autoname(doc, method): doc.name = get_next_employee_id(department_abbr) doc.employee = doc.name - def get_next_employee_id(department_abbr): - series_prefix = f"MB/{department_abbr}/" - employees = frappe.db.get_all( - 'Employee', - filters={'name': ['like', f'{series_prefix}%']}, - pluck='name' + ''' + Method to get next Employee ID + ''' + p = f"MB/{department_abbr}/" + last = frappe.db.get_all( + "Employee", + filters={"name": ["like", f"{p}%"]}, + order_by="creation desc", + pluck="name", + limit=1 ) - max_no = 0 - for emp in employees: - num = int(emp.replace(series_prefix, "")) - if num > max_no: - max_no = num - next_no = max_no + 1 - return f"{series_prefix}{str(next_no).zfill(3)}" + last_no = int(last[0].split("/")[-1]) if last else 0 + return f"{p}{last_no + 1:03d}" def validate_offer_dates(doc, method): """Validate Employee fields before saving/submitting.""" From b4d892ef041419fe8b36d2dd7bf79e8468b77d3e Mon Sep 17 00:00:00 2001 From: NaseeraVP Date: Wed, 14 Jan 2026 17:16:23 +0530 Subject: [PATCH 16/50] fix:Fix Visibility of Driver Assignment Table in employee travel request --- .../employee_travel_request.js | 21 ++++++++++++------- .../employee_travel_request.json | 5 ++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/beams/beams/doctype/employee_travel_request/employee_travel_request.js b/beams/beams/doctype/employee_travel_request/employee_travel_request.js index 56c2c7a89..6c660028c 100644 --- a/beams/beams/doctype/employee_travel_request/employee_travel_request.js +++ b/beams/beams/doctype/employee_travel_request/employee_travel_request.js @@ -13,7 +13,7 @@ frappe.ui.form.on('Employee Travel Request', { set_expense_claim_html(frm) }, refresh: function (frm) { - + toggle_vehicle_allocation_table(frm); create_batta_claim_from_travel(frm); if (!frm.is_new() && frappe.user.has_role("Admin")) { frm.add_custom_button(__('Journal Entry'), function () { @@ -214,12 +214,6 @@ frappe.ui.form.on('Employee Travel Request', { dialog.show(); }, __('Create')); - if (frm.doc.workflow_state === "Approved by HOD" && frm.doc.is_vehicle_required) { - frm.set_df_property("travel_vehicle_allocation", "read_only", 0); - } else { - frm.set_df_property("travel_vehicle_allocation", "read_only", 1); - } - if (frm.doc.is_unplanned === 1) { frm.set_df_property("attachments", "read_only", 0); } else if (frm.doc.workflow_state === "Approved by HOD") { @@ -522,3 +516,16 @@ function clear_checkbox_exceed(frm){ frm.set_value("is__budget_exceed",0); } } + +/** + * Toggles the read-only state of the vehicle allocation table based on whether a vehicle is required + * and the current workflow state of the travel request. + */ +function toggle_vehicle_allocation_table(frm) { + if (frm.doc.is_vehicle_required && + (frm.doc.workflow_state === "Approved by HOD" || frm.doc.workflow_state === "Pending Admin Approval")) { + frm.set_df_property("travel_vehicle_allocation", "read_only", 0); + } else { + frm.set_df_property("travel_vehicle_allocation", "read_only", 1); + } +} diff --git a/beams/beams/doctype/employee_travel_request/employee_travel_request.json b/beams/beams/doctype/employee_travel_request/employee_travel_request.json index b15ef4f05..d50e37f39 100644 --- a/beams/beams/doctype/employee_travel_request/employee_travel_request.json +++ b/beams/beams/doctype/employee_travel_request/employee_travel_request.json @@ -230,7 +230,7 @@ "fieldname": "travel_vehicle_allocation", "fieldtype": "Table", "label": "Travel Vehicle Allocation", - "mandatory_depends_on": "eval:doc.workflow_state == 'Approved by HOD' && doc.is_vehicle_required == 1\n", + "mandatory_depends_on": "eval:\ndoc.is_vehicle_required == 1 &&\n['Approved by HOD', 'Pending Admin Approval'].includes(doc.workflow_state)\n", "options": "Vehicle Allocation" }, { @@ -312,8 +312,7 @@ "link_fieldname": "travel_request" } ], - - "modified": "2025-12-12 16:02:28.896125", + "modified": "2026-01-14 15:45:41.546113", "modified_by": "Administrator", "module": "BEAMS", "name": "Employee Travel Request", From 6b11f36eefd8c6bc622b4fa52c66023496e2b81f Mon Sep 17 00:00:00 2001 From: NaseeraVP Date: Thu, 15 Jan 2026 17:19:32 +0530 Subject: [PATCH 17/50] fix:fix notification for regional bureau head in stringer bill doctype --- .../doctype/stringer_bill/stringer_bill.py | 158 ++++++++++++------ 1 file changed, 105 insertions(+), 53 deletions(-) diff --git a/beams/beams/doctype/stringer_bill/stringer_bill.py b/beams/beams/doctype/stringer_bill/stringer_bill.py index 0a654470b..db5b2263d 100644 --- a/beams/beams/doctype/stringer_bill/stringer_bill.py +++ b/beams/beams/doctype/stringer_bill/stringer_bill.py @@ -8,56 +8,108 @@ from frappe.utils.user import get_users_with_role class StringerBill(Document): - def on_submit(self): - if self.workflow_state == 'Approved': - self.create_purchase_invoice_from_stringer_bill() - - def create_purchase_invoice_from_stringer_bill(self): - """ - Creation of Purchase Invoice On The Approval Of the Stringer Bill. - """ - # Fetch the item code from the Stringer Type - item_code = frappe.db.get_single_value('Beams Accounts Settings', 'stringer_service_item') - - - - # Create a new Purchase Invoice - purchase_invoice = frappe.new_doc('Purchase Invoice') - purchase_invoice.stringer_bill_reference = self.name - purchase_invoice.supplier = self.supplier - purchase_invoice.invoice_type = 'Stringer Bill' # Set invoice type to "Stringer Bill" - purchase_invoice.posting_date = frappe.utils.nowdate() - - purchase_invoice.bureau = self.bureau - purchase_invoice.cost_center = self.cost_center - - # Populate Child Table - purchase_invoice.append('items', { - 'item_code': item_code, - 'qty': 1, - 'rate': self.stringer_amount - }) - - # Insert and submit the document - purchase_invoice.insert() - purchase_invoice.save() - - # Confirm success - frappe.msgprint(f"Purchase Invoice {purchase_invoice.name} created successfully with Stringer Bill reference {self.name}.", alert=True, indicator="green") - - def after_insert(self): - self.create_todo_on_creation_for_stringer_bill() - - def create_todo_on_creation_for_stringer_bill(self): - """ - Create a ToDo for Accounts Manager when a new Stringer Bill is created. - """ - users = get_users_with_role("Accounts Manager") - if users: - description = f"New Stringer Bill Created for {self.supplier}.
Please Review and Update Details or Take Necessary Actions." - add_assign({ - "assign_to": users, - "doctype": "Stringer Bill", - "name": self.name, - "description": description - }) + def on_submit(self): + if self.workflow_state == 'Approved': + self.create_purchase_invoice_from_stringer_bill() + + def on_update(self): + self.notify_regional_bureau_head() + + def create_purchase_invoice_from_stringer_bill(self): + """ + Creation of Purchase Invoice On The Approval Of the Stringer Bill. + """ + # Fetch the item code from the Stringer Type + item_code = frappe.db.get_single_value('Beams Accounts Settings', 'stringer_service_item') + + # Create a new Purchase Invoice + purchase_invoice = frappe.new_doc('Purchase Invoice') + purchase_invoice.stringer_bill_reference = self.name + purchase_invoice.supplier = self.supplier + purchase_invoice.invoice_type = 'Stringer Bill' + purchase_invoice.posting_date = frappe.utils.nowdate() + purchase_invoice.bureau = self.bureau + purchase_invoice.cost_center = self.cost_center + + # Populate Child Table + purchase_invoice.append('items', { + 'item_code': item_code, + 'qty': 1, + 'rate': self.stringer_amount + }) + + # Insert and submit the document + purchase_invoice.insert() + purchase_invoice.save() + + # Confirm success + frappe.msgprint(f"Purchase Invoice {purchase_invoice.name} created successfully with Stringer Bill reference {self.name}.", alert=True, indicator="green") + + def after_insert(self): + self.create_todo_on_creation_for_stringer_bill() + + def create_todo_on_creation_for_stringer_bill(self): + """ + Create a ToDo for Accounts Manager when a new Stringer Bill is created. + """ + users = get_users_with_role("Accounts Manager") + if users: + description = f"New Stringer Bill Created for {self.supplier}.
Please Review and Update Details or Take Necessary Actions." + add_assign({ + "assign_to": users, + "doctype": "Stringer Bill", + "name": self.name, + "description": description + }) + + def notify_regional_bureau_head(self): + """ + Send bell notification to Regional Bureau Head + when Stringer Bill reaches Pending Bureau Head Approval + """ + previous_doc = self.get_doc_before_save() + + if not previous_doc: + return + + if ( + previous_doc.workflow_state != "Pending Bureau Head Approval" + and self.workflow_state == "Pending Bureau Head Approval" + ): + + if not self.bureau: + return + + emp = frappe.db.get_value( + "Bureau", + self.bureau, + "regional_bureau_head" + ) + + if not emp: + return + + user = frappe.db.get_value("Employee", emp, "user_id") + + if not user: + return + + if frappe.db.exists( + "Notification Log", + { + "document_type": "Stringer Bill", + "document_name": self.name, + "for_user": user, + "subject": f"Stringer Bill {self.name} is pending your approval." + } + ): + return + + frappe.get_doc({ + "doctype": "Notification Log", + "subject": f"Stringer Bill {self.name} is pending your approval.", + "for_user": user, + "document_type": "Stringer Bill", + "document_name": self.name + }).insert(ignore_permissions=True) + From 7726c1e8c21f217fbcef1c937545581bd1d2c8fb Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Wed, 21 Jan 2026 12:04:03 +0530 Subject: [PATCH 18/50] refactor: remove purchase invoice connection --- beams/beams/doctype/batta_claim/batta_claim.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/beams/beams/doctype/batta_claim/batta_claim.json b/beams/beams/doctype/batta_claim/batta_claim.json index bf78578ac..592a6dbe0 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.json +++ b/beams/beams/doctype/batta_claim/batta_claim.json @@ -339,16 +339,12 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [ - { - "link_doctype": "Purchase Invoice", - "link_fieldname": "batta_claim_reference" - }, { "link_doctype": "Journal Entry", "link_fieldname": "batta_claim_reference" } ], - "modified": "2026-01-05 14:51:52.507275", + "modified": "2026-01-21 11:13:17.198649", "modified_by": "Administrator", "module": "BEAMS", "name": "Batta Claim", From d98fce5b53eb71534ac2e454c9d81e687d3073d0 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Sat, 31 Jan 2026 12:58:46 +0530 Subject: [PATCH 19/50] feat: Budget changes as per new sheet --- beams/__init__.py | 6 +- beams/beams/custom_scripts/budget/budget.js | 502 +++++++++--------- beams/beams/custom_scripts/budget/budget.py | 103 ++-- beams/beams/doctype/accounts/accounts.json | 9 +- .../__init__.py | 0 .../budget_group.js} | 2 +- .../budget_group.json} | 23 +- .../budget_group.py} | 2 +- .../test_budget_group.py} | 2 +- .../budget_template/budget_template.js | 124 +++-- .../budget_template/budget_template.json | 27 +- .../budget_template/budget_template.py | 21 +- .../budget_template_item.json | 46 +- .../doctype/budget_tool/budget_tool.html | 188 +++---- .../beams/doctype/budget_tool/budget_tool.js | 400 +++++++------- .../beams/doctype/budget_tool/budget_tool.py | 65 ++- .../doctype/cost_category/cost_category.json | 8 +- beams/beams/doctype/cost_head/cost_head.js | 22 +- beams/beams/doctype/cost_head/cost_head.json | 61 ++- .../doctype/cost_subhead/cost_subhead.js | 29 - .../doctype/cost_subhead/cost_subhead.json | 67 --- .../doctype/cost_subhead/cost_subhead.py | 30 -- .../doctype/cost_subhead/test_cost_subhead.py | 9 - .../__init__.py | 0 .../m1_budget_account/m1_budget_account.json | 315 +++++++++++ .../m1_budget_account/m1_budget_account.py | 9 + beams/beams/utils.py | 8 + beams/fixtures/cost_category.json | 23 + beams/hooks.py | 13 +- beams/setup.py | 96 +--- 30 files changed, 1256 insertions(+), 954 deletions(-) rename beams/beams/doctype/{cost_subhead => budget_group}/__init__.py (100%) rename beams/beams/doctype/{finance_group/finance_group.js => budget_group/budget_group.js} (76%) rename beams/beams/doctype/{finance_group/finance_group.json => budget_group/budget_group.json} (59%) rename beams/beams/doctype/{finance_group/finance_group.py => budget_group/budget_group.py} (84%) rename beams/beams/doctype/{finance_group/test_finance_group.py => budget_group/test_budget_group.py} (77%) delete mode 100644 beams/beams/doctype/cost_subhead/cost_subhead.js delete mode 100644 beams/beams/doctype/cost_subhead/cost_subhead.json delete mode 100644 beams/beams/doctype/cost_subhead/cost_subhead.py delete mode 100644 beams/beams/doctype/cost_subhead/test_cost_subhead.py rename beams/beams/doctype/{finance_group => m1_budget_account}/__init__.py (100%) create mode 100644 beams/beams/doctype/m1_budget_account/m1_budget_account.json create mode 100644 beams/beams/doctype/m1_budget_account/m1_budget_account.py create mode 100644 beams/beams/utils.py create mode 100644 beams/fixtures/cost_category.json diff --git a/beams/__init__.py b/beams/__init__.py index 7829046c8..eb94bdec9 100644 --- a/beams/__init__.py +++ b/beams/__init__.py @@ -25,16 +25,16 @@ # You’ve been warned. # ----------------------------------------------------------------------------- -from erpnext.accounts.doctype.budget import budget +# from erpnext.accounts.doctype.budget import budget from helpdesk.helpdesk.doctype.hd_ticket import hd_ticket from beams.beams.custom_scripts.hd_ticket.hd_ticket import ( get_permission_query_conditions, has_permission, ) -from beams.beams.overrides.budget import validate_expense_against_budget +# from beams.beams.overrides.budget import validate_expense_against_budget -budget.validate_expense_against_budget = validate_expense_against_budget +# budget.validate_expense_against_budget = validate_expense_against_budget hd_ticket.permission_query = get_permission_query_conditions hd_ticket.has_permission = has_permission diff --git a/beams/beams/custom_scripts/budget/budget.js b/beams/beams/custom_scripts/budget/budget.js index 681f7f7d4..98e7a71ae 100644 --- a/beams/beams/custom_scripts/budget/budget.js +++ b/beams/beams/custom_scripts/budget/budget.js @@ -1,270 +1,296 @@ frappe.ui.form.on('Budget', { - refresh: function (frm) { - set_filters(frm); - if (!frm.is_new()) { - frm.add_custom_button('Open Budget Tool', () => { - frappe.set_route('Form', 'Budget Tool', 'Budget Tool'); - }); - } - if (frappe.user_roles.includes("HOD")) { - frm.toggle_display("budget_accounts_custom", true); - } else { - frm.toggle_display("budget_accounts_custom", false); - } - if (frappe.user_roles.includes("HR Manager")) { - frm.toggle_display("budget_accounts_hr", true); - } else { - frm.toggle_display("budget_accounts_hr", false); - } - }, - department: function (frm) { - set_filters(frm); - if (!frm.doc.department) { - frm.set_value('division', null); - } - }, - company: function (frm) { - frm.set_value('department', null); - }, - budget_template: function (frm) { - if (!frm.doc.budget_template) { - frm.set_value('cost_center', null); - frm.set_value('region', null); - frm.clear_table('budget_accounts_custom'); - frm.clear_table('accounts'); - frm.refresh_field('budget_accounts_custom'); - frm.refresh_field('accounts'); - return; - } + onload: function (frm) { + hide_main_tables(frm); + }, + refresh: function (frm) { + hide_main_tables(frm); + set_filters(frm); + if (!frm.is_new()) { + frm.add_custom_button('Open Budget Tool', () => { + frappe.set_route('Form', 'Budget Tool', 'Budget Tool'); + }); + } + }, + department: function (frm) { + set_filters(frm); + if (!frm.doc.department) { + frm.set_value('division', null); + } + }, + company: function (frm) { + frm.set_value('department', null); + }, + budget_template: function (frm) { + if (!frm.doc.budget_template) { + frm.set_value('cost_center', null); + frm.set_value('region', null); + frm.clear_table('budget_accounts'); + frm.refresh_field('budget_accounts'); + frm.clear_table('accounts'); + frm.refresh_field('accounts'); + return; + } - if (frm.doc.budget_template === frm._previous_budget_template) { - return; - } + if (frm.doc.budget_template === frm._previous_budget_template) { + return; + } - let previous_template = frm.doc.__last_value || frm._previous_budget_template; + let previous_template = frm.doc.__last_value || frm._previous_budget_template; - frappe.confirm( - __('Are you sure you want to change the Budget Template? This will reset existing budget data.'), - function () { - frm.clear_table('budget_accounts_custom'); - frm.clear_table('accounts'); - frm.refresh_field('accounts'); + frappe.confirm( + __('Are you sure you want to change the Budget Template? This will reset existing budget data.'), + function () { + frm.clear_table('budget_accounts'); + frm.clear_table('accounts'); + frm.refresh_field('accounts'); - frappe.call({ - method: 'frappe.client.get', - args: { - doctype: 'Budget Template', - name: frm.doc.budget_template - }, - callback: function (response) { - if (response.message) { - let budget_template = response.message; - frm.set_value('cost_center', budget_template.cost_center); - frm.set_value('region', budget_template.region); + frappe.call({ + method: 'frappe.client.get', + args: { + doctype: 'Budget Template', + name: frm.doc.budget_template + }, + callback: function (response) { + if (response.message) { + let budget_template = response.message; + frm.set_value('cost_center', budget_template.cost_center); + frm.set_value('region', budget_template.region); - let budget_template_items = budget_template.budget_template_item || []; - budget_template_items.forEach(function (item) { - let row1 = frm.add_child('budget_accounts_custom'); - row1.cost_head = item.cost_head; - row1.cost_subhead = item.cost_sub_head; - row1.account = item.account; - row1.cost_category = item.cost_category; - let row2 = frm.add_child('accounts'); - row2.cost_head = item.cost_head; - row2.cost_subhead = item.cost_sub_head; - row2.account = item.account; - row2.cost_category = item.cost_category; - }); - frm.refresh_field('budget_accounts_custom'); - frm.refresh_field('accounts'); - } - } - }); + let budget_template_items = budget_template.budget_template_items || []; + let accountMap = {}; + budget_template_items.forEach(function (item) { + let row1 = frm.add_child('budget_accounts'); + row1.cost_head = item.cost_head; + row1.budget_group = item.budget_group; + row1.account = item.account_head; + row1.cost_category = item.cost_category; + row1.budget_amount = 0 - frm._previous_budget_template = frm.doc.budget_template; - }, - function () { - frm.set_value('budget_template', previous_template); - } - ); -} + if (!accountMap[item.account_head]) { + accountMap[item.account_head] = { + account: item.account_head, + budget_amount: 0 + }; + } + + // Add amount (use item.budget_amount if available, else 0) + accountMap[item.account_head].budget_amount += flt(item.budget_amount || 0); + }); + frm.refresh_field('budget_accounts'); + + // Update sum to accounts table + Object.values(accountMap).forEach(data => { + let row = frm.add_child('accounts'); + row.account = data.account; + row.budget_amount = data.budget_amount; + }); + frm.refresh_field('accounts'); + } + } + }); + + frm._previous_budget_template = frm.doc.budget_template; + }, + function () { + frm.set_value('budget_template', previous_template); + } + ); + } }); + // Function to apply filters in the cost subhead field in Budget Account function set_filters(frm) { - frm.set_query('division', function () { - return { - filters: { - department: frm.doc.department, - company: frm.doc.company - } - }; - }); - frm.set_query('budget_template', function () { - return { - filters: { - division: frm.doc.division, - company: frm.doc.company - } - }; - }); - frm.set_query('department', function () { - return { - filters: { - company: frm.doc.company - } - }; - }); - frm.set_query('region', function () { - return { - filters: { - company: frm.doc.company - } - }; - }); + frm.set_query('division', function () { + return { + filters: { + department: frm.doc.department, + company: frm.doc.company + } + }; + }); + frm.set_query('budget_template', function () { + return { + filters: { + division: frm.doc.division, + company: frm.doc.company + } + }; + }); + frm.set_query('department', function () { + return { + filters: { + company: frm.doc.company + } + }; + }); + frm.set_query('region', function () { + return { + filters: { + company: frm.doc.company + } + }; + }); } -frappe.ui.form.on('Budget Account', { - cost_subhead: function (frm, cdt, cdn) { - var row = locals[cdt][cdn]; - - if (row.cost_subhead && frm.doc.company) { - frappe.db.get_doc('Cost Subhead', row.cost_subhead).then(doc => { - if (doc.accounts && doc.accounts.length > 0) { - let account_found = doc.accounts.find(acc => acc.company === frm.doc.company); - if (account_found) { - frappe.model.set_value(cdt, cdn, 'account', account_found.default_account); - } else { - frappe.model.set_value(cdt, cdn, 'account', ''); - frappe.msgprint(__('No default account found for the selected Cost Subhead and Company.')); - } - } else { - frappe.model.set_value(cdt, cdn, 'account', ''); - } - }); - } - }, - budget_amount: function (frm, cdt, cdn) { - let row = locals[cdt][cdn]; - if (row.equal_monthly_distribution && row.budget_amount) { - distribute_budget_equally(frm, cdt, cdn, row.budget_amount); - } - }, - equal_monthly_distribution: function (frm, cdt, cdn) { - let row = locals[cdt][cdn]; +frappe.ui.form.on('M1 Budget Account', { + cost_head: function (frm, cdt, cdn) { + const row = locals[cdt][cdn]; + if (!row.cost_head || !frm.doc.company) { + frappe.model.set_value(cdt, cdn, 'cost_category',); + frappe.model.set_value(cdt, cdn, 'budget_group',); + frappe.model.set_value(cdt, cdn, 'account',); + return; + } + else { + let cost_head = row.cost_head; + // Fetching default account for cost head + frappe.call("beams.beams.utils.get_default_account_of_cost_head", { + cost_head: cost_head, + company: frm.doc.company + }).then(r => { + if (r.message) { + frappe.model.set_value(cdt, cdn, 'account', r.message); + } + else { + frappe.model.set_value(cdt, cdn, 'cost_head',); + frappe.model.set_value(cdt, cdn, 'cost_category',); + frappe.model.set_value(cdt, cdn, 'budget_group',); + frappe.throw({ + title: __('Missing Default Account'), + message: __( + 'No default account found for Cost Head : {0} for the selected company.', [cost_head] + ) + }); + } + }) + } + }, + budget_amount: function (frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.equal_monthly_distribution && row.budget_amount) { + distribute_budget_equally(frm, cdt, cdn, row.budget_amount); + } + }, + equal_monthly_distribution: function (frm, cdt, cdn) { + let row = locals[cdt][cdn]; - if (!row.equal_monthly_distribution) { - frappe.confirm( - "Are you sure you want to uncheck Equal Monthly Distribution?", - function() { - clear_monthly_values(frm, cdt, cdn); - }, - function() { - frappe.model.set_value(cdt, cdn, "equal_monthly_distribution", 1); - } - ); - } else if (row.budget_amount) { - distribute_budget_equally(frm, cdt, cdn, row.budget_amount); - } - }, - january: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - february: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - march: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - april: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - may: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - june: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - july: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - august: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - september: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - october: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - november: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - }, - december: function (frm, cdt, cdn) { - calculate_budget_amount(frm, cdt, cdn); - } + if (!row.equal_monthly_distribution) { + frappe.confirm( + "Are you sure you want to uncheck Equal Monthly Distribution?", + function () { + clear_monthly_values(frm, cdt, cdn); + }, + function () { + frappe.model.set_value(cdt, cdn, "equal_monthly_distribution", 1); + } + ); + } else if (row.budget_amount) { + distribute_budget_equally(frm, cdt, cdn, row.budget_amount); + } + }, + january: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + february: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + march: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + april: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + may: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + june: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + july: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + august: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + september: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + october: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + november: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + }, + december: function (frm, cdt, cdn) { + calculate_budget_amount(frm, cdt, cdn); + } }); function calculate_budget_amount(frm, cdt, cdn) { - let row = locals[cdt][cdn]; - // Calculate the total of all monthly amounts - let total = - (row.january || 0) + - (row.february || 0) + - (row.march || 0) + - (row.april || 0) + - (row.may || 0) + - (row.june || 0) + - (row.july || 0) + - (row.august || 0) + - (row.september || 0) + - (row.october || 0) + - (row.november || 0) + - (row.december || 0); + let row = locals[cdt][cdn]; + // Calculate the total of all monthly amounts + let total = + (row.january || 0) + + (row.february || 0) + + (row.march || 0) + + (row.april || 0) + + (row.may || 0) + + (row.june || 0) + + (row.july || 0) + + (row.august || 0) + + (row.september || 0) + + (row.october || 0) + + (row.november || 0) + + (row.december || 0); - frappe.model.set_value(cdt, cdn, 'budget_amount', total); - frm.refresh_field('budget_account'); + frappe.model.set_value(cdt, cdn, 'budget_amount', total); + frm.refresh_field('budget_account'); } function distribute_budget_equally(frm, cdt, cdn, budget_amount) { - let row = locals[cdt][cdn]; + let row = locals[cdt][cdn]; - // Calculate equal amount for each month and rounding adjustment - let equal_amount = Math.floor((budget_amount / 12) * 100) / 100; - let total = equal_amount * 12; - let difference = Math.round((budget_amount - total) * 100) / 100; + // Calculate equal amount for each month and rounding adjustment + let equal_amount = Math.floor((budget_amount / 12) * 100) / 100; + let total = equal_amount * 12; + let difference = Math.round((budget_amount - total) * 100) / 100; - // Distribute the amounts - frappe.model.set_value(cdt, cdn, 'january', equal_amount); - frappe.model.set_value(cdt, cdn, 'february', equal_amount); - frappe.model.set_value(cdt, cdn, 'march', equal_amount); - frappe.model.set_value(cdt, cdn, 'april', equal_amount); - frappe.model.set_value(cdt, cdn, 'may', equal_amount); - frappe.model.set_value(cdt, cdn, 'june', equal_amount); - frappe.model.set_value(cdt, cdn, 'july', equal_amount); - frappe.model.set_value(cdt, cdn, 'august', equal_amount); - frappe.model.set_value(cdt, cdn, 'september', equal_amount); - frappe.model.set_value(cdt, cdn, 'october', equal_amount); - frappe.model.set_value(cdt, cdn, 'november', equal_amount); - frappe.model.set_value(cdt, cdn, 'december', equal_amount + difference); + // Distribute the amounts + frappe.model.set_value(cdt, cdn, 'january', equal_amount); + frappe.model.set_value(cdt, cdn, 'february', equal_amount); + frappe.model.set_value(cdt, cdn, 'march', equal_amount); + frappe.model.set_value(cdt, cdn, 'april', equal_amount); + frappe.model.set_value(cdt, cdn, 'may', equal_amount); + frappe.model.set_value(cdt, cdn, 'june', equal_amount); + frappe.model.set_value(cdt, cdn, 'july', equal_amount); + frappe.model.set_value(cdt, cdn, 'august', equal_amount); + frappe.model.set_value(cdt, cdn, 'september', equal_amount); + frappe.model.set_value(cdt, cdn, 'october', equal_amount); + frappe.model.set_value(cdt, cdn, 'november', equal_amount); + frappe.model.set_value(cdt, cdn, 'december', equal_amount + difference); - frm.refresh_field('budget_account'); + frm.refresh_field('budget_account'); } function clear_monthly_values(frm, cdt, cdn) { - let fields = [ - 'january', 'february', 'march', 'april', 'may', 'june', - 'july', 'august', 'september', 'october', 'november', 'december' - ]; + let fields = [ + 'january', 'february', 'march', 'april', 'may', 'june', + 'july', 'august', 'september', 'october', 'november', 'december' + ]; - fields.forEach(field => frappe.model.set_value(cdt, cdn, field, 0)); + fields.forEach(field => frappe.model.set_value(cdt, cdn, field, 0)); - frm.refresh_field('budget_account'); + frm.refresh_field('budget_account'); } frappe.ui.form.on("Rejection Feedback", { - rejection_feedback_add: function(frm, cdt, cdn) { - let row = frappe.get_doc(cdt, cdn); - row.user = frappe.session.user_fullname; - frm.refresh_field("rejection_feedback"); - } + rejection_feedback_add: function (frm, cdt, cdn) { + let row = frappe.get_doc(cdt, cdn); + row.user = frappe.session.user_fullname; + frm.refresh_field("rejection_feedback"); + } }); + +function hide_main_tables(frm) { + frm.toggle_display("accounts", false); +} \ No newline at end of file diff --git a/beams/beams/custom_scripts/budget/budget.py b/beams/beams/custom_scripts/budget/budget.py index 537991f4f..b0c9e399b 100644 --- a/beams/beams/custom_scripts/budget/budget.py +++ b/beams/beams/custom_scripts/budget/budget.py @@ -1,55 +1,72 @@ import frappe +month_fields = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] def beams_budget_validate(doc, method=None): - """method runs custom validations for budget doctype""" - update_total_amount(doc, method) - convert_currency(doc, method) - + """ + method runs custom validations for budget doctype + """ + update_total_amount(doc, method) + convert_currency(doc, method) def update_total_amount(doc, method): - total = sum([row.budget_amount for row in doc.get("accounts") if row.budget_amount]) - doc.total_amount = total - + total = sum([row.budget_amount for row in doc.get("accounts") if row.budget_amount]) + doc.total_amount = total def populate_og_accounts(doc, method=None): - doc.accounts = [] - for row in doc.budget_accounts_custom: - accounts_row = row.as_dict() - accounts_row.pop('name') - accounts_row.pop('idx') - doc.append("accounts", accounts_row) - for row in doc.budget_accounts_hr: - accounts_row = row.as_dict() - accounts_row.pop('name') - accounts_row.pop('idx') - doc.append("accounts", accounts_row) + doc.accounts = [] + accounts_map = {} + + #Process OPEX budget accounts + for row in doc.budget_accounts: + # Accumulate amounts per account + account = row.account + # Initialize account if not exists + if account not in accounts_map: + accounts_map[account] = { + "account": account, + "budget_amount": 0 + } + # Initialize all months + for month in month_fields: + accounts_map[account][month] = 0 + # Add monthly values + for month in month_fields: + month_value = row.get(month) or 0 + accounts_map[account][month] += month_value + accounts_map[account]["budget_amount"] += month_value + #Update accumulated amounts for each account in main table + for account_data in accounts_map.values(): + doc.append("accounts", account_data) def convert_currency(doc, method): - """Convert budget amounts for non-INR companies""" - company_currency = frappe.db.get_value("Company", doc.company, "default_currency") - exchange_rate = 1 - - if company_currency != "INR": - exchange_rate = frappe.db.get_value("Company", doc.company, "exchange_rate_to_inr") - if not exchange_rate: - frappe.throw( - f"Please set Exchange Rate from {company_currency} to INR for {doc.company}", - title="Message", - ) - - months = [ - "january", "february", "march", "april", "may", "june", - "july", "august", "september", "october", "november", "december" - ] - - def apply_conversion(row): - """Apply exchange rate conversion to a budget row""" - row.budget_amount_inr = row.budget_amount * exchange_rate - for month in months: - setattr(row, f"{month}_inr", (getattr(row, month, 0) or 0) * exchange_rate) - - for row in (*doc.accounts, *doc.budget_accounts_custom): - apply_conversion(row) + """ + Convert budget amounts for non-INR companies + """ + company_currency = frappe.db.get_value("Company", doc.company, "default_currency") + exchange_rate = 1 + + if company_currency != "INR": + exchange_rate = frappe.db.get_value("Company", doc.company, "exchange_rate_to_inr") + if not exchange_rate: + frappe.throw( + f"Please set Exchange Rate from {company_currency} to INR for {doc.company}", + title="Message", + ) + + months = [ + "january", "february", "march", "april", "may", "june", + "july", "august", "september", "october", "november", "december" + ] + + def apply_conversion(row): + """ + Apply exchange rate conversion to a budget row + """ + row.budget_amount_inr = row.budget_amount * exchange_rate + for month in months: + setattr(row, f"{month}_inr", (getattr(row, month, 0) or 0) * exchange_rate) + for row in (*doc.accounts, *doc.budget_accounts): + apply_conversion(row) diff --git a/beams/beams/doctype/accounts/accounts.json b/beams/beams/doctype/accounts/accounts.json index 55e116436..d6d2d4d8b 100644 --- a/beams/beams/doctype/accounts/accounts.json +++ b/beams/beams/doctype/accounts/accounts.json @@ -15,25 +15,28 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Company", - "options": "Company" + "options": "Company", + "reqd": 1 }, { "fieldname": "default_account", "fieldtype": "Link", "in_list_view": 1, "label": "Default Account", - "options": "Account" + "options": "Account", + "reqd": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-08-21 09:47:36.824116", + "modified": "2026-02-03 14:11:17.379971", "modified_by": "Administrator", "module": "BEAMS", "name": "Accounts", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [] diff --git a/beams/beams/doctype/cost_subhead/__init__.py b/beams/beams/doctype/budget_group/__init__.py similarity index 100% rename from beams/beams/doctype/cost_subhead/__init__.py rename to beams/beams/doctype/budget_group/__init__.py diff --git a/beams/beams/doctype/finance_group/finance_group.js b/beams/beams/doctype/budget_group/budget_group.js similarity index 76% rename from beams/beams/doctype/finance_group/finance_group.js rename to beams/beams/doctype/budget_group/budget_group.js index 1fa9ccc9a..ce2bb8ce8 100644 --- a/beams/beams/doctype/finance_group/finance_group.js +++ b/beams/beams/doctype/budget_group/budget_group.js @@ -1,7 +1,7 @@ // Copyright (c) 2025, efeone and contributors // For license information, please see license.txt -// frappe.ui.form.on("Finance Group", { +// frappe.ui.form.on("Budget Group", { // refresh(frm) { // }, diff --git a/beams/beams/doctype/finance_group/finance_group.json b/beams/beams/doctype/budget_group/budget_group.json similarity index 59% rename from beams/beams/doctype/finance_group/finance_group.json rename to beams/beams/doctype/budget_group/budget_group.json index 1a723ff94..44a712322 100644 --- a/beams/beams/doctype/finance_group/finance_group.json +++ b/beams/beams/doctype/budget_group/budget_group.json @@ -1,29 +1,37 @@ { "actions": [], + "allow_import": 1, "allow_rename": 1, - "autoname": "field:finance_group", - "creation": "2025-03-05 10:56:14.289286", + "autoname": "field:budget_group_name", + "creation": "2025-01-27 10:25:39.939893", "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "finance_group" + "budget_group_name", + "description" ], "fields": [ { - "fieldname": "finance_group", + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "budget_group_name", "fieldtype": "Data", "in_list_view": 1, - "label": "Finance Group", + "label": "Budget Group Name", + "no_copy": 1, "reqd": 1, "unique": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-03-05 11:37:31.454741", + "modified": "2026-01-31 12:46:39.613937", "modified_by": "Administrator", "module": "BEAMS", - "name": "Finance Group", + "name": "Budget Group", "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ @@ -40,6 +48,7 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [] diff --git a/beams/beams/doctype/finance_group/finance_group.py b/beams/beams/doctype/budget_group/budget_group.py similarity index 84% rename from beams/beams/doctype/finance_group/finance_group.py rename to beams/beams/doctype/budget_group/budget_group.py index 0460788e6..914ef8640 100644 --- a/beams/beams/doctype/finance_group/finance_group.py +++ b/beams/beams/doctype/budget_group/budget_group.py @@ -5,5 +5,5 @@ from frappe.model.document import Document -class FinanceGroup(Document): +class BudgetGroup(Document): pass diff --git a/beams/beams/doctype/finance_group/test_finance_group.py b/beams/beams/doctype/budget_group/test_budget_group.py similarity index 77% rename from beams/beams/doctype/finance_group/test_finance_group.py rename to beams/beams/doctype/budget_group/test_budget_group.py index 363c91f36..4fd8e8ab4 100644 --- a/beams/beams/doctype/finance_group/test_finance_group.py +++ b/beams/beams/doctype/budget_group/test_budget_group.py @@ -5,5 +5,5 @@ from frappe.tests.utils import FrappeTestCase -class TestFinanceGroup(FrappeTestCase): +class TestBudgetGroup(FrappeTestCase): pass diff --git a/beams/beams/doctype/budget_template/budget_template.js b/beams/beams/doctype/budget_template/budget_template.js index d61f5c6cd..58dea7663 100644 --- a/beams/beams/doctype/budget_template/budget_template.js +++ b/beams/beams/doctype/budget_template/budget_template.js @@ -2,64 +2,80 @@ // For license information, please see license.txt frappe.ui.form.on('Budget Template', { - refresh: function (frm) { - set_filters(frm); - }, - department: function (frm) { - set_filters(frm); - frm.set_value('division', null); - if (!frm.doc.department) { - frm.set_value('division',) - frm.clear_table('budget_template_item'); - frm.refresh_field('budget_template_item'); - } - }, - company: function (frm) { - frm.set_value('department', null); - frm.set_value('division', null); - if (frm.doc.company) { - // frm.clear_table("budget_template_item"); - frm.refresh_field("budget_template_item"); - } - } + refresh: function (frm) { + set_filters(frm); + }, + department: function (frm) { + set_filters(frm); + frm.set_value('division', null); + if (!frm.doc.department) { + frm.set_value('division',) + clear_budget_items(frm); + } + }, + company: function (frm) { + frm.set_value('department', null); + frm.set_value('division', null); + if (frm.doc.company) { + clear_budget_items(frm); + } + } }); frappe.ui.form.on('Budget Template Item', { - cost_sub_head: function (frm, cdt, cdn) { - var row = locals[cdt][cdn]; - - if (row.cost_sub_head && frm.doc.company) { - frappe.db.get_doc('Cost Subhead', row.cost_sub_head).then(doc => { - if (doc.accounts && doc.accounts.length > 0) { - let account_found = doc.accounts.find(acc => acc.company === frm.doc.company); - if (account_found) { - frappe.model.set_value(cdt, cdn, 'account', account_found.default_account); - } else { - frappe.model.set_value(cdt, cdn, 'account', ''); - frappe.msgprint(__('No default account found for the selected Cost Subhead and Company.')); - } - } else { - frappe.model.set_value(cdt, cdn, 'account', ''); - } - }); - } - } + cost_head: function (frm, cdt, cdn) { + const row = locals[cdt][cdn]; + if (!row.cost_head || !frm.doc.company) { + frappe.model.set_value(cdt, cdn, 'cost_category',); + frappe.model.set_value(cdt, cdn, 'budget_group',); + frappe.model.set_value(cdt, cdn, 'account_head',); + return; + } + else { + let cost_head = row.cost_head; + // Fetching default account for cost head + frappe.call("beams.beams.utils.get_default_account_of_cost_head", { + cost_head: cost_head, + company: frm.doc.company + }).then(r => { + if (r.message) { + frappe.model.set_value(cdt, cdn, 'account_head', r.message); + } + else { + frappe.model.set_value(cdt, cdn, 'cost_head',); + frappe.model.set_value(cdt, cdn, 'cost_category',); + frappe.model.set_value(cdt, cdn, 'budget_group',); + frappe.throw({ + title: __('Missing Default Account'), + message: __( + 'No default account found for Cost Head : {0} for the selected company.', [cost_head] + ) + }); + } + }) + } + }, }); function set_filters(frm) { - frm.set_query('division', function () { - return { - filters: { - department: frm.doc.department, - company: frm.doc.company - } - }; - }); - frm.set_query('department', function () { - return { - filters: { - company: frm.doc.company - } - }; - }); + frm.set_query('division', function () { + return { + filters: { + department: frm.doc.department, + company: frm.doc.company + } + }; + }); + frm.set_query('department', function () { + return { + filters: { + company: frm.doc.company + } + }; + }); } + +function clear_budget_items(frm) { + frm.clear_table('budget_template_items'); + frm.refresh_field('budget_template_items'); +} \ No newline at end of file diff --git a/beams/beams/doctype/budget_template/budget_template.json b/beams/beams/doctype/budget_template/budget_template.json index 8d76c85c1..a2f8d5911 100644 --- a/beams/beams/doctype/budget_template/budget_template.json +++ b/beams/beams/doctype/budget_template/budget_template.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_import": 1, "allow_rename": 1, "autoname": "field:template_name", "creation": "2025-01-23 15:24:52.064169", @@ -16,7 +17,7 @@ "cost_center", "region", "section_break", - "budget_template_item" + "budget_template_items" ], "fields": [ { @@ -26,13 +27,6 @@ "options": "Department", "reqd": 1 }, - { - "depends_on": "eval:doc.company", - "fieldname": "budget_template_item", - "fieldtype": "Table", - "label": "Budget Template Item", - "options": "Budget Template Item" - }, { "fieldname": "division", "fieldtype": "Link", @@ -60,14 +54,16 @@ "fieldname": "budget_head", "fieldtype": "Link", "label": "Budget Head", - "options": "Employee" + "options": "Employee", + "reqd": 1 }, { "fetch_from": "budget_head.user_id", "fieldname": "budget_head_user", "fieldtype": "Link", "label": "Budget Head User", - "options": "User" + "options": "User", + "read_only": 1 }, { "fieldname": "cost_center", @@ -86,13 +82,21 @@ "fieldname": "template_name", "fieldtype": "Data", "label": "Template Name", + "no_copy": 1, "reqd": 1, "unique": 1 + }, + { + "depends_on": "eval:doc.company", + "fieldname": "budget_template_items", + "fieldtype": "Table", + "label": "Budget Template Items", + "options": "Budget Template Item" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-03-13 16:04:02.546476", + "modified": "2026-02-04 11:37:43.926926", "modified_by": "Administrator", "module": "BEAMS", "name": "Budget Template", @@ -112,6 +116,7 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [] diff --git a/beams/beams/doctype/budget_template/budget_template.py b/beams/beams/doctype/budget_template/budget_template.py index 29a70a623..f3a26c9cf 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -5,23 +5,4 @@ from frappe.model.document import Document class BudgetTemplate(Document): - - def set_default_account(self): - if not hasattr(self, "budget_template_item") or not self.budget_template_item: - return - - for item in self.budget_template_item: - if not item.cost_sub_head or not self.company: - item.account = "" - continue - - cost_subhead_doc = frappe.get_doc("Cost Subhead", item.cost_sub_head) - - if cost_subhead_doc.accounts: - account_found = next((acc for acc in cost_subhead_doc.accounts if acc.company == self.company), None) - item.account = account_found.default_account if account_found else "" - else: - item.account = "" - - def before_save(self): - self.set_default_account() + pass diff --git a/beams/beams/doctype/budget_template_item/budget_template_item.json b/beams/beams/doctype/budget_template_item/budget_template_item.json index 58374b875..fbb923fbe 100644 --- a/beams/beams/doctype/budget_template_item/budget_template_item.json +++ b/beams/beams/doctype/budget_template_item/budget_template_item.json @@ -7,50 +7,64 @@ "engine": "InnoDB", "field_order": [ "cost_head", - "cost_sub_head", - "account", + "budget_group", + "column_break_jjqr", + "account_head", "cost_category" ], "fields": [ { - "fieldname": "cost_sub_head", + "fetch_from": "cost_head.cost_category", + "fetch_if_empty": 1, + "fieldname": "cost_category", "fieldtype": "Link", "in_list_view": 1, - "label": "Cost Sub Head", - "options": "Cost Subhead" + "label": "Cost Category", + "options": "Cost Category", + "read_only": 1 }, { - "fieldname": "account", + "fieldname": "cost_head", "fieldtype": "Link", "in_list_view": 1, - "label": "Account", - "options": "Account", - "read_only": 1 + "label": "Cost Head", + "options": "Cost Head", + "reqd": 1 }, { - "fieldname": "cost_category", + "fieldname": "account_head", "fieldtype": "Link", "in_list_view": 1, - "label": "Cost Category", - "options": "Cost Category" + "label": "Account Head", + "options": "Account", + "read_only": 1, + "reqd": 1 }, { - "fieldname": "cost_head", + "fetch_from": "cost_head.budget_group", + "fieldname": "budget_group", "fieldtype": "Link", "in_list_view": 1, - "label": "Cost Head", - "options": "Cost Head" + "label": "Budget Group", + "options": "Budget Group", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_jjqr", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-02-13 11:59:51.014980", + "modified": "2026-02-03 14:39:31.292729", "modified_by": "Administrator", "module": "BEAMS", "name": "Budget Template Item", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [] diff --git a/beams/beams/doctype/budget_tool/budget_tool.html b/beams/beams/doctype/budget_tool/budget_tool.html index 4a1208839..6f9fee3bf 100644 --- a/beams/beams/doctype/budget_tool/budget_tool.html +++ b/beams/beams/doctype/budget_tool/budget_tool.html @@ -2,107 +2,107 @@ - - Pradan Budget - + function clear_cell_value(cell) { + if (cell.innerHTML === '0') { + cell.innerHTML = ''; + } + } + -
- - - - - {% for column in columns %} - {% if column == 'Cost Description' %} - - {% else %} - - {% endif %} - {% endfor %} - - - - {% set table_row = {'id': 0} %} - {% for row in data %} - - - {% for col in row %} - {% if col.primary %} - {% if col.ref_link %} - - {% else %} - - {% endif %} - {% else %} - {% if col.read_only %} - {% if col.class_name %} - - {% else %} - - {% endif %} - {% else %} - {% if col.type == 'text' %} - {% if col.class_name %} - - {% else %} - - {% endif %} - {% endif %} - {% if col.type == 'number' %} - {% if col.class_name %} - - {% else %} - - {% endif %} - {% endif %} - {% endif %} - {% endif %} - {% endfor %} - - {% if table_row.update({'id': table_row.id + 1}) %} {% endif %} - {% endfor %} - -
- No - Cost Description{{column}}
{{loop.index}}{{col.value}}{{col.value}}{{col.value}}{{col.value}}{{col.value}}{{col.value}}{{col.value}}{{col.value}}
-
-
+
+ + + + + {% for column in columns %} + {% if column == 'Cost Description' %} + + {% else %} + + {% endif %} + {% endfor %} + + + + {% set table_row = {'id': 0} %} + {% for row in data %} + + + {% for col in row %} + {% if col.primary %} + {% if col.ref_link %} + + {% else %} + + {% endif %} + {% else %} + {% if col.read_only %} + {% if col.class_name %} + + {% else %} + + {% endif %} + {% else %} + {% if col.type == 'text' %} + {% if col.class_name %} + + {% else %} + + {% endif %} + {% endif %} + {% if col.type == 'number' %} + {% if col.class_name %} + + {% else %} + + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + + {% if table_row.update({'id': table_row.id + 1}) %} {% endif %} + {% endfor %} + +
+ No + Cost Description{{column}}
{{loop.index}}{{col.value}}{{col.value}}{{col.value}}{{col.value}}{{col.value}}{{col.value}}{{col.value}}{{col.value}}
+
+
\ No newline at end of file diff --git a/beams/beams/doctype/budget_tool/budget_tool.js b/beams/beams/doctype/budget_tool/budget_tool.js index fadd24068..dac06a739 100644 --- a/beams/beams/doctype/budget_tool/budget_tool.js +++ b/beams/beams/doctype/budget_tool/budget_tool.js @@ -2,221 +2,229 @@ // For license information, please see license.txt frappe.ui.form.on("Budget Tool", { - onload: function (frm) { - $('.indicator-pill').hide(); - if (!frm.doc.budget) { - var prev_route = frappe.get_prev_route(); - if (prev_route[1] === 'Budget') { - frm.set_value('budget', prev_route[2]); - } - } - }, - refresh: function (frm) { - $('.menu-btn-group').hide(); - $('.indicator-pill').hide(); - frm.disable_save(); - if (!frm.doc.budget) { - let $el = cur_frm.fields_dict.budget.$wrapper; - $el.find('input').focus(); - } - frm.add_custom_button('Reload', () => { - localStorage.clear(); - frappe.ui.toolbar.clear_cache(); - }).addClass('btn-primary'); - }, - budget: function (frm) { - if (frm.doc.budget) { - set_budget_html(frm, frm.doc.budget); - } - else { - frm.clear_custom_buttons(); - $(frm.fields_dict['budget_html'].wrapper).html(''); - frm.set_value('has_unsaved_changes', 0); - refresh_field('budget_html'); - } - }, - has_unsaved_changes: function (frm) { - make_buttons(frm); - }, - add_row: function (frm) { - show_add_row_popup(frm); - } + onload: function (frm) { + $('.indicator-pill').hide(); + if (!frm.doc.budget) { + var prev_route = frappe.get_prev_route(); + if (prev_route[1] === 'Budget') { + frm.set_value('budget', prev_route[2]); + } + } + }, + refresh: function (frm) { + $('.menu-btn-group').hide(); + $('.indicator-pill').hide(); + frm.disable_save(); + if (!frm.doc.budget) { + let $el = cur_frm.fields_dict.budget.$wrapper; + $el.find('input').focus(); + } + frm.add_custom_button('Reload', () => { + localStorage.clear(); + frappe.ui.toolbar.clear_cache(); + }).addClass('btn-primary'); + }, + budget: function (frm) { + if (frm.doc.budget) { + set_budget_html(frm, frm.doc.budget); + } + else { + frm.clear_custom_buttons(); + $(frm.fields_dict['budget_html'].wrapper).html(''); + frm.set_value('has_unsaved_changes', 0); + refresh_field('budget_html'); + } + }, + has_unsaved_changes: function (frm) { + make_buttons(frm); + }, + add_row: function (frm) { + show_add_row_popup(frm); + } }); function set_budget_html(frm, budget) { - frappe.call({ - method: 'beams.beams.doctype.budget_tool.budget_tool.get_budget_html', - args: { - 'budget': frm.doc.budget - }, - freeze: true, - freeze_message: __('Loading......'), - callback: (r) => { - if (r.message) { - var data = r.message; - $(frm.fields_dict['budget_html'].wrapper).html(data.html); - frm.set_value('has_unsaved_changes', 0); - frm.set_value('is_editable', data.is_editable); - frm.refresh_fields(); - make_buttons(frm); - } - } - }); + frappe.call({ + method: 'beams.beams.doctype.budget_tool.budget_tool.get_budget_html', + args: { + 'budget': frm.doc.budget + }, + freeze: true, + freeze_message: __('Loading......'), + callback: (r) => { + if (r.message) { + var data = r.message; + $(frm.fields_dict['budget_html'].wrapper).html(data.html); + frm.set_value('has_unsaved_changes', 0); + frm.set_value('is_editable', data.is_editable); + frm.refresh_fields(); + make_buttons(frm); + } + } + }); } function make_buttons(frm) { - frm.clear_custom_buttons(); - if (frm.doc.budget) { - if (frm.doc.has_unsaved_changes) { - frm.add_custom_button('Save', () => { - saveData(frm); - }).addClass('btn-primary saveBtn'); - } - frm.add_custom_button('Open Budget', () => { - frappe.set_route('Form', 'Budget', frm.doc.budget); - }).addClass('btn-primary saveBtn'); - } - frm.add_custom_button('Reload', () => { - localStorage.clear(); - frappe.ui.toolbar.clear_cache(); - }).addClass('btn-primary saveBtn'); + frm.clear_custom_buttons(); + if (frm.doc.budget) { + if (frm.doc.has_unsaved_changes) { + frm.add_custom_button('Save', () => { + saveData(frm); + }).addClass('btn-primary saveBtn'); + } + frm.add_custom_button('Open Budget', () => { + frappe.set_route('Form', 'Budget', frm.doc.budget); + }).addClass('btn-primary saveBtn'); + } + frm.add_custom_button('Reload', () => { + localStorage.clear(); + frappe.ui.toolbar.clear_cache(); + }).addClass('btn-primary saveBtn'); } function saveData(frm) { - var table = document.getElementById("data-table").getElementsByTagName('tbody')[0]; - var data = []; - for (var i = 0; i < table.rows.length; i++) { - var row = table.rows.item(i).cells; - var data_row = [] - for (var j = 0; j < row.length; j++) { - var val = row.item(j).innerHTML; - //Remove html tags from primary columns - if (j > 0 && j < 5) { - var div = document.createElement("div"); - div.innerHTML = val; - var text = div.textContent || div.innerText || ""; - data_row.push(text) - } - else { - data_row.push(val) - } - } - if (data_row[1]) { - data.push(data_row) - } - } - var jsonData = JSON.stringify(data); - frappe.call({ - method: 'beams.beams.doctype.budget_tool.budget_tool.save_budget_data', - args: { - 'budget': frm.doc.budget, - 'data': jsonData - }, - freeze: true, - freeze_message: __("Saving..."), - callback: (r) => { - if (r.message) { - frappe.msgprint({ - title: __('Notification'), - indicator: 'green', - message: __('Data updated successfully') - }); - set_budget_html(frm, frm.doc.budget); - } - } - }); + var table = document.getElementById("data-table").getElementsByTagName('tbody')[0]; + var data = []; + for (var i = 0; i < table.rows.length; i++) { + var row = table.rows.item(i).cells; + var data_row = [] + for (var j = 0; j < row.length; j++) { + var val = row.item(j).innerHTML; + //Remove html tags from primary columns + if (j > 0 && j < 5) { + var div = document.createElement("div"); + div.innerHTML = val; + var text = div.textContent || div.innerText || ""; + data_row.push(text) + } + else { + data_row.push(val) + } + } + if (data_row[1]) { + data.push(data_row) + } + } + var jsonData = JSON.stringify(data); + frappe.call({ + method: 'beams.beams.doctype.budget_tool.budget_tool.save_budget_data', + args: { + 'budget': frm.doc.budget, + 'data': jsonData + }, + freeze: true, + freeze_message: __("Saving..."), + callback: (r) => { + if (r.message) { + frappe.msgprint({ + title: __('Notification'), + indicator: 'green', + message: __('Data updated successfully') + }); + set_budget_html(frm, frm.doc.budget); + } + } + }); } frappe.ui.keys.on("ctrl+s", function (frm) { - if (cur_frm.doc.budget) { - if (cur_frm.doc.has_unsaved_changes) { - saveData(cur_frm); - } - } - else { - frappe.show_alert({ - message: __('Nothing to save, Please select Budget'), - indicator: 'red' - }, 5); - } + if (cur_frm.doc.budget) { + if (cur_frm.doc.has_unsaved_changes) { + saveData(cur_frm); + } + } + else { + frappe.show_alert({ + message: __('Nothing to save, Please select Budget'), + indicator: 'red' + }, 5); + } }); frappe.ui.keys.on("ctrl+i", function (frm) { - if (cur_frm.doc.budget) { - show_add_row_popup(cur_frm); - } - else { - frappe.show_alert({ - message: __('Please select Budget'), - indicator: 'red' - }, 5); - } + if (cur_frm.doc.budget) { + show_add_row_popup(cur_frm); + } + else { + frappe.show_alert({ + message: __('Please select Budget'), + indicator: 'red' + }, 5); + } }); function show_add_row_popup(frm) { - let d = new frappe.ui.Dialog({ - title: 'Add Budget Row', - fields: [ - { - label: 'Cost Head', - fieldname: 'cost_head', - fieldtype: 'Link', - options: 'Cost Head', - reqd: 1 - }, - { - label: 'Cost Sub Head', - fieldname: 'cost_subhead', - fieldtype: 'Link', - options: 'Cost Subhead', - get_query: function () { - return { - filters: { - "cost_head": d.get_value('cost_head') - } - }; - }, - reqd: 1 - }, - { - label: 'Cost Category', - fieldname: 'cost_category', - fieldtype: 'Link', - options: 'Cost Category', - reqd: 1 - } - ], - primary_action_label: 'Add', - primary_action(values) { - add_row_primary_action(frm, values); - d.hide(); - } - }); - d.show(); + let d = new frappe.ui.Dialog({ + title: 'Add Budget Row', + fields: [ + { + label: 'Cost Head', + fieldname: 'cost_head', + fieldtype: 'Link', + options: 'Cost Head', + reqd: 1, + onchange: function () { + let cost_head = d.get_value('cost_head'); + if (cost_head) { + frappe.call({ + method: 'beams.beams.doctype.budget_tool.budget_tool.get_cost_head_details', + args: { + 'cost_head': cost_head + }, + callback: (r) => { + if (r.message) { + d.set_value('budget_group', r.message.budget_group); + d.set_value('cost_category', r.message.cost_category); + } + } + }); + } + } + }, + { + label: 'Budget Group', + fieldname: 'budget_group', + fieldtype: 'Link', + options: 'Budget Group', + reqd: 1 + }, + { + label: 'Cost Category', + fieldname: 'cost_category', + fieldtype: 'Link', + options: 'Cost Category', + reqd: 1 + } + ], + primary_action_label: 'Add', + primary_action(values) { + add_row_primary_action(frm, values); + d.hide(); + } + }); + d.show(); } function add_row_primary_action(frm, values) { - if (frm.doc.budget && values.cost_head && values.cost_subhead && values.cost_category) { - frappe.call({ - method: 'beams.beams.doctype.budget_tool.budget_tool.add_budget_row', - args: { - 'budget': frm.doc.budget, - 'cost_head': values.cost_head, - 'cost_subhead': values.cost_subhead, - 'cost_category': values.cost_category, - }, - freeze: true, - freeze_message: __("Adding row..."), - callback: (r) => { - if (r.message) { - frappe.msgprint({ - title: __('Notification'), - indicator: 'green', - message: __('Row added successfully') - }); - set_budget_html(frm, frm.doc.budget); - } - } - }); - } + if (frm.doc.budget && values.cost_head) { + frappe.call({ + method: 'beams.beams.doctype.budget_tool.budget_tool.add_budget_row', + args: { + 'budget': frm.doc.budget, + 'cost_head': values.cost_head + }, + freeze: true, + freeze_message: __("Adding row..."), + callback: (r) => { + if (r.message) { + frappe.msgprint({ + title: __('Notification'), + indicator: 'green', + message: __('Row added successfully') + }); + set_budget_html(frm, frm.doc.budget); + } + } + }); + } } \ No newline at end of file diff --git a/beams/beams/doctype/budget_tool/budget_tool.py b/beams/beams/doctype/budget_tool/budget_tool.py index a3486bc79..b0b4bdadb 100644 --- a/beams/beams/doctype/budget_tool/budget_tool.py +++ b/beams/beams/doctype/budget_tool/budget_tool.py @@ -21,9 +21,9 @@ def get_budget_html(budget): is_editable = 0 if frappe.db.exists('Budget', budget): # Defining Columns - columns = ['Cost Head', 'Cost Sub Head', 'Cost Category', 'Cost Description', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', 'Total Budget'] + columns = ['Cost Head', 'Budget Group', 'Account Head', 'Cost Category', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', 'Total Budget'] - # Building Data + # Building Data data = [] budget_doc = frappe.get_doc('Budget', budget) is_editable = 1 @@ -31,15 +31,15 @@ def get_budget_html(budget): if budget_doc.docstatus: is_editable = 0 values_read_only = 1 - for row in budget_doc.accounts: + for row in budget_doc.budget_accounts: budget_row = get_budget_item_details(row.name, values_read_only) data.append(budget_row) total_row = '' html_data = frappe.render_template('beams/doctype/budget_tool/budget_tool.html', { - 'columns': columns, - 'data': data, - 'total_row': total_row - }) + 'columns': columns, + 'data': data, + 'total_row': total_row + }) return { 'html':html_data, 'is_editable': is_editable @@ -47,16 +47,16 @@ def get_budget_html(budget): def get_budget_item_details(row_id, read_only=0): ''' - Method to get Budget Account Row Details + Method to get Budget Account Row Details ''' data = [] - if frappe.db.exists('Budget Account', row_id): - row_detail = frappe.get_doc('Budget Account', row_id) + if frappe.db.exists('M1 Budget Account', row_id): + row_detail = frappe.get_doc('M1 Budget Account', row_id) # Set Master Links data.append({ 'type':'text', 'value': row_detail.cost_head, 'read_only':1, 'primary': 1, 'ref_link': get_absolute_url('Cost Head', row_detail.cost_head) }) - data.append({ 'type':'text', 'value': row_detail.cost_subhead, 'read_only':1, 'primary': 1, 'ref_link': get_absolute_url('Cost Subhead', row_detail.cost_subhead) }) + data.append({ 'type':'text', 'value': row_detail.budget_group, 'read_only':1, 'primary': 1, 'ref_link': get_absolute_url('Budget Group', row_detail.budget_group) }) + data.append({ 'type':'text', 'value': row_detail.account, 'read_only':1, 'primary': 1, 'ref_link': get_absolute_url('Account', row_detail.account) }) data.append({ 'type':'text', 'value': row_detail.cost_category, 'read_only':1, 'primary': 1, 'ref_link': get_absolute_url('Cost Category', row_detail.cost_category) }) - data.append({ 'type':'text', 'value': row_detail.cost_description or '', 'read_only':read_only, 'class_name':'budget_notes' }) # Monthly Distribution for field_name in month_fields: data.append({ 'type':'number', 'value': int(row_detail.get(field_name)), 'read_only':read_only, 'class_name':'text-right month_input'}) @@ -69,11 +69,10 @@ def save_budget_data(budget, data): budget_doc = frappe.get_doc('Budget', budget) data = json.loads(data) row_idx = 0 - for budget_row in budget_doc.accounts: + # Update Budget Rows + for budget_row in budget_doc.budget_accounts: month_idx = 5 budget_total = 0 - cost_description = data[row_idx][4] or '' - budget_row.cost_description = cost_description for month in month_fields: value = 0 try: @@ -90,20 +89,36 @@ def save_budget_data(budget, data): return 1 @frappe.whitelist() -def add_budget_row(budget, cost_head, cost_subhead, cost_category): +def add_budget_row(budget, cost_head): ''' - Method to add a row in Budget + Method to add a row in Budget ''' if frappe.db.exists('Budget', budget): budget_doc = frappe.get_doc('Budget', budget) - budget_row = budget_doc.append('accounts') + budget_row = budget_doc.append('budget_accounts') if frappe.db.exists('Cost Head', cost_head): - budget_row.cost_head = cost_head - if frappe.db.exists('Cost Subhead', cost_subhead): - budget_row.cost_subhead = cost_subhead - budget_row.account = frappe.get_value('Cost Subhead', cost_subhead, 'account') - if frappe.db.exists('Cost Category', cost_category): - budget_row.cost_category = cost_category + cost_head_doc = frappe.get_doc('Cost Head', cost_head) + budget_row.cost_head = cost_head_doc.name + budget_row.budget_group = cost_head_doc.budget_group + budget_row.cost_category = cost_head_doc.cost_category + for account in cost_head_doc.accounts: + if account.company == budget_doc.company: + budget_row.account = account.default_account + break budget_doc.flags.ignore_mandatory = 1 budget_doc.save(ignore_permissions=True) - return 1 \ No newline at end of file + return 1 + +@frappe.whitelist() +def get_cost_head_details(cost_head): + ''' + Method to get Cost Head Details + ''' + if frappe.db.exists('Cost Head', cost_head): + cost_head_doc = frappe.get_doc('Cost Head', cost_head) + return { + 'cost_head': cost_head_doc.name, + 'budget_group': cost_head_doc.budget_group, + 'cost_category': cost_head_doc.cost_category + } + return {} \ No newline at end of file diff --git a/beams/beams/doctype/cost_category/cost_category.json b/beams/beams/doctype/cost_category/cost_category.json index 4fa54aea8..8eb6f294c 100644 --- a/beams/beams/doctype/cost_category/cost_category.json +++ b/beams/beams/doctype/cost_category/cost_category.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_rename": 1, "autoname": "field:cost_category", "creation": "2024-10-17 17:15:46.279393", "doctype": "DocType", @@ -12,13 +11,17 @@ { "fieldname": "cost_category", "fieldtype": "Data", + "in_list_view": 1, "label": "Cost Category", + "no_copy": 1, + "reqd": 1, "unique": 1 } ], + "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-10-17 17:17:26.219451", + "modified": "2026-02-03 14:13:41.038383", "modified_by": "Administrator", "module": "BEAMS", "name": "Cost Category", @@ -38,6 +41,7 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [] diff --git a/beams/beams/doctype/cost_head/cost_head.js b/beams/beams/doctype/cost_head/cost_head.js index c8decac30..1ef48d487 100644 --- a/beams/beams/doctype/cost_head/cost_head.js +++ b/beams/beams/doctype/cost_head/cost_head.js @@ -1,8 +1,22 @@ // Copyright (c) 2025, efeone and contributors // For license information, please see license.txt -// frappe.ui.form.on("Cost Head", { -// refresh(frm) { +frappe.ui.form.on("Cost Head", { + refresh(frm) { + set_filters(frm); + }, +}); -// }, -// }); +function set_filters(frm) { + frm.set_query('default_account', 'accounts', (frm, cdt, cdn) => { + const row = locals[cdt][cdn]; + return { + filters: { + is_group: 0, + disabled: 0, + report_type: 'Profit and Loss', + company: row.company + } + } + }) +} \ No newline at end of file diff --git a/beams/beams/doctype/cost_head/cost_head.json b/beams/beams/doctype/cost_head/cost_head.json index eb76e2059..5eec9e13b 100644 --- a/beams/beams/doctype/cost_head/cost_head.json +++ b/beams/beams/doctype/cost_head/cost_head.json @@ -1,32 +1,70 @@ { "actions": [], + "allow_import": 1, "allow_rename": 1, - "autoname": "field:cost_head_name", - "creation": "2025-01-27 10:25:39.939893", + "autoname": "field:cost_head", + "creation": "2024-10-17 14:25:11.465299", "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "cost_head_name", - "description" + "section_break_9os0", + "cost_head", + "budget_group", + "column_break_llbo", + "cost_category", + "accounts_section", + "accounts" ], "fields": [ { - "fieldname": "cost_head_name", + "fieldname": "section_break_9os0", + "fieldtype": "Section Break" + }, + { + "fieldname": "cost_head", "fieldtype": "Data", - "in_list_view": 1, - "label": "Cost Head Name", + "label": "Cost Head", + "no_copy": 1, "reqd": 1, "unique": 1 }, { - "fieldname": "description", - "fieldtype": "Small Text", - "label": "Description" + "fieldname": "budget_group", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Budget Group", + "options": "Budget Group", + "reqd": 1 + }, + { + "fieldname": "column_break_llbo", + "fieldtype": "Column Break" + }, + { + "fieldname": "cost_category", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Cost Category", + "options": "Cost Category", + "reqd": 1 + }, + { + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Accounts", + "reqd": 1 + }, + { + "fieldname": "accounts_section", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-01-27 10:26:14.849066", + "modified": "2026-02-03 15:26:04.052315", "modified_by": "Administrator", "module": "BEAMS", "name": "Cost Head", @@ -46,6 +84,7 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [] diff --git a/beams/beams/doctype/cost_subhead/cost_subhead.js b/beams/beams/doctype/cost_subhead/cost_subhead.js deleted file mode 100644 index 864249d5c..000000000 --- a/beams/beams/doctype/cost_subhead/cost_subhead.js +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2024, efeone and contributors -// For license information, please see license.txt - -frappe.ui.form.on("Cost Subhead", { - refresh(frm) { - frm.fields_dict.accounts.grid.get_field("default_account").get_query = function(doc, cdt, cdn) { - let row = locals[cdt][cdn]; - return { - filters: { - is_group: 0, - company: row.company - } - }; - }; - frm.add_custom_button(__('Update Budget Template'), function() { - frappe.call({ - method: "beams.beams.doctype.cost_subhead.cost_subhead.update_budget_templates", - args: { - cost_subhead: frm.doc.name - }, - callback: function(response) { - if (response.message) { - frappe.msgprint(__('Budget Templates updated successfully')); - } - } - }); - }); - } -}); diff --git a/beams/beams/doctype/cost_subhead/cost_subhead.json b/beams/beams/doctype/cost_subhead/cost_subhead.json deleted file mode 100644 index 18d889aeb..000000000 --- a/beams/beams/doctype/cost_subhead/cost_subhead.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "field:cost_subhead", - "creation": "2024-10-17 14:25:11.465299", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "section_break_9os0", - "cost_subhead", - "cost_head", - "accounts" - ], - "fields": [ - { - "fieldname": "section_break_9os0", - "fieldtype": "Section Break" - }, - { - "fieldname": "cost_subhead", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Cost Subhead", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "cost_head", - "fieldtype": "Link", - "label": "Cost Head", - "options": "Cost Head", - "reqd": 1 - }, - { - "fieldname": "accounts", - "fieldtype": "Table", - "label": "Accounts", - "options": "Accounts", - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2025-03-03 09:24:01.050075", - "modified_by": "Administrator", - "module": "BEAMS", - "name": "Cost Subhead", - "naming_rule": "By fieldname", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/beams/beams/doctype/cost_subhead/cost_subhead.py b/beams/beams/doctype/cost_subhead/cost_subhead.py deleted file mode 100644 index a9f55a8ad..000000000 --- a/beams/beams/doctype/cost_subhead/cost_subhead.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2024, efeone and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document - - -class CostSubhead(Document): - pass - -@frappe.whitelist() -def update_budget_templates(cost_subhead): - cost_subhead_doc = frappe.get_doc("Cost Subhead", cost_subhead) - - budget_templates = frappe.get_all("Budget Template Item", - filters={"cost_sub_head": cost_subhead}, fields=["parent"]) - - for budget in {bt["parent"] for bt in budget_templates}: - budget_doc = frappe.get_doc("Budget Template", budget) - - for item in budget_doc.get("budget_template_item", []): - if item.cost_sub_head == cost_subhead: - item.account = next( - (acc.default_account for acc in cost_subhead_doc.accounts if acc.company == budget_doc.company), - item.account - ) - - budget_doc.save() - - return "Success" diff --git a/beams/beams/doctype/cost_subhead/test_cost_subhead.py b/beams/beams/doctype/cost_subhead/test_cost_subhead.py deleted file mode 100644 index 4ffbd465c..000000000 --- a/beams/beams/doctype/cost_subhead/test_cost_subhead.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2024, efeone and Contributors -# See license.txt - -# import frappe -from frappe.tests.utils import FrappeTestCase - - -class TestCostSubhead(FrappeTestCase): - pass diff --git a/beams/beams/doctype/finance_group/__init__.py b/beams/beams/doctype/m1_budget_account/__init__.py similarity index 100% rename from beams/beams/doctype/finance_group/__init__.py rename to beams/beams/doctype/m1_budget_account/__init__.py diff --git a/beams/beams/doctype/m1_budget_account/m1_budget_account.json b/beams/beams/doctype/m1_budget_account/m1_budget_account.json new file mode 100644 index 000000000..6150b4ac5 --- /dev/null +++ b/beams/beams/doctype/m1_budget_account/m1_budget_account.json @@ -0,0 +1,315 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-01-30 16:52:46.142596", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "cost_head", + "budget_group", + "account", + "cost_category", + "column_break_ndgp", + "cost_description", + "equal_monthly_distribution", + "budget_amount", + "monthly_amount_distribution_section", + "january", + "february", + "march", + "april", + "column_break_qqgq", + "may", + "june", + "july", + "august", + "column_break_iiwj", + "september", + "october", + "november", + "december", + "monthly_amount_distribution_inr_section", + "january_inr", + "february_inr", + "march_inr", + "april_inr", + "column_break_ovgi", + "may_inr", + "june_inr", + "july_inr", + "august_inr", + "column_break_enqc", + "september_inr", + "october_inr", + "november_inr", + "december_inr", + "budget_amount_inr" + ], + "fields": [ + { + "fieldname": "cost_head", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Cost Head", + "options": "Cost Head", + "reqd": 1 + }, + { + "fetch_from": "cost_head.budget_group", + "fieldname": "budget_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Budget Group", + "options": "Budget Group", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Account", + "options": "Account", + "read_only": 1, + "reqd": 1 + }, + { + "fetch_from": "cost_head.cost_category", + "fieldname": "cost_category", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Cost Category", + "options": "Cost Category", + "read_only": 1 + }, + { + "fieldname": "column_break_ndgp", + "fieldtype": "Column Break" + }, + { + "fieldname": "cost_description", + "fieldtype": "Small Text", + "label": "Cost Description" + }, + { + "default": "0", + "fieldname": "equal_monthly_distribution", + "fieldtype": "Check", + "label": "Equal Monthly Distribution " + }, + { + "default": "0", + "fieldname": "budget_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Budget Amount", + "reqd": 1 + }, + { + "fieldname": "monthly_amount_distribution_section", + "fieldtype": "Section Break", + "label": "Monthly Amount Distribution" + }, + { + "default": "0", + "fieldname": "january", + "fieldtype": "Currency", + "label": "January" + }, + { + "default": "0", + "fieldname": "february", + "fieldtype": "Currency", + "label": "February" + }, + { + "default": "0", + "fieldname": "march", + "fieldtype": "Currency", + "label": "March" + }, + { + "default": "0", + "fieldname": "april", + "fieldtype": "Currency", + "label": "April" + }, + { + "fieldname": "column_break_qqgq", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "may", + "fieldtype": "Currency", + "label": "May" + }, + { + "default": "0", + "fieldname": "june", + "fieldtype": "Currency", + "label": "June" + }, + { + "default": "0", + "fieldname": "july", + "fieldtype": "Currency", + "label": "July" + }, + { + "default": "0", + "fieldname": "august", + "fieldtype": "Currency", + "label": "August" + }, + { + "fieldname": "column_break_iiwj", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "september", + "fieldtype": "Currency", + "label": "September" + }, + { + "default": "0", + "fieldname": "october", + "fieldtype": "Currency", + "label": "October" + }, + { + "default": "0", + "fieldname": "november", + "fieldtype": "Currency", + "label": "November" + }, + { + "default": "0", + "fieldname": "december", + "fieldtype": "Currency", + "label": "December" + }, + { + "collapsible": 1, + "fieldname": "monthly_amount_distribution_inr_section", + "fieldtype": "Section Break", + "label": "Monthly Amount Distribution (INR)" + }, + { + "default": "0", + "fieldname": "january_inr", + "fieldtype": "Currency", + "label": "January (INR)", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "february_inr", + "fieldtype": "Currency", + "label": "February (INR)", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "march_inr", + "fieldtype": "Currency", + "label": "March (INR)", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "april_inr", + "fieldtype": "Currency", + "label": "April (INR)", + "read_only": 1 + }, + { + "fieldname": "column_break_ovgi", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "may_inr", + "fieldtype": "Currency", + "label": "May (INR)", + "read_only": 1 + }, + { + "fieldname": "column_break_enqc", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "september_inr", + "fieldtype": "Currency", + "label": "September (INR)", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "october_inr", + "fieldtype": "Currency", + "label": "October (INR)", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "november_inr", + "fieldtype": "Currency", + "label": "November (INR)", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "december_inr", + "fieldtype": "Currency", + "label": "December (INR)", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "budget_amount_inr", + "fieldtype": "Currency", + "label": "Budget Amount (INR)", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "june_inr", + "fieldtype": "Currency", + "label": "June (INR)", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "july_inr", + "fieldtype": "Currency", + "label": "July (INR)", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "august_inr", + "fieldtype": "Currency", + "label": "August (INR)", + "read_only": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-02-03 16:48:05.678640", + "modified_by": "Administrator", + "module": "BEAMS", + "name": "M1 Budget Account", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/beams/beams/doctype/m1_budget_account/m1_budget_account.py b/beams/beams/doctype/m1_budget_account/m1_budget_account.py new file mode 100644 index 000000000..5849b0730 --- /dev/null +++ b/beams/beams/doctype/m1_budget_account/m1_budget_account.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, efeone and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class M1BudgetAccount(Document): + pass diff --git a/beams/beams/utils.py b/beams/beams/utils.py new file mode 100644 index 000000000..009c09708 --- /dev/null +++ b/beams/beams/utils.py @@ -0,0 +1,8 @@ +import frappe + +@frappe.whitelist() +def get_default_account_of_cost_head(cost_head, company): + """ + Method to get default account of cost head + """ + return frappe.db.get_value("Accounts", {"parent": cost_head, "company": company}, "default_account") diff --git a/beams/fixtures/cost_category.json b/beams/fixtures/cost_category.json new file mode 100644 index 000000000..a02d9ef2c --- /dev/null +++ b/beams/fixtures/cost_category.json @@ -0,0 +1,23 @@ +[ + { + "cost_category": "Operational Expenses", + "docstatus": 0, + "doctype": "Cost Category", + "modified": "2026-01-30 12:41:34.900178", + "name": "Operational Expenses" + }, + { + "cost_category": "HR Overheads", + "docstatus": 0, + "doctype": "Cost Category", + "modified": "2026-01-30 12:42:17.934486", + "name": "HR Overheads" + }, + { + "cost_category": "Capital Expenses", + "docstatus": 0, + "doctype": "Cost Category", + "modified": "2026-02-03 14:13:33.137732", + "name": "Capital Expenses" + } +] \ No newline at end of file diff --git a/beams/hooks.py b/beams/hooks.py index a5963222b..e86069bd1 100644 --- a/beams/hooks.py +++ b/beams/hooks.py @@ -76,8 +76,8 @@ "Employment Type":"beams/custom_scripts/employment_type/employment_type.js", "HD Team":"beams/custom_scripts/hd_team/hd_team.js", "Item":"beams/custom_scripts/item/item.js", - "Journal Entry":"beams/custom_scripts/journal_entry/journal_entry.js", - "Expense Claim":"beams/custom_scripts/expense_claim/expense_claim.js", + "Journal Entry":"beams/custom_scripts/journal_entry/journal_entry.js", + "Expense Claim":"beams/custom_scripts/expense_claim/expense_claim.js", } doctype_list_js = { @@ -230,7 +230,7 @@ "before_save":[ "beams.beams.custom_scripts.purchase_order.purchase_order.validate_budget", "beams.beams.custom_scripts.material_request.material_request.set_checkbox_for_item_type" - ], + ], "after_insert":"beams.beams.custom_scripts.material_request.material_request.notify_stock_managers", "on_update": "beams.beams.custom_scripts.material_request.material_request.create_todo_for_hod", "validate": "beams.beams.custom_scripts.material_request.material_request.validate" @@ -490,7 +490,7 @@ # ------------------------------ # override_whitelisted_methods = { - "erpnext.buying.doctype.supplier_quotation.supplier_quotation.make_purchase_order":"beams.beams.custom_scripts.supplier_quotation.supplier_quotation.make_purchase_order_from_supplier_quotation", + "erpnext.buying.doctype.supplier_quotation.supplier_quotation.make_purchase_order":"beams.beams.custom_scripts.supplier_quotation.supplier_quotation.make_purchase_order_from_supplier_quotation", } # # each overriding function accepts a `data` argument; @@ -598,5 +598,8 @@ ], ] ], - } + }, + { + "dt": "Cost Category", + }, ] diff --git a/beams/setup.py b/beams/setup.py index fcff6eb68..93d909352 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -866,13 +866,6 @@ def get_department_custom_fields(): "label": "Threshold Amount", "insert_after": "parent_department" }, - { - "fieldname": "finance_group", - "fieldtype": "Link", - "label": "Finance Group", - "options":"Finance Group", - "insert_after": "company" - } ] } @@ -1109,15 +1102,6 @@ def get_budget_custom_fields(): "reqd": 1, "insert_after": "company" }, - { - "fieldname": "finance_group", - "fieldtype": "Link", - "label": "Finance Group", - "options":"Finance Group", - "insert_after": "department", - "read_only":1, - "fetch_from": "department.finance_group" - }, { "fieldname": "division", "fieldtype": "Link", @@ -1140,14 +1124,6 @@ def get_budget_custom_fields(): "options":"Budget Template", "insert_after": "fiscal_year" }, - { - "fieldname": "rejection_feedback", - "fieldtype": "Table", - "label": "Rejection Feedback", - "options":"Rejection Feedback", - "insert_after": "december", - "depends_on": "eval: doc.workflow_state == 'Rejected'" - }, { "fieldname": "total_amount", "fieldtype": "Currency", @@ -1157,18 +1133,11 @@ def get_budget_custom_fields(): "options": "company_currency" }, { - "fieldname": "budget_accounts_custom", + "fieldname": "budget_accounts", "fieldtype": "Table", "label": "Budget Accounts", - "options": "Budget Account", - "insert_after": "accounts" - }, - { - "fieldname": "budget_accounts_hr", - "fieldtype": "Table", - "label": "Budget Accounts(HR Overheads)", - "options": "Budget Account", - "insert_after": "budget_accounts_custom" + "options": "M1 Budget Account", + "insert_after": "accounts", }, { "fieldname": "default_currency", @@ -1177,7 +1146,7 @@ def get_budget_custom_fields(): "options": "Currency", "read_only": 1, "hidden":1, - "insert_after": "budget_accounts_hr", + "insert_after": "budget_accounts", "default": "INR" }, { @@ -1190,50 +1159,16 @@ def get_budget_custom_fields(): "insert_after": "default_currency", "fetch_from": "company.default_currency" }, - ], - "Budget Account": [ - { - "fieldname": "cost_head", - "fieldtype": "Link", - "label": "Cost Head", - "options":"Cost Head", - "insert_before": "cost_subhead", - "in_list_view":1 - }, { - "fieldname": "cost_subhead", - "fieldtype": "Link", - "label": "Cost Sub Head", - "options":"Cost Subhead", - "insert_after": "cost_head", - "in_list_view":1 - }, - { - "fieldname": "cost_category", - "fieldtype": "Link", - "label": "Cost Category", - "options":"Cost Category", - "insert_after": "account", - "in_list_view":1 - }, - { - "fieldname": "column_break_cd", - "fieldtype": "Column Break", - "label": " ", - "insert_after": "cost_category" - }, - { - "fieldname": "cost_description", - "fieldtype": "Small Text", - "label": "Cost Description", - "insert_after": "column_break_cd" - }, - { - "fieldname": "equal_monthly_distribution", - "fieldtype": "Check", - "label": "Equal Monthly Distribution ", - "insert_after": "cost_description" + "fieldname": "rejection_feedback", + "fieldtype": "Table", + "label": "Rejection Feedback", + "options":"Rejection Feedback", + "insert_after": "company_currency", + "read_only":1 }, + ], + "Budget Account": [ { "fieldname": "section_break_ab", "fieldtype": "Section Break", @@ -4851,13 +4786,6 @@ def get_property_setters(): "property_type": "Link", "value":1 }, - { - "doctype_or_field": "DocField", - "doc_type": "Budget", - "field_name": "accounts", - "property": "hidden", - "value":1 - }, { "doctype_or_field": "DocField", "doc_type": "Budget", From a771da99aa1f35d78aec2edb913c6c8e69cf26a6 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Wed, 4 Feb 2026 15:54:05 +0530 Subject: [PATCH 20/50] chore: patch to delete existing fields --- beams/patches.txt | 2 +- beams/patches/delete_custom_fields.py | 44 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/beams/patches.txt b/beams/patches.txt index f5c2581d6..75675ba81 100644 --- a/beams/patches.txt +++ b/beams/patches.txt @@ -2,7 +2,7 @@ beams.patches.rename_hod_role #30-10-2024 beams.patches.no_of_children_patch #06-03-2025 beams.patches.delete_property_setter #21-11-2025 -beams.patches.delete_custom_fields #18-12-2025 +beams.patches.delete_custom_fields #04-02-2026 [post_model_sync] # Patches added in this section will be executed after doctypes are migrated diff --git a/beams/patches/delete_custom_fields.py b/beams/patches/delete_custom_fields.py index fba1b2be9..4d26e9275 100644 --- a/beams/patches/delete_custom_fields.py +++ b/beams/patches/delete_custom_fields.py @@ -225,6 +225,50 @@ 'dt':'HD Settings', 'fieldname': 'escalation_notifications_templates' }, + { + 'dt':'Department', + 'fieldname': 'finance_group' + }, + { + 'dt':'Budget', + 'fieldname': 'finance_group' + }, + { + 'dt':'Budget', + 'fieldname': 'rejection_feedback' + }, + { + 'dt':'Budget', + 'fieldname': 'budget_accounts_custom' + }, + { + 'dt':'Budget', + 'fieldname': 'budget_accounts_hr' + }, + { + 'dt':'Budget Account', + 'fieldname': 'cost_head' + }, + { + 'dt':'Budget Account', + 'fieldname': 'cost_subhead' + }, + { + 'dt':'Budget Account', + 'fieldname': 'cost_category' + }, + { + 'dt':'Budget Account', + 'fieldname': 'column_break_cd' + }, + { + 'dt':'Budget Account', + 'fieldname': 'cost_description' + }, + { + 'dt':'Budget Account', + 'fieldname': 'equal_monthly_distribution' + } ] def execute(): From 815a1ea1ddd75c65583e348d4650c3d684d6f500 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Wed, 4 Feb 2026 17:28:44 +0530 Subject: [PATCH 21/50] fix: Undefined error on Budget override --- beams/__init__.py | 6 +++--- beams/beams/overrides/budget.py | 17 +---------------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/beams/__init__.py b/beams/__init__.py index eb94bdec9..7829046c8 100644 --- a/beams/__init__.py +++ b/beams/__init__.py @@ -25,16 +25,16 @@ # You’ve been warned. # ----------------------------------------------------------------------------- -# from erpnext.accounts.doctype.budget import budget +from erpnext.accounts.doctype.budget import budget from helpdesk.helpdesk.doctype.hd_ticket import hd_ticket from beams.beams.custom_scripts.hd_ticket.hd_ticket import ( get_permission_query_conditions, has_permission, ) -# from beams.beams.overrides.budget import validate_expense_against_budget +from beams.beams.overrides.budget import validate_expense_against_budget -# budget.validate_expense_against_budget = validate_expense_against_budget +budget.validate_expense_against_budget = validate_expense_against_budget hd_ticket.permission_query = get_permission_query_conditions hd_ticket.has_permission = has_permission diff --git a/beams/beams/overrides/budget.py b/beams/beams/overrides/budget.py index cad0e321f..05f13a1d0 100644 --- a/beams/beams/overrides/budget.py +++ b/beams/beams/overrides/budget.py @@ -16,19 +16,15 @@ def validate_expense_against_budget(args, expense_amount=0, for_check=0): if not frappe.get_all("Budget", limit=1): return - if args.get("company") and not args.fiscal_year: args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0] frappe.flags.exception_approver_role = frappe.get_cached_value( "Company", args.get("company"), "exception_budget_approver_role" ) - if not frappe.get_cached_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): # nosec return - - if not args.account: args.account = args.get("expense_account") @@ -38,7 +34,6 @@ def validate_expense_against_budget(args, expense_amount=0, for_check=0): if not args.account: return - default_dimensions = [ { "fieldname": "project", @@ -97,7 +92,6 @@ def validate_expense_against_budget(args, expense_amount=0, for_check=0): as_dict=True, ) # nosec - if budget_records: validate_budget_records(args, budget_records, expense_amount, for_check) @@ -136,7 +130,6 @@ def validate_budget_records(args, budget_records, expense_amount, for_check): for_check ) - def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0, for_check=0): args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0 if not amount: @@ -150,7 +143,6 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_ total_expense = args.actual_expense + amount - if total_expense > budget_amount: if args.actual_expense > budget_amount: error_tense = _("is already") @@ -186,7 +178,7 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_ args.get("object").budget_exceeded = 1 else: if action == "Stop": - frappe.throw(msg, BudgetError, title=_("Budget Exceeded")) + frappe.throw(msg, title=_("Budget Exceeded")) else: frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded")) else: @@ -197,9 +189,6 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_ elif args.get("doctype") == "Material Request" and args.for_material_request: args.get("object").budget_exceeded = 0 - - - def get_expense_breakup(args, currency, budget_against): msg = "
Total Expenses booked through -
    " @@ -291,7 +280,6 @@ def get_actions(args, budget): return yearly_action, monthly_action - def get_requested_amount(args): item_code = args.get("item_code") condition = get_other_condition(args, "Material Request") @@ -335,7 +323,6 @@ def get_ordered_amount(args, for_check): data[0][0] += unsubmitted_ordered_amount return data[0][0] if data else 0 - def get_other_condition(args, for_doc): condition = "expense_account = '%s'" % (args.expense_account) budget_against_field = args.get("budget_against_field") @@ -406,7 +393,6 @@ def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_ye ] dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date") - accumulated_percentage = 0.0 accummulated_budget = 0 @@ -447,7 +433,6 @@ def get_item_details(args): return cost_center, expense_account - def get_expense_cost_center(doctype, args): if doctype == "Item Group": return frappe.db.get_value( From 3bc8fc4866a7bb9c9bd27f0645f280b0a5d7a3f2 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Thu, 5 Feb 2026 09:36:06 +0530 Subject: [PATCH 22/50] chore: Filter applied for account in cost head --- beams/beams/doctype/cost_head/cost_head.js | 1 + 1 file changed, 1 insertion(+) diff --git a/beams/beams/doctype/cost_head/cost_head.js b/beams/beams/doctype/cost_head/cost_head.js index 1ef48d487..9e531670e 100644 --- a/beams/beams/doctype/cost_head/cost_head.js +++ b/beams/beams/doctype/cost_head/cost_head.js @@ -15,6 +15,7 @@ function set_filters(frm) { is_group: 0, disabled: 0, report_type: 'Profit and Loss', + root_type: 'Expense', company: row.company } } From 699957945b7342ef9c467591db2317dbed4702c4 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Thu, 5 Feb 2026 12:08:14 +0530 Subject: [PATCH 23/50] feat: customize budget and template with filters --- beams/beams/custom_scripts/budget/budget.js | 267 ++++++++++-------- beams/beams/custom_scripts/budget/budget.py | 17 +- .../budget_template/budget_template.js | 49 +++- .../budget_template/budget_template.py | 47 ++- beams/setup.py | 69 +++++ 5 files changed, 303 insertions(+), 146 deletions(-) diff --git a/beams/beams/custom_scripts/budget/budget.js b/beams/beams/custom_scripts/budget/budget.js index 98e7a71ae..46785c3f8 100644 --- a/beams/beams/custom_scripts/budget/budget.js +++ b/beams/beams/custom_scripts/budget/budget.js @@ -1,135 +1,152 @@ frappe.ui.form.on('Budget', { - onload: function (frm) { - hide_main_tables(frm); - }, - refresh: function (frm) { - hide_main_tables(frm); - set_filters(frm); - if (!frm.is_new()) { - frm.add_custom_button('Open Budget Tool', () => { - frappe.set_route('Form', 'Budget Tool', 'Budget Tool'); - }); - } - }, - department: function (frm) { - set_filters(frm); - if (!frm.doc.department) { - frm.set_value('division', null); - } - }, - company: function (frm) { - frm.set_value('department', null); - }, - budget_template: function (frm) { - if (!frm.doc.budget_template) { - frm.set_value('cost_center', null); - frm.set_value('region', null); - frm.clear_table('budget_accounts'); - frm.refresh_field('budget_accounts'); - frm.clear_table('accounts'); - frm.refresh_field('accounts'); - return; - } + onload: function (frm) { + hide_main_tables(frm); + }, - if (frm.doc.budget_template === frm._previous_budget_template) { - return; - } + refresh: function (frm) { + hide_main_tables(frm); + set_filters(frm); - let previous_template = frm.doc.__last_value || frm._previous_budget_template; - - frappe.confirm( - __('Are you sure you want to change the Budget Template? This will reset existing budget data.'), - function () { - frm.clear_table('budget_accounts'); - frm.clear_table('accounts'); - frm.refresh_field('accounts'); - - frappe.call({ - method: 'frappe.client.get', - args: { - doctype: 'Budget Template', - name: frm.doc.budget_template - }, - callback: function (response) { - if (response.message) { - let budget_template = response.message; - frm.set_value('cost_center', budget_template.cost_center); - frm.set_value('region', budget_template.region); - - let budget_template_items = budget_template.budget_template_items || []; - let accountMap = {}; - budget_template_items.forEach(function (item) { - let row1 = frm.add_child('budget_accounts'); - row1.cost_head = item.cost_head; - row1.budget_group = item.budget_group; - row1.account = item.account_head; - row1.cost_category = item.cost_category; - row1.budget_amount = 0 - - if (!accountMap[item.account_head]) { - accountMap[item.account_head] = { - account: item.account_head, - budget_amount: 0 - }; - } - - // Add amount (use item.budget_amount if available, else 0) - accountMap[item.account_head].budget_amount += flt(item.budget_amount || 0); - }); - frm.refresh_field('budget_accounts'); - - // Update sum to accounts table - Object.values(accountMap).forEach(data => { - let row = frm.add_child('accounts'); - row.account = data.account; - row.budget_amount = data.budget_amount; - }); - frm.refresh_field('accounts'); - } - } - }); - - frm._previous_budget_template = frm.doc.budget_template; - }, - function () { - frm.set_value('budget_template', previous_template); - } - ); - } + if (!frm.is_new()) { + frm.add_custom_button('Open Budget Tool', () => { + frappe.set_route('Form', 'Budget Tool', 'Budget Tool'); + }); + } + }, + + department: function (frm) { + set_filters(frm); + if (!frm.doc.department) { + frm.set_value('division', null); + } + }, + + company: function (frm) { + frm.set_value('department', null); + }, + + budget_template: function (frm) { + // If cleared + if (!frm.doc.budget_template) { + frm.set_value('cost_center', null); + frm.set_value('region', null); + + frm.clear_table('budget_accounts'); + frm.clear_table('accounts'); + + frm.refresh_field('budget_accounts'); + frm.refresh_field('accounts'); + return; + } + + // Prevent re-trigger + if (frm.doc.budget_template === frm._previous_budget_template) { + return; + } + + let previous_template = frm._previous_budget_template; + + frappe.confirm( + __('Are you sure you want to change the Budget Template? This will reset existing budget data.'), + function () { + frm.clear_table('budget_accounts'); + frm.clear_table('accounts'); + frm.refresh_field('accounts'); + + frappe.call({ + method: 'frappe.client.get', + args: { + doctype: 'Budget Template', + name: frm.doc.budget_template + }, + callback: function (response) { + if (!response.message) return; + + let budget_template = response.message; + frm.set_value('cost_center', budget_template.cost_center); + frm.set_value('region', budget_template.region); + + let items = budget_template.budget_template_items || []; + let accountMap = {}; + + items.forEach(item => { + let row = frm.add_child('budget_accounts'); + row.cost_head = item.cost_head; + row.budget_group = item.budget_group; + row.account = item.account_head; + row.cost_category = item.cost_category; + row.budget_amount = 0; + + if (!accountMap[item.account_head]) { + accountMap[item.account_head] = { + account: item.account_head, + budget_amount: 0 + }; + } + + accountMap[item.account_head].budget_amount += flt(item.budget_amount || 0); + }); + + frm.refresh_field('budget_accounts'); + + Object.values(accountMap).forEach(data => { + let row = frm.add_child('accounts'); + row.account = data.account; + row.budget_amount = data.budget_amount; + }); + + frm.refresh_field('accounts'); + } + }); + + frm._previous_budget_template = frm.doc.budget_template; + }, + function () { + frm.set_value('budget_template', previous_template); + } + ); + }, + + budget_for: function (frm) { + if (frm.doc.budget_for) { + frm.set_value("budget_against", frm.doc.budget_for); + } + } }); // Function to apply filters in the cost subhead field in Budget Account function set_filters(frm) { - frm.set_query('division', function () { - return { - filters: { - department: frm.doc.department, - company: frm.doc.company - } - }; - }); - frm.set_query('budget_template', function () { - return { - filters: { - division: frm.doc.division, - company: frm.doc.company - } - }; - }); - frm.set_query('department', function () { - return { - filters: { - company: frm.doc.company - } - }; - }); - frm.set_query('region', function () { - return { - filters: { - company: frm.doc.company - } - }; - }); + frm.set_query('division', function () { + return { + filters: { + department: frm.doc.department, + company: frm.doc.company + } + }; + }); + frm.set_query('budget_template', function () { + return { + filters: { + division: frm.doc.division, + company: frm.doc.company, + cost_center: frm.doc.cost_center + } + }; + }); + frm.set_query('department', function () { + return { + filters: { + company: frm.doc.company + } + }; + }); + frm.set_query('region', function () { + return { + filters: { + company: frm.doc.company + } + }; + }); } frappe.ui.form.on('M1 Budget Account', { diff --git a/beams/beams/custom_scripts/budget/budget.py b/beams/beams/custom_scripts/budget/budget.py index b0c9e399b..983f00661 100644 --- a/beams/beams/custom_scripts/budget/budget.py +++ b/beams/beams/custom_scripts/budget/budget.py @@ -3,11 +3,11 @@ month_fields = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] def beams_budget_validate(doc, method=None): - """ - method runs custom validations for budget doctype - """ - update_total_amount(doc, method) - convert_currency(doc, method) + """method runs custom validations for budget doctype""" + update_total_amount(doc, method) + convert_currency(doc, method) + update_budget_against(doc, method) + def update_total_amount(doc, method): total = sum([row.budget_amount for row in doc.get("accounts") if row.budget_amount]) @@ -70,3 +70,10 @@ def apply_conversion(row): for row in (*doc.accounts, *doc.budget_accounts): apply_conversion(row) + + +def update_budget_against(doc, method=None): + """Set budget_against field based on budget_for selection""" + if doc.budget_for: + doc.budget_against = doc.budget_for + diff --git a/beams/beams/doctype/budget_template/budget_template.js b/beams/beams/doctype/budget_template/budget_template.js index 58dea7663..8924478b5 100644 --- a/beams/beams/doctype/budget_template/budget_template.js +++ b/beams/beams/doctype/budget_template/budget_template.js @@ -58,21 +58,40 @@ frappe.ui.form.on('Budget Template Item', { }); function set_filters(frm) { - frm.set_query('division', function () { - return { - filters: { - department: frm.doc.department, - company: frm.doc.company - } - }; - }); - frm.set_query('department', function () { - return { - filters: { - company: frm.doc.company - } - }; - }); + + frm.set_query('division', function () { + return { + filters: { + department: frm.doc.department, + company: frm.doc.company + } + }; + }); + frm.set_query('department', function () { + return { + filters: { + company: frm.doc.company + } + }; + }); + frm.set_query("cost_center", function () { + return { + filters: { + company: frm.doc.company, + is_group: 0, + disabled: 0 + } + } + }); + frm.set_query('budget_head', function() { + return { + query: 'beams.beams.doctype.budget_template.budget_template.get_budget_approver_employees', + filters: { + 'company': frm.doc.company, + 'department': frm.doc.department + } + }; + }); } function clear_budget_items(frm) { diff --git a/beams/beams/doctype/budget_template/budget_template.py b/beams/beams/doctype/budget_template/budget_template.py index f3a26c9cf..e7d589620 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -5,4 +5,49 @@ from frappe.model.document import Document class BudgetTemplate(Document): - pass + + def set_default_account(self): + if not hasattr(self, "budget_template_item") or not self.budget_template_item: + return + + for item in self.budget_template_item: + if not item.cost_sub_head or not self.company: + item.account = "" + continue + + cost_subhead_doc = frappe.get_doc("Cost Subhead", item.cost_sub_head) + + if cost_subhead_doc.accounts: + account_found = next((acc for acc in cost_subhead_doc.accounts if acc.company == self.company), None) + item.account = account_found.default_account if account_found else "" + else: + item.account = "" + + def before_save(self): + self.set_default_account() + + + +@frappe.whitelist() +def get_budget_approver_employees(doctype, txt, searchfield, start, page_len, filters): + + users = frappe.get_all( + "Has Role", + filters={"role": "Budget Approver"}, + pluck="parent" + ) + + if not users: + return [] + + result = frappe.get_all( + "Employee", + filters={ + "user_id": ["in", users] + }, + fields=["name", "employee_name"], + ) + + return [(row.name, row.employee_name) for row in result] + + diff --git a/beams/setup.py b/beams/setup.py index 93d909352..aaf9ad0b3 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -1167,6 +1167,39 @@ def get_budget_custom_fields(): "insert_after": "company_currency", "read_only":1 }, + { + "label": "Budget For", + "fieldname": "budget_for", + "fieldtype": "Select", + "options": "Cost Center\nProject", + "insert_after": "budget_against", + "reqd": 1, + "in_list_view": 1, + "in_standard_filter": 1, + }, + { + "fieldtype": "Column Break", + "fieldname": "column_break_ab", + "label": " ", + "insert_after": "division" + }, + { + "fieldname": "budget_head", + "fieldtype": "Data", + "label": "Budget Head", + "insert_after": "monthly_distribution", + "read_only": 1, + "fetch_from": "budget_template.budget_head" + }, + { + "fieldname": "budget_head_user", + "fieldtype": "Data", + "label": "Budget Head User", + "insert_after": "budget_head", + "read_only": 1, + "fetch_from": "budget_template.budget_head_user" + } + ], "Budget Account": [ { @@ -5549,6 +5582,42 @@ def get_property_setters(): "property_type": "Data", "value": '["workflow_state", "title", "naming_series", "invoice_type", "purchase_order_id", "stringer_bill_reference", "batta_claim_reference", "supplier", "bureau", "barter_invoice", "quotation", "supplier_name", "ewaybill", "tally_masterid", "tally_voucherno", "tax_id", "company", "column_break_6", "posting_date", "posting_time", "set_posting_time", "due_date", "column_break1", "is_paid", "is_return", "return_against", "update_outstanding_for_self", "update_billed_amount_in_purchase_order", "update_billed_amount_in_purchase_receipt", "apply_tds", "is_reverse_charge", "is_budgeted", "budget_exceeded", "from_bureau", "tax_withholding_category", "amended_from", "payments_section", "mode_of_payment", "base_paid_amount", "clearance_date", "col_br_payments", "cash_bank_account", "paid_amount", "supplier_invoice_details", "bill_no", "column_break_15", "bill_date", "accounting_dimensions_section", "cost_center", "dimension_col_break", "project", "currency_and_price_list", "currency", "conversion_rate", "use_transaction_date_exchange_rate", "column_break2", "buying_price_list", "price_list_currency", "plc_conversion_rate", "ignore_pricing_rule", "sec_warehouse", "scan_barcode", "col_break_warehouse", "update_stock", "set_warehouse", "set_from_warehouse", "is_subcontracted", "rejected_warehouse", "supplier_warehouse", "items_section", "items", "section_break_26", "total_qty", "total_net_weight", "column_break_50", "base_total", "base_net_total", "attach", "column_break_28", "total", "net_total", "tax_withholding_net_total", "base_tax_withholding_net_total", "taxes_section", "tax_category", "taxes_and_charges", "column_break_58", "shipping_rule", "column_break_49", "incoterm", "named_place", "section_break_51", "taxes", "totals", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted", "base_total_taxes_and_charges", "column_break_40", "taxes_and_charges_added", "taxes_and_charges_deducted", "total_taxes_and_charges", "section_break_49", "base_grand_total", "base_rounding_adjustment", "base_rounded_total", "base_in_words", "column_break8", "grand_total", "rounding_adjustment", "use_company_roundoff_cost_center", "rounded_total", "in_words", "total_advance", "outstanding_amount", "disable_rounded_total", "section_break_44", "apply_discount_on", "base_discount_amount", "column_break_46", "additional_discount_percentage", "discount_amount", "tax_withheld_vouchers_section", "tax_withheld_vouchers", "sec_tax_breakup", "other_charges_calculation", "section_gst_breakup", "gst_breakup_table", "pricing_rule_details", "pricing_rules", "raw_materials_supplied", "supplied_items", "payments_tab", "advances_section", "allocate_advances_automatically", "only_include_allocated_payments", "get_advances", "advances", "advance_tax", "write_off", "write_off_amount", "base_write_off_amount", "column_break_61", "write_off_account", "write_off_cost_center", "address_and_contact_tab", "section_addresses", "supplier_address", "address_display", "supplier_gstin", "gst_category", "col_break_address", "contact_person", "contact_display", "contact_mobile", "contact_email", "company_shipping_address_section", "dispatch_address", "dispatch_address_display", "column_break_126", "shipping_address", "shipping_address_display", "company_billing_address_section", "billing_address", "column_break_130", "billing_address_display", "company_gstin", "place_of_supply", "terms_tab", "payment_schedule_section", "payment_terms_template", "ignore_default_payment_terms_template", "payment_schedule", "terms_section_break", "tc_name", "terms", "more_info_tab", "status_section", "status", "column_break_177", "per_received", "accounting_details_section", "credit_to", "party_account_currency", "is_opening", "against_expense_account", "column_break_63", "unrealized_profit_loss_account", "subscription_section", "subscription", "auto_repeat", "update_auto_repeat_reference", "column_break_114", "from_date", "to_date", "printing_settings", "letter_head", "group_same_items", "column_break_112", "select_print_heading", "language", "transporter_info", "transporter", "gst_transporter_id", "driver", "lr_no", "vehicle_no", "distance", "transporter_col_break", "transporter_name", "mode_of_transport", "driver_name", "lr_date", "gst_vehicle_type", "gst_section", "itc_classification", "ineligibility_reason", "reconciliation_status", "sb_14", "on_hold", "release_date", "cb_17", "hold_comment", "additional_info_section", "is_internal_supplier", "represents_company", "supplier_group", "column_break_147", "inter_company_invoice_reference", "is_old_subcontracting_flow", "remarks", "connections_tab"]' }, + { + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "doc_type": "Budget", + "field_name": "budget_against", + "property": "default", + "property_type": "Data", + "value": "Cost Center" + }, + { + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "doc_type": "Budget", + "field_name": "budget_against", + "property": "hidden", + "property_type": "Check", + "value": "1" + }, + { + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "doc_type": "Budget", + "field_name": "budget_against", + "property": "in_list_view", + "property_type": "Check", + "value": "0" + }, + { + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "doc_type": "Budget", + "field_name": "budget_against", + "property": "in_standard_filter", + "property_type": "Check", + "value": "0" + }, ] def get_material_request_custom_fields(): From d0c4cc0e48f86e8bcfc7f58960fa3cde1dbe5d61 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Thu, 5 Feb 2026 14:13:49 +0530 Subject: [PATCH 24/50] chore: documentation --- .../budget_template/budget_template.js | 2 + .../budget_template/budget_template.py | 72 ++++++++++--------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/beams/beams/doctype/budget_template/budget_template.js b/beams/beams/doctype/budget_template/budget_template.js index 8924478b5..f95f50fb1 100644 --- a/beams/beams/doctype/budget_template/budget_template.js +++ b/beams/beams/doctype/budget_template/budget_template.js @@ -57,6 +57,7 @@ frappe.ui.form.on('Budget Template Item', { }, }); +// Set query filters for link fields function set_filters(frm) { frm.set_query('division', function () { @@ -94,6 +95,7 @@ function set_filters(frm) { }); } +// Clear budget items table function clear_budget_items(frm) { frm.clear_table('budget_template_items'); frm.refresh_field('budget_template_items'); diff --git a/beams/beams/doctype/budget_template/budget_template.py b/beams/beams/doctype/budget_template/budget_template.py index e7d589620..a9ead957e 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -6,48 +6,52 @@ class BudgetTemplate(Document): - def set_default_account(self): - if not hasattr(self, "budget_template_item") or not self.budget_template_item: - return + def set_default_account(self): + """ Set the default account for each budget template item based on the associated cost subhead and company. + """ + if not hasattr(self, "budget_template_item") or not self.budget_template_item: + return - for item in self.budget_template_item: - if not item.cost_sub_head or not self.company: - item.account = "" - continue + for item in self.budget_template_item: + if not item.cost_sub_head or not self.company: + item.account = "" + continue - cost_subhead_doc = frappe.get_doc("Cost Subhead", item.cost_sub_head) + cost_subhead_doc = frappe.get_doc("Cost Subhead", item.cost_sub_head) - if cost_subhead_doc.accounts: - account_found = next((acc for acc in cost_subhead_doc.accounts if acc.company == self.company), None) - item.account = account_found.default_account if account_found else "" - else: - item.account = "" + if cost_subhead_doc.accounts: + account_found = next((acc for acc in cost_subhead_doc.accounts if acc.company == self.company), None) + item.account = account_found.default_account if account_found else "" + else: + item.account = "" - def before_save(self): - self.set_default_account() + def before_save(self): + self.set_default_account() @frappe.whitelist() def get_budget_approver_employees(doctype, txt, searchfield, start, page_len, filters): - - users = frappe.get_all( - "Has Role", - filters={"role": "Budget Approver"}, - pluck="parent" - ) - - if not users: - return [] - - result = frappe.get_all( - "Employee", - filters={ - "user_id": ["in", users] - }, - fields=["name", "employee_name"], - ) - - return [(row.name, row.employee_name) for row in result] + """ + Fetch employees with the role of 'Budget Approver' for the current company. + """ + users = frappe.get_all( + "Has Role", + filters={"role": "Budget Approver"}, + pluck="parent" + ) + + if not users: + return [] + + result = frappe.get_all( + "Employee", + filters={ + "user_id": ["in", users] + }, + fields=["name", "employee_name"], + ) + + return [(row.name, row.employee_name) for row in result] From b221b1a3c75453f06a37e033f80087ccc6f453cd Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Thu, 5 Feb 2026 15:32:38 +0530 Subject: [PATCH 25/50] feat: budget validation based on cost head and refactored budget_exceed fields --- .../purchase_order/purchase_order.js | 5 +- .../purchase_order/purchase_order.py | 69 +-- beams/beams/custom_scripts/utils.py | 30 ++ .../beams/doctype/batta_claim/batta_claim.js | 5 +- .../doctype/batta_claim/batta_claim.json | 26 +- .../employee_travel_request.js | 8 +- .../employee_travel_request.json | 20 +- .../employee_travel_request.py | 4 +- .../doctype/stringer_bill/stringer_bill.js | 38 +- .../doctype/stringer_bill/stringer_bill.json | 20 +- beams/beams/doctype/trip_sheet/trip_sheet.js | 4 +- .../beams/doctype/trip_sheet/trip_sheet.json | 10 +- beams/beams/overrides/budget.py | 402 ++++++++++-------- beams/hooks.py | 11 +- beams/patches.txt | 2 +- beams/patches/delete_custom_fields.py | 20 + beams/setup.py | 16 +- 17 files changed, 372 insertions(+), 318 deletions(-) create mode 100644 beams/beams/custom_scripts/utils.py diff --git a/beams/beams/custom_scripts/purchase_order/purchase_order.js b/beams/beams/custom_scripts/purchase_order/purchase_order.js index 6978b3b8f..46e5f6f4b 100644 --- a/beams/beams/custom_scripts/purchase_order/purchase_order.js +++ b/beams/beams/custom_scripts/purchase_order/purchase_order.js @@ -1,7 +1,6 @@ frappe.ui.form.on('Purchase Order', { refresh(frm) { workflow_actions(frm); - clear_checkbox_exceed(frm); }, is_budgeted: function(frm){ clear_checkbox_exceed(frm); @@ -42,10 +41,10 @@ function workflow_actions(frm) { } /** -* Clears the "is_budget_exceed" checkbox if "is_budgeted" is unchecked. +* Clears the "is_budget_exceeded" checkbox if "is_budgeted" is unchecked. */ function clear_checkbox_exceed(frm){ if (frm.doc.is_budgeted == 0){ - frm.set_value("is_budget_exceed", 0); + frm.set_value("is_budget_exceeded", 0); } } diff --git a/beams/beams/custom_scripts/purchase_order/purchase_order.py b/beams/beams/custom_scripts/purchase_order/purchase_order.py index 73054a180..ed30f64a5 100644 --- a/beams/beams/custom_scripts/purchase_order/purchase_order.py +++ b/beams/beams/custom_scripts/purchase_order/purchase_order.py @@ -6,17 +6,17 @@ def validate_reason_for_rejection(doc,method): - ''' - Validate that "Reason for Rejection" is filled if the status is "Rejected" - ''' - rejection_states = [ - "Rejected", - "Rejected By Finance", - "Rejected by CEO" - ] + ''' + Validate that "Reason for Rejection" is filled if the status is "Rejected" + ''' + rejection_states = [ + "Rejected", + "Rejected By Finance", + "Rejected by CEO" + ] - if doc.workflow_state in rejection_states and not doc.reason_for_rejection: - frappe.throw("Please provide a Reason for Rejection before rejecting this request.") + if doc.workflow_state in rejection_states and not doc.reason_for_rejection: + frappe.throw("Please provide a Reason for Rejection before rejecting this request.") @frappe.whitelist() def create_todo_on_finance_verification(doc, method): @@ -47,55 +47,6 @@ def create_todo_on_finance_verification(doc, method): "description": description }) - -def validate(self): - ''' - This function validates the expenses for each item in the document against the defined budget. - ''' - for item in self.items: - if item.cost_center: - budget = frappe.get_value('Budget', {'cost_center': item.cost_center, 'fiscal_year': self.fiscal_year}, 'total_budget') - - # Get the actual expenses from GL Entry - actual_expense = frappe.db.sql(""" - SELECT SUM(credit) - FROM `tabGL Entry` - WHERE cost_center = %s - AND account = %s - AND fiscal_year = %s - """, (item.cost_center, item.expense_account, self.fiscal_year)) - - # Calculate the total expense including the current Purchase Order amount - total_expense = actual_expense[0][0] or 0 - total_expense += item.amount - - if total_expense > budget: - self.is_budget_exceed = 1 # Automatically check the checkbox - frappe.msgprint(_("The budget for Cost Center {0} has been exceeded.").format(item.cost_center)) - -def validate_budget(self, method=None): - ''' - Validating Budget for Purchase order and material request - ''' - from beams.beams.overrides.budget import validate_expense_against_budget - if self.name: - for data in self.get("items"): - args = data.as_dict() - args.update( - { - "object": self, - "doctype": self.doctype, - "company": self.company, - "posting_date": ( - self.schedule_date - if self.doctype == "Material Request" - else self.transaction_date - ), - } - ) - - validate_expense_against_budget(args, 0, 1) - @frappe.whitelist() def fetch_department_from_cost_center(doc, method): """ diff --git a/beams/beams/custom_scripts/utils.py b/beams/beams/custom_scripts/utils.py new file mode 100644 index 000000000..312e5569e --- /dev/null +++ b/beams/beams/custom_scripts/utils.py @@ -0,0 +1,30 @@ +import frappe +from beams.beams.overrides.budget import validate_expense_against_budget + +budget_validation_doctypes = ["Purchase Order", "Material Request"] + +def validate_budget(self, method=None): + ''' + Custom Method trigger on on_update event of all doctypes + ''' + #Skip if doctype is not in budget_validation_doctypes + if self.doctype not in budget_validation_doctypes: + return + + #Apply budget validation + if self.name: + for data in self.get("items"): + args = data.as_dict() + args.update( + { + "object": self, + "doctype": self.doctype, + "company": self.company, + "posting_date": ( + self.schedule_date + if self.doctype == "Material Request" + else self.transaction_date + ), + } + ) + validate_expense_against_budget(args, 0, 1) diff --git a/beams/beams/doctype/batta_claim/batta_claim.js b/beams/beams/doctype/batta_claim/batta_claim.js index 9e101909d..a455f03c6 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.js +++ b/beams/beams/doctype/batta_claim/batta_claim.js @@ -70,7 +70,6 @@ frappe.ui.form.on('Batta Claim', { }, refresh: function(frm) { toggle_room_rent_batta_field(frm); - clear_checkbox_exceed(frm); frappe.call({ method: "beams.beams.doctype.batta_claim.batta_claim.get_batta_policy_values", callback: function(response) { @@ -451,10 +450,10 @@ function calculate_total_batta(frm, cdt, cdn) { } /** -* Clears the "is_budget_exceed" checkbox if "is_budgeted" is unchecked. +* Clears the "is_budget_exceeded" checkbox if "is_budgeted" is unchecked. */ function clear_checkbox_exceed(frm){ if(frm.doc.is_budgeted == 0){ - frm.set_value("is_budget_exceed", 0); + frm.set_value("is_budget_exceeded", 0); } } diff --git a/beams/beams/doctype/batta_claim/batta_claim.json b/beams/beams/doctype/batta_claim/batta_claim.json index 592a6dbe0..3b494027c 100644 --- a/beams/beams/doctype/batta_claim/batta_claim.json +++ b/beams/beams/doctype/batta_claim/batta_claim.json @@ -25,7 +25,7 @@ "mode_of_travelling", "from_bureau", "is_budgeted", - "is_budget_exceed", + "is_budget_exceeded", "is_travelling_outside_kerala", "is_overnight_stay", "is_avail_room_rent", @@ -299,13 +299,15 @@ "fieldname": "trip_sheet", "fieldtype": "Link", "label": "Trip Sheet", - "options": "Trip Sheet" + "options": "Trip Sheet", + "search_index": 1 }, { "fieldname": "travel_request", "fieldtype": "Link", "label": "Travel Request", - "options": "Employee Travel Request" + "options": "Employee Travel Request", + "search_index": 1 }, { "default": "1", @@ -313,13 +315,6 @@ "fieldtype": "Check", "label": "Is Budgeted" }, - { - "default": "0", - "depends_on": "eval:doc.is_budgeted == 1", - "fieldname": "is_budget_exceed", - "fieldtype": "Check", - "label": " Is Budget Exceed" - }, { "default": "0", "fieldname": "from_bureau", @@ -334,6 +329,13 @@ "label": "HOD Email", "options": "Email", "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.is_budgeted == 1", + "fieldname": "is_budget_exceeded", + "fieldtype": "Check", + "label": " Is Budget Exceeded" } ], "index_web_pages_for_search": 1, @@ -344,11 +346,11 @@ "link_fieldname": "batta_claim_reference" } ], - "modified": "2026-01-21 11:13:17.198649", + "modified": "2026-02-05 14:16:38.084292", "modified_by": "Administrator", "module": "BEAMS", "name": "Batta Claim", - "naming_rule": "Expression", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { diff --git a/beams/beams/doctype/employee_travel_request/employee_travel_request.js b/beams/beams/doctype/employee_travel_request/employee_travel_request.js index 6c660028c..d6a4dc79d 100644 --- a/beams/beams/doctype/employee_travel_request/employee_travel_request.js +++ b/beams/beams/doctype/employee_travel_request/employee_travel_request.js @@ -83,7 +83,7 @@ frappe.ui.form.on('Employee Travel Request', { expenses: expenses, mode_of_payment: values.mode_of_payment, is_budgeted: frm.doc.is_budgeted || 0, - budget_exceeded: frm.doc.is__budget_exceed || 0 + budget_exceeded: frm.doc.is_budget_exceeded || 0 }, callback: function (r) { if (!r.exc) { @@ -200,7 +200,7 @@ frappe.ui.form.on('Employee Travel Request', { travel_request: frm.doc.name, expenses: expenses, is_budgeted: frm.doc.is_budgeted || 0, - budget_exceeded: frm.doc.is__budget_exceed || 0 + budget_exceeded: frm.doc.is_budget_exceeded || 0 }, callback: function (r) { if (!r.exc) { @@ -498,7 +498,7 @@ function create_batta_claim_from_travel(frm) { args: { travel_request: frm.doc.name, is_budgeted: frm.doc.is_budgeted || 0, - is_budget_exceed: frm.doc.is__budget_exceed || 0 + is_budget_exceeded: frm.doc.is_budget_exceeded || 0 }, callback: function (r) { if (!r.message) return; @@ -513,7 +513,7 @@ function create_batta_claim_from_travel(frm) { */ function clear_checkbox_exceed(frm){ if(!frm.doc.is_budgeted){ - frm.set_value("is__budget_exceed",0); + frm.set_value("is_budget_exceeded", 0); } } diff --git a/beams/beams/doctype/employee_travel_request/employee_travel_request.json b/beams/beams/doctype/employee_travel_request/employee_travel_request.json index d50e37f39..6d9942f27 100644 --- a/beams/beams/doctype/employee_travel_request/employee_travel_request.json +++ b/beams/beams/doctype/employee_travel_request/employee_travel_request.json @@ -18,7 +18,7 @@ "from_bureau", "is_management_employee", "is_budgeted", - "is__budget_exceed", + "is_budget_exceeded", "has_bureau_head_role", "travel_details_section", "travel_type", @@ -270,13 +270,6 @@ "fieldtype": "Check", "label": "Is Budgeted" }, - { - "default": "0", - "depends_on": "eval:doc.is_budgeted == 1", - "fieldname": "is__budget_exceed", - "fieldtype": "Check", - "label": "Is Budget Exceed" - }, { "default": "0", "fieldname": "from_bureau", @@ -290,6 +283,13 @@ "fieldtype": "Check", "hidden": 1, "label": "Has Bureau Head Role" + }, + { + "default": "0", + "depends_on": "eval:doc.is_budgeted == 1", + "fieldname": "is_budget_exceeded", + "fieldtype": "Check", + "label": "Is Budget Exceeded" } ], "index_web_pages_for_search": 1, @@ -312,11 +312,11 @@ "link_fieldname": "travel_request" } ], - "modified": "2026-01-14 15:45:41.546113", + "modified": "2026-02-05 15:08:09.440498", "modified_by": "Administrator", "module": "BEAMS", "name": "Employee Travel Request", - "naming_rule": "Expression", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { diff --git a/beams/beams/doctype/employee_travel_request/employee_travel_request.py b/beams/beams/doctype/employee_travel_request/employee_travel_request.py index 1c20902ad..b6ef7b8f8 100644 --- a/beams/beams/doctype/employee_travel_request/employee_travel_request.py +++ b/beams/beams/doctype/employee_travel_request/employee_travel_request.py @@ -818,7 +818,7 @@ def assign_todo_for_accounts(employee_travel_request, journal_entry_name): }) @frappe.whitelist() -def create_batta_claim_from_etr(travel_request, is_budgeted, is_budget_exceed): +def create_batta_claim_from_etr(travel_request, is_budgeted, is_budget_exceeded): ''' Create Batta Claim from Employee Travel Request. ''' @@ -836,7 +836,7 @@ def create_batta_claim_from_etr(travel_request, is_budgeted, is_budget_exceed): bc.travel_request = doc.name bc.employee = employee bc.is_budgeted = is_budgeted - bc.is_budget_exceed = is_budget_exceed + bc.is_budget_exceeded = is_budget_exceeded bc.origin = doc.source bc.destination = doc.destination bc.purpose= doc.travel_type diff --git a/beams/beams/doctype/stringer_bill/stringer_bill.js b/beams/beams/doctype/stringer_bill/stringer_bill.js index 103cf9248..1af24732c 100644 --- a/beams/beams/doctype/stringer_bill/stringer_bill.js +++ b/beams/beams/doctype/stringer_bill/stringer_bill.js @@ -1,21 +1,8 @@ frappe.ui.form.on('Stringer Bill', { - onload: function(frm) { - /* - * Set a query filter for the 'supplier' field to only show suppliers with 'is_stringer' set to 1 - */ - frm.set_query('supplier', function() { - return { - filters: { - 'is_stringer': 1 - } - }; - }); - }, refresh: function (frm) { - calculate_total(frm); - clear_checkbox_exceed(frm); + set_filters(frm); }, - is_budgeted: function(frm){ + is_budgeted: function (frm) { clear_checkbox_exceed(frm); } }); @@ -26,6 +13,9 @@ frappe.ui.form.on('Stringer Bill Detail', { }, stringer_bill_detail_remove: function (frm) { calculate_total(frm); + }, + stringer_bill_detail_add: function (frm) { + calculate_total(frm); } }); @@ -38,10 +28,20 @@ function calculate_total(frm) { } /** -* Clears the "is_budget_exceed" checkbox if "is_budgeted" is unchecked. +* Clears the "is_budget_exceeded" checkbox if "is_budgeted" is unchecked. */ -function clear_checkbox_exceed(frm){ - if(frm.doc.is_budgeted == 0){ - frm.set_value("is_budget_exceed", 0); +function clear_checkbox_exceed(frm) { + if (frm.doc.is_budgeted == 0) { + frm.set_value("is_budget_exceeded", 0); } } + +function set_filters(frm) { + frm.set_query('supplier', function () { + return { + filters: { + 'is_stringer': 1 + } + }; + }); +} \ No newline at end of file diff --git a/beams/beams/doctype/stringer_bill/stringer_bill.json b/beams/beams/doctype/stringer_bill/stringer_bill.json index 090b39892..5fe29044a 100644 --- a/beams/beams/doctype/stringer_bill/stringer_bill.json +++ b/beams/beams/doctype/stringer_bill/stringer_bill.json @@ -15,7 +15,7 @@ "bureau", "cost_center", "is_budgeted", - "is_budget_exceed", + "is_budget_exceeded", "bureau_head", "section_break_njgm", "stringer_bill_detail", @@ -122,13 +122,6 @@ "fieldtype": "Check", "label": "Is Budgeted" }, - { - "default": "0", - "depends_on": "eval:doc.is_budgeted == 1", - "fieldname": "is_budget_exceed", - "fieldtype": "Check", - "label": "Is Budget Exceed" - }, { "fetch_from": "bureau.regional_bureau_head", "fieldname": "bureau_head", @@ -136,6 +129,13 @@ "hidden": 1, "label": "Bureau Head", "options": "Employee" + }, + { + "default": "0", + "depends_on": "eval:doc.is_budgeted == 1", + "fieldname": "is_budget_exceeded", + "fieldtype": "Check", + "label": "Is Budget Exceeded" } ], "index_web_pages_for_search": 1, @@ -146,11 +146,11 @@ "link_fieldname": "stringer_bill_reference" } ], - "modified": "2026-01-03 15:22:45.560448", + "modified": "2026-02-05 14:21:26.112314", "modified_by": "Administrator", "module": "BEAMS", "name": "Stringer Bill", - "naming_rule": "Expression", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { diff --git a/beams/beams/doctype/trip_sheet/trip_sheet.js b/beams/beams/doctype/trip_sheet/trip_sheet.js index ed4611aa3..611aeb77a 100644 --- a/beams/beams/doctype/trip_sheet/trip_sheet.js +++ b/beams/beams/doctype/trip_sheet/trip_sheet.js @@ -247,10 +247,10 @@ frappe.ui.form.on('Trip Details', { }); /* -clears the "is_budget_exceed" checkbox if "is_budgeted" is unchecked. +clears the "is_budget_exceeded" checkbox if "is_budgeted" is unchecked. */ function clear_checkbox_exceed(frm){ if (frm.doc.is_budgeted == 0){ - frm.set_value("is_budget_exceed", 0); + frm.set_value("is_budget_exceeded", 0); } } diff --git a/beams/beams/doctype/trip_sheet/trip_sheet.json b/beams/beams/doctype/trip_sheet/trip_sheet.json index 09ed44b3d..8b59f64e6 100644 --- a/beams/beams/doctype/trip_sheet/trip_sheet.json +++ b/beams/beams/doctype/trip_sheet/trip_sheet.json @@ -29,7 +29,7 @@ "trip_details_section", "trip_details", "is_budgeted", - "is_budget_exceed", + "is_budget_exceeded", "section_break_ygej", "initial_odometer_reading", "final_odometer_reading", @@ -250,9 +250,9 @@ { "default": "0", "depends_on": "eval:doc.is_budgeted == 1", - "fieldname": "is_budget_exceed", + "fieldname": "is_budget_exceeded", "fieldtype": "Check", - "label": "Is Budget Exceed " + "label": "Is Budget Exceeded" } ], "index_web_pages_for_search": 1, @@ -267,11 +267,11 @@ "link_fieldname": "trip_sheet" } ], - "modified": "2025-12-04 15:00:58.861731", + "modified": "2026-02-05 14:23:33.583528", "modified_by": "Administrator", "module": "BEAMS", "name": "Trip Sheet", - "naming_rule": "Expression", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { diff --git a/beams/beams/overrides/budget.py b/beams/beams/overrides/budget.py index 05f13a1d0..dbdfabc49 100644 --- a/beams/beams/overrides/budget.py +++ b/beams/beams/overrides/budget.py @@ -4,31 +4,28 @@ import frappe from frappe import _ -from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate - -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( - get_accounting_dimensions, -) from erpnext.accounts.utils import get_fiscal_year +from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate -def validate_expense_against_budget(args, expense_amount=0, for_check=0): +#for_check_only is used for early budget check on draft for PO and MR +def validate_expense_against_budget(args, expense_amount=0, for_check_only=0): args = frappe._dict(args) - if not frappe.get_all("Budget", limit=1): + if not frappe.get_all('Budget', limit=1): return - if args.get("company") and not args.fiscal_year: - args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0] + if args.get('company') and not args.fiscal_year: + args.fiscal_year = get_fiscal_year(args.get('posting_date'), company=args.get('company'))[0] frappe.flags.exception_approver_role = frappe.get_cached_value( - "Company", args.get("company"), "exception_budget_approver_role" + 'Company', args.get('company'), 'exception_budget_approver_role' ) - if not frappe.get_cached_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): # nosec + if not frappe.get_cached_value('Budget', {'fiscal_year': args.fiscal_year, 'company': args.company}): # nosec return if not args.account: - args.account = args.get("expense_account") + args.account = args.get('expense_account') - if not (args.get("account") and args.get("cost_center")) and args.item_code: + if not (args.get('account') and args.get('cost_center')) and args.item_code: args.cost_center, args.account = get_item_details(args) if not args.account: @@ -36,43 +33,39 @@ def validate_expense_against_budget(args, expense_amount=0, for_check=0): default_dimensions = [ { - "fieldname": "project", - "document_type": "Project", - }, - { - "fieldname": "cost_center", - "document_type": "Cost Center", + 'fieldname': 'project', + 'document_type': 'Project', }, { - "fieldname": "department", - "document_type": "Department", - }, + 'fieldname': 'cost_center', + 'document_type': 'Cost Center', + } ] - for dimension in default_dimensions + get_accounting_dimensions(as_list=False): - budget_against = dimension.get("fieldname") + for dimension in default_dimensions: + budget_against = dimension.get('fieldname') if ( args.get(budget_against) and args.account - and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense") + and (frappe.get_cached_value('Account', args.account, 'root_type') == 'Expense') ): - doctype = dimension.get("document_type") + doctype = dimension.get('document_type') - if frappe.get_cached_value("DocType", doctype, "is_tree"): - lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"]) - condition = f"""and exists(select name from `tab{doctype}` - where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec + if frappe.get_cached_value('DocType', doctype, 'is_tree'): + lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ['lft', 'rgt']) + condition = f'''and exists(select name from `tab{doctype}` + where lft<={lft} and rgt>={rgt} and name=b.{budget_against})''' # nosec args.is_tree = True else: - condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}" + condition = f'and b.{budget_against}={frappe.db.escape(args.get(budget_against))}' args.is_tree = False args.budget_against_field = budget_against args.budget_against_doctype = doctype budget_records = frappe.db.sql( - f""" + f''' select b.name, b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution, ifnull(b.applicable_on_material_request, 0) as for_material_request, @@ -87,75 +80,87 @@ def validate_expense_against_budget(args, expense_amount=0, for_check=0): b.name=ba.parent and b.fiscal_year=%s and ba.account=%s and b.docstatus=1 {condition} - """, + ''', (args.fiscal_year, args.account), as_dict=True, ) # nosec if budget_records: - validate_budget_records(args, budget_records, expense_amount, for_check) + validate_budget_records(args, budget_records, expense_amount, for_check_only) -def validate_budget_records(args, budget_records, expense_amount, for_check): +def validate_budget_records(args, budget_records, expense_amount, for_check_only): for budget in budget_records: if flt(budget.budget_amount): yearly_action, monthly_action = get_actions(args, budget) - args["for_material_request"] = budget.for_material_request - args["for_purchase_order"] = budget.for_purchase_order + args['for_material_request'] = budget.for_material_request + args['for_purchase_order'] = budget.for_purchase_order - if yearly_action in ("Stop", "Warn"): + if yearly_action in ('Stop', 'Warn'): + budget_amount = get_yearly_budget_amount(budget.name, cost_head=args.get('cost_head')) compare_expense_with_budget( args, - flt(budget.budget_amount), - _("Annual"), + budget_amount, + _('Annual'), yearly_action, budget.budget_against, expense_amount, + for_check_only ) - if monthly_action in ["Stop", "Warn"]: + if monthly_action in ['Stop', 'Warn']: budget_amount = get_accumulated_monthly_budget( - budget.name, args.posting_date, args.fiscal_year, budget.budget_amount + budget.name, args.posting_date, args.fiscal_year, budget.budget_amount, args.cost_head ) - args["month_end_date"] = get_last_day(args.posting_date) + args['month_end_date'] = get_last_day(args.posting_date) compare_expense_with_budget( args, budget_amount, - _("Accumulated Monthly"), + _('Accumulated Monthly'), monthly_action, budget.budget_against, expense_amount, - for_check + for_check_only ) -def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0, for_check=0): +def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0, for_check_only=0): + #Setting doctype and docname for setting is_budget_exceeded field + doctype, docname = None, None + if args.get('doctype') and args.get('parent'): + doctype = args.get('doctype') + docname = args.get('parent') + elif args.get('voucher_type') and args.get('voucher_no'): + doctype = args.get('voucher_type') + docname = args.get('voucher_no') + args.actual_expense, args.requested_amount, args.ordered_amount = get_actual_expense(args), 0, 0 if not amount: - args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args, for_check) + args.requested_amount, args.ordered_amount = get_requested_amount(args), get_ordered_amount(args) - if args.get("doctype") == "Material Request" and args.for_material_request: + if args.get('doctype') == 'Material Request' and args.for_material_request: amount = args.requested_amount + args.ordered_amount - elif args.get("doctype") == "Purchase Order" and args.for_purchase_order: + elif args.get('doctype') == 'Purchase Order' and args.for_purchase_order: amount = args.ordered_amount total_expense = args.actual_expense + amount if total_expense > budget_amount: if args.actual_expense > budget_amount: - error_tense = _("is already") + error_tense = _('is already') diff = args.actual_expense - budget_amount else: - error_tense = _("will be") + error_tense = _('will be') diff = total_expense - budget_amount - currency = frappe.get_cached_value("Company", args.company, "default_currency") + currency = frappe.get_cached_value('Company', args.company, 'default_currency') - msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format( + msg = _('{0} Budget for Account {1} with Cost Head {2} against {3} {4} is {5}. It {6} exceed by {7}').format( _(action_for), frappe.bold(args.account), + frappe.bold(args.get('cost_head', 'N/A')), frappe.unscrub(args.budget_against_field), frappe.bold(budget_against), frappe.bold(fmt_money(budget_amount, currency=currency)), @@ -168,99 +173,96 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_ if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles( frappe.session.user ): - action = "Warn" - - if for_check: - # Set is_budget_exceed field in the Purchase Order doctype before showing warning or error - if args.get("doctype") == "Purchase Order" and args.for_purchase_order: - args.get("object").is_budget_exceed = 1 - elif args.get("doctype") == "Material Request" and args.for_material_request: - args.get("object").budget_exceeded = 1 + action = 'Warn' + + #Setting Checkboxes for Budget Exceeded in the respective Doctypes + if doctype and docname: + if field_exists(doctype, 'is_budget_exceededed') and frappe.db.exists(doctype, docname): + frappe.db.set_value(doctype, docname, 'is_budget_exceeded', 1, update_modified=False) + + if for_check_only: + #For custom check on validate + frappe.msgprint(msg, indicator='orange', title=_('Budget Exceeded')) else: - if action == "Stop": - frappe.throw(msg, title=_("Budget Exceeded")) + if action == 'Stop': + frappe.throw(msg, title=_('Budget Exceeded')) else: - frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded")) + frappe.msgprint(msg, indicator='orange', title=_('Budget Exceeded')) else: - if for_check: - # If total_expense is less than or equal to budget_amount, reset the is_budget_exceed field - if args.get("doctype") == "Purchase Order" and args.for_purchase_order: - args.get("object").is_budget_exceed = 0 - elif args.get("doctype") == "Material Request" and args.for_material_request: - args.get("object").budget_exceeded = 0 + if doctype and docname: + if field_exists(doctype, 'is_budget_exceeded') and frappe.db.exists(doctype, docname): + frappe.db.set_value(doctype, docname, 'is_budget_exceeded', 0, update_modified=False) def get_expense_breakup(args, currency, budget_against): - msg = "
    Total Expenses booked through -
      " + msg = '
      Total Expenses booked through -
        ' common_filters = frappe._dict( { args.budget_against_field: budget_against, - "account": args.account, - "company": args.company, + 'account': args.account, + 'company': args.company, } ) msg += ( - "
      • " + '
      • ' + frappe.utils.get_link_to_report( - "General Ledger", - label="Actual Expenses", + 'General Ledger', + label='Actual Expenses', filters=common_filters.copy().update( { - "from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"), - "to_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_end_date"), - "is_cancelled": 0, + 'from_date': frappe.get_cached_value('Fiscal Year', args.fiscal_year, 'year_start_date'), + 'to_date': frappe.get_cached_value('Fiscal Year', args.fiscal_year, 'year_end_date'), + 'is_cancelled': 0, } ), ) - + " - " + + ' - ' + frappe.bold(fmt_money(args.actual_expense, currency=currency)) - + "
      • " + + '' ) msg += ( - "
      • " + '
      • ' + frappe.utils.get_link_to_report( - "Material Request", - label="Material Requests", - report_type="Report Builder", - doctype="Material Request", + 'Material Request', + label='Material Requests', + report_type='Report Builder', + doctype='Material Request', filters=common_filters.copy().update( { - "status": [["!=", "Stopped"]], - "docstatus": 1, - "material_request_type": "Purchase", - "schedule_date": [["fiscal year", "2023-2024"]], - "item_code": args.item_code, - "per_ordered": [["<", 100]], + 'status': [['!=', 'Stopped']], + 'docstatus': 1, + 'material_request_type': 'Purchase', + 'item_code': args.item_code, + 'per_ordered': [['<', 100]], } ), ) - + " - " + + ' - ' + frappe.bold(fmt_money(args.requested_amount, currency=currency)) - + "
      • " + + '' ) msg += ( - "
      • " + '
      • ' + frappe.utils.get_link_to_report( - "Purchase Order", - label="Unbilled Orders", - report_type="Report Builder", - doctype="Purchase Order", + 'Purchase Order', + label='Unbilled Orders', + report_type='Report Builder', + doctype='Purchase Order', filters=common_filters.copy().update( { - "status": [["!=", "Closed"]], - "docstatus": 1, - "transaction_date": [["fiscal year", "2023-2024"]], - "item_code": args.item_code, - "per_billed": [["<", 100]], + 'status': [['!=', 'Closed']], + 'docstatus': 1, + 'item_code': args.item_code, + 'per_billed': [['<', 100]], } ), ) - + " - " + + ' - ' + frappe.bold(fmt_money(args.ordered_amount, currency=currency)) - + "
      " + + '
    ' ) return msg @@ -270,103 +272,133 @@ def get_actions(args, budget): yearly_action = budget.action_if_annual_budget_exceeded monthly_action = budget.action_if_accumulated_monthly_budget_exceeded - if args.get("doctype") == "Material Request" and budget.for_material_request: + if args.get('doctype') == 'Material Request' and budget.for_material_request: yearly_action = budget.action_if_annual_budget_exceeded_on_mr monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr - elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order: + elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order: yearly_action = budget.action_if_annual_budget_exceeded_on_po monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po return yearly_action, monthly_action def get_requested_amount(args): - item_code = args.get("item_code") - condition = get_other_condition(args, "Material Request") - - data = frappe.db.sql( - """ select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount - from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and - child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {} and - parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition), - item_code, - as_list=1, - ) - - if args.get("doctype") == "Material Request": + item_code = args.get('item_code') + cost_head = args.get('cost_head', '') + condition = get_other_condition(args, 'Material Request') + + request_query = f''' + select + ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount + from + `tabMaterial Request Item` child, + `tabMaterial Request` parent + where + parent.name = child.parent and + child.item_code = '{item_code}' and + child.cost_head = '{cost_head}' and + parent.docstatus = 1 and + child.stock_qty > child.ordered_qty and + {condition} and + parent.material_request_type = 'Purchase' and + parent.status != 'Stopped' + ''' + data = frappe.db.sql(request_query, as_list=1) + + if args.get('doctype') == 'Material Request' and args.get('object'): unsubmitted_requested_amount = 0 - for item in args.get("object").items: - unsubmitted_requested_amount += (item.stock_qty - item.ordered_qty) * item.rate + for item in args.get('object').items: + if item.get('cost_head', '') == cost_head: + unsubmitted_requested_amount += (item.stock_qty - item.ordered_qty) * item.rate data[0][0] += unsubmitted_requested_amount return data[0][0] if data else 0 -def get_ordered_amount(args, for_check): - item_code = args.get("item_code") - condition = get_other_condition(args, "Purchase Order") +def get_ordered_amount(args): + item_code = args.get('item_code') + cost_head = args.get('cost_head', '') + condition = get_other_condition(args, 'Purchase Order') - data = frappe.db.sql( - f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount - from `tabPurchase Order Item` child, `tabPurchase Order` parent where - parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt - and parent.status != 'Closed' and {condition}""", - item_code, - as_list=1, - ) - - if args.get("doctype") == "Purchase Order" and for_check: + order_query = f''' + select + ifnull(sum(child.amount - child.billed_amt), 0) as amount + from + `tabPurchase Order Item` child, + `tabPurchase Order` parent + where + parent.name = child.parent and + child.item_code = '{item_code}' and + child.cost_head = '{cost_head}' and + parent.docstatus = 1 and + child.amount > child.billed_amt and + parent.status != 'Closed' and + {condition} + ''' + data = frappe.db.sql(order_query, as_list=1) + + if args.get('doctype') == 'Purchase Order' and args.get('object'): unsubmitted_ordered_amount = 0 - for item in args.get("object").items: - unsubmitted_ordered_amount += item.amount - item.billed_amt + for item in args.get('object').items: + if item.get('cost_head', '') == cost_head: + unsubmitted_ordered_amount += item.amount - item.billed_amt data[0][0] += unsubmitted_ordered_amount return data[0][0] if data else 0 def get_other_condition(args, for_doc): - condition = "expense_account = '%s'" % (args.expense_account) - budget_against_field = args.get("budget_against_field") + condition = 'expense_account = "%s"' % (args.expense_account) + budget_against_field = args.get('budget_against_field') if budget_against_field and args.get(budget_against_field): - condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'" + condition += f' and child.{budget_against_field} = "{args.get(budget_against_field)}"' - if args.get("fiscal_year"): - date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date" + if args.get('fiscal_year'): + date_field = 'schedule_date' if for_doc == 'Material Request' else 'transaction_date' start_date, end_date = frappe.get_cached_value( - "Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"] + 'Fiscal Year', args.get('fiscal_year'), ['year_start_date', 'year_end_date'] ) - condition += f""" and parent.{date_field} - between '{start_date}' and '{end_date}' """ + condition += f''' and parent.{date_field} + between '{start_date}' and '{end_date}' ''' return condition def get_actual_expense(args): + ''' + Method to get Actual Expense from GL Entry + ''' + + #Checking Cost Head based expenses + condition3 = '' + if field_exists('GL Entry', 'cost_head'): + condition3 = 'and gle.cost_head = %(cost_head)s' + if not args.budget_against_doctype: args.budget_against_doctype = frappe.unscrub(args.budget_against_field) - budget_against_field = args.get("budget_against_field") - condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else "" + budget_against_field = args.get('budget_against_field') + condition1 = ' and gle.posting_date <= %(month_end_date)s' if args.get('month_end_date') else '' if args.is_tree: lft_rgt = frappe.db.get_value( - args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1 + args.budget_against_doctype, args.get(budget_against_field), ['lft', 'rgt'], as_dict=1 ) args.update(lft_rgt) - condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}` + condition2 = f'''and exists(select name from `tab{args.budget_against_doctype}` where lft>=%(lft)s and rgt<=%(rgt)s - and name=gle.{budget_against_field})""" + and name=gle.{budget_against_field})''' else: - condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}` + condition2 = f'''and exists(select name from `tab{args.budget_against_doctype}` where name=gle.{budget_against_field} and - gle.{budget_against_field} = %({budget_against_field})s)""" + gle.{budget_against_field} = %({budget_against_field})s)''' amount = flt( frappe.db.sql( - f""" + f''' select sum(gle.debit) - sum(gle.credit) from `tabGL Entry` gle where @@ -377,28 +409,31 @@ def get_actual_expense(args): and gle.company=%(company)s and gle.docstatus=1 {condition2} - """, + {condition3} + ''', (args), )[0][0] - ) # nosec + )# nosec return amount -def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget): +def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget, cost_head): + ''' + Method to get Accumulated Monthly Budget for the selected Cost Head + ''' # List of months explicitly defined months = [ - "january", "february", "march", "april", "may", "june", - "july", "august", "september", "october", "november", "december" + 'january', 'february', 'march', 'april', 'may', 'june', + 'july', 'august', 'september', 'october', 'november', 'december' ] - dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date") + dt = frappe.get_cached_value('Fiscal Year', fiscal_year, 'year_start_date') accummulated_budget = 0 while dt <= getdate(posting_date): - accummulated_budget += frappe.db.get_value("Budget Account", {"parent":monthly_distribution}, months[dt.month - 1]) - + accummulated_budget += frappe.db.get_value('M1 Budget Account', {'parent':monthly_distribution, 'cost_head':cost_head}, months[dt.month - 1]) or 0 dt = add_months(dt, 1) return accummulated_budget @@ -406,20 +441,21 @@ def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_ye def get_item_details(args): cost_center, expense_account = None, None - if not args.get("company"): + if not args.get('company'): return cost_center, expense_account if args.item_code: - item_defaults = frappe.db.get_value( - "Item Default", - {"parent": args.item_code, "company": args.get("company")}, - ["buying_cost_center", "expense_account"], + item_defaults = frappe.db.get_value('Item Default',{ + 'parent': args.item_code, + 'company': args.get('company') + }, + ['buying_cost_center', 'expense_account'], ) if item_defaults: cost_center, expense_account = item_defaults if not (cost_center and expense_account): - for doctype in ["Item Group", "Company"]: + for doctype in ['Item Group', 'Company']: data = get_expense_cost_center(doctype, args) if not cost_center and data: @@ -434,13 +470,31 @@ def get_item_details(args): return cost_center, expense_account def get_expense_cost_center(doctype, args): - if doctype == "Item Group": - return frappe.db.get_value( - "Item Default", - {"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")}, - ["buying_cost_center", "expense_account"], + ''' + Method to get Expense Account and Cost Center from Item Group and Company + ''' + if doctype == 'Item Group': + return frappe.db.get_value('Item Default',{ + 'parent': args.get(frappe.scrub(doctype)), + 'company': args.get('company') + }, + ['buying_cost_center', 'expense_account'], ) else: return frappe.db.get_value( - doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"] + doctype, args.get(frappe.scrub(doctype)), ['cost_center', 'default_expense_account'] ) + +def field_exists(doctype, fieldname): + ''' + Method to check wether field exists or not in a doctype + ''' + meta = frappe.get_meta(doctype) + return meta.has_field(fieldname) + +def get_yearly_budget_amount(budget_id, cost_head=None): + ''' + Method to get Budget Amount for the selected Cost Head in the annual budget + ''' + budget_amount = frappe.db.get_value('M1 Budget Account', {'parent': budget_id, 'cost_head': cost_head}, 'budget_amount') or 0 + return budget_amount diff --git a/beams/hooks.py b/beams/hooks.py index e86069bd1..f86eeb89f 100644 --- a/beams/hooks.py +++ b/beams/hooks.py @@ -188,6 +188,9 @@ # Hook on document methods and events doc_events = { + "*":{ + "on_update": "beams.beams.custom_scripts.utils.validate_budget" + }, "Sales Invoice": { "on_update_after_submit":"beams.beams.custom_scripts.sales_invoice.sales_invoice.on_update_after_submit", "autoname": "beams.beams.custom_scripts.sales_invoice.sales_invoice.autoname", @@ -222,15 +225,11 @@ }, "Purchase Order": { "on_update": "beams.beams.custom_scripts.purchase_order.purchase_order.create_todo_on_finance_verification", - "before_save": "beams.beams.custom_scripts.purchase_order.purchase_order.validate_budget", "validate": "beams.beams.custom_scripts.purchase_order.purchase_order.validate_reason_for_rejection", "on_change":"beams.beams.custom_scripts.purchase_order.purchase_order.update_equipment_quantities" }, "Material Request":{ - "before_save":[ - "beams.beams.custom_scripts.purchase_order.purchase_order.validate_budget", - "beams.beams.custom_scripts.material_request.material_request.set_checkbox_for_item_type" - ], + "before_save": "beams.beams.custom_scripts.material_request.material_request.set_checkbox_for_item_type", "after_insert":"beams.beams.custom_scripts.material_request.material_request.notify_stock_managers", "on_update": "beams.beams.custom_scripts.material_request.material_request.create_todo_for_hod", "validate": "beams.beams.custom_scripts.material_request.material_request.validate" @@ -436,7 +435,7 @@ }, "Supplier Quotation": { "validate":"beams.beams.custom_scripts.supplier_quotation.supplier_quotation.clear_rate_if_no_rate_provided" - } + }, } # Scheduled Tasks diff --git a/beams/patches.txt b/beams/patches.txt index 75675ba81..f69152102 100644 --- a/beams/patches.txt +++ b/beams/patches.txt @@ -2,7 +2,7 @@ beams.patches.rename_hod_role #30-10-2024 beams.patches.no_of_children_patch #06-03-2025 beams.patches.delete_property_setter #21-11-2025 -beams.patches.delete_custom_fields #04-02-2026 +beams.patches.delete_custom_fields #05-02-2026 [post_model_sync] # Patches added in this section will be executed after doctypes are migrated diff --git a/beams/patches/delete_custom_fields.py b/beams/patches/delete_custom_fields.py index 4d26e9275..c23dfe5ba 100644 --- a/beams/patches/delete_custom_fields.py +++ b/beams/patches/delete_custom_fields.py @@ -268,6 +268,26 @@ { 'dt':'Budget Account', 'fieldname': 'equal_monthly_distribution' + }, + { + 'dt':'Expense Claim', + 'fieldname': 'budget_exceeded' + }, + { + 'dt':'Journal Entry', + 'fieldname': 'budget_exceeded' + }, + { + 'dt':'Material Request', + 'fieldname': 'budget_exceeded' + }, + { + 'dt':'Purchase Invoice', + 'fieldname': 'budget_exceeded' + }, + { + 'dt':'Purchase Order', + 'fieldname': 'is_budget_exceed' } ] diff --git a/beams/setup.py b/beams/setup.py index 93d909352..46a20e91a 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -1061,9 +1061,9 @@ def get_purchase_order_custom_fields(): "insert_after": "is_subcontracted" }, { - "fieldname": "is_budget_exceed", + "fieldname": "is_budget_exceeded", "fieldtype": "Check", - "label": "Is Budget Exceed", + "label": "Is Budget Exceeded", "insert_after": "is_budgeted", "no_copy":1, "depends_on": "eval:doc.is_budgeted == 1" @@ -1725,7 +1725,7 @@ def get_purchase_invoice_custom_fields(): "default": "1" }, { - "fieldname": "budget_exceeded", + "fieldname": "is_budget_exceeded", "fieldtype": "Check", "label": "Budget Exceeded", "insert_after": "is_budgeted", @@ -1735,7 +1735,7 @@ def get_purchase_invoice_custom_fields(): "fieldname": "from_bureau", "fieldtype": "Check", "label": "From Bureau", - "insert_after": "budget_exceeded", + "insert_after": "is_budget_exceeded", "hidden": 1, } ] @@ -5547,7 +5547,7 @@ def get_property_setters(): "doc_type": "Purchase Invoice", "property": "field_order", "property_type": "Data", - "value": '["workflow_state", "title", "naming_series", "invoice_type", "purchase_order_id", "stringer_bill_reference", "batta_claim_reference", "supplier", "bureau", "barter_invoice", "quotation", "supplier_name", "ewaybill", "tally_masterid", "tally_voucherno", "tax_id", "company", "column_break_6", "posting_date", "posting_time", "set_posting_time", "due_date", "column_break1", "is_paid", "is_return", "return_against", "update_outstanding_for_self", "update_billed_amount_in_purchase_order", "update_billed_amount_in_purchase_receipt", "apply_tds", "is_reverse_charge", "is_budgeted", "budget_exceeded", "from_bureau", "tax_withholding_category", "amended_from", "payments_section", "mode_of_payment", "base_paid_amount", "clearance_date", "col_br_payments", "cash_bank_account", "paid_amount", "supplier_invoice_details", "bill_no", "column_break_15", "bill_date", "accounting_dimensions_section", "cost_center", "dimension_col_break", "project", "currency_and_price_list", "currency", "conversion_rate", "use_transaction_date_exchange_rate", "column_break2", "buying_price_list", "price_list_currency", "plc_conversion_rate", "ignore_pricing_rule", "sec_warehouse", "scan_barcode", "col_break_warehouse", "update_stock", "set_warehouse", "set_from_warehouse", "is_subcontracted", "rejected_warehouse", "supplier_warehouse", "items_section", "items", "section_break_26", "total_qty", "total_net_weight", "column_break_50", "base_total", "base_net_total", "attach", "column_break_28", "total", "net_total", "tax_withholding_net_total", "base_tax_withholding_net_total", "taxes_section", "tax_category", "taxes_and_charges", "column_break_58", "shipping_rule", "column_break_49", "incoterm", "named_place", "section_break_51", "taxes", "totals", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted", "base_total_taxes_and_charges", "column_break_40", "taxes_and_charges_added", "taxes_and_charges_deducted", "total_taxes_and_charges", "section_break_49", "base_grand_total", "base_rounding_adjustment", "base_rounded_total", "base_in_words", "column_break8", "grand_total", "rounding_adjustment", "use_company_roundoff_cost_center", "rounded_total", "in_words", "total_advance", "outstanding_amount", "disable_rounded_total", "section_break_44", "apply_discount_on", "base_discount_amount", "column_break_46", "additional_discount_percentage", "discount_amount", "tax_withheld_vouchers_section", "tax_withheld_vouchers", "sec_tax_breakup", "other_charges_calculation", "section_gst_breakup", "gst_breakup_table", "pricing_rule_details", "pricing_rules", "raw_materials_supplied", "supplied_items", "payments_tab", "advances_section", "allocate_advances_automatically", "only_include_allocated_payments", "get_advances", "advances", "advance_tax", "write_off", "write_off_amount", "base_write_off_amount", "column_break_61", "write_off_account", "write_off_cost_center", "address_and_contact_tab", "section_addresses", "supplier_address", "address_display", "supplier_gstin", "gst_category", "col_break_address", "contact_person", "contact_display", "contact_mobile", "contact_email", "company_shipping_address_section", "dispatch_address", "dispatch_address_display", "column_break_126", "shipping_address", "shipping_address_display", "company_billing_address_section", "billing_address", "column_break_130", "billing_address_display", "company_gstin", "place_of_supply", "terms_tab", "payment_schedule_section", "payment_terms_template", "ignore_default_payment_terms_template", "payment_schedule", "terms_section_break", "tc_name", "terms", "more_info_tab", "status_section", "status", "column_break_177", "per_received", "accounting_details_section", "credit_to", "party_account_currency", "is_opening", "against_expense_account", "column_break_63", "unrealized_profit_loss_account", "subscription_section", "subscription", "auto_repeat", "update_auto_repeat_reference", "column_break_114", "from_date", "to_date", "printing_settings", "letter_head", "group_same_items", "column_break_112", "select_print_heading", "language", "transporter_info", "transporter", "gst_transporter_id", "driver", "lr_no", "vehicle_no", "distance", "transporter_col_break", "transporter_name", "mode_of_transport", "driver_name", "lr_date", "gst_vehicle_type", "gst_section", "itc_classification", "ineligibility_reason", "reconciliation_status", "sb_14", "on_hold", "release_date", "cb_17", "hold_comment", "additional_info_section", "is_internal_supplier", "represents_company", "supplier_group", "column_break_147", "inter_company_invoice_reference", "is_old_subcontracting_flow", "remarks", "connections_tab"]' + "value": '["workflow_state", "title", "naming_series", "invoice_type", "purchase_order_id", "stringer_bill_reference", "batta_claim_reference", "supplier", "bureau", "barter_invoice", "quotation", "supplier_name", "ewaybill", "tally_masterid", "tally_voucherno", "tax_id", "company", "column_break_6", "posting_date", "posting_time", "set_posting_time", "due_date", "column_break1", "is_paid", "is_return", "return_against", "update_outstanding_for_self", "update_billed_amount_in_purchase_order", "update_billed_amount_in_purchase_receipt", "apply_tds", "is_reverse_charge", "is_budgeted", "is_budget_exceeded", "from_bureau", "tax_withholding_category", "amended_from", "payments_section", "mode_of_payment", "base_paid_amount", "clearance_date", "col_br_payments", "cash_bank_account", "paid_amount", "supplier_invoice_details", "bill_no", "column_break_15", "bill_date", "accounting_dimensions_section", "cost_center", "dimension_col_break", "project", "currency_and_price_list", "currency", "conversion_rate", "use_transaction_date_exchange_rate", "column_break2", "buying_price_list", "price_list_currency", "plc_conversion_rate", "ignore_pricing_rule", "sec_warehouse", "scan_barcode", "col_break_warehouse", "update_stock", "set_warehouse", "set_from_warehouse", "is_subcontracted", "rejected_warehouse", "supplier_warehouse", "items_section", "items", "section_break_26", "total_qty", "total_net_weight", "column_break_50", "base_total", "base_net_total", "attach", "column_break_28", "total", "net_total", "tax_withholding_net_total", "base_tax_withholding_net_total", "taxes_section", "tax_category", "taxes_and_charges", "column_break_58", "shipping_rule", "column_break_49", "incoterm", "named_place", "section_break_51", "taxes", "totals", "base_taxes_and_charges_added", "base_taxes_and_charges_deducted", "base_total_taxes_and_charges", "column_break_40", "taxes_and_charges_added", "taxes_and_charges_deducted", "total_taxes_and_charges", "section_break_49", "base_grand_total", "base_rounding_adjustment", "base_rounded_total", "base_in_words", "column_break8", "grand_total", "rounding_adjustment", "use_company_roundoff_cost_center", "rounded_total", "in_words", "total_advance", "outstanding_amount", "disable_rounded_total", "section_break_44", "apply_discount_on", "base_discount_amount", "column_break_46", "additional_discount_percentage", "discount_amount", "tax_withheld_vouchers_section", "tax_withheld_vouchers", "sec_tax_breakup", "other_charges_calculation", "section_gst_breakup", "gst_breakup_table", "pricing_rule_details", "pricing_rules", "raw_materials_supplied", "supplied_items", "payments_tab", "advances_section", "allocate_advances_automatically", "only_include_allocated_payments", "get_advances", "advances", "advance_tax", "write_off", "write_off_amount", "base_write_off_amount", "column_break_61", "write_off_account", "write_off_cost_center", "address_and_contact_tab", "section_addresses", "supplier_address", "address_display", "supplier_gstin", "gst_category", "col_break_address", "contact_person", "contact_display", "contact_mobile", "contact_email", "company_shipping_address_section", "dispatch_address", "dispatch_address_display", "column_break_126", "shipping_address", "shipping_address_display", "company_billing_address_section", "billing_address", "column_break_130", "billing_address_display", "company_gstin", "place_of_supply", "terms_tab", "payment_schedule_section", "payment_terms_template", "ignore_default_payment_terms_template", "payment_schedule", "terms_section_break", "tc_name", "terms", "more_info_tab", "status_section", "status", "column_break_177", "per_received", "accounting_details_section", "credit_to", "party_account_currency", "is_opening", "against_expense_account", "column_break_63", "unrealized_profit_loss_account", "subscription_section", "subscription", "auto_repeat", "update_auto_repeat_reference", "column_break_114", "from_date", "to_date", "printing_settings", "letter_head", "group_same_items", "column_break_112", "select_print_heading", "language", "transporter_info", "transporter", "gst_transporter_id", "driver", "lr_no", "vehicle_no", "distance", "transporter_col_break", "transporter_name", "mode_of_transport", "driver_name", "lr_date", "gst_vehicle_type", "gst_section", "itc_classification", "ineligibility_reason", "reconciliation_status", "sb_14", "on_hold", "release_date", "cb_17", "hold_comment", "additional_info_section", "is_internal_supplier", "represents_company", "supplier_group", "column_break_147", "inter_company_invoice_reference", "is_old_subcontracting_flow", "remarks", "connections_tab"]' }, ] @@ -5596,7 +5596,7 @@ def get_material_request_custom_fields(): "insert_after": "location", }, { - "fieldname": "budget_exceeded", + "fieldname": "is_budget_exceeded", "fieldtype": "Check", "label": " Is Budget Exceed", "insert_after": "is_budgeted", @@ -5770,7 +5770,7 @@ def get_journal_entry_custom_fields(): "insert_after": "apply_tds", }, { - "fieldname": "budget_exceeded", + "fieldname": "is_budget_exceeded", "fieldtype": "Check", "label": " Is Budget Exceed", "insert_after": "is_budgeted", @@ -6167,7 +6167,7 @@ def get_expense_claim_custom_fields(): "insert_after": "travel_request", }, { - "fieldname": "budget_exceeded", + "fieldname": "is_budget_exceeded", "fieldtype": "Check", "label": " Is Budget Exceed", "insert_after": "is_budgeted", From e30ca1350bf2d324aa92adbcb36162bb824a72ae Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Thu, 5 Feb 2026 16:54:06 +0530 Subject: [PATCH 26/50] refactor: fieldname changed to is_budget_exceeded --- beams/beams/custom_scripts/expense_claim/expense_claim.js | 4 ++-- beams/beams/custom_scripts/journal_entry/journal_entry.js | 4 ++-- .../custom_scripts/material_request/material_request.js | 4 ++-- .../employee_travel_request/employee_travel_request.js | 6 +++--- .../employee_travel_request/employee_travel_request.py | 8 ++++---- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/beams/beams/custom_scripts/expense_claim/expense_claim.js b/beams/beams/custom_scripts/expense_claim/expense_claim.js index 096733ad0..1273084e6 100644 --- a/beams/beams/custom_scripts/expense_claim/expense_claim.js +++ b/beams/beams/custom_scripts/expense_claim/expense_claim.js @@ -8,10 +8,10 @@ frappe.ui.form.on('Expense Claim',{ }); /** -* clear the "budget_exceeded" checkbox if "is_budgeted" is unchecked. +* clear the "is_budget_exceeded" checkbox if "is_budgeted" is unchecked. */ function clear_checkbox_exceed(frm){ if(frm.doc.is_budgeted == 0){ - frm.set_value("budget_exceeded", 0); + frm.set_value("is_budget_exceeded", 0); } } diff --git a/beams/beams/custom_scripts/journal_entry/journal_entry.js b/beams/beams/custom_scripts/journal_entry/journal_entry.js index 67c2c4b0d..078936ec4 100644 --- a/beams/beams/custom_scripts/journal_entry/journal_entry.js +++ b/beams/beams/custom_scripts/journal_entry/journal_entry.js @@ -8,10 +8,10 @@ frappe.ui.form.on('Journal Entry',{ }); /** -* Clears the "budget_exceeded" checkbox if "is_budgeted" is unchecked. +* Clears the "is_budget_exceeded" checkbox if "is_budgeted" is unchecked. */ function clear_checkbox_exceed(frm){ if(frm.doc.is_budgeted == 0){ - frm.set_value("budget_exceeded", 0); + frm.set_value("is_budget_exceeded", 0); } } diff --git a/beams/beams/custom_scripts/material_request/material_request.js b/beams/beams/custom_scripts/material_request/material_request.js index a7ea037de..73d1c5942 100644 --- a/beams/beams/custom_scripts/material_request/material_request.js +++ b/beams/beams/custom_scripts/material_request/material_request.js @@ -49,10 +49,10 @@ function calculate_total_amount(frm) { } /** -* Clears the "budget_exceeded" checkbox if "is_budgeted" is unchecked. +* Clears the "is_budget_exceeded" checkbox if "is_budgeted" is unchecked. */ function clear_checkbox_exceed(frm){ if (frm.doc.is_budgeted == 0){ - frm.set_value("budget_exceeded", 0); + frm.set_value("is_budget_exceeded", 0); } } diff --git a/beams/beams/doctype/employee_travel_request/employee_travel_request.js b/beams/beams/doctype/employee_travel_request/employee_travel_request.js index d6a4dc79d..16b9f88db 100644 --- a/beams/beams/doctype/employee_travel_request/employee_travel_request.js +++ b/beams/beams/doctype/employee_travel_request/employee_travel_request.js @@ -83,7 +83,7 @@ frappe.ui.form.on('Employee Travel Request', { expenses: expenses, mode_of_payment: values.mode_of_payment, is_budgeted: frm.doc.is_budgeted || 0, - budget_exceeded: frm.doc.is_budget_exceeded || 0 + is_budget_exceeded: frm.doc.is_budget_exceeded || 0 }, callback: function (r) { if (!r.exc) { @@ -200,7 +200,7 @@ frappe.ui.form.on('Employee Travel Request', { travel_request: frm.doc.name, expenses: expenses, is_budgeted: frm.doc.is_budgeted || 0, - budget_exceeded: frm.doc.is_budget_exceeded || 0 + is_budget_exceeded: frm.doc.is_budget_exceeded || 0 }, callback: function (r) { if (!r.exc) { @@ -509,7 +509,7 @@ function create_batta_claim_from_travel(frm) { } /** -* clear the "budget_exceeded" checkbox if "is_budgeted" is unchecked. +* clear the "is_budget_exceeded" checkbox if "is_budgeted" is unchecked. */ function clear_checkbox_exceed(frm){ if(!frm.doc.is_budgeted){ diff --git a/beams/beams/doctype/employee_travel_request/employee_travel_request.py b/beams/beams/doctype/employee_travel_request/employee_travel_request.py index b6ef7b8f8..453890bf1 100644 --- a/beams/beams/doctype/employee_travel_request/employee_travel_request.py +++ b/beams/beams/doctype/employee_travel_request/employee_travel_request.py @@ -509,7 +509,7 @@ def filter_mode_of_travel(batta_policy_name): return [] @frappe.whitelist() -def create_expense_claim(employee, travel_request, expenses, is_budgeted, budget_exceeded): +def create_expense_claim(employee, travel_request, expenses, is_budgeted, is_budget_exceeded): ''' Create an Expense Claim from Travel Request. ''' @@ -523,7 +523,7 @@ def create_expense_claim(employee, travel_request, expenses, is_budgeted, budget expense_claim = frappe.new_doc("Expense Claim") expense_claim.travel_request = travel_request expense_claim.is_budgeted = is_budgeted - expense_claim.budget_exceeded = budget_exceeded + expense_claim.is_budget_exceeded = is_budget_exceeded expense_claim.employee = employee expense_claim.approval_status = "Draft" expense_claim.posting_date = today() @@ -713,7 +713,7 @@ def get_permission_query_conditions(user): return " OR ".join(f"({cond.strip()})" for cond in conditions) @frappe.whitelist() -def create_journal_entry_from_travel(employee, employee_travel_request, expenses, mode_of_payment, is_budgeted, budget_exceeded): +def create_journal_entry_from_travel(employee, employee_travel_request, expenses, mode_of_payment, is_budgeted, is_budget_exceeded): """ Create a Journal Entry from Travel Request """ @@ -743,7 +743,7 @@ def create_journal_entry_from_travel(employee, employee_travel_request, expenses jv.user_remark = f"Journal Entry for Travel Request {employee_travel_request}" jv.employee = employee jv.employee_travel_request = employee_travel_request - jv.budget_exceeded = budget_exceeded + jv.is_budget_exceeded = is_budget_exceeded jv.is_budgeted = is_budgeted jv.docstatus = 0 From eaeea1e2a1af826f005f5ac126e156a15a40bcc1 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Fri, 6 Feb 2026 10:57:47 +0530 Subject: [PATCH 27/50] feat: prevent budget template duplicate with cost center --- .../budget_template/budget_template.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/beams/beams/doctype/budget_template/budget_template.py b/beams/beams/doctype/budget_template/budget_template.py index a9ead957e..c1ef776de 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -3,9 +3,13 @@ import frappe from frappe.model.document import Document +from frappe import _ class BudgetTemplate(Document): + def validate(self): + self.validate_account_per_cost_center() + def set_default_account(self): """ Set the default account for each budget template item based on the associated cost subhead and company. """ @@ -29,6 +33,54 @@ def before_save(self): self.set_default_account() + def validate_account_per_cost_center(self): + if not self.budget_template_items and self.cost_center: + return + + for row in self.budget_template_items or []: + if not row.account_head: + continue + + duplicates = frappe.get_all( + "Budget Template Item", + filters={ + "account_head": row.account_head, + "parenttype": "Budget Template", + "parent": ["!=", self.name], + }, + fields=["parent"], + limit=1, + ) + + if not duplicates: + continue + + template = duplicates[0].parent + + # check cost center of that template + cost_center = frappe.db.get_value( + "Budget Template", template, "cost_center" + ) + + if cost_center != self.cost_center: + continue + + template_link = frappe.utils.get_link_to_form( + "Budget Template", template + ) + + frappe.throw( + _( + "Account : {0} is used in the {1} Budget Template " + "with the same Cost Center : {2}." + ).format( + row.account_head, + template_link, + self.cost_center, + ), + title=_("Duplicate Account Found"), + ) + @frappe.whitelist() def get_budget_approver_employees(doctype, txt, searchfield, start, page_len, filters): From 58af0240e2c6d8f931671b0c1d6081e90dfd54f7 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Fri, 6 Feb 2026 10:58:26 +0530 Subject: [PATCH 28/50] feat: add budget region in company --- beams/beams/doctype/budget_region/__init__.py | 0 .../doctype/budget_region/budget_region.js | 8 +++ .../doctype/budget_region/budget_region.json | 49 +++++++++++++++++++ .../doctype/budget_region/budget_region.py | 9 ++++ .../budget_region/test_budget_region.py | 9 ++++ beams/setup.py | 7 +++ 6 files changed, 82 insertions(+) create mode 100644 beams/beams/doctype/budget_region/__init__.py create mode 100644 beams/beams/doctype/budget_region/budget_region.js create mode 100644 beams/beams/doctype/budget_region/budget_region.json create mode 100644 beams/beams/doctype/budget_region/budget_region.py create mode 100644 beams/beams/doctype/budget_region/test_budget_region.py diff --git a/beams/beams/doctype/budget_region/__init__.py b/beams/beams/doctype/budget_region/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/beams/beams/doctype/budget_region/budget_region.js b/beams/beams/doctype/budget_region/budget_region.js new file mode 100644 index 000000000..b19a898c8 --- /dev/null +++ b/beams/beams/doctype/budget_region/budget_region.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, efeone and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Budget Region", { +// refresh(frm) { + +// }, +// }); diff --git a/beams/beams/doctype/budget_region/budget_region.json b/beams/beams/doctype/budget_region/budget_region.json new file mode 100644 index 000000000..f516f8624 --- /dev/null +++ b/beams/beams/doctype/budget_region/budget_region.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:budget_region", + "creation": "2026-02-05 14:28:41.918778", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "budget_region" + ], + "fields": [ + { + "fieldname": "budget_region", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Budget Region", + "reqd": 1, + "unique": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-02-05 14:36:01.274733", + "modified_by": "Administrator", + "module": "BEAMS", + "name": "Budget Region", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/beams/beams/doctype/budget_region/budget_region.py b/beams/beams/doctype/budget_region/budget_region.py new file mode 100644 index 000000000..839cb4405 --- /dev/null +++ b/beams/beams/doctype/budget_region/budget_region.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, efeone and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BudgetRegion(Document): + pass diff --git a/beams/beams/doctype/budget_region/test_budget_region.py b/beams/beams/doctype/budget_region/test_budget_region.py new file mode 100644 index 000000000..8392c2007 --- /dev/null +++ b/beams/beams/doctype/budget_region/test_budget_region.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, efeone and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBudgetRegion(FrappeTestCase): + pass diff --git a/beams/setup.py b/beams/setup.py index c6bbf7c89..f065f4fd3 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -3589,6 +3589,13 @@ def get_company_custom_fields(): "label": "Budget Exchange Rate to INR", "insert_after": "exception_budget_column", "description": "1 Unit of Company Currency = [?] INR" + }, + { + "fieldtype": "Link", + "fieldname": "budget_region", + "label": "Budget Region", + "options": "Budget Region", + "insert_after": "default_holiday_list" } ] } From 987c6c0a5c34ee0b1270d6672375cbc228d094b2 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Fri, 6 Feb 2026 11:36:46 +0530 Subject: [PATCH 29/50] feat: validate cost head for same template --- .../budget_template/budget_template.py | 111 ++++++++++-------- beams/setup.py | 2 +- 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/beams/beams/doctype/budget_template/budget_template.py b/beams/beams/doctype/budget_template/budget_template.py index c1ef776de..f664465f5 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -33,53 +33,70 @@ def before_save(self): self.set_default_account() - def validate_account_per_cost_center(self): - if not self.budget_template_items and self.cost_center: - return - - for row in self.budget_template_items or []: - if not row.account_head: - continue - - duplicates = frappe.get_all( - "Budget Template Item", - filters={ - "account_head": row.account_head, - "parenttype": "Budget Template", - "parent": ["!=", self.name], - }, - fields=["parent"], - limit=1, - ) - - if not duplicates: - continue - - template = duplicates[0].parent - - # check cost center of that template - cost_center = frappe.db.get_value( - "Budget Template", template, "cost_center" - ) - - if cost_center != self.cost_center: - continue - - template_link = frappe.utils.get_link_to_form( - "Budget Template", template - ) - - frappe.throw( - _( - "Account : {0} is used in the {1} Budget Template " - "with the same Cost Center : {2}." - ).format( - row.account_head, - template_link, - self.cost_center, - ), - title=_("Duplicate Account Found"), - ) +def validate_account_per_cost_center(self): + """ + Validates that there are no duplicate Cost Heads within the same Budget Template and + no duplicate Account Heads across different Budget Templates for the same Cost Center. + """ + + if not self.cost_center or not self.budget_template_items: + return + + seen_cost_heads = set() + + for row in self.budget_template_items: + # Duplicate Cost Head in same Template + if row.cost_head: + if row.cost_head in seen_cost_heads: + frappe.throw( + _("Duplicate Cost Head {0} is not allowed in the same Budget Template.") + .format(row.cost_head), + title=_("Duplicate Cost Head"), + ) + seen_cost_heads.add(row.cost_head) + + # Duplicate Account across Templates (same Cost Center) + if not row.account_head: + continue + + duplicates = frappe.get_all( + "Budget Template Item", + filters={ + "account_head": row.account_head, + "parenttype": "Budget Template", + "parent": ["!=", self.name], + }, + fields=["parent"], + limit=1, + ) + + if not duplicates: + continue + + template = duplicates[0].parent + + template_cost_center = frappe.db.get_value( + "Budget Template", template, "cost_center" + ) + + if template_cost_center != self.cost_center: + continue + + template_link = frappe.utils.get_link_to_form( + "Budget Template", template + ) + + frappe.throw( + _( + "Account : {0} is used in the {1} Budget Template " + "with the same Cost Center : {2}." + ).format( + row.account_head, + template_link, + self.cost_center, + ), + title=_("Duplicate Account Found"), + ) @frappe.whitelist() diff --git a/beams/setup.py b/beams/setup.py index f065f4fd3..49a33c5a1 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -3596,7 +3596,7 @@ def get_company_custom_fields(): "label": "Budget Region", "options": "Budget Region", "insert_after": "default_holiday_list" - } + }, ] } From 9f88d66012ef98a62ecbffcd6a8ba1f5fb7765f0 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Fri, 6 Feb 2026 11:39:34 +0530 Subject: [PATCH 30/50] refactor: whitesape to tab and costhead filter on report view --- beams/beams/overrides/budget.py | 1 + beams/setup.py | 225 ++++++++++++++++---------------- 2 files changed, 113 insertions(+), 113 deletions(-) diff --git a/beams/beams/overrides/budget.py b/beams/beams/overrides/budget.py index dbdfabc49..fa5d0d3de 100644 --- a/beams/beams/overrides/budget.py +++ b/beams/beams/overrides/budget.py @@ -201,6 +201,7 @@ def get_expense_breakup(args, currency, budget_against): args.budget_against_field: budget_against, 'account': args.account, 'company': args.company, + 'cost_head': args.get('cost_head'), } ) diff --git a/beams/setup.py b/beams/setup.py index c6bbf7c89..4543512e4 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -749,7 +749,7 @@ def get_leave_application_custom_fields(): "fieldtype": "Attach", "label": "Medical Certificate", "hidden": 1, - "insert_after": "leave_type" + "insert_after": "leave_type" } ] @@ -1677,7 +1677,7 @@ def get_quotation_custom_fields(): "label": "Sales Type", "options": "Sales Type", "insert_after": "item_name" - } + } ] } @@ -1866,60 +1866,60 @@ def get_item_custom_fields(): "label": "Sales Type", "options": "Sales Type", "insert_after": "is_production_item" - }, - { - "fieldname": "hireable", - "fieldtype": "Check", - "label": "Hireable", - "fetch_from":"item_group.hireable", - "set_only_once":1, - "insert_after": "gst_hsn_code" - }, - { - "fieldname": "service_item", - "fieldtype": "Link", - "label": "Service Item", - "options": "Item", - "read_only":1, - "insert_after": "item_group" - }, - { - "fieldname": "item_audit_notification", - "fieldtype": "Check", - "label": "Periodic Notification for Asset Auditing ", - "depends_on": "eval:doc.is_fixed_asset == 1", - "insert_after": "asset_category" - }, - { - "fieldname": "item_notification_frequency", - "fieldtype": "Select", - "label": "Notification Frequency", - "options":"\nMonthly\nTrimonthly\nQuarterly\nHalf Yearly\nYearly", - "depends_on": "eval:doc.item_audit_notification == 1", - "insert_after": "item_audit_notification" - } , - { - "fieldname": "item_notification_template", - "fieldtype": "Link", - "label": "Notification Template", - "options":"Email Template", - "depends_on": "eval:doc.item_audit_notification == 1", - "insert_after": "item_notification_frequency" - }, - { - "fieldname": "start_notification_from", - "fieldtype": "Select", - "label": "Start Notification From", - "options":"\nJanuary\nFebruary\nMarch\nApril\nMay\nJune\nJuly\nAugust\nSeptember\nOctober\nNovember\nDecember", - "depends_on": "eval:doc.item_audit_notification == 1", - "insert_after": "item_audit_notification" - }, - { - "fieldname": "is_makeup_item", - "fieldtype": "Check", - "label": "Is Makeup Item", - "insert_after": "is_exempt" - }, + }, + { + "fieldname": "hireable", + "fieldtype": "Check", + "label": "Hireable", + "fetch_from":"item_group.hireable", + "set_only_once":1, + "insert_after": "gst_hsn_code" + }, + { + "fieldname": "service_item", + "fieldtype": "Link", + "label": "Service Item", + "options": "Item", + "read_only":1, + "insert_after": "item_group" + }, + { + "fieldname": "item_audit_notification", + "fieldtype": "Check", + "label": "Periodic Notification for Asset Auditing ", + "depends_on": "eval:doc.is_fixed_asset == 1", + "insert_after": "asset_category" + }, + { + "fieldname": "item_notification_frequency", + "fieldtype": "Select", + "label": "Notification Frequency", + "options":"\nMonthly\nTrimonthly\nQuarterly\nHalf Yearly\nYearly", + "depends_on": "eval:doc.item_audit_notification == 1", + "insert_after": "item_audit_notification" + }, + { + "fieldname": "item_notification_template", + "fieldtype": "Link", + "label": "Notification Template", + "options":"Email Template", + "depends_on": "eval:doc.item_audit_notification == 1", + "insert_after": "item_notification_frequency" + }, + { + "fieldname": "start_notification_from", + "fieldtype": "Select", + "label": "Start Notification From", + "options":"\nJanuary\nFebruary\nMarch\nApril\nMay\nJune\nJuly\nAugust\nSeptember\nOctober\nNovember\nDecember", + "depends_on": "eval:doc.item_audit_notification == 1", + "insert_after": "item_audit_notification" + }, + { + "fieldname": "is_makeup_item", + "fieldtype": "Check", + "label": "Is Makeup Item", + "insert_after": "is_exempt" + }, { "fieldname": "item_type", "fieldtype": "Select", @@ -2256,7 +2256,6 @@ def get_employee_custom_fields(): "insert_after": "address_section" }, ], - "Employee External Work History":[ { "fieldname": "period_from", @@ -2323,7 +2322,7 @@ def get_voucher_entry_custom_fields(): "insert_after": "project", "default": "1", }, - { + { "fieldname": "is_budget_exceeded", "fieldtype": "Check", "label": "Is Budget Exceeded", @@ -3488,7 +3487,7 @@ def get_job_opening_custom_fields(): "insert_after": "license_type", }, { - "fieldname": "min_education_qual", + "fieldname": "min_education_qual", "fieldtype": "Table MultiSelect", "label": "Preferred Educational Qualification", 'options':"Educational Qualifications", @@ -3535,7 +3534,7 @@ def get_job_opening_custom_fields(): "insert_after": "skill_proficiency" }, { - "fieldname": "interview_rounds", + "fieldname": "interview_rounds", "fieldtype": "Table MultiSelect", "label": "Interview Rounds", 'options':"Interview Rounds", @@ -3714,24 +3713,24 @@ def get_leave_type_custom_fields(): "insert_after": "min_continuous_days_allowed" }, { - "fieldname": "is_proof_document", - "fieldtype": "Check", - "label": "Is Proof Document Required", - "insert_after": "is_optional_leave" + "fieldname": "is_proof_document", + "fieldtype": "Check", + "label": "Is Proof Document Required", + "insert_after": "is_optional_leave" }, { "fieldname": "medical_leave_required", - "fieldtype": "Float", - "label": "Medical Leave Required for Days", - "depends_on": "eval:doc.is_proof_document", - "insert_after": "is_proof_document" + "fieldtype": "Float", + "label": "Medical Leave Required for Days", + "depends_on": "eval:doc.is_proof_document", + "insert_after": "is_proof_document" }, { - "fieldname": "allow_in_notice_period", - "fieldtype": "Check", - "label": "Allow in Notice Period", - "insert_after": "is_compensatory" + "fieldname": "allow_in_notice_period", + "fieldtype": "Check", + "label": "Allow in Notice Period", + "insert_after": "is_compensatory" }, { @@ -5132,7 +5131,7 @@ def get_property_setters(): "property": "allow_in_quick_entry", "value": 1, }, - { + { "doctype_or_field": "DocField", "doc_type": "HD Ticket", "field_name": "agent_group", @@ -5952,14 +5951,13 @@ def get_training_event_custom_fields(): ''' return { "Training Event": [ - { + { "fieldname": "training_request", "fieldtype": "Link", "label": "Training Request", "options": "Training Request", "insert_after": "company", "hidden": 1 - } ] } @@ -6018,12 +6016,12 @@ def get_email_templates(): 'name': 'Job Applicant Follow Up', 'subject': "{{applicant_name}}, Complete your Application", 'response': """Dear {{ applicant_name }}, - We're excited to move forward with your application! - To continue, please upload the required documents by clicking the link: Click Here. - Thank you for your interest in joining us! - If you have any questions, feel free to reach out. - Best regards, - HR Manager""" + We're excited to move forward with your application! + To continue, please upload the required documents by clicking the link: Click Here. + Thank you for your interest in joining us! + If you have any questions, feel free to reach out. + Best regards, + HR Manager""" } ] @@ -6273,13 +6271,13 @@ def get_supplier_quotation_custom_fields(): "label": "Attachments", "insert_after": "base_net_total" }, - { + { "fieldname": "suggested_items_by_supplier", "fieldtype": "Table", "label": "Suggested Items by Supplier", "options": "Suggested Items By Supplier", "insert_after": "items" - }, + }, { "fieldname": "priority", "fieldtype": "Select", @@ -6288,8 +6286,8 @@ def get_supplier_quotation_custom_fields(): "default":"Medium", "insert_after": "company", "in_list_view": 1 - }, - + }, + ] } @@ -6446,31 +6444,32 @@ def get_hd_agent_custom_fields(): } def update_portal_settings(): - """Update Portal Settings: - - Remove standard RFQ & SQ pages - - Add custom menu items with custom routes and roles - """ - portal_settings = frappe.get_single('Portal Settings') - replace_titles = ["Request for Quotations", "Supplier Quotation"] - portal_settings.menu = [row for row in portal_settings.menu if row.title not in replace_titles] - custom_menu = [ - { - "title": "Request for Quotations", - "route": "/request_for_quotation_list_view", - "enabled": 1, - "reference_doctype": "Request for Quotation", - "role": "Supplier" - }, - { - "title": "Supplier Quotation", - "route": "/supplier_quotation_list_view", - "enabled": 1, - "reference_doctype": "Supplier Quotation", - "role": "Supplier" - } - ] - existing_titles = [row.title for row in portal_settings.custom_menu] - for item in custom_menu: - if item["title"] not in existing_titles: - portal_settings.append("custom_menu", item) - portal_settings.save() + """ + Update Portal Settings: + - Remove standard RFQ & SQ pages + - Add custom menu items with custom routes and roles + """ + portal_settings = frappe.get_single('Portal Settings') + replace_titles = ["Request for Quotations", "Supplier Quotation"] + portal_settings.menu = [row for row in portal_settings.menu if row.title not in replace_titles] + custom_menu = [ + { + "title": "Request for Quotations", + "route": "/request_for_quotation_list_view", + "enabled": 1, + "reference_doctype": "Request for Quotation", + "role": "Supplier" + }, + { + "title": "Supplier Quotation", + "route": "/supplier_quotation_list_view", + "enabled": 1, + "reference_doctype": "Supplier Quotation", + "role": "Supplier" + } + ] + existing_titles = [row.title for row in portal_settings.custom_menu] + for item in custom_menu: + if item["title"] not in existing_titles: + portal_settings.append("custom_menu", item) + portal_settings.save() From 5bb84043beb284c666285985766781ab05230a40 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Fri, 6 Feb 2026 13:43:03 +0530 Subject: [PATCH 31/50] feat: Budget Allocation Report with new changes --- .../budget_allocation/budget_allocation.js | 256 ++++++++---------- .../budget_allocation/budget_allocation.py | 95 ++++--- 2 files changed, 159 insertions(+), 192 deletions(-) diff --git a/beams/beams/report/budget_allocation/budget_allocation.js b/beams/beams/report/budget_allocation/budget_allocation.js index 971764fee..dbd3bd8f3 100644 --- a/beams/beams/report/budget_allocation/budget_allocation.js +++ b/beams/beams/report/budget_allocation/budget_allocation.js @@ -2,160 +2,128 @@ // For license information, please see license.txt frappe.query_reports["Budget Allocation"] = { - filters: get_filters(), - formatter: function (value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data); + filters: get_filters(), + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); - if (column.fieldname.includes(__("variance"))) { - if (data[column.fieldname] < 0) { - value = "" + value + ""; - } else if (data[column.fieldname] > 0) { - value = "" + value + ""; - } - } + if (column.fieldname.includes(__("variance"))) { + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } - return value; - }, - after_datatable_render: function (datatable) { - let data = frappe.query_report.data; - if (!data || !data.length) return; + return value; + }, + after_datatable_render: function (datatable) { + let data = frappe.query_report.data; + if (!data || !data.length) return; - let columns = frappe.query_report.columns; - let total_row = {}; + let columns = frappe.query_report.columns; + let total_row = {}; - // Check if total row already exists - let first_column = columns[0]?.fieldname; - let total_row_exists = data.some(row => row[first_column] === __("Total")); - if (total_row_exists) return; + // Check if total row already exists + let first_column = columns[0]?.fieldname; + let total_row_exists = data.some(row => row[first_column] === __("Total")); + if (total_row_exists) return; - columns.forEach((col) => { - if (col.fieldtype === "Currency" || col.fieldtype === "Float") { - total_row[col.fieldname] = data.reduce((sum, row) => sum + (row[col.fieldname] || 0), 0); - } else { - total_row[col.fieldname] = col.fieldname === first_column ? __("Total") : ""; - } - }); + columns.forEach((col) => { + if (col.fieldtype === "Currency" || col.fieldtype === "Float") { + total_row[col.fieldname] = data.reduce((sum, row) => sum + (row[col.fieldname] || 0), 0); + } else { + total_row[col.fieldname] = col.fieldname === first_column ? __("Total") : ""; + } + }); - data.push(total_row); - datatable.refresh(data); - } + data.push(total_row); + datatable.refresh(data); + } }; function get_filters() { - function get_dimensions() { - let result = []; - frappe.call({ - method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", - args: { - with_cost_center_and_project: true, - }, - async: false, - callback: function (r) { - if (!r.exc) { - result = r.message[0].map((elem) => elem.document_type); - } - }, - }); - return result; - } + let filters = [ + { + fieldname: "from_fiscal_year", + label: __("From Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + }, + { + fieldname: "to_fiscal_year", + label: __("To Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + }, + { + fieldname: "period", + label: __("Period"), + fieldtype: "Select", + options: [ + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, + ], + default: "Yearly", + reqd: 1, + }, + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + { + fieldname: "budget_against", + label: __("Budget Against"), + fieldtype: "Select", + options: "Cost Center\nProject", + default: "Cost Center", + reqd: 1, + on_change: function () { + frappe.query_report.set_filter_value("budget_against_filter", []); + frappe.query_report.refresh(); + }, + }, + { + fieldname: "budget_against_filter", + label: __("Dimension Filter"), + fieldtype: "MultiSelectList", + get_data: function (txt) { + if (!frappe.query_report.filters) return; - let budget_against_options = get_dimensions(); + let budget_against = frappe.query_report.get_filter_value("budget_against"); + if (!budget_against) return; - let filters = [ - { - fieldname: "from_fiscal_year", - label: __("From Fiscal Year"), - fieldtype: "Link", - options: "Fiscal Year", - default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - reqd: 1, - }, - { - fieldname: "to_fiscal_year", - label: __("To Fiscal Year"), - fieldtype: "Link", - options: "Fiscal Year", - default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - reqd: 1, - }, - { - fieldname: "period", - label: __("Period"), - fieldtype: "Select", - options: [ - { value: "Monthly", label: __("Monthly") }, - { value: "Quarterly", label: __("Quarterly") }, - { value: "Half-Yearly", label: __("Half-Yearly") }, - { value: "Yearly", label: __("Yearly") }, - ], - default: "Yearly", - reqd: 1, - }, - { - fieldname: "company", - label: __("Company"), - fieldtype: "Link", - options: "Company", - default: frappe.defaults.get_user_default("Company"), - reqd: 1, - }, - { - fieldname: "budget_against", - label: __("Budget Against"), - fieldtype: "Select", - options: budget_against_options, - default: "Cost Center", - reqd: 1, - on_change: function () { - frappe.query_report.set_filter_value("budget_against_filter", []); - frappe.query_report.refresh(); - }, - }, - { - fieldname: "budget_against_filter", - label: __("Dimension Filter"), - fieldtype: "MultiSelectList", - get_data: function (txt) { - if (!frappe.query_report.filters) return; + return frappe.db.get_link_options(budget_against, txt); + }, + }, + { + fieldname: "cost_head", + label: __("Cost Head"), + fieldtype: "Link", + options: "Cost Head" + }, + { + fieldname: "budget_group", + label: __("Budget Group"), + fieldtype: "Link", + options: "Budget Group" + }, + { + fieldname: "cost_category", + label: __("Cost Category"), + fieldtype: "Link", + options: "Cost Category" + } + ]; - let budget_against = frappe.query_report.get_filter_value("budget_against"); - if (!budget_against) return; - - return frappe.db.get_link_options(budget_against, txt); - }, - }, - { - fieldname: "finance_group", - label: __("Finance Group"), - fieldtype: "Link", - options: "Finance Group" - }, - { - fieldname: "cost_head", - label: __("Cost Head"), - fieldtype: "Link", - options: "Cost Head" - }, - { - fieldname: "cost_subhead", - label: __("Cost Subhead"), - fieldtype: "Link", - options: "Cost Subhead", - get_query: function () { - return { - filters: { - 'cost_head': frappe.query_report.get_filter_value('cost_head') - } - } - } - }, - { - fieldname: "cost_category", - label: __("Cost Category"), - fieldtype: "Link", - options: "Cost Category" - } - ]; - - return filters; + return filters; } \ No newline at end of file diff --git a/beams/beams/report/budget_allocation/budget_allocation.py b/beams/beams/report/budget_allocation/budget_allocation.py index c3422e829..d640adee5 100644 --- a/beams/beams/report/budget_allocation/budget_allocation.py +++ b/beams/beams/report/budget_allocation/budget_allocation.py @@ -1,10 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt - -import datetime - import frappe +import datetime from frappe import _ from frappe.utils import flt, formatdate @@ -34,12 +32,15 @@ def execute(filters=None): def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation): - for account, monthwise_data in dimension_items.items(): - cost_head = monthwise_data.get("cost_head", "") - cost_subhead = monthwise_data.get("cost_subhead", "") - cost_category = monthwise_data.get("cost_category", "") # Added cost_category - - row = [dimension, account, cost_head, cost_subhead, cost_category] # Added cost_category to row + ''' + Method to prepare the final data for the report based on the dimension (Cost Center/Project) and the month-wise target distribution details. + ''' + for cost_head, monthwise_data in dimension_items.items(): + account = monthwise_data.get("account", "") + budget_group = monthwise_data.get("budget_group", "") + cost_category = monthwise_data.get("cost_category", "") + + row = [dimension, cost_head, account, budget_group, cost_category] totals = [0] for year in get_fiscal_years(filters): @@ -73,32 +74,34 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "budget_against", "options": filters.get("budget_against"), - "width": 150, + "width": 200, + }, + { + "label": _("Cost Head"), + "fieldname": "cost_head", + "fieldtype": "Link", + "options": "Cost Head", + "width": 200, }, { "label": _("Account"), "fieldname": "Account", "fieldtype": "Link", "options": "Account", - "width": 150, + "width": 220, }, { - "label": _("Cost Head"), - "fieldname": "cost_head", - "fieldtype": "Data", - "width": 150, - }, - { - "label": _("Cost Subhead"), - "fieldname": "cost_subhead", - "fieldtype": "Data", - "width": 150, + "label": _("Budget Group"), + "fieldname": "budget_group", + "fieldtype": "Link", + "options": "Budget Group", + "width": 200, }, { "label": _("Cost Category"), "fieldname": "cost_category", "fieldtype": "Data", - "width": 150, + "width": 160, } ] @@ -170,7 +173,7 @@ def get_cost_centers(filters): from `tab{tab}` """.format(tab=filters.get("budget_against")) - ) # nosec + ) # nosec # Get dimension & target details @@ -183,8 +186,8 @@ def get_dimension_target_details(filters): ) if filters.get("cost_head"): cond += "and ba.cost_head = '{0}'".format(filters.get("cost_head")) - if filters.get("cost_subhead"): - cond += "and ba.cost_subhead = '{0}'".format(filters.get("cost_subhead")) + if filters.get("budget_group"): + cond += "and ba.budget_group = '{0}'".format(filters.get("budget_group")) if filters.get("cost_category"): cond += "and ba.cost_category = '{0}'".format(filters.get("cost_category")) if filters.get("finance_group"): @@ -198,12 +201,12 @@ def get_dimension_target_details(filters): ba.account, ba.budget_amount, ba.cost_head, - ba.cost_subhead, - ba.cost_category, -- Added cost_category field + ba.budget_group, + ba.cost_category, b.fiscal_year from `tabBudget` b, - `tabBudget Account` ba + `tabM1 Budget Account` ba where b.name = ba.parent and b.fiscal_year between %s and %s @@ -245,14 +248,14 @@ def get_target_distribution_details(filters): ): # Get the Budget Account details for each budget budget_accounts = frappe.get_all( - "Budget Account", + "M1 Budget Account", filters={"parent": budget.budget_name}, - fields=["account", "january", "february", "march", "april", "may", "june", + fields=["cost_head", "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"] ) for d in budget_accounts: - target_details.setdefault(d.account, {}).setdefault(budget.fiscal_year, {}) + target_details.setdefault(d.cost_head, {}).setdefault(budget.fiscal_year, {}) # Assign the actual amount for each month for month, amount in zip( @@ -261,8 +264,7 @@ def get_target_distribution_details(filters): [d.january, d.february, d.march, d.april, d.may, d.june, d.july, d.august, d.september, d.october, d.november, d.december] ): - target_details[d.account][budget.fiscal_year][month] = flt(amount) - + target_details[d.cost_head][budget.fiscal_year][month] = flt(amount) return target_details @@ -281,33 +283,30 @@ def get_dimension_account_month_map(filters): } for ccd in dimension_target_details: - # Ensure cost_head, cost_subhead, and cost_category are stored at the account level - cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.account, { - "cost_head": ccd.cost_head, - "cost_subhead": ccd.cost_subhead, - "cost_category": ccd.cost_category # Added cost_category + # Ensure cost_head, budget_group, and cost_category are stored at the account level + cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.cost_head, { + "account": ccd.account, + "budget_group": ccd.budget_group, + "cost_category": ccd.cost_category }).setdefault(ccd.fiscal_year, {}) for month_id in range(1, 13): month = datetime.date(2013, month_id, 1).strftime("%B") - cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year].setdefault( + cam_map[ccd.budget_against][ccd.cost_head][ccd.fiscal_year].setdefault( month, frappe._dict({"target": 0.0, "actual": 0.0}) ) - tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] - - month_percentage = ( - tdd.get(ccd.monthly_distribution, {}).get(month, 0) - if ccd.monthly_distribution - else 100.0 / 12 - ) + tav_dict = cam_map[ccd.budget_against][ccd.cost_head][ccd.fiscal_year][month] - tav_dict.target = tdd[ccd.account][ccd.fiscal_year][month_map[month]] + tav_dict.target = tdd[ccd.cost_head][ccd.fiscal_year][month_map[month]] return cam_map def get_fiscal_years(filters): + ''' + Returns the list of fiscal years between from_fiscal_year and to_fiscal_year + ''' fiscal_year = frappe.db.sql( """ select @@ -320,4 +319,4 @@ def get_fiscal_years(filters): {"from_fiscal_year": filters["from_fiscal_year"], "to_fiscal_year": filters["to_fiscal_year"]}, ) - return fiscal_year \ No newline at end of file + return fiscal_year From 503c9f95c9e59a48dd18b90e39047501a6650af7 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Fri, 6 Feb 2026 14:03:18 +0530 Subject: [PATCH 32/50] fix: indentation --- .../budget_template/budget_template.py | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/beams/beams/doctype/budget_template/budget_template.py b/beams/beams/doctype/budget_template/budget_template.py index f664465f5..b70b11eff 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -33,70 +33,70 @@ def before_save(self): self.set_default_account() -def validate_account_per_cost_center(self): + def validate_account_per_cost_center(self): + """ + Validates that there are no duplicate Cost Heads within the same Budget Template and + no duplicate Account Heads across different Budget Templates for the same Cost Center. """ - Validates that there are no duplicate Cost Heads within the same Budget Template and - no duplicate Account Heads across different Budget Templates for the same Cost Center. - """ - - if not self.cost_center or not self.budget_template_items: - return - - seen_cost_heads = set() - - for row in self.budget_template_items: - # Duplicate Cost Head in same Template - if row.cost_head: - if row.cost_head in seen_cost_heads: - frappe.throw( - _("Duplicate Cost Head {0} is not allowed in the same Budget Template.") - .format(row.cost_head), - title=_("Duplicate Cost Head"), - ) - seen_cost_heads.add(row.cost_head) - - # Duplicate Account across Templates (same Cost Center) - if not row.account_head: - continue - - duplicates = frappe.get_all( - "Budget Template Item", - filters={ - "account_head": row.account_head, - "parenttype": "Budget Template", - "parent": ["!=", self.name], - }, - fields=["parent"], - limit=1, - ) - - if not duplicates: - continue - - template = duplicates[0].parent - - template_cost_center = frappe.db.get_value( - "Budget Template", template, "cost_center" - ) - - if template_cost_center != self.cost_center: - continue - - template_link = frappe.utils.get_link_to_form( - "Budget Template", template - ) - - frappe.throw( - _( - "Account : {0} is used in the {1} Budget Template " - "with the same Cost Center : {2}." - ).format( - row.account_head, - template_link, - self.cost_center, - ), - title=_("Duplicate Account Found"), - ) + + if not self.cost_center or not self.budget_template_items: + return + + seen_cost_heads = set() + + for row in self.budget_template_items: + # Duplicate Cost Head in same Template + if row.cost_head: + if row.cost_head in seen_cost_heads: + frappe.throw( + _("Duplicate Cost Head {0} is not allowed in the same Budget Template.") + .format(row.cost_head), + title=_("Duplicate Cost Head"), + ) + seen_cost_heads.add(row.cost_head) + + # Duplicate Account across Templates (same Cost Center) + if not row.account_head: + continue + + duplicates = frappe.get_all( + "Budget Template Item", + filters={ + "account_head": row.account_head, + "parenttype": "Budget Template", + "parent": ["!=", self.name], + }, + fields=["parent"], + limit=1, + ) + + if not duplicates: + continue + + template = duplicates[0].parent + + template_cost_center = frappe.db.get_value( + "Budget Template", template, "cost_center" + ) + + if template_cost_center != self.cost_center: + continue + + template_link = frappe.utils.get_link_to_form( + "Budget Template", template + ) + + frappe.throw( + _( + "Account : {0} is used in the {1} Budget Template " + "with the same Cost Center : {2}." + ).format( + row.account_head, + template_link, + self.cost_center, + ), + title=_("Duplicate Account Found"), + ) @frappe.whitelist() From 663b4c3b11137aa1f6bd261e08bfe9b48ff44cc3 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Fri, 6 Feb 2026 16:23:01 +0530 Subject: [PATCH 33/50] feat: Detailed Budget Allocation report --- .../detailed_budget_allocation_report.js | 253 ++++--- .../detailed_budget_allocation_report.py | 638 +++++++++--------- .../detailed_budget_allocation_report_old.py | 281 -------- 3 files changed, 430 insertions(+), 742 deletions(-) delete mode 100644 beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report_old.py diff --git a/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report.js b/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report.js index bc62d5517..debb9a608 100644 --- a/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report.js +++ b/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report.js @@ -2,135 +2,126 @@ // For license information, please see license.txt frappe.query_reports["Detailed Budget Allocation Report"] = { - filters: [ - { - fieldname: "fiscal_year", - label: "Fiscal Year", - fieldtype: "Link", - options: "Fiscal Year", - reqd: 1 - }, - { - fieldname: "period", - label: __("Period"), - fieldtype: "Select", - options: [ - { value: "Monthly", label: __("Monthly") }, - { value: "Quarterly", label: __("Quarterly") }, - { value: "Half-Yearly", label: __("Half-Yearly") }, - { value: "Yearly", label: __("Yearly") }, - ], - default: "Yearly", - reqd: 1, - }, - { - fieldname: "month", - label: __("Month"), - fieldtype: "Select", - options: "\nJan\nFeb\nMar\nApr\nMay\nJun\nJul\nAug\nSep\nOct\nNov\nDec", - depends_on: "eval: doc.period == 'Monthly'", - }, - { - fieldname: "company", - label: "Company", - fieldtype: "Link", - options: "Company", - default: frappe.defaults.get_user_default("Company") - }, - { - fieldname: "region", - label: "Region", - fieldtype: "Select", - options: "\nNational\nGCC" - }, - { - fieldname: "department", - label: "Department", - fieldtype: "MultiSelectList", - options: "Department", - get_data: function (txt) { - let dept_mult_filters = {} - if (frappe.query_report.get_filter_value('company')) { - dept_mult_filters['company'] = frappe.query_report.get_filter_value('company'); - } - return frappe.db.get_link_options("Department", txt, dept_mult_filters); - }, - on_change: function () { - frappe.query_report.set_filter_value("division", []) - frappe.query_report.refresh(); - } - }, - { - fieldname: "division", - label: "Division", - fieldtype: "Link", - options: "Division", - get_query: function () { - let div_filters = {} - if (frappe.query_report.get_filter_value('department').length) { - div_filters['department'] = ['in', frappe.query_report.get_filter_value('department')] - } - if (frappe.query_report.get_filter_value('company')) { - div_filters['company'] = frappe.query_report.get_filter_value('company'); - } - return { - filters: div_filters - } - } - }, - { - fieldname: "cost_head", - label: "Cost Head", - fieldtype: "Link", - options: "Cost Head", - }, - { - fieldname: "cost_subhead", - label: "Cost Subhead", - fieldtype: "Link", - options: "Cost Subhead", - get_query: function () { - let csh_filters = {} - if (frappe.query_report.get_filter_value('cost_head')) { - csh_filters['cost_head'] = frappe.query_report.get_filter_value('cost_head'); - } - return { - filters: csh_filters - } - } - }, - { - fieldname: "cost_category", - label: "Cost Category", - fieldtype: "Select", - options: "\nHR Overheads\nOperational Exp", - }, - { - fieldname: "sort_by", - label: "Sort By", - fieldtype: "Select", - options: "ASC\nDESC", - default: "DESC" - }, - { - fieldname: "budget_amount_only", - label: "Budget Amount Only", - fieldtype: "Check", - default: 1 - }, - ], - tree: true, - treeView: true, - name_field: "id", - parent_field: "parent", - initial_depth: 4, - formatter: function (value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data); - if (data && data.indent < 4) { - value = $(`${value}`); - var $value = $(value).css("font-weight", "bold"); - value = $value.wrap("

    ").parent().html(); - } - return value; - } + filters: [ + { + fieldname: "fiscal_year", + label: "Fiscal Year", + fieldtype: "Link", + options: "Fiscal Year", + reqd: 1 + }, + { + fieldname: "period", + label: __("Period"), + fieldtype: "Select", + options: [ + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, + ], + default: "Yearly", + reqd: 1, + }, + { + fieldname: "month", + label: __("Month"), + fieldtype: "Select", + options: "\nJan\nFeb\nMar\nApr\nMay\nJun\nJul\nAug\nSep\nOct\nNov\nDec", + depends_on: "eval: doc.period == 'Monthly'", + }, + { + fieldname: "company", + label: "Company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company") + }, + { + fieldname: "budget_region", + label: "Budget Region", + fieldtype: "Link", + options: "Budget Region", + }, + { + fieldname: "department", + label: "Department", + fieldtype: "MultiSelectList", + options: "Department", + get_data: function (txt) { + let dept_mult_filters = {} + if (frappe.query_report.get_filter_value('company')) { + dept_mult_filters['company'] = frappe.query_report.get_filter_value('company'); + } + return frappe.db.get_link_options("Department", txt, dept_mult_filters); + }, + on_change: function () { + frappe.query_report.set_filter_value("division", []) + frappe.query_report.refresh(); + } + }, + { + fieldname: "division", + label: "Division", + fieldtype: "Link", + options: "Division", + get_query: function () { + let div_filters = {} + if (frappe.query_report.get_filter_value('department').length) { + div_filters['department'] = ['in', frappe.query_report.get_filter_value('department')] + } + if (frappe.query_report.get_filter_value('company')) { + div_filters['company'] = frappe.query_report.get_filter_value('company'); + } + return { + filters: div_filters + } + } + }, + { + fieldname: "budget_group", + label: "Budget Group", + fieldtype: "Link", + options: "Budget Group", + }, + { + fieldname: "cost_head", + label: "Cost Head", + fieldtype: "Link", + options: "Cost Head", + }, + { + fieldname: "cost_category", + label: "Cost Category", + fieldtype: "Link", + options: "Cost Category", + }, + { + fieldname: "sort_by", + label: "Sort By", + fieldtype: "Select", + options: "ASC\nDESC", + default: "DESC" + }, + { + fieldname: "budget_amount_only", + label: "Budget Amount Only", + fieldtype: "Check", + default: 1 + }, + ], + tree: true, + treeView: true, + name_field: "id", + parent_field: "parent", + initial_depth: 4, + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (data && data.indent < 4) { + value = $(`${value}`); + var $value = $(value).css("font-weight", "bold"); + value = $value.wrap("

    ").parent().html(); + } + return value; + } }; diff --git a/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report.py b/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report.py index c7922b6d0..7bf99983d 100644 --- a/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report.py +++ b/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report.py @@ -4,341 +4,319 @@ from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges def execute(filters=None): - columns = get_columns(filters) - data = get_data(filters) + columns = get_columns(filters) + data = get_data(filters) - if not data: - return columns, [] + if not data: + return columns, [] - return columns, data + return columns, data def get_columns(filters): - columns = [ - { - 'fieldname': 'name', - 'label': 'Name', - 'fieldtype': 'Data', - 'width': 500 - } - ] - fiscal_year = filters.get('fiscal_year') - period = filters.get('period') - month_name = filters.get('month') - group_months = False if period == 'Monthly' else True - currency_fields = [] - if month_name: - label = 'Budget ({0})'.format(month_name) - currency_fields.append(frappe.scrub(label)) - columns.append( - {'label': label, 'fieldtype': 'Currency', 'fieldname': frappe.scrub(label), 'width': 200} - ) - filters["currency_fields"] = currency_fields - return columns - for from_date, to_date in get_period_date_ranges(period, fiscal_year): - if period == 'Yearly': - label = _('Budget') - columns.append( - {'label': label, 'fieldtype': 'Currency', 'fieldname': 'total_budget', 'width': 200} - ) - currency_fields.append('total_budget') - else: - for label in [ - _('Budget') + ' (%s)', - ]: - if group_months: - label = label % ( - formatdate(from_date, format_string='MMM') - + '-' - + formatdate(to_date, format_string='MMM') - ) - else: - label = label % formatdate(from_date, format_string='MMM') - - currency_fields.append(frappe.scrub(label)) - columns.append( - {'label': label, 'fieldtype': 'Currency', 'fieldname': frappe.scrub(label), 'width': 200} - ) - if period != 'Yearly' and (period =='Monthly' or not month_name): - currency_fields.append('total_budget') - columns.append( - {'label': _('Total Budget'), 'fieldtype': 'Currency', 'fieldname': 'total_budget', 'width': 200} - ) - - if not filters.get("budget_amount_only"): - fields = [ - {'label': _('Finance Group'), 'fieldtype': 'Link', 'fieldname': 'finance_group', 'options': 'Finance Group', 'width': 200}, - {'label': _('Department'), 'fieldtype': 'Link', 'fieldname': 'department', 'options': 'Department', 'width': 200}, - {'label': _('Division'), 'fieldtype': 'Link', 'fieldname': 'division', 'options': 'Division', 'width': 200}, - {'label': _('Cost Head'), 'fieldtype': 'Link', 'fieldname': 'cost_head', 'options': 'Cost Head', 'width': 200}, - {'label': _('Cost Subhead'), 'fieldtype': 'Link', 'fieldname': 'cost_subhead', 'options': 'Cost Subhead', 'width': 200} - ] - # Insert all fields at index 1 in one step - columns[1:1] = fields - - filters["currency_fields"] = currency_fields - return columns + columns = [ + { + 'fieldname': 'name', + 'label': 'Name', + 'fieldtype': 'Data', + 'width': 500 + } + ] + fiscal_year = filters.get('fiscal_year') + period = filters.get('period') + month_name = filters.get('month') + group_months = False if period == 'Monthly' else True + currency_fields = [] + if month_name: + label = 'Budget ({0})'.format(month_name) + currency_fields.append(frappe.scrub(label)) + columns.append( + {'label': label, 'fieldtype': 'Currency', 'fieldname': frappe.scrub(label), 'width': 200} + ) + filters["currency_fields"] = currency_fields + return columns + for from_date, to_date in get_period_date_ranges(period, fiscal_year): + if period == 'Yearly': + label = _('Budget') + columns.append( + {'label': label, 'fieldtype': 'Currency', 'fieldname': 'total_budget', 'width': 200} + ) + currency_fields.append('total_budget') + else: + for label in [ + _('Budget') + ' (%s)', + ]: + if group_months: + label = label % ( + formatdate(from_date, format_string='MMM') + + '-' + + formatdate(to_date, format_string='MMM') + ) + else: + label = label % formatdate(from_date, format_string='MMM') + + currency_fields.append(frappe.scrub(label)) + columns.append( + {'label': label, 'fieldtype': 'Currency', 'fieldname': frappe.scrub(label), 'width': 200} + ) + if period != 'Yearly' and (period =='Monthly' or not month_name): + currency_fields.append('total_budget') + columns.append( + {'label': _('Total Budget'), 'fieldtype': 'Currency', 'fieldname': 'total_budget', 'width': 200} + ) + + if not filters.get("budget_amount_only"): + fields = [ + {'label': _('Department'), 'fieldtype': 'Link', 'fieldname': 'department', 'options': 'Department', 'width': 200}, + {'label': _('Division'), 'fieldtype': 'Link', 'fieldname': 'division', 'options': 'Division', 'width': 200}, + {'label': _('Cost Head'), 'fieldtype': 'Link', 'fieldname': 'cost_head', 'options': 'Cost Head', 'width': 200}, + {'label': _('Budget Group'), 'fieldtype': 'Link', 'fieldname': 'budget_group', 'options': 'Budget Group', 'width': 200} + ] + # Insert all fields at index 1 in one step + columns[1:1] = fields + + filters["currency_fields"] = currency_fields + return columns def get_data(filters): - data = [] - period = filters.get('period', 'Yearly') - fiscal_year = filters.get('fiscal_year') - cost_category = filters.get('cost_category', '') - division = filters.get('division', '') - sort_by_filter = filters.get('sort_by', 'DESC') - - #Get Months list as per fiscal year - period_month_ranges = get_period_month_ranges('Monthly', fiscal_year) - months_order = [f"{month[0].lower()}_inr" for month in period_month_ranges] - - # Dictionary to store budget amounts for each parent - currency_fields = filters.get("currency_fields", ["total_budget"]) - budget_map = {} - - if filters.get('company'): - companies = [filters.get('company')] - else: - companies = frappe.get_all('Company', pluck='name') - - for company in companies: - abbr = frappe.db.get_value('Company', company, 'abbr') - data.append({'id': abbr, 'parent': '', 'indent': 0, 'name': company, 'total_budget': 0}) - budget_map[abbr] = {field: 0 for field in currency_fields}# Initialize parent total - - if filters.get('region'): - if filters.get('region') == 'GCC': - finance_groups = ['GCC'] - else: - finance_groups = frappe.get_all('Finance Group', { 'name': ['!=', 'GCC'] }, pluck='name') - else: - finance_groups = frappe.get_all('Finance Group', pluck='name') - - for fg in finance_groups: - fg_id = f'{fg}-{abbr}' - data.append({'id': fg_id, 'parent': abbr, 'indent': 1, 'name': fg, 'total_budget': 0}) - budget_map[fg_id] = {field: 0 for field in currency_fields} - - dept_filters = {'finance_group': fg, 'company': company} - if filters.get('department'): - filter_depts = frappe.parse_json(filters.get('department')) - dept_filters['name'] = ['in', filter_depts] - departments = frappe.db.get_all('Department', filters=dept_filters , pluck='name') - - for dept in departments: - dept_name = frappe.db.get_value('Department', dept, 'department_name') - data.append({'id': dept, 'parent': fg_id, 'indent': 2, 'name': dept_name, 'total_budget': 0}) - budget_map[dept] = {field: 0 for field in currency_fields} - - division_filter = {'department': dept} - if division: - division_filter['name'] = division - divisions = frappe.get_all('Division', filters=division_filter, pluck='name') - - for div in divisions: - division_name = frappe.db.get_value('Division', div, 'division') - data.append({'id': div, 'parent': dept, 'indent': 3, 'name': division_name, 'total_budget': 0}) - budget_map[div] = {field: 0 for field in currency_fields} - - cost_head = filters.get('cost_head', '') - cost_heads = get_cost_heads(div, fiscal_year, cost_category=cost_category, cost_head=cost_head, order_by=sort_by_filter) - - for ch in cost_heads: - ch_id = f'{div}-{ch}' - data.append({'id': ch_id, 'parent': div, 'indent': 4, 'name': ch, 'total_budget': 0}) - budget_map[ch_id] = {field: 0 for field in currency_fields} - - cost_subhead = filters.get('cost_subhead') - cost_subheads = get_cost_subheads(div, ch, fiscal_year, cost_category=cost_category, cost_subhead=cost_subhead, order_by=sort_by_filter) - - for csh in cost_subheads: - csh_id = f'{div}-{ch}-{csh}' - cost_details = get_cost_subhead_details(div, ch, csh, fiscal_year) - total_budget = cost_details.get('total_budget', 0) - row_id = cost_details.get('name', ) - csh_row = { - 'id': csh_id, - 'parent': ch_id, - 'indent': 5, - 'name': csh, - 'cost_category': cost_details.get('cost_category', ''), - 'account': cost_details.get('account', ''), - 'total_budget': total_budget - } - - if not filters.get("budget_amount_only"): - csh_row.update({ - 'finance_group': fg, - 'department': dept, - 'division': div, - 'cost_head': ch, - 'cost_subhead': csh, - }) - - if period != 'Yearly': - budget_column_data = get_budget_column_data(period, months_order, row_id) - csh_row.update(budget_column_data) - data.append(csh_row) - - # Accumulate child budget into its parent - for field in currency_fields: - budget_map[ch_id].update({ - 'finance_group': fg, - 'department': dept, - 'division': div, - 'cost_head': ch, - }) - budget_map[ch_id][field] += csh_row.get(field, 0) - - # Propagate cost head budget to department - for field in currency_fields: - budget_map[div].update({ - 'finance_group': fg, - 'department': dept, - 'division': div, - }) - budget_map[div][field] += budget_map[ch_id][field] - - # Propagate division budget to departments - for field in currency_fields: - budget_map[dept].update({ - 'finance_group': fg, - 'department': dept, - }) - budget_map[dept][field] += budget_map[div][field] - # Propagate department budget to finance group - for field in currency_fields: - budget_map[fg_id]['finance_group'] = fg - budget_map[fg_id][field] += budget_map[dept][field] - - # Propagate finance group budget to company - for field in currency_fields: - budget_map[abbr][field] += budget_map[fg_id][field] - - # Update budget amounts in the data list - for row in data: - row.update(budget_map.get(row['id'], {})) - - return data - -def get_cost_heads(division, fiscal_year, cost_category=None, cost_head=None, order_by='DESC'): - ''' - Method to get Cost Heads based on Fiscal Year and Department - ''' - query = ''' - SELECT - DISTINCT ba.cost_head - FROM - `tabBudget Account` ba - JOIN - `tabBudget` b ON ba.parent = b.name - WHERE - b.division = %(division)s AND - b.fiscal_year = %(fiscal_year)s - ''' - query_filters = { - 'division': division, - 'fiscal_year': fiscal_year - } - if cost_category: - query += ' AND ba.cost_category = %(cost_category)s' - query_filters['cost_category'] = cost_category - if cost_head: - query += ' AND ba.cost_head = %(cost_head)s' - query_filters['cost_head'] = cost_head - query += 'GROUP BY ba.cost_head ORDER BY SUM(ba.budget_amount) {0}'.format(order_by) - cost_heads = frappe.db.sql(query, query_filters, as_dict=True) - return [row.cost_head for row in cost_heads] - -def get_cost_subheads(division, cost_head, fiscal_year, cost_category=None, cost_subhead=None, order_by='DESC'): - ''' - Method to get Cost Subeads based on Fiscal Year and Department - ''' - query = ''' - SELECT - DISTINCT ba.cost_subhead - FROM - `tabBudget Account` ba - JOIN - `tabBudget` b ON ba.parent = b.name - WHERE - ba.cost_head = %(cost_head)s AND - b.fiscal_year = %(fiscal_year)s AND - b.division = %(division)s - ''' - query_filters = { - 'cost_head':cost_head, - 'fiscal_year':fiscal_year, - 'division':division, - 'order_by': order_by - } - if cost_category: - query += ' AND ba.cost_category = %(cost_category)s' - query_filters['cost_category'] = cost_category - if cost_subhead: - query += ' AND ba.cost_subhead = %(cost_subhead)s' - query_filters['cost_subhead'] = cost_subhead - query += 'ORDER BY ba.budget_amount {0}'.format(order_by) - cost_subheads = frappe.db.sql(query, query_filters, as_dict=True) - return [row.cost_subhead for row in cost_subheads] - -def get_cost_subhead_details(division, cost_head, cost_subhead, fiscal_year): - subhead_details = { - 'cost_category': '', - 'account': '', - 'total_budget': 0 - } - query = ''' - SELECT - ba.name, - ba.cost_category, - ba.account, - ba.budget_amount_inr as total_budget - FROM - `tabBudget Account` ba - JOIN - `tabBudget` b ON ba.parent = b.name - WHERE - b.division = %(division)s AND - b.fiscal_year = %(fiscal_year)s AND - ba.cost_head = %(cost_head)s AND - ba.cost_subhead = %(cost_subhead)s - ''' - query_params = { - 'division':division, - 'fiscal_year':fiscal_year, - 'cost_head':cost_head, - 'cost_subhead':cost_subhead - } - - details = frappe.db.sql(query, query_params, as_dict=True) - if details: - subhead_details = details[0] - return subhead_details + data = [] + period = filters.get('period', 'Yearly') + fiscal_year = filters.get('fiscal_year') + cost_category = filters.get('cost_category', '') + division = filters.get('division', '') + sort_by_filter = filters.get('sort_by', 'DESC') + + #Get Months list as per fiscal year + period_month_ranges = get_period_month_ranges('Monthly', fiscal_year) + months_order = [f"{month[0].lower()}_inr" for month in period_month_ranges] + + # Dictionary to store budget amounts for each parent + currency_fields = filters.get("currency_fields", ["total_budget"]) + budget_map = {} + + if filters.get('company'): + companies = [filters.get('company')] + else: + companies = frappe.get_all('Company', pluck='name') + + for company in companies: + abbr = frappe.db.get_value('Company', company, 'abbr') + data.append({'id': abbr, 'parent': '', 'indent': 0, 'name': company, 'total_budget': 0}) + budget_map[abbr] = {field: 0 for field in currency_fields}# Initialize parent total + + dept_filters = {'company': company} + if filters.get('department'): + filter_depts = frappe.parse_json(filters.get('department')) + dept_filters['name'] = ['in', filter_depts] + departments = frappe.db.get_all('Department', filters=dept_filters , pluck='name') + + for dept in departments: + dept_name = frappe.db.get_value('Department', dept, 'department_name') + data.append({'id': dept, 'parent': abbr, 'indent': 1, 'name': dept_name, 'total_budget': 0}) + budget_map[dept] = {field: 0 for field in currency_fields} + + division_filter = {'department': dept} + if division: + division_filter['name'] = division + divisions = frappe.get_all('Division', filters=division_filter, pluck='name') + + for div in divisions: + division_name = frappe.db.get_value('Division', div, 'division') + data.append({'id': div, 'parent': dept, 'indent': 2, 'name': division_name, 'total_budget': 0}) + budget_map[div] = {field: 0 for field in currency_fields} + + budget_group = filters.get('budget_group', '') + budget_groups = get_budget_groups(div, fiscal_year, cost_category=cost_category, budget_group=budget_group, order_by=sort_by_filter) + + for bg in budget_groups: + bg_id = f'{div}-{bg}' + data.append({'id': bg_id, 'parent': div, 'indent': 3, 'name': bg, 'total_budget': 0}) + budget_map[bg_id] = {field: 0 for field in currency_fields} + + cost_head = filters.get('cost_head') + cost_heads = get_cost_heads(div, bg, fiscal_year, cost_category=cost_category, cost_head=cost_head, order_by=sort_by_filter) + + for ch in cost_heads: + ch_id = f'{div}-{bg}-{ch}' + cost_details = get_cost_head_details(div, bg, ch, fiscal_year) + total_budget = cost_details.get('total_budget', 0) + row_id = cost_details.get('name', ) + csh_row = { + 'id': ch_id, + 'parent': bg_id, + 'indent': 4, + 'name': ch, + 'cost_category': cost_details.get('cost_category', ''), + 'account': cost_details.get('account', ''), + 'total_budget': total_budget + } + + if not filters.get("budget_amount_only"): + csh_row.update({ + 'department': dept, + 'division': div, + 'budget_group': bg, + 'cost_head': ch, + }) + + if period != 'Yearly': + budget_column_data = get_budget_column_data(period, months_order, row_id) + csh_row.update(budget_column_data) + data.append(csh_row) + + # Accumulate child budget into its parent + for field in currency_fields: + budget_map[bg_id].update({ + 'department': dept, + 'division': div, + 'cost_head': bg, + }) + budget_map[bg_id][field] += csh_row.get(field, 0) + + # Propagate cost head budget to department + for field in currency_fields: + budget_map[div].update({ + 'department': dept, + 'division': div, + }) + budget_map[div][field] += budget_map[bg_id][field] + + # Propagate division budget to departments + for field in currency_fields: + budget_map[dept].update({ + 'department': dept, + }) + budget_map[dept][field] += budget_map[div][field] + + # Propagate department group budget to company + for field in currency_fields: + budget_map[abbr][field] += budget_map[dept][field] + + # Update budget amounts in the data list + for row in data: + row.update(budget_map.get(row['id'], {})) + + return data + +def get_budget_groups(division, fiscal_year, cost_category=None, budget_group=None, order_by='DESC'): + ''' + Method to get Cost Heads based on Fiscal Year and Department + ''' + query = ''' + SELECT + DISTINCT ba.budget_group + FROM + `tabM1 Budget Account` ba + JOIN + `tabBudget` b ON ba.parent = b.name + WHERE + b.division = %(division)s AND + b.fiscal_year = %(fiscal_year)s + ''' + query_filters = { + 'division': division, + 'fiscal_year': fiscal_year + } + if cost_category: + query += ' AND ba.cost_category = %(cost_category)s' + query_filters['cost_category'] = cost_category + if budget_group: + query += ' AND ba.budget_group = %(budget_group)s' + query_filters['budget_group'] = budget_group + query += 'GROUP BY ba.budget_group ORDER BY SUM(ba.budget_amount) {0}'.format(order_by) + budget_groups = frappe.db.sql(query, query_filters, as_dict=True) + return [row.budget_group for row in budget_groups] + +def get_cost_heads(division, budget_group, fiscal_year, cost_category=None, cost_head=None, order_by='DESC'): + ''' + Method to get Cost Heads based on Fiscal Year and Department + ''' + query = ''' + SELECT + DISTINCT ba.cost_head + FROM + `tabM1 Budget Account` ba + JOIN + `tabBudget` b ON ba.parent = b.name + WHERE + ba.budget_group = %(budget_group)s AND + b.fiscal_year = %(fiscal_year)s AND + b.division = %(division)s + ''' + query_filters = { + 'budget_group':budget_group, + 'fiscal_year':fiscal_year, + 'division':division, + 'order_by': order_by + } + if cost_category: + query += ' AND ba.cost_category = %(cost_category)s' + query_filters['cost_category'] = cost_category + if cost_head: + query += ' AND ba.cost_head = %(cost_head)s' + query_filters['cost_head'] = cost_head + query += 'ORDER BY ba.budget_amount {0}'.format(order_by) + cost_heads = frappe.db.sql(query, query_filters, as_dict=True) + return [row.cost_head for row in cost_heads] + +def get_cost_head_details(division, budget_group, cost_head, fiscal_year): + head_details = { + 'cost_category': '', + 'account': '', + 'total_budget': 0 + } + query = ''' + SELECT + ba.name, + ba.cost_category, + ba.account, + ba.budget_amount_inr as total_budget + FROM + `tabM1 Budget Account` ba + JOIN + `tabBudget` b ON ba.parent = b.name + WHERE + b.division = %(division)s AND + b.fiscal_year = %(fiscal_year)s AND + ba.budget_group = %(budget_group)s AND + ba.cost_head = %(cost_head)s + ''' + query_params = { + 'division':division, + 'fiscal_year':fiscal_year, + 'budget_group':budget_group, + 'cost_head':cost_head + } + + details = frappe.db.sql(query, query_params, as_dict=True) + if details: + head_details = details[0] + return head_details def get_budget_column_data(period, months_order, row_id): - ''' - Get Columnar data specif to period - ''' - budget_column_data = {} - if frappe.db.exists('Budget Account', row_id): - data = frappe.db.get_value('Budget Account', row_id, fieldname=months_order, as_dict=True) - if period == 'Monthly': - for month in months_order: - label = 'budget_({0})'.format(month[0:3]) - budget_column_data[label] = data.get(month) - if period == 'Quarterly': - total = 0 - for i, month in enumerate(months_order): - total += data.get(month) - if i in [2, 5, 8, 11]: - label = 'budget_({0}_{1})'.format(months_order[i-2][0:3], month[0:3]) - budget_column_data[label] = total - total = 0 - if period == 'Half-Yearly': - total = 0 - for i, month in enumerate(months_order): - total += data.get(month) - if i in [5, 11]: - label = 'budget_({0}_{1})'.format(months_order[i-5][0:3], month[0:3]) - budget_column_data[label] = total - total = 0 - return budget_column_data \ No newline at end of file + ''' + Get Columnar data specif to period + ''' + budget_column_data = {} + if frappe.db.exists('Budget Account', row_id): + data = frappe.db.get_value('Budget Account', row_id, fieldname=months_order, as_dict=True) + if period == 'Monthly': + for month in months_order: + label = 'budget_({0})'.format(month[0:3]) + budget_column_data[label] = data.get(month) + if period == 'Quarterly': + total = 0 + for i, month in enumerate(months_order): + total += data.get(month) + if i in [2, 5, 8, 11]: + label = 'budget_({0}_{1})'.format(months_order[i-2][0:3], month[0:3]) + budget_column_data[label] = total + total = 0 + if period == 'Half-Yearly': + total = 0 + for i, month in enumerate(months_order): + total += data.get(month) + if i in [5, 11]: + label = 'budget_({0}_{1})'.format(months_order[i-5][0:3], month[0:3]) + budget_column_data[label] = total + total = 0 + return budget_column_data \ No newline at end of file diff --git a/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report_old.py b/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report_old.py deleted file mode 100644 index 68d50a991..000000000 --- a/beams/beams/report/detailed_budget_allocation_report/detailed_budget_allocation_report_old.py +++ /dev/null @@ -1,281 +0,0 @@ -import frappe -from frappe import _ -from frappe.utils import flt, formatdate -from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges - -def execute(filters=None): - columns = get_columns(filters) - data = get_data(filters) - - if not data: - return columns, [] - - total_budget = sum(row.get('total_budget', 0) for row in data if row['indent'] == 0) - - data.append({ - 'name': 'Total', - 'parent': '', - 'cost_category': '', - 'account': '', - 'total_budget': total_budget - }) - - return columns, data - -def get_columns(filters): - columns = [ - { - 'fieldname': 'name', - 'label': 'Name', - 'fieldtype': 'Data', - 'width': 500 - }, - { - 'fieldname': 'cost_category', - 'label': 'Cost Category', - 'fieldtype': 'Data', - 'width': 200}, - { - 'fieldname': 'account', - 'label': 'Account', - 'fieldtype': 'Data', - 'width': 200 - } - ] - fiscal_year = filters.get('fiscal_year') - period = filters.get('period') - group_months = False if period == 'Monthly' else True - for from_date, to_date in get_period_date_ranges(period, fiscal_year): - if period == 'Yearly': - label = _('Budget') - columns.append( - {'label': label, 'fieldtype': 'Currency', 'fieldname': 'total_budget', 'width': 200} - ) - else: - for label in [ - _('Budget') + ' (%s)', - ]: - if group_months: - label = label % ( - formatdate(from_date, format_string='MMM') - + '-' - + formatdate(to_date, format_string='MMM') - ) - else: - label = label % formatdate(from_date, format_string='MMM') - - columns.append( - {'label': label, 'fieldtype': 'Currency', 'fieldname': frappe.scrub(label), 'width': 200} - ) - if period != 'Yearly': - columns.append( - {'label': _('Total Budget'), 'fieldtype': 'Currency', 'fieldname': 'total_budget', 'width': 200} - ) - return columns - -def get_data(filters): - data = [] - period = filters.get('period', 'Yearly') - fiscal_year = filters.get('fiscal_year') - cost_category = filters.get('cost_category', '') - - #Get Months list as per fiscal year - period_month_ranges = get_period_month_ranges('Monthly', fiscal_year) - months_order = [month[0].lower() for month in period_month_ranges] - - # Dictionary to store budget amounts for each parent - budget_map = {} - - if filters.get('company'): - companies = [filters.get('company')] - else: - companies = frappe.get_all('Company', pluck='name') - - for company in companies: - abbr = frappe.db.get_value('Company', company, 'abbr') - data.append({'id': abbr, 'parent': '', 'indent': 0, 'name': company, 'total_budget': 0}) - budget_map[abbr] = 0 # Initialize parent total - - if filters.get('finance_group'): - finance_groups = [filters.get('finance_group')] - else: - finance_groups = frappe.get_all('Finance Group', pluck='name') - - for fg in finance_groups: - fg_id = f'{fg}-{abbr}' - data.append({'id': fg_id, 'parent': abbr, 'indent': 1, 'name': fg, 'total_budget': 0}) - budget_map[fg_id] = 0 - - if filters.get('department'): - departments = [filters.get('department')] - else: - departments = frappe.get_all('Department', filters={'finance_group': fg, 'company': company}, pluck='name') - - for dept in departments: - data.append({'id': dept, 'parent': fg_id, 'indent': 2, 'name': dept, 'total_budget': 0}) - budget_map[dept] = 0 - - if filters.get('cost_head'): - cost_heads = [filters.get('cost_head')] - else: - cost_heads = get_cost_heads(dept, fiscal_year, cost_category) - - for ch in cost_heads: - ch_id = f'{dept}-{ch}' - data.append({'id': ch_id, 'parent': dept, 'indent': 3, 'name': ch, 'total_budget': 0}) - budget_map[ch_id] = 0 - - if filters.get('cost_subhead'): - cost_subheads = [filters.get('cost_subhead')] - else: - cost_subheads = get_cost_subheads(ch, fiscal_year, cost_category) - - for csh in cost_subheads: - csh_id = f'{dept}-{ch}-{csh}' - cost_details = get_cost_subhead_details(dept, ch, csh, fiscal_year) - total_budget = cost_details.get('total_budget', 0) - row_id = cost_details.get('name', ) - csh_row = { - 'id': csh_id, - 'parent': ch_id, - 'indent': 4, - 'name': csh, - 'cost_category': cost_details.get('cost_category', ''), - 'account': cost_details.get('account', ''), - 'total_budget': total_budget - } - if period != 'Yearly': - budget_column_data = get_budget_column_data(period, months_order, row_id) - csh_row.update(budget_column_data) - data.append(csh_row) - - # Accumulate child budget into its parent - budget_map[ch_id] += total_budget - - # Propagate cost head budget to department - budget_map[dept] += budget_map[ch_id] - - # Propagate department budget to finance group - budget_map[fg_id] += budget_map[dept] - - # Propagate finance group budget to company - budget_map[abbr] += budget_map[fg_id] - - # Update budget amounts in the data list - for row in data: - row['total_budget'] = budget_map.get(row['id'], row.get('total_budget', 0)) - - return data - -def get_cost_heads(department, fiscal_year, cost_category=None): - ''' - Method to get Cost Heads based on Fiscal Year and Department - ''' - query = ''' - SELECT - DISTINCT ba.cost_head - FROM - `tabBudget Account` ba - JOIN - `tabBudget` b ON ba.parent = b.name - WHERE - b.department = %(department)s AND - b.fiscal_year = %(fiscal_year)s - ''' - query_filters = { - 'department': department, - 'fiscal_year': fiscal_year - } - if cost_category: - query += ' AND ba.cost_category = %(cost_category)s' - query_filters['cost_category'] = cost_category - cost_heads = frappe.db.sql(query, query_filters, as_dict=True) - return [row.cost_head for row in cost_heads] - -def get_cost_subheads(cost_head, fiscal_year, cost_category=None): - ''' - Method to get Cost Subeads based on Fiscal Year and Department - ''' - query = ''' - SELECT - DISTINCT ba.cost_subhead - FROM - `tabBudget Account` ba - JOIN - `tabBudget` b ON ba.parent = b.name - WHERE - ba.cost_head = %(cost_head)s AND - b.fiscal_year = %(fiscal_year)s - ''' - query_filters = { - 'cost_head':cost_head, - 'fiscal_year':fiscal_year, - } - if cost_category: - query += ' AND ba.cost_category = %(cost_category)s' - query_filters['cost_category'] = cost_category - cost_subheads = frappe.db.sql(query, query_filters, as_dict=True) - return [row.cost_subhead for row in cost_subheads] - -def get_cost_subhead_details(department, cost_head, cost_subhead, fiscal_year): - subhead_details = { - 'cost_category': '', - 'account': '', - 'total_budget': 0 - } - query = ''' - SELECT - ba.name, - ba.cost_category, - ba.account, - ba.budget_amount as total_budget - FROM - `tabBudget Account` ba - JOIN - `tabBudget` b ON ba.parent = b.name - WHERE - b.department = %(department)s AND - b.fiscal_year = %(fiscal_year)s AND - ba.cost_head = %(cost_head)s AND - ba.cost_subhead = %(cost_subhead)s - ''' - query_params = { - 'department':department, - 'fiscal_year':fiscal_year, - 'cost_head':cost_head, - 'cost_subhead':cost_subhead - } - - details = frappe.db.sql(query, query_params, as_dict=True) - if details: - subhead_details = details[0] - return subhead_details - -def get_budget_column_data(period, months_order, row_id): - ''' - Get Columnar data specif to period - ''' - budget_column_data = {} - if frappe.db.exists('Budget Account', row_id): - data = frappe.db.get_value('Budget Account', row_id, fieldname=months_order, as_dict=True) - if period == 'Monthly': - for month in months_order: - label = 'budget_({0})'.format(month[0:3]) - budget_column_data[label] = data.get(month) - if period == 'Quarterly': - total = 0 - for i, month in enumerate(months_order): - total += data.get(month) - if i in [2, 5, 8, 11]: - label = 'budget_({0}_{1})'.format(months_order[i-2][0:3], month[0:3]) - budget_column_data[label] = total - total = 0 - if period == 'Half-Yearly': - total = 0 - for i, month in enumerate(months_order): - total += data.get(month) - if i in [5, 11]: - label = 'budget_({0}_{1})'.format(months_order[i-5][0:3], month[0:3]) - budget_column_data[label] = total - total = 0 - return budget_column_data \ No newline at end of file From ea941405b5caed43751822ce1ba91c3f25ce461d Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Fri, 6 Feb 2026 16:26:38 +0530 Subject: [PATCH 34/50] fix: incorrect amount due to missing company filter --- .../budget_allocation/budget_allocation.py | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/beams/beams/report/budget_allocation/budget_allocation.py b/beams/beams/report/budget_allocation/budget_allocation.py index d640adee5..bc6b80f70 100644 --- a/beams/beams/report/budget_allocation/budget_allocation.py +++ b/beams/beams/report/budget_allocation/budget_allocation.py @@ -190,8 +190,6 @@ def get_dimension_target_details(filters): cond += "and ba.budget_group = '{0}'".format(filters.get("budget_group")) if filters.get("cost_category"): cond += "and ba.cost_category = '{0}'".format(filters.get("cost_category")) - if filters.get("finance_group"): - cond += "and b.finance_group = '{0}'".format(filters.get("finance_group")) return frappe.db.sql( f""" @@ -230,22 +228,39 @@ def get_dimension_target_details(filters): def get_target_distribution_details(filters): + budget_against = frappe.scrub(filters.get("budget_against")) + budget_against_filter = filters.get("budget_against_filter") or [] + if not budget_against_filter: + if filters.get("budget_against") in ["Cost Center", "Project"]: + budget_against_filter = get_cost_centers(filters) + budget_against_data = ( + ",".join(f"'{bud_ag}'" for bud_ag in budget_against_filter) + if budget_against_filter + else "''" + ) + target_details = {} + budget_query = f""" + select + b.name as budget_name, + b.fiscal_year + from + `tabBudget` b + where + b.fiscal_year between %s and %s and + b.company = %s and + b.budget_against = %s and + b.{budget_against} in ({budget_against_data}) + """ + budgets = frappe.db.sql( + budget_query, + (filters.from_fiscal_year, filters.to_fiscal_year, filters.company, filters.budget_against), + as_dict=True + ) + # Loop through the Budget records to get the amounts for each month from the Budget Account child table - for budget in frappe.db.sql( - """ - select - b.name as budget_name, - b.fiscal_year - from - `tabBudget` b - where - b.fiscal_year between %s and %s - """, - (filters.from_fiscal_year, filters.to_fiscal_year), - as_dict=True, - ): + for budget in budgets: # Get the Budget Account details for each budget budget_accounts = frappe.get_all( "M1 Budget Account", @@ -300,7 +315,6 @@ def get_dimension_account_month_map(filters): tav_dict = cam_map[ccd.budget_against][ccd.cost_head][ccd.fiscal_year][month] tav_dict.target = tdd[ccd.cost_head][ccd.fiscal_year][month_map[month]] - return cam_map def get_fiscal_years(filters): From d0add1d92c9e79daa9e911a4bedadcc46d3c9a25 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Fri, 6 Feb 2026 17:42:15 +0530 Subject: [PATCH 35/50] feat: Budget Comparison Report --- .../budget_comparison_report.js | 256 ++++++++---------- .../budget_comparison_report.py | 202 ++++++-------- 2 files changed, 202 insertions(+), 256 deletions(-) diff --git a/beams/beams/report/budget_comparison_report/budget_comparison_report.js b/beams/beams/report/budget_comparison_report/budget_comparison_report.js index 1a27b3acd..2be623673 100644 --- a/beams/beams/report/budget_comparison_report/budget_comparison_report.js +++ b/beams/beams/report/budget_comparison_report/budget_comparison_report.js @@ -2,160 +2,128 @@ // For license information, please see license.txt frappe.query_reports["Budget Comparison Report"] = { - filters: get_filters(), - formatter: function (value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data); + filters: get_filters(), + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); - if (column.fieldname.includes(__("variance"))) { - if (data[column.fieldname] < 0) { - value = "" + value + ""; - } else if (data[column.fieldname] > 0) { - value = "" + value + ""; - } - } + if (column.fieldname.includes(__("variance"))) { + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } - return value; - }, - after_datatable_render: function (datatable) { - let data = frappe.query_report.data; - if (!data || !data.length) return; + return value; + }, + after_datatable_render: function (datatable) { + let data = frappe.query_report.data; + if (!data || !data.length) return; - let columns = frappe.query_report.columns; - let total_row = {}; + let columns = frappe.query_report.columns; + let total_row = {}; - // Check if total row already exists - let first_column = columns[0]?.fieldname; - let total_row_exists = data.some(row => row[first_column] === __("Total")); - if (total_row_exists) return; + // Check if total row already exists + let first_column = columns[0]?.fieldname; + let total_row_exists = data.some(row => row[first_column] === __("Total")); + if (total_row_exists) return; - columns.forEach((col) => { - if (col.fieldtype === "Currency" || col.fieldtype === "Float") { - total_row[col.fieldname] = data.reduce((sum, row) => sum + (row[col.fieldname] || 0), 0); - } else { - total_row[col.fieldname] = col.fieldname === first_column ? __("Total") : ""; - } - }); + columns.forEach((col) => { + if (col.fieldtype === "Currency" || col.fieldtype === "Float") { + total_row[col.fieldname] = data.reduce((sum, row) => sum + (row[col.fieldname] || 0), 0); + } else { + total_row[col.fieldname] = col.fieldname === first_column ? __("Total") : ""; + } + }); - data.push(total_row); - datatable.refresh(data); - } + data.push(total_row); + datatable.refresh(data); + } }; function get_filters() { - function get_dimensions() { - let result = []; - frappe.call({ - method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", - args: { - with_cost_center_and_project: true, - }, - async: false, - callback: function (r) { - if (!r.exc) { - result = r.message[0].map((elem) => elem.document_type); - } - }, - }); - return result; - } + let filters = [ + { + fieldname: "from_fiscal_year", + label: __("From Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + }, + { + fieldname: "to_fiscal_year", + label: __("To Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + }, + { + fieldname: "period", + label: __("Period"), + fieldtype: "Select", + options: [ + { value: "Monthly", label: __("Monthly") }, + { value: "Quarterly", label: __("Quarterly") }, + { value: "Half-Yearly", label: __("Half-Yearly") }, + { value: "Yearly", label: __("Yearly") }, + ], + default: "Yearly", + reqd: 1, + }, + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + { + fieldname: "budget_against", + label: __("Budget Against"), + fieldtype: "Select", + options: "Cost Center\nProject", + default: "Cost Center", + reqd: 1, + on_change: function () { + frappe.query_report.set_filter_value("budget_against_filter", []); + frappe.query_report.refresh(); + }, + }, + { + fieldname: "budget_against_filter", + label: __("Dimension Filter"), + fieldtype: "MultiSelectList", + get_data: function (txt) { + if (!frappe.query_report.filters) return; - let budget_against_options = get_dimensions(); + let budget_against = frappe.query_report.get_filter_value("budget_against"); + if (!budget_against) return; - let filters = [ - { - fieldname: "from_fiscal_year", - label: __("From Fiscal Year"), - fieldtype: "Link", - options: "Fiscal Year", - default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - reqd: 1, - }, - { - fieldname: "to_fiscal_year", - label: __("To Fiscal Year"), - fieldtype: "Link", - options: "Fiscal Year", - default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), - reqd: 1, - }, - { - fieldname: "period", - label: __("Period"), - fieldtype: "Select", - options: [ - { value: "Monthly", label: __("Monthly") }, - { value: "Quarterly", label: __("Quarterly") }, - { value: "Half-Yearly", label: __("Half-Yearly") }, - { value: "Yearly", label: __("Yearly") }, - ], - default: "Yearly", - reqd: 1, - }, - { - fieldname: "company", - label: __("Company"), - fieldtype: "Link", - options: "Company", - default: frappe.defaults.get_user_default("Company"), - reqd: 1, - }, - { - fieldname: "budget_against", - label: __("Budget Against"), - fieldtype: "Select", - options: budget_against_options, - default: "Cost Center", - reqd: 1, - on_change: function () { - frappe.query_report.set_filter_value("budget_against_filter", []); - frappe.query_report.refresh(); - }, - }, - { - fieldname: "budget_against_filter", - label: __("Dimension Filter"), - fieldtype: "MultiSelectList", - get_data: function (txt) { - if (!frappe.query_report.filters) return; + return frappe.db.get_link_options(budget_against, txt); + }, + }, + { + fieldname: "cost_head", + label: __("Cost Head"), + fieldtype: "Link", + options: "Cost Head" + }, + { + fieldname: "budget_group", + label: __("Budget Group"), + fieldtype: "Link", + options: "Budget Group" + }, + { + fieldname: "cost_category", + label: __("Cost Category"), + fieldtype: "Link", + options: "Cost Category" + } + ]; - let budget_against = frappe.query_report.get_filter_value("budget_against"); - if (!budget_against) return; - - return frappe.db.get_link_options(budget_against, txt); - }, - }, - { - fieldname: "finance_group", - label: __("Finance Group"), - fieldtype: "Link", - options: "Finance Group" - }, - { - fieldname: "cost_head", - label: __("Cost Head"), - fieldtype: "Link", - options: "Cost Head" - }, - { - fieldname: "cost_subhead", - label: __("Cost Subhead"), - fieldtype: "Link", - options: "Cost Subhead", - get_query: function () { - return { - filters: { - 'cost_head': frappe.query_report.get_filter_value('cost_head') - } - } - } - }, - { - fieldname: "cost_category", - label: __("Cost Category"), - fieldtype: "Link", - options: "Cost Category" - } - ]; - - return filters; + return filters; } diff --git a/beams/beams/report/budget_comparison_report/budget_comparison_report.py b/beams/beams/report/budget_comparison_report/budget_comparison_report.py index 2b3041a56..8a21e2cf8 100644 --- a/beams/beams/report/budget_comparison_report/budget_comparison_report.py +++ b/beams/beams/report/budget_comparison_report/budget_comparison_report.py @@ -36,12 +36,12 @@ def execute(filters=None): def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation): - for account, monthwise_data in dimension_items.items(): - cost_head = monthwise_data.get("cost_head", "") - cost_subhead = monthwise_data.get("cost_subhead", "") - cost_category = monthwise_data.get("cost_category", "") # Added cost_category + for cost_head, monthwise_data in dimension_items.items(): + account = monthwise_data.get("account", "") + budget_group = monthwise_data.get("budget_group", "") + cost_category = monthwise_data.get("cost_category", "") - row = [dimension, account, cost_head, cost_subhead, cost_category] # Added cost_category to row + row = [dimension, cost_head, account, budget_group, cost_category] totals = [0, 0, 0] for year in get_fiscal_years(filters): @@ -62,9 +62,6 @@ def get_final_data(dimension, dimension_items, filters, period_month_ranges, dat period_data[0] = period_data[0] * (DCC_allocation / 100) period_data[1] = period_data[1] * (DCC_allocation / 100) - # if filters.get("show_cumulative"): - # last_total = period_data[0] - period_data[1] - period_data[2] = period_data[0] - period_data[1] row += period_data @@ -83,32 +80,34 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "budget_against", "options": filters.get("budget_against"), - "width": 150, + "width": 200, + }, + { + "label": _("Cost Head"), + "fieldname": "cost_head", + "fieldtype": "Link", + "options": "Cost Head", + "width": 200, }, { "label": _("Account"), "fieldname": "Account", "fieldtype": "Link", "options": "Account", - "width": 150, - }, - { - "label": _("Cost Head"), - "fieldname": "cost_head", - "fieldtype": "Data", - "width": 150, + "width": 220, }, { - "label": _("Cost Subhead"), - "fieldname": "cost_subhead", - "fieldtype": "Data", - "width": 150, + "label": _("Budget Group"), + "fieldname": "budget_group", + "fieldtype": "Link", + "options": "Budget Group", + "width": 200, }, { "label": _("Cost Category"), "fieldname": "cost_category", "fieldtype": "Data", - "width": 150, + "width": 160, } ] @@ -184,7 +183,7 @@ def get_cost_centers(filters): from `tab{tab}` """.format(tab=filters.get("budget_against")) - ) # nosec + ) # Get dimension & target details @@ -195,10 +194,10 @@ def get_dimension_target_details(filters): cond += f""" and b.{budget_against} in (%s)""" % ", ".join( ["%s"] * len(filters.get("budget_against_filter")) ) + if filters.get("budget_group"): + cond += "and ba.budget_group = '{0}'".format(filters.get("budget_group")) if filters.get("cost_head"): cond += "and ba.cost_head = '{0}'".format(filters.get("cost_head")) - if filters.get("cost_subhead"): - cond += "and ba.cost_subhead = '{0}'".format(filters.get("cost_subhead")) if filters.get("cost_category"): cond += "and ba.cost_category = '{0}'".format(filters.get("cost_category")) if filters.get("finance_group"): @@ -211,13 +210,13 @@ def get_dimension_target_details(filters): b.monthly_distribution, ba.account, ba.budget_amount, + ba.budget_group, ba.cost_head, - ba.cost_subhead, - ba.cost_category, -- Added cost_category field + ba.cost_category, b.fiscal_year from `tabBudget` b, - `tabBudget Account` ba + `tabM1 Budget Account` ba where b.name = ba.parent and b.fiscal_year between %s and %s @@ -239,34 +238,50 @@ def get_dimension_target_details(filters): as_dict=True, ) - def get_target_distribution_details(filters): + budget_against = frappe.scrub(filters.get("budget_against")) + budget_against_filter = filters.get("budget_against_filter") or [] + if not budget_against_filter: + if filters.get("budget_against") in ["Cost Center", "Project"]: + budget_against_filter = get_cost_centers(filters) + budget_against_data = ( + ",".join(f"'{bud_ag}'" for bud_ag in budget_against_filter) + if budget_against_filter + else "''" + ) + target_details = {} + budget_query = f""" + select + b.name as budget_name, + b.fiscal_year + from + `tabBudget` b + where + b.fiscal_year between %s and %s and + b.company = %s and + b.budget_against = %s and + b.{budget_against} in ({budget_against_data}) + """ + budgets = frappe.db.sql( + budget_query, + (filters.from_fiscal_year, filters.to_fiscal_year, filters.company, filters.budget_against), + as_dict=True + ) + # Loop through the Budget records to get the amounts for each month from the Budget Account child table - for budget in frappe.db.sql( - """ - select - b.name as budget_name, - b.fiscal_year - from - `tabBudget` b - where - b.fiscal_year between %s and %s - """, - (filters.from_fiscal_year, filters.to_fiscal_year), - as_dict=True, - ): + for budget in budgets: # Get the Budget Account details for each budget budget_accounts = frappe.get_all( - "Budget Account", + "M1 Budget Account", filters={"parent": budget.budget_name}, - fields=["account", "january", "february", "march", "april", "may", "june", + fields=["cost_head", "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"] ) for d in budget_accounts: - target_details.setdefault(d.account, {}).setdefault(budget.fiscal_year, {}) + target_details.setdefault(d.cost_head, {}).setdefault(budget.fiscal_year, {}) # Assign the actual amount for each month for month, amount in zip( @@ -275,12 +290,10 @@ def get_target_distribution_details(filters): [d.january, d.february, d.march, d.april, d.may, d.june, d.july, d.august, d.september, d.october, d.november, d.december] ): - target_details[d.account][budget.fiscal_year][month] = flt(amount) - + target_details[d.cost_head][budget.fiscal_year][month] = flt(amount) return target_details - def get_dimension_account_month_map(filters): dimension_target_details = get_dimension_target_details(filters) tdd = get_target_distribution_details(filters) @@ -295,91 +308,56 @@ def get_dimension_account_month_map(filters): } for ccd in dimension_target_details: - actual_details = get_actual_details(ccd.budget_against, filters) - - # Ensure cost_head, cost_subhead, and cost_category are stored at the account level - cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.account, { - "cost_head": ccd.cost_head, - "cost_subhead": ccd.cost_subhead, - "cost_category": ccd.cost_category # Added cost_category + # Ensure cost_head, budget_group, and cost_category are stored at the account level + cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.cost_head, { + "account": ccd.account, + "budget_group": ccd.budget_group, + "cost_category": ccd.cost_category }).setdefault(ccd.fiscal_year, {}) for month_id in range(1, 13): month = datetime.date(2013, month_id, 1).strftime("%B") - cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year].setdefault( + cam_map[ccd.budget_against][ccd.cost_head][ccd.fiscal_year].setdefault( month, frappe._dict({"target": 0.0, "actual": 0.0}) ) - tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] - - month_percentage = ( - tdd.get(ccd.monthly_distribution, {}).get(month, 0) - if ccd.monthly_distribution - else 100.0 / 12 - ) - - tav_dict.target = tdd[ccd.account][ccd.fiscal_year][month_map[month]] - - for ad in actual_details.get(ccd.account, []): - if ad.month_name == month and ad.fiscal_year == ccd.fiscal_year: - tav_dict.actual += flt(ad.debit) - flt(ad.credit) + tav_dict = cam_map[ccd.budget_against][ccd.cost_head][ccd.fiscal_year][month] + tav_dict.target = tdd[ccd.cost_head][ccd.fiscal_year][month_map[month]] + tav_dict.actual = get_actual_expenses(ccd.budget_against, ccd.cost_head, ccd.account, month, ccd.fiscal_year) return cam_map # Get actual details from gl entry -def get_actual_details(name, filters): - budget_against = frappe.scrub(filters.get("budget_against")) - cond = "" - - if filters.get("budget_against") == "Cost Center": - cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) - cond = f""" - and lft >= "{cc_lft}" - and rgt <= "{cc_rgt}" - """ - +def get_actual_expenses(cost_center, cost_head, account, month, fy): + expenses = 0 ac_details = frappe.db.sql( f""" select gl.account, - gl.debit, - gl.credit, + gl.cost_head, + SUM(gl.debit) as debit, + SUM(gl.credit) as credit, gl.fiscal_year, - MONTHNAME(gl.posting_date) as month_name, - b.{budget_against} as budget_against + MONTHNAME(gl.posting_date) as month_name from - `tabGL Entry` gl, - `tabBudget Account` ba, - `tabBudget` b + `tabGL Entry` gl where - b.name = ba.parent - and ba.account=gl.account - and b.{budget_against} = gl.{budget_against} - and gl.fiscal_year between %s and %s - and b.{budget_against} = %s - and exists( - select - name - from - `tab{filters.budget_against}` - where - name = gl.{budget_against} - {cond} - ) - group by - gl.name - order by gl.fiscal_year + gl.fiscal_year = '{fy}' + and gl.account = '{account}' + and gl.cost_center = '{cost_center}' + and gl.cost_head = '{cost_head}' + and MONTHNAME(gl.posting_date) = '{month}' + group by + gl.cost_head + order by + gl.fiscal_year """, - (filters.from_fiscal_year, filters.to_fiscal_year, name), as_dict=1, ) - - cc_actual_details = {} - for d in ac_details: - cc_actual_details.setdefault(d.account, []).append(d) - - return cc_actual_details + if ac_details: + expenses = flt(ac_details[0].debit) - flt(ac_details[0].credit) + return expenses def get_fiscal_years(filters): @@ -427,13 +405,13 @@ def get_chart_data(filters, columns, data): budget_values, actual_values = [0] * no_of_columns, [0] * no_of_columns for d in data: - values = d[5:] # Start from index 5 (after cost_category) + values = d[5:] # Start from index 5 (after cost_category) index = 0 for i in range(no_of_columns): budget_values[i] += values[index] actual_values[i] += values[index + 1] - index += 3 # Skip to the next (budget, actual, variance) set + index += 3 # Skip to the next (budget, actual, variance) set return { From 07beffbcb892e509815d80bf4c369501bc006d43 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Sat, 7 Feb 2026 11:13:55 +0530 Subject: [PATCH 36/50] fix: scope duplicate account check by parentfield --- beams/beams/doctype/budget_template/budget_template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beams/beams/doctype/budget_template/budget_template.py b/beams/beams/doctype/budget_template/budget_template.py index b70b11eff..1ba47bdfc 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -64,6 +64,7 @@ def validate_account_per_cost_center(self): filters={ "account_head": row.account_head, "parenttype": "Budget Template", + "parentfield": "budget_template_items", "parent": ["!=", self.name], }, fields=["parent"], From 422e22e2693f679b7404d07793b7681cf2924203 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Sat, 7 Feb 2026 11:31:34 +0530 Subject: [PATCH 37/50] chore: code cleanup, removed unwanted code snippet --- .../budget_template/budget_template.py | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/beams/beams/doctype/budget_template/budget_template.py b/beams/beams/doctype/budget_template/budget_template.py index 1ba47bdfc..c6ed068f0 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -10,34 +10,11 @@ class BudgetTemplate(Document): def validate(self): self.validate_account_per_cost_center() - def set_default_account(self): - """ Set the default account for each budget template item based on the associated cost subhead and company. - """ - if not hasattr(self, "budget_template_item") or not self.budget_template_item: - return - - for item in self.budget_template_item: - if not item.cost_sub_head or not self.company: - item.account = "" - continue - - cost_subhead_doc = frappe.get_doc("Cost Subhead", item.cost_sub_head) - - if cost_subhead_doc.accounts: - account_found = next((acc for acc in cost_subhead_doc.accounts if acc.company == self.company), None) - item.account = account_found.default_account if account_found else "" - else: - item.account = "" - - def before_save(self): - self.set_default_account() - - def validate_account_per_cost_center(self): """ - Validates that there are no duplicate Cost Heads within the same Budget Template and - no duplicate Account Heads across different Budget Templates for the same Cost Center. - """ + Validates that there are no duplicate Cost Heads within the same Budget Template and + no duplicate Account Heads across different Budget Templates for the same Cost Center. + """ if not self.cost_center or not self.budget_template_items: return @@ -103,7 +80,7 @@ def validate_account_per_cost_center(self): @frappe.whitelist() def get_budget_approver_employees(doctype, txt, searchfield, start, page_len, filters): """ - Fetch employees with the role of 'Budget Approver' for the current company. + Fetch employees with the role of 'Budget Approver' for the current company. """ users = frappe.get_all( "Has Role", @@ -123,5 +100,3 @@ def get_budget_approver_employees(doctype, txt, searchfield, start, page_len, fi ) return [(row.name, row.employee_name) for row in result] - - From 1c00d6a184d7620063249a58240f781f129c0826 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Sat, 7 Feb 2026 12:22:34 +0530 Subject: [PATCH 38/50] feat: applied company currency to currency field to handle muti currency --- beams/beams/custom_scripts/budget/budget.py | 62 +++++++++++-------- .../m1_budget_account/m1_budget_account.json | 46 ++++++++++---- 2 files changed, 69 insertions(+), 39 deletions(-) diff --git a/beams/beams/custom_scripts/budget/budget.py b/beams/beams/custom_scripts/budget/budget.py index 983f00661..11d8e83fa 100644 --- a/beams/beams/custom_scripts/budget/budget.py +++ b/beams/beams/custom_scripts/budget/budget.py @@ -3,29 +3,38 @@ month_fields = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] def beams_budget_validate(doc, method=None): - """method runs custom validations for budget doctype""" - update_total_amount(doc, method) - convert_currency(doc, method) - update_budget_against(doc, method) - + ''' + Method trigger on `validate` event of Budget + ''' + update_budget_against(doc, method) + update_total_amount(doc, method) + convert_currency(doc, method) def update_total_amount(doc, method): - total = sum([row.budget_amount for row in doc.get("accounts") if row.budget_amount]) + total = sum([row.budget_amount for row in doc.get('accounts') if row.budget_amount]) doc.total_amount = total def populate_og_accounts(doc, method=None): + ''' + Method trigger on `before_validate` event of Budget + Populate OPEX budget accounts in the main table for better reporting and to avoid issues with child table data retrieval in case of large number of rows + ''' doc.accounts = [] accounts_map = {} #Process OPEX budget accounts for row in doc.budget_accounts: + #Set Company Currency in each row for reference during currency conversion in case company currency is not INR + if doc.company_currency: + row.company_currency = doc.company_currency + # Accumulate amounts per account account = row.account # Initialize account if not exists if account not in accounts_map: accounts_map[account] = { - "account": account, - "budget_amount": 0 + 'account': account, + 'budget_amount': 0 } # Initialize all months for month in month_fields: @@ -34,46 +43,47 @@ def populate_og_accounts(doc, method=None): for month in month_fields: month_value = row.get(month) or 0 accounts_map[account][month] += month_value - accounts_map[account]["budget_amount"] += month_value + accounts_map[account]['budget_amount'] += month_value #Update accumulated amounts for each account in main table for account_data in accounts_map.values(): - doc.append("accounts", account_data) + doc.append('accounts', account_data) def convert_currency(doc, method): - """ + ''' Convert budget amounts for non-INR companies - """ - company_currency = frappe.db.get_value("Company", doc.company, "default_currency") + ''' + company_currency = frappe.db.get_value('Company', doc.company, 'default_currency') exchange_rate = 1 - if company_currency != "INR": - exchange_rate = frappe.db.get_value("Company", doc.company, "exchange_rate_to_inr") + if company_currency != 'INR': + exchange_rate = frappe.db.get_value('Company', doc.company, 'exchange_rate_to_inr') if not exchange_rate: frappe.throw( - f"Please set Exchange Rate from {company_currency} to INR for {doc.company}", - title="Message", + f'Please set Exchange Rate from {company_currency} to INR for {doc.company}', + title='Message', ) months = [ - "january", "february", "march", "april", "may", "june", - "july", "august", "september", "october", "november", "december" + 'january', 'february', 'march', 'april', 'may', 'june', + 'july', 'august', 'september', 'october', 'november', 'december' ] def apply_conversion(row): - """ + ''' Apply exchange rate conversion to a budget row - """ + ''' row.budget_amount_inr = row.budget_amount * exchange_rate for month in months: - setattr(row, f"{month}_inr", (getattr(row, month, 0) or 0) * exchange_rate) + setattr(row, f'{month}_inr', (getattr(row, month, 0) or 0) * exchange_rate) for row in (*doc.accounts, *doc.budget_accounts): apply_conversion(row) def update_budget_against(doc, method=None): - """Set budget_against field based on budget_for selection""" - if doc.budget_for: - doc.budget_against = doc.budget_for - + ''' + Set budget_against field based on budget_for selection + ''' + if doc.budget_for: + doc.budget_against = doc.budget_for diff --git a/beams/beams/doctype/m1_budget_account/m1_budget_account.json b/beams/beams/doctype/m1_budget_account/m1_budget_account.json index 6150b4ac5..70b3a3835 100644 --- a/beams/beams/doctype/m1_budget_account/m1_budget_account.json +++ b/beams/beams/doctype/m1_budget_account/m1_budget_account.json @@ -10,6 +10,7 @@ "budget_group", "account", "cost_category", + "company_currency", "column_break_ndgp", "cost_description", "equal_monthly_distribution", @@ -104,6 +105,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Budget Amount", + "options": "company_currency", "reqd": 1 }, { @@ -115,25 +117,29 @@ "default": "0", "fieldname": "january", "fieldtype": "Currency", - "label": "January" + "label": "January", + "options": "company_currency" }, { "default": "0", "fieldname": "february", "fieldtype": "Currency", - "label": "February" + "label": "February", + "options": "company_currency" }, { "default": "0", "fieldname": "march", "fieldtype": "Currency", - "label": "March" + "label": "March", + "options": "company_currency" }, { "default": "0", "fieldname": "april", "fieldtype": "Currency", - "label": "April" + "label": "April", + "options": "company_currency" }, { "fieldname": "column_break_qqgq", @@ -143,25 +149,29 @@ "default": "0", "fieldname": "may", "fieldtype": "Currency", - "label": "May" + "label": "May", + "options": "company_currency" }, { "default": "0", "fieldname": "june", "fieldtype": "Currency", - "label": "June" + "label": "June", + "options": "company_currency" }, { "default": "0", "fieldname": "july", "fieldtype": "Currency", - "label": "July" + "label": "July", + "options": "company_currency" }, { "default": "0", "fieldname": "august", "fieldtype": "Currency", - "label": "August" + "label": "August", + "options": "company_currency" }, { "fieldname": "column_break_iiwj", @@ -171,25 +181,29 @@ "default": "0", "fieldname": "september", "fieldtype": "Currency", - "label": "September" + "label": "September", + "options": "company_currency" }, { "default": "0", "fieldname": "october", "fieldtype": "Currency", - "label": "October" + "label": "October", + "options": "company_currency" }, { "default": "0", "fieldname": "november", "fieldtype": "Currency", - "label": "November" + "label": "November", + "options": "company_currency" }, { "default": "0", "fieldname": "december", "fieldtype": "Currency", - "label": "December" + "label": "December", + "options": "company_currency" }, { "collapsible": 1, @@ -295,13 +309,19 @@ "fieldtype": "Currency", "label": "August (INR)", "read_only": 1 + }, + { + "fieldname": "company_currency", + "fieldtype": "Link", + "label": "Company Currency", + "options": "Currency" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-02-03 16:48:05.678640", + "modified": "2026-02-07 12:11:36.342976", "modified_by": "Administrator", "module": "BEAMS", "name": "M1 Budget Account", From b70cc0cbddf8bddda42383c9e322468ef01ca540 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Sat, 7 Feb 2026 12:42:50 +0530 Subject: [PATCH 39/50] chore: field reorder on Budget --- beams/patches.txt | 2 +- beams/patches/delete_custom_fields.py | 4 ++++ beams/setup.py | 17 +++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/beams/patches.txt b/beams/patches.txt index f69152102..e13098fbf 100644 --- a/beams/patches.txt +++ b/beams/patches.txt @@ -2,7 +2,7 @@ beams.patches.rename_hod_role #30-10-2024 beams.patches.no_of_children_patch #06-03-2025 beams.patches.delete_property_setter #21-11-2025 -beams.patches.delete_custom_fields #05-02-2026 +beams.patches.delete_custom_fields #07-02-2026 [post_model_sync] # Patches added in this section will be executed after doctypes are migrated diff --git a/beams/patches/delete_custom_fields.py b/beams/patches/delete_custom_fields.py index c23dfe5ba..7708f9efa 100644 --- a/beams/patches/delete_custom_fields.py +++ b/beams/patches/delete_custom_fields.py @@ -288,6 +288,10 @@ { 'dt':'Purchase Order', 'fieldname': 'is_budget_exceed' + }, + { + 'dt':'Budget', + 'fieldname': 'column_break_ab' } ] diff --git a/beams/setup.py b/beams/setup.py index 734e20c15..75930b7e8 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -1177,12 +1177,6 @@ def get_budget_custom_fields(): "in_list_view": 1, "in_standard_filter": 1, }, - { - "fieldtype": "Column Break", - "fieldname": "column_break_ab", - "label": " ", - "insert_after": "division" - }, { "fieldname": "budget_head", "fieldtype": "Data", @@ -1197,7 +1191,8 @@ def get_budget_custom_fields(): "label": "Budget Head User", "insert_after": "budget_head", "read_only": 1, - "fetch_from": "budget_template.budget_head_user" + "fetch_from": "budget_template.budget_head_user", + "hidden": 1 } ], @@ -1266,7 +1261,7 @@ def get_budget_custom_fields(): "fieldname": "column_break_ab", "fieldtype": "Column Break", "label": " ", - "insert_after": "august" + "insert_after": "july" }, { "fieldname": "september", @@ -5624,6 +5619,12 @@ def get_property_setters(): "property_type": "Check", "value": "0" }, + { + "doctype_or_field": "DocType", + "doc_type": "Budget", + "property": "field_order", + "value": "[\"workflow_state\", \"naming_series\", \"budget_against\", \"budget_for\", \"project\", \"cost_center\", \"cost_head\", \"fiscal_year\", \"budget_head\", \"budget_head_user\", \"total_amount\", \"column_break_3\", \"company\", \"department\", \"division\", \"budget_template\", \"region\", \"monthly_distribution\", \"amended_from\", \"section_break_6\", \"applicable_on_material_request\", \"action_if_annual_budget_exceeded_on_mr\", \"action_if_accumulated_monthly_budget_exceeded_on_mr\", \"column_break_13\", \"applicable_on_purchase_order\", \"action_if_annual_budget_exceeded_on_po\", \"action_if_accumulated_monthly_budget_exceeded_on_po\", \"section_break_16\", \"applicable_on_booking_actual_expenses\", \"action_if_annual_budget_exceeded\", \"action_if_accumulated_monthly_budget_exceeded\", \"section_break_21\", \"accounts\", \"budget_accounts\", \"default_currency\", \"company_currency\", \"rejection_feedback\"]" + }, ] def get_material_request_custom_fields(): From 7123a5b8fff25101e29c7c8dbf0cb8ad54b243af Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Sat, 7 Feb 2026 12:52:31 +0530 Subject: [PATCH 40/50] chore: removed custom html block for Ticket Module --- beams/fixtures/custom_html_block.json | 35 --------------------------- beams/hooks.py | 2 -- 2 files changed, 37 deletions(-) diff --git a/beams/fixtures/custom_html_block.json b/beams/fixtures/custom_html_block.json index 1ef06f026..c51af5e6b 100644 --- a/beams/fixtures/custom_html_block.json +++ b/beams/fixtures/custom_html_block.json @@ -74,40 +74,5 @@ ], "script": "(function () {\n if (typeof root_element === \"undefined\" || !root_element) {\n console.error(\"The 'root_element' variable is not available.\");\n return;\n }\n\n // ---------------------------\n // Manual Tab Switching\n // ---------------------------\n // This code uses data-toggle, which is for Bootstrap 4.\n // However, it's manually handling the logic, so it will still work even with Bootstrap 5.\n const tabButtons = root_element.querySelectorAll(\n 'button[data-bs-toggle=\"tab\"]'\n );\n tabButtons.forEach((tabBtn) => {\n tabBtn.addEventListener(\"click\", function (e) {\n e.preventDefault();\n\n const parentCard = this.closest(\".card\");\n parentCard\n .querySelectorAll(\".nav-link\")\n .forEach((btn) => btn.classList.remove(\"active\"));\n parentCard\n .querySelectorAll(\".tab-pane\")\n .forEach((pane) => pane.classList.remove(\"show\", \"active\"));\n\n this.classList.add(\"active\");\n const tabTarget = this.getAttribute(\"data-bs-target\");\n if (tabTarget) {\n const pane = parentCard.querySelector(tabTarget);\n if (pane) {\n pane.classList.add(\"show\", \"active\");\n }\n }\n });\n });\n\n // ---------------------------\n // Important Links - Holiday List Modal\n // ---------------------------\n root_element.addEventListener(\"click\", function (e) {\n let link = e.target.closest(\"a.open-holiday-list\");\n if (link) {\n e.preventDefault();\n\n // Only append modal if it isn't there already\n if ($(\"#holidayListModal\").length === 0) {\n $(\"body\").append(`\n
    \n
    \n
    \n
    \n

    Holiday List 2025

    \n \n
    \n
    \n
    Loading...
    \n
    \n
    \n
    \n
    \n `);\n }\n\n $(\"#holidayListModal\").modal(\"show\");\n $(\"#holiday-list-content\").html(\"Loading...\");\n\n // Get Holiday List assigned to Employee\n frappe.call({\n method: \"frappe.client.get_value\",\n args: {\n doctype: \"Employee\",\n filters: { user_id: frappe.session.user },\n fieldname: \"holiday_list\",\n },\n callback: function (res) {\n if (res && res.message && res.message.holiday_list) {\n let holiday_list_name = res.message.holiday_list;\n frappe.call({\n method: \"beams.api.workspace.get_holidays_by_list_name\",\n args: { holiday_list_name: holiday_list_name },\n callback: function (r) {\n if (r.message && r.message.length > 0) {\n let html = `\n \n \n \n \n \n \n \n \n \n \n `;\n r.message.forEach(function (holiday, index) {\n html += `\n \n \n \n \n \n \n `;\n });\n html += \"
    SL. No.Holiday ForHoliday DateNature of Holiday
    ${index + 1}${\n holiday.description || \"\"\n }${frappe.format(\n holiday.holiday_date,\n { fieldtype: \"Date\" }\n )}${\n holiday.weekly_off\n ? \"Statutory Holiday\"\n : \"Festive\"\n }
    \";\n $(\"#holiday-list-content\").html(html);\n } else {\n $(\"#holiday-list-content\").html(\n `

    No holidays found in Holiday List: ${holiday_list_name}.

    `\n );\n }\n },\n });\n } else {\n $(\"#holiday-list-content\").html(\n \"

    No Holiday List is assigned to your Employee record.

    \"\n );\n }\n },\n });\n }\n });\n\n // ---------------------------\n // Leave Tab - Load Data\n // ---------------------------\n root_element\n .querySelector(\"#leave-tab\")\n .addEventListener(\"click\", function () {\n const leaveContainer = root_element.querySelector(\"#leave\");\n leaveContainer.innerHTML = '

    Loading leaves...

    ';\n\n frappe.call({\n method: \"beams.api.workspace.get_absent_days\",\n args: {}, // No user_id to get all employees on leave today\n callback: function (r) {\n if (r.message && r.message.length > 0) {\n let html = \"\";\n\n // Fetch employee details for each leave record\n let employeePromises = r.message.map((row) => {\n return new Promise((resolve) => {\n frappe.call({\n method: \"frappe.client.get_value\",\n args: {\n doctype: \"Employee\",\n filters: { name: row.employee },\n fieldname: [\n \"employee_name\",\n \"image\",\n \"department\",\n \"designation\",\n ],\n },\n callback: function (empRes) {\n resolve({\n ...row,\n employee_name:\n empRes.message?.employee_name || row.employee,\n image:\n empRes.message?.image ||\n \"/assets/frappe/images/default-avatar.png\",\n department: empRes.message?.department || \"\",\n designation: empRes.message?.designation || \"\",\n });\n },\n });\n });\n });\n\n Promise.all(employeePromises).then((enrichedLeaves) => {\n enrichedLeaves.forEach((row) => {\n const statusBadge =\n row.workflow_state || row.status || \"Pending\";\n const statusClass =\n statusBadge === \"Approved\"\n ? \"common_success\"\n : statusBadge === \"Rejected\"\n ? \"text-danger\"\n : \"common_applied\";\n\n html += `\n
    \n
    \n \"Avatar\"\n
    \n
    ${row.employee_name}
    \n
    ${row.leave_type}
    \n
    ${frappe.format(\n row.from_date,\n { fieldtype: \"Date\" }\n )} to ${frappe.format(row.to_date, {\n fieldtype: \"Date\",\n })}
    \n
    \n
    \n
    \n ${statusBadge}\n
    \n
    `;\n });\n leaveContainer.innerHTML = html;\n });\n } else {\n leaveContainer.innerHTML =\n '

    No one is on leave today 🎉

    ';\n }\n },\n });\n });\n\n // ---------------------------\n // Birthday Tab\n // ---------------------------\n root_element\n .querySelector(\"#birthday-tab\")\n .addEventListener(\"click\", function () {\n const birthdayContainer = root_element.querySelector(\"#birthday\");\n birthdayContainer.innerHTML =\n '

    Loading birthdays...

    ';\n\n frappe.call({\n method: \"beams.api.workspace.get_today_birthdays\",\n callback: function (r) {\n if (r.message) {\n let html = \"\";\n r.message.forEach((row) => {\n const img =\n row.image || \"/assets/frappe/images/default-avatar.png\";\n html += `\n
    \n
    \n \"Avatar\"\n
    \n
    ${row.employee_name}
    \n
    (${\n row.department || \"\"\n }${\n row.designation ? \", \" + row.designation : \"\"\n })
    \n
    \n
    \n
    \n \"Birthday\"\n
    \n
    `;\n });\n if (!html) {\n html = `
    No birthdays today 🎉
    `;\n }\n birthdayContainer.innerHTML = html;\n }\n },\n });\n });\n\n // ---------------------------\n // Anniversary Tab\n // ---------------------------\n root_element\n .querySelector(\"#anniversary-tab\")\n .addEventListener(\"click\", function () {\n const anniversaryContainer = root_element.querySelector(\"#anniversary\");\n anniversaryContainer.innerHTML =\n '

    Loading anniversaries...

    ';\n\n frappe.call({\n method: \"beams.api.workspace.get_today_anniversaries\",\n callback: function (r) {\n if (r.message) {\n let html = \"\";\n r.message.forEach((row) => {\n const img =\n row.image || \"/assets/frappe/images/default-avatar.png\";\n html += `\n
    \n
    \n \"Avatar\"\n
    \n
    ${row.employee_name}
    \n
    (${\n row.department || \"\"\n }${\n row.designation ? \", \" + row.designation : \"\"\n })
    \n
    \n
    \n
    \n \"Anniversary\"\n
    \n
    `;\n });\n if (!html) {\n html = `
    No anniversaries today 🎉
    `;\n }\n anniversaryContainer.innerHTML = html;\n }\n },\n });\n });\n root_element.querySelector(\"#leave-tab\").click();\n})();\n\n\n\n\n// Show Available Leaves\nlet btn = root_element.querySelector(\".view-leave\");\n\nif (btn) {\n btn.addEventListener(\"click\", function () {\n\n // 1. Get Employee from current user\n frappe.call({\n method: \"frappe.client.get_list\",\n args: {\n doctype: \"Employee\",\n filters: { user_id: frappe.session.user },\n fields: [\"name\"],\n limit_page_length: 1\n },\n callback: function (emp_res) {\n let employee =\n emp_res.message?.length ? emp_res.message[0].name : null;\n\n if (!employee) {\n frappe.msgprint(\"No Employee record linked to your User.\");\n return;\n }\n\n // 2. Fetch Leave Details using Employee ID\n frappe.call({\n method: \"hrms.hr.doctype.leave_application.leave_application.get_leave_details\",\n args: {\n employee: employee,\n date: frappe.datetime.get_today()\n },\n callback: function (r) {\n let html = \"\";\n let data = r.message?.leave_allocation || {};\n\n if (Object.keys(data).length > 0) {\n html = `\n \n \n \n \n \n \n \n \n \n \n \n \n `;\n\n Object.entries(data).forEach(([key, value]) => {\n let color =\n value.remaining_leaves > 0\n ? \"green\"\n : \"red\";\n\n html += `\n \n \n \n \n \n \n \n \n `;\n });\n\n html += `
    Leave TypeTotal Allocated LeavesExpired LeavesUsed LeavesLeaves Pending ApprovalAvailable Leaves
    ${key}${value.total_leaves}${value.expired_leaves}${value.leaves_taken}${value.leaves_pending_approval}${value.remaining_leaves}
    `;\n } else {\n html = `

    No leaves have been allocated.

    `;\n }\n\n // 3. Show Dialog\n let d = new frappe.ui.Dialog({\n title: \"Available Leaves\",\n size: \"extra-large\",\n primary_action_label: \"Close\",\n primary_action: () => d.hide()\n });\n\n d.$body.html(html);\n d.show();\n }\n });\n }\n });\n });\n}\n", "style": "body {\r\n font-size: 14px;\r\n font-family: \"Inter\", sans-serif;\r\n font-optical-sizing: auto; \r\n font-style: normal;\r\n}\r\n.welcome_msg {\r\n border: 1px solid #E2E8F0;\r\n}\r\n.welcome_title {\r\n color: #0F172B;\r\n font-size: 20px;\r\n font-weight: 500;\r\n}\r\n.welcome_msg p {\r\n color: #45556C;\r\n font-size: 14px;\r\n margin: 0;\r\n padding: 0;\r\n}\r\n.welcome_widget {\r\n margin: 0 0 0 -30px;\r\n}\r\n.mood_widget p {\r\n color: #0F172B;\r\n font-size: 12px;\r\n}\r\nh5 { \r\n margin: 0;\r\n padding: 0;\r\n font-size: 18px;\r\n font-weight: bold;\r\n color: #0F172B;\r\n}\r\n.tabs_widget li {\r\n width: 33.3333%;\r\n}\r\n.common_tabs_widget {\r\n background: #F1F5F9;\r\n padding: 5px;\r\n border-radius: 4px;\r\n}\r\n.common-sm-font {\r\n font-size: 12px;\r\n}\r\n.btn-sm {\r\n font-size: 12px;\r\n}\r\n.text-muted {\r\n color: #8b93ab !important;\r\n}\r\n.time-label {\r\n color: #0F172B;\r\n font-size: 20px;\r\n}\r\n.card {\r\n border: 1px solid #eee;\r\n border-radius: 12px;\r\n}\r\n.small-box {\r\n background: #fff;\r\n border-radius: 12px;\r\n padding: 15px;\r\n text-align: center;\r\n}\r\n.small-box h6 {\r\n font-size: 0.75rem;\r\n color: #6c757d;\r\n text-transform: uppercase;\r\n}\r\n.small-box h2 {\r\n font-size: 1.75rem;\r\n font-weight: bold;\r\n color: #000;\r\n}\r\n.info-icon {\r\n float: right;\r\n font-size: 1rem;\r\n color: #ccc;\r\n}\r\n.label {\r\n font-weight: 500;\r\n font-size: 0.7rem;\r\n color: #3e708d;\r\n}\r\n.work-hours {\r\n font-size: 1.5rem;\r\n font-weight: 700;\r\n color: #1d1d1d;\r\n}\r\n.break-info {\r\n font-weight: 600;\r\n color: #343a40;\r\n}\r\n.btn-punch {\r\n border: 1px solid red;\r\n color: red;\r\n font-weight: bold;\r\n border-radius: 4px;\r\n padding: 7px 10px !important;\r\n margin-top: -10px;\r\n}\r\n.btn-punch:hover img {\r\n filter: brightness(0) invert(1);\r\n}\r\n.availability-section {\r\n max-width: 600px;\r\n margin: auto;\r\n background: #f9fbfd;\r\n border-radius: 16px;\r\n padding: 20px;\r\n}\r\n.section-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 20px;\r\n}\r\n.section-title {\r\n font-weight: 700;\r\n color: #1d2c4d;\r\n font-size: 16px;\r\n}\r\n.card-container {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 5px;\r\n}\r\n.availability-card {\r\n flex: 1 1 calc(50% - 15px);\r\n background: #F8FAFC;\r\n border-radius: 4px; \r\n padding: 8px;\r\n}\r\n.card-title {\r\n font-size: 13px;\r\n font-weight: 600;\r\n color: #314158;\r\n margin-bottom: 0;\r\n}\r\n.card-value {\r\n font-size: 24px;\r\n font-weight: 700;\r\n color: #0F172B;\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n}\r\n.card-icon {\r\n font-size: 18px;\r\n color: #cde5f7;\r\n}\r\n.common-bg-light {\r\n background: #f2f4f7;\r\n border: 1px solid #eff1f4;\r\n border-radius: 10px;\r\n}\r\n.section-label {\r\n font-size: 14px;\r\n color: #7B849F;\r\n}\r\n.common_blue {\r\n background-color: #eaf2ff;\r\n font-size: 12px;\r\n color: #3aa1d6;\r\n}\r\n.common_blue:hover {\r\n background-color: #c1d5f6;\r\n color: #3aa1d6;\r\n}\r\n.common_select {\r\n background-color: transparent;\r\n font-size: 14px;\r\n color: #0F172B;\r\n border: 1px solid transparent;\r\n padding: 3px !important;\r\n border-radius: 4px;\r\n transition: all 0.5s ease-in-out 0s;\r\n margin-top: -10px;\r\n}\r\n.common_select:hover {\r\n background-color: #e4e4f4;\r\n}\r\n.common_select:focus {\r\n outline: none;\r\n box-shadow: none;\r\n}\r\n.text-primary {\r\n color: #3aa1d6 !important;\r\n}\r\n.common_tabs_widget .nav-pills .nav-link.active {\r\n background: #fff;\r\n color: #0F172B;\r\n font-weight: 500;\r\n}\r\n.common_tabs_widget .nav-pills .nav-link {\r\n background: #F1F5F9;\r\n color: #45556C;\r\n text-align: center;\r\n width: 100%;\r\n padding: 6px;\r\n font-size: 14px;\r\n}\r\n.list-group-item {\r\n border: none !important;\r\n padding: 0.40rem 0 !important;\r\n color: #0F172B;\r\n font-size: 14px;\r\n}\r\n.list-group-item:first-child {\r\n border-top-left-radius: 0;\r\n border-top-right-radius: 0;\r\n}\r\n.list-group-item img {\r\n width: 18px;\r\n margin: 0 5px 0 0;\r\n}\r\n.btn-outline-primary {\r\n border: 1px solid #3aa1d6;\r\n color: #3aa1d6;\r\n}\r\n.btn-outline-primary:hover {\r\n background: #3aa1d6;\r\n color: #fff;\r\n}\r\n.fw-bold {\r\n font-weight: bold;\r\n}\r\n.g-0>[class*=\"col\"],\r\n.gx-0>[class*=\"col\"] {\r\n padding-left: 0 !important;\r\n padding-right: 0 !important;\r\n}\r\n.g-0,\r\n.gx-0 {\r\n margin-left: 0 !important;\r\n margin-right: 0 !important;\r\n}\r\n.g-0>[class*=\"col\"],\r\n.gy-0>[class*=\"col\"] {\r\n padding-top: 0 !important;\r\n padding-bottom: 0 !important;\r\n}\r\n.g-0,\r\n.gy-0 {\r\n margin-top: 0 !important;\r\n margin-bottom: 0 !important;\r\n}\r\n.g-1>[class*=\"col\"] {\r\n padding: 4px !important;\r\n}\r\n.g-1 {\r\n margin: -4px !important;\r\n}\r\n.gx-1>[class*=\"col\"] {\r\n padding-left: 4px !important;\r\n padding-right: 4px !important;\r\n}\r\n.gx-1 {\r\n margin-left: 4px !important;\r\n margin-right: 4px !important;\r\n}\r\n.gy-1>[class*=\"col\"] {\r\n padding-top: 4px !important;\r\n padding-bottom: 4px !important;\r\n}\r\n.gy-1 {\r\n margin-top: 4px !important;\r\n margin-bottom: 4px !important;\r\n}\r\n/* Repeat for g-2 to g-5 */\r\n.g-2>[class*=\"col\"] {\r\n padding: 8px !important;\r\n}\r\n.g-2 {\r\n margin: -16px !important;\r\n}\r\n.rounded-3 {\r\n border-radius: 6px;\r\n}\r\n.common_badge_color {\r\n background: #2b7fff;\r\n color: #fff;\r\n}\r\ninput:checked+.slider {\r\n background-color: #01c16a !important;\r\n}\r\n.btn-secondary .icon {\r\n filter: brightness(0) invert(1) !important;\r\n}\r\n.border-none {\r\n border: none !important;\r\n background: transparent;\r\n}\r\n.carousel-control-prev {\r\n left: 44% !important;\r\n}\r\n.carousel-control-next {\r\n right: 44% !important;\r\n}\r\n.carousel-control-prev,\r\n.carousel-control-next {\r\n top: auto !important;\r\n}\r\n.btn_contoler {\r\n width: 30px;\r\n height: 30px;\r\n background: rgba(0, 0, 0, 0.2);\r\n border-radius: 50%;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n.notice_height {\r\n height: 130px;\r\n}\r\n.list-group {\r\n height: auto;\r\n max-height: 250px;\r\n overflow: auto;\r\n}\r\n.common_link_apply {\r\n color: #2B7FFF;\r\n font-weight: 500; \r\n transition: all 0.5s ease-in-out 0s;\r\n font-size: 14px;\r\n}\r\n.common_link_apply:hover {\r\n color: #2B7FFF;\r\n text-decoration: underline;\r\n}\r\n.common_success {\r\n color: #00C16A;\r\n font-weight: 500;\r\n}\r\n.common_applied {\r\n color: #62748E;\r\n font-weight: 500;\r\n}\r\n.common_font_size {\r\n font-size: 14px;\r\n color: #0F172B;\r\n}\r\n.separate_border {\r\n border-top: 1px solid #E2E8F0;\r\n}\r\n.counter_color {\r\n color: #0F172B;\r\n font-size: 20px;\r\n}\r\n.common_highlight {\r\n color: #2B7FFF;\r\n font-weight: 500; \r\n transition: all 0.5s ease-in-out 0s;\r\n font-size: 14px;\r\n}\r\n.common_widget p {\r\n color: #45556C;\r\n}\r\n.counter_big {\r\n font-size: 20px;\r\n color: #62748E;\r\n font-weight: 500; \r\n}\r\n.counter_big span {\r\n color: #0F172B;\r\n font-weight: 600;\r\n}\r\n.switch {\r\n position: relative;\r\n display: inline-block;\r\n width: 40px;\r\n height: 20px;\r\n}\r\n\r\n.switch input { \r\n opacity: 0;\r\n width: 0;\r\n height: 0;\r\n}\r\n\r\n.slider {\r\n position: absolute;\r\n cursor: pointer;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n bottom: 0;\r\n background-color: #E2E8F0;\r\n -webkit-transition: .4s;\r\n transition: .4s;\r\n}\r\n\r\n.slider:before {\r\n position: absolute;\r\n content: \"\";\r\n height: 16px;\r\n width: 16px;\r\n left: 2px;\r\n bottom: 2px;\r\n background-color: white;\r\n -webkit-transition: .4s;\r\n transition: .4s;\r\n}\r\n\r\ninput:checked + .slider {\r\n background-color: #00C16A;\r\n}\r\n\r\ninput:focus + .slider {\r\n box-shadow: 0 0 1px #00C16A;\r\n}\r\n\r\ninput:checked + .slider:before {\r\n -webkit-transform: translateX(20px);\r\n -ms-transform: translateX(20px);\r\n transform: translateX(20px);\r\n}\r\n\r\n/* Rounded sliders */\r\n.slider.round {\r\n border-radius: 34px;\r\n}\r\n\r\n.slider.round:before {\r\n border-radius: 50%;\r\n}\r\n.donut-center {\r\n position: absolute;\r\n top: 50%;\r\n left: 38%;\r\n transform: translate(-50%, -50%);\r\n text-align: center;\r\n pointer-events: none;\r\n}\r\n.donut-center .number {\r\n font-size: 3.5rem;\r\n font-weight: bold;\r\n color: #111;\r\n}\r\n.donut-center .label {\r\n font-size: 1.5rem;\r\n color: #555;\r\n}\r\n.break_widget .col-3 {\r\n text-align: right;\r\n}\r\n@media all and (max-width:768px) {\r\n \r\n.welcome_title {\r\n font-size: 1.5rem;\r\n}\r\n.time-label {\r\n font-size: 16px;\r\n}\r\n.time-utilization-card img {\r\n margin-top: 10px;\r\n}\r\n .welcome_widget {\r\n margin: 0;\r\n }\r\n .welcome_msg .col-sm-2,\r\n.welcome_msg .col-sm-7,\r\n.welcome_msg .col-sm-3 { \r\n max-width: 100%;\r\n text-align: center;\r\n}\r\n.common_widget .counter_big {\r\n margin: 20px 0;\r\n}\r\n.common_widget .col-sm-6 {\r\n max-width: 50%;\r\n}\r\n.common_widget p {\r\n margin: 20px 0 0 0!important;\r\n}\r\n.count_widget_ouput {\r\n margin-top: 20px;\r\n}\r\n.btn-punch {\r\n margin-top: 0;\r\n margin-bottom: 10px;\r\n}\r\n.break_widget .col-6 {\r\n max-width: 100%;\r\n flex: 0 0 100%;\r\n}\r\n.border-right {\r\n border-right: 0 !important;\r\n border-bottom: 1px solid #dee2e6;\r\n margin-bottom: 10px;\r\n}\r\n.border_right_right {\r\n border-right: 0 !important;\r\n border-bottom: 1px solid #dee2e6;\r\n margin: 0 0 10px 0;\r\n}\r\n.break_widget .mb-4 {\r\n margin-bottom: .5rem !important;\r\n}\r\n.break_widget .pl-2 {\r\n padding-left: 0!important;\r\n}\r\n.break_widget .pr-2 {\r\n padding-right: 0!important;\r\n}\r\n}\r\n.layout-main-section {\r\n padding: 0!important;\r\n border: none!important;\r\n}\r\n.layout-main-section-wrapper {\r\n overflow-y: inherit!important;\r\n}" - }, - { - "docstatus": 0, - "doctype": "Custom HTML Block", - "html": "", - "modified": "2025-12-16 15:09:16.242452", - "name": "Ticket Dashboard", - "private": 0, - "roles": [], - "script": "frappe.after_ajax(() => {\n const iframe = root_element.querySelector('#ticket_dashboard_iframe'); // your iframe id\n iframe.addEventListener('load', () => {\n const iframeDoc = iframe.contentDocument;\n const observer = new MutationObserver(() => {\n const sidebar = iframeDoc.querySelector('.flex.select-none.flex-col.border-r.border-gray-200.bg-gray-50');\n if (sidebar) {\n sidebar.style.display = 'none';\n }\n });\n\n observer.observe(iframeDoc.body, {\n childList: true,\n subtree: true\n });\n });\n});", - "style": null - }, - { - "docstatus": 0, - "doctype": "Custom HTML Block", - "html": "
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n \r\n \r\n \r\n
    \r\n
    Tickets Overview
    \r\n
    \r\n
    \r\n
    \r\n
    Total
    \r\n
    0
    \r\n
    \r\n
    \r\n
    Open
    \r\n
    0
    \r\n
    \r\n
    \r\n
    \r\n\r\n \r\n
    \r\n
    \r\n
    \r\n \r\n \r\n \r\n
    \r\n
    Status Breakdown
    \r\n
    \r\n
    \r\n
    \r\n
    Hold
    \r\n
    0
    \r\n
    \r\n
    \r\n
    Closed
    \r\n
    0
    \r\n
    \r\n
    \r\n
    \r\n\r\n \r\n
    \r\n
    \r\n
    \r\n \r\n \r\n \r\n
    \r\n
    Performance
    \r\n
    \r\n
    \r\n
    \r\n
    Response
    \r\n
    0h
    \r\n
    \r\n
    \r\n
    Resolution
    \r\n
    0h
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n\r\n\r\n
    \r\n
    \r\n \r\n \r\n
    \r\n\r\n \r\n\r\n \r\n \r\n \r\n\r\n \r\n
    \r\n\r\n
    \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
    \r\n \r\n IDSubjectStatusPriorityAgentCreated ByCreatedModified
    \r\n Loading...\r\n
    \r\n\r\n
    \r\n \r\n \r\n \r\n
    \r\n
    \r\n
    ", - "modified": "2025-12-16 15:59:27.978784", - "name": "Ticket Summary", - "private": 0, - "roles": [ - { - "parent": "Ticket Summary", - "parentfield": "roles", - "parenttype": "Custom HTML Block", - "role": "Agent" - }, - { - "parent": "Ticket Summary", - "parentfield": "roles", - "parenttype": "Custom HTML Block", - "role": "Agent Manager" - } - ], - "script": "frappe.after_ajax(() => {\r\n console.log(\"Ticket UI loaded\");\r\n\r\n let currentPage = 1;\r\n const pageSize = 10;\r\n let searchTimeout = null;\r\n\r\n if (!root_element) {\r\n console.error(\"root_element is undefined or null\");\r\n return;\r\n }\r\n\r\n // Hide/show bulk actions based on user email\r\n function toggleBulkActionsVisibility() {\r\n const actionSelect = root_element.querySelector(\"#actionSelect\");\r\n if (!actionSelect) return;\r\n\r\n const allowedEmails = [\r\n \"developer@mediaonetv.in\",\r\n \"habeeb@mediaonetv.in\",\r\n \"yoonus@mediaonetv.in\",\r\n \"vaishakh.prakash@mediaonetv.in\"\r\n ];\r\n\r\n const currentUserEmail = frappe.session.user;\r\n\r\n if (allowedEmails.includes(currentUserEmail)) {\r\n actionSelect.style.display = \"block\";\r\n } else {\r\n actionSelect.style.display = \"none\";\r\n }\r\n }\r\n\r\n // ==========================\r\n // DASHBOARD CARDS (COUNTS)\r\n // ==========================\r\n// ==========================\r\n// DASHBOARD CARDS (COUNTS)\r\n// ==========================\r\n// ==========================\r\n// DASHBOARD CARDS (COUNTS)\r\n// ==========================\r\nfunction updateDashboardCounts() {\r\n const totalEl = root_element.querySelector(\"#totalTicketsCount\");\r\n const openEl = root_element.querySelector(\"#openTicketsCount\");\r\n const holdEl = root_element.querySelector(\"#holdTicketsCount\");\r\n const closedEl = root_element.querySelector(\"#closedTicketsCount\");\r\n\r\n // 1) TOTAL: all statuses, ticket_type filter\r\n const baseFilters = {\r\n ticket_type: [\"in\", [\"Technical\", \"Unspecified\"]]\r\n };\r\n\r\n // helper: small wrapper for get_count\r\n function getCount(extraFilters) {\r\n const filters = Object.assign({}, baseFilters, extraFilters || {});\r\n return frappe.call({\r\n method: \"frappe.client.get_count\",\r\n args: {\r\n doctype: \"HD Ticket\",\r\n filters: filters\r\n }\r\n });\r\n }\r\n\r\n Promise.all([\r\n // total\r\n getCount(),\r\n // open = Open + Working\r\n getCount({ status: [\"in\", [\"Open\", \"Working\"]] }),\r\n // hold = Hold\r\n getCount({ status: \"Hold\" }),\r\n // closed = Closed\r\n getCount({ status: \"Closed\" })\r\n ])\r\n .then(([totalRes, openRes, holdRes, closedRes]) => {\r\n const total = (totalRes && totalRes.message) || 0;\r\n const open = (openRes && openRes.message) || 0;\r\n const hold = (holdRes && holdRes.message) || 0;\r\n const closed = (closedRes && closedRes.message) || 0;\r\n\r\n if (totalEl) {\r\n totalEl.textContent = total;\r\n totalEl.classList.add(\"animate-count\");\r\n setTimeout(() => totalEl.classList.remove(\"animate-count\"), 600);\r\n }\r\n\r\n if (openEl) {\r\n openEl.textContent = open;\r\n openEl.classList.add(\"animate-count\");\r\n setTimeout(() => openEl.classList.remove(\"animate-count\"), 600);\r\n }\r\n\r\n if (holdEl) {\r\n holdEl.textContent = hold;\r\n holdEl.classList.add(\"animate-count\");\r\n setTimeout(() => holdEl.classList.remove(\"animate-count\"), 600);\r\n }\r\n\r\n if (closedEl) {\r\n closedEl.textContent = closed;\r\n closedEl.classList.add(\"animate-count\");\r\n setTimeout(() => closedEl.classList.remove(\"animate-count\"), 600);\r\n }\r\n })\r\n .catch(err => {\r\n console.error(\"Error updating dashboard counts:\", err);\r\n });\r\n}\r\n\r\n\r\n\r\n initFilters();\r\n loadTicketsWithDefaultTypes();\r\n updateDashboardCounts();\r\n toggleBulkActionsVisibility();\r\n\r\n // Search input debounce + prevent ERPNext shortcuts\r\n const searchInput = root_element.querySelector(\"#ticketSearchBox\");\r\n [\"keydown\", \"keypress\", \"keyup\"].forEach(evt =>\r\n searchInput.addEventListener(evt, e => e.stopPropagation())\r\n );\r\n searchInput.addEventListener(\"input\", () => {\r\n clearTimeout(searchTimeout);\r\n searchTimeout = setTimeout(() => {\r\n currentPage = 1;\r\n loadTicketsWithDefaultTypes();\r\n updateDashboardCounts();\r\n toggleBulkActionsVisibility();\r\n }, 300);\r\n });\r\n\r\n // Pagination buttons\r\n root_element.querySelector(\"#nextPage\").addEventListener(\"click\", () => {\r\n currentPage++;\r\n loadTicketsWithDefaultTypes();\r\n updateDashboardCounts();\r\n toggleBulkActionsVisibility();\r\n });\r\n root_element.querySelector(\"#prevPage\").addEventListener(\"click\", () => {\r\n if (currentPage > 1) currentPage--;\r\n loadTicketsWithDefaultTypes();\r\n updateDashboardCounts();\r\n toggleBulkActionsVisibility();\r\n });\r\n\r\n // New Ticket button\r\n const newTicketBtn = root_element.querySelector(\"#newTicketButton\");\r\n if (newTicketBtn) {\r\n newTicketBtn.addEventListener(\"click\", () => {\r\n frappe.new_doc(\"HD Ticket\");\r\n });\r\n }\r\n\r\n // Select All checkbox handler\r\n root_element\r\n .querySelector(\"#selectAllCheckbox\")\r\n .addEventListener(\"change\", e => {\r\n const checked = e.target.checked;\r\n root_element.querySelectorAll(\".ticket-checkbox\").forEach(cb => {\r\n cb.checked = checked;\r\n });\r\n });\r\n\r\n // Action dropdown for bulk actions\r\n const actionSelect = root_element.querySelector(\"#actionSelect\");\r\n actionSelect.addEventListener(\"change\", async function () {\r\n const selectedAction = this.value;\r\n if (!selectedAction) return;\r\n\r\n const selectedCheckboxes = root_element.querySelectorAll(\r\n \".ticket-checkbox:checked\"\r\n );\r\n if (selectedCheckboxes.length === 0) {\r\n alert(\"Please select at least one ticket to perform the action.\");\r\n this.value = \"\";\r\n return;\r\n }\r\n\r\n const ticketIds = Array.from(selectedCheckboxes).map(cb =>\r\n cb.getAttribute(\"data-ticket-id\")\r\n );\r\n\r\n if (selectedAction === \"5\") {\r\n const confirmed = confirm(\r\n `Are you sure you want to delete ${ticketIds.length} ticket(s)? This action cannot be undone.`\r\n );\r\n if (!confirmed) {\r\n this.value = \"\";\r\n return;\r\n }\r\n }\r\n\r\n const statusMap = {\r\n \"1\": \"Working\",\r\n \"2\": \"Hold\",\r\n \"3\": \"Working\",\r\n \"4\": \"Closed\"\r\n };\r\n\r\n try {\r\n for (const id of ticketIds) {\r\n if (selectedAction === \"5\") {\r\n await frappe.call({\r\n method: \"frappe.client.delete\",\r\n args: { doctype: \"HD Ticket\", name: id }\r\n });\r\n } else {\r\n await frappe.call({\r\n method: \"frappe.client.set_value\",\r\n args: {\r\n doctype: \"HD Ticket\",\r\n name: id,\r\n fieldname: \"status\",\r\n value: statusMap[selectedAction]\r\n }\r\n });\r\n }\r\n }\r\n alert(\r\n `Action '${this.options[this.selectedIndex].text}' performed on ${ticketIds.length} ticket(s).`\r\n );\r\n this.value = \"\";\r\n loadTicketsWithDefaultTypes();\r\n updateDashboardCounts();\r\n toggleBulkActionsVisibility();\r\n } catch (error) {\r\n console.error(\"Error during bulk action:\", error);\r\n alert(\"An error occurred performing the action. See console.\");\r\n }\r\n });\r\n\r\n // =====================================================\r\n // FULL FORM + NATIVE TIMELINE IN A POPUP (IFRAME DIALOG)\r\n // =====================================================\r\n function openTicketFullFormDialog(ticketId) {\r\n if (!window.ticketFullFormDialog) {\r\n window.ticketFullFormDialog = new frappe.ui.Dialog({\r\n title: __(\"HD Ticket\"),\r\n size: \"extra-large\",\r\n fields: [\r\n {\r\n fieldname: \"ticket_frame\",\r\n fieldtype: \"HTML\"\r\n }\r\n ],\r\n primary_action_label: __(\"Open in Full Page\"),\r\n primary_action() {\r\n if (window.ticketFullFormDialog.ticketId) {\r\n frappe.set_route(\r\n \"Form\",\r\n \"HD Ticket\",\r\n window.ticketFullFormDialog.ticketId\r\n );\r\n }\r\n }\r\n });\r\n }\r\n\r\n window.ticketFullFormDialog.ticketId = ticketId;\r\n\r\n const formUrl = `/app/hd-ticket/${encodeURIComponent(ticketId)}`;\r\n\r\n const html = `\r\n
    \r\n \r\n
    \r\n `;\r\n\r\n window.ticketFullFormDialog\r\n .get_field(\"ticket_frame\")\r\n .$wrapper.html(html);\r\n window.ticketFullFormDialog.show();\r\n }\r\n\r\n // Initialize status filter checkboxes instead of dropdown\r\n function initFilters() {\r\n const statuses = [\"Open\", \"Working\", \"Hold\", \"Closed\"];\r\n const container = root_element.querySelector(\"#statusFilter\");\r\n container.innerHTML = \"\";\r\n statuses.forEach((status, idx) => {\r\n const id = `statusCheckbox_${idx}`;\r\n const checkboxHTML = `\r\n \r\n `;\r\n container.innerHTML += checkboxHTML;\r\n });\r\n\r\n container.querySelectorAll(\"input[type=checkbox]\").forEach(checkbox => {\r\n checkbox.addEventListener(\"change\", () => {\r\n currentPage = 1;\r\n loadTicketsWithDefaultTypes();\r\n updateDashboardCounts();\r\n toggleBulkActionsVisibility();\r\n });\r\n });\r\n }\r\n\r\n // Format date for relative time string\r\n function formatTimeDifference(isoDateStr) {\r\n if (!isoDateStr) return \"-\";\r\n const date = new Date(isoDateStr);\r\n const now = new Date();\r\n const diffMs = now - date;\r\n const diffSec = Math.floor(diffMs / 1000);\r\n if (diffSec < 60) return `${diffSec} sec Ago`;\r\n const diffMin = Math.floor(diffSec / 60);\r\n if (diffMin < 60) return `${diffMin} min Ago`;\r\n const diffHr = Math.floor(diffMin / 60);\r\n if (diffHr < 24) return `${diffHr} hr Ago`;\r\n const diffDay = Math.floor(diffHr / 24);\r\n if (diffDay < 7) return `${diffDay} day${diffDay > 1 ? \"s\" : \"\"} Ago`;\r\n return isoDateStr.slice(0, 10);\r\n }\r\n\r\n // Load tickets filtered by selected statuses (checkboxes)\r\n function loadTicketsWithDefaultTypes() {\r\n const checkedBoxes = Array.from(\r\n root_element.querySelectorAll(\"#statusFilter input[type=checkbox]:checked\")\r\n );\r\n const selectedStatuses = checkedBoxes.map(cb => cb.value);\r\n\r\n const searchTerm =\r\n root_element.querySelector(\"#ticketSearchBox\")?.value\r\n .trim()\r\n .toLowerCase() || \"\";\r\n\r\n const filters = {};\r\n if (selectedStatuses.length > 0) {\r\n filters.status = [\"in\", selectedStatuses];\r\n }\r\n filters.ticket_type = [\"in\", [\"Technical\", \"Unspecified\"]];\r\n\r\n frappe.call({\r\n method: \"frappe.client.get_list\",\r\n args: {\r\n doctype: \"HD Ticket\",\r\n fields: [\r\n \"name\",\r\n \"subject\",\r\n \"status\",\r\n \"priority\",\r\n \"assigned_agent_name\",\r\n \"employee_name\",\r\n \"contact\",\r\n \"description\",\r\n \"creation\",\r\n \"modified\"\r\n ],\r\n limit_page_length: pageSize,\r\n limit_start: (currentPage - 1) * pageSize,\r\n filters: filters,\r\n order_by: \"modified desc\"\r\n },\r\n callback: renderTickets\r\n });\r\n }\r\n\r\n // Render tickets into the table\r\n function renderTickets(res) {\r\n const tbody = root_element.querySelector(\"#ticketTableBody\");\r\n tbody.innerHTML = \"\";\r\n\r\n const allTickets = res.message;\r\n\r\n if (!allTickets.length) {\r\n tbody.innerHTML = `No records found`;\r\n root_element.querySelector(\"#pageIndicator\").innerText = `Page ${currentPage}`;\r\n toggleBulkActionsVisibility();\r\n return;\r\n }\r\n\r\n const searchTerm =\r\n root_element.querySelector(\"#ticketSearchBox\")?.value\r\n .trim()\r\n .toLowerCase() || \"\";\r\n\r\n const filteredMessages = allTickets.filter(t => {\r\n if (!searchTerm) return true;\r\n\r\n const haystack = [\r\n t.name,\r\n t.subject,\r\n t.description,\r\n t.status,\r\n t.priority,\r\n t.assigned_agent_name,\r\n t.employee_name,\r\n t.contact,\r\n formatTimeDifference(t.creation),\r\n formatTimeDifference(t.modified)\r\n ]\r\n .filter(Boolean)\r\n .join(\" \")\r\n .toLowerCase();\r\n\r\n return haystack.includes(searchTerm);\r\n });\r\n\r\n if (!filteredMessages.length) {\r\n tbody.innerHTML = `No matching records found`;\r\n root_element.querySelector(\"#pageIndicator\").innerText = `Page ${currentPage}`;\r\n toggleBulkActionsVisibility();\r\n return;\r\n }\r\n\r\n filteredMessages.forEach(t => {\r\n const statusClass = `status-${t.status.replace(/\\s+/g, \"\")}`;\r\n const priorityClass = `priority-${t.priority.replace(/\\s+/g, \"\")}`;\r\n const createdAgo = formatTimeDifference(t.creation);\r\n const modifiedAgo = formatTimeDifference(t.modified);\r\n const plainDesc = t.description\r\n ? new DOMParser().parseFromString(t.description, \"text/html\").body\r\n .textContent\r\n : \"\";\r\n const plainSub = t.subject\r\n ? new DOMParser().parseFromString(t.subject, \"text/html\").body\r\n .textContent\r\n : \"\";\r\n const safeDesc = plainDesc.replace(/\"/g, \""\");\r\n const safeSub = plainSub.replace(/\"/g, \""\");\r\n\r\n const row = `\r\n \r\n \r\n ${t.name}\r\n ${(t.subject && t.subject.length > 10) ? t.subject.slice(0, 10) + \"...\" : t.subject}\r\n ${t.status}\r\n ${t.priority}\r\n ${t.assigned_agent_name || \"Not Assigned\"}\r\n ${t.employee_name || t.contact || \"-\"}\r\n ${createdAgo}\r\n ${modifiedAgo}\r\n \r\n `;\r\n\r\n tbody.innerHTML += row;\r\n });\r\n\r\n // Row click → open full form in popup\r\n tbody.querySelectorAll(\"tr\").forEach(tr => {\r\n tr.addEventListener(\"click\", e => {\r\n if (e.target.tagName.toLowerCase() === \"input\" && e.target.type === \"checkbox\") return;\r\n const ticketId = tr\r\n .querySelector(\"input.ticket-checkbox\")\r\n ?.getAttribute(\"data-ticket-id\");\r\n if (ticketId) {\r\n openTicketFullFormDialog(ticketId);\r\n }\r\n });\r\n });\r\n\r\n const allCheckboxes = root_element.querySelectorAll(\".ticket-checkbox\");\r\n allCheckboxes.forEach(cb => {\r\n cb.addEventListener(\"change\", () => {\r\n const selectAllCheckbox = root_element.querySelector(\r\n \"#selectAllCheckbox\"\r\n );\r\n selectAllCheckbox.checked = Array.from(allCheckboxes).every(\r\n chk => chk.checked\r\n );\r\n });\r\n });\r\n\r\n root_element.querySelector(\"#pageIndicator\").innerText = `Page ${currentPage}`;\r\n toggleBulkActionsVisibility();\r\n }\r\n});\r\n", - "style": "#ticket-table {\r\n width: 100%;\r\n border-collapse: collapse;\r\n font-size: 15px;\r\n background: #fff;\r\n border: 1px solid #cdd5e0;\r\n}\r\n\r\n#ticket-table thead th {\r\n background: #1d467d;\r\n color: #fff;\r\n font-weight: 700;\r\n padding: 3px 8px;\r\n font-size: 12px;\r\n border-right: 1px solid #cdd5e0;\r\n letter-spacing: 0.03em;\r\n text-transform: uppercase;\r\n}\r\n#ticket-table thead th:last-child {\r\n border-right: none;\r\n}\r\n\r\n#ticket-table td {\r\n padding: 3px 8px;\r\n border-bottom: 1px solid #cdd5e0;\r\n \r\n font-size: 12px;\r\n color: #28354a; /* Not grey */\r\n background: #fff;\r\n}\r\n#ticket-table td:last-child {\r\n border-right: none;\r\n}\r\n\r\n#ticket-table tbody tr:hover td {\r\n background: #D3D3D3 !important; /* Soft highlight on hover */\r\n color: black;\r\n cursor: pointer;\r\n}\r\n\r\n.ticket-priority::before {\r\n content: \"\";\r\n display: inline-block;\r\n width: 10px;\r\n height: 10px;\r\n border-radius: 50%;\r\n margin-right: 8px;\r\n vertical-align: middle;\r\n}\r\n\r\n/* Colored dots by priority */\r\n.priority-High::before {\r\n background-color: #e74c3c; /* Red */\r\n}\r\n\r\n.priority-Medium::before {\r\n background-color: #f1c40f; /* Yellow */\r\n}\r\n\r\n.priority-Low::before {\r\n background-color: #27ae60; /* Green */\r\n}\r\n\r\n.pagination-btn {\r\n padding: 3px 8px;\r\n margin: 0 6px;\r\n font-size: 12px;\r\n background: #f5f9fb;\r\n border: 1px solid #cdd5e0;\r\n color: #1d467d;\r\n border-radius: 5px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n}\r\n.pagination-btn:disabled {\r\n background: #f5f5f5;\r\n color: #94a4c0;\r\n cursor: not-allowed;\r\n}\r\n.pagination-btn:hover:not(:disabled) {\r\n background: #e1eaf4;\r\n color: #1d467d;\r\n}\r\n#ticket-table thead th:first-child {\r\n border-top-left-radius: 9px;\r\n border-bottom-left-radius: 9px;\r\n}\r\n#ticket-table thead th:last-child {\r\n border-top-right-radius: 9px;\r\n border-bottom-right-radius: 9px;\r\n}\r\n\r\n/* Tooltip container */\r\n.tooltip {\r\n position: relative;\r\n display: inline-block;\r\n cursor: help;\r\n}\r\n\r\n/* Tooltip text box - hidden by default */\r\n.tooltip .tooltiptext {\r\n visibility: hidden;\r\n width: 220px;\r\n background-color: #1d467d;\r\n color: #fff;\r\n text-align: left;\r\n border-radius: 8px;\r\n padding: 10px 12px;\r\n position: absolute;\r\n z-index: 10;\r\n bottom: 125%; /* above the text */\r\n left: 50%;\r\n transform: translateX(-50%);\r\n box-shadow: 0 4px 10px rgba(29, 70, 125, 0.3);\r\n opacity: 0;\r\n transition: opacity 0.3s ease;\r\n font-size: 13px;\r\n line-height: 1.4;\r\n}\r\n\r\n/* Arrow pointer */\r\n.tooltip .tooltiptext::after {\r\n content: \"\";\r\n position: absolute;\r\n top: 100%;\r\n left: 50%;\r\n margin-left: -7px;\r\n border-width: 7px;\r\n border-style: solid;\r\n border-color: #1d467d transparent transparent transparent;\r\n}\r\n\r\n/* Show the tooltip text on hover */\r\n.tooltip:hover .tooltiptext {\r\n visibility: visible;\r\n opacity: 1;\r\n}\r\n\r\n.dropdown-content label {\r\n display: block;\r\n font-size: 12px;\r\n cursor: pointer;\r\n user-select: none;\r\n padding: 4px 0;\r\n}\r\n\r\n.dropdown-content label:hover {\r\n background-color: #e1eaf4;\r\n}\r\n\r\n.dashboard-hero {\r\n background: none;\r\n border-radius: 16px;\r\n margin: 10px auto 20px;\r\n max-width: 1100px;\r\n position: relative;\r\n overflow: hidden;\r\n}\r\n\r\n/* optional grain disabled for clean look */\r\n.dashboard-hero::before {\r\n content: '';\r\n position: absolute;\r\n inset: 0;\r\n pointer-events: none;\r\n}\r\n\r\n.card-row {\r\n display: flex;\r\n gap: 12px; /* smaller gap */\r\n justify-content: center;\r\n position: relative;\r\n z-index: 1;\r\n}\r\n\r\n.card.wow-card {\r\n flex: 1;\r\n max-width: 320px; /* slimmer */\r\n background: rgba(255,255,255,0.97);\r\n backdrop-filter: blur(12px);\r\n border-radius: 16px;\r\n padding: 14px 16px; /* reduced padding */\r\n box-shadow: 0 6px 18px rgba(0,0,0,0.08);\r\n border: 1px solid rgba(15,23,42,0.06);\r\n transition: all 0.25s ease;\r\n position: relative;\r\n overflow: hidden;\r\n}\r\n\r\n.card.wow-card::before {\r\n content: '';\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n height: 3px;\r\n background: linear-gradient(90deg, var(--card-accent));\r\n}\r\n\r\n.card.wow-card:hover {\r\n transform: translateY(-4px);\r\n box-shadow: 0 10px 28px rgba(0,0,0,0.16);\r\n}\r\n\r\n.card.wow-card.active { --card-accent: #10b981; }\r\n.card.wow-card.warning { --card-accent: #f59e0b; }\r\n.card.wow-card.premium { --card-accent: #8b5cf6; }\r\n\r\n/* header: icon + title same row */\r\n.compact-header {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n margin-bottom: 10px;\r\n}\r\n\r\n.icon-wrapper {\r\n width: 32px;\r\n height: 32px;\r\n border-radius: 10px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: #fff;\r\n font-size: 16px;\r\n margin-bottom: 0;\r\n}\r\n\r\n.icon-wrapper.success { background: linear-gradient(135deg, #10b981, #059669); }\r\n.icon-wrapper.warning { background: linear-gradient(135deg, #f59e0b, #d97706); }\r\n.icon-wrapper.premium { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }\r\n\r\n.card-title {\r\n font-weight: 700;\r\n font-size: 14px;\r\n color: #1f2937;\r\n margin: 0;\r\n letter-spacing: -0.01em;\r\n}\r\n\r\n/* metrics compact */\r\n.metric-group {\r\n display: flex;\r\n gap: 12px;\r\n}\r\n\r\n/* Make the two metrics sit left and right in one row */\r\n.metric-group.compact-metrics {\r\n display: flex;\r\n justify-content: space-between; /* pushes left metric to start, right to end */\r\n align-items: flex-end;\r\n}\r\n\r\n/* Do not let items stretch weirdly */\r\n.metric-group.compact-metrics .metric-item {\r\n flex: 0 0 auto;\r\n}\r\n\r\n/* Optional: explicit text alignment */\r\n.metric-group.compact-metrics .metric-item:first-child {\r\n text-align: left;\r\n}\r\n\r\n.metric-group.compact-metrics .metric-item:last-child {\r\n text-align: right;\r\n}\r\n\r\n\r\n.compact-metrics {\r\n align-items: flex-end;\r\n}\r\n\r\n.metric-item {\r\n flex: 1;\r\n}\r\n\r\n.metric-label {\r\n font-size: 11px;\r\n color: #6b7280;\r\n font-weight: 600;\r\n margin-bottom: 4px;\r\n letter-spacing: 0.04em;\r\n}\r\n\r\n.metric-value {\r\n color: #1f2937 !important;\r\n font-size: 32px !important;\r\n font-weight: 700 !important;\r\n background: none !important;\r\n -webkit-text-fill-color: #1f2937 !important;\r\n background-clip: initial !important;\r\n}\r\n\r\n/* counter animation (optional) */\r\n.animate-count {\r\n animation: countUp 1s ease-out;\r\n}\r\n\r\n@keyframes countUp {\r\n from { transform: scale(0.9); opacity: 0; }\r\n to { transform: scale(1); opacity: 1; }\r\n}\r\n\r\n/* responsive */\r\n@media (max-width: 768px) {\r\n .card-row { flex-direction: column; }\r\n .metric-group { gap: 8px; }\r\n}\r\n" } ] \ No newline at end of file diff --git a/beams/hooks.py b/beams/hooks.py index f86eeb89f..0d7ff199e 100644 --- a/beams/hooks.py +++ b/beams/hooks.py @@ -579,8 +579,6 @@ "Availability and Attendance", "HR Message", "Adherence and Break", - "Ticket Summary", - "Ticket Dashboard", ], ] ], From f50e3d977a5caec11000a84b9d05b2b1041fae59 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Sat, 7 Feb 2026 13:18:05 +0530 Subject: [PATCH 41/50] fix: filter company and department and budget_approver --- .../budget_template/budget_template.js | 7 +- .../budget_template/budget_template.py | 67 +++++++++++++------ 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/beams/beams/doctype/budget_template/budget_template.js b/beams/beams/doctype/budget_template/budget_template.js index f95f50fb1..ba08b932b 100644 --- a/beams/beams/doctype/budget_template/budget_template.js +++ b/beams/beams/doctype/budget_template/budget_template.js @@ -12,6 +12,7 @@ frappe.ui.form.on('Budget Template', { frm.set_value('division',) clear_budget_items(frm); } + set_filters(frm); }, company: function (frm) { frm.set_value('department', null); @@ -84,12 +85,12 @@ function set_filters(frm) { } } }); - frm.set_query('budget_head', function() { + frm.set_query('budget_head', function () { return { query: 'beams.beams.doctype.budget_template.budget_template.get_budget_approver_employees', filters: { - 'company': frm.doc.company, - 'department': frm.doc.department + company: frm.doc.company || "", + department: frm.doc.department || "" } }; }); diff --git a/beams/beams/doctype/budget_template/budget_template.py b/beams/beams/doctype/budget_template/budget_template.py index c6ed068f0..7db377bcf 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -2,8 +2,9 @@ # For license information, please see license.txt import frappe -from frappe.model.document import Document from frappe import _ +from frappe.model.document import Document + class BudgetTemplate(Document): @@ -79,24 +80,46 @@ def validate_account_per_cost_center(self): @frappe.whitelist() def get_budget_approver_employees(doctype, txt, searchfield, start, page_len, filters): - """ - Fetch employees with the role of 'Budget Approver' for the current company. - """ - users = frappe.get_all( - "Has Role", - filters={"role": "Budget Approver"}, - pluck="parent" - ) - - if not users: - return [] - - result = frappe.get_all( - "Employee", - filters={ - "user_id": ["in", users] - }, - fields=["name", "employee_name"], - ) - - return [(row.name, row.employee_name) for row in result] + """ + Fetch active employees for a given company & department + whose user has the role 'Budget Approver'. + """ + + if not filters: + return [] + + company = filters.get("company") + department = filters.get("department") + + if not company or not department: + return [] + + # Users with 'Budget Approver' role + budget_approver_users = frappe.get_all( + "Has Role", + filters={"role": "Budget Approver"}, + pluck="parent", + ) + + if not budget_approver_users: + return [] + + employee_filters = { + "user_id": ["in", budget_approver_users], + "company": company, + "department": department, + "status": "Active", + } + + + employees = frappe.get_all( + "Employee", + filters=employee_filters, + fields=["name", "employee_name"], + limit_start=int(start or 0), + limit_page_length=int(page_len or 20), + order_by="employee_name", + ) + + return [(emp.name, emp.employee_name) for emp in employees] + From 7050d5e5bacc914014f1143a827ce280b19c9435 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Sat, 7 Feb 2026 14:12:59 +0530 Subject: [PATCH 42/50] feat: applied filter for region and budget head --- .../budget_template/budget_template.js | 75 ++++++----- .../budget_template/budget_template.py | 124 +++++++++--------- 2 files changed, 105 insertions(+), 94 deletions(-) diff --git a/beams/beams/doctype/budget_template/budget_template.js b/beams/beams/doctype/budget_template/budget_template.js index ba08b932b..5b3cf7411 100644 --- a/beams/beams/doctype/budget_template/budget_template.js +++ b/beams/beams/doctype/budget_template/budget_template.js @@ -60,40 +60,47 @@ frappe.ui.form.on('Budget Template Item', { // Set query filters for link fields function set_filters(frm) { - - frm.set_query('division', function () { - return { - filters: { - department: frm.doc.department, - company: frm.doc.company - } - }; - }); - frm.set_query('department', function () { - return { - filters: { - company: frm.doc.company - } - }; - }); - frm.set_query("cost_center", function () { - return { - filters: { - company: frm.doc.company, - is_group: 0, - disabled: 0 - } - } - }); - frm.set_query('budget_head', function () { - return { - query: 'beams.beams.doctype.budget_template.budget_template.get_budget_approver_employees', - filters: { - company: frm.doc.company || "", - department: frm.doc.department || "" - } - }; - }); + frm.set_query('department', function () { + return { + filters: { + company: frm.doc.company, + is_group: 0, + } + }; + }); + frm.set_query('division', function () { + return { + filters: { + department: frm.doc.department, + company: frm.doc.company + } + }; + }); + frm.set_query('region', function () { + return { + filters: { + company: frm.doc.company + } + }; + }); + frm.set_query("cost_center", function () { + return { + filters: { + company: frm.doc.company, + is_group: 0, + disabled: 0 + } + } + }); + frm.set_query('budget_head', function () { + return { + query: 'beams.beams.doctype.budget_template.budget_template.get_budget_approver_employees', + filters: { + company: frm.doc.company || "", + department: frm.doc.department || "" + } + }; + }); } // Clear budget items table diff --git a/beams/beams/doctype/budget_template/budget_template.py b/beams/beams/doctype/budget_template/budget_template.py index 7db377bcf..9f3386c89 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -5,17 +5,16 @@ from frappe import _ from frappe.model.document import Document - class BudgetTemplate(Document): def validate(self): self.validate_account_per_cost_center() def validate_account_per_cost_center(self): - """ + ''' Validates that there are no duplicate Cost Heads within the same Budget Template and no duplicate Account Heads across different Budget Templates for the same Cost Center. - """ + ''' if not self.cost_center or not self.budget_template_items: return @@ -27,9 +26,9 @@ def validate_account_per_cost_center(self): if row.cost_head: if row.cost_head in seen_cost_heads: frappe.throw( - _("Duplicate Cost Head {0} is not allowed in the same Budget Template.") + _('Duplicate Cost Head {0} is not allowed in the same Budget Template.') .format(row.cost_head), - title=_("Duplicate Cost Head"), + title=_('Duplicate Cost Head'), ) seen_cost_heads.add(row.cost_head) @@ -38,14 +37,14 @@ def validate_account_per_cost_center(self): continue duplicates = frappe.get_all( - "Budget Template Item", + 'Budget Template Item', filters={ - "account_head": row.account_head, - "parenttype": "Budget Template", - "parentfield": "budget_template_items", - "parent": ["!=", self.name], + 'account_head': row.account_head, + 'parenttype': 'Budget Template', + 'parentfield': 'budget_template_items', + 'parent': ['!=', self.name], }, - fields=["parent"], + fields=['parent'], limit=1, ) @@ -55,71 +54,76 @@ def validate_account_per_cost_center(self): template = duplicates[0].parent template_cost_center = frappe.db.get_value( - "Budget Template", template, "cost_center" + 'Budget Template', template, 'cost_center' ) if template_cost_center != self.cost_center: continue template_link = frappe.utils.get_link_to_form( - "Budget Template", template + 'Budget Template', template ) frappe.throw( _( - "Account : {0} is used in the {1} Budget Template " - "with the same Cost Center : {2}." + 'Account : {0} is used in the {1} Budget Template ' + 'with the same Cost Center : {2}.' ).format( row.account_head, template_link, self.cost_center, ), - title=_("Duplicate Account Found"), + title=_('Duplicate Account Found'), ) - @frappe.whitelist() def get_budget_approver_employees(doctype, txt, searchfield, start, page_len, filters): - """ - Fetch active employees for a given company & department - whose user has the role 'Budget Approver'. - """ - - if not filters: - return [] - - company = filters.get("company") - department = filters.get("department") - - if not company or not department: - return [] - - # Users with 'Budget Approver' role - budget_approver_users = frappe.get_all( - "Has Role", - filters={"role": "Budget Approver"}, - pluck="parent", - ) - - if not budget_approver_users: - return [] - - employee_filters = { - "user_id": ["in", budget_approver_users], - "company": company, - "department": department, - "status": "Active", - } - - - employees = frappe.get_all( - "Employee", - filters=employee_filters, - fields=["name", "employee_name"], - limit_start=int(start or 0), - limit_page_length=int(page_len or 20), - order_by="employee_name", - ) - - return [(emp.name, emp.employee_name) for emp in employees] - + ''' + Fetch active employees for a given company & department + whose user has the role `Budget Approver`. + ''' + if not filters: + return [] + + company = filters.get('company') + department = filters.get('department') + + if not company or not department: + return [] + + role_name = 'Budget Approver' + query = ''' + SELECT + emp.name, + emp.employee_name + FROM + `tabEmployee` emp + INNER JOIN `tabUser` u + ON u.name = emp.user_id + INNER JOIN `tabHas Role` hr + ON hr.parent = u.name + WHERE + hr.role = %(role)s + AND emp.status = 'Active' + AND ( + emp.name LIKE %(search_txt)s + OR emp.employee_name LIKE %(search_txt)s + ) + AND emp.company = %(company)s + AND emp.department = %(department)s + LIMIT %(start)s, %(page_len)s + ''' + + # Prepare query parameters + params = { + 'role': role_name, + 'search_txt': f'%{txt}%', + 'start': start, + 'page_len': page_len, + 'company': company, + 'department': department + } + + # Execute query + budget_heads = frappe.db.sql(query, params, as_list=True) + return budget_heads From 9a387cb60997d41aa910dedff853a75064624c79 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Mon, 9 Feb 2026 10:05:19 +0530 Subject: [PATCH 43/50] feat: validate duplicate company and default account in Cost Head --- beams/beams/doctype/cost_head/cost_head.py | 27 ++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/beams/beams/doctype/cost_head/cost_head.py b/beams/beams/doctype/cost_head/cost_head.py index 37794bfab..6aab0b7f4 100644 --- a/beams/beams/doctype/cost_head/cost_head.py +++ b/beams/beams/doctype/cost_head/cost_head.py @@ -1,9 +1,32 @@ # Copyright (c) 2025, efeone and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from frappe import _ class CostHead(Document): - pass + def validate(self): + self.validate_no_duplicate_company_account() + + def validate_no_duplicate_company_account(self): + """No duplicate rows with same Company and Default Account in this Cost Head.""" + if not getattr(self, "accounts", None): + return + + seen = set() + for row in self.accounts: + if not row.company or not row.default_account: + continue + key = (row.company, row.default_account) + if key in seen: + print(seen, "set") + frappe.throw( + _( + "Duplicate Company and Account: {0} and {1} are already used in another row." + ).format(row.company, row.default_account), + title=_("Duplicate Company & Account"), + ) + seen.add(key) + From c22b79f2eb8f0a75263bbeb059c3e98977554b98 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Mon, 9 Feb 2026 10:27:36 +0530 Subject: [PATCH 44/50] feat: prevent duplicate company entries in accounts table --- beams/beams/doctype/cost_head/cost_head.py | 25 +++++++++++----------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/beams/beams/doctype/cost_head/cost_head.py b/beams/beams/doctype/cost_head/cost_head.py index 6aab0b7f4..a92e1f982 100644 --- a/beams/beams/doctype/cost_head/cost_head.py +++ b/beams/beams/doctype/cost_head/cost_head.py @@ -8,25 +8,24 @@ class CostHead(Document): def validate(self): - self.validate_no_duplicate_company_account() + self.validate_duplicate_company() - def validate_no_duplicate_company_account(self): - """No duplicate rows with same Company and Default Account in this Cost Head.""" + def validate_duplicate_company(self): + """Allow only one row per Company in this Cost Head.""" if not getattr(self, "accounts", None): return - seen = set() + seen_companies = set() + for row in self.accounts: - if not row.company or not row.default_account: + if not row.company: continue - key = (row.company, row.default_account) - if key in seen: - print(seen, "set") + + if row.company in seen_companies: frappe.throw( - _( - "Duplicate Company and Account: {0} and {1} are already used in another row." - ).format(row.company, row.default_account), - title=_("Duplicate Company & Account"), + _("Cannot set multiple Item Defaults for a company."), + title=_("Duplicate Company"), ) - seen.add(key) + + seen_companies.add(row.company) From 0c0d9dcac14d37e5017c0fcc6329941ed2679eff Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Mon, 9 Feb 2026 10:43:09 +0530 Subject: [PATCH 45/50] chore: clean up duplicate company validation in Cost Head --- beams/beams/doctype/cost_head/cost_head.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/beams/beams/doctype/cost_head/cost_head.py b/beams/beams/doctype/cost_head/cost_head.py index a92e1f982..e5c280b42 100644 --- a/beams/beams/doctype/cost_head/cost_head.py +++ b/beams/beams/doctype/cost_head/cost_head.py @@ -12,8 +12,6 @@ def validate(self): def validate_duplicate_company(self): """Allow only one row per Company in this Cost Head.""" - if not getattr(self, "accounts", None): - return seen_companies = set() From c65ede405d9cb38c9c4cac48c71c31174a0a7435 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Mon, 9 Feb 2026 12:49:51 +0530 Subject: [PATCH 46/50] feat: create budget workflow via setup --- beams/setup.py | 139 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/beams/setup.py b/beams/setup.py index 75930b7e8..d54c28833 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -116,6 +116,9 @@ def after_install(): #Creating BEAMS specific Email Template create_email_templates(get_email_templates()) + # Create Budget Workflow + setup_budget_workflow() + def after_migrate(): after_install() update_portal_settings() @@ -5889,6 +5892,7 @@ def create_email_templates(email_templates): frappe.get_doc(email_template).insert(ignore_permissions=True) frappe.db.commit() + def get_interview_feedback_custom_fields(): ''' Custom fields that need to be added to the Interview Feedback @@ -6481,3 +6485,138 @@ def update_portal_settings(): if item["title"] not in existing_titles: portal_settings.append("custom_menu", item) portal_settings.save() + + +def setup_budget_workflow(): + """ + Create Budget Workflow + """ + setup_workflow(get_budget_workflow_config()) + + +def setup_workflow(workflow_config): + """ + General workflow setup: ensure all Workflow States, Workflow Action Master + records, and Roles referenced in the workflow exist, then create the workflow + """ + workflow_name = workflow_config.get("workflow_name") + if not workflow_name: + return + + # Create master records and roles if missing (order: states, actions, roles) + state_names = get_states_from_workflow_config(workflow_config) + ensure_workflow_states_exist(state_names) + + action_names = get_actions_from_workflow_config(workflow_config) + ensure_workflow_actions_exist(action_names) + + roles = get_roles_from_workflow_config(workflow_config) + ensure_roles_exist(roles) + + # Create workflow + if frappe.db.exists("Workflow", workflow_name): + return + workflow = frappe.get_doc(workflow_config) + workflow.insert(ignore_permissions=True) + + +def get_states_from_workflow_config(workflow_config): + """Extract unique state names from workflow states and transitions (state, next_state).""" + states = set() + for s in workflow_config.get("states") or []: + if s.get("state"): + states.add(s["state"]) + for t in workflow_config.get("transitions") or []: + if t.get("state"): + states.add(t["state"]) + if t.get("next_state"): + states.add(t["next_state"]) + return list(states) + + +def get_actions_from_workflow_config(workflow_config): + """Extract unique action names from workflow transitions.""" + actions = set() + for t in workflow_config.get("transitions") or []: + if t.get("action"): + actions.add(t["action"]) + return list(actions) + + +def get_roles_from_workflow_config(workflow_config): + """Extract unique role names from workflow states .""" + roles = set() + for state in workflow_config.get("states") or []: + if state.get("allow_edit"): + roles.add(state["allow_edit"]) + for transition in workflow_config.get("transitions") or []: + if transition.get("allowed"): + roles.add(transition["allowed"]) + return list(roles) + + +def ensure_workflow_states_exist(state_names): + """Create any Workflow State that does not exist.""" + for name in state_names: + if not name or frappe.db.exists("Workflow State", name): + continue + frappe.get_doc({ + "doctype": "Workflow State", + "workflow_state_name": name, + }).insert(ignore_permissions=True) + + +def ensure_workflow_actions_exist(action_names): + """Create any Workflow Action Master that does not exist.""" + for name in action_names: + if not name or frappe.db.exists("Workflow Action Master", name): + continue + frappe.get_doc({ + "doctype": "Workflow Action Master", + "workflow_action_name": name, + }).insert(ignore_permissions=True) + + +def ensure_roles_exist(role_names): + """Create any Role that does not exist.""" + for role_name in role_names: + if not role_name or frappe.db.exists("Role", role_name): + continue + frappe.get_doc({ + "doctype": "Role", + "role_name": role_name, + }).insert(ignore_permissions=True) + + +def get_budget_workflow_config(): + """Return the Budget Workflow configuration (states and transitions).""" + return { + "doctype": "Workflow", + "workflow_name": "Budget Workflow", + "document_type": "Budget", + "is_active": 1, + "override_status": 0, + "send_email_alert": 0, + "workflow_state_field": "workflow_state", + "states": [ + {"state": "Draft", "doc_status": "0", "allow_edit": "Budget User", "idx": 1}, + {"state": "Pending Department Verification", "doc_status": "0", "allow_edit": "Budget Approver", "idx": 2}, + {"state": "Pending Accounts Approval", "doc_status": "0", "allow_edit": "Budget Manager", "idx": 3}, + {"state": "Pending Finance Approval", "doc_status": "0", "allow_edit": "Finance Manager", "idx": 4}, + {"state": "Approved by Finance", "doc_status": "1", "allow_edit": "CEO", "idx": 5}, + {"state": "Approved", "doc_status": "1", "allow_edit": "CEO", "idx": 6}, + {"state": "Rejected", "doc_status": "2", "allow_edit": "CEO", "idx": 7}, + ], + "transitions": [ + {"state": "Draft", "action": "Request for Review", "next_state": "Pending Department Verification", "allowed": "Budget User", "idx": 1}, + {"state": "Pending Department Verification", "action": "Forward to Accounts", "next_state": "Pending Accounts Approval", "allowed": "Budget Approver", "idx": 2}, + {"state": "Pending Department Verification", "action": "Send for Revision", "next_state": "Draft", "allowed": "Budget Approver", "idx": 3}, + {"state": "Pending Accounts Approval", "action": "Forward to FM", "next_state": "Pending Finance Approval", "allowed": "Budget Manager", "idx": 4}, + {"state": "Pending Accounts Approval", "action": "Send for Revision", "next_state": "Pending Department Verification", "allowed": "Budget Manager", "idx": 5}, + {"state": "Pending Finance Approval", "action": "Approve", "next_state": "Approved by Finance", "allowed": "Finance Manager", "idx": 6}, + {"state": "Pending Finance Approval", "action": "Send for Revision", "next_state": "Pending Accounts Approval", "allowed": "Finance Manager", "idx": 7}, + {"state": "Approved by Finance", "action": "Approve", "next_state": "Approved", "allowed": "CEO", "idx": 8}, + {"state": "Approved by Finance", "action": "Reject", "next_state": "Rejected", "allowed": "CEO", "idx": 9}, + ], + } + From 3bcd0325ba52ae2caf9fb50992ba711d9fb65253 Mon Sep 17 00:00:00 2001 From: Jumana-K Date: Tue, 17 Feb 2026 16:44:29 +0530 Subject: [PATCH 47/50] feat:enhancement in hrms --- .../appointment_letter/appointment_letter.js | 11 ++ .../beams/custom_scripts/employee/employee.py | 1 + .../job_applicant/job_applicant.js | 3 +- .../custom_scripts/job_offer/job_offer.js | 149 ++++++++++++++---- .../custom_scripts/job_offer/job_offer.py | 46 +++++- .../compensation_proposal.py | 2 +- .../job_offer_salary_detail.json | 50 ++++++ .../job_offer_salary_detail.py | 8 + beams/hooks.py | 5 +- beams/setup.py | 91 ++++++++++- 10 files changed, 316 insertions(+), 50 deletions(-) create mode 100644 beams/beams/custom_scripts/appointment_letter/appointment_letter.js create mode 100644 beams/beams/doctype/job_offer_salary_detail/job_offer_salary_detail.json create mode 100644 beams/beams/doctype/job_offer_salary_detail/job_offer_salary_detail.py diff --git a/beams/beams/custom_scripts/appointment_letter/appointment_letter.js b/beams/beams/custom_scripts/appointment_letter/appointment_letter.js new file mode 100644 index 000000000..c575b44d3 --- /dev/null +++ b/beams/beams/custom_scripts/appointment_letter/appointment_letter.js @@ -0,0 +1,11 @@ +frappe.ui.form.on("Appointment Letter", { + job_applicant: function(frm) { + if (frm.doc.job_applicant) { + frappe.db.get_value('Job Applicant', frm.doc.job_applicant, 'salutation', (r) => { + if (r && r.salutation) { + frm.set_value('salutation', r.salutation); + } + }); + } + } +}); diff --git a/beams/beams/custom_scripts/employee/employee.py b/beams/beams/custom_scripts/employee/employee.py index 5560c908d..76193efaf 100644 --- a/beams/beams/custom_scripts/employee/employee.py +++ b/beams/beams/custom_scripts/employee/employee.py @@ -582,6 +582,7 @@ def populate_employee_details_from_applicant(doc, method): doc.permanent_address = "\n ".join([part for part in permanent_address_parts if part]) doc.permanent_pin_code = job_applicant.ppin_code doc.aadhar_id = job_applicant.aadhar_number + doc.salutation = job_applicant.salutation doc.education_qualification = [] for row in job_applicant.education_qualification: doc.append("education_qualification", { diff --git a/beams/beams/custom_scripts/job_applicant/job_applicant.js b/beams/beams/custom_scripts/job_applicant/job_applicant.js index f69f13524..210aced7a 100644 --- a/beams/beams/custom_scripts/job_applicant/job_applicant.js +++ b/beams/beams/custom_scripts/job_applicant/job_applicant.js @@ -75,7 +75,8 @@ function handle_custom_buttons(frm) { if (!result || !result.name) { frm.add_custom_button(__('Appointment Letter'), function () { frappe.new_doc('Appointment Letter', { - job_applicant: frm.doc.name + job_applicant: frm.doc.name, + salutation: frm.doc.salutation }); }, __('Create')); } diff --git a/beams/beams/custom_scripts/job_offer/job_offer.js b/beams/beams/custom_scripts/job_offer/job_offer.js index 25988cd64..4aacbd27c 100644 --- a/beams/beams/custom_scripts/job_offer/job_offer.js +++ b/beams/beams/custom_scripts/job_offer/job_offer.js @@ -1,40 +1,119 @@ frappe.ui.form.on("Job Offer", { - refresh: function (frm) { - if ( - !frm.doc.__islocal && - frm.doc.status == "Accepted" && - frm.doc.docstatus === 1 && - (!frm.doc.__onload || !frm.doc.__onload.employee) - ) { - frm.remove_custom_button(__("Create Employee")); - } - - setTimeout(1000); - - if ( - !frm.doc.__islocal && - frm.doc.status == "Accepted" && - frm.doc.docstatus === 1 && - (!frm.doc.__onload || !frm.doc.__onload.employee) - ) { - frm.add_custom_button(__("Create Employee"), function () { - make_employee(frm); - }); - } - }, - validate: function(frm) { - if (frm.doc.ctc){ - if (frm.doc.ctc < 0) { - frappe.msgprint(__('CTC cannot be a Negative Value')); - frappe.validated = false - } - } -} + job_applicant: function(frm) { + if (frm.doc.job_applicant) { + frappe.db.get_value('Job Applicant', frm.doc.job_applicant, 'salutation', (r) => { + if (r && r.salutation) { + frm.set_value('salutation', r.salutation); + } + }); + } + }, + refresh: function (frm) { + if ( + !frm.doc.__islocal && + frm.doc.status == "Accepted" && + frm.doc.docstatus === 1 && + (!frm.doc.__onload || !frm.doc.__onload.employee) + ) { + frm.remove_custom_button(__("Create Employee")); + frm.add_custom_button(__("Create Employee"), function () { + make_employee(frm); + }); + } + + // Ensure CTC is editable even if it has fetch_from property + if (frm.doc.docstatus === 0) { + frm.set_df_property('ctc', 'read_only', 0); + } + + check_ctc_mismatch(frm); + }, + validate: function(frm) { + if (frm.doc.ctc) { + if (frm.doc.ctc < 0) { + frappe.msgprint(__('CTC cannot be a Negative Value')); + frappe.validated = false; + } + } + // Ensure totals are calculated before sending to server + calculate_all_totals(frm); + }, + ctc: function(frm) { + check_ctc_mismatch(frm); + }, + salary_details_add: function(frm) { + calculate_all_totals(frm); + }, + salary_details_remove: function(frm) { + calculate_all_totals(frm); + }, + other_contribution_details_add: function(frm) { + calculate_all_totals(frm); + }, + other_contribution_details_remove: function(frm) { + calculate_all_totals(frm); + } +}); + +frappe.ui.form.on("Job Offer Salary Detail", { + amount: function(frm, cdt, cdn) { + calculate_all_totals(frm); + } }); +function calculate_all_totals(frm) { + let gross = 0; + (frm.doc.salary_details || []).forEach(d => { + gross += flt(d.amount); + }); + if (flt(frm.doc.gross_monthly_salary) !== gross) { + frm.doc.gross_monthly_salary = gross; + frm.refresh_field('gross_monthly_salary'); + } + + let other = 0; + (frm.doc.other_contribution_details || []).forEach(d => { + other += flt(d.amount); + }); + + let total_ctc = gross + other; + if (flt(frm.doc.total_ctc_per_month) !== total_ctc) { + frm.doc.total_ctc_per_month = total_ctc; + frm.refresh_field('total_ctc_per_month'); + } + + if (!frm.doc.ctc || flt(frm.doc.ctc) === 0) { + frm.doc.ctc = total_ctc; + frm.refresh_field('ctc'); + } + + check_ctc_mismatch(frm); +} + +function check_ctc_mismatch(frm) { + if (!frm || !frm.dashboard) return; + + let total = flt(frm.doc.total_ctc_per_month); + let ctc = flt(frm.doc.ctc); + + if (ctc > 0 && Math.abs(ctc - total) > 0.01) { + // Use doc currency, or fallback to system default if not available + let currency = frm.doc.currency || (frappe.boot && frappe.boot.sysdefaults && frappe.boot.sysdefaults.currency); + let formatted_total = format_currency(total, currency); + let formatted_ctc = format_currency(ctc, currency); + + frm.dashboard.set_headline( + __('Total CTC per month ({0}) does not match CTC ({1})', [formatted_total, formatted_ctc]), + 'orange' + ); + } else { + frm.dashboard.clear_headline(); + } +} + function make_employee(frm) { - frappe.model.open_mapped_doc({ - method: "beams.beams.custom_scripts.job_offer.job_offer.make_employee", - frm: frm, - }); + frappe.model.open_mapped_doc({ + method: "beams.beams.custom_scripts.job_offer.job_offer.make_employee", + frm: frm, + }); } diff --git a/beams/beams/custom_scripts/job_offer/job_offer.py b/beams/beams/custom_scripts/job_offer/job_offer.py index 6efc6a951..758465e0f 100644 --- a/beams/beams/custom_scripts/job_offer/job_offer.py +++ b/beams/beams/custom_scripts/job_offer/job_offer.py @@ -1,5 +1,6 @@ import frappe import json +from frappe import _ from frappe.model.mapper import get_mapped_doc @frappe.whitelist() @@ -35,6 +36,7 @@ def set_missing_values(source, target): "field_map": { "applicant_name": "employee_name", "offer_date": "scheduled_confirmation_date", + "salutation": "salutation", }, } }, @@ -56,6 +58,7 @@ def set_missing_values(source, target): if job_offer.job_applicant: applicant_data = frappe.get_doc("Job Applicant", job_offer.job_applicant) mapping = { + "salutation": applicant_data.get("salutation"), "gender": applicant_data.get("gender"), "date_of_birth": applicant_data.get("date_of_birth"), "cell_number": applicant_data.get("phone_number"), @@ -77,11 +80,38 @@ def set_missing_values(source, target): frappe.throw(f"An error occurred while creating employee: {str(e)}") -@frappe.whitelist() -def validate_ctc(doc,method): - """ - Validate that the CTC value is not negative. - """ - if doc.ctc: - if doc.ctc < 0: - frappe.throw("CTC cannot be a Negative Value") \ No newline at end of file +def validate_ctc(doc, method=None): + """ + Validate that the CTC value is not negative. + Calculate totals for salary and other contribution details. + Ensure CTC matches Total CTC per month. + """ + if not doc.salutation and doc.job_applicant: + doc.salutation = frappe.db.get_value("Job Applicant", doc.job_applicant, "salutation") + + if doc.ctc and frappe.utils.flt(doc.ctc) < 0: + frappe.throw("CTC cannot be a Negative Value") + + doc.gross_monthly_salary = sum(frappe.utils.flt(d.amount) for d in doc.get("salary_details") or []) + other_contribution = sum(frappe.utils.flt(d.amount) for d in doc.get("other_contribution_details") or []) + doc.total_ctc_per_month = doc.gross_monthly_salary + other_contribution + + if not doc.ctc or frappe.utils.flt(doc.ctc) == 0: + doc.ctc = doc.total_ctc_per_month + + # Validation logic strictly enforce only on submission. + if doc.docstatus == 1: + if not doc.get("salary_details") and not doc.get("other_contribution_details"): + frappe.throw(_("Please add Salary Details or Other Contribution Details before submitting.")) + + ctc_diff = abs(frappe.utils.flt(doc.ctc) - frappe.utils.flt(doc.total_ctc_per_month)) + if ctc_diff > 0.01: + currency = getattr(doc, "currency", None) + if not currency and doc.company: + currency = frappe.get_cached_value('Company', doc.company, 'default_currency') + + msg = _("Total CTC per month ({0}) does not match CTC ({1})").format( + frappe.utils.fmt_money(doc.total_ctc_per_month, currency=currency), + frappe.utils.fmt_money(doc.ctc, currency=currency) + ) + frappe.throw(msg) diff --git a/beams/beams/doctype/compensation_proposal/compensation_proposal.py b/beams/beams/doctype/compensation_proposal/compensation_proposal.py index 9ffc93b48..6544415d3 100644 --- a/beams/beams/doctype/compensation_proposal/compensation_proposal.py +++ b/beams/beams/doctype/compensation_proposal/compensation_proposal.py @@ -24,6 +24,7 @@ def create_offer_from_compensation_proposal(self): if self.workflow_state == "Applicant Accepted": job_offer = frappe.new_doc('Job Offer') job_offer.job_applicant = self.job_applicant + job_offer.salutation = frappe.db.get_value("Job Applicant", self.job_applicant, "salutation") job_offer.designation = self.designation job_offer.offer_date = getdate(today()) job_offer.compensation_proposal = self.name @@ -47,7 +48,6 @@ def create_offer_from_compensation_proposal(self): job_offer.flags.ignore_mandatory = True job_offer.flags.ignore_validate = True job_offer.insert() - job_offer.submit() frappe.db.set_value("Compensation Proposal", self.name, "job_offer", job_offer.name) frappe.msgprint( 'Job Offer Created: {1}'.format( diff --git a/beams/beams/doctype/job_offer_salary_detail/job_offer_salary_detail.json b/beams/beams/doctype/job_offer_salary_detail/job_offer_salary_detail.json new file mode 100644 index 000000000..7253c06b8 --- /dev/null +++ b/beams/beams/doctype/job_offer_salary_detail/job_offer_salary_detail.json @@ -0,0 +1,50 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-05-22 12:00:00.000000", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "component", + "amount", + "description" + ], + "fields": [ + { + "fieldname": "component", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Component", + "options": "Salary Component", + "reqd": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "reqd": 1 + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-05-22 12:00:00.000000", + "modified_by": "Administrator", + "module": "BEAMS", + "name": "Job Offer Salary Detail", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] + } + \ No newline at end of file diff --git a/beams/beams/doctype/job_offer_salary_detail/job_offer_salary_detail.py b/beams/beams/doctype/job_offer_salary_detail/job_offer_salary_detail.py new file mode 100644 index 000000000..f608f6811 --- /dev/null +++ b/beams/beams/doctype/job_offer_salary_detail/job_offer_salary_detail.py @@ -0,0 +1,8 @@ +# Copyright (c) 2024, efeone and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class JobOfferSalaryDetail(Document): + pass diff --git a/beams/hooks.py b/beams/hooks.py index 0d7ff199e..38954ac72 100644 --- a/beams/hooks.py +++ b/beams/hooks.py @@ -56,6 +56,7 @@ "Employee Onboarding":"beams/custom_scripts/employee_onboarding/employee_onboarding.js", "Leave Application":"beams/custom_scripts/leave_application/leave_application.js", "Job Offer": "beams/custom_scripts/job_offer/job_offer.js", + "Appointment Letter": "beams/custom_scripts/appointment_letter/appointment_letter.js", "Appraisal":"beams/custom_scripts/appraisal/appraisal.js", "Project":"beams/custom_scripts/project/project.js", "Asset Movement":"beams/custom_scripts/asset_movement/asset_movement.js", @@ -324,7 +325,9 @@ }, "Job Offer" : { "on_submit":"beams.beams.custom_scripts.job_offer.job_offer.make_employee", - "validate":"beams.beams.custom_scripts.job_offer.job_offer.validate_ctc" + "validate":"beams.beams.custom_scripts.job_offer.job_offer.validate_ctc", + "before_update_after_submit":"beams.beams.custom_scripts.job_offer.job_offer.validate_ctc", + "on_update_after_submit":"beams.beams.custom_scripts.job_offer.job_offer.validate_ctc" }, "Employee Separation": { "on_update": "beams.beams.custom_scripts.employee_separation.employee_separation.create_exit_clearance" diff --git a/beams/setup.py b/beams/setup.py index 75930b7e8..4393d5b5a 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -120,6 +120,15 @@ def after_migrate(): after_install() update_portal_settings() +def update_salary_detail_fields(): + # Ensure all fields in Job Offer Salary Detail allow on submit + frappe.db.sql(""" + UPDATE `tabDocField` + SET allow_on_submit = 1 + WHERE parent = 'Job Offer Salary Detail' + """) + frappe.clear_cache(doctype='Job Offer Salary Detail') + def before_uninstall(): delete_custom_fields(get_customer_custom_fields()) @@ -1021,9 +1030,56 @@ def get_job_offer_custom_fields(): "fieldname": "ctc", "fieldtype": "Currency", "label": "CTC", - "insert_after": "Compensation Proposal", - "fetch_from" : "Compensation Proposal.proposed_ctc" - } + "insert_after": "compensation_proposal", + "fetch_from" : "compensation_proposal.proposed_ctc" + }, + { + "fieldname": "salutation", + "fieldtype": "Link", + "label": "Salutation", + "options":"Salutation", + "insert_after": "job_applicant" + }, + { + "fieldname": "salary_details_section", + "fieldtype": "Section Break", + "label": "Salary Details", + "insert_after": "company" + }, + { + "fieldname": "salary_details", + "fieldtype": "Table", + "label": "Salary Details", + "options": "Job Offer Salary Detail", + "insert_after": "salary_details_section" + }, + { + "fieldname": "gross_monthly_salary", + "fieldtype": "Currency", + "label": "Gross Monthly Salary", + "read_only": 1, + "insert_after": "salary_details" + }, + { + "fieldname": "other_contribution_details_section", + "fieldtype": "Section Break", + "label": "Other Contribution Details", + "insert_after": "gross_monthly_salary" + }, + { + "fieldname": "other_contribution_details", + "fieldtype": "Table", + "label": "Other Contribution Details", + "options": "Job Offer Salary Detail", + "insert_after": "other_contribution_details_section" + }, + { + "fieldname": "total_ctc_per_month", + "fieldtype": "Currency", + "label": "Total CTC per month", + "read_only": 1, + "insert_after": "other_contribution_details" + }, ] } @@ -2250,6 +2306,19 @@ def get_employee_custom_fields(): "label": "Current Address", "insert_after": "address_section" }, + { + "fieldname": "division", + "fieldtype": "Link", + "options": "Division", + "label": "Division", + "insert_after": "department" + }, + { + "fieldname": "employee_location", + "fieldtype": "Data", + "label": "Employee Location", + "insert_after": "designation" + }, ], "Employee External Work History":[ { @@ -2687,6 +2756,13 @@ def get_job_applicant_custom_fields(): ''' return { "Job Applicant": [ + { + "fieldname": "salutation", + "fieldtype": "Link", + "label": "Salutation", + "options": "Salutation", + "insert_after": "applicant_name" + }, { "fieldname": "date_of_birth", "fieldtype": "Date", @@ -6043,8 +6119,15 @@ def get_appointment_letter_custom_fields(): "fieldname": "notice_period", "fieldtype": "Int", "label": "Notice Period (In Days)", + "insert_after": "salutation" + }, + { + "fieldname": "salutation", + "fieldtype": "Link", + "label": "Salutation", + "options": "Salutation", "insert_after": "applicant_name" - } + }, ] } From 79c03982001beef5e485509fe4250d3a7c3c2045 Mon Sep 17 00:00:00 2001 From: Jumana-K Date: Wed, 18 Feb 2026 10:40:38 +0530 Subject: [PATCH 48/50] feat:enhancement in shift assignment --- .../shift_assignment/shift_assignment.js | 7 ++++++ .../shift_assignment/shift_assignment.py | 20 ++++++++++++++++ .../shift_assignment_tool.js | 23 +++++++++++++++++++ beams/hooks.py | 5 ++++ beams/setup.py | 2 +- 5 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 beams/beams/custom_scripts/shift_assignment/shift_assignment.js create mode 100644 beams/beams/custom_scripts/shift_assignment/shift_assignment.py create mode 100644 beams/beams/custom_scripts/shift_assignment_tool/shift_assignment_tool.js diff --git a/beams/beams/custom_scripts/shift_assignment/shift_assignment.js b/beams/beams/custom_scripts/shift_assignment/shift_assignment.js new file mode 100644 index 000000000..9bc56fdd9 --- /dev/null +++ b/beams/beams/custom_scripts/shift_assignment/shift_assignment.js @@ -0,0 +1,7 @@ +frappe.ui.form.on('Shift Assignment', { + start_date: function(frm) { + if (frm.doc.start_date) { + frm.set_value('end_date', frm.doc.start_date); + } + } +}); diff --git a/beams/beams/custom_scripts/shift_assignment/shift_assignment.py b/beams/beams/custom_scripts/shift_assignment/shift_assignment.py new file mode 100644 index 000000000..963699f78 --- /dev/null +++ b/beams/beams/custom_scripts/shift_assignment/shift_assignment.py @@ -0,0 +1,20 @@ +import frappe + +def validate(doc, method=None): + """ + Check if the employee has another shift assignment on the same day. + If yes, set roster_type to 'Double Shift'. + """ + if not doc.employee or not doc.start_date: + return + + # Check for existing assignments on the same day for the same employee + existing_assignments = frappe.get_all("Shift Assignment", filters={ + "employee": doc.employee, + "start_date": doc.start_date, + "name": ["!=", doc.name], + "docstatus": ["!=", 2] + }) + + if existing_assignments: + doc.roster_type = "Double Shift" diff --git a/beams/beams/custom_scripts/shift_assignment_tool/shift_assignment_tool.js b/beams/beams/custom_scripts/shift_assignment_tool/shift_assignment_tool.js new file mode 100644 index 000000000..ee7bbcdda --- /dev/null +++ b/beams/beams/custom_scripts/shift_assignment_tool/shift_assignment_tool.js @@ -0,0 +1,23 @@ +frappe.ui.form.on('Shift Assignment Tool', { + onload: function(frm) { + if (!frm.doc.department || !frm.doc.company) { + frappe.db.get_value('Employee', {'user_id': frappe.session.user}, ['department', 'company']) + .then(r => { + let values = r.message; + if (values) { + if (!frm.doc.department && values.department) { + frm.set_value('department', values.department); + } + if (!frm.doc.company && values.company) { + frm.set_value('company', values.company); + } + } + }); + } + }, + start_date: function(frm) { + if (frm.doc.start_date) { + frm.set_value('end_date', frm.doc.start_date); + } + } +}); diff --git a/beams/hooks.py b/beams/hooks.py index 38954ac72..1b41e3a29 100644 --- a/beams/hooks.py +++ b/beams/hooks.py @@ -79,6 +79,8 @@ "Item":"beams/custom_scripts/item/item.js", "Journal Entry":"beams/custom_scripts/journal_entry/journal_entry.js", "Expense Claim":"beams/custom_scripts/expense_claim/expense_claim.js", + "Shift Assignment":"beams/custom_scripts/shift_assignment/shift_assignment.js", + "Shift Assignment Tool":"beams/custom_scripts/shift_assignment_tool/shift_assignment_tool.js", } doctype_list_js = { @@ -439,6 +441,9 @@ "Supplier Quotation": { "validate":"beams.beams.custom_scripts.supplier_quotation.supplier_quotation.clear_rate_if_no_rate_provided" }, + "Shift Assignment": { + "validate": "beams.beams.custom_scripts.shift_assignment.shift_assignment.validate" + }, } # Scheduled Tasks diff --git a/beams/setup.py b/beams/setup.py index 4393d5b5a..55885976c 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -370,7 +370,7 @@ def get_shift_assignment_custom_fields(): "fieldname": "roster_type", "fieldtype": "Select", "label": "Roster Type", - "options":"\nRegular\nDouble Shift", + "options":"Regular\nDouble Shift", "insert_after": "shift_type" }, { From 4e230b3c665918daf6ff0b67ce78bf6015cb7d65 Mon Sep 17 00:00:00 2001 From: Jumana-K Date: Fri, 20 Feb 2026 09:56:35 +0530 Subject: [PATCH 49/50] fix:start date not able to change manually --- .../payroll_entry/payroll_entry.js | 39 +++++++++++-------- .../payroll_entry/payroll_entry.py | 25 +++++------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/beams/beams/custom_scripts/payroll_entry/payroll_entry.js b/beams/beams/custom_scripts/payroll_entry/payroll_entry.js index 4df84ce3d..b325dddbb 100644 --- a/beams/beams/custom_scripts/payroll_entry/payroll_entry.js +++ b/beams/beams/custom_scripts/payroll_entry/payroll_entry.js @@ -1,16 +1,17 @@ frappe.ui.form.on('Payroll Entry', { refresh: function(frm) { - set_previous_month_dates(frm); + if (frm.is_new() && frm.doc.posting_date && frm.doc.payroll_frequency === 'Monthly' && !frm.__dates_set_by_beams) { + set_previous_month_dates(frm); + frm.__dates_set_by_beams = true; + } }, - + posting_date: function(frm) { set_previous_month_dates(frm); }, - - onload: function(frm) { - setTimeout(() => { - set_previous_month_dates(frm); - }, 500); + + payroll_frequency: function(frm) { + set_previous_month_dates(frm); } }); @@ -19,29 +20,35 @@ frappe.ui.form.on('Payroll Entry', { * relative to the selected `posting_date`. */ function set_previous_month_dates(frm) { - if (!frm.doc.posting_date) { + if (!frm.doc.posting_date || frm.doc.payroll_frequency !== 'Monthly') { return; } - + let posting_date = frappe.datetime.str_to_obj(frm.doc.posting_date); + if (!posting_date) { + return; + } + let prev_year = posting_date.getFullYear(); let prev_month = posting_date.getMonth() - 1; - + if (prev_month < 0) { prev_month = 11; prev_year = prev_year - 1; } - + let start_date = new Date(prev_year, prev_month, 1); let end_date = new Date(prev_year, prev_month + 1, 0); let start_date_formatted = frappe.datetime.obj_to_str(start_date); let end_date_formatted = frappe.datetime.obj_to_str(end_date); - + if (frm.doc.start_date !== start_date_formatted) { - frm.set_value('start_date', start_date_formatted); + frm.doc.start_date = start_date_formatted; + frm.refresh_field('start_date'); } - + if (frm.doc.end_date !== end_date_formatted) { - frm.set_value('end_date', end_date_formatted); + frm.doc.end_date = end_date_formatted; + frm.refresh_field('end_date'); } -} \ No newline at end of file +} diff --git a/beams/beams/custom_scripts/payroll_entry/payroll_entry.py b/beams/beams/custom_scripts/payroll_entry/payroll_entry.py index eed5e30e8..9f81d8ed6 100644 --- a/beams/beams/custom_scripts/payroll_entry/payroll_entry.py +++ b/beams/beams/custom_scripts/payroll_entry/payroll_entry.py @@ -2,19 +2,12 @@ from frappe.utils import add_months, get_first_day, get_last_day, getdate def set_previous_month_dates(doc, method=None): - """Auto-set start_date and end_date to PREVIOUS month based on posting_date""" - - if not doc.posting_date: - return - - # Convert posting_date to date object - posting_date = getdate(doc.posting_date) - - # Get PREVIOUS month's first and last day - prev_month_date = add_months(posting_date, -1) - start_date = get_first_day(prev_month_date) - end_date = get_last_day(prev_month_date) - - # Set the dates to previous month - doc.start_date = start_date - doc.end_date = end_date + """Auto-set start_date and end_date to PREVIOUS month based on posting_date""" + if doc.posting_date and doc.payroll_frequency == 'Monthly': + if not doc.start_date or not doc.end_date: + # Get the first day of the previous month + posting_date = getdate(doc.posting_date) + prev_month_date = add_months(posting_date, -1) + + doc.start_date = get_first_day(prev_month_date) + doc.end_date = get_last_day(prev_month_date) From d504ac019c0d886d0915bb052f119149354bf6e4 Mon Sep 17 00:00:00 2001 From: ajmalroshan123 Date: Fri, 20 Feb 2026 11:28:09 +0530 Subject: [PATCH 50/50] feat: fetch values from budget template and change field_order --- beams/setup.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/beams/setup.py b/beams/setup.py index 3f566c03c..7102fbd94 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -5702,7 +5702,35 @@ def get_property_setters(): "doctype_or_field": "DocType", "doc_type": "Budget", "property": "field_order", - "value": "[\"workflow_state\", \"naming_series\", \"budget_against\", \"budget_for\", \"project\", \"cost_center\", \"cost_head\", \"fiscal_year\", \"budget_head\", \"budget_head_user\", \"total_amount\", \"column_break_3\", \"company\", \"department\", \"division\", \"budget_template\", \"region\", \"monthly_distribution\", \"amended_from\", \"section_break_6\", \"applicable_on_material_request\", \"action_if_annual_budget_exceeded_on_mr\", \"action_if_accumulated_monthly_budget_exceeded_on_mr\", \"column_break_13\", \"applicable_on_purchase_order\", \"action_if_annual_budget_exceeded_on_po\", \"action_if_accumulated_monthly_budget_exceeded_on_po\", \"section_break_16\", \"applicable_on_booking_actual_expenses\", \"action_if_annual_budget_exceeded\", \"action_if_accumulated_monthly_budget_exceeded\", \"section_break_21\", \"accounts\", \"budget_accounts\", \"default_currency\", \"company_currency\", \"rejection_feedback\"]" + "value": "[\"workflow_state\", \"naming_series\", \"budget_against\", \"budget_for\", \"project\", \"budget_template\", \"cost_center\", \"cost_head\", \"fiscal_year\", \"budget_head\", \"budget_head_user\", \"total_amount\", \"column_break_3\", \"company\", \"department\", \"division\", \"region\", \"monthly_distribution\", \"amended_from\", \"section_break_6\", \"applicable_on_material_request\", \"action_if_annual_budget_exceeded_on_mr\", \"action_if_accumulated_monthly_budget_exceeded_on_mr\", \"column_break_13\", \"applicable_on_purchase_order\", \"action_if_annual_budget_exceeded_on_po\", \"action_if_accumulated_monthly_budget_exceeded_on_po\", \"section_break_16\", \"applicable_on_booking_actual_expenses\", \"action_if_annual_budget_exceeded\", \"action_if_accumulated_monthly_budget_exceeded\", \"section_break_21\", \"accounts\", \"budget_accounts\", \"default_currency\", \"company_currency\", \"rejection_feedback\"]" + }, + { + "doctype_or_field": "DocField", + "doc_type": "Budget", + "field_name": "department", + "property": "fetch_from", + "value": "budget_template.department" + }, + { + "doctype_or_field": "DocField", + "doc_type": "Budget", + "field_name": "division", + "property": "fetch_from", + "value": "budget_template.division" + }, + { + "doctype_or_field": "DocField", + "doc_type": "Budget", + "field_name": "cost_center", + "property": "fetch_from", + "value": "budget_template.cost_center" + }, + { + "doctype_or_field": "DocField", + "doc_type": "Budget", + "field_name": "region", + "property": "fetch_from", + "value": "budget_template.region" }, ]