Skip to content

Commit

Permalink
Merge pull request #2308 from frappe/mergify/bp/version-15-hotfix/pr-…
Browse files Browse the repository at this point in the history
…2302

fix(Payroll CTC Calculation): compute future non taxable earnings with full payment days (backport #2302)
  • Loading branch information
ruchamahabal authored Oct 18, 2024
2 parents 215d6b1 + 0d94370 commit e34cd31
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 13 deletions.
23 changes: 17 additions & 6 deletions hrms/payroll/doctype/salary_slip/salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ def set_salary_structure_assignment(self):
)
)

def calculate_net_pay(self):
def calculate_net_pay(self, skip_tax_breakup_computation: bool = False):
def set_gross_pay_and_base_gross_pay():
self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1)
self.base_gross_pay = flt(
Expand Down Expand Up @@ -798,7 +798,8 @@ def set_gross_pay_and_base_gross_pay():

self.set_precision_for_component_amounts()
self.set_net_pay()
self.compute_income_tax_breakup()
if not skip_tax_breakup_computation:
self.compute_income_tax_breakup()

def set_net_pay(self):
self.total_deduction = self.get_component_totals("deductions")
Expand Down Expand Up @@ -966,10 +967,7 @@ def compute_non_taxable_earnings(self):
non_taxable_additional_salary,
) = self.get_non_taxable_earnings_for_current_period()

# Future period non taxable earnings
future_period_non_taxable_earnings = current_period_non_taxable_earnings * (
ceil(self.remaining_sub_periods) - 1
)
future_period_non_taxable_earnings = self.get_future_period_non_taxable_earnings()

non_taxable_earnings = (
prev_period_non_taxable_earnings
Expand All @@ -980,6 +978,19 @@ def compute_non_taxable_earnings(self):

return non_taxable_earnings

def get_future_period_non_taxable_earnings(self):
salary_slip = frappe.copy_doc(self)
# consider full payment days for future period
salary_slip.payment_days = salary_slip.total_working_days
salary_slip.calculate_net_pay(skip_tax_breakup_computation=True)

future_period_non_taxable_earnings = 0
for earning in salary_slip.earnings:
if not earning.is_tax_applicable and not earning.additional_salary:
future_period_non_taxable_earnings += earning.amount

return future_period_non_taxable_earnings * (ceil(self.remaining_sub_periods) - 1)

def get_non_taxable_earnings_for_current_period(self):
current_period_non_taxable_earnings = 0.0

Expand Down
121 changes: 114 additions & 7 deletions hrms/payroll/doctype/salary_slip/test_salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
LEAVE_TYPE_MAP,
SALARY_COMPONENT_VALUES,
TAX_COMPONENTS_BY_COMPANY,
SalarySlip,
_safe_eval,
make_salary_slip_from_timesheet,
)
Expand Down Expand Up @@ -1428,17 +1429,14 @@ def test_salary_slip_generation_against_opening_entries_in_ssa(self):
def test_income_tax_breakup_fields(self):
from hrms.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure

frappe.db.sql("DELETE FROM `tabIncome Tax Slab` where currency = 'INR'")

frappe.db.delete("Income Tax Slab", {"currency": "INR"})
emp = make_employee(
"[email protected]",
company="_Test Company",
**{"date_of_joining": "2021-12-01"},
date_of_joining="2021-01-01",
)
employee_doc = frappe.get_cached_doc("Employee", emp)

payroll_period = frappe.get_all("Payroll Period", filters={"company": "_Test Company"}, limit=1)
payroll_period = frappe.get_cached_doc("Payroll Period", payroll_period[0].name)
payroll_period = frappe.get_last_doc("Payroll Period", filters={"company": "_Test Company"})
create_tax_slab(payroll_period, effective_date=payroll_period.start_date, allow_tax_exemption=True)

salary_structure_name = "Test Salary Structure to test Income Tax Breakup"
Expand All @@ -1462,7 +1460,7 @@ def test_income_tax_breakup_fields(self):

