Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADD] hr_payroll_tds: Introduced Tax Declaration Sub-menu #650

Draft
wants to merge 1 commit into
base: 18.0
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -127,3 +127,4 @@ dmypy.json

# Pyre type checker
.pyre/
.vscode/shortcuts.json
2 changes: 2 additions & 0 deletions hr_payroll_tds/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import wizard
20 changes: 20 additions & 0 deletions hr_payroll_tds/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Payroll/TDS",
"version": "1.0",
"author": "Harsh Siddhpara siha",
"summary": "Add Tax Declaration in contracts menu",
"depends": ["l10n_in_hr_payroll"],
"data": [
"security/security.xml",
"security/ir.model.access.csv",
"wizard/hr_tds_declaration_wizard_view.xml",
"views/hr_tds_declaration_views.xml",
"views/hr_payroll_menu.xml",
"views/hr_tds_declaration_details_views.xml",
"views/hr_tds_report.xml",
"views/report_tds_declaration_template.xml",
"data/hr_rule_parameters_data.xml"
],
"installable": True,
"license": "LGPL-3",
}
33 changes: 33 additions & 0 deletions hr_payroll_tds/data/hr_rule_parameters_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<record id="l10n_in_rule_parameter_tds_tax_slabs" model="hr.rule.parameter">
<field name="name">India: TDS Rate Chart New Regime</field>
<field name="code">l10n_in_tds_rate_chart_new_regime</field>
<field name="country_id" ref="base.in"/>
</record>
<record id="l10n_in_rule_parameter_tds_tax_slabs_value" model="hr.rule.parameter.value">
<field name="parameter_value">[
(0.0, (0, 300000),0),
(0.05, (300001, 700000),0),
(0.1, (700001, 1000000),20000),
(0.15, (1000001, 1200000),50000),
(0.2, (1200001, 1500000),80000),
(0.3, (1500001, float('inf')),140000)
]</field>
<field name="rule_parameter_id" ref="l10n_in_rule_parameter_tds_tax_slabs"/>
<field name="date_from" eval="datetime(2000, 1, 1).date()"/>
</record>

<record id="l10n_in_rule_parameter_standard_deduction_new_regime" model="hr.rule.parameter">
<field name="name">India: Standard Deduction New Regime</field>
<field name="code">l10n_in_standard_deduction_new_regime</field>
<field name="country_id" ref="base.in"/>
</record>
<record id="l10n_in_rule_parameter_standard_deduction_value" model="hr.rule.parameter.value">
<field name="parameter_value">75000</field>
<field name="rule_parameter_id" ref="l10n_in_rule_parameter_standard_deduction_new_regime"/>
<field name="date_from" eval="datetime(2000, 1, 1).date()"/>
</record>

</odoo>
3 changes: 3 additions & 0 deletions hr_payroll_tds/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import hr_tds_declaration
from . import hr_tds_declaration_details
from . import hr_employee
16 changes: 16 additions & 0 deletions hr_payroll_tds/models/hr_employee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from odoo import exceptions, models

class Employee(models.Model):
_inherit = "hr.employee"

def action_open_related_tds_declaration(self):
"""Opens the TDS Declarations associated with the current employee."""

action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll_tds.action_open_declarations")
target_ids = self.env["hr.tds.declaration.details"].search([("employee_id", "=", self.id)])
if not target_ids:
raise exceptions.UserError("No TDS declaration available for current employee.")
action["views"] = [[False, "list"], [False, "form"]]
action["domain"] = [("id", "in", target_ids.ids)]

return action
61 changes: 61 additions & 0 deletions hr_payroll_tds/models/hr_tds_declaration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import datetime
from odoo import api, fields, models

class HrTdsDeclaration(models.Model):
_name = "hr.tds.declaration"
_description = "TDS declaration"

def _get_financial_year_selection(self):
current_year = datetime.date.today().year
previous_year = f"{current_year-1}-{current_year}"
current_financial_year = f"{current_year}-{current_year+1}"
return [(previous_year, previous_year), (current_financial_year, current_financial_year)]

