Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion hrms/payroll/doctype/salary_slip/salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions hrms/payroll/doctype/salary_slip/test_salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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 = [
Expand Down
Loading