Skip to content

Commit 7f46486

Browse files
committed
[ADD] hr_payroll_tds: Introduced Tax Declaration Sub-menu
With this commit ================ - Added a Tax Declaration sub-menu under the Contracts menu, allowing users to define new TDS declarations. - Introduced a "Generate Declarations" button in the TDS declaration form, opening a wizard for selecting employees. - Implemented four selection modes for employee filtering: - By Employee - By Department - By Job Position - By Salary Structure - Based on the selected mode, the relevant field (e.g., list of departments for "By Department") is dynamically displayed. - Employees are filtered according to the selected criteria, ensuring that only those with valid contracts are included. - TDS Declaration Form Enhancements: - A stat button now displays the count of generated tax declarations. - Clicking the stat button opens a list view of the generated declarations. - Clicking on a specific declaration opens its form view for detailed review. - TDS Calculation & Deductions: - The Tax Declaration tab in the form now computes TDS based on the New Tax Regime and an employee’s total income. - Added an "Other Deductions" tab to manage additional deductions. - Integration with Employee Form: - A TDS Declaration stat button is now available on the Employee form. - Clicking it opens a list of TDS declarations related to the selected employee.
1 parent 4c650f3 commit 7f46486

20 files changed

+917
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,4 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
.vscode/shortcuts.json

hr_payroll_tds/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import models
2+
from . import wizard

hr_payroll_tds/__manifest__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "Payroll/TDS",
3+
"version": "1.0",
4+
"author": "Harsh Siddhpara siha",
5+
"summary": "Add Tax Declaration in contracts menu",
6+
"depends": ["l10n_in_hr_payroll"],
7+
"data": [
8+
"security/security.xml",
9+
"security/ir.model.access.csv",
10+
"wizard/hr_tds_declaration_wizard_view.xml",
11+
"views/hr_tds_declaration_views.xml",
12+
"views/hr_payroll_menu.xml",
13+
"views/hr_tds_declaration_details_views.xml",
14+
"views/hr_tds_report.xml",
15+
"views/report_tds_declaration_template.xml",
16+
"data/hr_rule_parameters_data.xml"
17+
],
18+
"installable": True,
19+
"license": "LGPL-3",
20+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
4+
<record id="l10n_in_rule_parameter_tds_tax_slabs" model="hr.rule.parameter">
5+
<field name="name">India: TDS Rate Chart New Regime</field>
6+
<field name="code">l10n_in_tds_rate_chart_new_regime</field>
7+
<field name="country_id" ref="base.in"/>
8+
</record>
9+
<record id="l10n_in_rule_parameter_tds_tax_slabs_value" model="hr.rule.parameter.value">
10+
<field name="parameter_value">[
11+
(0.0, (0, 300000),0),
12+
(0.05, (300001, 700000),0),
13+
(0.1, (700001, 1000000),20000),
14+
(0.15, (1000001, 1200000),50000),
15+
(0.2, (1200001, 1500000),80000),
16+
(0.3, (1500001, float('inf')),140000)
17+
]</field>
18+
<field name="rule_parameter_id" ref="l10n_in_rule_parameter_tds_tax_slabs"/>
19+
<field name="date_from" eval="datetime(2000, 1, 1).date()"/>
20+
</record>
21+
22+
<record id="l10n_in_rule_parameter_standard_deduction_new_regime" model="hr.rule.parameter">
23+
<field name="name">India: Standard Deduction New Regime</field>
24+
<field name="code">l10n_in_standard_deduction_new_regime</field>
25+
<field name="country_id" ref="base.in"/>
26+
</record>
27+
<record id="l10n_in_rule_parameter_standard_deduction_value" model="hr.rule.parameter.value">
28+
<field name="parameter_value">75000</field>
29+
<field name="rule_parameter_id" ref="l10n_in_rule_parameter_standard_deduction_new_regime"/>
30+
<field name="date_from" eval="datetime(2000, 1, 1).date()"/>
31+
</record>
32+
33+
</odoo>

hr_payroll_tds/models/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import hr_tds_declaration
2+
from . import hr_tds_declaration_details
3+
from . import hr_employee