name = fields.Char(string="TDS Declaration", required=True)
tds_declaration_ids = fields.One2many("hr.tds.declaration.details", "tds_declaration_id")
financial_year = fields.Selection(
selection=_get_financial_year_selection,
string="Financial Year",
required=True,
default=lambda self: f"{datetime.date.today().year}-{datetime.date.today().year + 1}",
)
start_date = fields.Date(string="Start Date", compute="_compute_dates",readonly=False)
end_date = fields.Date(string="End Date", compute="_compute_dates",readonly=False)
tds_declaration_count = fields.Integer(compute="_compute_declarations_count")
company_id = fields.Many2one("res.company", default=lambda self: self.env.company)
state = fields.Selection(
selection=[
("new", "New"),
("draft", "Draft"),
("confirmed", "Confirmed"),
("accepted", "Accepted")
],
string="Status",
default = "new"
)

@api.depends("financial_year")
def _compute_dates(self):
if self.financial_year:
start_year = int(self.financial_year.split("-")[0])
end_year = int(self.financial_year.split("-")[1])
self.start_date = fields.Date.to_date(f"{start_year}-04-01")
self.end_date = fields.Date.to_date(f"{end_year}-03-31")

def _compute_declarations_count(self):
self.tds_declaration_count = len(self.tds_declaration_ids)

def action_approved(self):
self.state = "accepted"

def action_set_to_draft(self):
self.state = "draft"

def action_open_declarations(self):
return {
"type": "ir.actions.act_window",
"res_model": "hr.tds.declaration.details",
"views": [[False, "list"], [False, "form"]],
"domain": [["id", "in", self.tds_declaration_ids.ids]],
"name": f"TDS Declaration {self.financial_year}",
}
160 changes: 160 additions & 0 deletions hr_payroll_tds/models/hr_tds_declaration_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
from odoo import api, fields, models

class HrTdsDeclarationDetails(models.Model):
_name = "hr.tds.declaration.details"
_description = "Hr TDS declaration details for genearated employees"

name = fields.Char(string="Tax Declarations")
tds_declaration_id = fields.Many2one("hr.tds.declaration", string="TDS Declaration")
employee_id = fields.Many2one("hr.employee")
contract_id = fields.Many2one("hr.contract", string="Contract")
start_date = fields.Date(string="Start Date")
end_date = fields.Date(string="End Date")
financial_year = fields.Char(string="Financial Year")
state = fields.Selection(
selection= [
("new", "New"),
("verify", "Confirmed"),
("approve", "Approved"),
("cancel", "Cancelled")
],
string="Status",
default="new"
)
tax_regime = fields.Selection(
selection= [
("new_regime", "New Regime"),
("old_regiem", "Old Regime")
],
string="Tax Regime",
default="new_regime"
)
age_category = fields.Selection(
selection= [
("lt60", "Less Than 60"),
("60to80", "60 - 80"),
("gt80", "Above 80")
],
string="Category (Age)",
default="lt60"
)
currency_id = fields.Many2one("res.currency", default=lambda self: self.env.company.currency_id.id)
total_income = fields.Monetary(string="Total Income (yearly)", compute="_compute_total_income", inverse="_inverse_total_income", readonly=False, store=True)
standard_deduction = fields.Monetary(string="Standard Deducation", compute="_compute_standard_deduction", readonly=True)
taxable_amount = fields.Monetary(string="Taxable Amount", compute="_compute_taxable_amount")
tax_on_taxable_amount = fields.Monetary(string="Tax On Taxable Amount", compute="_compute_taxable_amount")
rebate = fields.Monetary(string="Rebate Under Section 87A(a)", compute="_compute_taxable_amount")
total_tax_on_income = fields.Monetary(string="Total Tax On Income", compute="_compute_taxable_amount")
surcharge = fields.Monetary(string="Surcharge", compute="_compute_taxable_amount")
health_education_cess = fields.Monetary(string="Health and Education Cess", compute="_compute_taxable_amount")
total_tax_to_pay = fields.Monetary(string="Total Tax to be Paid", compute="_compute_taxable_amount")
monthly_tds = fields.Monetary(string="Monthly TDS Payable", compute="_compute_monthly_tds")
other_income_source = fields.Monetary(string="Other Source of Income")
other_allowance = fields.Monetary(string="Other Allowance Details")
#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
manually_added_total_income = fields.Float()

@api.depends("other_income_source", "other_allowance")
def _compute_total_income(self):
for record in self:
if record.manually_added_total_income and record.total_income:
record.total_income = record.manually_added_total_income + record.other_income_source + record.other_allowance
else:
record.total_income = (record.contract_id.wage * 12 if record.contract_id.wage else 0) + record.other_income_source + record.other_allowance

def _inverse_total_income(self):
for record in self:
if record.total_income != (record.contract_id.wage * 12 if record.contract_id.wage else 0) + record.other_income_source + record.other_allowance:
record.manually_added_total_income = record.total_income
record.total_income += record.other_income_source + record.other_allowance


