-
Notifications
You must be signed in to change notification settings - Fork 2.2k
[ADD] hr_payroll_tds: Introduced Tax Declaration Sub-menu #650
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
base: 18.0
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,3 +127,4 @@ dmypy.json | |
|
||
# Pyre type checker | ||
.pyre/ | ||
.vscode/shortcuts.json |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from . import models | ||
from . import wizard |
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", | ||
} |
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> |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
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 |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,64 @@ | ||||||||
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") | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TDS comes under the indian localization just for your knowledge we prefer to define field with l10n_in prefix There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it. |
||||||||
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): | ||||||||
for financial_year_record in self: | ||||||||
start_year = int(financial_year_record.financial_year.split("-")[0]) | ||||||||
end_year = int(financial_year_record.financial_year.split("-")[1]) | ||||||||
financial_year_record.start_date = fields.Date.to_date(f"{start_year}-04-01") | ||||||||
financial_year_record.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) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here too |
||||||||
|
||||||||
def action_approved(self): | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should use self.ensure_one() at the beginning of the method, as it is designed for singleton actions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it. |
||||||||
self.ensure_one() | ||||||||
self.state = "accepted" | ||||||||
|
||||||||
def action_set_to_draft(self): | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
self.ensure_one() | ||||||||
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}", | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
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", | ||
default=lambda self: self.env['hr.rule.parameter']._get_parameter_from_code('l10n_in_standard_deduction_new_regime'), | ||
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 reflected correctly | ||
# in the Total Income. | ||
manually_added_total_income = fields.Float() | ||
|
||
@api.depends("other_income_source", "other_allowance") | ||
def _compute_total_income(self): | ||
for record in self: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be a proper name instead of mentioning record. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay. |
||
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 | ||
|
||
@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 employee_declaration in self: | ||
employee_declaration.taxable_amount = max(employee_declaration.total_income - employee_declaration.standard_deduction, 0) | ||
|
||
tax = 0 | ||
for rate, (lower, upper), fixed_tax in tax_slabs: | ||
if employee_declaration.taxable_amount >= lower and employee_declaration.taxable_amount <= upper: | ||
taxable_amount_temp = employee_declaration.taxable_amount - lower | ||
tax = fixed_tax + round(taxable_amount_temp * rate) | ||
employee_declaration.tax_on_taxable_amount = tax | ||
|
||
if employee_declaration.taxable_amount >= min_income_for_rebate: | ||
marginal_income = employee_declaration.taxable_amount - min_income_for_rebate | ||
employee_declaration.rebate = max(employee_declaration.tax_on_taxable_amount - marginal_income, 0) | ||
else: | ||
employee_declaration.rebate = employee_declaration.tax_on_taxable_amount | ||
employee_declaration.total_tax_on_income = employee_declaration.tax_on_taxable_amount - employee_declaration.rebate | ||
|
||
if employee_declaration.taxable_amount > min_income_for_surcharge: | ||
surcharge = 0 | ||
for rate, amount in tax_slabs_for_surcharge: | ||
if employee_declaration.taxable_amount <= float(amount[1]): | ||
surcharge = employee_declaration.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 employee_declaration.taxable_amount <= income: | ||
break | ||
else: | ||
max_taxable_income, max_tax, max_surcharge = income, tax, surcharge_rate | ||
|
||
excess_income = employee_declaration.taxable_amount - max_taxable_income | ||
max_tax_with_surcharge = max_tax + max_surcharge | ||
total_tax_with_surcharge = employee_declaration.total_tax_on_income + surcharge | ||
excess_tax = total_tax_with_surcharge - max_tax_with_surcharge | ||
|
||
if excess_tax - excess_income > 0: | ||
employee_declaration.surcharge = max_tax_with_surcharge + employee_declaration.taxable_amount - max_taxable_income - employee_declaration.total_tax_on_income | ||
else: | ||
employee_declaration.surcharge = surcharge | ||
else: | ||
employee_declaration.surcharge = 0.0 | ||
|
||
employee_declaration.health_education_cess = (employee_declaration.total_tax_on_income + employee_declaration.surcharge) * 0.04 | ||
employee_declaration.total_tax_to_pay = employee_declaration.total_tax_on_income + employee_declaration.health_education_cess + employee_declaration.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) |
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 |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import test_hr_tds_declaration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is a large amount of data, more than 1000 declarations, during a certain period, does that mean it impacts performance when clicking on the button takes extra time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it can impact performance if there more data to load. but I think making field indexed can optimize performance even for large dataset.