Merge pull request #29963 from nabinhait/income-tax-computation
feat: Income tax computation Report
This commit is contained in:
commit
1659f1eb6d
@ -24,7 +24,9 @@ class TestGratuity(unittest.TestCase):
|
|||||||
frappe.db.delete("Gratuity")
|
frappe.db.delete("Gratuity")
|
||||||
frappe.db.delete("Additional Salary", {"ref_doctype": "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"])
|
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):
|
def test_get_last_salary_slip_should_return_none_for_new_employee(self):
|
||||||
|
@ -952,8 +952,12 @@ class SalarySlip(TransactionBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Structured tax amount
|
# Structured tax amount
|
||||||
total_structured_tax_amount = self.calculate_tax_by_tax_slab(
|
eval_locals = self.get_data_for_eval()
|
||||||
total_taxable_earnings_without_full_tax_addl_components, tax_slab
|
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 = (
|
current_structured_tax_amount = (
|
||||||
total_structured_tax_amount - previous_total_paid_taxes
|
total_structured_tax_amount - previous_total_paid_taxes
|
||||||
@ -962,7 +966,9 @@ class SalarySlip(TransactionBase):
|
|||||||
# Total taxable earnings with additional earnings with full tax
|
# Total taxable earnings with additional earnings with full tax
|
||||||
full_tax_on_additional_earnings = 0.0
|
full_tax_on_additional_earnings = 0.0
|
||||||
if current_additional_earnings_with_full_tax:
|
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
|
full_tax_on_additional_earnings = total_tax_amount - total_structured_tax_amount
|
||||||
|
|
||||||
current_tax_amount = current_structured_tax_amount + full_tax_on_additional_earnings
|
current_tax_amount = current_structured_tax_amount + full_tax_on_additional_earnings
|
||||||
@ -1278,50 +1284,6 @@ class SalarySlip(TransactionBase):
|
|||||||
fields="SUM(amount) as total_amount",
|
fields="SUM(amount) as total_amount",
|
||||||
)[0].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} <br> 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):
|
def get_component_totals(self, component_type, depends_on_payment_days=0):
|
||||||
joining_date, relieving_date = frappe.get_cached_value(
|
joining_date, relieving_date = frappe.get_cached_value(
|
||||||
"Employee", self.employee, ["date_of_joining", "relieving_date"]
|
"Employee", self.employee, ["date_of_joining", "relieving_date"]
|
||||||
@ -1705,3 +1667,60 @@ def get_payroll_payable_account(company, payroll_entry):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return payroll_payable_account
|
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} <br> 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
|
||||||
|
@ -772,6 +772,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
"Monthly",
|
"Monthly",
|
||||||
other_details={"max_benefits": 100000},
|
other_details={"max_benefits": 100000},
|
||||||
test_tax=True,
|
test_tax=True,
|
||||||
|
include_flexi_benefits=True,
|
||||||
employee=employee,
|
employee=employee,
|
||||||
payroll_period=payroll_period,
|
payroll_period=payroll_period,
|
||||||
)
|
)
|
||||||
@ -875,6 +876,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
"Monthly",
|
"Monthly",
|
||||||
other_details={"max_benefits": 100000},
|
other_details={"max_benefits": 100000},
|
||||||
test_tax=True,
|
test_tax=True,
|
||||||
|
include_flexi_benefits=True,
|
||||||
employee=employee,
|
employee=employee,
|
||||||
payroll_period=payroll_period,
|
payroll_period=payroll_period,
|
||||||
)
|
)
|
||||||
@ -1022,7 +1024,9 @@ def create_account(account_name, company, parent_account, account_type=None):
|
|||||||
return account
|
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 = [
|
data = [
|
||||||
{
|
{
|
||||||
"salary_component": "Basic Salary",
|
"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"},
|
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
|
||||||
]
|
]
|
||||||
if test_tax:
|
if include_flexi_benefits:
|
||||||
data.extend(
|
data.extend(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@ -1063,11 +1067,18 @@ def make_earning_salary_component(setup=False, test_tax=False, company_list=None
|
|||||||
"type": "Earning",
|
"type": "Earning",
|
||||||
"max_benefit_amount": 15000,
|
"max_benefit_amount": 15000,
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if test_tax:
|
||||||
|
data.extend(
|
||||||
|
[
|
||||||
{"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"},
|
{"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if setup or test_tax:
|
if setup or test_tax:
|
||||||
make_salary_component(data, test_tax, company_list)
|
make_salary_component(data, test_tax, company_list)
|
||||||
|
|
||||||
data.append(
|
data.append(
|
||||||
{
|
{
|
||||||
"salary_component": "Basic Salary",
|
"salary_component": "Basic Salary",
|
||||||
|
@ -149,6 +149,7 @@ def make_salary_structure(
|
|||||||
company=None,
|
company=None,
|
||||||
currency=erpnext.get_default_currency(),
|
currency=erpnext.get_default_currency(),
|
||||||
payroll_period=None,
|
payroll_period=None,
|
||||||
|
include_flexi_benefits=False,
|
||||||
):
|
):
|
||||||
if test_tax:
|
if test_tax:
|
||||||
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
|
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
|
||||||
@ -161,7 +162,10 @@ def make_salary_structure(
|
|||||||
"name": salary_structure,
|
"name": salary_structure,
|
||||||
"company": company or erpnext.get_default_company(),
|
"company": company or erpnext.get_default_company(),
|
||||||
"earnings": make_earning_salary_component(
|
"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(
|
"deductions": make_deduction_salary_component(
|
||||||
setup=True, test_tax=test_tax, company_list=["_Test Company"]
|
setup=True, test_tax=test_tax, company_list=["_Test Company"]
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
// 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",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"consider_tax_exemption_declaration",
|
||||||
|
"label": __("Consider Tax Exemption Declaration"),
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"width": "180px"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,513 @@
|
|||||||
|
# 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, 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
|
||||||
|
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
"status": "Active",
|
||||||
|
"relieving_date": ["between", [self.payroll_period_start_date, self.payroll_period_end_date]],
|
||||||
|
}
|
||||||
|
if self.filters.employee:
|
||||||
|
filters = {"name": self.filters.employee}
|
||||||
|
elif 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",
|
||||||
|
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
|
||||||
|
ss.company = self.filters.company
|
||||||
|
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, "disabled": 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, "disabled": 0}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
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.employees_with_proofs = []
|
||||||
|
self.get_tax_exemptions("Employee Tax Exemption Proof Submission")
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
for d in records:
|
||||||
|
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
|
||||||
|
|
||||||
|
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"
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_hra(self):
|
||||||
|
if not frappe.get_meta("Employee Tax Exemption Declaration").has_field("monthly_house_rent"):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.add_column("HRA")
|
||||||
|
|
||||||
|
self.employees_with_proofs = []
|
||||||
|
self.get_eligible_hra("Employee Tax Exemption Proof Submission")
|
||||||
|
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":
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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])
|
||||||
|
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, "docstatus": 1, "disabled": 0},
|
||||||
|
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)
|
||||||
|
|
||||||
|
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():
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if is_tax_rounded:
|
||||||
|
tax_amount = rounded(tax_amount)
|
||||||
|
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 "140px",
|
||||||
|
}
|
||||||
|
self.columns.append(col)
|
||||||
|
|
||||||
|
def get_fixed_columns(self):
|
||||||
|
self.columns = [
|
||||||
|
{
|
||||||
|
"label": _("Employee"),
|
||||||
|
"fieldname": "employee",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Employee",
|
||||||
|
"width": "140px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Employee Name"),
|
||||||
|
"fieldname": "employee_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": "160px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Department"),
|
||||||
|
"fieldname": "department",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Department",
|
||||||
|
"width": "140px",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Designation"),
|
||||||
|
"fieldname": "designation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Designation",
|
||||||
|
"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",
|
||||||
|
},
|
||||||
|
{"label": _("CTC"), "fieldname": "ctc", "fieldtype": "Currency", "width": "140px"},
|
||||||
|
]
|
@ -0,0 +1,115 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import getdate
|
||||||
|
|
||||||
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
|
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 `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`")
|
||||||
|
|
||||||
|
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,
|
||||||
|
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)
|
@ -245,6 +245,17 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"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",
|
"dependencies": "Salary Slip",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -312,7 +323,7 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2022-01-13 17:41:19.098813",
|
"modified": "2022-02-23 17:41:19.098813",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Payroll",
|
"name": "Payroll",
|
||||||
|
Loading…
Reference in New Issue
Block a user