diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 67bb447e91..aa03d80d63 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -24,7 +24,9 @@ class TestGratuity(unittest.TestCase): 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 dbeadc5900..869ea83f27 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 @@ class TestSalarySlip(unittest.TestCase): "Monthly", other_details={"max_benefits": 100000}, test_tax=True, + include_flexi_benefits=True, employee=employee, payroll_period=payroll_period, ) @@ -875,6 +876,7 @@ class TestSalarySlip(unittest.TestCase): "Monthly", other_details={"max_benefits": 100000}, test_tax=True, + include_flexi_benefits=True, employee=employee, payroll_period=payroll_period, ) @@ -1022,7 +1024,9 @@ def create_account(account_name, company, parent_account, account_type=None): 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 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None }, {"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"}, ] - if test_tax: + if include_flexi_benefits: data.extend( [ { @@ -1063,11 +1067,18 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None "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 def622bf80..2eb16711b1 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 @@ def make_salary_structure( 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 @@ def make_salary_structure( "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 cbde3a6095..cf822aac7f 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 @@ from erpnext.payroll.doctype.salary_slip.salary_slip import calculate_tax_by_tax 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): 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 @@ class IncomeTaxComputationReport(object): "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 @@ class IncomeTaxComputationReport(object): "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" - }, - { - "label": _("Date of Joining"), - "fieldname": "date_of_joining", - "fieldtype": "Date" + "width": "140px", }, + {"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 0000000000..8d0df92814 --- /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)