Skip to content

Commit

Permalink
feat: Tax for recurring additional salary (frappe#27459)
Browse files Browse the repository at this point in the history
* fix: Logic for tax calculation on recurring additional salary

* fix: Get actual amount always in case of overwritten additional salary even if based on payment days

* feat: Test case added for recurring additional salary

* fix: use query builder to get additional salaries instead of raw SQL

* fix: query formatting and remove trailing spaces

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
  • Loading branch information
nabinhait and ruchamahabal committed Oct 20, 2021
1 parent 830fa60 commit 2ef4844
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 36 deletions.
1 change: 1 addition & 0 deletions erpnext/hr/doctype/employee/test_employee.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def make_employee(user, company=None, **kwargs):
"email": user,
"first_name": user,
"new_password": "password",
"send_welcome_email": 0,
"roles": [{"doctype": "Has Role", "role": "Employee"}]
}).insert()

Expand Down
41 changes: 21 additions & 20 deletions erpnext/payroll/doctype/additional_salary/additional_salary.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,27 +125,28 @@ def get_amount(self, sal_start_date, sal_end_date):
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days

@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type):
additional_salary_list = frappe.db.sql("""
select name, salary_component as component, type, amount,
overwrite_salary_structure_amount as overwrite,
deduct_full_tax_on_selected_payroll_date
from `tabAdditional Salary`
where employee=%(employee)s
and docstatus = 1
and (
payroll_date between %(from_date)s and %(to_date)s
or
from_date <= %(to_date)s and to_date >= %(to_date)s
)
and type = %(component_type)s
order by salary_component, overwrite ASC
""", {
'employee': employee,
'from_date': start_date,
'to_date': end_date,
'component_type': "Earning" if component_type == "earnings" else "Deduction"
}, as_dict=1)
comp_type = 'Earning' if component_type == 'earnings' else 'Deduction'

additional_sal = frappe.qb.DocType('Additional Salary')
component_field = additional_sal.salary_component.as_('component')
overwrite_field = additional_sal.overwrite_salary_structure_amount.as_('overwrite')

additional_salary_list = frappe.qb.from_(
additional_sal
).select(
additional_sal.name, component_field, additional_sal.type,
additional_sal.amount, additional_sal.is_recurring, overwrite_field,
additional_sal.deduct_full_tax_on_selected_payroll_date
).where(
(additional_sal.employee == employee)
& (additional_sal.docstatus == 1)
& (additional_sal.type == comp_type)
).where(
additional_sal.payroll_date[start_date: end_date]
| ((additional_sal.from_date <= end_date) & (additional_sal.to_date >= end_date))
).run(as_dict=True)

additional_salaries = []
components_to_overwrite = []
Expand Down
13 changes: 11 additions & 2 deletions erpnext/payroll/doctype/salary_detail/salary_detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"year_to_date",
"section_break_5",
"additional_salary",
"is_recurring_additional_salary",
"statistical_component",
"depends_on_payment_days",
"exempted_from_income_tax",
Expand Down Expand Up @@ -235,11 +236,19 @@
"label": "Year To Date",
"options": "currency",
"read_only": 1
}
},
{
"default": "0",
"depends_on": "eval:doc.parenttype=='Salary Slip' && doc.additional_salary",
"fieldname": "is_recurring_additional_salary",
"fieldtype": "Check",
"label": "Is Recurring Additional Salary",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2021-01-14 13:39:15.847158",
"modified": "2021-08-30 13:39:15.847158",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Detail",
Expand Down
49 changes: 35 additions & 14 deletions erpnext/payroll/doctype/salary_slip/salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,8 @@ def add_additional_salary_components(self, component_type):
get_salary_component_data(additional_salary.component),
additional_salary.amount,
component_type,
additional_salary
additional_salary,
is_recurring = additional_salary.is_recurring
)

def add_tax_components(self, payroll_period):
Expand All @@ -651,7 +652,7 @@ def add_tax_components(self, payroll_period):
tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions")