def _compute_standard_deduction(self):
for record in self:
record.standard_deduction = record.env['hr.rule.parameter']._get_parameter_from_code('l10n_in_standard_deduction_new_regime')

@api.depends("total_income")
def _compute_taxable_amount(self):
"""Computes the taxable amount, tax, rebate, surcharge, and total tax payable
based on predefined tax slabs and thresholds.
- Calculates `taxable_amount` after standard deductions.
- Determines tax liability using progressive tax slabs.
- Applies rebate if income is below the rebate threshold.
- Computes surcharge for incomes exceeding the surcharge threshold.
- Ensures surcharge does not exceed legal limits.
- Adds health & education cess (4%) to derive total tax payable.
"""
rule_parameter = self.env['hr.rule.parameter']
tax_slabs = rule_parameter._get_parameter_from_code('l10n_in_tds_rate_chart_new_regime')
tax_slabs_for_surcharge = rule_parameter._get_parameter_from_code('l10n_in_surcharge_rate')
min_income_for_surcharge = rule_parameter._get_parameter_from_code('l10n_in_min_income_surcharge')
min_income_for_rebate = rule_parameter._get_parameter_from_code('l10n_in_min_income_tax_rebate')

for record in self:
record.taxable_amount = max(record.total_income - record.standard_deduction, 0)

tax = 0
for rate, (lower, upper), fixed_tax in tax_slabs:
if record.taxable_amount >= lower and record.taxable_amount <= upper:
taxable_amount_temp = record.taxable_amount - lower
tax = fixed_tax + round(taxable_amount_temp * rate)
record.tax_on_taxable_amount = tax

if record.taxable_amount >= min_income_for_rebate:
marginal_income = record.taxable_amount - min_income_for_rebate
record.rebate = max(record.tax_on_taxable_amount - marginal_income, 0)
else:
record.rebate = record.tax_on_taxable_amount
record.total_tax_on_income = record.tax_on_taxable_amount - record.rebate

if record.taxable_amount > min_income_for_surcharge:
surcharge = 0
for rate, amount in tax_slabs_for_surcharge:
if record.taxable_amount <= float(amount[1]):
surcharge = record.total_tax_on_income * rate
break

max_tax_slabs = rule_parameter._get_parameter_from_code('l10n_in_max_surcharge_tax_rate')
max_taxable_income, max_tax, max_surcharge = 0, 0, 0

for income, tax, surcharge_rate in max_tax_slabs:
if record.taxable_amount <= income:
break
else:
max_taxable_income, max_tax, max_surcharge = income, tax, surcharge_rate

excess_income = record.taxable_amount - max_taxable_income
max_tax_with_surcharge = max_tax + max_surcharge
total_tax_with_surcharge = record.total_tax_on_income + surcharge
excess_tax = total_tax_with_surcharge - max_tax_with_surcharge

if excess_tax - excess_income > 0:
record.surcharge = max_tax_with_surcharge + record.taxable_amount - max_taxable_income - record.total_tax_on_income
else:
record.surcharge = surcharge
else:
record.surcharge = 0.0

record.health_education_cess = (record.total_tax_on_income + record.surcharge) * 0.04
record.total_tax_to_pay = record.total_tax_on_income + record.health_education_cess + record.surcharge

@api.depends("total_tax_to_pay")
def _compute_monthly_tds(self):
for record in self:
record.monthly_tds = record.total_tax_to_pay / 12

def action_tds_declaration_confirm(self):
if self.state == "new":
self.state = "verify"

def action_tds_declaration_approve(self):
if self.state in ("new", "verify"):
self.state = "approve"

def action_tds_declaration_cancel(self):
if self.state not in ("cancel"):
self.state = "cancel"

def action_print_tds_declaration(self):
return self.env.ref('hr_payroll_tds.action_report_tds_declaration').report_action(self.id)
5 changes: 5 additions & 0 deletions hr_payroll_tds/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
hr_payroll_tds_declaration,hr_payroll_tds_declaration,model_hr_tds_declaration,hr_payroll_tds.group_hr_tds_user,1,1,1,1
hr_payroll_tds_declaration_wizard,hr_payroll_tds_declaration_wizard,model_hr_tds_declaration_wizard,base.group_user,1,1,1,0
hr_tds_declaration_details,hr_tds_declaration_details,model_hr_tds_declaration_details,hr_payroll_tds.group_hr_tds_user,1,1,1,0
hr_employee_tds,hr_employee_tds,model_hr_employee,hr_payroll.group_hr_payroll_user,1,1,1,0
16 changes: 16 additions & 0 deletions hr_payroll_tds/security/security.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<record id="group_hr_tds_user" model="res.groups">
<field name="name">Officer: Manage all tds</field>
<field name="category_id" ref="base.module_category_human_resources_payroll"/>
</record>

