diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js index 8df443f5b6..57ef4ab2a0 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.js @@ -27,10 +27,8 @@ frappe.query_reports["Employee CTC Break-up"] = { }, on_change: function () { let employee = frappe.query_report.get_filter_value("employee"); - if (!employee) { - frappe.query_report.set_filter_value("salary_structure_assignment", ""); - return; - } + frappe.query_report.set_filter_value("salary_structure_assignment", ""); + if (!employee) return; frappe.db .get_list("Salary Structure Assignment", { filters: { employee: employee, docstatus: 1 }, @@ -39,6 +37,8 @@ frappe.query_reports["Employee CTC Break-up"] = { limit: 1, }) .then(function (result) { + if (frappe.query_report.get_filter_value("salary_structure_assignment")) + return; frappe.query_report.set_filter_value( "salary_structure_assignment", (result[0] && result[0].name) || "", @@ -66,6 +66,8 @@ frappe.query_reports["Employee CTC Break-up"] = { }, ], onload: async function (report) { + if (report.get_filter_value("employee")) return; + const employee = await hrms.get_current_employee(); if (!employee) return; report.set_filter_value("employee", employee); diff --git a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py index fe58b558cf..3484e2fedd 100644 --- a/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py +++ b/hrms/payroll/report/employee_ctc_break_up/employee_ctc_break_up.py @@ -45,6 +45,7 @@ def __init__(self, employee, salary_structure_assignment): self.earning_components = [] self.deduction_components = [] self.tax_components = [] + self.employer_contribution_components = [] self.total_net_earnings = [] self.total_gross_earnings = [] @@ -64,6 +65,7 @@ def validate_ctc(self): def get_data(self): self.set_salary_component_details() + self.set_employer_contribution_details() self.calculate_yearly_amounts_and_percent_of_ctc() self.indent_salary_components() self.separate_salary_components_by_type() @@ -75,6 +77,7 @@ def get_data(self): self.earning_components + self.deduction_components + self.tax_components + + self.employer_contribution_components + self.total_net_earnings + self.total_gross_earnings ) @@ -108,6 +111,26 @@ def set_salary_component_details(self): ) component.update(component_details) + def set_employer_contribution_details(self): + evaluated_components = getattr(self.salary_slip, "_evaluated_components", None) + if evaluated_components is None: + evaluated_components = frappe.get_cached_doc( + "Salary Structure Assignment", self.salary_structure_assignment + ).get_evaluated_components() + + self.salary_components += [ + { + "salary_component": component.salary_component, + "per_cycle": flt(component.default_amount), + "abbr": component.abbr, + "amount_based_on_formula": component.amount_based_on_formula, + "formula": component.formula, + "component_type": "employer_contributions", + } + for component in evaluated_components["employer_contributions"] + if not component.statistical_component + ] + def calculate_yearly_amounts_and_percent_of_ctc(self): for component in self.salary_components: annual_amount = component.get("per_cycle", 0) * self.cycle_multiplier @@ -130,9 +153,16 @@ def separate_salary_components_by_type(self): self.tax_components = [ component for component in self.salary_components if component.get("is_tax_component") ] + self.employer_contribution_components = [ + component + for component in self.salary_components + if component.get("component_type") == "employer_contributions" + ] def set_abbr_type_and_formula(self): - for component in self.earning_components + self.deduction_components: + for component in ( + self.earning_components + self.deduction_components + self.employer_contribution_components + ): component["salary_component"] = f"{component.get("salary_component")} ({component.get("abbr")})" component["type"] = "Formula" if component.get("amount_based_on_formula") else "Fixed" component["formula"] = ( @@ -153,6 +183,7 @@ def set_totals_row(component_type): "Earnings": self.earning_components, "Deductions": self.deduction_components, "Tax Deductions": self.tax_components, + "Employer Contributions": self.employer_contribution_components, }.get(component_type) totals_row = { "salary_component": component_type, @@ -165,7 +196,7 @@ def set_totals_row(component_type): } components.insert(0, totals_row) - for component_type in ("Earnings", "Deductions", "Tax Deductions"): + for component_type in ("Earnings", "Deductions", "Tax Deductions", "Employer Contributions"): set_totals_row(component_type) def set_net_and_gross_earning_rows(self): diff --git a/hrms/payroll/report/employee_ctc_break_up/test_employee_ctc_breakup.py b/hrms/payroll/report/employee_ctc_break_up/test_employee_ctc_breakup.py index f48bafa09b..71a22ed741 100644 --- a/hrms/payroll/report/employee_ctc_break_up/test_employee_ctc_breakup.py +++ b/hrms/payroll/report/employee_ctc_break_up/test_employee_ctc_breakup.py @@ -101,7 +101,8 @@ def test_income_tax_net_pay_calculation_at_the_start_of_payroll_period(self): frappe.flags.posting_date = get_year_start(getdate()) ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name) self.assertEqual(ctc_breakup.net_pay, 81396) - income_tax_row = ctc_breakup.get_data()[-3] + ctc_breakup.get_data() + income_tax_row = ctc_breakup.tax_components[-1] self.assertEqual(income_tax_row.get("per_cycle"), monthly_income_tax) self.assertEqual(income_tax_row.get("annual"), annual_tax) @@ -126,7 +127,8 @@ def test_income_tax_net_pay_calculation_at_the_end_of_payroll_period(self): frappe.flags.posting_date = add_months(get_year_start(getdate()), 10) ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name) self.assertEqual(ctc_breakup.net_pay, 92800) - income_tax_row = ctc_breakup.get_data()[-3] + ctc_breakup.get_data() + income_tax_row = ctc_breakup.tax_components[-1] self.assertEqual(income_tax_row.get("per_cycle"), monthly_income_tax) self.assertEqual(income_tax_row.get("annual"), annual_tax) @@ -143,11 +145,12 @@ def test_report(self): frappe.flags.posting_date = get_year_start(getdate()) ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name) data = ctc_breakup.get_data() - self.assertEqual(len(data), 11) + self.assertEqual(len(data), 12) earning_components = ctc_breakup.earning_components deduction_components = ctc_breakup.deduction_components tax_components = ctc_breakup.tax_components + employer_contribution_components = ctc_breakup.employer_contribution_components net_pay_row = ctc_breakup.total_net_earnings gross_pay_row = ctc_breakup.total_gross_earnings @@ -221,6 +224,13 @@ def test_report(self): self.assertEqual(gross_pay_row[0].get("annual"), 1116000) self.assertEqual(gross_pay_row[0].get("percent_of_ctc"), 100) + self.assertEqual(len(employer_contribution_components), 1) + self.assertEqual( + employer_contribution_components[0].get("salary_component"), "Employer Contributions" + ) + self.assertEqual(employer_contribution_components[0].get("per_cycle"), 0) + self.assertEqual(employer_contribution_components[0].get("annual"), 0) + self.assertEqual( flt( basic_earning_component.get("percent_of_ctc") @@ -241,6 +251,73 @@ def test_report(self): earning_components_totals.get("percent_of_ctc"), ) + def test_employer_contributions_section(self): + employee = make_employee("test_ctc@example.com", company="_Test Company") + + for component, abbr in (("Test CTC Employer PF", "TCEPF"), ("Test CTC Employer NPS", "TCENPS")): + if frappe.db.exists("Salary Component", component): + frappe.delete_doc("Salary Component", component, force=True) + frappe.get_doc( + { + "doctype": "Salary Component", + "salary_component": component, + "salary_component_abbr": abbr, + "type": "Employer Contribution", + } + ).insert() + + salary_structure = make_salary_structure( + "Test CTC Breakup with Employer Contribution", + payroll_frequency="Monthly", + currency="INR", + other_details={ + "employer_contributions": [ + {"salary_component": "Test CTC Employer PF", "abbr": "TCEPF", "amount": 6000}, + { + "salary_component": "Test CTC Employer NPS", + "abbr": "TCENPS", + "amount_based_on_formula": 1, + "formula": "BS * 0.12", + }, + ] + }, + ) + salary_structure_assignment = create_salary_structure_assignment( + employee, salary_structure.name, base=60000, currency="INR", from_date=get_year_start(getdate()) + ) + salary_structure_assignment.ctc = 1274400 + salary_structure_assignment.save() + + frappe.flags.posting_date = get_year_start(getdate()) + ctc_breakup = SalaryBreakupReport(employee, salary_structure_assignment.name) + data = ctc_breakup.get_data() + + employer_components = ctc_breakup.employer_contribution_components + self.assertEqual(data[-2], ctc_breakup.total_net_earnings[0]) + self.assertEqual(data[-1], ctc_breakup.total_gross_earnings[0]) + self.assertEqual(data[-2 - len(employer_components) : -2], employer_components) + + totals_row = employer_components[0] + self.assertEqual(totals_row.get("salary_component"), "Employer Contributions") + self.assertTrue(totals_row.get("bold")) + self.assertEqual(totals_row.get("per_cycle"), 13200) + self.assertEqual(totals_row.get("annual"), 158400) + + pf_row = employer_components[1] + self.assertEqual(pf_row.get("salary_component"), "Test CTC Employer PF (TCEPF)") + self.assertEqual(pf_row.get("type"), "Fixed") + self.assertEqual(pf_row.get("per_cycle"), 6000) + self.assertEqual(pf_row.get("annual"), 72000) + self.assertEqual(pf_row.get("percent_of_ctc"), flt(72000 * 100 / 1274400, 2)) + + nps_row = employer_components[2] + self.assertEqual(nps_row.get("salary_component"), "Test CTC Employer NPS (TCENPS)") + self.assertEqual(nps_row.get("type"), "Formula") + self.assertEqual(nps_row.get("formula"), "BS * 0.12") + self.assertEqual(nps_row.get("per_cycle"), 7200) + self.assertEqual(nps_row.get("annual"), 86400) + self.assertEqual(nps_row.get("percent_of_ctc"), flt(86400 * 100 / 1274400, 2)) + def test_ctc_validation(self): employee = make_employee("test_ctc@example.com", company="_Test Company") salary_structure = make_salary_structure(