def update_component_row(self, component_data, amount, component_type, additional_salary=None):
def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0):
component_row = None
for d in self.get(component_type):
if d.salary_component != component_data.salary_component:
Expand Down Expand Up @@ -698,6 +699,8 @@ def update_component_row(self, component_data, amount, component_type, additiona
else:
component_row.default_amount = 0
component_row.additional_amount = amount

component_row.is_recurring_additional_salary = is_recurring
component_row.additional_salary = additional_salary.name
component_row.deduct_full_tax_on_selected_payroll_date = \
additional_salary.deduct_full_tax_on_selected_payroll_date
Expand Down Expand Up @@ -894,25 +897,33 @@ def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=
amount, additional_amount = earning.default_amount, earning.additional_amount

if earning.is_tax_applicable:
if additional_amount:
if earning.is_flexible_benefit:
flexi_benefits += amount
else:
taxable_earnings += (amount - additional_amount)
additional_income += additional_amount

# Get additional amount based on future recurring additional salary
if additional_amount and earning.is_recurring_additional_salary:
additional_income += self.get_future_recurring_additional_amount(earning.additional_salary,
earning.additional_amount) # Used earning.additional_amount to consider the amount for the full month

if earning.deduct_full_tax_on_selected_payroll_date:
additional_income_with_full_tax += additional_amount
continue

if earning.is_flexible_benefit:
flexi_benefits += amount
else:
taxable_earnings += amount

if allow_tax_exemption:
for ded in self.deductions:
if ded.exempted_from_income_tax:
amount = ded.amount
amount, additional_amount = ded.amount, ded.additional_amount
if based_on_payment_days:
amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0]
taxable_earnings -= flt(amount)
amount, additional_amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)

taxable_earnings -= flt(amount - additional_amount)
additional_income -= additional_amount

if additional_amount and ded.is_recurring_additional_salary:
additional_income -= self.get_future_recurring_additional_amount(ded.additional_salary,
ded.additional_amount) # Used ded.additional_amount to consider the amount for the full month

return frappe._dict({
"taxable_earnings": taxable_earnings,
Expand All @@ -921,11 +932,21 @@ def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=
"flexi_benefits": flexi_benefits
})

def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
future_recurring_additional_amount = 0
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
# future month count excluding current
future_recurring_period = (getdate(to_date).month - getdate(self.start_date).month)
if future_recurring_period > 0:
future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month
return future_recurring_additional_amount

def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
amount, additional_amount = row.amount, row.additional_amount
if (self.salary_structure and
cint(row.depends_on_payment_days) and cint(self.total_working_days) and
(not self.salary_slip_based_on_timesheet or
cint(row.depends_on_payment_days) and cint(self.total_working_days)
and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary
and (not self.salary_slip_based_on_timesheet or
getdate(self.start_date) < joining_date or
(relieving_date and getdate(self.end_date) > relieving_date)
)):
Expand Down
69 changes: 69 additions & 0 deletions erpnext/payroll/doctype/salary_slip/test_salary_slip.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,61 @@ def test_tax_for_payroll_period(self):
# undelete fixture data
frappe.db.rollback()

def test_tax_for_recurring_additional_salary(self):
frappe.db.sql("""delete from `tabPayroll Period`""")
frappe.db.sql("""delete from `tabSalary Component`""")

payroll_period = create_payroll_period()

create_tax_slab(payroll_period, allow_tax_exemption=True)

employee = make_employee("test_tax@salary.slip")
delete_docs = [
"Salary Slip",
"Additional Salary",
"Employee Tax Exemption Declaration",
"Employee Tax Exemption Proof Submission",
"Employee Benefit Claim",
"Salary Structure Assignment"
]
for doc in delete_docs:
frappe.db.sql("delete from `tab%s` where employee='%s'" % (doc, employee))

from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure

salary_structure = make_salary_structure("Stucture to test tax", "Monthly",
other_details={"max_benefits": 100000}, test_tax=True,
employee=employee, payroll_period=payroll_period)


create_salary_slips_for_payroll_period(employee, salary_structure.name,
payroll_period, deduct_random=False, num=3)

tax_paid = get_tax_paid_in_period(employee)

annual_tax = 23196.0
self.assertEqual(tax_paid, annual_tax)

frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))

#------------------------------------
# Recurring additional salary
start_date = add_months(payroll_period.start_date, 3)
end_date = add_months(payroll_period.start_date, 5)
create_recurring_additional_salary(employee, "Performance Bonus", 20000, start_date, end_date)

frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))

create_salary_slips_for_payroll_period(employee, salary_structure.name,
payroll_period, deduct_random=False, num=4)

tax_paid = get_tax_paid_in_period(employee)

annual_tax = 32315.0
self.assertEqual(tax_paid, annual_tax)

frappe.db.rollback()

def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 50
Expand Down Expand Up @@ -1007,3 +1062,17 @@ def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)

return salary_slip

def create_recurring_additional_salary(employee, salary_component, amount, from_date, to_date, company=None):
frappe.get_doc({
"doctype": "Additional Salary",
"employee": employee,
"company": company or erpnext.get_default_company(),
"salary_component": salary_component,
"is_recurring": 1,
"from_date": from_date,
"to_date": to_date,
"amount": amount,
"type": "Earning",
"currency": erpnext.get_default_currency()
}).submit()

0 comments on commit 2ef4844

Please sign in to comment.