Skip to content

Commit 09411bd

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 09411bd

20 files changed

+911
-0
lines changed

.gitignore

+1
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

+2
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

+20
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+
}
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

+3
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

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

+16
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

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_hr_tds_declaration

0 commit comments

Comments
 (0)