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/budget/budget.js b/beams/beams/custom_scripts/budget/budget.js index 681f7f7d4..46785c3f8 100644 --- a/beams/beams/custom_scripts/budget/budget.js +++ b/beams/beams/custom_scripts/budget/budget.js @@ -1,94 +1,119 @@ 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'); }); } - 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; - } - - if (frm.doc.budget_template === frm._previous_budget_template) { - return; - } - let previous_template = frm.doc.__last_value || frm._previous_budget_template; + budget_template: function (frm) { + // If cleared + if (!frm.doc.budget_template) { + frm.set_value('cost_center', null); + frm.set_value('region', null); - 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('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; - 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; + 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_custom'); + + 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); + 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 () { @@ -103,7 +128,8 @@ function set_filters(frm) { return { filters: { division: frm.doc.division, - company: frm.doc.company + company: frm.doc.company, + cost_center: frm.doc.cost_center } }; }); @@ -123,148 +149,165 @@ function set_filters(frm) { }); } -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]; - - 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); - } +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); + } }); 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); - - frappe.model.set_value(cdt, cdn, 'budget_amount', total); - frm.refresh_field('budget_account'); + 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'); } function distribute_budget_equally(frm, cdt, cdn, budget_amount) { - 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; - - // 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'); + 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; + + // 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'); } 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..11d8e83fa 100644 --- a/beams/beams/custom_scripts/budget/budget.py +++ b/beams/beams/custom_scripts/budget/budget.py @@ -1,55 +1,89 @@ 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 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]) - 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) + ''' + 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 + } + # 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) + +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/custom_scripts/employee/employee.py b/beams/beams/custom_scripts/employee/employee.py index 7a3821367..76193efaf 100644 --- a/beams/beams/custom_scripts/employee/employee.py +++ b/beams/beams/custom_scripts/employee/employee.py @@ -122,16 +122,16 @@ 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 - + p = f"MB/{department_abbr}/" + last = frappe.db.get_all( + "Employee", + filters={"name": ["like", f"{p}%"]}, + order_by="creation desc", + pluck="name", + limit=1 + ) + 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.""" @@ -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/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/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/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 7d71d3e99..73d1c5942 100644 --- a/beams/beams/custom_scripts/material_request/material_request.js +++ b/beams/beams/custom_scripts/material_request/material_request.js @@ -12,17 +12,47 @@ 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. +* 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.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/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) 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/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/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/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/batta_claim/batta_claim.js b/beams/beams/doctype/batta_claim/batta_claim.js index 036758956..a455f03c6 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,7 +69,7 @@ frappe.ui.form.on('Batta Claim', { }) }, refresh: function(frm) { - clear_checkbox_exceed(frm); + toggle_room_rent_batta_field(frm); frappe.call({ method: "beams.beams.doctype.batta_claim.batta_claim.get_batta_policy_values", callback: function(response) { @@ -214,6 +215,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. */ @@ -436,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 60d896d8b..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", @@ -33,6 +33,7 @@ "attach", "trip_sheet", "travel_request", + "hod_email", "section_break_bkxb", "room_rent_batta", "batta_based_on", @@ -298,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", @@ -314,36 +317,40 @@ }, { "default": "0", - "depends_on": "eval:doc.is_budgeted == 1", - "fieldname": "is_budget_exceed", + "fieldname": "from_bureau", "fieldtype": "Check", - "label": " Is Budget Exceed" + "hidden": 1, + "label": "From Bureau" + }, + { + "fieldname": "hod_email", + "fieldtype": "Data", + "hidden": 1, + "label": "HOD Email", + "options": "Email", + "read_only": 1 }, { "default": "0", - "fieldname": "from_bureau", + "depends_on": "eval:doc.is_budgeted == 1", + "fieldname": "is_budget_exceeded", "fieldtype": "Check", - "hidden": 1, - "label": "From Bureau" + "label": " Is Budget Exceeded" } ], "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": "2025-12-06 11:55:57.725516", + "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/batta_claim/batta_claim.py b/beams/beams/doctype/batta_claim/batta_claim.py index b180316ea..7ee0174a2 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() @@ -139,7 +140,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 +151,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): ''' @@ -267,6 +291,40 @@ 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): + ''' + 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" + ) + if not department: + return + + hod_employee = frappe.db.get_value( + "Department", + department, + "head_of_department" + ) + if not hod_employee: + return + + hod_user = frappe.db.get_value( + "Employee", + hod_employee, + "user_id" + ) + if not hod_user: + return + + self.hod_email = hod_user + + @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): ''' 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/cost_subhead/cost_subhead.json b/beams/beams/doctype/budget_group/budget_group.json similarity index 51% rename from beams/beams/doctype/cost_subhead/cost_subhead.json rename to beams/beams/doctype/budget_group/budget_group.json index 18d889aeb..44a712322 100644 --- a/beams/beams/doctype/cost_subhead/cost_subhead.json +++ b/beams/beams/doctype/budget_group/budget_group.json @@ -1,50 +1,37 @@ { "actions": [], + "allow_import": 1, "allow_rename": 1, - "autoname": "field:cost_subhead", - "creation": "2024-10-17 14:25:11.465299", + "autoname": "field:budget_group_name", + "creation": "2025-01-27 10:25:39.939893", "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "section_break_9os0", - "cost_subhead", - "cost_head", - "accounts" + "budget_group_name", + "description" ], "fields": [ { - "fieldname": "section_break_9os0", - "fieldtype": "Section Break" + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" }, { - "fieldname": "cost_subhead", + "fieldname": "budget_group_name", "fieldtype": "Data", "in_list_view": 1, - "label": "Cost Subhead", + "label": "Budget Group Name", + "no_copy": 1, "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": "2026-01-31 12:46:39.613937", "modified_by": "Administrator", "module": "BEAMS", - "name": "Cost Subhead", + "name": "Budget Group", "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ @@ -61,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/finance_group/__init__.py b/beams/beams/doctype/budget_region/__init__.py similarity index 100% rename from beams/beams/doctype/finance_group/__init__.py rename to beams/beams/doctype/budget_region/__init__.py 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/finance_group/finance_group.json b/beams/beams/doctype/budget_region/budget_region.json similarity index 66% rename from beams/beams/doctype/finance_group/finance_group.json rename to beams/beams/doctype/budget_region/budget_region.json index 1a723ff94..f516f8624 100644 --- a/beams/beams/doctype/finance_group/finance_group.json +++ b/beams/beams/doctype/budget_region/budget_region.json @@ -1,29 +1,30 @@ { "actions": [], "allow_rename": 1, - "autoname": "field:finance_group", - "creation": "2025-03-05 10:56:14.289286", + "autoname": "field:budget_region", + "creation": "2026-02-05 14:28:41.918778", "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "finance_group" + "budget_region" ], "fields": [ { - "fieldname": "finance_group", + "fieldname": "budget_region", "fieldtype": "Data", "in_list_view": 1, - "label": "Finance Group", + "label": "Budget Region", "reqd": 1, "unique": 1 } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-03-05 11:37:31.454741", + "modified": "2026-02-05 14:36:01.274733", "modified_by": "Administrator", "module": "BEAMS", - "name": "Finance Group", + "name": "Budget Region", "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ @@ -40,6 +41,8 @@ "write": 1 } ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, "sort_field": "modified", "sort_order": "DESC", "states": [] 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/cost_subhead/test_cost_subhead.py b/beams/beams/doctype/budget_region/test_budget_region.py similarity index 50% rename from beams/beams/doctype/cost_subhead/test_cost_subhead.py rename to beams/beams/doctype/budget_region/test_budget_region.py index 4ffbd465c..8392c2007 100644 --- a/beams/beams/doctype/cost_subhead/test_cost_subhead.py +++ b/beams/beams/doctype/budget_region/test_budget_region.py @@ -1,9 +1,9 @@ -# Copyright (c) 2024, efeone and Contributors +# Copyright (c) 2026, efeone and Contributors # See license.txt # import frappe from frappe.tests.utils import FrappeTestCase -class TestCostSubhead(FrappeTestCase): +class TestBudgetRegion(FrappeTestCase): pass diff --git a/beams/beams/doctype/budget_template/budget_template.js b/beams/beams/doctype/budget_template/budget_template.js index d61f5c6cd..5b3cf7411 100644 --- a/beams/beams/doctype/budget_template/budget_template.js +++ b/beams/beams/doctype/budget_template/budget_template.js @@ -2,64 +2,109 @@ // 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); + } + set_filters(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] + ) + }); + } + }) + } + }, }); +// 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('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 +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..9f3386c89 100644 --- a/beams/beams/doctype/budget_template/budget_template.py +++ b/beams/beams/doctype/budget_template/budget_template.py @@ -2,26 +2,128 @@ # For license information, please see license.txt import frappe +from frappe import _ 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 + def validate(self): + self.validate_account_per_cost_center() - for item in self.budget_template_item: - if not item.cost_sub_head or not self.company: - item.account = "" - continue + 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. + ''' - cost_subhead_doc = frappe.get_doc("Cost Subhead", item.cost_sub_head) + if not self.cost_center or not self.budget_template_items: + return - 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 = "" + seen_cost_heads = set() - def before_save(self): - self.set_default_account() + 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', + 'parentfield': 'budget_template_items', + '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() +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 [] + + 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 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/bureau/bureau.js b/beams/beams/doctype/bureau/bureau.js index c533665cc..b8540a8d1 100644 --- a/beams/beams/doctype/bureau/bureau.js +++ b/beams/beams/doctype/bureau/bureau.js @@ -4,6 +4,9 @@ frappe.ui.form.on("Bureau", { setup(frm) { set_filters(frm); + }, + regional_bureau(frm) { + set_regional_bureau_head(frm); } }); @@ -19,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); + } + } + ); + } +} + diff --git a/beams/beams/doctype/bureau/bureau.json b/beams/beams/doctype/bureau/bureau.json index 6dccea586..f9b694847 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,28 @@ "label": "Is Parent Bureau" }, { - "fetch_from": "regional_bureau.parent_bureau_head", - "fieldname": "parent_bureau_head", + "fieldname": "regional_bureau_head", "fieldtype": "Link", "in_list_view": 1, + "label": "Regional Bureau Head", + "options": "Employee" + }, + { + "fieldname": "bureau_head", + "fieldtype": "Link", "label": "Bureau Head", "options": "Employee" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-12-01 14:26:58.708333", + "modified": "2026-01-01 15:25:28.045824", "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", 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/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..9e531670e 100644 --- a/beams/beams/doctype/cost_head/cost_head.js +++ b/beams/beams/doctype/cost_head/cost_head.js @@ -1,8 +1,23 @@ // 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', + root_type: 'Expense', + 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_head/cost_head.py b/beams/beams/doctype/cost_head/cost_head.py index 37794bfab..e5c280b42 100644 --- a/beams/beams/doctype/cost_head/cost_head.py +++ b/beams/beams/doctype/cost_head/cost_head.py @@ -1,9 +1,29 @@ # 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_duplicate_company() + + def validate_duplicate_company(self): + """Allow only one row per Company in this Cost Head.""" + + seen_companies = set() + + for row in self.accounts: + if not row.company: + continue + + if row.company in seen_companies: + frappe.throw( + _("Cannot set multiple Item Defaults for a company."), + title=_("Duplicate Company"), + ) + + seen_companies.add(row.company) + 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.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/employee_travel_request/employee_travel_request.js b/beams/beams/doctype/employee_travel_request/employee_travel_request.js index 56c2c7a89..16b9f88db 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 () { @@ -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 + 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_exceed || 0 + is_budget_exceeded: frm.doc.is_budget_exceeded || 0 }, callback: function (r) { if (!r.exc) { @@ -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") { @@ -504,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; @@ -515,10 +509,23 @@ 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){ - frm.set_value("is__budget_exceed",0); + frm.set_value("is_budget_exceeded", 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..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", @@ -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" }, { @@ -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,12 +312,11 @@ "link_fieldname": "travel_request" } ], - - "modified": "2025-12-12 16:02:28.896125", + "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..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 @@ -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/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/beams/doctype/m1_budget_account/__init__.py b/beams/beams/doctype/m1_budget_account/__init__.py new file mode 100644 index 000000000..e69de29bb 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..70b3a3835 --- /dev/null +++ b/beams/beams/doctype/m1_budget_account/m1_budget_account.json @@ -0,0 +1,335 @@ +{ + "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", + "company_currency", + "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", + "options": "company_currency", + "reqd": 1 + }, + { + "fieldname": "monthly_amount_distribution_section", + "fieldtype": "Section Break", + "label": "Monthly Amount Distribution" + }, + { + "default": "0", + "fieldname": "january", + "fieldtype": "Currency", + "label": "January", + "options": "company_currency" + }, + { + "default": "0", + "fieldname": "february", + "fieldtype": "Currency", + "label": "February", + "options": "company_currency" + }, + { + "default": "0", + "fieldname": "march", + "fieldtype": "Currency", + "label": "March", + "options": "company_currency" + }, + { + "default": "0", + "fieldname": "april", + "fieldtype": "Currency", + "label": "April", + "options": "company_currency" + }, + { + "fieldname": "column_break_qqgq", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "may", + "fieldtype": "Currency", + "label": "May", + "options": "company_currency" + }, + { + "default": "0", + "fieldname": "june", + "fieldtype": "Currency", + "label": "June", + "options": "company_currency" + }, + { + "default": "0", + "fieldname": "july", + "fieldtype": "Currency", + "label": "July", + "options": "company_currency" + }, + { + "default": "0", + "fieldname": "august", + "fieldtype": "Currency", + "label": "August", + "options": "company_currency" + }, + { + "fieldname": "column_break_iiwj", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "september", + "fieldtype": "Currency", + "label": "September", + "options": "company_currency" + }, + { + "default": "0", + "fieldname": "october", + "fieldtype": "Currency", + "label": "October", + "options": "company_currency" + }, + { + "default": "0", + "fieldname": "november", + "fieldtype": "Currency", + "label": "November", + "options": "company_currency" + }, + { + "default": "0", + "fieldname": "december", + "fieldtype": "Currency", + "label": "December", + "options": "company_currency" + }, + { + "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 + }, + { + "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-07 12:11:36.342976", + "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/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 9c5dc61d0..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", @@ -123,19 +123,19 @@ "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.parent_bureau_head", + "fetch_from": "bureau.regional_bureau_head", "fieldname": "bureau_head", "fieldtype": "Link", "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": "2025-12-24 11:01:47.212630", + "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/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) + diff --git a/beams/beams/doctype/substitute_booking/substitute_booking.js b/beams/beams/doctype/substitute_booking/substitute_booking.js index eca7d978d..9b14cd84c 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); + } + }); +} 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 cad0e321f..fa5d0d3de 100644 --- a/beams/beams/overrides/budget.py +++ b/beams/beams/overrides/budget.py @@ -4,80 +4,68 @@ 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: return - default_dimensions = [ { - "fieldname": "project", - "document_type": "Project", + 'fieldname': 'project', + 'document_type': 'Project', }, { - "fieldname": "cost_center", - "document_type": "Cost Center", - }, - { - "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, @@ -92,78 +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_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') -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: - 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)), @@ -176,102 +173,97 @@ 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, BudgetError, 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 - ' ) return msg @@ -281,105 +273,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") - - 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, - ) +def get_ordered_amount(args): + item_code = args.get('item_code') + cost_head = args.get('cost_head', '') + condition = get_other_condition(args, 'Purchase Order') - 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 @@ -390,29 +410,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") - accumulated_percentage = 0.0 + 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 @@ -420,20 +442,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: @@ -447,15 +470,32 @@ 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/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..bc6b80f70 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, - }, - { - "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, } ] @@ -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,12 +186,10 @@ 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"): - cond += "and b.finance_group = '{0}'".format(filters.get("finance_group")) return frappe.db.sql( f""" @@ -198,12 +199,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 @@ -227,32 +228,49 @@ 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( - "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 +279,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 +298,29 @@ 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.target = tdd[ccd.account][ccd.fiscal_year][month_map[month]] + 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): + ''' + Returns the list of fiscal years between from_fiscal_year and to_fiscal_year + ''' fiscal_year = frappe.db.sql( """ select @@ -320,4 +333,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 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 { 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 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/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 a5963222b..1b41e3a29 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", @@ -76,8 +77,10 @@ "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", + "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 = { @@ -188,6 +191,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 +228,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" @@ -325,7 +327,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" @@ -436,7 +440,10 @@ }, "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 @@ -490,7 +497,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; @@ -580,8 +587,6 @@ "Availability and Attendance", "HR Message", "Adherence and Break", - "Ticket Summary", - "Ticket Dashboard", ], ] ], @@ -598,5 +603,8 @@ ], ] ], - } + }, + { + "dt": "Cost Category", + }, ] diff --git a/beams/patches.txt b/beams/patches.txt index f5c2581d6..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 #18-12-2025 +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 fba1b2be9..7708f9efa 100644 --- a/beams/patches/delete_custom_fields.py +++ b/beams/patches/delete_custom_fields.py @@ -225,6 +225,74 @@ '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' + }, + { + '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' + }, + { + 'dt':'Budget', + 'fieldname': 'column_break_ab' + } ] def execute(): diff --git a/beams/setup.py b/beams/setup.py index 079a20d66..7102fbd94 100644 --- a/beams/setup.py +++ b/beams/setup.py @@ -116,10 +116,22 @@ 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() +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()) @@ -361,7 +373,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" }, { @@ -749,7 +761,7 @@ def get_leave_application_custom_fields(): "fieldtype": "Attach", "label": "Medical Certificate", "hidden": 1, - "insert_after": "leave_type" + "insert_after": "leave_type" } ] @@ -866,13 +878,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" - } ] } @@ -1028,9 +1033,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" + }, ] } @@ -1068,9 +1120,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" @@ -1109,15 +1161,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 +1183,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.includes('Rejected')" - }, { "fieldname": "total_amount", "fieldtype": "Currency", @@ -1157,18 +1192,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 +1205,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 +1218,44 @@ 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": "rejection_feedback", + "fieldtype": "Table", + "label": "Rejection Feedback", + "options":"Rejection Feedback", + "insert_after": "company_currency", + "read_only":1 }, { - "fieldname": "column_break_cd", - "fieldtype": "Column Break", - "label": " ", - "insert_after": "cost_category" + "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, }, { - "fieldname": "cost_description", - "fieldtype": "Small Text", - "label": "Cost Description", - "insert_after": "column_break_cd" + "fieldname": "budget_head", + "fieldtype": "Data", + "label": "Budget Head", + "insert_after": "monthly_distribution", + "read_only": 1, + "fetch_from": "budget_template.budget_head" }, { - "fieldname": "equal_monthly_distribution", - "fieldtype": "Check", - "label": "Equal Monthly Distribution ", - "insert_after": "cost_description" - }, + "fieldname": "budget_head_user", + "fieldtype": "Data", + "label": "Budget Head User", + "insert_after": "budget_head", + "read_only": 1, + "fetch_from": "budget_template.budget_head_user", + "hidden": 1 + } + + ], + "Budget Account": [ { "fieldname": "section_break_ab", "fieldtype": "Section Break", @@ -1298,7 +1320,7 @@ def get_budget_custom_fields(): "fieldname": "column_break_ab", "fieldtype": "Column Break", "label": " ", - "insert_after": "august" + "insert_after": "july" }, { "fieldname": "september", @@ -1709,7 +1731,7 @@ def get_quotation_custom_fields(): "label": "Sales Type", "options": "Sales Type", "insert_after": "item_name" - } + } ] } @@ -1790,7 +1812,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", @@ -1800,7 +1822,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, } ] @@ -1898,60 +1920,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", @@ -2287,8 +2309,20 @@ 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":[ { "fieldname": "period_from", @@ -2355,7 +2389,7 @@ def get_voucher_entry_custom_fields(): "insert_after": "project", "default": "1", }, - { + { "fieldname": "is_budget_exceeded", "fieldtype": "Check", "label": "Is Budget Exceeded", @@ -2725,6 +2759,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", @@ -3520,7 +3561,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", @@ -3567,7 +3608,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", @@ -3621,7 +3662,14 @@ 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" + }, ] } @@ -3746,24 +3794,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" }, { @@ -4851,13 +4899,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", @@ -5171,7 +5212,7 @@ def get_property_setters(): "property": "allow_in_quick_entry", "value": 1, }, - { + { "doctype_or_field": "DocField", "doc_type": "HD Ticket", "field_name": "agent_group", @@ -5614,6 +5655,83 @@ 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", "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"]' + }, + { + "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" + }, + { + "doctype_or_field": "DocType", + "doc_type": "Budget", + "property": "field_order", + "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" + }, ] def get_material_request_custom_fields(): @@ -5641,9 +5759,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", @@ -5661,7 +5779,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", @@ -5682,6 +5800,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" + }, ] } @@ -5827,7 +5953,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", @@ -5870,6 +5996,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 @@ -5940,14 +6067,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 - } ] } @@ -6006,12 +6132,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""" } ] @@ -6025,8 +6151,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" - } + }, ] } @@ -6224,7 +6357,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", @@ -6261,13 +6394,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", @@ -6276,8 +6409,8 @@ def get_supplier_quotation_custom_fields(): "default":"Medium", "insert_after": "company", "in_list_view": 1 - }, - + }, + ] } @@ -6434,31 +6567,167 @@ 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() + + +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}, + ], + } +