From d5d5d1523c5b6fa17811a48500e479fff7f3c7d8 Mon Sep 17 00:00:00 2001 From: Asmita Hase Date: Mon, 22 Jun 2026 15:15:51 +0530 Subject: [PATCH] fix: filter employees in the selected payroll period for income tax report fix: 0 check for remaining sub periods in additional salary addition --- .../doctype/salary_slip/salary_slip.py | 4 +- .../doctype/salary_slip/test_salary_slip.py | 30 +++++++++ .../income_tax_computation.py | 7 ++ .../test_income_tax_computation.py | 67 ++++++++++++++++++- 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/hrms/payroll/doctype/salary_slip/salary_slip.py b/hrms/payroll/doctype/salary_slip/salary_slip.py index 7b8f74d1ef..230a87b396 100644 --- a/hrms/payroll/doctype/salary_slip/salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/salary_slip.py @@ -1900,10 +1900,12 @@ def calculate_variable_tax(self, tax_component, has_additional_salary_tax_compon if has_additional_salary_tax_component: self.current_structured_tax_amount = self.additional_salary_amount - else: + elif self.remaining_sub_periods > 0: self.current_structured_tax_amount = ( self.total_structured_tax_amount - self.previous_total_paid_taxes ) / self.remaining_sub_periods + else: + self.current_structured_tax_amount = 0.0 # Total taxable earnings with additional earnings with full tax self.full_tax_on_additional_earnings = 0.0 diff --git a/hrms/payroll/doctype/salary_slip/test_salary_slip.py b/hrms/payroll/doctype/salary_slip/test_salary_slip.py index bb5dd0f53f..885b1ffac1 100644 --- a/hrms/payroll/doctype/salary_slip/test_salary_slip.py +++ b/hrms/payroll/doctype/salary_slip/test_salary_slip.py @@ -1751,6 +1751,36 @@ def test_tax_period_for_mid_month_payroll_period(self): # to handle cases like 16th Jul 2024 - 15th Jul 2025 self.assertEqual(period_factor, 12) + def test_variable_tax_with_zero_remaining_sub_periods(self): + # regression: when an employee has no remaining sub-periods in the payroll period + # (e.g. relieved before the period started), remaining_sub_periods is 0 and the + # structured tax calculation must not raise ZeroDivisionError + from hrms.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + payroll_period = create_payroll_period(company="_Test Company") + create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR") + employee = make_employee("test_zero_remaining_period@salary.slip", company="_Test Company") + + salary_structure = make_salary_structure( + "Structure to test zero remaining sub periods", + "Monthly", + test_tax=True, + employee=employee, + payroll_period=payroll_period, + company="_Test Company", + ) + + salary_slip = make_salary_slip(salary_structure.name, employee=employee) + + # force the boundary condition and re-run the variable tax calculation + salary_slip.remaining_sub_periods = 0 + salary_slip.calculate_variable_tax("TDS") + + # no structured tax to spread over future periods => 0, and no ZeroDivisionError + self.assertEqual(salary_slip.current_structured_tax_amount, 0.0) + + frappe.db.rollback() + @HRMSTestSuite.change_settings("Payroll Settings", {"payroll_based_on": "Leave"}) def test_lwp_calculation_based_on_relieving_date(self): emp_id = make_employee("test_lwp_based_on_relieving_date@salary.com", company="_Test Company") diff --git a/hrms/payroll/report/income_tax_computation/income_tax_computation.py b/hrms/payroll/report/income_tax_computation/income_tax_computation.py index e45f516748..e115bea43f 100644 --- a/hrms/payroll/report/income_tax_computation/income_tax_computation.py +++ b/hrms/payroll/report/income_tax_computation/income_tax_computation.py @@ -64,6 +64,13 @@ def get_employee_details(self): for d in employees: if d.employee in list(ss_assignments.keys()): + # skip employees whose employment does not overlap the payroll period, + # i.e. those who joined after the period ended or were relieved before it started + if getdate(d.date_of_joining) > getdate(self.payroll_period_end_date): + continue + if d.relieving_date and getdate(d.relieving_date) < getdate(self.payroll_period_start_date): + continue + d.update(ss_assignments[d.employee]) self.employees.setdefault(d.employee, d) diff --git a/hrms/payroll/report/income_tax_computation/test_income_tax_computation.py b/hrms/payroll/report/income_tax_computation/test_income_tax_computation.py index bf8edb7acd..f450d8d30f 100644 --- a/hrms/payroll/report/income_tax_computation/test_income_tax_computation.py +++ b/hrms/payroll/report/income_tax_computation/test_income_tax_computation.py @@ -1,5 +1,5 @@ import frappe -from frappe.utils import getdate +from frappe.utils import add_days, getdate from erpnext.setup.doctype.employee.test_employee import make_employee @@ -109,6 +109,71 @@ def test_report(self): for key, val in expected_data.items(): self.assertEqual(result[1][0].get(key), val) + def test_employee_joined_after_payroll_period_is_excluded(self): + # employee joins after the payroll period ends -> employment does not overlap + # the period, so they must be excluded from the report (and not crash it while + # trying to build a salary slip with no applicable salary structure) + joining_date = add_days(self.payroll_period.end_date, 90) + employee = make_employee( + "joined_after_period@example.com", + company="_Test Company", + date_of_joining=joining_date, + ) + make_salary_structure( + "Monthly Salary Structure Joined After Period", + "Monthly", + employee=employee, + company="_Test Company", + currency="INR", + from_date=joining_date, + payroll_period=self.payroll_period, + test_tax=True, + ) + + filters = frappe._dict( + { + "company": "_Test Company", + "payroll_period": self.payroll_period.name, + } + ) + result = execute(filters)[1] + + employees_in_report = [row.get("employee") for row in result] + self.assertNotIn(employee, employees_in_report) + + def test_employee_relieved_before_payroll_period_is_excluded(self): + # employee relieved before the payroll period starts -> employment does not + # overlap the period, so they must be excluded from the report + joining_date = add_days(self.payroll_period.start_date, -400) + relieving_date = add_days(self.payroll_period.start_date, -1) + employee = make_employee( + "relieved_before_period@example.com", + company="_Test Company", + date_of_joining=joining_date, + ) + make_salary_structure( + "Monthly Salary Structure Relieved Before Period", + "Monthly", + employee=employee, + company="_Test Company", + currency="INR", + from_date=joining_date, + payroll_period=self.payroll_period, + test_tax=True, + ) + frappe.db.set_value("Employee", employee, {"relieving_date": relieving_date, "status": "Left"}) + + filters = frappe._dict( + { + "company": "_Test Company", + "payroll_period": self.payroll_period.name, + } + ) + result = execute(filters)[1] + + employees_in_report = [row.get("employee") for row in result] + self.assertNotIn(employee, employees_in_report) + def test_get_report_for_all_employees(self): frappe.db.delete("Employee") users = [