# Create Salary Slip
salary_slip = make_salary_slip(
salary_structure_doc.name, employee=employee_doc.name, posting_date=payroll_period.start_date
salary_structure_doc.name, employee=emp, posting_date=payroll_period.start_date
)

monthly_tax_amount = 11403.6
Expand All @@ -1480,6 +1478,28 @@ def test_income_tax_breakup_fields(self):
self.assertEqual(flt(salary_slip.future_income_tax_deductions, 2), 125439.65)
self.assertEqual(flt(salary_slip.total_income_tax, 2), 136843.25)

def test_consistent_future_earnings_irrespective_of_payment_days(self):
"""
For CTC calculation, verifies that future non taxable earnings remain
consistent irrespective of the payment days of current month
"""
salary_slip = make_salary_slip_with_non_taxable_component()
salary_slip.save()
future_non_taxable_earnings_with_full_payment_days = (
salary_slip.get_future_period_non_taxable_earnings()
)

salary_slip.payment_days = 20
salary_slip.calculate_net_pay()
future_non_taxable_earnings_with_reduced_payment_days = (
salary_slip.get_future_period_non_taxable_earnings()
)

self.assertEqual(
future_non_taxable_earnings_with_full_payment_days,
future_non_taxable_earnings_with_reduced_payment_days,
)

def test_tax_period_for_mid_month_payroll_period(self):
from hrms.payroll.doctype.payroll_period.payroll_period import get_period_factor

Expand Down Expand Up @@ -2524,6 +2544,93 @@ def make_salary_structure_for_statistical_component(company):
return salary_structure_doc


def make_salary_slip_with_non_taxable_component() -> SalarySlip:
from hrms.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment,
make_salary_structure,
)

frappe.db.delete("Income Tax Slab", {"currency": "INR"})
emp = make_employee(
"[email protected]",
company="_Test Company",
date_of_joining="2021-01-01",
)

payroll_period = frappe.get_last_doc("Payroll Period", filters={"company": "_Test Company"})
create_tax_slab(payroll_period, effective_date=payroll_period.start_date, allow_tax_exemption=True)

earnings = [
{
"salary_component": "Basic Salary",
"abbr": "P_BS",
"type": "Earning",
"formula": "base",
"amount_based_on_formula": 1,
},
# non taxable component
{
"salary_component": "Children Education Allowance",
"abbr": "CH_EDU",
"type": "Earning",
"depends_on_payment_days": 1,
"amount_based_on_formula": 1,
"formula": "base * 0.20",
"is_tax_applicable": 0,
},
]
make_salary_component(earnings, False, company_list=["_Test Company"])

deductions = [
{
"salary_component": "P - Professional Tax",
"abbr": "P_PT",
"type": "Deduction",
"depends_on_payment_days": 1,
"amount": 200.00,
},
]
make_salary_component(deductions, False, company_list=["_Test Company"])

salary_structure = "Salary Structure with Non Taxable Component"
if frappe.db.exists("Salary Structure", salary_structure):
frappe.db.delete("Salary Structure", salary_structure)

details = {
"doctype": "Salary Structure",
"name": salary_structure,
"company": "_Test Company",
"payroll_frequency": "Monthly",
"payment_account": get_random("Account", filters={"account_currency": "INR"}),
"currency": "INR",
}

salary_structure_doc = frappe.get_doc(details)

for entry in earnings:
salary_structure_doc.append("earnings", entry)

for entry in deductions:
salary_structure_doc.append("deductions", entry)

salary_structure_doc.insert().submit()
create_salary_structure_assignment(
emp,
salary_structure_doc.name,
from_date=payroll_period.start_date,
company="_Test Company",
currency="INR",
payroll_period=payroll_period,
base=65000,
)

# Create Salary Slip
salary_slip = make_salary_slip(
salary_structure_doc.name, employee=emp, posting_date=payroll_period.start_date
)
return salary_slip


def mark_attendance(
employee,
attendance_date,
Expand Down

0 comments on commit e34cd31

Please sign in to comment.