From f9633bbd48649caf6157c5ffbc3a85a9b1de1749 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 23 Feb 2022 14:01:39 +0530 Subject: [PATCH 01/17] feat: Income Tax Computation Report --- .../doctype/salary_slip/salary_slip.py | 106 ++-- .../report/income_tax_computation/__init__.py | 0 .../income_tax_computation.js | 41 ++ .../income_tax_computation.json | 36 ++ .../income_tax_computation.py | 456 ++++++++++++++++++ 5 files changed, 592 insertions(+), 47 deletions(-) create mode 100644 erpnext/payroll/report/income_tax_computation/__init__.py create mode 100644 erpnext/payroll/report/income_tax_computation/income_tax_computation.js create mode 100644 erpnext/payroll/report/income_tax_computation/income_tax_computation.json create mode 100644 erpnext/payroll/report/income_tax_computation/income_tax_computation.py diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 38fecac970..df938ada6a 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -952,8 +952,10 @@ class SalarySlip(TransactionBase): ) # Structured tax amount - total_structured_tax_amount = self.calculate_tax_by_tax_slab( - total_taxable_earnings_without_full_tax_addl_components, tax_slab + eval_locals = self.get_data_for_eval() + total_structured_tax_amount = calculate_tax_by_tax_slab( + total_taxable_earnings_without_full_tax_addl_components, + tax_slab, self.whitelisted_globals, eval_locals ) current_structured_tax_amount = ( total_structured_tax_amount - previous_total_paid_taxes @@ -962,7 +964,8 @@ class SalarySlip(TransactionBase): # Total taxable earnings with additional earnings with full tax full_tax_on_additional_earnings = 0.0 if current_additional_earnings_with_full_tax: - total_tax_amount = self.calculate_tax_by_tax_slab(total_taxable_earnings, tax_slab) + total_tax_amount = calculate_tax_by_tax_slab(total_taxable_earnings, + tax_slab, self.whitelisted_globals, eval_locals) full_tax_on_additional_earnings = total_tax_amount - total_structured_tax_amount current_tax_amount = current_structured_tax_amount + full_tax_on_additional_earnings @@ -1278,50 +1281,6 @@ class SalarySlip(TransactionBase): fields="SUM(amount) as total_amount", )[0].total_amount - def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab): - data = self.get_data_for_eval() - data.update({"annual_taxable_earning": annual_taxable_earning}) - tax_amount = 0 - for slab in tax_slab.slabs: - cond = cstr(slab.condition).strip() - if cond and not self.eval_tax_slab_condition(cond, data): - continue - if not slab.to_amount and annual_taxable_earning >= slab.from_amount: - tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 - continue - if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount: - tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 - elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount: - tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * 0.01 - - # other taxes and charges on income tax - for d in tax_slab.other_taxes_and_charges: - if flt(d.min_taxable_income) and flt(d.min_taxable_income) > annual_taxable_earning: - continue - - if flt(d.max_taxable_income) and flt(d.max_taxable_income) < annual_taxable_earning: - continue - - tax_amount += tax_amount * flt(d.percent) / 100 - - return tax_amount - - def eval_tax_slab_condition(self, condition, data): - try: - condition = condition.strip() - if condition: - return frappe.safe_eval(condition, self.whitelisted_globals, data) - except NameError as err: - frappe.throw( - _("{0}
This error can be due to missing or deleted field.").format(err), - title=_("Name error"), - ) - except SyntaxError as err: - frappe.throw(_("Syntax error in condition: {0}").format(err)) - except Exception as e: - frappe.throw(_("Error in formula or condition: {0}").format(e)) - raise - def get_component_totals(self, component_type, depends_on_payment_days=0): joining_date, relieving_date = frappe.get_cached_value( "Employee", self.employee, ["date_of_joining", "relieving_date"] @@ -1705,3 +1664,56 @@ def get_payroll_payable_account(company, payroll_entry): ) return payroll_payable_account + +def calculate_tax_by_tax_slab(annual_taxable_earning, tax_slab, eval_globals=None, eval_locals=None): + eval_locals.update({"annual_taxable_earning": annual_taxable_earning}) + tax_amount = 0 + for slab in tax_slab.slabs: + cond = cstr(slab.condition).strip() + if cond and not eval_tax_slab_condition(cond, eval_globals, eval_locals): + continue + if not slab.to_amount and annual_taxable_earning >= slab.from_amount: + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 + continue + if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount: + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 + elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount: + tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * 0.01 + + # other taxes and charges on income tax + for d in tax_slab.other_taxes_and_charges: + if flt(d.min_taxable_income) and flt(d.min_taxable_income) > annual_taxable_earning: + continue + + if flt(d.max_taxable_income) and flt(d.max_taxable_income) < annual_taxable_earning: + continue + + tax_amount += tax_amount * flt(d.percent) / 100 + + return tax_amount + +def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None): + if not eval_globals: + eval_globals = { + "int": int, + "float": float, + "long": int, + "round": round, + "date": datetime.date, + "getdate": getdate + } + + try: + condition = condition.strip() + if condition: + return frappe.safe_eval(condition, eval_globals, eval_locals) + except NameError as err: + frappe.throw( + _("{0}
This error can be due to missing or deleted field.").format(err), + title=_("Name error"), + ) + except SyntaxError as err: + frappe.throw(_("Syntax error in condition: {0} in Income Tax Slab").format(err)) + except Exception as e: + frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e)) + raise diff --git a/erpnext/payroll/report/income_tax_computation/__init__.py b/erpnext/payroll/report/income_tax_computation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/report/income_tax_computation/income_tax_computation.js b/erpnext/payroll/report/income_tax_computation/income_tax_computation.js new file mode 100644 index 0000000000..26b09bd2db --- /dev/null +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.js @@ -0,0 +1,41 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Income Tax Computation"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "width": "100px", + "reqd": 1 + }, + { + "fieldname":"payroll_period", + "label": __("Payroll Period"), + "fieldtype": "Link", + "options": "Payroll Period", + "width": "100px", + "reqd": 1 + }, + { + "fieldname":"employee", + "label": __("Employee"), + "fieldtype": "Link", + "options": "Employee", + "width": "100px" + }, + { + "fieldname":"department", + "label": __("Department"), + "fieldtype": "Link", + "options": "Department", + "width": "100px", + } + ] +}; + + diff --git a/erpnext/payroll/report/income_tax_computation/income_tax_computation.json b/erpnext/payroll/report/income_tax_computation/income_tax_computation.json new file mode 100644 index 0000000000..7cb5b2270c --- /dev/null +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.json @@ -0,0 +1,36 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2022-02-17 17:19:30.921422", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "", + "modified": "2022-02-23 13:07:30.347861", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Income Tax Computation", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Salary Slip", + "report_name": "Income Tax Computation", + "report_type": "Script Report", + "roles": [ + { + "role": "Employee" + }, + { + "role": "HR User" + }, + { + "role": "HR Manager" + }, + { + "role": "Employee Self Service" + } + ] +} \ No newline at end of file diff --git a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py new file mode 100644 index 0000000000..6b66ccdcbe --- /dev/null +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -0,0 +1,456 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, scrub +from frappe.query_builder.functions import Sum +from frappe.utils import add_days, flt, getdate + +from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates +from erpnext.payroll.doctype.salary_slip.salary_slip import calculate_tax_by_tax_slab + + +def execute(filters=None): + return IncomeTaxComputationReport(filters).run() + +class IncomeTaxComputationReport(object): + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + self.columns = [] + self.data = [] + self.employees = frappe._dict() + 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"]) + + def run(self): + self.get_fixed_columns() + self.get_data() + return self.columns, self.data + + def get_data(self): + self.get_employee_details() + self.get_future_salary_slips() + self.get_ctc() + self.get_tax_exempted_earnings_and_deductions() + self.get_employee_tax_exemptions() + self.get_hra() + self.get_standard_tax_exemption() + self.add_column("Total Exemption") + self.get_total_taxable_amount() + self.get_applicable_tax() + self.get_total_deducted_tax() + self.get_payable_tax() + + self.data = list(self.employees.values()) + + 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"] + + 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]) + + for d in employees: + if d.employee in list(ss_assignments.keys()): + d.update(ss_assignments[d.employee]) + self.employees.setdefault(d.employee, d) + + def get_employee_filters(self): + filters = { + "company": self.filters.company + } + or_filters = { + "status": "Active", + "relieving_date": ["between", [self.payroll_period_start_date, self.payroll_period_end_date]] + } + if self.filters.employee: + filters = { + "name": self.filters.employee + } + else: + if self.filters.department: + filters.update({ + "department": self.filters.department + }) + if self.filters.designation: + filters.update({ + "designation": self.filters.designation + }) + + return filters, or_filters + + def get_ss_assignments(self, employees): + ss_assignments = frappe.get_all("Salary Structure Assignment", + filters={ + "employee": ["in", employees], + "docstatus": 1, + "salary_structure": ["is", "set"], + "income_tax_slab": ["is", "set"] + }, + fields=["employee", "income_tax_slab", "salary_structure"], + 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) + + 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 + }) + return employee_ss_assignments + + def get_future_salary_slips(self): + self.future_salary_slips = frappe._dict() + for employee in list(self.employees.keys()): + last_ss = self.get_last_salary_slip(employee) + if last_ss and last_ss.end_date == self.payroll_period_end_date: + continue + + relieving_date = self.employees[employee].get("relieving_date", "") + if last_ss: + 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") + }) + + 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") + ss.employee = employee + ss.start_date = ss_start_date + ss.end_date = ss_end_date + ss.salary_structure = last_ss.salary_structure + ss.payroll_frequency = last_ss.payroll_frequency + try: + ss.process_salary_structure(for_preview=1) + self.future_salary_slips.setdefault(employee, []).append(ss.as_dict()) + except Exception: + break + + 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", + { + "employee": employee, + "docstatus": 1, + "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 + ) + + return last_salary_slip + + 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()) + + for employee in list(self.employees.keys()): + future_ss_earnings = self.get_future_earnings(employee) + ctc = flt(existing_ss.get(employee)) + future_ss_earnings + + self.employees[employee].setdefault("ctc", ctc) + + def get_future_earnings(self, employee): + future_earnings = 0.0 + for ss in self.future_salary_slips.get(employee, []): + future_earnings += flt(ss.base_gross_pay) + + return future_earnings + + def get_tax_exempted_earnings_and_deductions(self): + tax_exempted_components = self.get_tax_exempted_components() + + # Get component totals from existing salary slips + ss = frappe.qb.DocType("Salary Slip") + ss_comps = frappe.qb.DocType("Salary Detail") + + records = ( + 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()))) + .where(ss_comps.salary_component.isin(tax_exempted_components)) + .where(ss.start_date >= self.payroll_period_start_date) + .where(ss.end_date <= self.payroll_period_end_date) + .groupby(ss.employee, ss_comps.salary_component) + ).run(as_dict=True) + + existing_ss_exemptions = frappe._dict() + for d in records: + 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"]: + continue + + exemptions = existing_ss_exemptions.get(employee, {}) + self.add_exemptions_from_future_salary_slips(employee, exemptions) + self.employees[employee].update(exemptions) + + total_exemptions = sum(list(exemptions.values())) + self.add_to_total_exemption(employee, total_exemptions) + + def add_exemptions_from_future_salary_slips(self, employee, exemptions): + for ss in self.future_salary_slips.get(employee, []): + for e in ss.earnings: + if not e.is_tax_applicable: + exemptions.setdefault(scrub(e.salary_component), 0) + exemptions[scrub(e.salary_component)] += flt(e.amount) + + for d in ss.deductions: + if d.exempted_from_income_tax: + exemptions.setdefault(scrub(d.salary_component), 0) + exemptions[scrub(d.salary_component)] += flt(d.amount) + + return exemptions + + 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})] + + # 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_components = nontaxable_earning_components + tax_exempted_deduction_components + + # Add columns + for d in tax_exempted_components: + self.add_column(d) + + return tax_exempted_components + + def add_to_total_exemption(self, employee, amount): + self.employees[employee].setdefault("total_exemption", 0) + self.employees[employee]["total_exemption"] += amount + + def get_employee_tax_exemptions(self): + # add columns + exemption_categories = frappe.get_all("Employee Tax Exemption Category", {"is_active": 1}) + for d in exemption_categories: + self.add_column(d.name) + + self.get_tax_exemptions("Employee Tax Exemption Proof Submission") + self.get_tax_exemptions("Employee Tax Exemption Declaration") + + def get_tax_exemptions(self, source): + # Get category-wise exmeptions based on submitted proofs or declarations + if source == "Employee Tax Exemption Proof Submission": + child_doctype = "Employee Tax Exemption Proof Submission Detail" + else: + child_doctype = "Employee Tax Exemption Declaration Category" + + max_exemptions = self.get_max_exemptions_based_on_category() + + par = frappe.qb.DocType(source) + child = frappe.qb.DocType(child_doctype) + + records = ( + 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()))) + .where(par.payroll_period == self.filters.payroll_period) + .groupby(par.employee, child.exemption_category) + ).run(as_dict=True) + + self.employees_with_proofs = [] + for d in records: + if not self.employees[d.employee]["allow_tax_exemption"]: + continue + + if d.employee not in self.employees_with_proofs: + amount = flt(d.amount) + max_eligible_amount = flt(max_exemptions.get(d.exemption_category)) + if max_eligible_amount and amount > max_eligible_amount: + amount = max_eligible_amount + + 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": + 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)) + + def get_hra(self): + if not frappe.get_meta("Employee Tax Exemption Declaration").has_field("monthly_house_rent"): + return + + self.add_column("HRA") + + self.get_eligible_hra("Employee Tax Exemption Proof Submission") + self.get_eligible_hra("Employee Tax Exemption Declaration") + + def get_eligible_hra(self, source): + if source == "Employee Tax Exemption Proof Submission": + hra_amount_field = "total_eligible_hra_exemption" + else: + hra_amount_field = "annual_hra_exemption" + + records = frappe.get_all(source, + filters = { + "docstatus": 1, + "employee": ["in", list(self.employees.keys())], + "payroll_period": self.filters.payroll_period + }, + fields = ["employee", hra_amount_field], as_list=1 + ) + + self.employees_with_proofs = [] + for d in records: + if not self.employees[d[0]]["allow_tax_exemption"]: + continue + + if d[0] not in self.employees_with_proofs: + self.employees[d[0]].setdefault("hra", d[1]) + self.add_to_total_exemption(d[0], d[1]) + + if source == "Employee Tax Exemption Proof Submission": + self.employees_with_proofs.append(d[0]) + + 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)) + + for emp, emp_details in self.employees.items(): + if not self.employees[emp]["allow_tax_exemption"]: + continue + + income_tax_slab = emp_details.get("income_tax_slab") + standard_exemption = standard_exemptions_per_slab.get(income_tax_slab, 0) + emp_details["standard_tax_exemption"] = standard_exemption + self.add_to_total_exemption(emp, standard_exemption) + + 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")) + + def get_applicable_tax(self): + self.add_column("Applicable Tax") + + 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) + else: + tax_amount = 0.0 + + emp_details["applicable_tax"] = tax_amount + + def get_total_deducted_tax(self): + self.add_column("Total Tax Deducted") + + ss = frappe.qb.DocType("Salary Slip") + ss_ded = frappe.qb.DocType("Salary Detail") + + records = ( + 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()))) + .where(ss_ded.parentfield == "deductions") + .where(ss_ded.variable_based_on_taxable_salary == 1) + .where(ss.start_date >= self.payroll_period_start_date) + .where(ss.end_date <= self.payroll_period_end_date) + .groupby(ss.employee) + ).run(as_dict=True) + + for d in records: + self.employees[d.employee].setdefault("total_tax_deducted", d.amount) + + def get_payable_tax(self): + 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")) + + def add_column(self, label, fieldname=None, fieldtype=None, options=None, width=None): + col = { + "label": _(label), + "fieldname": fieldname or scrub(label), + "fieldtype": fieldtype or "Currency", + "options": options, + "width": width or "150px" + } + self.columns.append(col) + + def get_fixed_columns(self): + self.columns = [ + { + "label": _("Employee"), + "fieldname": "employee", + "fieldtype": "Link", + "options": "Employee", + "width": "120px" + }, + { + "label": _("Employee Name"), + "fieldname": "employee_name", + "fieldtype": "Data", + "width": "140px" + }, + { + "label": _("Department"), + "fieldname": "department", + "fieldtype": "Link", + "options": "Department", + "width": "120px" + }, + { + "label": _("Designation"), + "fieldname": "designation", + "fieldtype": "Link", + "options": "Designation", + "width": "120px" + }, + { + "label": _("Date of Joining"), + "fieldname": "date_of_joining", + "fieldtype": "Date" + }, + { + "label": _("CTC"), + "fieldname": "ctc", + "fieldtype": "Currency", + "width": "120px" + }, + ] \ No newline at end of file From 0480bb318e97d1d6cf6ea53feea905a7af50498f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 23 Feb 2022 14:07:24 +0530 Subject: [PATCH 02/17] fix: Modified column width --- .../income_tax_computation/income_tax_computation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 6b66ccdcbe..534eabea4a 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -409,7 +409,7 @@ class IncomeTaxComputationReport(object): "fieldname": fieldname or scrub(label), "fieldtype": fieldtype or "Currency", "options": options, - "width": width or "150px" + "width": width or "140px" } self.columns.append(col) @@ -420,27 +420,27 @@ class IncomeTaxComputationReport(object): "fieldname": "employee", "fieldtype": "Link", "options": "Employee", - "width": "120px" + "width": "140px" }, { "label": _("Employee Name"), "fieldname": "employee_name", "fieldtype": "Data", - "width": "140px" + "width": "160px" }, { "label": _("Department"), "fieldname": "department", "fieldtype": "Link", "options": "Department", - "width": "120px" + "width": "140px" }, { "label": _("Designation"), "fieldname": "designation", "fieldtype": "Link", "options": "Designation", - "width": "120px" + "width": "140px" }, { "label": _("Date of Joining"), @@ -451,6 +451,6 @@ class IncomeTaxComputationReport(object): "label": _("CTC"), "fieldname": "ctc", "fieldtype": "Currency", - "width": "120px" + "width": "140px" }, ] \ No newline at end of file From a9b5d990a4de394d2c019d473152689b662f779d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 23 Feb 2022 14:26:46 +0530 Subject: [PATCH 03/17] fix: Removed designation filter --- .../income_tax_computation.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) 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 534eabea4a..e518a45a28 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -71,15 +71,10 @@ class IncomeTaxComputationReport(object): filters = { "name": self.filters.employee } - else: - if self.filters.department: - filters.update({ - "department": self.filters.department - }) - if self.filters.designation: - filters.update({ - "designation": self.filters.designation - }) + elif self.filters.department: + filters.update({ + "department": self.filters.department + }) return filters, or_filters From a74eec01eae4d6e6bafc536d0dacc78e5987be21 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 23 Feb 2022 14:27:09 +0530 Subject: [PATCH 04/17] fix: Added report link in the Payroll workspace --- erpnext/payroll/workspace/payroll/payroll.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/workspace/payroll/payroll.json b/erpnext/payroll/workspace/payroll/payroll.json index 762bea02c7..5629e63021 100644 --- a/erpnext/payroll/workspace/payroll/payroll.json +++ b/erpnext/payroll/workspace/payroll/payroll.json @@ -245,6 +245,17 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "Salary Structure", + "hidden": 0, + "is_query_report": 1, + "label": "Income Tax Computation", + "link_count": 0, + "link_to": "Income Tax Computation", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Salary Slip", "hidden": 0, @@ -312,7 +323,7 @@ "type": "Link" } ], - "modified": "2022-01-13 17:41:19.098813", + "modified": "2022-02-23 17:41:19.098813", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll", From b4900ef220c8d69d368103f2401d41380804a1c7 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 23 Feb 2022 15:09:21 +0530 Subject: [PATCH 05/17] fix: duplicate exemption amount and rounded tax --- .../income_tax_computation/income_tax_computation.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 e518a45a28..078158e7dc 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -4,7 +4,7 @@ import frappe from frappe import _, scrub from frappe.query_builder.functions import Sum -from frappe.utils import add_days, flt, getdate +from frappe.utils import add_days, flt, getdate, rounded from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates from erpnext.payroll.doctype.salary_slip.salary_slip import calculate_tax_by_tax_slab @@ -254,6 +254,7 @@ class IncomeTaxComputationReport(object): for d in exemption_categories: self.add_column(d.name) + self.employees_with_proofs = [] self.get_tax_exemptions("Employee Tax Exemption Proof Submission") self.get_tax_exemptions("Employee Tax Exemption Declaration") @@ -278,7 +279,6 @@ class IncomeTaxComputationReport(object): .groupby(par.employee, child.exemption_category) ).run(as_dict=True) - self.employees_with_proofs = [] for d in records: if not self.employees[d.employee]["allow_tax_exemption"]: continue @@ -305,6 +305,7 @@ class IncomeTaxComputationReport(object): self.add_column("HRA") + self.employees_with_proofs = [] self.get_eligible_hra("Employee Tax Exemption Proof Submission") self.get_eligible_hra("Employee Tax Exemption Declaration") @@ -323,7 +324,6 @@ class IncomeTaxComputationReport(object): fields = ["employee", hra_amount_field], as_list=1 ) - self.employees_with_proofs = [] for d in records: if not self.employees[d[0]]["allow_tax_exemption"]: continue @@ -359,6 +359,9 @@ class IncomeTaxComputationReport(object): 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") + for emp, emp_details in self.employees.items(): tax_slab = emp_details.get("income_tax_slab") if tax_slab: @@ -369,6 +372,8 @@ class IncomeTaxComputationReport(object): else: tax_amount = 0.0 + if is_tax_rounded: + tax_amount = rounded(tax_amount) emp_details["applicable_tax"] = tax_amount def get_total_deducted_tax(self): From 1f018a912bb0f18415f5c9aaae51f97da86e4dd5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 23 Feb 2022 15:29:06 +0530 Subject: [PATCH 06/17] fix: Get exemptions from declaration only if proof not submitted --- .../income_tax_computation.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) 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 078158e7dc..4dfc63c6e5 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -283,17 +283,21 @@ class IncomeTaxComputationReport(object): if not self.employees[d.employee]["allow_tax_exemption"]: continue - if d.employee not in self.employees_with_proofs: - amount = flt(d.amount) - max_eligible_amount = flt(max_exemptions.get(d.exemption_category)) - if max_eligible_amount and amount > max_eligible_amount: - amount = max_eligible_amount + if (source=="Employee Tax Exemption Declaration" + and d.employee in self.employees_with_proofs): + continue - self.employees[d.employee].setdefault(scrub(d.exemption_category), amount) - self.add_to_total_exemption(d.employee, amount) + amount = flt(d.amount) + max_eligible_amount = flt(max_exemptions.get(d.exemption_category)) + if max_eligible_amount and amount > max_eligible_amount: + amount = max_eligible_amount - if source == "Employee Tax Exemption Proof Submission": - self.employees_with_proofs.append(d.employee) + 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) def get_max_exemptions_based_on_category(self): return dict(frappe.get_all("Employee Tax Exemption Category", @@ -331,8 +335,6 @@ class IncomeTaxComputationReport(object): if d[0] not in self.employees_with_proofs: self.employees[d[0]].setdefault("hra", d[1]) self.add_to_total_exemption(d[0], d[1]) - - if source == "Employee Tax Exemption Proof Submission": self.employees_with_proofs.append(d[0]) def get_standard_tax_exemption(self): From 535217a042e62e55ba5ab36acad0cb8b7ad9cdb2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 23 Feb 2022 15:42:23 +0530 Subject: [PATCH 07/17] fix: Added income tax slab column --- .../income_tax_computation/income_tax_computation.py | 7 +++++++ 1 file changed, 7 insertions(+) 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 4dfc63c6e5..1aa22a1a58 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -449,6 +449,13 @@ class IncomeTaxComputationReport(object): "fieldname": "date_of_joining", "fieldtype": "Date" }, + { + "label": _("Income Tax Slab"), + "fieldname": "income_tax_slab", + "fieldtype": "Link", + "options": "Income Tax Slab", + "width": "140px" + }, { "label": _("CTC"), "fieldname": "ctc", From e3a53590dea2be6ec655ccf75e938c1b587b3d9a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 23 Feb 2022 16:01:50 +0530 Subject: [PATCH 08/17] fix: Added filter to consider Tax Exemption Declaration if proof not submitted --- .../report/income_tax_computation/income_tax_computation.js | 6 ++++++ .../report/income_tax_computation/income_tax_computation.py | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/report/income_tax_computation/income_tax_computation.js b/erpnext/payroll/report/income_tax_computation/income_tax_computation.js index 26b09bd2db..26e463f268 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.js +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.js @@ -34,6 +34,12 @@ frappe.query_reports["Income Tax Computation"] = { "fieldtype": "Link", "options": "Department", "width": "100px", + }, + { + "fieldname":"consider_tax_exemption_declaration", + "label": __("Consider Tax Exemption Declaration"), + "fieldtype": "Check", + "width": "180px" } ] }; 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 1aa22a1a58..cbde3a6095 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -256,7 +256,8 @@ class IncomeTaxComputationReport(object): self.employees_with_proofs = [] self.get_tax_exemptions("Employee Tax Exemption Proof Submission") - self.get_tax_exemptions("Employee Tax Exemption Declaration") + if self.filters.consider_tax_exemption_declaration: + self.get_tax_exemptions("Employee Tax Exemption Declaration") def get_tax_exemptions(self, source): # Get category-wise exmeptions based on submitted proofs or declarations @@ -311,7 +312,8 @@ class IncomeTaxComputationReport(object): self.employees_with_proofs = [] self.get_eligible_hra("Employee Tax Exemption Proof Submission") - self.get_eligible_hra("Employee Tax Exemption Declaration") + if self.filters.consider_tax_exemption_declaration: + self.get_eligible_hra("Employee Tax Exemption Declaration") def get_eligible_hra(self, source): if source == "Employee Tax Exemption Proof Submission": From c7848089ab5878bf175973c53f2391b93577f651 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 11 Apr 2022 14:51:23 +0530 Subject: [PATCH 09/17] tests: Added unit tests for income tax computation report --- .../payroll/doctype/gratuity/test_gratuity.py | 4 +- .../doctype/salary_slip/test_salary_slip.py | 15 +- .../salary_structure/test_salary_structure.py | 6 +- .../income_tax_computation.py | 219 +++++++++++------- .../test_income_tax_computation.py | 116 ++++++++++ 5 files changed, 266 insertions(+), 94 deletions(-) create mode 100644 erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py 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) From 7072dda31f73c5675a4bccb74bbe2452afda1198 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 11 Apr 2022 15:19:42 +0530 Subject: [PATCH 10/17] fix: sider issues --- .../payroll/doctype/salary_slip/salary_slip.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index df938ada6a..192232949a 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -955,7 +955,9 @@ class SalarySlip(TransactionBase): eval_locals = self.get_data_for_eval() total_structured_tax_amount = calculate_tax_by_tax_slab( total_taxable_earnings_without_full_tax_addl_components, - tax_slab, self.whitelisted_globals, eval_locals + tax_slab, + self.whitelisted_globals, + eval_locals, ) current_structured_tax_amount = ( total_structured_tax_amount - previous_total_paid_taxes @@ -964,8 +966,9 @@ class SalarySlip(TransactionBase): # Total taxable earnings with additional earnings with full tax full_tax_on_additional_earnings = 0.0 if current_additional_earnings_with_full_tax: - total_tax_amount = calculate_tax_by_tax_slab(total_taxable_earnings, - tax_slab, self.whitelisted_globals, eval_locals) + total_tax_amount = calculate_tax_by_tax_slab( + total_taxable_earnings, tax_slab, self.whitelisted_globals, eval_locals + ) full_tax_on_additional_earnings = total_tax_amount - total_structured_tax_amount current_tax_amount = current_structured_tax_amount + full_tax_on_additional_earnings @@ -1665,7 +1668,10 @@ def get_payroll_payable_account(company, payroll_entry): return payroll_payable_account -def calculate_tax_by_tax_slab(annual_taxable_earning, tax_slab, eval_globals=None, eval_locals=None): + +def calculate_tax_by_tax_slab( + annual_taxable_earning, tax_slab, eval_globals=None, eval_locals=None +): eval_locals.update({"annual_taxable_earning": annual_taxable_earning}) tax_amount = 0 for slab in tax_slab.slabs: @@ -1692,6 +1698,7 @@ def calculate_tax_by_tax_slab(annual_taxable_earning, tax_slab, eval_globals=Non return tax_amount + def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None): if not eval_globals: eval_globals = { @@ -1700,7 +1707,7 @@ def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None): "long": int, "round": round, "date": datetime.date, - "getdate": getdate + "getdate": getdate, } try: From c27e3ef03eeedb28709b39c052efeb187034d895 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 11 Apr 2022 15:30:26 +0530 Subject: [PATCH 11/17] fix: Show message is no employee found --- .../report/income_tax_computation/income_tax_computation.py | 3 +++ 1 file changed, 3 insertions(+) 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 cf822aac7f..cebf6de342 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -67,6 +67,9 @@ class IncomeTaxComputationReport(object): d.update(ss_assignments[d.employee]) self.employees.setdefault(d.employee, d) + if not self.employees: + frappe.throw(_("No employees found with selected filters and active salary structure")) + def get_employee_filters(self): filters = {"company": self.filters.company} or_filters = { From aad29ad57269b236479907c064b2cfae0eec8be3 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 11 Apr 2022 15:33:22 +0530 Subject: [PATCH 12/17] fix: removed unused imports --- .../income_tax_computation/test_income_tax_computation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 8d0df92814..fd829813de 100644 --- a/erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py @@ -1,10 +1,9 @@ import unittest import frappe -from frappe.utils import add_days, getdate +from frappe.utils import 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 e46898f5f5e3d9f1818a6e7c663a21b3122eaf6b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 11 Apr 2022 16:37:34 +0530 Subject: [PATCH 13/17] fix: test cases --- .../test_income_tax_computation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 index fd829813de..57ca317160 100644 --- a/erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/test_income_tax_computation.py @@ -25,11 +25,12 @@ class TestIncomeTaxComputation(unittest.TestCase): 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 Tax Exemption Declaration`") + frappe.db.sql("delete from `tabPayroll Period`") + frappe.db.sql("delete from `tabIncome Tax Slab`") + 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`") @@ -47,7 +48,6 @@ class TestIncomeTaxComputation(unittest.TestCase): 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", ) From 67086e618d8822708ee466137ea44086269acd52 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 14 Apr 2022 14:00:28 +0530 Subject: [PATCH 14/17] fix: get enabled earning components Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- .../report/income_tax_computation/income_tax_computation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cebf6de342..51417e4757 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -250,7 +250,7 @@ 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}) + d.name for d in frappe.get_all("Salary Component", {"type": "Earning", "is_tax_applicable": 0, "disabled": 0}) ] # tax exempted deduction components From d06b7378f8cfae3fed3877109542e1b08fe861f2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 14 Apr 2022 14:01:44 +0530 Subject: [PATCH 15/17] fix: get enabled and submitted income tax slab Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- .../report/income_tax_computation/income_tax_computation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 51417e4757..98c22b59ff 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -381,7 +381,7 @@ class IncomeTaxComputationReport(object): standard_exemptions_per_slab = dict( frappe.get_all( "Income Tax Slab", - filters={"company": self.filters.company}, + filters={"company": self.filters.company, "docstatus": 1, "disabled": 0}, fields=["name", "standard_tax_exemption_amount"], as_list=1, ) From bc7007d5889aba30015aadfb95e921a1cb933dfc Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 14 Apr 2022 14:02:01 +0530 Subject: [PATCH 16/17] fix: get enabled deduction components Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- .../report/income_tax_computation/income_tax_computation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 98c22b59ff..10670f81dc 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -257,7 +257,7 @@ class IncomeTaxComputationReport(object): tax_exempted_deduction_components = [ d.name for d in frappe.get_all( - "Salary Component", {"type": "Deduction", "exempted_from_income_tax": 1} + "Salary Component", {"type": "Deduction", "exempted_from_income_tax": 1, "disabled": 0} ) ] From cd2ab322420d7fded2d0a2e1969ba280f1bec1e1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 14 Apr 2022 14:05:21 +0530 Subject: [PATCH 17/17] fix: orginised code --- .../income_tax_computation/income_tax_computation.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 10670f81dc..739ed8eb2d 100644 --- a/erpnext/payroll/report/income_tax_computation/income_tax_computation.py +++ b/erpnext/payroll/report/income_tax_computation/income_tax_computation.py @@ -40,7 +40,6 @@ class IncomeTaxComputationReport(object): self.get_employee_tax_exemptions() self.get_hra() self.get_standard_tax_exemption() - self.add_column("Total Exemption") self.get_total_taxable_amount() self.get_applicable_tax() self.get_total_deducted_tax() @@ -250,7 +249,10 @@ 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, "disabled": 0}) + d.name + for d in frappe.get_all( + "Salary Component", {"type": "Earning", "is_tax_applicable": 0, "disabled": 0} + ) ] # tax exempted deduction components @@ -396,6 +398,8 @@ class IncomeTaxComputationReport(object): emp_details["standard_tax_exemption"] = standard_exemption self.add_to_total_exemption(emp, standard_exemption) + self.add_column("Total Exemption") + def get_total_taxable_amount(self): self.add_column("Total Taxable Amount") for emp, emp_details in self.employees.items():