tests: Added unit tests for income tax computation report
diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py
index 67bb447..aa03d80 100644
--- a/erpnext/payroll/doctype/gratuity/test_gratuity.py
+++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py
@@ -24,7 +24,9 @@
frappe.db.delete("Gratuity")
frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
- make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
+ make_earning_salary_component(
+ setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True
+ )
make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
def test_get_last_salary_slip_should_return_none_for_new_employee(self):
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index dbeadc5..869ea83 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -772,6 +772,7 @@
"Monthly",
other_details={"max_benefits": 100000},
test_tax=True,
+ include_flexi_benefits=True,
employee=employee,
payroll_period=payroll_period,
)
@@ -875,6 +876,7 @@
"Monthly",
other_details={"max_benefits": 100000},
test_tax=True,
+ include_flexi_benefits=True,
employee=employee,
payroll_period=payroll_period,
)
@@ -1022,7 +1024,9 @@
return account
-def make_earning_salary_component(setup=False, test_tax=False, company_list=None):
+def make_earning_salary_component(
+ setup=False, test_tax=False, company_list=None, include_flexi_benefits=False
+):
data = [
{
"salary_component": "Basic Salary",
@@ -1043,7 +1047,7 @@
},
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
]
- if test_tax:
+ if include_flexi_benefits:
data.extend(
[
{
@@ -1063,11 +1067,18 @@
"type": "Earning",
"max_benefit_amount": 15000,
},
+ ]
+ )
+ if test_tax:
+ data.extend(
+ [
{"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"},
]
)
+
if setup or test_tax:
make_salary_component(data, test_tax, company_list)
+
data.append(
{
"salary_component": "Basic Salary",
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index def622b..2eb1671 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -149,6 +149,7 @@
company=None,
currency=erpnext.get_default_currency(),
payroll_period=None,
+ include_flexi_benefits=False,
):
if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
@@ -161,7 +162,10 @@
"name": salary_structure,
"company": company or erpnext.get_default_company(),
"earnings": make_earning_salary_component(
- setup=True, test_tax=test_tax, company_list=["_Test Company"]
+ setup=True,
+ test_tax=test_tax,
+ company_list=["_Test Company"],
+ include_flexi_benefits=include_flexi_benefits,
),
"deductions": make_deduction_salary_component(
setup=True, test_tax=test_tax, company_list=["_Test Company"]
diff --git a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py
index cbde3a6..cf822aa 100644
--- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py
+++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py
@@ -13,6 +13,7 @@
def execute(filters=None):
return IncomeTaxComputationReport(filters).run()
+
class IncomeTaxComputationReport(object):
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
@@ -22,8 +23,9 @@
self.payroll_period_start_date = None
self.payroll_period_end_date = None
if self.filters.payroll_period:
- self.payroll_period_start_date, self.payroll_period_end_date = \
- frappe.db.get_value("Payroll Period", self.filters.payroll_period, ["start_date", "end_date"])
+ self.payroll_period_start_date, self.payroll_period_end_date = frappe.db.get_value(
+ "Payroll Period", self.filters.payroll_period, ["start_date", "end_date"]
+ )
def run(self):
self.get_fixed_columns()
@@ -48,8 +50,14 @@
def get_employee_details(self):
filters, or_filters = self.get_employee_filters()
- fields = ["name as employee", "employee_name", "department",
- "designation", "date_of_joining", "relieving_date"]
+ fields = [
+ "name as employee",
+ "employee_name",
+ "department",
+ "designation",
+ "date_of_joining",
+ "relieving_date",
+ ]
employees = frappe.get_all("Employee", filters=filters, or_filters=or_filters, fields=fields)
ss_assignments = self.get_ss_assignments([d.employee for d in employees])
@@ -60,48 +68,47 @@
self.employees.setdefault(d.employee, d)
def get_employee_filters(self):
- filters = {
- "company": self.filters.company
- }
+ filters = {"company": self.filters.company}
or_filters = {
"status": "Active",
- "relieving_date": ["between", [self.payroll_period_start_date, self.payroll_period_end_date]]
+ "relieving_date": ["between", [self.payroll_period_start_date, self.payroll_period_end_date]],
}
if self.filters.employee:
- filters = {
- "name": self.filters.employee
- }
+ filters = {"name": self.filters.employee}
elif self.filters.department:
- filters.update({
- "department": self.filters.department
- })
+ filters.update({"department": self.filters.department})
return filters, or_filters
def get_ss_assignments(self, employees):
- ss_assignments = frappe.get_all("Salary Structure Assignment",
+ ss_assignments = frappe.get_all(
+ "Salary Structure Assignment",
filters={
"employee": ["in", employees],
"docstatus": 1,
"salary_structure": ["is", "set"],
- "income_tax_slab": ["is", "set"]
+ "income_tax_slab": ["is", "set"],
},
fields=["employee", "income_tax_slab", "salary_structure"],
- order_by="from_date desc"
+ order_by="from_date desc",
)
employee_ss_assignments = frappe._dict()
for d in ss_assignments:
if d.employee not in list(employee_ss_assignments.keys()):
- tax_slab = frappe.get_cached_value("Income Tax Slab",
- d.income_tax_slab, ["allow_tax_exemption", "disabled"], as_dict=1)
+ tax_slab = frappe.get_cached_value(
+ "Income Tax Slab", d.income_tax_slab, ["allow_tax_exemption", "disabled"], as_dict=1
+ )
if tax_slab and not tax_slab.disabled:
- employee_ss_assignments.setdefault(d.employee, {
- "salary_structure": d.salary_structure,
- "income_tax_slab": d.income_tax_slab,
- "allow_tax_exemption": tax_slab.allow_tax_exemption
- })
+ employee_ss_assignments.setdefault(
+ d.employee,
+ {
+ "salary_structure": d.salary_structure,
+ "income_tax_slab": d.income_tax_slab,
+ "allow_tax_exemption": tax_slab.allow_tax_exemption,
+ },
+ )
return employee_ss_assignments
def get_future_salary_slips(self):
@@ -116,13 +123,16 @@
ss_start_date = add_days(last_ss.end_date, 1)
else:
ss_start_date = self.payroll_period_start_date
- last_ss = frappe._dict({
- "payroll_frequency": "Monthly",
- "salary_structure": self.employees[employee].get("salary_structure")
- })
+ last_ss = frappe._dict(
+ {
+ "payroll_frequency": "Monthly",
+ "salary_structure": self.employees[employee].get("salary_structure"),
+ }
+ )
- while(getdate(ss_start_date) < getdate(self.payroll_period_end_date)
- and (not relieving_date or getdate(ss_start_date) < relieving_date)):
+ while getdate(ss_start_date) < getdate(self.payroll_period_end_date) and (
+ not relieving_date or getdate(ss_start_date) < relieving_date
+ ):
ss_end_date = get_start_end_dates(last_ss.payroll_frequency, ss_start_date).end_date
ss = frappe.new_doc("Salary Slip")
@@ -131,6 +141,7 @@
ss.end_date = ss_end_date
ss.salary_structure = last_ss.salary_structure
ss.payroll_frequency = last_ss.payroll_frequency
+ ss.company = self.filters.company
try:
ss.process_salary_structure(for_preview=1)
self.future_salary_slips.setdefault(employee, []).append(ss.as_dict())
@@ -140,15 +151,16 @@
ss_start_date = add_days(ss_end_date, 1)
def get_last_salary_slip(self, employee):
- last_salary_slip = frappe.db.get_value("Salary Slip",
+ last_salary_slip = frappe.db.get_value(
+ "Salary Slip",
{
"employee": employee,
"docstatus": 1,
- "start_date": ["between", [self.payroll_period_start_date, self.payroll_period_end_date]]
+ "start_date": ["between", [self.payroll_period_start_date, self.payroll_period_end_date]],
},
["start_date", "end_date", "salary_structure", "payroll_frequency"],
order_by="start_date desc",
- as_dict=1
+ as_dict=1,
)
return last_salary_slip
@@ -156,15 +168,17 @@
def get_ctc(self):
# Get total earnings from existing salary slip
ss = frappe.qb.DocType("Salary Slip")
- existing_ss = frappe._dict((
- frappe.qb.from_(ss)
- .select(ss.employee, Sum(ss.base_gross_pay).as_("amount"))
- .where(ss.docstatus == 1)
- .where(ss.employee.isin(list(self.employees.keys())))
- .where(ss.start_date >= self.payroll_period_start_date)
- .where(ss.end_date <= self.payroll_period_end_date)
- .groupby(ss.employee)
- ).run())
+ existing_ss = frappe._dict(
+ (
+ frappe.qb.from_(ss)
+ .select(ss.employee, Sum(ss.base_gross_pay).as_("amount"))
+ .where(ss.docstatus == 1)
+ .where(ss.employee.isin(list(self.employees.keys())))
+ .where(ss.start_date >= self.payroll_period_start_date)
+ .where(ss.end_date <= self.payroll_period_end_date)
+ .groupby(ss.employee)
+ ).run()
+ )
for employee in list(self.employees.keys()):
future_ss_earnings = self.get_future_earnings(employee)
@@ -187,7 +201,9 @@
ss_comps = frappe.qb.DocType("Salary Detail")
records = (
- frappe.qb.from_(ss).inner_join(ss_comps).on(ss.name == ss_comps.parent)
+ frappe.qb.from_(ss)
+ .inner_join(ss_comps)
+ .on(ss.name == ss_comps.parent)
.select(ss.name, ss.employee, ss_comps.salary_component, Sum(ss_comps.amount).as_("amount"))
.where(ss.docstatus == 1)
.where(ss.employee.isin(list(self.employees.keys())))
@@ -199,8 +215,9 @@
existing_ss_exemptions = frappe._dict()
for d in records:
- existing_ss_exemptions.setdefault(d.employee, {})\
- .setdefault(scrub(d.salary_component), d.amount)
+ existing_ss_exemptions.setdefault(d.employee, {}).setdefault(
+ scrub(d.salary_component), d.amount
+ )
for employee in list(self.employees.keys()):
if not self.employees[employee]["allow_tax_exemption"]:
@@ -229,12 +246,17 @@
def get_tax_exempted_components(self):
# nontaxable earning components
- nontaxable_earning_components = [d.name for d in frappe.get_all("Salary Component",
- {"type": "Earning", "is_tax_applicable": 0})]
+ nontaxable_earning_components = [
+ d.name for d in frappe.get_all("Salary Component", {"type": "Earning", "is_tax_applicable": 0})
+ ]
# tax exempted deduction components
- tax_exempted_deduction_components = [d.name for d in frappe.get_all("Salary Component",
- {"type": "Deduction", "exempted_from_income_tax": 1})]
+ tax_exempted_deduction_components = [
+ d.name
+ for d in frappe.get_all(
+ "Salary Component", {"type": "Deduction", "exempted_from_income_tax": 1}
+ )
+ ]
tax_exempted_components = nontaxable_earning_components + tax_exempted_deduction_components
@@ -272,7 +294,9 @@
child = frappe.qb.DocType(child_doctype)
records = (
- frappe.qb.from_(par).inner_join(child).on(par.name == child.parent)
+ frappe.qb.from_(par)
+ .inner_join(child)
+ .on(par.name == child.parent)
.select(par.employee, child.exemption_category, Sum(child.amount).as_("amount"))
.where(par.docstatus == 1)
.where(par.employee.isin(list(self.employees.keys())))
@@ -284,9 +308,8 @@
if not self.employees[d.employee]["allow_tax_exemption"]:
continue
- if (source=="Employee Tax Exemption Declaration"
- and d.employee in self.employees_with_proofs):
- continue
+ if source == "Employee Tax Exemption Declaration" and d.employee in self.employees_with_proofs:
+ continue
amount = flt(d.amount)
max_eligible_amount = flt(max_exemptions.get(d.exemption_category))
@@ -296,13 +319,21 @@
self.employees[d.employee].setdefault(scrub(d.exemption_category), amount)
self.add_to_total_exemption(d.employee, amount)
- if (source == "Employee Tax Exemption Proof Submission"
- and d.employee not in self.employees_with_proofs):
- self.employees_with_proofs.append(d.employee)
+ if (
+ source == "Employee Tax Exemption Proof Submission"
+ and d.employee not in self.employees_with_proofs
+ ):
+ self.employees_with_proofs.append(d.employee)
def get_max_exemptions_based_on_category(self):
- return dict(frappe.get_all("Employee Tax Exemption Category",
- filters={"is_active": 1}, fields=["name", "max_amount"], as_list=1))
+ return dict(
+ frappe.get_all(
+ "Employee Tax Exemption Category",
+ filters={"is_active": 1},
+ fields=["name", "max_amount"],
+ as_list=1,
+ )
+ )
def get_hra(self):
if not frappe.get_meta("Employee Tax Exemption Declaration").has_field("monthly_house_rent"):
@@ -321,13 +352,15 @@
else:
hra_amount_field = "annual_hra_exemption"
- records = frappe.get_all(source,
- filters = {
+ records = frappe.get_all(
+ source,
+ filters={
"docstatus": 1,
"employee": ["in", list(self.employees.keys())],
- "payroll_period": self.filters.payroll_period
+ "payroll_period": self.filters.payroll_period,
},
- fields = ["employee", hra_amount_field], as_list=1
+ fields=["employee", hra_amount_field],
+ as_list=1,
)
for d in records:
@@ -342,9 +375,14 @@
def get_standard_tax_exemption(self):
self.add_column("Standard Tax Exemption")
- standard_exemptions_per_slab = dict(frappe.get_all("Income Tax Slab",
- filters={"company": self.filters.company},
- fields=["name", "standard_tax_exemption_amount"], as_list=1))
+ standard_exemptions_per_slab = dict(
+ frappe.get_all(
+ "Income Tax Slab",
+ filters={"company": self.filters.company},
+ fields=["name", "standard_tax_exemption_amount"],
+ as_list=1,
+ )
+ )
for emp, emp_details in self.employees.items():
if not self.employees[emp]["allow_tax_exemption"]:
@@ -358,21 +396,27 @@
def get_total_taxable_amount(self):
self.add_column("Total Taxable Amount")
for emp, emp_details in self.employees.items():
- emp_details["total_taxable_amount"] = flt(emp_details.get("ctc")) - flt(emp_details.get("total_exemption"))
+ emp_details["total_taxable_amount"] = flt(emp_details.get("ctc")) - flt(
+ emp_details.get("total_exemption")
+ )
def get_applicable_tax(self):
self.add_column("Applicable Tax")
- is_tax_rounded = frappe.db.get_value("Salary Component",
- {"variable_based_on_taxable_salary": 1, "disabled": 0}, "round_to_the_nearest_integer")
+ is_tax_rounded = frappe.db.get_value(
+ "Salary Component",
+ {"variable_based_on_taxable_salary": 1, "disabled": 0},
+ "round_to_the_nearest_integer",
+ )
for emp, emp_details in self.employees.items():
tax_slab = emp_details.get("income_tax_slab")
if tax_slab:
tax_slab = frappe.get_cached_doc("Income Tax Slab", tax_slab)
employee_dict = frappe.get_doc("Employee", emp).as_dict()
- tax_amount = calculate_tax_by_tax_slab(emp_details["total_taxable_amount"],
- tax_slab, eval_globals=None, eval_locals=employee_dict)
+ tax_amount = calculate_tax_by_tax_slab(
+ emp_details["total_taxable_amount"], tax_slab, eval_globals=None, eval_locals=employee_dict
+ )
else:
tax_amount = 0.0
@@ -387,7 +431,9 @@
ss_ded = frappe.qb.DocType("Salary Detail")
records = (
- frappe.qb.from_(ss).inner_join(ss_ded).on(ss.name == ss_ded.parent)
+ frappe.qb.from_(ss)
+ .inner_join(ss_ded)
+ .on(ss.name == ss_ded.parent)
.select(ss.employee, Sum(ss_ded.amount).as_("amount"))
.where(ss.docstatus == 1)
.where(ss.employee.isin(list(self.employees.keys())))
@@ -405,7 +451,9 @@
self.add_column("Payable Tax")
for emp, emp_details in self.employees.items():
- emp_details["payable_tax"] = flt(emp_details.get("applicable_tax")) - flt(emp_details.get("total_tax_deducted"))
+ emp_details["payable_tax"] = flt(emp_details.get("applicable_tax")) - flt(
+ emp_details.get("total_tax_deducted")
+ )
def add_column(self, label, fieldname=None, fieldtype=None, options=None, width=None):
col = {
@@ -413,7 +461,7 @@
"fieldname": fieldname or scrub(label),
"fieldtype": fieldtype or "Currency",
"options": options,
- "width": width or "140px"
+ "width": width or "140px",
}
self.columns.append(col)
@@ -424,44 +472,35 @@
"fieldname": "employee",
"fieldtype": "Link",
"options": "Employee",
- "width": "140px"
+ "width": "140px",
},
{
"label": _("Employee Name"),
"fieldname": "employee_name",
"fieldtype": "Data",
- "width": "160px"
+ "width": "160px",
},
{
"label": _("Department"),
"fieldname": "department",
"fieldtype": "Link",
"options": "Department",
- "width": "140px"
+ "width": "140px",
},
{
"label": _("Designation"),
"fieldname": "designation",
"fieldtype": "Link",
"options": "Designation",
- "width": "140px"
+ "width": "140px",
},
- {
- "label": _("Date of Joining"),
- "fieldname": "date_of_joining",
- "fieldtype": "Date"
- },
+ {"label": _("Date of Joining"), "fieldname": "date_of_joining", "fieldtype": "Date"},
{
"label": _("Income Tax Slab"),
"fieldname": "income_tax_slab",
"fieldtype": "Link",
"options": "Income Tax Slab",
- "width": "140px"
+ "width": "140px",
},
- {
- "label": _("CTC"),
- "fieldname": "ctc",
- "fieldtype": "Currency",
- "width": "140px"
- },
- ]
\ No newline at end of file
+ {"label": _("CTC"), "fieldname": "ctc", "fieldtype": "Currency", "width": "140px"},
+ ]
diff --git a/erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py b/erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py
new file mode 100644
index 0000000..8d0df92
--- /dev/null
+++ b/erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py
@@ -0,0 +1,116 @@
+import unittest
+
+import frappe
+from frappe.utils import add_days, getdate
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.report.employee_exits.test_employee_exits import create_company
+from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
+ create_payroll_period,
+)
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ create_exemption_declaration,
+ create_salary_slips_for_payroll_period,
+ create_tax_slab,
+)
+from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+from erpnext.payroll.report.income_tax_computation.income_tax_computation import execute
+
+
+class TestIncomeTaxComputation(unittest.TestCase):
+ def setUp(self):
+ self.cleanup_records()
+ self.create_records()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def cleanup_records(self):
+ frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""")
+ frappe.db.sql("""delete from `tabPayroll Period`""")
+ frappe.db.sql("""delete from `tabSalary Component`""")
+ frappe.db.sql("""delete from `tabEmployee Benefit Application`""")
+ frappe.db.sql("""delete from `tabEmployee Benefit Claim`""")
+ frappe.db.sql("delete from `tabEmployee` where company='_Test Company'")
+ frappe.db.sql("delete from `tabSalary Slip`")
+
+ def create_records(self):
+ self.employee = make_employee(
+ "employee_tax_computation@example.com",
+ company="_Test Company",
+ date_of_joining=getdate("01-10-2021"),
+ )
+
+ self.payroll_period = create_payroll_period(
+ name="_Test Payroll Period 1", company="_Test Company"
+ )
+
+ self.income_tax_slab = create_tax_slab(
+ self.payroll_period,
+ allow_tax_exemption=True,
+ currency="INR",
+ effective_date=getdate("2019-04-01"),
+ company="_Test Company",
+ )
+ salary_structure = make_salary_structure(
+ "Monthly Salary Structure Test Income Tax Computation",
+ "Monthly",
+ employee=self.employee,
+ company="_Test Company",
+ currency="INR",
+ payroll_period=self.payroll_period,
+ test_tax=True,
+ )
+
+ create_exemption_declaration(self.employee, self.payroll_period.name)
+
+ create_salary_slips_for_payroll_period(
+ self.employee, salary_structure.name, self.payroll_period, deduct_random=False, num=3
+ )
+
+ def test_report(self):
+ filters = frappe._dict(
+ {
+ "company": "_Test Company",
+ "payroll_period": self.payroll_period.name,
+ "employee": self.employee,
+ }
+ )
+
+ result = execute(filters)
+
+ expected_data = {
+ "employee": self.employee,
+ "employee_name": "employee_tax_computation@example.com",
+ "department": "All Departments",
+ "income_tax_slab": self.income_tax_slab,
+ "ctc": 936000.0,
+ "professional_tax": 2400.0,
+ "standard_tax_exemption": 50000,
+ "total_exemption": 52400.0,
+ "total_taxable_amount": 883600.0,
+ "applicable_tax": 92789.0,
+ "total_tax_deducted": 17997.0,
+ "payable_tax": 74792,
+ }
+
+ for key, val in expected_data.items():
+ self.assertEqual(result[1][0].get(key), val)
+
+ # Run report considering tax exemption declaration
+ filters.consider_tax_exemption_declaration = 1
+
+ result = execute(filters)
+
+ expected_data.update(
+ {
+ "_test_category": 100000.0,
+ "total_exemption": 152400.0,
+ "total_taxable_amount": 783600.0,
+ "applicable_tax": 71989.0,
+ "payable_tax": 53992.0,
+ }
+ )
+
+ for key, val in expected_data.items():
+ self.assertEqual(result[1][0].get(key), val)