<record id="hr_tds_declaration_rule" model="ir.rule">
<field name="name">Record rule according to current company</field>
<field name="model_id" ref="model_hr_tds_declaration"/>
<field name="domain_force">[('company_id', '=', company_id)]</field>
<field name="groups" eval="[(4, ref('group_hr_tds_user'))]"/>
</record>

</odoo>
1 change: 1 addition & 0 deletions hr_payroll_tds/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_hr_tds_declaration
72 changes: 72 additions & 0 deletions hr_payroll_tds/tests/test_hr_tds_declaration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from datetime import date
from odoo.tests.common import TransactionCase
from odoo.tests import tagged


@tagged("post_install", "-at_install")
class TestHrTdsDeclaration(TransactionCase):
""" Involves necessary test cases of hr_tds_declaration & hr_tds_declaration_details models."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.tds_declaration = cls.env["hr.tds.declaration"].create(
{
"name": "Test Declaration",
"financial_year": f"{date.today().year}-{date.today().year}",
"state": "new",
}
)

cls.declaration_details = cls.env["hr.tds.declaration.details"].create(
{
"tds_declaration_id": cls.tds_declaration.id,
"state": "new",
"total_income": 1400000,
"standard_deduction": 75000,
"other_income_source": 100000,
"other_allowance": 50000,
}
)

def test_start_end_date_computation(self):
self.tds_declaration._compute_dates()
start_year = int(self.tds_declaration.financial_year.split("-")[0])
end_year = int(self.tds_declaration.financial_year.split("-")[1])

expected_start_date = date(start_year, 4, 1)
expected_end_date = date(end_year, 3, 31)

self.assertEqual(self.tds_declaration.start_date, expected_start_date, "Start Date computation failed.")
self.assertEqual(self.tds_declaration.end_date, expected_end_date, "End Date computation failed.")

def test_state_transitions(self):
self.tds_declaration.action_approved()
self.assertEqual(self.tds_declaration.state, "accepted", "State transition in action approve failed")

self.tds_declaration.action_set_to_draft()
self.assertEqual(self.tds_declaration.state, "draft", "State transition in action set to draft failed")

def test_tds_declaration_count(self):
self.tds_declaration._compute_declarations_count()
self.assertEqual(self.tds_declaration.tds_declaration_count, 1, "TDS declaration count computation failed.")

def test_action_open_declaration(self):
action = self.tds_declaration.action_open_declarations()
self.assertEqual(action["res_model"], "hr.tds.declaration.details", "Incorrect res_model being passed in action.")

def test_declaration_details_state_transition(self):
self.declaration_details.action_tds_declaration_confirm()
self.assertEqual(self.declaration_details.state, "verify", "State transition failed on action of Confirm button.")

self.declaration_details.action_tds_declaration_approve()
self.assertEqual(self.declaration_details.state, "approve", "State transition failed on action of Approve button.")

self.declaration_details.action_tds_declaration_cancel()
self.assertEqual(self.declaration_details.state, "cancel", "State transition failed on action of Cancel button.")

def test_tds_calculations(self):
self.assertEqual(self.declaration_details.total_income, 1550000, "Incorrect Total Income is being calculated.")
self.assertEqual(self.declaration_details.taxable_amount, 1475000, "Incorrect Total Taxable amount is being calculated.")
self.assertEqual(self.declaration_details.tax_on_taxable_amount, 135000, "Incorrect Tax payable on Total Taxable amount is being calculated.")
self.assertEqual(self.declaration_details.total_tax_to_pay, 140400, "Incorrect Total Tax Payable is being calculated.")
self.assertEqual(self.declaration_details.monthly_tds, 11700, "Incorrect Monthly is being calculated.")
12 changes: 12 additions & 0 deletions hr_payroll_tds/views/hr_payroll_menu.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<menuitem
id="menu_contracts_l10n_in"
name="Tax Declaration"
sequence="40"
action="action_hr_tds_declaration"
parent="hr_payroll.menu_hr_payroll_employees_root"
groups="hr_payroll_tds.group_hr_tds_user" />

</odoo>
130 changes: 130 additions & 0 deletions hr_payroll_tds/views/hr_tds_declaration_details_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<record id="hr_menu_tds_declaration_list_view" model="ir.ui.view">
<field name="name">hr.tds.dclaration.list.view</field>
<field name="model">hr.tds.declaration.details</field>
<field name="arch" type="xml">
<list string="TDS Declaration Details">
<field name="employee_id" />
<field name="tds_declaration_id" />
<field name="contract_id" optional="hide" />
<field name="financial_year" />
<field name="tax_regime" />
<field name="total_income" />
<field name="total_tax_to_pay" />
<field name="other_income_source" optional="hide" />
<field name="other_allowance" optional="hide" />
<field name="health_education_cess" optional="hide" />
<field name="standard_deduction" optional="hide" />
<field name="state" />
</list>
</field>
</record>

<record id="hr_menu_tds_declaration_form_view" model="ir.ui.view">
<field name="name">hr.tds.dclaration.form.view</field>
<field name="model">hr.tds.declaration.details</field>
<field name="arch" type="xml">
<form string="TDS Declaration Details">
<header>
<button string="Confirm" name="action_tds_declaration_confirm" type="object" invisible="state != 'new'" class="btn-primary" />
<button string="Approve" name="action_tds_declaration_approve" type="object" invisible="state != 'verify'" class="btn-primary" />
<button string="Cancel" name="action_tds_declaration_cancel" type="object" invisible="state == 'cancel'" />
<button string="Print" name="action_print_tds_declaration" type="object" />
<field name="state" widget="statusbar" statusbar_visible="new, verify, approve" />
</header>
<sheet>
<div class="row justify-content-between position-relative w-100 m-0">
<div class="oe_title mw-75 ps-0 pe-2" name="title">
<h1 class="d-flex flex-row align-items-center">
<field name="employee_id" placeholder="Employee" readonly="state not in ('new')" />
</h1>
</div>
</div>
<group>
<group>
<field name="contract_id" required="1" readonly="state not in ('new')" />
<field name="tds_declaration_id" />
<field name="financial_year" />
<label for="start_date" string="Period" />
<div>
<field name="start_date" class="oe_inline" readonly="state not in ('new')" />
-<field name="end_date" class="oe_inline" readonly="state not in ('new')" />
</div>
</group>
<group>
<field name="tax_regime" required="1" />
<field name="age_category" required="1" />
</group>
</group>
<notebook>
<page string="TDS Calculation">
<field name="currency_id" invisible="1" />
<group>
<field name="total_income" />
<field name="standard_deduction" />
<field name="taxable_amount" />
<field name="tax_on_taxable_amount" />
<field name="rebate" />
<field name="total_tax_on_income" />
<field name="surcharge" />
<field name="health_education_cess" />
<field name="total_tax_to_pay" />
<field name="monthly_tds" />
</group>
</page>
<page string="Other Deduction">
<group string="Particulars">
<field name="other_income_source" />
<field name="other_allowance" />
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>

<!-- Search view of declaration details -->
<record id="hr_tds_declaration_details_search" model="ir.ui.view">
<field name="name">hr.tds.declaration.search</field>
<field name="model">hr.tds.declaration.details</field>
<field name="arch" type="xml">
<search string="TDS Declaration Search">
<!-- Filters -->
<field name="employee_id" string="Employee" />
<filter name="new" string="New" domain="[('state', '=', 'new')]" />
<filter name="verify" string="Confirm" domain="[('state', '=', 'verify')]" />
<filter name="approve" string="Approve" domain="[('state', '=', 'approve')]" />
<field name="financial_year" />
<separator />
<!-- Group By -->
<group string="Group By">
<filter string="Status" name="group_status" context="{'group_by': 'state'}" />
<filter string="Financial Year" name="group_financial_year" context="{'group_by': 'financial_year'}" />
<filter string="Tax Regime" name="group_tax_regime" context="{'group_by': 'tax_regime'}" />
</group>
</search>
</field>
</record>

<!-- TDS Declaration stat button on employee form. -->
<record id="hr_employee_form_view_tds_declaration" model="ir.ui.view">
<field name="name">hr.employee.form.view.tds,decalration</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form" />
<field name="arch" type="xml">
<data>
<div name="button_box" position="inside">
<button name="action_open_related_tds_declaration" type="object" icon="fa-book" class="oe_stat_button">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">TDS Declarations</span>
</div>
</button>
</div>
</data>
</field>
</record>

</odoo>
85 changes: 85 additions & 0 deletions hr_payroll_tds/views/hr_tds_declaration_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<record id="action_hr_tds_declaration" model="ir.actions.act_window">
<field name="name">TDS Declaration</field>
<field name="res_model">hr.tds.declaration</field>
<field name="view_mode">list,form</field>
</record>

<record id="hr_tds_declaration_list_view" model="ir.ui.view">
<field name="name">hr.tds.declaration.list.view</field>
<field name="model">hr.tds.declaration</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="financial_year"/>
<field name="state"/>
</list>
</field>
</record>

<record id="action_open_declarations" model="ir.actions.act_window">
<field name="name">TDS Declarations</field>
<field name="res_model">hr.tds.declaration.details</field>
<field name="view_mode">list,form</field>
</record>

<record id="hr_tds_declaration_form_view" model="ir.ui.view">
<field name="name">hr.tds.declaration.form.view</field>
<field name="model">hr.tds.declaration</field>
<field name="arch" type="xml">
<form string="TDS declaration">
<header>
<button name="%(hr_payroll_tds.action_genreate_wizard)d" class="btn-primary" type="action" string="Generate Declarations" invisible="state != 'new'"/>
<button name="action_approved" string="Approve" class="btn-primary" type="object" invisible="state not in ('draft', 'confirmed')"/>
<button name="action_set_to_draft" string="Set to Draft" type="object" invisible="state == 'new'"/>
<field name="state" widget="statusbar" statusbar_visible="new, draft, confirmed, accepted"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_open_declarations" class="oe_stat_button" icon="fa-book" type="object" help="Generated TDS Declarations" invisible="tds_declaration_count == 0">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="tds_declaration_count"/></span>
<span class="o_stat_text">TDS Declarations</span>
</div>
</button>
</div>
<h1>
<field name="name" placeholder="E.g. FY 2024-2025"/>
</h1>
<group>
<field name="financial_year"/>
<label for="start_date" string="Period"/>
<div class="o_row">
<field name="start_date" class="oe_inline o_hr_narrow_field"/> - <field name="end_date" class="oe_inline o_hr_narrow_field"/>
</div>
</group>
</sheet>
</form>
</field>
</record>

<!-- Search view of declaration details -->
<record id="hr_tds_declaration_search" model="ir.ui.view">
<field name="name">hr.tds.declaration.search.view</field>
<field name="model">hr.tds.declaration</field>
<field name="arch" type="xml">
<search>
<!-- Filters -->
<filter name="new" string="New" domain="[('state', '=', 'new')]" />
<filter name="confirmed" string="Confirm" domain="[('state', '=', 'confirmed')]" />
<filter name="accepted" string="Approve" domain="[('state', '=', 'accepted')]" />
<field name="financial_year" />
<field name="name"/>
<separator />
<!-- Group By -->
<group string="Group By">
<filter string="Status" name="group_status" context="{'group_by': 'state'}" />
<filter string="Financial Year" name="group_financial_year" context="{'group_by': 'financial_year'}" />
</group>
</search>
</field>
</record>

</odoo>
15 changes: 15 additions & 0 deletions hr_payroll_tds/views/hr_tds_report.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>

<record id="action_report_tds_declaration" model="ir.actions.report">
<field name="name">TDS Declaration Report</field>
<field name="model">hr.tds.declaration.details</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">hr_payroll_tds.report_tax_declaration_lang</field>
<field name="report_file">hr_payroll_tds.report_tax_declaration_lang</field>
<field name="print_report_name">'TDS Declaration - %s' % (object.employee_id.name)</field>
<field name="binding_model_id" ref="model_hr_tds_declaration_details"/>
<field name="binding_type">report</field>
</record>

</odoo>
138 changes: 138 additions & 0 deletions hr_payroll_tds/views/report_tds_declaration_template.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?xml version="1.0"?>
<odoo>
<template id="report_tax_declaration">
<t t-call="web.external_layout">
<div class="page">
<h5 id="tax_declaration_name">
<span t-esc="o.name"/> - Tax Declaration
</h5>
<div id="employee_info">
<table class="table table-sm table-borderless">
<thead class="o_black_border">
<tr>
<th>Employee Information</th>
<th>Contract Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div id="employee_name">
<strong class="me-2">Name:</strong>
<span t-field="o.employee_id.name"/>
</div>
<div id="employee_id">
<strong class="me-2">TDS Delaration:</strong>
<span t-field="o.tds_declaration_id"/>
</div>
<div id="employee_department">
<strong class="me-2">Department:</strong>
<span t-field="o.employee_id.department_id.name"/>
</div>
</td>
<td>
<div id="contract">
<strong class="me-2">Contract:</strong>
<span t-field="o.contract_id.name"/>
</div>
<div id="financial_year">
<strong class="me-2">Financial Year:</strong>
<span t-field="o.financial_year"/>
</div>
<div id="status">
<strong class="me-2">Status:</strong>
<span t-field="o.state"/>
</div>
<div id="tax_regime">
<strong class="me-2">Tax Regime:</strong>
<span t-field="o.tax_regime"/>
</div>
<div id="age_category">
<strong class="me-2">Category (Age):</strong>
<span t-field="o.age_category"/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div id="tax_details">
<h3>Tax Declarations</h3>
<table class="table table-sm table-borderless">
<tbody>
<tr>
<td><strong>Total Income (Yearly):</strong></td>
<td t-if="o.total_income and o.total_income &gt; 0">
<span t-field="o.total_income" t-options="{'widget': 'monetary'}"/>
</td>
</tr>
<tr>
<td><strong>Standard Deduction:</strong></td>
<td t-if="o.standard_deduction and o.standard_deduction &gt; 0">
<span t-field="o.standard_deduction" t-options="{'widget': 'monetary'}"/>
</td>
</tr>
<tr>
<td><strong>Taxable Amount:</strong></td>
<td t-if="o.taxable_amount and o.taxable_amount &gt; 0">
<span t-field="o.taxable_amount" t-options="{'widget': 'monetary'}"/>
</td>
</tr>
<tr>
<td><strong>Tax on Taxable Amount:</strong></td>
<td t-if="o.tax_on_taxable_amount and o.tax_on_taxable_amount &gt; 0">
<span t-field="o.tax_on_taxable_amount" t-options="{'widget': 'monetary'}"/>
</td>
</tr>
<tr>
<td><strong>Rebate Under Section 87A:</strong></td>
<td t-if="o.rebate and o.rebate &gt; 0">
<span t-field="o.rebate" t-options="{'widget': 'monetary'}"/>
</td>
</tr>
<tr>
<td><strong>Total Tax on Income:</strong></td>
<td t-if="o.total_tax_on_income and o.total_tax_on_income &gt; 0">
<span t-field="o.total_tax_on_income" t-options="{'widget': 'monetary'}"/>
</td>
</tr>
<tr>
<td><strong>Surcharge:</strong></td>
<td t-if="o.surcharge and o.surcharge &gt; 0">
<span t-field="o.surcharge" t-options="{'widget': 'monetary'}"/>
</td>
</tr>
<tr>
<td><strong>Health and Education Cess:</strong></td>
<td t-if="o.health_education_cess and o.health_education_cess &gt; 0">
<span t-field="o.health_education_cess" t-options="{'widget': 'monetary'}"/>
</td>
</tr>
<tr>
<td><strong>Total Tax to be Paid:</strong></td>
<td t-if="o.total_tax_to_pay and o.total_tax_to_pay &gt; 0">
<span t-field="o.total_tax_to_pay" t-options="{'widget': 'monetary'}"/>
</td>
</tr>
<tr>
<td><strong>Monthly TDS Payable:</strong></td>
<td t-if="o.monthly_tds and o.monthly_tds &gt; 0">
<span t-field="o.monthly_tds" t-options="{'widget': 'monetary'}"/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</t>
</template>

<template id="report_tax_declaration_lang">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-set="o" t-value="o.with_context(lang=(o.employee_id.lang or o.env.lang))"/>
<t t-call="hr_payroll_tds.report_tax_declaration" t-lang="o.env.lang"/>
</t>
</t>
</template>
</odoo>
1 change: 1 addition & 0 deletions hr_payroll_tds/wizard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import hr_tds_declaration_wizard
89 changes: 89 additions & 0 deletions hr_payroll_tds/wizard/hr_tds_declaration_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from odoo import api, fields, models

class HrTdsDeclarationWizard(models.TransientModel):
_name= "hr.tds.declaration.wizard"
_description = "Generate declaration wizard"

selection_mode = fields.Selection(
selection= [
("by_employee", "By Employee"),
("by_department", "By Department"),
("by_job_position", "By Job Position"),
("by_salary_structure", "By Salary Structure")
],
string="Selection Mode",
default="by_employee",
required=True,
)

#Fields is used to show emplpoyees in list view according to filter is selected.
employee_ids = fields.Many2many("hr.employee","hr_employee_tds_rel", string="Employees Selection", compute="_compute_employee_id", store=True, readonly=False)
#Field is used to select employee when selection mode is by_employee.
employee_id = fields.Many2one("hr.employee", string="Employees")
structure_id = fields.Many2one("hr.payroll.structure", string="Salary Structure")
department_id = fields.Many2one("hr.department", string="Department")
job_id = fields.Many2one("hr.job", string="Job Position")

@api.depends("selection_mode", "structure_id", "department_id", "job_id", "employee_id")
def _compute_employee_id(self):
for wizard in self:
domain = wizard._get_employee_domain()
wizard.employee_ids = self.env["hr.employee"].search(domain)

def _get_employee_domain(self):
"""Determines the domain for filtering employees based on the selected mode.
This function constructs a domain filter based on the `selection_mode` and
the corresponding selected field (`employee_id`, `department_id`, `job_id`, or `structure_id`).
Returns:
list: A domain list to filter employees in `hr.employee` based on the selection mode."""

domain = [("company_id", "=", self.env.company.id)] # Default domain
if self.selection_mode == "by_employee" and self.employee_id:
domain = [("id", "=", self.employee_id.id)]
elif self.selection_mode == "by_department" and self.department_id:
domain = [("department_id", "=", self.department_id.id)]
elif self.selection_mode == "by_job_position" and self.job_id:
domain = [("job_id", "=", self.job_id.id)]
elif self.selection_mode == "by_salary_structure" and self.structure_id:
domain = [("structure_type_id", "=", self.structure_id.type_id.id)]

return domain

def generate_tds_declaration(self):
"""Generates TDS declarations for selected employees based on their active contracts.
This method retrieves the active TDS declaration from the context, fetches employees
linked to it, and determines their valid contracts within the specified date range.
It then creates TDS declaration details for each employee's contract.
Updates the TDS declaration state to 'confirmed' upon successful creation."""

tds_declaration = self.env["hr.tds.declaration"].browse(self.env.context.get("active_id"))
employees = self.employee_ids
contracts = employees._get_contracts(tds_declaration.start_date, tds_declaration.end_date, states=["open"])
tds_declaration_details = []
for contract in contracts:
tds_declaration_details.append(
{
"name": f"TDS Declaration - {contract.employee_id.name}",
"tds_declaration_id": tds_declaration.id,
"employee_id": contract.employee_id.id,
"contract_id": contract.id,
"start_date": tds_declaration.start_date,
"end_date": tds_declaration.end_date,
"financial_year": tds_declaration.financial_year,
}
)
self.env["hr.tds.declaration.details"].create(tds_declaration_details)

success_result = {
"type": "ir.actions.act_window",
"res_model": "hr.tds.declaration",
"views": [[False, "form"]],
"res_id": tds_declaration.id,
}
tds_declaration.state = "confirmed"

return success_result
51 changes: 51 additions & 0 deletions hr_payroll_tds/wizard/hr_tds_declaration_wizard_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<record id="hr_tds_declaration_wizard_form_view" model="ir.ui.view">
<field name="name">hr.tds.declaration.wizard.form.view</field>
<field name="model">hr.tds.declaration.wizard</field>
<field name="arch" type="xml">
<form>
<header>
<span>Generate Declaration</span>
</header>
<sheet>
<group>
<field name="selection_mode"/>
</group>
<group>
<field name="employee_id" invisible="selection_mode not in ('by_employee')"/>
<field name="structure_id" invisible="selection_mode not in ('by_salary_structure')"/>
<field name="department_id" invisible="selection_mode not in ('by_department')"/>
<field name="job_id" invisible="selection_mode not in ('by_job_position')"/>
</group>
<notebook>
<page string="Employees">
<field name="employee_ids" widget="employee_line_many2many">
<list>
<field name="name"/>
<field name="work_email"/>
<field name="department_id"/>
<field name="job_id"/>
</list>
</field>
</page>
</notebook>
</sheet>
<footer>
<button string="Generate" name="generate_tds_declaration" type="object" class="btn-primary" invisible="not employee_ids"/>
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>

<record id="action_genreate_wizard" model="ir.actions.act_window">
<field name="name">Wizard</field>
<field name="res_model">hr.tds.declaration.wizard</field>
<field name="view_mode">form,list</field>
<field name="view_id" ref="hr_payroll_tds.hr_tds_declaration_wizard_form_view"/>
<field name="target">new</field>
</record>

</odoo>