feat: Tax for recurring additional salary (#27459)

* 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>
This commit is contained in:
Nabin Hait 2021-10-20 15:50:30 +05:30 committed by GitHub
parent 830fa60acd
commit 2ef4844a3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 138 additions and 37 deletions

View File

@ -55,6 +55,7 @@ def make_employee(user, company=None, **kwargs):
"email": user, "email": user,
"first_name": user, "first_name": user,
"new_password": "password", "new_password": "password",
"send_welcome_email": 0,
"roles": [{"doctype": "Has Role", "role": "Employee"}] "roles": [{"doctype": "Has Role", "role": "Employee"}]
}).insert() }).insert()

View File

@ -125,27 +125,28 @@ class AdditionalSalary(Document):
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days return amount_per_day * no_of_days
@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type): def get_additional_salaries(employee, start_date, end_date, component_type):
additional_salary_list = frappe.db.sql(""" comp_type = 'Earning' if component_type == 'earnings' else 'Deduction'
select name, salary_component as component, type, amount,
overwrite_salary_structure_amount as overwrite, additional_sal = frappe.qb.DocType('Additional Salary')
deduct_full_tax_on_selected_payroll_date component_field = additional_sal.salary_component.as_('component')
from `tabAdditional Salary` overwrite_field = additional_sal.overwrite_salary_structure_amount.as_('overwrite')
where employee=%(employee)s
and docstatus = 1 additional_salary_list = frappe.qb.from_(
and ( additional_sal
payroll_date between %(from_date)s and %(to_date)s ).select(
or additional_sal.name, component_field, additional_sal.type,
from_date <= %(to_date)s and to_date >= %(to_date)s additional_sal.amount, additional_sal.is_recurring, overwrite_field,
) additional_sal.deduct_full_tax_on_selected_payroll_date
and type = %(component_type)s ).where(
order by salary_component, overwrite ASC (additional_sal.employee == employee)
""", { & (additional_sal.docstatus == 1)
'employee': employee, & (additional_sal.type == comp_type)
'from_date': start_date, ).where(
'to_date': end_date, additional_sal.payroll_date[start_date: end_date]
'component_type': "Earning" if component_type == "earnings" else "Deduction" | ((additional_sal.from_date <= end_date) & (additional_sal.to_date >= end_date))
}, as_dict=1) ).run(as_dict=True)
additional_salaries = [] additional_salaries = []
components_to_overwrite = [] components_to_overwrite = []

View File

@ -12,6 +12,7 @@
"year_to_date", "year_to_date",
"section_break_5", "section_break_5",
"additional_salary", "additional_salary",
"is_recurring_additional_salary",
"statistical_component", "statistical_component",
"depends_on_payment_days", "depends_on_payment_days",
"exempted_from_income_tax", "exempted_from_income_tax",
@ -235,11 +236,19 @@
"label": "Year To Date", "label": "Year To Date",
"options": "currency", "options": "currency",
"read_only": 1 "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, "istable": 1,
"links": [], "links": [],
"modified": "2021-01-14 13:39:15.847158", "modified": "2021-08-30 13:39:15.847158",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Detail", "name": "Salary Detail",

View File

@ -630,7 +630,8 @@ class SalarySlip(TransactionBase):
get_salary_component_data(additional_salary.component), get_salary_component_data(additional_salary.component),
additional_salary.amount, additional_salary.amount,
component_type, component_type,
additional_salary additional_salary,
is_recurring = additional_salary.is_recurring
) )
def add_tax_components(self, payroll_period): def add_tax_components(self, payroll_period):
@ -651,7 +652,7 @@ class SalarySlip(TransactionBase):
tax_row = get_salary_component_data(d) tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions") 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 component_row = None
for d in self.get(component_type): for d in self.get(component_type):
if d.salary_component != component_data.salary_component: if d.salary_component != component_data.salary_component:
@ -698,6 +699,8 @@ class SalarySlip(TransactionBase):
else: else:
component_row.default_amount = 0 component_row.default_amount = 0
component_row.additional_amount = amount component_row.additional_amount = amount
component_row.is_recurring_additional_salary = is_recurring
component_row.additional_salary = additional_salary.name component_row.additional_salary = additional_salary.name
component_row.deduct_full_tax_on_selected_payroll_date = \ component_row.deduct_full_tax_on_selected_payroll_date = \
additional_salary.deduct_full_tax_on_selected_payroll_date additional_salary.deduct_full_tax_on_selected_payroll_date
@ -894,25 +897,33 @@ class SalarySlip(TransactionBase):
amount, additional_amount = earning.default_amount, earning.additional_amount amount, additional_amount = earning.default_amount, earning.additional_amount
if earning.is_tax_applicable: if earning.is_tax_applicable:
if additional_amount:
taxable_earnings += (amount - additional_amount)
additional_income += additional_amount
if earning.deduct_full_tax_on_selected_payroll_date:
additional_income_with_full_tax += additional_amount
continue
if earning.is_flexible_benefit: if earning.is_flexible_benefit:
flexi_benefits += amount flexi_benefits += amount
else: else:
taxable_earnings += amount 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
if allow_tax_exemption: if allow_tax_exemption:
for ded in self.deductions: for ded in self.deductions:
if ded.exempted_from_income_tax: if ded.exempted_from_income_tax:
amount = ded.amount amount, additional_amount = ded.amount, ded.additional_amount
if based_on_payment_days: if based_on_payment_days:
amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0] amount, additional_amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)
taxable_earnings -= flt(amount)
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({ return frappe._dict({
"taxable_earnings": taxable_earnings, "taxable_earnings": taxable_earnings,
@ -921,11 +932,21 @@ class SalarySlip(TransactionBase):
"flexi_benefits": flexi_benefits "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): def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
amount, additional_amount = row.amount, row.additional_amount amount, additional_amount = row.amount, row.additional_amount
if (self.salary_structure and if (self.salary_structure and
cint(row.depends_on_payment_days) and cint(self.total_working_days) and cint(row.depends_on_payment_days) and cint(self.total_working_days)
(not self.salary_slip_based_on_timesheet or 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 getdate(self.start_date) < joining_date or
(relieving_date and getdate(self.end_date) > relieving_date) (relieving_date and getdate(self.end_date) > relieving_date)
)): )):

View File

@ -536,6 +536,61 @@ class TestSalarySlip(unittest.TestCase):
# undelete fixture data # undelete fixture data
frappe.db.rollback() 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): def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type") activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 50 activity_type.billing_rate = 50
@ -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) salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
return salary_slip 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()