hr_payroll_tds/models/hr_employee.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from odoo import exceptions, models
2+
3+
4+
class Employee(models.Model):
5+
_inherit = "hr.employee"
6+
7+
def action_open_related_tds_declaration(self):
8+
"""Opens the TDS Declarations associated with the current employee."""
9+
10+
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll_tds.action_open_declarations")
11+
target_ids = self.env["hr.tds.declaration.details"].search([("employee_id", "=", self.id)])
12+
if not target_ids:
13+
raise exceptions.UserError("No TDS declaration available for current employee.")
14+
action["views"] = [[False, "list"], [False, "form"]]
15+
action["domain"] = [("id", "in", target_ids.ids)]
16+
17+
return action
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import datetime
2+
from odoo import api, fields, models
3+
4+
5+
class HrTdsDeclaration(models.Model):
6+
_name = "hr.tds.declaration"
7+
_description = "TDS declaration"
8+
9+
def _get_financial_year_selection(self):
10+
current_year = datetime.date.today().year
11+
previous_year = f"{current_year-1} - {current_year}"
12+
current_financial_year = f"{current_year} - {current_year+1}"
13+
return [(previous_year, previous_year), (current_financial_year, current_financial_year)]
14+
15+
name = fields.Char(string="TDS Declaration", required=True)
16+
tds_declaration_ids = fields.One2many("hr.tds.declaration.details", "tds_declaration_id")
17+
financial_year = fields.Selection(
18+
selection=_get_financial_year_selection,
19+
string="Financial Year",
20+
required=True,
21+
default=lambda self: f"{datetime.date.today().year}-{datetime.date.today().year + 1}",
22+
)
23+
start_date = fields.Date(string="Start Date", compute="_compute_dates", readonly=False)
24+
end_date = fields.Date(string="End Date", compute="_compute_dates", readonly=False)
25+
tds_declaration_count = fields.Integer(compute="_compute_declarations_count")
26+
company_id = fields.Many2one("res.company", default=lambda self: self.env.company)
27+
state = fields.Selection(
28+
selection=[
29+
("new", "New"),
30+
("draft", "Draft"),
31+
("confirmed", "Confirmed"),
32+
("accepted", "Accepted")
33+
],
34+
string="Status",
35+
default="new"
36+
)
37+
38+
@api.depends("financial_year")
39+
def _compute_dates(self):
40+
for financial_year_record in self:
41+
start_year = int(financial_year_record.financial_year.split("-")[0])
42+
end_year = int(financial_year_record.financial_year.split("-")[1])
43+
financial_year_record.start_date = fields.Date.to_date(f"{start_year}-04-01")
44+
financial_year_record.end_date = fields.Date.to_date(f"{end_year}-03-31")
45+
46+
def _compute_declarations_count(self):
47+
self.tds_declaration_count = len(self.tds_declaration_ids)
48+
49+
def action_approved(self):
50+
self.ensure_one()
51+
self.state = "accepted"
52+
53+
def action_set_to_draft(self):
54+
self.ensure_one()
55+
self.state = "draft"
56+
57+
def action_open_declarations(self):
58+
return {
59+
"type": "ir.actions.act_window",
60+
"res_model": "hr.tds.declaration.details",
61+
"views": [[False, "list"], [False, "form"]],
62+
"domain": [["id", "in", self.tds_declaration_ids.ids]],
63+
"name": f"TDS Declaration {self.financial_year}",
64+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from odoo import api, fields, models
2+
3+
4+
class HrTdsDeclarationDetails(models.Model):
5+
_name = "hr.tds.declaration.details"
6+
_description = "Hr TDS declaration details for genearated employees"
7+
8+
name = fields.Char(string="Tax Declarations")
9+
tds_declaration_id = fields.Many2one("hr.tds.declaration", string="TDS Declaration")
10+
employee_id = fields.Many2one("hr.employee")
11+
contract_id = fields.Many2one("hr.contract", string="Contract")
12+
start_date = fields.Date(string="Start Date")
13+
end_date = fields.Date(string="End Date")
14+
financial_year = fields.Char(string="Financial Year")
15+
state = fields.Selection(
16+
selection=[
17+
("new", "New"),
18+
("verify", "Confirmed"),
19+
("approve", "Approved"),
20+
("cancel", "Cancelled")
21+
],
22+
string="Status",
23+
default="new"
24+
)
25+
tax_regime = fields.Selection(
26+
selection=[
27+
("new_regime", "New Regime"),
28+
("old_regiem", "Old Regime")
29+
],
30+
string="Tax Regime",
31+
default="new_regime"
32+
)
33+
age_category = fields.Selection(
34+
selection=[
35+
("lt60", "Less Than 60"),
36+
("60to80", "60 - 80"),
37+
("gt80", "Above 80")
38+
],
39+
string="Category (Age)",
40+
default="lt60"
41+
)
42+
currency_id = fields.Many2one("res.currency", default=lambda self: self.env.company.currency_id.id)
43+
total_income = fields.Monetary(string="Total Income (yearly)", compute="_compute_total_income", inverse="_inverse_total_income", readonly=False, store=True)
44+
standard_deduction = fields.Monetary(
45+
string="Standard Deducation",
46+
default=lambda self: self.env['hr.rule.parameter']._get_parameter_from_code('l10n_in_standard_deduction_new_regime'),
47+
readonly=True)
48+
taxable_amount = fields.Monetary(string="Taxable Amount", compute="_compute_taxable_amount")
49+
tax_on_taxable_amount = fields.Monetary(string="Tax On Taxable Amount", compute="_compute_taxable_amount")
50+
rebate = fields.Monetary(string="Rebate Under Section 87A(a)", compute="_compute_taxable_amount")
51+
total_tax_on_income = fields.Monetary(string="Total Tax On Income", compute="_compute_taxable_amount")
52+
surcharge = fields.Monetary(string="Surcharge", compute="_compute_taxable_amount")
53+
health_education_cess = fields.Monetary(string="Health and Education Cess", compute="_compute_taxable_amount")
54+
total_tax_to_pay = fields.Monetary(string="Total Tax to be Paid", compute="_compute_taxable_amount")
55+
monthly_tds = fields.Monetary(string="Monthly TDS Payable", compute="_compute_monthly_tds")
56+
other_income_source = fields.Monetary(string="Other Source of Income")
57+
other_allowance = fields.Monetary(string="Other Allowance Details")
58+
# Below field is used to store manually added Total Income to ensure that while
59+
# modifying other_income_source & other_allowance values are reflected correctly
60+
# in the Total Income.
61+
manually_added_total_income = fields.Float()
62+
63+
@api.depends("other_income_source", "other_allowance")
64+
def _compute_total_income(self):
65+
for record in self:
66+
if record.manually_added_total_income and record.total_income:
67+
record.total_income = record.manually_added_total_income + record.other_income_source + record.other_allowance
68+
else:
69+
record.total_income = (record.contract_id.wage * 12 if record.contract_id.wage else 0) + record.other_income_source + record.other_allowance
70+
71+
def _inverse_total_income(self):
72+
for record in self:
73+
if record.total_income != (record.contract_id.wage * 12 if record.contract_id.wage else 0) + record.other_income_source + record.other_allowance:
74+
record.manually_added_total_income = record.total_income
75+
record.total_income += record.other_income_source + record.other_allowance
76+
77+
@api.depends("total_income")
78+
def _compute_taxable_amount(self):
79+
"""Computes the taxable amount, tax, rebate, surcharge, and total tax payable
80+
based on predefined tax slabs and thresholds.
81+
82+
- Calculates `taxable_amount` after standard deductions.
83+
- Determines tax liability using progressive tax slabs.
84+
- Applies rebate if income is below the rebate threshold.
85+
- Computes surcharge for incomes exceeding the surcharge threshold.
86+
- Ensures surcharge does not exceed legal limits.
87+
- Adds health & education cess (4%) to derive total tax payable.
88+
"""
89+
rule_parameter = self.env['hr.rule.parameter']
90+
tax_slabs = rule_parameter._get_parameter_from_code('l10n_in_tds_rate_chart_new_regime')
91+
tax_slabs_for_surcharge = rule_parameter._get_parameter_from_code('l10n_in_surcharge_rate')
92+
min_income_for_surcharge = rule_parameter._get_parameter_from_code('l10n_in_min_income_surcharge')
93+
min_income_for_rebate = rule_parameter._get_parameter_from_code('l10n_in_min_income_tax_rebate')
94+
95+
for employee_declaration in self:
96+
employee_declaration.taxable_amount = max(employee_declaration.total_income - employee_declaration.standard_deduction, 0)
97+
98+
tax = 0
99+
for rate, (lower, upper), fixed_tax in tax_slabs:
100+
if employee_declaration.taxable_amount >= lower and employee_declaration.taxable_amount <= upper:
101+
taxable_amount_temp = employee_declaration.taxable_amount - lower
102+
tax = fixed_tax + round(taxable_amount_temp * rate)
103+
employee_declaration.tax_on_taxable_amount = tax
104+
105+
if employee_declaration.taxable_amount >= min_income_for_rebate:
106+
marginal_income = employee_declaration.taxable_amount - min_income_for_rebate
107+
employee_declaration.rebate = max(employee_declaration.tax_on_taxable_amount - marginal_income, 0)
108+
else:
109+
employee_declaration.rebate = employee_declaration.tax_on_taxable_amount
110+
employee_declaration.total_tax_on_income = employee_declaration.tax_on_taxable_amount - employee_declaration.rebate
111+
112+
if employee_declaration.taxable_amount > min_income_for_surcharge:
113+
surcharge = 0
114+
for rate, amount in tax_slabs_for_surcharge:
115+
if employee_declaration.taxable_amount <= float(amount[1]):
116+
surcharge = employee_declaration.total_tax_on_income * rate
117+
break
118+
119+
max_tax_slabs = rule_parameter._get_parameter_from_code('l10n_in_max_surcharge_tax_rate')
120+
max_taxable_income, max_tax, max_surcharge = 0, 0, 0
121+
122+
for income, tax, surcharge_rate in max_tax_slabs:
123+
if employee_declaration.taxable_amount <= income:
124+
break
125+
else:
126+
max_taxable_income, max_tax, max_surcharge = income, tax, surcharge_rate
127+
128+
excess_income = employee_declaration.taxable_amount - max_taxable_income
129+
max_tax_with_surcharge = max_tax + max_surcharge
130+
total_tax_with_surcharge = employee_declaration.total_tax_on_income + surcharge
131+
excess_tax = total_tax_with_surcharge - max_tax_with_surcharge
132+
133+
if excess_tax - excess_income > 0:
134+
employee_declaration.surcharge = max_tax_with_surcharge + employee_declaration.taxable_amount - max_taxable_income - employee_declaration.total_tax_on_income
135+
else:
136+
employee_declaration.surcharge = surcharge
137+
else:
138+
employee_declaration.surcharge = 0.0
139+
140+
employee_declaration.health_education_cess = (employee_declaration.total_tax_on_income + employee_declaration.surcharge) * 0.04
141+
employee_declaration.total_tax_to_pay = employee_declaration.total_tax_on_income + employee_declaration.health_education_cess + employee_declaration.surcharge
142+
143+
@api.depends("total_tax_to_pay")
144+
def _compute_monthly_tds(self):
145+
for record in self:
146+
record.monthly_tds = record.total_tax_to_pay / 12
147+
148+
def action_tds_declaration_confirm(self):
149+
if self.state == "new":
150+
self.state = "verify"
151+
152+
def action_tds_declaration_approve(self):
153+
if self.state in ("new", "verify"):
154+
self.state = "approve"
155+
156+
def action_tds_declaration_cancel(self):
157+
if self.state not in ("cancel"):
158+
self.state = "cancel"
159+
160+
def action_print_tds_declaration(self):
161+
return self.env.ref('hr_payroll_tds.action_report_tds_declaration').report_action(self.id)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2+
hr_payroll_tds_declaration,hr_payroll_tds_declaration,model_hr_tds_declaration,hr_payroll_tds.group_hr_tds_user,1,1,1,1
3+
hr_payroll_tds_declaration_wizard,hr_payroll_tds_declaration_wizard,model_hr_tds_declaration_wizard,base.group_user,1,1,1,0
4+
hr_tds_declaration_details,hr_tds_declaration_details,model_hr_tds_declaration_details,hr_payroll_tds.group_hr_tds_user,1,1,1,0
5+
hr_employee_tds,hr_employee_tds,model_hr_employee,hr_payroll.group_hr_payroll_user,1,1,1,0

hr_payroll_tds/security/security.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
4+
<record id="group_hr_tds_user" model="res.groups">
5+
<field name="name">Officer: Manage all tds</field>
6+
<field name="category_id" ref="base.module_category_human_resources_payroll"/>
7+
</record>
8+
9+
<record id="hr_tds_declaration_rule" model="ir.rule">
10+
<field name="name">Record rule according to current company</field>
11+
<field name="model_id" ref="model_hr_tds_declaration"/>
12+
<field name="domain_force">[('company_id', '=', company_id)]</field>
13+
<field name="groups" eval="[(4, ref('group_hr_tds_user'))]"/>
14+
</record>
15+
16+
</odoo>

hr_payroll_tds/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_hr_tds_declaration

0 commit comments

Comments
 (0)