Compare commits
60 Commits
feature/od
...
feature/od
| Author | SHA1 | Date |
|---|---|---|
|
|
41f493840b | |
|
|
54541998c5 | |
|
|
68e62956b7 | |
|
|
14e725be4f | |
|
|
adfe801d8e | |
|
|
5c6341d8b7 | |
|
|
71f416e8e8 | |
|
|
b5f643fb18 | |
|
|
3092032fee | |
|
|
f118600ab6 | |
|
|
05bdddc472 | |
|
|
582225e11e | |
|
|
478c1708fb | |
|
|
945aedc0b4 | |
|
|
eb17d717dd | |
|
|
604d556501 | |
|
|
90211776a1 | |
|
|
923304f759 | |
|
|
96493be796 | |
|
|
5460f6c207 | |
|
|
4db7e5ade2 | |
|
|
f2788e025d | |
|
|
0e51ac85e9 | |
|
|
19e5f1db80 | |
|
|
c2e33753bb | |
|
|
f6bfd46f2c | |
|
|
c9746456b8 | |
|
|
b0ec5ee508 | |
|
|
e2d6a8c417 | |
|
|
9c33507a45 | |
|
|
a9a6c683d7 | |
|
|
723dcbe225 | |
|
|
4139e5fa33 | |
|
|
ce93d9601c | |
|
|
1b21175e75 | |
|
|
74526cc1a2 | |
|
|
6a32ac3f37 | |
|
|
fa3833bac3 | |
|
|
73a27d8921 | |
|
|
26923e20b9 | |
|
|
ff806505e2 | |
|
|
b5b276f552 | |
|
|
5d6c2c09aa | |
|
|
4b85cc0f59 | |
|
|
ad5967d420 | |
|
|
53f90a7834 | |
|
|
87824199d0 | |
|
|
92543295d6 | |
|
|
7b6d108ace | |
|
|
6f77059f85 | |
|
|
c93d208990 | |
|
|
bfd7890cbc | |
|
|
d362ef87aa | |
|
|
20d22c1f04 | |
|
|
44e5ee7e2f | |
|
|
0fa84c6d43 | |
|
|
4ce02e58fa | |
|
|
66077d1819 | |
|
|
2dbdb58127 | |
|
|
5f267e96da |
|
|
@ -0,0 +1,54 @@
|
||||||
|
FROM python:3.12-bookworm
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# System dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
npm \
|
||||||
|
libpq-dev \
|
||||||
|
libldap2-dev \
|
||||||
|
libsasl2-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt1-dev \
|
||||||
|
libjpeg62-turbo-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libssl-dev \
|
||||||
|
liblcms2-dev \
|
||||||
|
wkhtmltopdf \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Less compiler used by Odoo
|
||||||
|
RUN npm install -g less less-plugin-clean-css
|
||||||
|
|
||||||
|
# Create odoo user
|
||||||
|
RUN useradd -m -d /opt/odoo -U -r -s /bin/bash odoo
|
||||||
|
|
||||||
|
WORKDIR /opt/odoo/odoo18
|
||||||
|
|
||||||
|
# Copy project
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Upgrade pip tools
|
||||||
|
RUN pip install --upgrade pip setuptools wheel
|
||||||
|
|
||||||
|
# Install requirements
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# PostgreSQL driver
|
||||||
|
RUN pip install psycopg2-binary
|
||||||
|
|
||||||
|
# Permissions
|
||||||
|
RUN chown -R odoo:odoo /opt/odoo
|
||||||
|
|
||||||
|
USER odoo
|
||||||
|
|
||||||
|
EXPOSE 8069
|
||||||
|
|
||||||
|
CMD ["python3", "odoo-bin", "-c", "odoo.conf"]
|
||||||
|
|
@ -103,7 +103,7 @@ class ImBus(models.Model):
|
||||||
"""Low-level method to send ``notification_type`` and ``message`` to ``target``.
|
"""Low-level method to send ``notification_type`` and ``message`` to ``target``.
|
||||||
|
|
||||||
Using ``_bus_send()`` from ``bus.listener.mixin`` is recommended for simplicity and
|
Using ``_bus_send()`` from ``bus.listener.mixin`` is recommended for simplicity and
|
||||||
security.
|
security.
|
||||||
|
|
||||||
When using ``_sendone`` directly, ``target`` (if str) should not be guessable by an
|
When using ``_sendone`` directly, ``target`` (if str) should not be guessable by an
|
||||||
attacker.
|
attacker.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'category': 'Accounting/Localizations/Point of Sale',
|
'category': 'Accounting/Localizations/Point of Sale',
|
||||||
'description': """
|
'description': """
|
||||||
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C).
|
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability,security, storage and archiving of data related to sales to private individuals (B2C).
|
||||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
Install it if you use the Point of Sale app to sell to individuals.
|
Install it if you use the Point of Sale app to sell to individuals.
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ class Survey(http.Controller):
|
||||||
|
|
||||||
def _check_validity(self, survey_token, answer_token, ensure_token=True, check_partner=True):
|
def _check_validity(self, survey_token, answer_token, ensure_token=True, check_partner=True):
|
||||||
""" Check survey is open and can be taken. This does not checks for
|
""" Check survey is open and can be taken. This does not checks for
|
||||||
security rules, only functional / business rules. It returns a string key
|
security rules, only functional / business rules. It returns a string key
|
||||||
allowing further manipulation of validity issues
|
allowing further manipulation of validity issues
|
||||||
|
|
||||||
* survey_wrong: survey does not exist;
|
* survey_wrong: survey does not exist;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
},
|
},
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'data': [
|
'data': [
|
||||||
# security.xml first, data.xml need the group to exist (checking it)
|
#security.xml first, data.xml need the group to exist (checking it)
|
||||||
'security/website_security.xml',
|
'security/website_security.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/image_library.xml',
|
'data/image_library.xml',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
'name': 'Bench Management',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Human Resources',
|
||||||
|
'summary': 'Bench Management System',
|
||||||
|
'author': 'Team Srivyn',
|
||||||
|
'depends': [
|
||||||
|
'hr',
|
||||||
|
'project',
|
||||||
|
'hr_timesheet',
|
||||||
|
'project_task_timesheet_extended',
|
||||||
|
'hr_employee_extended'
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/sync_team_lines.xml',
|
||||||
|
'views/project.xml',
|
||||||
|
'views/bench_management_view.xml',
|
||||||
|
],
|
||||||
|
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'auto_install': False,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<function model="project.project" name="_sync_all_team_lines_from_members"/>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import project
|
||||||
|
from . import bench_management
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
from odoo import models, fields, tools, api
|
||||||
|
|
||||||
|
|
||||||
|
class BenchManagementLine(models.Model):
|
||||||
|
_name = "bench.management.line"
|
||||||
|
_description = "Employee Availability (Bench)"
|
||||||
|
_auto = False
|
||||||
|
_rec_name = 'employee_id'
|
||||||
|
|
||||||
|
employee_id = fields.Many2one("hr.employee", readonly=True)
|
||||||
|
job_id = fields.Many2one("hr.job", readonly=True)
|
||||||
|
company_id = fields.Many2one("res.company", related="employee_id.company_id")
|
||||||
|
|
||||||
|
project_line_ids = fields.Many2many(
|
||||||
|
'project.team.line',
|
||||||
|
compute='_compute_bench_details',
|
||||||
|
string='Project Assignments',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
limited_project_line_ids = fields.Many2many(
|
||||||
|
compute='_compute_bench_details',
|
||||||
|
comodel_name='project.team.line',
|
||||||
|
string='Kanban Projects',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
project_names_tooltip = fields.Text(
|
||||||
|
string="Project Names",
|
||||||
|
compute='_compute_bench_details',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
project_count = fields.Integer(
|
||||||
|
string="Project Count",
|
||||||
|
compute='_compute_bench_details',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
active_project_count = fields.Integer(
|
||||||
|
string="Active Projects",
|
||||||
|
compute='_compute_bench_details',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
future_project_count = fields.Integer(
|
||||||
|
string="Upcoming Projects",
|
||||||
|
compute='_compute_bench_details',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
completed_project_count = fields.Integer(
|
||||||
|
string="Completed Projects",
|
||||||
|
compute='_compute_bench_details',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
status = fields.Selection([
|
||||||
|
("bench", "Bench"),
|
||||||
|
("partial", "Partial"),
|
||||||
|
("full", "Full"),
|
||||||
|
], readonly=True)
|
||||||
|
|
||||||
|
def _get_line_availability_status(self, line, today):
|
||||||
|
return line.status or 'not_started'
|
||||||
|
|
||||||
|
def _compute_bench_details(self):
|
||||||
|
project_team_line = self.env['project.team.line'].sudo()
|
||||||
|
today = fields.Date.context_today(self)
|
||||||
|
for rec in self:
|
||||||
|
project_lines = project_team_line.search(
|
||||||
|
[('employee_id', '=', rec.employee_id.id)],
|
||||||
|
order='start_date desc, id desc'
|
||||||
|
)
|
||||||
|
active_lines = project_lines.filtered(
|
||||||
|
lambda line: rec._get_line_availability_status(line, today) == 'in_progress'
|
||||||
|
)
|
||||||
|
future_lines = project_lines.filtered(
|
||||||
|
lambda line: rec._get_line_availability_status(line, today) == 'not_started'
|
||||||
|
)
|
||||||
|
completed_lines = project_lines.filtered(
|
||||||
|
lambda line: rec._get_line_availability_status(line, today) == 'done'
|
||||||
|
)
|
||||||
|
project_records = project_lines.mapped('project_id')
|
||||||
|
|
||||||
|
if active_lines:
|
||||||
|
bench_status = 'full'
|
||||||
|
elif future_lines:
|
||||||
|
bench_status = 'partial'
|
||||||
|
else:
|
||||||
|
bench_status = 'bench'
|
||||||
|
|
||||||
|
rec.project_line_ids = project_lines
|
||||||
|
rec.limited_project_line_ids = project_lines[:3]
|
||||||
|
rec.project_count = len(project_records)
|
||||||
|
rec.active_project_count = len(active_lines)
|
||||||
|
rec.future_project_count = len(future_lines)
|
||||||
|
rec.completed_project_count = len(completed_lines)
|
||||||
|
rec.project_names_tooltip = '\n'.join(
|
||||||
|
f"{line.project_id.display_name or 'No Project'} - {dict(line._fields['status'].selection).get(rec._get_line_availability_status(line, today), 'N/A')}"
|
||||||
|
for line in project_lines
|
||||||
|
) or ''
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
tools.drop_view_if_exists(self.env.cr, self._table)
|
||||||
|
|
||||||
|
self.env.cr.execute("""
|
||||||
|
CREATE OR REPLACE VIEW bench_management_line AS (
|
||||||
|
SELECT
|
||||||
|
he.id AS id,
|
||||||
|
he.id AS employee_id,
|
||||||
|
he.job_id AS job_id,
|
||||||
|
CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM project_team_line tpl
|
||||||
|
WHERE tpl.employee_id = he.id
|
||||||
|
AND tpl.status = 'in_progress'
|
||||||
|
) THEN 'full'
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM project_team_line tpl
|
||||||
|
WHERE tpl.employee_id = he.id
|
||||||
|
AND tpl.status = 'not_started'
|
||||||
|
) THEN 'partial'
|
||||||
|
ELSE 'bench'
|
||||||
|
END AS status
|
||||||
|
FROM hr_employee he
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
class ProjectTeamLine(models.Model):
|
||||||
|
_inherit = 'project.team.line'
|
||||||
|
|
||||||
|
line_status_color = fields.Integer(
|
||||||
|
compute='_compute_line_status_color',
|
||||||
|
string='Status Color',
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('status')
|
||||||
|
def _compute_line_status_color(self):
|
||||||
|
color_map = {
|
||||||
|
'not_started': 8,
|
||||||
|
'in_progress': 2,
|
||||||
|
'done': 10,
|
||||||
|
}
|
||||||
|
for line in self:
|
||||||
|
line.line_status_color = color_map.get(line.status, 0)
|
||||||
|
|
||||||
|
def name_get(self):
|
||||||
|
result = []
|
||||||
|
for rec in self:
|
||||||
|
name = rec.project_id.display_name or 'No Project'
|
||||||
|
result.append((rec.id, name))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _sync_project_members(self):
|
||||||
|
if self.env.context.get('skip_project_team_member_sync'):
|
||||||
|
return True
|
||||||
|
self.mapped('project_id')._sync_members_from_team_lines()
|
||||||
|
return True
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
records = super().create(vals_list)
|
||||||
|
records._sync_project_members()
|
||||||
|
return records
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
projects = self.mapped('project_id')
|
||||||
|
res = super().write(vals)
|
||||||
|
(projects | self.mapped('project_id'))._sync_members_from_team_lines()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
projects = self.mapped('project_id')
|
||||||
|
res = super().unlink()
|
||||||
|
projects._sync_members_from_team_lines()
|
||||||
|
return res
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
from odoo import Command, api, fields, models, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
class ProjectProject(models.Model):
|
||||||
|
_inherit = 'project.project'
|
||||||
|
|
||||||
|
team_line_ids = fields.One2many(
|
||||||
|
'project.team.line',
|
||||||
|
'project_id',
|
||||||
|
string="Team Details"
|
||||||
|
)
|
||||||
|
can_manage_team_lines = fields.Boolean(
|
||||||
|
compute='_compute_can_manage_team_lines',
|
||||||
|
string='Can Manage Team Lines'
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('user_id', 'project_lead')
|
||||||
|
def _compute_can_manage_team_lines(self):
|
||||||
|
current_user = self.env.user
|
||||||
|
for project in self:
|
||||||
|
project.can_manage_team_lines = bool(
|
||||||
|
self.env.is_superuser()
|
||||||
|
or project.user_id == current_user
|
||||||
|
or ('project_lead' in project._fields and project.project_lead == current_user) or (current_user.has_group("project.group_project_manager"))
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.onchange('team_line_ids')
|
||||||
|
def _onchange_team_line_ids(self):
|
||||||
|
for project in self:
|
||||||
|
users = project.team_line_ids.mapped('user_id')
|
||||||
|
project.members_ids = [(6, 0, users.ids)]
|
||||||
|
|
||||||
|
def _sync_members_from_team_lines(self):
|
||||||
|
if self.env.context.get('skip_project_team_member_sync'):
|
||||||
|
return
|
||||||
|
for project in self:
|
||||||
|
users = project.team_line_ids.mapped('user_id')
|
||||||
|
if set(project.members_ids.ids) != set(users.ids):
|
||||||
|
project.with_context(skip_project_team_member_sync=True).sudo().write({
|
||||||
|
'members_ids': [Command.set(users.ids)],
|
||||||
|
})
|
||||||
|
|
||||||
|
def _sync_team_lines_from_members(self):
|
||||||
|
if self.env.context.get('skip_project_team_member_sync'):
|
||||||
|
return
|
||||||
|
|
||||||
|
TeamLine = self.env['project.team.line'].sudo().with_context(skip_project_team_member_sync=True)
|
||||||
|
for project in self.sudo():
|
||||||
|
member_ids = set(project.members_ids.ids)
|
||||||
|
kept_user_ids = set()
|
||||||
|
lines_to_remove = self.env['project.team.line'].sudo()
|
||||||
|
|
||||||
|
for line in project.team_line_ids.sorted('id'):
|
||||||
|
user_id = line.user_id.id
|
||||||
|
if not user_id or user_id not in member_ids or user_id in kept_user_ids:
|
||||||
|
lines_to_remove |= line
|
||||||
|
else:
|
||||||
|
kept_user_ids.add(user_id)
|
||||||
|
|
||||||
|
if lines_to_remove:
|
||||||
|
lines_to_remove.with_context(skip_project_team_member_sync=True).unlink()
|
||||||
|
|
||||||
|
for user_id in member_ids - kept_user_ids:
|
||||||
|
TeamLine.create({
|
||||||
|
'project_id': project.id,
|
||||||
|
'user_id': user_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _sync_all_team_lines_from_members(self):
|
||||||
|
self.search([])._sync_team_lines_from_members()
|
||||||
|
return True
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
projects = super().create(vals_list)
|
||||||
|
for project, vals in zip(projects, vals_list):
|
||||||
|
if 'team_line_ids' in vals:
|
||||||
|
project._sync_members_from_team_lines()
|
||||||
|
elif 'members_ids' in vals:
|
||||||
|
project._sync_team_lines_from_members()
|
||||||
|
return projects
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
res = super().write(vals)
|
||||||
|
if 'team_line_ids' in vals:
|
||||||
|
self._sync_members_from_team_lines()
|
||||||
|
elif 'members_ids' in vals:
|
||||||
|
self._sync_team_lines_from_members()
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectTeamLine(models.Model):
|
||||||
|
_name = 'project.team.line'
|
||||||
|
_description = 'Project Team Line'
|
||||||
|
_rec_name = 'project_id'
|
||||||
|
|
||||||
|
project_id = fields.Many2one('project.project', ondelete='cascade')
|
||||||
|
user_id = fields.Many2one('res.users')
|
||||||
|
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
compute="_compute_employee",
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
job_id = fields.Many2one(
|
||||||
|
'hr.job',
|
||||||
|
related='employee_id.job_id',
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
start_date = fields.Date()
|
||||||
|
end_date = fields.Date()
|
||||||
|
|
||||||
|
status = fields.Selection([
|
||||||
|
('not_started', 'Not Started'),
|
||||||
|
('in_progress', 'In Progress'),
|
||||||
|
('done', 'Completed')
|
||||||
|
], compute='_compute_status', inverse='_inverse_status', store=True, readonly=False)
|
||||||
|
can_edit_assignment = fields.Boolean(
|
||||||
|
compute='_compute_can_edit_assignment',
|
||||||
|
string='Can Edit Assignment'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# COMPUTE EMPLOYEE
|
||||||
|
# ------------------------
|
||||||
|
@api.depends('user_id')
|
||||||
|
def _compute_employee(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.employee_id = self.env['hr.employee'].search([
|
||||||
|
('user_id', '=', rec.user_id.id)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
@api.depends('start_date', 'end_date')
|
||||||
|
def _compute_status(self):
|
||||||
|
today = fields.Date.context_today(self)
|
||||||
|
for rec in self:
|
||||||
|
if rec.end_date and rec.end_date < today:
|
||||||
|
rec.status = 'done'
|
||||||
|
elif rec.start_date and rec.start_date > today:
|
||||||
|
rec.status = 'not_started'
|
||||||
|
else:
|
||||||
|
rec.status = 'in_progress'
|
||||||
|
|
||||||
|
@api.depends('project_id.user_id', 'project_id.project_lead')
|
||||||
|
def _compute_can_edit_assignment(self):
|
||||||
|
current_user = self.env.user
|
||||||
|
for rec in self:
|
||||||
|
project = rec.project_id
|
||||||
|
rec.can_edit_assignment = bool(
|
||||||
|
self.env.is_superuser()
|
||||||
|
or (project and project.user_id == current_user)
|
||||||
|
or (project and 'project_lead' in project._fields and project.project_lead == current_user) or (current_user.has_group("project.group_project_manager"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _inverse_status(self):
|
||||||
|
# Allow manual edits to the stored computed field.
|
||||||
|
# When start/end dates change later, compute will refresh it again.
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _check_manager_access(self):
|
||||||
|
if self.env.is_superuser():
|
||||||
|
return
|
||||||
|
unauthorized = self.filtered(lambda rec: not rec.can_edit_assignment)
|
||||||
|
if unauthorized:
|
||||||
|
raise ValidationError(_("Only the related project manager can update team assignment dates or status."))
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# SYNC BENCH
|
||||||
|
# ------------------------
|
||||||
|
def _sync_bench(self):
|
||||||
|
# Bench data is read live from SQL view / computed fields,
|
||||||
|
# so there is no separate sync model to refresh here.
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# CREATE
|
||||||
|
# ------------------------
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
if not self.env.is_superuser():
|
||||||
|
for vals in vals_list:
|
||||||
|
project_id = vals.get('project_id')
|
||||||
|
if project_id:
|
||||||
|
project = self.env['project.project'].browse(project_id)
|
||||||
|
if not (
|
||||||
|
project.user_id == self.env.user
|
||||||
|
or ('project_lead' in project._fields and project.project_lead == self.env.user)
|
||||||
|
):
|
||||||
|
raise ValidationError(_("Only the related project manager can add team assignments."))
|
||||||
|
records = super().create(vals_list)
|
||||||
|
records._sync_bench()
|
||||||
|
records.mapped('project_id')._sync_members_from_team_lines()
|
||||||
|
return records
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# WRITE
|
||||||
|
# ------------------------
|
||||||
|
def write(self, vals):
|
||||||
|
if any(key in vals for key in ('status', 'start_date', 'end_date', 'user_id', 'project_id')):
|
||||||
|
self._check_manager_access()
|
||||||
|
projects = self.mapped('project_id')
|
||||||
|
res = super().write(vals)
|
||||||
|
self._sync_bench()
|
||||||
|
if any(key in vals for key in ('user_id', 'project_id')):
|
||||||
|
(projects | self.mapped('project_id'))._sync_members_from_team_lines()
|
||||||
|
return res
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# UNLINK
|
||||||
|
# ------------------------
|
||||||
|
def unlink(self):
|
||||||
|
projects = self.mapped('project_id')
|
||||||
|
self._check_manager_access()
|
||||||
|
res = super().unlink()
|
||||||
|
self._sync_bench()
|
||||||
|
projects._sync_members_from_team_lines()
|
||||||
|
return res
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_project_team_line,project.team.line,model_project_team_line,,1,1,1,1
|
||||||
|
access_bench_management_line,bench.management.line,model_bench_management_line,,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,252 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_bench_management_tree" model="ir.ui.view">
|
||||||
|
<field name="name">bench.management.line.list</field>
|
||||||
|
<field name="model">bench.management.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Bench Management">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="job_id"/>
|
||||||
|
<field name="status"/>
|
||||||
|
<field name="limited_project_line_ids" widget="many2many_tags" options="{'color_field': 'line_status_color'}"/>
|
||||||
|
<field name="active_project_count"/>
|
||||||
|
<field name="future_project_count"/>
|
||||||
|
<field name="completed_project_count"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_bench_management_search" model="ir.ui.view">
|
||||||
|
<field name="name">bench.management.line.search</field>
|
||||||
|
<field name="model">bench.management.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Bench Management">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="job_id"/>
|
||||||
|
<field name="status"/>
|
||||||
|
<filter name="filter_bench" string="Bench" domain="[('status', '=', 'bench')]"/>
|
||||||
|
<filter name="filter_partial" string="Partially Available" domain="[('status', '=', 'partial')]"/>
|
||||||
|
<filter name="filter_full" string="Fully Allocated" domain="[('status', '=', 'full')]"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter name="group_by_status" string="Status" context="{'group_by': 'status'}"/>
|
||||||
|
<filter name="group_by_job" string="Job Position" context="{'group_by': 'job_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="view_bench_management_form" model="ir.ui.view">
|
||||||
|
<field name="name">bench.management.line.form</field>
|
||||||
|
<field name="model">bench.management.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Bench Management">
|
||||||
|
<sheet>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="job_id"/>
|
||||||
|
<field name="status"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Project Information">
|
||||||
|
<field name="project_line_ids" nolabel="1" readonly="0">
|
||||||
|
<list create="0" delete="0" editable="bottom">
|
||||||
|
<field name="project_id" readonly="1"/>
|
||||||
|
<field name="can_edit_assignment" column_invisible="1"/>
|
||||||
|
<field name="status" readonly="not can_edit_assignment"/>
|
||||||
|
<field name="start_date" readonly="not can_edit_assignment"/>
|
||||||
|
<field name="end_date" readonly="not can_edit_assignment"/>
|
||||||
|
<field name="job_id" optional="hide" readonly="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="view_bench_management_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">bench.management.line.kanban</field>
|
||||||
|
<field name="model">bench.management.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<kanban class="o_kanban_mobile">
|
||||||
|
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="job_id"/>
|
||||||
|
<field name="status"/>
|
||||||
|
<field name="limited_project_line_ids"/>
|
||||||
|
<field name="project_count"/>
|
||||||
|
<field name="active_project_count"/>
|
||||||
|
<field name="future_project_count"/>
|
||||||
|
<field name="completed_project_count"/>
|
||||||
|
<field name="project_names_tooltip"/>
|
||||||
|
|
||||||
|
<templates>
|
||||||
|
|
||||||
|
<t t-name="kanban-box">
|
||||||
|
|
||||||
|
<div class="oe_kanban_card oe_kanban_global_click"
|
||||||
|
style="border-radius:16px;border:1px solid #dbe4ee;background:linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);padding:16px;min-height:260px;box-shadow:0 8px 24px rgba(15, 23, 42, 0.06);">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
|
||||||
|
<img t-att-src="'/web/image/hr.employee/' + record.employee_id.raw_value + '/avatar_128'"
|
||||||
|
style="
|
||||||
|
width:42px;
|
||||||
|
height:42px;
|
||||||
|
border-radius:50%;
|
||||||
|
object-fit:cover;
|
||||||
|
margin-right:10px;
|
||||||
|
"/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
font-size:16px;
|
||||||
|
font-weight:600;
|
||||||
|
color:#1f2937;
|
||||||
|
">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
font-size:12px;
|
||||||
|
color:#6b7280;
|
||||||
|
">
|
||||||
|
<field name="job_id"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div class="mb-3 d-flex align-items-center justify-content-between">
|
||||||
|
|
||||||
|
<t t-if="record.status.raw_value == 'bench'">
|
||||||
|
<span style="
|
||||||
|
background:#e2e8f0;
|
||||||
|
color:#334155;
|
||||||
|
padding:5px 10px;
|
||||||
|
border-radius:20px;
|
||||||
|
font-size:11px;
|
||||||
|
font-weight:600;
|
||||||
|
">
|
||||||
|
Bench
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-if="record.status.raw_value == 'partial'">
|
||||||
|
<span style="
|
||||||
|
background:#fef3c7;
|
||||||
|
color:#92400e;
|
||||||
|
padding:5px 10px;
|
||||||
|
border-radius:20px;
|
||||||
|
font-size:11px;
|
||||||
|
font-weight:600;
|
||||||
|
">
|
||||||
|
Partially Available
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-if="record.status.raw_value == 'full'">
|
||||||
|
<span style="
|
||||||
|
background:#dcfce7;
|
||||||
|
color:#166534;
|
||||||
|
padding:5px 10px;
|
||||||
|
border-radius:20px;
|
||||||
|
font-size:11px;
|
||||||
|
font-weight:600;
|
||||||
|
">
|
||||||
|
Fully Allocated
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div style="font-size:11px;color:#64748b;">
|
||||||
|
<t t-out="record.active_project_count.raw_value"/> Active
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 mb-3" style="gap:8px;">
|
||||||
|
<div style="flex:1;border:1px solid #e2e8f0;border-radius:12px;padding:8px 10px;background:#ffffff;">
|
||||||
|
<div style="font-size:10px;color:#64748b;text-transform:uppercase;">Current</div>
|
||||||
|
<div style="font-size:18px;font-weight:700;color:#0f172a;">
|
||||||
|
<t t-out="record.active_project_count.raw_value"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;border:1px solid #e2e8f0;border-radius:12px;padding:8px 10px;background:#ffffff;">
|
||||||
|
<div style="font-size:10px;color:#64748b;text-transform:uppercase;">Upcoming</div>
|
||||||
|
<div style="font-size:18px;font-weight:700;color:#0f172a;">
|
||||||
|
<t t-out="record.future_project_count.raw_value"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;border:1px solid #e2e8f0;border-radius:12px;padding:8px 10px;background:#ffffff;">
|
||||||
|
<div style="font-size:10px;color:#64748b;text-transform:uppercase;">Completed</div>
|
||||||
|
<div style="font-size:18px;font-weight:700;color:#0f172a;">
|
||||||
|
<t t-out="record.completed_project_count.raw_value"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Project Info -->
|
||||||
|
<div t-if="record.project_count.raw_value"
|
||||||
|
style="border-top:1px solid #f3f4f6;padding-top:12px;">
|
||||||
|
|
||||||
|
<div style="
|
||||||
|
font-size:13px;
|
||||||
|
font-weight:600;
|
||||||
|
color:#374151;
|
||||||
|
margin-bottom:8px;
|
||||||
|
">
|
||||||
|
Projects
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div t-att-title="record.project_names_tooltip.raw_value"
|
||||||
|
style="max-height:48px;overflow:hidden;">
|
||||||
|
<field name="limited_project_line_ids"
|
||||||
|
widget="many2many_tags"
|
||||||
|
options="{'no_create': True, 'no_create_edit': True, 'no_open': True, 'color_field': 'line_status_color'}"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t t-if="record.project_count.raw_value > 3">
|
||||||
|
<div t-att-title="record.project_names_tooltip.raw_value"
|
||||||
|
style="margin-top:8px;font-size:11px;color:#2563eb;font-weight:600;">
|
||||||
|
+ <t t-out="record.project_count.raw_value - 3"/> more
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
||||||
|
</kanban>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="action_bench_management" model="ir.actions.act_window">
|
||||||
|
|
||||||
|
<field name="name">Bench Management</field>
|
||||||
|
<field name="res_model">bench.management.line</field>
|
||||||
|
<field name="view_mode">kanban,list,form</field>
|
||||||
|
<field name="domain">[('company_id', 'in', allowed_company_ids)]</field>
|
||||||
|
<field name="search_view_id" ref="view_bench_management_search"/>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_bench_management"
|
||||||
|
name="Employee Bench"
|
||||||
|
parent="hr.menu_hr_root"
|
||||||
|
action="action_bench_management"
|
||||||
|
sequence="3"/>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="project_project_inherit_form_view2_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">project.project.inherit.form.view.inherit</field>
|
||||||
|
<field name="model">project.project</field>
|
||||||
|
<field name="inherit_id" ref="project_task_timesheet_extended.project_project_inherit_form_view2"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//form" position="inside">
|
||||||
|
<field name="can_manage_team_lines" invisible="1"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//page[@name='team']/group[field[@name='members_ids']]" position="after">
|
||||||
|
<group string="Project Team Details">
|
||||||
|
<field name="team_line_ids" nolabel="1" readonly="not can_manage_team_lines">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="user_id" string="Employee Name"/>
|
||||||
|
<field name="employee_id" string="Employee" readonly="1" column_invisible="1" invisible="1"/>
|
||||||
|
<field name="job_id" string="Job Position"/>
|
||||||
|
<field name="project_id" column_invisible="1"/>
|
||||||
|
<field name="can_edit_assignment" column_invisible="1"/>
|
||||||
|
<field name="start_date" string="Start Date" readonly="not can_edit_assignment"/>
|
||||||
|
<field name="end_date" string="End date" readonly="not can_edit_assignment"/>
|
||||||
|
<field name="status" string="Status" readonly="not can_edit_assignment"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</group>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "Document Parser",
|
||||||
|
"summary": "Reusable AI-assisted document text and data extraction",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "Tools",
|
||||||
|
"author": "Pranay",
|
||||||
|
"website": "https://www.ftprotech.com",
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"depends": ["base"],
|
||||||
|
"data": [
|
||||||
|
"views/res_config_settings_views.xml",
|
||||||
|
],
|
||||||
|
"installable": True,
|
||||||
|
"application": False,
|
||||||
|
"auto_install": False,
|
||||||
|
"external_dependencies": {
|
||||||
|
"python": ["requests"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import document_parser_service
|
||||||
|
from . import res_config_settings
|
||||||
|
|
@ -0,0 +1,444 @@
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import mimetypes
|
||||||
|
import re
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from odoo import _, api, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pytesseract
|
||||||
|
except Exception: # pragma: no cover - optional dependency
|
||||||
|
pytesseract = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
except Exception: # pragma: no cover - optional dependency
|
||||||
|
Image = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pdf2image import convert_from_bytes
|
||||||
|
except Exception: # pragma: no cover - optional dependency
|
||||||
|
convert_from_bytes = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pypdf import PdfReader
|
||||||
|
except Exception: # pragma: no cover - optional dependency
|
||||||
|
PdfReader = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from docx import Document
|
||||||
|
except Exception: # pragma: no cover - optional dependency
|
||||||
|
Document = None
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentParserService(models.AbstractModel):
|
||||||
|
_name = "document.parser.service"
|
||||||
|
_description = "Document Parser Service"
|
||||||
|
|
||||||
|
TOGETHER_ENDPOINT = "https://api.together.xyz/v1/chat/completions"
|
||||||
|
OPENROUTER_ENDPOINT = "https://openrouter.ai/api/v1/chat/completions"
|
||||||
|
|
||||||
|
TOGETHER_MODELS = [
|
||||||
|
"Qwen/Qwen2.5-7B-Instruct-Turbo",
|
||||||
|
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
|
||||||
|
]
|
||||||
|
OPENROUTER_MODELS = [
|
||||||
|
"qwen/qwen-2.5-7b-instruct",
|
||||||
|
"qwen/qwen-2.5-7b-instruct:free",
|
||||||
|
"deepseek/deepseek-chat:free",
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def parse_document(
|
||||||
|
self,
|
||||||
|
file_content,
|
||||||
|
filename=None,
|
||||||
|
required_fields=None,
|
||||||
|
extra_instructions=None,
|
||||||
|
json_schema=None,
|
||||||
|
):
|
||||||
|
if not file_content:
|
||||||
|
raise UserError(_("No document provided."))
|
||||||
|
if not filename:
|
||||||
|
raise UserError(_("Filename is required."))
|
||||||
|
|
||||||
|
binary = self._decode_file_content(file_content)
|
||||||
|
mimetype = self._detect_mimetype(binary, filename)
|
||||||
|
text_content = self._extract_text(binary, mimetype)
|
||||||
|
fields_spec = self._normalize_required_fields(required_fields or {})
|
||||||
|
|
||||||
|
if not text_content.strip():
|
||||||
|
return {
|
||||||
|
"filename": filename,
|
||||||
|
"mimetype": mimetype,
|
||||||
|
"text": "",
|
||||||
|
"result": {},
|
||||||
|
"provider": False,
|
||||||
|
"errors": [_("No text could be extracted from the document.")],
|
||||||
|
"error": _("No text could be extracted from the document."),
|
||||||
|
}
|
||||||
|
|
||||||
|
schema_text = json_schema or self._build_json_schema_text(fields_spec)
|
||||||
|
ai_result, provider_used, provider_errors = self._send_to_ai(
|
||||||
|
text_content=text_content[:45000],
|
||||||
|
schema_text=schema_text,
|
||||||
|
extra_instructions=extra_instructions,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not ai_result:
|
||||||
|
ai_result = self._extract_with_heuristics(text_content, fields_spec)
|
||||||
|
|
||||||
|
ai_result = ai_result or {}
|
||||||
|
error_message = False
|
||||||
|
if not ai_result and provider_errors:
|
||||||
|
error_message = "; ".join(provider_errors[:3])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"filename": filename,
|
||||||
|
"mimetype": mimetype,
|
||||||
|
"text": text_content,
|
||||||
|
"result": ai_result,
|
||||||
|
"provider": provider_used,
|
||||||
|
"errors": provider_errors,
|
||||||
|
"error": error_message,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def extract_requested_data(self, file_content, filename, required_fields, extra_instructions=None, json_schema=None):
|
||||||
|
return self.parse_document(
|
||||||
|
file_content=file_content,
|
||||||
|
filename=filename,
|
||||||
|
required_fields=required_fields,
|
||||||
|
extra_instructions=extra_instructions,
|
||||||
|
json_schema=json_schema,
|
||||||
|
)["result"]
|
||||||
|
|
||||||
|
def _decode_file_content(self, file_content):
|
||||||
|
if isinstance(file_content, bytes):
|
||||||
|
if file_content.startswith((b"%PDF", b"\xFF\xD8", b"\x89PNG", b"PK")):
|
||||||
|
return file_content
|
||||||
|
try:
|
||||||
|
return base64.b64decode(file_content)
|
||||||
|
except Exception:
|
||||||
|
return file_content
|
||||||
|
if isinstance(file_content, str):
|
||||||
|
try:
|
||||||
|
return base64.b64decode(file_content)
|
||||||
|
except Exception as exc:
|
||||||
|
raise UserError(_("Invalid base64 document.")) from exc
|
||||||
|
raise UserError(_("Unsupported file format."))
|
||||||
|
|
||||||
|
def _detect_mimetype(self, binary, filename):
|
||||||
|
if filename:
|
||||||
|
guessed = mimetypes.guess_type(filename)[0]
|
||||||
|
if guessed:
|
||||||
|
return guessed
|
||||||
|
if binary.startswith(b"%PDF"):
|
||||||
|
return "application/pdf"
|
||||||
|
if binary.startswith(b"\xFF\xD8"):
|
||||||
|
return "image/jpeg"
|
||||||
|
if binary.startswith(b"\x89PNG"):
|
||||||
|
return "image/png"
|
||||||
|
if binary[:2] == b"PK":
|
||||||
|
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
return "application/octet-stream"
|
||||||
|
|
||||||
|
def _extract_text(self, binary, mimetype):
|
||||||
|
text_content = ""
|
||||||
|
try:
|
||||||
|
if mimetype == "application/pdf":
|
||||||
|
text_content = self._extract_text_from_pdf(binary)
|
||||||
|
elif mimetype in {"image/png", "image/jpeg", "image/jpg"}:
|
||||||
|
text_content = self._extract_text_from_image(binary)
|
||||||
|
elif mimetype == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||||
|
text_content = self._extract_text_from_docx(binary)
|
||||||
|
elif mimetype.startswith("text/"):
|
||||||
|
text_content = binary.decode("utf-8", errors="ignore")
|
||||||
|
except Exception as exc:
|
||||||
|
_logger.exception("Document text extraction failed: %s", exc)
|
||||||
|
return (text_content or "").strip()
|
||||||
|
|
||||||
|
def _extract_text_from_pdf(self, binary):
|
||||||
|
extracted_parts = []
|
||||||
|
if PdfReader:
|
||||||
|
try:
|
||||||
|
reader = PdfReader(BytesIO(binary))
|
||||||
|
extracted_parts.extend(page.extract_text() or "" for page in reader.pages)
|
||||||
|
except Exception as exc:
|
||||||
|
_logger.warning("PdfReader extraction failed: %s", exc)
|
||||||
|
text_content = "\n".join(part for part in extracted_parts if part).strip()
|
||||||
|
if text_content:
|
||||||
|
return text_content
|
||||||
|
if convert_from_bytes and pytesseract:
|
||||||
|
try:
|
||||||
|
images = convert_from_bytes(binary, dpi=300)
|
||||||
|
return "\n".join(
|
||||||
|
pytesseract.image_to_string(image)
|
||||||
|
for image in images
|
||||||
|
).strip()
|
||||||
|
except Exception as exc:
|
||||||
|
_logger.warning("PDF OCR extraction failed: %s", exc)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _extract_text_from_image(self, binary):
|
||||||
|
if not pytesseract or not Image:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
image = Image.open(BytesIO(binary))
|
||||||
|
return pytesseract.image_to_string(image).strip()
|
||||||
|
except Exception as exc:
|
||||||
|
_logger.warning("Image OCR extraction failed: %s", exc)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _extract_text_from_docx(self, binary):
|
||||||
|
if not Document:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
document = Document(BytesIO(binary))
|
||||||
|
return "\n".join(
|
||||||
|
paragraph.text for paragraph in document.paragraphs if paragraph.text
|
||||||
|
).strip()
|
||||||
|
except Exception as exc:
|
||||||
|
_logger.warning("DOCX extraction failed: %s", exc)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _send_to_ai(self, text_content, schema_text, extra_instructions=None):
|
||||||
|
prompt = self._build_prompt(text_content, schema_text, extra_instructions)
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
together_key = self._get_param("document_parser.together_ai_key") or self._get_param("document_parser.together_api_key")
|
||||||
|
openrouter_key = self._get_param("document_parser.openrouter_ai_key") or self._get_param("document_parser.openrouter_api_key")
|
||||||
|
|
||||||
|
if together_key:
|
||||||
|
result, provider_errors = self._call_provider(
|
||||||
|
provider_name="Together",
|
||||||
|
endpoint=self.TOGETHER_ENDPOINT,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {together_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
models=self.TOGETHER_MODELS,
|
||||||
|
prompt=prompt,
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
return result, "together", errors
|
||||||
|
errors.extend(provider_errors)
|
||||||
|
else:
|
||||||
|
errors.append(_("Together AI key is not configured."))
|
||||||
|
|
||||||
|
if openrouter_key:
|
||||||
|
result, provider_errors = self._call_provider(
|
||||||
|
provider_name="OpenRouter",
|
||||||
|
endpoint=self.OPENROUTER_ENDPOINT,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {openrouter_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"HTTP-Referer": self._get_param("web.base.url") or "odoo.local",
|
||||||
|
"X-Title": "Document Parser",
|
||||||
|
},
|
||||||
|
models=self.OPENROUTER_MODELS,
|
||||||
|
prompt=prompt,
|
||||||
|
)
|
||||||
|
if result:
|
||||||
|
return result, "openrouter", errors
|
||||||
|
errors.extend(provider_errors)
|
||||||
|
else:
|
||||||
|
errors.append(_("OpenRouter key is not configured."))
|
||||||
|
|
||||||
|
return {}, False, errors
|
||||||
|
|
||||||
|
def _build_prompt(self, text_content, schema_text, extra_instructions=None):
|
||||||
|
return f"""
|
||||||
|
You are a strict JSON generator.
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Output ONLY valid raw JSON.
|
||||||
|
- No explanation.
|
||||||
|
- No markdown.
|
||||||
|
- No backticks.
|
||||||
|
- No extra text.
|
||||||
|
- Follow schema strictly.
|
||||||
|
- If a field is missing in text, return null.
|
||||||
|
- Scan the entire document carefully before answering.
|
||||||
|
- Extract ONLY what exists in text.
|
||||||
|
- FOR ANY DATES CHANGE FORMAT TO %Y-%m-%d
|
||||||
|
|
||||||
|
FIELD RULES:
|
||||||
|
- If "skills" exists, extract only explicit technical skills written in the document.
|
||||||
|
- Do NOT infer similar skills from role names, responsibilities, or projects.
|
||||||
|
- Normalize names like "Expert Python" to "Python".
|
||||||
|
- Exclude soft skills and business phrases.
|
||||||
|
- Exclude responsibility-style phrases like Cross-Functional Collaboration, Cost Saving, Resource Utilization, Documentation, Reporting, and Team Handling.
|
||||||
|
- Prefer concrete tools, methods, technologies, platforms, certifications, engineering/process methods, and domain techniques explicitly written in the resume.
|
||||||
|
- If the resume explicitly mentions items like AutoCAD, Root Cause Analysis, Project Management, Manufacturing Processes, Lean, Six Sigma, or Quality Control, include them.
|
||||||
|
- Remove duplicates and return each skill only once.
|
||||||
|
- If "email" exists, return one valid normalized email.
|
||||||
|
- If "name" exists, prefer the full name at the top and exclude titles, companies, and addresses.
|
||||||
|
- If "phone" exists, return the most complete phone number found.
|
||||||
|
- If "experience" exists, return only clearly supported numeric values.
|
||||||
|
|
||||||
|
Schema:
|
||||||
|
{schema_text}
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
{extra_instructions or "None"}
|
||||||
|
|
||||||
|
Document:
|
||||||
|
{text_content}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _call_provider(self, provider_name, endpoint, headers, models, prompt):
|
||||||
|
errors = []
|
||||||
|
for model in models:
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [{"role": "user", "content": prompt}],
|
||||||
|
"temperature": 0,
|
||||||
|
"max_tokens": 1500,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(endpoint, headers=headers, json=payload, timeout=90)
|
||||||
|
if response.status_code != 200:
|
||||||
|
message = _("%(provider)s model %(model)s failed with %(status)s: %(body)s") % {
|
||||||
|
"provider": provider_name,
|
||||||
|
"model": model,
|
||||||
|
"status": response.status_code,
|
||||||
|
"body": (response.text or "")[:300],
|
||||||
|
}
|
||||||
|
_logger.warning(message)
|
||||||
|
errors.append(message)
|
||||||
|
continue
|
||||||
|
|
||||||
|
body = response.json()
|
||||||
|
content = self._extract_message_content(body)
|
||||||
|
parsed = self._safe_json_load(content)
|
||||||
|
if parsed:
|
||||||
|
return parsed, errors
|
||||||
|
|
||||||
|
message = _("%(provider)s model %(model)s returned invalid JSON.") % {
|
||||||
|
"provider": provider_name,
|
||||||
|
"model": model,
|
||||||
|
}
|
||||||
|
_logger.warning(message)
|
||||||
|
errors.append(message)
|
||||||
|
except Exception as exc:
|
||||||
|
message = _("%(provider)s model %(model)s error: %(error)s") % {
|
||||||
|
"provider": provider_name,
|
||||||
|
"model": model,
|
||||||
|
"error": str(exc),
|
||||||
|
}
|
||||||
|
_logger.warning(message)
|
||||||
|
errors.append(message)
|
||||||
|
return {}, errors
|
||||||
|
|
||||||
|
def _extract_message_content(self, response_body):
|
||||||
|
try:
|
||||||
|
content = response_body["choices"][0]["message"]["content"]
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
if isinstance(content, list):
|
||||||
|
parts = []
|
||||||
|
for item in content:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
if item.get("type") == "text":
|
||||||
|
parts.append(item.get("text", ""))
|
||||||
|
elif item.get("text"):
|
||||||
|
parts.append(item.get("text"))
|
||||||
|
else:
|
||||||
|
parts.append(str(item))
|
||||||
|
return "\n".join(part for part in parts if part)
|
||||||
|
if isinstance(content, dict):
|
||||||
|
return content.get("text", "")
|
||||||
|
return content or ""
|
||||||
|
|
||||||
|
def _safe_json_load(self, content):
|
||||||
|
if not content:
|
||||||
|
return {}
|
||||||
|
content = content.strip().replace("```json", "").replace("```", "").strip()
|
||||||
|
try:
|
||||||
|
return json.loads(content)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
match = re.search(r"\{[\s\S]*\}", content)
|
||||||
|
if match:
|
||||||
|
try:
|
||||||
|
return json.loads(match.group(0))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_logger.warning("JSON parse failed for provider response: %s", content[:500])
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _extract_with_heuristics(self, text_content, fields):
|
||||||
|
result = {}
|
||||||
|
email_match = re.search(r"([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})", text_content or "", re.I)
|
||||||
|
phone_match = re.search(r"(\+?\d[\d\-\s()]{7,}\d)", text_content or "")
|
||||||
|
linkedin_match = re.search(r"(https?://(?:www\.)?linkedin\.com/[^\s]+)", text_content or "", re.I)
|
||||||
|
name_guess = self._guess_name(text_content or "")
|
||||||
|
skills_guess = self._guess_skills(text_content or "")
|
||||||
|
|
||||||
|
for field_name, field_spec in fields.items():
|
||||||
|
field_type = field_spec.get("type", "string")
|
||||||
|
if field_name in {"email", "email_from"}:
|
||||||
|
result[field_name] = email_match.group(1).lower() if email_match else None
|
||||||
|
elif field_name in {"phone", "mobile", "partner_phone"}:
|
||||||
|
result[field_name] = phone_match.group(1).strip() if phone_match else None
|
||||||
|
elif field_name in {"linkedin_profile", "linkedin"}:
|
||||||
|
result[field_name] = linkedin_match.group(1).strip() if linkedin_match else None
|
||||||
|
elif field_name in {"name", "full_name", "partner_name"}:
|
||||||
|
result[field_name] = name_guess
|
||||||
|
elif field_name == "skills" and field_type == "list":
|
||||||
|
result[field_name] = skills_guess
|
||||||
|
else:
|
||||||
|
result[field_name] = None
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _guess_name(self, text_content):
|
||||||
|
for line in [line.strip() for line in (text_content or "").splitlines() if line.strip()][:12]:
|
||||||
|
cleaned = re.sub(r"[^A-Za-z .'-]", "", line).strip()
|
||||||
|
if len(cleaned.split()) in {2, 3, 4} and not re.search(r"(resume|cv|email|phone|linkedin|skills|experience)", cleaned, re.I):
|
||||||
|
return cleaned
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _guess_skills(self, text_content):
|
||||||
|
section = re.search(r"(skills|technical skills|core competencies)(.*?)(experience|education|projects|certifications|$)", text_content or "", re.I | re.S)
|
||||||
|
if not section:
|
||||||
|
return []
|
||||||
|
parts = re.split(r"[,;\n|•]", section.group(2))
|
||||||
|
cleaned = []
|
||||||
|
for part in parts:
|
||||||
|
value = re.sub(r"\s+", " ", part).strip(" -:\t\r\n")
|
||||||
|
if value and 1 < len(value) < 50 and not re.search(r"^(skills?|experience|education)$", value, re.I):
|
||||||
|
cleaned.append(value)
|
||||||
|
return list(dict.fromkeys(cleaned[:25]))
|
||||||
|
|
||||||
|
def _get_param(self, key):
|
||||||
|
return self.env["ir.config_parameter"].sudo().get_param(key)
|
||||||
|
|
||||||
|
def _normalize_required_fields(self, fields):
|
||||||
|
if isinstance(fields, dict):
|
||||||
|
normalized = {}
|
||||||
|
for field_name, field_value in fields.items():
|
||||||
|
if isinstance(field_value, dict):
|
||||||
|
normalized[field_name] = {
|
||||||
|
"type": field_value.get("type", "string"),
|
||||||
|
"description": field_value.get("description", field_name.replace("_", " ").title()),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
normalized[field_name] = {
|
||||||
|
"type": "string",
|
||||||
|
"description": str(field_value or field_name.replace("_", " ").title()),
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
if isinstance(fields, list):
|
||||||
|
return {field_name: {"type": "string", "description": field_name.replace("_", " ").title()} for field_name in fields}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _build_json_schema_text(self, fields):
|
||||||
|
return json.dumps(fields, ensure_ascii=True)
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from odoo import _, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = "res.config.settings"
|
||||||
|
|
||||||
|
together_ai_key = fields.Char(
|
||||||
|
string="Together AI Key",
|
||||||
|
config_parameter="document_parser.together_ai_key",
|
||||||
|
)
|
||||||
|
openrouter_ai_key = fields.Char(
|
||||||
|
string="OpenRouter AI Key",
|
||||||
|
config_parameter="document_parser.openrouter_ai_key",
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_test_together_ai_connection(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.together_ai_key:
|
||||||
|
raise UserError(_("Please add the Together AI key first."))
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
"https://api.together.xyz/v1/models",
|
||||||
|
headers={"Authorization": f"Bearer {self.together_ai_key}"},
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
if response.ok:
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.client",
|
||||||
|
"tag": "display_notification",
|
||||||
|
"params": {
|
||||||
|
"title": _("Together AI Connection"),
|
||||||
|
"message": _("Connection successful."),
|
||||||
|
"type": "success",
|
||||||
|
"sticky": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
raise UserError(_("Together AI connection failed: %s") % (response.text or response.reason))
|
||||||
|
|
||||||
|
def action_test_openrouter_ai_connection(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.openrouter_ai_key:
|
||||||
|
raise UserError(_("Please add the OpenRouter key first."))
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
"https://openrouter.ai/api/v1/models",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {self.openrouter_ai_key}",
|
||||||
|
"HTTP-Referer": self.env["ir.config_parameter"].sudo().get_param("web.base.url", ""),
|
||||||
|
"X-Title": "Odoo Document Parser",
|
||||||
|
},
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
if response.ok:
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.client",
|
||||||
|
"tag": "display_notification",
|
||||||
|
"params": {
|
||||||
|
"title": _("OpenRouter Connection"),
|
||||||
|
"message": _("Connection successful."),
|
||||||
|
"type": "success",
|
||||||
|
"sticky": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
raise UserError(_("OpenRouter connection failed: %s") % (response.text or response.reason))
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="res_config_settings_view_form_document_parser" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.document.parser</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="priority" eval="80"/>
|
||||||
|
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//form" position="inside">
|
||||||
|
<app string="Document Parser" name="document_parser" groups="base.group_system">
|
||||||
|
<block title="AI Providers" name="document_parser_ai_provider_block">
|
||||||
|
<setting string="Together AI Key"
|
||||||
|
help="Primary provider used first for structured document extraction."
|
||||||
|
id="document_parser_together_ai_key">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<field name="together_ai_key" password="True" placeholder="together.ai API key"/>
|
||||||
|
<button name="action_test_together_ai_connection"
|
||||||
|
string="Test Connection"
|
||||||
|
type="object"
|
||||||
|
class="btn btn-secondary"/>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="OpenRouter AI Key"
|
||||||
|
help="Fallback provider used when Together AI is unavailable or quota is exhausted."
|
||||||
|
id="document_parser_openrouter_ai_key">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<field name="openrouter_ai_key" password="True" placeholder="openrouter API key"/>
|
||||||
|
<button name="action_test_openrouter_ai_connection"
|
||||||
|
string="Test Connection"
|
||||||
|
type="object"
|
||||||
|
class="btn btn-secondary"/>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
</app>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
from . import models
|
from . import models
|
||||||
from . import wizards
|
from . import wizards
|
||||||
|
from . import controllers
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
'version': '0.1',
|
'version': '0.1',
|
||||||
|
|
||||||
# any module necessary for this one to work correctly
|
# any module necessary for this one to work correctly
|
||||||
'depends': ['base','hr','hr_payroll','hr_employee_extended'],
|
'depends': ['base','hr','hr_payroll','hr_employee_extended','l10n_in_hr_payroll'],
|
||||||
|
|
||||||
# always loaded
|
# always loaded
|
||||||
'data': [
|
'data': [
|
||||||
|
|
@ -48,12 +48,15 @@
|
||||||
'report/report_action.xml',
|
'report/report_action.xml',
|
||||||
'report/it_tax_template.xml',
|
'report/it_tax_template.xml',
|
||||||
'views/it_tax_menu_and_wizard_view.xml',
|
'views/it_tax_menu_and_wizard_view.xml',
|
||||||
|
'views/employee_payslip_download_wizard_views.xml',
|
||||||
|
'wizards/hr_tds_calculation.xml',
|
||||||
'wizards/children_education_costing.xml',
|
'wizards/children_education_costing.xml',
|
||||||
'wizards/employee_life_insurance.xml',
|
'wizards/employee_life_insurance.xml',
|
||||||
'wizards/nsc_declaration.xml',
|
'wizards/nsc_declaration.xml',
|
||||||
'wizards/self_occupied_property.xml',
|
'wizards/self_occupied_property.xml',
|
||||||
'wizards/letout_house_property.xml',
|
'wizards/letout_house_property.xml',
|
||||||
'wizards/nsc_income_loss.xml',
|
'wizards/nsc_income_loss.xml',
|
||||||
|
'data/default_investment_types.xml',
|
||||||
# 'views/it_investment_type.xml',
|
# 'views/it_investment_type.xml',
|
||||||
# 'views/it_investment_costing.xml'
|
# 'views/it_investment_costing.xml'
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import main
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import io
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from odoo import _
|
||||||
|
from odoo.exceptions import AccessError, UserError
|
||||||
|
from odoo.http import Controller, content_disposition, request, route
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeePayslipDownloadController(Controller):
|
||||||
|
|
||||||
|
@route('/employee_it_declaration/my_payslips/<int:wizard_id>', type='http', auth='user')
|
||||||
|
def download_my_payslips(self, wizard_id, **kwargs):
|
||||||
|
wizard = request.env['employee.payslip.download.wizard'].browse(wizard_id)
|
||||||
|
if not wizard.exists() or wizard.create_uid != request.env.user:
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
try:
|
||||||
|
payslips = wizard._get_payslips_for_download()
|
||||||
|
except (AccessError, UserError):
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
if wizard.download_type == 'single':
|
||||||
|
payslip = payslips[:1]
|
||||||
|
report, pdf_content = wizard._get_pdf_content(payslip)
|
||||||
|
headers = [
|
||||||
|
('Content-Type', 'application/pdf'),
|
||||||
|
('Content-Length', len(pdf_content)),
|
||||||
|
('Content-Disposition', content_disposition(wizard._get_pdf_filename(payslip, report))),
|
||||||
|
]
|
||||||
|
return request.make_response(pdf_content, headers=headers)
|
||||||
|
|
||||||
|
zip_buffer = io.BytesIO()
|
||||||
|
used_filenames = set()
|
||||||
|
with zipfile.ZipFile(zip_buffer, 'w', compression=zipfile.ZIP_DEFLATED) as payslip_zip:
|
||||||
|
for payslip in payslips:
|
||||||
|
report, pdf_content = wizard._get_pdf_content(payslip)
|
||||||
|
filename = self._deduplicate_filename(wizard._get_pdf_filename(payslip, report), used_filenames)
|
||||||
|
payslip_zip.writestr(filename, pdf_content)
|
||||||
|
|
||||||
|
zip_content = zip_buffer.getvalue()
|
||||||
|
headers = [
|
||||||
|
('Content-Type', 'application/zip'),
|
||||||
|
('Content-Length', len(zip_content)),
|
||||||
|
('Content-Disposition', content_disposition(wizard._get_zip_filename())),
|
||||||
|
]
|
||||||
|
return request.make_response(zip_content, headers=headers)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _deduplicate_filename(filename, used_filenames):
|
||||||
|
if filename not in used_filenames:
|
||||||
|
used_filenames.add(filename)
|
||||||
|
return filename
|
||||||
|
|
||||||
|
stem, extension = filename.rsplit('.', 1) if '.' in filename else (filename, '')
|
||||||
|
counter = 2
|
||||||
|
while True:
|
||||||
|
candidate = _('%(stem)s (%(counter)s)', stem=stem, counter=counter)
|
||||||
|
if extension:
|
||||||
|
candidate = '%s.%s' % (candidate, extension)
|
||||||
|
if candidate not in used_filenames:
|
||||||
|
used_filenames.add(candidate)
|
||||||
|
return candidate
|
||||||
|
counter += 1
|
||||||
|
|
@ -0,0 +1,579 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="0">
|
||||||
|
<record id="investment_type_past_employment" model="it.investment.type">
|
||||||
|
<field name="sequence">5</field>
|
||||||
|
<field name="investment_type">past_employment</field>
|
||||||
|
<field name="regime">both</field>
|
||||||
|
</record>
|
||||||
|
<record id="investment_type_us80c" model="it.investment.type">
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="investment_type">us_80c</field>
|
||||||
|
<field name="regime">both</field>
|
||||||
|
</record>
|
||||||
|
<record id="investment_type_us80d" model="it.investment.type">
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="investment_type">us_80d</field>
|
||||||
|
<field name="regime">both</field>
|
||||||
|
</record>
|
||||||
|
<record id="investment_type_us10" model="it.investment.type">
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="investment_type">us_10</field>
|
||||||
|
<field name="regime">both</field>
|
||||||
|
</record>
|
||||||
|
<record id="investment_type_us80g" model="it.investment.type">
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="investment_type">us_80g</field>
|
||||||
|
<field name="regime">both</field>
|
||||||
|
</record>
|
||||||
|
<record id="investment_type_chapter_via" model="it.investment.type">
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="investment_type">chapter_via</field>
|
||||||
|
<field name="regime">both</field>
|
||||||
|
</record>
|
||||||
|
<record id="investment_type_us17" model="it.investment.type">
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="investment_type">us_17</field>
|
||||||
|
<field name="regime">both</field>
|
||||||
|
</record>
|
||||||
|
<record id="investment_type_house_rent" model="it.investment.type">
|
||||||
|
<field name="sequence">70</field>
|
||||||
|
<field name="investment_type">house_rent</field>
|
||||||
|
<field name="regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="investment_type_other_income_loss" model="it.investment.type">
|
||||||
|
<field name="sequence">80</field>
|
||||||
|
<field name="investment_type">other_i_or_l</field>
|
||||||
|
<field name="regime">both</field>
|
||||||
|
</record>
|
||||||
|
<record id="investment_type_other_declaration" model="it.investment.type">
|
||||||
|
<field name="sequence">90</field>
|
||||||
|
<field name="investment_type">other_declaration</field>
|
||||||
|
<field name="regime">both</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Past Employment: required for periods before 2026-2027, configurable on Payroll Periods -->
|
||||||
|
<record id="past_emp_total_income" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="name">Previous Employer Total Income (Gross Salary + Any Other Income)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PETI</field>
|
||||||
|
<field name="tax_regime">both</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_us10_lta_exemption" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="name">Previous Employer LESS: US10 LTA exemption</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PELTA</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_us10_gratuity_exemption" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="name">Previous Employer LESS: US10 Gratuity exemption</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PEGRAT</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_us10_leave_encashment_exemption" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="name">Previous Employer LESS: US10 Leave encashment exemption</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PELEAV</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_us10_others_exemption" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="name">Previous Employer LESS: US10 Others (HRAUniformWashingetc)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PEOTH</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_prof_tax" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="name">Previous Employer Prof.Tax</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PEPROF</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_standard_deduction" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">70</field>
|
||||||
|
<field name="name">Previous Employer Standard Deduction Benefit Claimed</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PESTD</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_invest_us80c" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">80</field>
|
||||||
|
<field name="name">Previous Employer Invest US 80C</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PEIUSC</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_chapter_via" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">90</field>
|
||||||
|
<field name="name">Previous Employer Chapter IVAUS80CCCothers</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PECHAP</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_net_taxable_income" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">100</field>
|
||||||
|
<field name="name">Net Taxable Income</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PENET</field>
|
||||||
|
<field name="compute_method">1</field>
|
||||||
|
<field name="compute_code">PETI - PELTA - PEGRAT - PELEAV - PEOTH - PEPROF - PESTD - PEIUSC - PECHAP</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_taxable_income_after_exemption" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">110</field>
|
||||||
|
<field name="name">Previous Employer Taxable Income After Exemption (Gross - US10 - Chapter VIA US80C) Except Prof.Tax</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PEAFT</field>
|
||||||
|
<field name="compute_method">1</field>
|
||||||
|
<field name="compute_code">PETI - PELTA - PEGRAT - PELEAV - PEOTH - PEIUSC - PECHAP</field>
|
||||||
|
<field name="tax_regime">both</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_tax" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">120</field>
|
||||||
|
<field name="name">Previous Employer Tax</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PETAX</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_surcharge" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">130</field>
|
||||||
|
<field name="name">Previous Employer Surcharge</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PESUR</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_cess" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">140</field>
|
||||||
|
<field name="name">Previous Employer Cess</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PECESS</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_tds_deduction" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">150</field>
|
||||||
|
<field name="name">Previous Employer TDS Deduction</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PETDS</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_hra_exemptions" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">160</field>
|
||||||
|
<field name="name">Previous Employer House Rent Allowance Exemptions</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PEHRA</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_conveyance_exemptions" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">170</field>
|
||||||
|
<field name="name">Previous Employer Conveyance Exemptions</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PECON</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_medical_reimbursement_exemptions" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">180</field>
|
||||||
|
<field name="name">Previous Employer Medical Reimbursement Exemptions</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PEMED</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_lta_exemption" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">190</field>
|
||||||
|
<field name="name">Previous Employer LTA Exemption</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PELT2</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_children_exemption" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">200</field>
|
||||||
|
<field name="name">Previous Employer Children Exemption</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PECHLD</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_other_exemption" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">210</field>
|
||||||
|
<field name="name">Previous Employer other Exemption</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PEOT2</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_infra_exemption" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">220</field>
|
||||||
|
<field name="name">Previous Employer Infra exemption</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PEINF</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="past_emp_gratuity_received" model="past_employment.investment.type">
|
||||||
|
<field name="sequence">230</field>
|
||||||
|
<field name="name">Previous Employer Gratuity Received</field>
|
||||||
|
<field name="investment_type" ref="investment_type_past_employment"/>
|
||||||
|
<field name="investment_code">PEGR2</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- US 80C: Old regime deductions -->
|
||||||
|
<record id="us80c_nps_employee" model="us80c.investment.type">
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="name">US 80CCD(1) - Contribution to NPS Scheme (10% of salary)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80NPS</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_elss" model="us80c.investment.type">
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="name">Mutual Fund - Equity Linked Savings Scheme (ELSS)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80ELSS</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_pension_funds" model="us80c.investment.type">
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="name">US 80CCC - Pension Funds</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80PEN</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_children_education_fees" model="us80c.investment.type">
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="name">Children Education Fees</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80EDU</field>
|
||||||
|
<field name="require_action">1</field>
|
||||||
|
<field name="action_id" ref="action_children_education"/>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_ppf" model="us80c.investment.type">
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="name">Public Provident Fund (PPF)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80PPF</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_lic_premiums" model="us80c.investment.type">
|
||||||
|
<field name="sequence">60</field>
|
||||||
|
<field name="name">LIC - Life Insurance Premiums</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80LIC</field>
|
||||||
|
<field name="require_action">1</field>
|
||||||
|
<field name="action_id" ref="action_us80c_insurance_line"/>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_nsc" model="us80c.investment.type">
|
||||||
|
<field name="sequence">70</field>
|
||||||
|
<field name="name">National Savings Certificate (NSC)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80NSC</field>
|
||||||
|
<field name="require_action">1</field>
|
||||||
|
<field name="action_id" ref="action_nsc_declaration_line"/>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_ulip" model="us80c.investment.type">
|
||||||
|
<field name="sequence">80</field>
|
||||||
|
<field name="name">Unit linked Insurance Plan (ULIP)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80ULIP</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_five_year_bank_fd" model="us80c.investment.type">
|
||||||
|
<field name="sequence">90</field>
|
||||||
|
<field name="name">5-Yr bank fixed deposits (FDs)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80FD5</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_post_office_time_deposit" model="us80c.investment.type">
|
||||||
|
<field name="sequence">100</field>
|
||||||
|
<field name="name">5-Yr post office time deposit (POTD) scheme</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80POT</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_home_loan_principal" model="us80c.investment.type">
|
||||||
|
<field name="sequence">110</field>
|
||||||
|
<field name="name">Certificate provided for Home Loan Principal Repayment</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80HLP</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_infrastructure_fund" model="us80c.investment.type">
|
||||||
|
<field name="sequence">120</field>
|
||||||
|
<field name="name">Infrastructure Fund</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80INF</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_nabard_rural_bonds" model="us80c.investment.type">
|
||||||
|
<field name="sequence">130</field>
|
||||||
|
<field name="name">NABARD rural bonds</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80NRB</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_pf" model="us80c.investment.type">
|
||||||
|
<field name="sequence">140</field>
|
||||||
|
<field name="name">Provident Fund (PF)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80PF</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_scss" model="us80c.investment.type">
|
||||||
|
<field name="sequence">150</field>
|
||||||
|
<field name="name">Senior Citizen Savings Scheme 2004 (SCSS)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80SCS</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_stamp_duty_registration" model="us80c.investment.type">
|
||||||
|
<field name="sequence">160</field>
|
||||||
|
<field name="name">Stamp Duty and Registration Charges for a home</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80SDR</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_superannuation" model="us80c.investment.type">
|
||||||
|
<field name="sequence">170</field>
|
||||||
|
<field name="name">Superannuation</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80SUP</field>
|
||||||
|
<field name="limit">150000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_vpf" model="us80c.investment.type">
|
||||||
|
<field name="sequence">180</field>
|
||||||
|
<field name="name">Voluntary Provident Fund (VPF)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80VPF</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80c_sukanya_samriddhi" model="us80c.investment.type">
|
||||||
|
<field name="sequence">190</field>
|
||||||
|
<field name="name">Sukanya Samriddhi Scheme</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80c"/>
|
||||||
|
<field name="investment_code">C80SSS</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- US 80D: Old regime deductions -->
|
||||||
|
<record id="us80d_medical_insurance_self" model="us80d.investment.type">
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="name">US 80D - Medical Insurance Premium Self</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80d"/>
|
||||||
|
<field name="for_family">1</field>
|
||||||
|
<field name="limit">25000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80d_preventive_health_checkup_self" model="us80d.investment.type">
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="name">US 80D - Preventive Health Checkup (Self)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80d"/>
|
||||||
|
<field name="for_family">1</field>
|
||||||
|
<field name="limit">5000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80d_medical_insurance_parents_below_60" model="us80d.investment.type">
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="name">US 80D - Medical Insurance Premium Parents below 60 years</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80d"/>
|
||||||
|
<field name="for_parents">1</field>
|
||||||
|
<field name="limit">25000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80d_medical_insurance_senior_parents" model="us80d.investment.type">
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="name">US 80D - Medical Insurance Premium Parents (Senior Citizen)</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80d"/>
|
||||||
|
<field name="for_senior_parent">1</field>
|
||||||
|
<field name="limit">50000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us80d_medical_expenditure_very_senior" model="us80d.investment.type">
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="name">US 80D - Medical Insurance Expenditure Very Senior Citizen</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us80d"/>
|
||||||
|
<field name="for_senior_parent">1</field>
|
||||||
|
<field name="limit">50000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- US 10: Old regime exemptions -->
|
||||||
|
<record id="us10_children_education_allowance" model="us10.investment.type">
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="name">Children Education Allowance Exemptions</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us10"/>
|
||||||
|
<field name="limit">72000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us10_retrenchment_exemptions" model="us10.investment.type">
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="name">Retrenchment Exemptions</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us10"/>
|
||||||
|
<field name="limit">500000</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us10_leave_travel_allowance" model="us10.investment.type">
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="name">Leave Travel Allowance Exemptions</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us10"/>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us10_medical_reimbursement" model="us10.investment.type">
|
||||||
|
<field name="sequence">40</field>
|
||||||
|
<field name="name">Medical Reimbursement Exemptions</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us10"/>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="us10_uniform_allowance" model="us10.investment.type">
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="name">Uniform Allowance US10</field>
|
||||||
|
<field name="investment_type" ref="investment_type_us10"/>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- US 80G: Old regime donations -->
|
||||||
|
<record id="us80g_donations" model="us80g.investment.type"><field name="sequence">10</field><field name="name">US 80G - Donations</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_national_defence_fund" model="us80g.investment.type"><field name="sequence">20</field><field name="name">01-National Defence Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_pm_national_relief_fund" model="us80g.investment.type"><field name="sequence">30</field><field name="name">02-PM's National Relief Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_pm_armenia_earthquake_relief_fund" model="us80g.investment.type"><field name="sequence">40</field><field name="name">03-PM's Armenia Earthquake Relief Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_africa_public_contributions_india_fund" model="us80g.investment.type"><field name="sequence">50</field><field name="name">04-Africa (Public Contributions - India) Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_national_foundation_communal_harmony" model="us80g.investment.type"><field name="sequence">60</field><field name="name">05-National Foundation for Communal Harmony</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_university_national_eminence" model="us80g.investment.type"><field name="sequence">70</field><field name="name">06-University/Educational Institution of National Eminence</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_maharashtra_cm_earthquake_relief_fund" model="us80g.investment.type"><field name="sequence">80</field><field name="name">07-Maharashtra CM's Earthquake relief Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_zila_saksharta_samiti" model="us80g.investment.type"><field name="sequence">90</field><field name="name">08-Zila Saksharta Samiti</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_national_blood_transfusion_council" model="us80g.investment.type"><field name="sequence">100</field><field name="name">10-The National Blood Transfusion Council</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_state_medical_relief_fund" model="us80g.investment.type"><field name="sequence">110</field><field name="name">11-State Government medical relief to the poor</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_army_central_welfare_fund" model="us80g.investment.type"><field name="sequence">120</field><field name="name">12-The Army Central Welfare Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_indian_naval_benevolent_fund" model="us80g.investment.type"><field name="sequence">130</field><field name="name">13-The Indian Naval Benevolent Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_air_force_central_welfare_fund" model="us80g.investment.type"><field name="sequence">140</field><field name="name">14-The Air Force Central Welfare Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_andhra_cm_cyclone_relief_fund_1996" model="us80g.investment.type"><field name="sequence">150</field><field name="name">15-Andhra Pradesh CM's Cyclone Relief Fund 1996</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_national_illness_assistance_fund" model="us80g.investment.type"><field name="sequence">160</field><field name="name">16-National Illness Assistance Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_cm_relief_fund_state" model="us80g.investment.type"><field name="sequence">170</field><field name="name">17-The CM's Relief Fund of any State</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_lg_relief_fund_union_territory" model="us80g.investment.type"><field name="sequence">180</field><field name="name">18-Lt. Governor's Relief Fund - Union Territory</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_national_sports_fund_central" model="us80g.investment.type"><field name="sequence">190</field><field name="name">19-National sports Fund - Central Government</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_national_cultural_fund_central" model="us80g.investment.type"><field name="sequence">200</field><field name="name">20-National Cultural Fund - Central Government</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_fund_technology_development_central" model="us80g.investment.type"><field name="sequence">210</field><field name="name">21-Fund for Tech Devt - Central Government</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_welfare_persons_disabilities" model="us80g.investment.type"><field name="sequence">220</field><field name="name">22-Welfare of persons with Disabilities</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_ay_trust_fund" model="us80g.investment.type"><field name="sequence">230</field><field name="name">23-Any Trust, institution or Fund covered under section 80G providing relief to the victims of earthquake in Gujarat, provided such donation is made between 26/01/2001 to 30/09/2001</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_pm_drought_relief_fund" model="us80g.investment.type"><field name="sequence">240</field><field name="name">25-Prime Minister's Drought Relief Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_family_planning" model="us80g.investment.type"><field name="sequence">250</field><field name="name">29-For promoting family planning</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_olympic_association" model="us80g.investment.type"><field name="sequence">260</field><field name="name">30-To any Olympic Association or to any other association or institution established in India and notified by the Central Government for Development of infrastructure of sports and Games, or sponsorship for sports or Games</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_slum_dwellers" model="us80g.investment.type"><field name="sequence">270</field><field name="name">31-For other than promoting family planning</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_charitable_trust_approval" model="us80g.investment.type"><field name="sequence">280</field><field name="name">32-Institutions/Charitable Trust with 80G approval</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_improvement_cities_towns_villages" model="us80g.investment.type"><field name="sequence">290</field><field name="name">33-For improvement of cities, towns or villages</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_corporation_minority" model="us80g.investment.type"><field name="sequence">300</field><field name="name">34-Corporation-Promoting Interests in Minority</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_renovation_notified_places_temples" model="us80g.investment.type"><field name="sequence">310</field><field name="name">35-For renovation-Notified Places like Temples</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_national_childrens_fund" model="us80g.investment.type"><field name="sequence">320</field><field name="name">36-National Children's Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_swachh_bharat_kosh" model="us80g.investment.type"><field name="sequence">330</field><field name="name">37-Swachh Bharat Kosh</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_clean_ganga_fund" model="us80g.investment.type"><field name="sequence">340</field><field name="name">38-Clean Ganga Fund</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us80g_drug_abuse_control_fund" model="us80g.investment.type"><field name="sequence">350</field><field name="name">39-National Fund for Control of Drug Abuse</field><field name="investment_type" ref="investment_type_us80g"/><field name="tax_regime">old</field></record>
|
||||||
|
|
||||||
|
<!-- Chapter VIA: old regime deductions plus allowed new-regime employer NPS -->
|
||||||
|
<record id="chapter_via_rgess" model="chapter.via.investment.type"><field name="sequence">10</field><field name="name">US 80CCG - Investments in RGESS</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">25000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80ddb_medical_treatment" model="chapter.via.investment.type"><field name="sequence">20</field><field name="name">US 80DDB - Maintenance Including medical treatment</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">125000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80ddb_non_senior" model="chapter.via.investment.type"><field name="sequence">30</field><field name="name">US 80DDB - Medical treatment for non senior citizens</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">40000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80ddbs_senior" model="chapter.via.investment.type"><field name="sequence">40</field><field name="name">US 80DDBS - Medical treatment for senior citizens</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">100000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80e_higher_education" model="chapter.via.investment.type"><field name="sequence">50</field><field name="name">US 80E - Higher education</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80ee_home_loan_interest" model="chapter.via.investment.type"><field name="sequence">60</field><field name="name">US 80EE - Interest Paid On Home Loan</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">50000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80gg_house_rent" model="chapter.via.investment.type"><field name="sequence">70</field><field name="name">US 80GG - House Rent Exemption</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">60000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_us24_home_loan_interest" model="chapter.via.investment.type"><field name="sequence">80</field><field name="name">US 24 - Certificate provided for Interest Paid On Home Loan</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">200000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_us24_first_time_buyer" model="chapter.via.investment.type"><field name="sequence">90</field><field name="name">US 24 - Interest Paid On Home Loan (First time buyer)</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">50000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_us24_letout_property" model="chapter.via.investment.type"><field name="sequence">100</field><field name="name">US 24 - Interest Paid On Home Loan For Let Out Property</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">200000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_us24_before_april_1999" model="chapter.via.investment.type"><field name="sequence">110</field><field name="name">US 24 - Interest Paid On Loan Before 1st April 1999</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">30000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80ccd_employee_contribution" model="chapter.via.investment.type"><field name="sequence">120</field><field name="name">US 80CCD - National Pension Scheme (Employee Contribution)</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">50000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80ccd2_employer_contribution" model="chapter.via.investment.type"><field name="sequence">130</field><field name="name">US 80CCD (2) - National Pension Scheme (Employer Contribution)</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="tax_regime">both</field></record>
|
||||||
|
<record id="chapter_via_80ccg_outside_nps_employer" model="chapter.via.investment.type"><field name="sequence">140</field><field name="name">US 80CCG Outside National Pension Scheme (Employee Contribution)</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">50000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80ccf_infra_bonds" model="chapter.via.investment.type"><field name="sequence">150</field><field name="name">US 80CCF - Long term Infrastructure bonds</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80qqb_royalty_books" model="chapter.via.investment.type"><field name="sequence">160</field><field name="name">US 80QQB - Royalty on Books</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">300000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80rrb_royalty_patents" model="chapter.via.investment.type"><field name="sequence">170</field><field name="name">US 80RRB - Royalty on patents</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">300000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80tta_saving_interest" model="chapter.via.investment.type"><field name="sequence">180</field><field name="name">US 80TTA - Interest on Saving accounts</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">10000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80u_disability" model="chapter.via.investment.type"><field name="sequence">190</field><field name="name">US 80U - Disability</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">125000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80ttb_senior_interest" model="chapter.via.investment.type"><field name="sequence">200</field><field name="name">US80TTB - For Senior Citizen, exempt Interest from FDs, Post Office</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">50000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80eea_home_loan" model="chapter.via.investment.type"><field name="sequence">210</field><field name="name">US80EEA - Home Loans Taken on Self-Occupied House Property BY 31-Mar-2020</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">150000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="chapter_via_80eeb_electric_vehicle" model="chapter.via.investment.type"><field name="sequence">220</field><field name="name">US80EEB - ELECTRONIC VEHICLE EXEMPTION</field><field name="investment_type" ref="investment_type_chapter_via"/><field name="limit">150000</field><field name="tax_regime">old</field></record>
|
||||||
|
|
||||||
|
<!-- US 17: Old regime reimbursements and allowances -->
|
||||||
|
<record id="us17_academic_allowance" model="us17.investment.type"><field name="sequence">10</field><field name="name">Academic Allowance</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_helper_allowance" model="us17.investment.type"><field name="sequence">20</field><field name="name">Helper Allowance</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_petrol_allowance" model="us17.investment.type"><field name="sequence">30</field><field name="name">Petrol Allowance</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_utility_reimbursement" model="us17.investment.type"><field name="sequence">40</field><field name="name">Utility Reimbursement</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_books_proofs" model="us17.investment.type"><field name="sequence">50</field><field name="name">Books Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_car_maintenance_reimbursement" model="us17.investment.type"><field name="sequence">60</field><field name="name">Car Maintenance Reimbursement</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_car_maintenance_small_car" model="us17.investment.type"><field name="sequence">70</field><field name="name">Car Maintenance - Small Upto 1600 CC</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_cell_phone_proofs" model="us17.investment.type"><field name="sequence">80</field><field name="name">Cell Phone Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_conveyance_proofs" model="us17.investment.type"><field name="sequence">90</field><field name="name">Conveyance Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_driver_salary_reimbursement" model="us17.investment.type"><field name="sequence">100</field><field name="name">Driver Salary Reimbursement</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_driver_salary_reimbursement_small_car" model="us17.investment.type"><field name="sequence">110</field><field name="name">Driver Salary Reimbursement - Small Car Upto 1600 CC</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_driver_proofs" model="us17.investment.type"><field name="sequence">120</field><field name="name">Driver Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="limit">36000</field><field name="tax_regime">both</field></record>
|
||||||
|
<record id="us17_driver_petrol_car_maintenance" model="us17.investment.type"><field name="sequence">130</field><field name="name">Driver, Petrol & Car Maintenance Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">both</field></record>
|
||||||
|
<record id="us17_petrol_car_maintenance" model="us17.investment.type"><field name="sequence">140</field><field name="name">Petrol & Car Maintenance Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="limit">84000</field><field name="tax_regime">both</field></record>
|
||||||
|
<record id="us17_entertainment_proofs" model="us17.investment.type"><field name="sequence">150</field><field name="name">Entertainment Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_fuel_reimbursement" model="us17.investment.type"><field name="sequence">160</field><field name="name">Fuel Reimbursement</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_general_other_reimbursement" model="us17.investment.type"><field name="sequence">170</field><field name="name">General Other Reimbursement</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_gift_reimbursement" model="us17.investment.type"><field name="sequence">180</field><field name="name">Gift Reimbursement</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_internet_reimbursement" model="us17.investment.type"><field name="sequence">190</field><field name="name">Internet Reimbursement</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_journal_reimbursement" model="us17.investment.type"><field name="sequence">200</field><field name="name">Journal Reimbursement</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_maintenance_reimbursement" model="us17.investment.type"><field name="sequence">210</field><field name="name">Maintenance Reimbursement</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_meal_proofs" model="us17.investment.type"><field name="sequence">220</field><field name="name">Meal Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_news_paper_proofs" model="us17.investment.type"><field name="sequence">230</field><field name="name">News Paper Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_parking_proofs" model="us17.investment.type"><field name="sequence">240</field><field name="name">Parking Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">both</field></record>
|
||||||
|
<record id="us17_telephone_allowance" model="us17.investment.type"><field name="sequence">250</field><field name="name">Telephone Allowance</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="us17_toll_proofs" model="us17.investment.type"><field name="sequence">260</field><field name="name">Toll Proofs</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">both</field></record>
|
||||||
|
<record id="us17_uniform_reimbursement" model="us17.investment.type"><field name="sequence">270</field><field name="name">Uniform Reimbursement</field><field name="investment_type" ref="investment_type_us17"/><field name="tax_regime">old</field></record>
|
||||||
|
|
||||||
|
<!-- Other income/loss: both-regime taxable income plus old-regime details -->
|
||||||
|
<record id="other_il_self_occupied_house_property" model="other.il.investment.type"><field name="sequence">10</field><field name="name">Self occupied house property U/S 24</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="require_action">1</field><field name="action_id" ref="action_self_occupied_property"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="other_il_bank_interest" model="other.il.investment.type"><field name="sequence">20</field><field name="name">Bank Interest</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="other_il_debenture_interest" model="other.il.investment.type"><field name="sequence">30</field><field name="name">Debenture Interest</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="other_il_external_income_others" model="other.il.investment.type"><field name="sequence">40</field><field name="name">External Income Others</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="other_il_income_let_out_house_property" model="other.il.investment.type"><field name="sequence">50</field><field name="name">Income on Let Out House Property</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="require_action">1</field><field name="action_id" ref="action_letout_house_property"/><field name="tax_regime">both</field></record>
|
||||||
|
<record id="other_il_loss_let_out_house_property" model="other.il.investment.type"><field name="sequence">60</field><field name="name">Loss on Let Out House Property</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="limit">-200000</field><field name="tax_regime">old</field></record>
|
||||||
|
<record id="other_il_interest_nsc_80i" model="other.il.investment.type"><field name="sequence">70</field><field name="name">Interest on NSC (80 I)</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="require_action">1</field><field name="action_id" ref="action_nsc_interest_line"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="other_il_previous_employer_tax_free_income" model="other.il.investment.type"><field name="sequence">80</field><field name="name">Previous Employer Tax free other income</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="other_il_fully_taxable_income" model="other.il.investment.type"><field name="sequence">90</field><field name="name">Fully Taxable Income</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="other_il_fully_taxable_other_income" model="other.il.investment.type"><field name="sequence">100</field><field name="name">Fully Taxable Other Income</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="tax_regime">both</field></record>
|
||||||
|
<record id="other_il_external_inc_var_percentage" model="other.il.investment.type"><field name="sequence">110</field><field name="name">EXTERNAL_INC_VAR_PERCENTAGE</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="tax_regime">old</field></record>
|
||||||
|
<record id="other_il_previous_employer_other_income" model="other.il.investment.type"><field name="sequence">120</field><field name="name">Previous Employer Other Income</field><field name="investment_type" ref="investment_type_other_income_loss"/><field name="tax_regime">old</field></record>
|
||||||
|
|
||||||
|
<!-- Other declarations: old regime supporting values -->
|
||||||
|
<record id="other_declaration_medical_insurance_manual_input" model="other.declaration.investment.type">
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="name">Medical Insurance Premium Manual Input</field>
|
||||||
|
<field name="investment_type" ref="investment_type_other_declaration"/>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="other_declaration_hostel_going_children" model="other.declaration.investment.type">
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="name">Number of Hostel going children</field>
|
||||||
|
<field name="investment_type" ref="investment_type_other_declaration"/>
|
||||||
|
<field name="limit">2</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
<record id="other_declaration_school_going_children" model="other.declaration.investment.type">
|
||||||
|
<field name="sequence">30</field>
|
||||||
|
<field name="name">Number of school going children</field>
|
||||||
|
<field name="investment_type" ref="investment_type_other_declaration"/>
|
||||||
|
<field name="limit">2</field>
|
||||||
|
<field name="tax_regime">old</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
from . import payroll_periods
|
from . import payroll_periods
|
||||||
from . import investment_types
|
from . import investment_types
|
||||||
from . import investment_costings
|
|
||||||
from . import emp_it_declaration
|
from . import emp_it_declaration
|
||||||
|
from . import investment_costings
|
||||||
from . import slab_master
|
from . import slab_master
|
||||||
from . import it_tax_statement
|
from . import it_tax_statement
|
||||||
from . import it_tax_statement_wiz
|
from . import it_tax_statement_wiz
|
||||||
|
from . import employee_payslip_download_wiz
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,39 @@
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from odoo import models, fields, api, _
|
from odoo import models, fields, api, _
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import UserError
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import calendar
|
|
||||||
|
class ITDeclarationSubmittedLockMixin(models.AbstractModel):
|
||||||
|
_name = 'it.declaration.submitted.lock.mixin'
|
||||||
|
_description = 'IT Declaration Submitted Lock Mixin'
|
||||||
|
|
||||||
|
def _get_related_it_declarations(self):
|
||||||
|
if 'it_declaration_id' in self._fields:
|
||||||
|
return self.mapped('it_declaration_id')
|
||||||
|
if 'child_education_id' in self._fields:
|
||||||
|
return self.mapped('child_education_id.it_declaration_id')
|
||||||
|
if 'parent_id' in self._fields:
|
||||||
|
return self.mapped('parent_id.it_declaration_id')
|
||||||
|
return self.env['emp.it.declaration']
|
||||||
|
|
||||||
|
def _check_it_declaration_is_editable(self):
|
||||||
|
if any(declaration.state == 'submitted' for declaration in self._get_related_it_declarations()):
|
||||||
|
raise UserError(_('Submitted IT declarations are read-only. Return the declaration to draft before editing it.'))
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
records = super().create(vals_list)
|
||||||
|
records._check_it_declaration_is_editable()
|
||||||
|
return records
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
self._check_it_declaration_is_editable()
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
self._check_it_declaration_is_editable()
|
||||||
|
return super().unlink()
|
||||||
|
|
||||||
|
|
||||||
class EmpITDeclaration(models.Model):
|
class EmpITDeclaration(models.Model):
|
||||||
|
|
@ -41,14 +73,47 @@ class EmpITDeclaration(models.Model):
|
||||||
('new', 'New Regime'),
|
('new', 'New Regime'),
|
||||||
('old', 'Old Regime')
|
('old', 'Old Regime')
|
||||||
], string="Tax Regime", required=True, default='new')
|
], string="Tax Regime", required=True, default='new')
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('submitted', 'Submitted'),
|
||||||
|
], string="Status", default='draft', required=True, copy=False)
|
||||||
|
return_reason = fields.Text(string="Return Reason", copy=False)
|
||||||
|
is_payroll_manager = fields.Boolean(compute='_compute_is_payroll_manager')
|
||||||
|
|
||||||
total_investment = fields.Float(string='Total Investment')
|
total_investment = fields.Float(string='Total Investment')
|
||||||
|
|
||||||
costing_details_generated = fields.Boolean(default=False)
|
costing_details_generated = fields.Boolean(default=False)
|
||||||
|
|
||||||
investment_costing_ids = fields.One2many('investment.costings','it_declaration_id')
|
investment_costing_ids = fields.One2many('investment.costings','it_declaration_id')
|
||||||
|
visible_investment_costing_ids = fields.Many2many(
|
||||||
|
'investment.costings',
|
||||||
|
compute='_compute_visible_investment_costing_ids',
|
||||||
|
string='Visible Investment Costings',
|
||||||
|
)
|
||||||
house_rent_costing_id = fields.Many2one('investment.costings', compute="_compute_investment_costing")
|
house_rent_costing_id = fields.Many2one('investment.costings', compute="_compute_investment_costing")
|
||||||
is_section_open = fields.Boolean()
|
is_section_open = fields.Boolean()
|
||||||
|
|
||||||
|
def _compute_is_payroll_manager(self):
|
||||||
|
is_manager = self.env.user.has_group('hr_payroll.group_hr_payroll_manager')
|
||||||
|
for rec in self:
|
||||||
|
rec.is_payroll_manager = is_manager
|
||||||
|
|
||||||
|
def _check_submitted_write_allowed(self, vals):
|
||||||
|
protected_vals = set(vals) - {'state', 'return_reason'}
|
||||||
|
submitted_records = self.filtered(lambda rec: rec.state == 'submitted')
|
||||||
|
if submitted_records and protected_vals:
|
||||||
|
raise UserError(_('Submitted IT declarations are read-only. Return the declaration to draft before editing it.'))
|
||||||
|
if submitted_records and set(vals) & {'state', 'return_reason'} and not self.env.user.has_group('hr_payroll.group_hr_payroll_manager'):
|
||||||
|
raise UserError(_('Only a Payroll Manager can return a submitted IT declaration.'))
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
self._check_submitted_write_allowed(vals)
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
if any(rec.state == 'submitted' for rec in self):
|
||||||
|
raise UserError(_('Submitted IT declarations cannot be deleted.'))
|
||||||
|
return super().unlink()
|
||||||
@api.depends('costing_details_generated','investment_costing_ids')
|
@api.depends('costing_details_generated','investment_costing_ids')
|
||||||
def _compute_investment_costing(self):
|
def _compute_investment_costing(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -58,82 +123,250 @@ class EmpITDeclaration(models.Model):
|
||||||
)[:1]
|
)[:1]
|
||||||
else:
|
else:
|
||||||
rec.house_rent_costing_id = False
|
rec.house_rent_costing_id = False
|
||||||
|
|
||||||
|
@api.depends(
|
||||||
|
'investment_costing_ids',
|
||||||
|
'investment_costing_ids.investment_type_id',
|
||||||
|
'investment_costing_ids.investment_type_id.active',
|
||||||
|
'period_id',
|
||||||
|
'tax_regime',
|
||||||
|
)
|
||||||
|
def _compute_visible_investment_costing_ids(self):
|
||||||
|
for rec in self:
|
||||||
|
visible_investment_type_ids = rec._get_visible_investment_types().ids
|
||||||
|
rec.visible_investment_costing_ids = rec.investment_costing_ids.filtered(
|
||||||
|
lambda costing: costing.investment_type_id
|
||||||
|
and costing.investment_type_id.id in visible_investment_type_ids
|
||||||
|
)
|
||||||
past_employment_costings = fields.One2many('past_employment.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
past_employment_costings = fields.One2many('past_employment.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
past_employment_costings_new = fields.One2many('past_employment.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
past_employment_costings_new = fields.One2many('past_employment.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
us80c_costings = fields.One2many('us80c.costing.type','it_declaration_id')
|
us80c_costings = fields.One2many('us80c.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
|
us80c_costings_new = fields.One2many('us80c.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
us80d_selection_type = fields.Selection([('self_family','Self-family'),('self_family_parent','Self-family and parent'),('self_family_senior_parent','Self-family and senior parent')], default='self_family',required=True)
|
us80d_selection_type = fields.Selection([('self_family','Self-family'),('self_family_parent','Self-family and parent'),('self_family_senior_parent','Self-family and senior parent')], default='self_family',required=True)
|
||||||
us80d_health_checkup = fields.Boolean(string='Preventive Health Checkup')
|
us80d_health_checkup = fields.Boolean(string='Preventive Health Checkup')
|
||||||
us80d_costings = fields.One2many('us80d.costing.type','it_declaration_id',domain=[('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',False),('investment_type_line_id.for_senior_parent','=',False)])
|
us80d_costings = fields.One2many('us80d.costing.type','it_declaration_id',domain=[('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',False),('investment_type_line_id.for_senior_parent','=',False),('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
us80d_costings_parents = fields.One2many('us80d.costing.type','it_declaration_id',domain=['|',('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',True),('investment_type_line_id.for_senior_parent','=',False)])
|
us80d_costings_new = fields.One2many('us80d.costing.type','it_declaration_id',domain=[('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',False),('investment_type_line_id.for_senior_parent','=',False),('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
us80d_costings_senior_parents = fields.One2many('us80d.costing.type','it_declaration_id',domain=['|','|',('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',True),('investment_type_line_id.for_senior_parent','=',True)])
|
us80d_costings_parents = fields.One2many('us80d.costing.type','it_declaration_id',domain=['|',('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',True),('investment_type_line_id.for_senior_parent','=',False),('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
|
us80d_costings_parents_new = fields.One2many('us80d.costing.type','it_declaration_id',domain=['|',('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',True),('investment_type_line_id.for_senior_parent','=',False),('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
|
us80d_costings_senior_parents = fields.One2many('us80d.costing.type','it_declaration_id',domain=['|','|',('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',True),('investment_type_line_id.for_senior_parent','=',True),('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
|
us80d_costings_senior_parents_new = fields.One2many('us80d.costing.type','it_declaration_id',domain=['|','|',('investment_type_line_id.for_family','=',True),('investment_type_line_id.for_parents','=',True),('investment_type_line_id.for_senior_parent','=',True),('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
|
|
||||||
us10_costings = fields.One2many('us10.costing.type','it_declaration_id')
|
us10_costings = fields.One2many('us10.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
us80g_costings = fields.One2many('us80g.costing.type','it_declaration_id')
|
us10_costings_new = fields.One2many('us10.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
|
us80g_costings = fields.One2many('us80g.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
|
us80g_costings_new = fields.One2many('us80g.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
chapter_via_costings = fields.One2many('chapter.via.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
chapter_via_costings = fields.One2many('chapter.via.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
chapter_via_costings_new = fields.One2many('chapter.via.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
chapter_via_costings_new = fields.One2many('chapter.via.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
us17_costings = fields.One2many('us17.costing.type','it_declaration_id')
|
us17_costings = fields.One2many('us17.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
|
us17_costings_new = fields.One2many('us17.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
|
|
||||||
house_rent_costings = fields.One2many('house.rent.declaration','it_declaration_id')
|
house_rent_costings = fields.One2many('house.rent.declaration','it_declaration_id')
|
||||||
|
|
||||||
other_il_costings = fields.One2many('other.il.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
other_il_costings = fields.One2many('other.il.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
other_il_costings_new = fields.One2many('other.il.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
other_il_costings_new = fields.One2many('other.il.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
other_declaration_costings = fields.One2many('other.declaration.costing.type','it_declaration_id')
|
other_declaration_costings = fields.One2many('other.declaration.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||||
|
other_declaration_costings_new = fields.One2many('other.declaration.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
|
||||||
|
show_past_employment = fields.Boolean(compute="_compute_show_records")
|
||||||
|
show_us_80c = fields.Boolean(compute="_compute_show_records")
|
||||||
|
show_us_80d = fields.Boolean(compute="_compute_show_records")
|
||||||
|
show_us_10 = fields.Boolean(compute="_compute_show_records")
|
||||||
|
show_us_80g = fields.Boolean(compute="_compute_show_records")
|
||||||
|
show_chapter_via = fields.Boolean(compute="_compute_show_records")
|
||||||
|
show_us_17 = fields.Boolean(compute="_compute_show_records")
|
||||||
|
show_house_rent = fields.Boolean(compute="_compute_show_records")
|
||||||
|
show_other_i_or_l = fields.Boolean(compute="_compute_show_records")
|
||||||
|
show_other_declaration = fields.Boolean(compute="_compute_show_records")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _investment_type_line_fields(self):
|
||||||
|
return {
|
||||||
|
'past_employment': 'past_employment_ids',
|
||||||
|
'us_80c': 'us80c_ids',
|
||||||
|
'us_80d': 'us80d_ids',
|
||||||
|
'us_10': 'us10_ids',
|
||||||
|
'us_80g': 'us80g_ids',
|
||||||
|
'chapter_via': 'chapter_via_ids',
|
||||||
|
'us_17': 'us17_ids',
|
||||||
|
'other_i_or_l': 'other_il_ids',
|
||||||
|
'other_declaration': 'other_declaration_ids',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_available_investment_types(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.period_id:
|
||||||
|
return self.env['it.investment.type']
|
||||||
|
return self.env['it.investment.type'].sudo().search([
|
||||||
|
('active', '=', True),
|
||||||
|
'|',
|
||||||
|
('period_ids', 'in', self.period_id.id),
|
||||||
|
('period_ids', '=', False),
|
||||||
|
])
|
||||||
|
|
||||||
|
def _get_visible_investment_types(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return self._get_available_investment_types().filtered(
|
||||||
|
lambda investment_type: self._is_investment_type_visible(investment_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _investment_type_generation_config(self):
|
||||||
|
return {
|
||||||
|
'past_employment': ('past_employment_ids', 'past_employment.costing.type'),
|
||||||
|
'us_80c': ('us80c_ids', 'us80c.costing.type'),
|
||||||
|
'us_80d': ('us80d_ids', 'us80d.costing.type'),
|
||||||
|
'us_10': ('us10_ids', 'us10.costing.type'),
|
||||||
|
'us_80g': ('us80g_ids', 'us80g.costing.type'),
|
||||||
|
'chapter_via': ('chapter_via_ids', 'chapter.via.costing.type'),
|
||||||
|
'us_17': ('us17_ids', 'us17.costing.type'),
|
||||||
|
'other_i_or_l': ('other_il_ids', 'other.il.costing.type'),
|
||||||
|
'other_declaration': ('other_declaration_ids', 'other.declaration.costing.type'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_regime_values(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return ['old', 'both'] if self.tax_regime == 'old' else ['new', 'both']
|
||||||
|
|
||||||
|
def _get_regime_filtered_costings(self, field_name):
|
||||||
|
self.ensure_one()
|
||||||
|
return self[field_name].filtered(
|
||||||
|
lambda line: line.investment_type_line_id
|
||||||
|
and line.investment_type_line_id.tax_regime in self._get_regime_values()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _ensure_investment_costing_records(self):
|
||||||
|
for rec in self:
|
||||||
|
available_investment_types = rec._get_available_investment_types()
|
||||||
|
generation_config = rec._investment_type_generation_config()
|
||||||
|
|
||||||
|
for inv_type in available_investment_types:
|
||||||
|
investment_costing = rec.investment_costing_ids.filtered(
|
||||||
|
lambda cost: cost.investment_type_id == inv_type
|
||||||
|
)[:1]
|
||||||
|
if not investment_costing:
|
||||||
|
investment_costing = self.env['investment.costings'].sudo().create({
|
||||||
|
'investment_type_id': inv_type.id,
|
||||||
|
'it_declaration_id': rec.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
config = generation_config.get(inv_type.investment_type)
|
||||||
|
if not config:
|
||||||
|
continue
|
||||||
|
|
||||||
|
line_field, costing_model = config
|
||||||
|
active_lines = getattr(inv_type, line_field).filtered(lambda line: line.active)
|
||||||
|
existing_costings = self.env[costing_model].sudo().search([
|
||||||
|
('it_declaration_id', '=', rec.id),
|
||||||
|
('costing_type', '=', investment_costing.id),
|
||||||
|
])
|
||||||
|
existing_line_ids = set(existing_costings.mapped('investment_type_line_id').ids)
|
||||||
|
|
||||||
|
for investment_line in active_lines:
|
||||||
|
if investment_line.id in existing_line_ids:
|
||||||
|
continue
|
||||||
|
self.env[costing_model].sudo().create({
|
||||||
|
'costing_type': investment_costing.id,
|
||||||
|
'it_declaration_id': rec.id,
|
||||||
|
'investment_type_line_id': investment_line.id,
|
||||||
|
'limit': investment_line.limit,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _update_investment_amounts(self):
|
||||||
|
for rec in self:
|
||||||
|
for investment_type in rec.investment_costing_ids:
|
||||||
|
if investment_type.investment_type_id.investment_type == 'past_employment':
|
||||||
|
costings = rec.past_employment_costings if rec.tax_regime == 'old' else rec.past_employment_costings_new
|
||||||
|
investment_type.amount = sum(
|
||||||
|
cost.declaration_amount
|
||||||
|
for cost in costings
|
||||||
|
if not cost.investment_type_line_id.compute_method
|
||||||
|
)
|
||||||
|
elif investment_type.investment_type_id.investment_type == 'us_80c':
|
||||||
|
costings = rec.us80c_costings if rec.tax_regime == 'old' else rec.us80c_costings_new
|
||||||
|
investment_type.amount = sum(
|
||||||
|
cost.declaration_amount
|
||||||
|
for cost in costings
|
||||||
|
if not cost.investment_type_line_id.compute_method
|
||||||
|
)
|
||||||
|
elif investment_type.investment_type_id.investment_type == 'us_80d':
|
||||||
|
if rec.us80d_selection_type == 'self_family':
|
||||||
|
costings = rec.us80d_costings if rec.tax_regime == 'old' else rec.us80d_costings_new
|
||||||
|
elif rec.us80d_selection_type == 'self_family_parent':
|
||||||
|
costings = rec.us80d_costings_parents if rec.tax_regime == 'old' else rec.us80d_costings_parents_new
|
||||||
|
else:
|
||||||
|
costings = rec.us80d_costings_senior_parents if rec.tax_regime == 'old' else rec.us80d_costings_senior_parents_new
|
||||||
|
investment_type.amount = sum(costings.mapped('declaration_amount') or [0])
|
||||||
|
elif investment_type.investment_type_id.investment_type == 'us_10':
|
||||||
|
costings = rec.us10_costings if rec.tax_regime == 'old' else rec.us10_costings_new
|
||||||
|
investment_type.amount = sum(costings.mapped('declaration_amount') or [0])
|
||||||
|
elif investment_type.investment_type_id.investment_type == 'us_80g':
|
||||||
|
costings = rec.us80g_costings if rec.tax_regime == 'old' else rec.us80g_costings_new
|
||||||
|
investment_type.amount = sum(costings.mapped('declaration_amount') or [0])
|
||||||
|
elif investment_type.investment_type_id.investment_type == 'chapter_via':
|
||||||
|
costings = rec.chapter_via_costings if rec.tax_regime == 'old' else rec.chapter_via_costings_new
|
||||||
|
investment_type.amount = sum(costings.mapped('declaration_amount') or [0])
|
||||||
|
elif investment_type.investment_type_id.investment_type == 'us_17':
|
||||||
|
costings = rec.us17_costings if rec.tax_regime == 'old' else rec.us17_costings_new
|
||||||
|
investment_type.amount = sum(costings.mapped('declaration_amount') or [0])
|
||||||
|
elif investment_type.investment_type_id.investment_type == 'house_rent':
|
||||||
|
investment_type.amount = sum(rec.house_rent_costings.mapped('rent_amount') or [0]) if rec.tax_regime == 'old' else 0
|
||||||
|
elif investment_type.investment_type_id.investment_type == 'other_i_or_l':
|
||||||
|
costings = rec.other_il_costings if rec.tax_regime == 'old' else rec.other_il_costings_new
|
||||||
|
investment_type.amount = sum(costings.mapped('declaration_amount') or [0])
|
||||||
|
elif investment_type.investment_type_id.investment_type == 'other_declaration':
|
||||||
|
costings = rec.other_declaration_costings if rec.tax_regime == 'old' else rec.other_declaration_costings_new
|
||||||
|
investment_type.amount = sum(costings.mapped('declaration_amount') or [0])
|
||||||
|
|
||||||
|
def _is_investment_type_visible(self, investment_type):
|
||||||
|
self.ensure_one()
|
||||||
|
if not investment_type or not investment_type.active:
|
||||||
|
return False
|
||||||
|
valid_regimes = ['old', 'both'] if self.tax_regime == 'old' else ['new', 'both']
|
||||||
|
if investment_type.regime not in valid_regimes:
|
||||||
|
return False
|
||||||
|
line_field = self._investment_type_line_fields().get(investment_type.investment_type)
|
||||||
|
if not line_field:
|
||||||
|
return True
|
||||||
|
return bool(getattr(investment_type, line_field).filtered(
|
||||||
|
lambda line: line.active and line.tax_regime in valid_regimes
|
||||||
|
))
|
||||||
|
|
||||||
|
@api.depends('period_id', 'tax_regime')
|
||||||
|
def _compute_show_records(self):
|
||||||
|
field_mapping = {
|
||||||
|
'past_employment': 'show_past_employment',
|
||||||
|
'us_80c': 'show_us_80c',
|
||||||
|
'us_80d': 'show_us_80d',
|
||||||
|
'us_10': 'show_us_10',
|
||||||
|
'us_80g': 'show_us_80g',
|
||||||
|
'chapter_via': 'show_chapter_via',
|
||||||
|
'us_17': 'show_us_17',
|
||||||
|
'house_rent': 'show_house_rent',
|
||||||
|
'other_i_or_l': 'show_other_i_or_l',
|
||||||
|
'other_declaration': 'show_other_declaration',
|
||||||
|
}
|
||||||
|
for rec in self:
|
||||||
|
visible_investment_types = rec._get_visible_investment_types()
|
||||||
|
for field_name in field_mapping.values():
|
||||||
|
rec[field_name] = False
|
||||||
|
|
||||||
|
for investment_type_key, field_name in field_mapping.items():
|
||||||
|
rec[field_name] = bool(visible_investment_types.filtered(
|
||||||
|
lambda inv: inv.investment_type == investment_type_key
|
||||||
|
))
|
||||||
|
|
||||||
def toggle_section_visibility(self):
|
def toggle_section_visibility(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
rec.is_section_open = not rec.is_section_open
|
rec.is_section_open = not rec.is_section_open
|
||||||
if rec.is_section_open:
|
if rec.is_section_open:
|
||||||
for investment_type in rec.investment_costing_ids:
|
if rec.costing_details_generated:
|
||||||
if investment_type.investment_type_id.investment_type == 'past_employment':
|
rec._ensure_investment_costing_records()
|
||||||
if rec.tax_regime == 'old':
|
rec._update_investment_amounts()
|
||||||
investment_type.amount = sum(
|
|
||||||
cost.declaration_amount
|
|
||||||
for cost in rec.past_employment_costings
|
|
||||||
if not cost.investment_type_line_id.compute_method
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
investment_type.amount = sum(
|
|
||||||
cost.declaration_amount
|
|
||||||
for cost in rec.past_employment_costings_new
|
|
||||||
if not cost.investment_type_line_id.compute_method
|
|
||||||
)
|
|
||||||
elif investment_type.investment_type_id.investment_type == 'us_80c':
|
|
||||||
investment_type.amount = sum(
|
|
||||||
cost.declaration_amount
|
|
||||||
for cost in rec.us80c_costings
|
|
||||||
if not cost.investment_type_line_id.compute_method
|
|
||||||
) if rec.tax_regime == 'old' else 0
|
|
||||||
elif investment_type.investment_type_id.investment_type == 'us_80d':
|
|
||||||
if rec.us80d_selection_type == 'self_family':
|
|
||||||
investment_type.amount = sum(rec.us80d_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
|
|
||||||
if rec.us80d_selection_type == 'self_family_parent':
|
|
||||||
investment_type.amount = sum(rec.us80d_costings_parents.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
|
|
||||||
if rec.us80d_selection_type == 'self_family_senior_parent':
|
|
||||||
investment_type.amount = sum(rec.us80d_costings_senior_parents.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
|
|
||||||
elif investment_type.investment_type_id.investment_type == 'us_10':
|
|
||||||
investment_type.amount = sum(rec.us10_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
|
|
||||||
elif investment_type.investment_type_id.investment_type == 'us_80g':
|
|
||||||
investment_type.amount = sum(rec.us80g_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
|
|
||||||
elif investment_type.investment_type_id.investment_type == 'chapter_via':
|
|
||||||
if rec.tax_regime == 'old':
|
|
||||||
investment_type.amount = sum(rec.chapter_via_costings.mapped('declaration_amount') or [0])
|
|
||||||
else:
|
|
||||||
investment_type.amount = sum(rec.chapter_via_costings_new.mapped('declaration_amount') or [0])
|
|
||||||
elif investment_type.investment_type_id.investment_type == 'us_17':
|
|
||||||
investment_type.amount = sum(rec.us17_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
|
|
||||||
elif investment_type.investment_type_id.investment_type == 'house_rent':
|
|
||||||
investment_type.amount = sum(rec.house_rent_costings.mapped('rent_amount') or [0]) if rec.tax_regime == 'old' else 0
|
|
||||||
elif investment_type.investment_type_id.investment_type == 'other_i_or_l':
|
|
||||||
if rec.tax_regime == 'old':
|
|
||||||
investment_type.amount = sum(rec.other_il_costings.mapped('declaration_amount') or [0])
|
|
||||||
else:
|
|
||||||
investment_type.amount = sum(rec.other_il_costings_new.mapped('declaration_amount') or [0])
|
|
||||||
elif investment_type.investment_type_id.investment_type == 'other_declaration':
|
|
||||||
investment_type.amount = sum(rec.other_declaration_costings.mapped('declaration_amount') or [0]) if rec.tax_regime == 'old' else 0
|
|
||||||
|
|
||||||
@api.onchange('tax_regime')
|
@api.onchange('tax_regime')
|
||||||
def _onchange_tax_regime(self):
|
def _onchange_tax_regime(self):
|
||||||
|
if self.costing_details_generated:
|
||||||
|
self._ensure_investment_costing_records()
|
||||||
|
self._update_investment_amounts()
|
||||||
if self.tax_regime:
|
if self.tax_regime:
|
||||||
# res = super(empITDeclaration, self).fields_get(allfields, attributes)
|
# res = super(empITDeclaration, self).fields_get(allfields, attributes)
|
||||||
# self.fields_get()
|
# self.fields_get()
|
||||||
|
|
@ -173,102 +406,158 @@ class EmpITDeclaration(models.Model):
|
||||||
|
|
||||||
def generate_declarations(self):
|
def generate_declarations(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
investment_types = self.env['it.investment.type'].sudo().search([])
|
rec._ensure_investment_costing_records()
|
||||||
for inv_type in investment_types:
|
rec._update_investment_amounts()
|
||||||
investment_costing = self.env['investment.costings'].sudo().create({
|
rec.costing_details_generated = True
|
||||||
'investment_type_id': inv_type.id,
|
|
||||||
'it_declaration_id': rec.id,
|
def action_submit(self):
|
||||||
|
for rec in self:
|
||||||
|
rec._update_investment_amounts()
|
||||||
|
rec.write({
|
||||||
|
'state': 'submitted',
|
||||||
|
'return_reason': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
def action_return_to_draft(self):
|
||||||
|
for rec in self:
|
||||||
|
if not self.env.user.has_group('hr_payroll.group_hr_payroll_manager'):
|
||||||
|
raise UserError(_('Only a Payroll Manager can return a submitted IT declaration.'))
|
||||||
|
if not rec.return_reason:
|
||||||
|
raise UserError(_('Please enter a return reason before returning the declaration to draft.'))
|
||||||
|
rec.state = 'draft'
|
||||||
|
|
||||||
|
def action_download_submission_pdf(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.state != 'submitted':
|
||||||
|
raise UserError(_('You can download the IT declaration PDF only after it is submitted.'))
|
||||||
|
return self.env.ref('employee_it_declaration.action_report_it_tax_statement').report_action(self)
|
||||||
|
|
||||||
|
def _get_regime_label(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return dict(self._fields['tax_regime'].selection).get(self.tax_regime, self.tax_regime)
|
||||||
|
|
||||||
|
def _get_binary_link(self, record, field_name, filename_field=False):
|
||||||
|
if not record or not record[field_name]:
|
||||||
|
return False
|
||||||
|
params = {
|
||||||
|
'model': record._name,
|
||||||
|
'id': record.id,
|
||||||
|
'field': field_name,
|
||||||
|
'download': 'true',
|
||||||
|
}
|
||||||
|
if filename_field:
|
||||||
|
params['filename_field'] = filename_field
|
||||||
|
return '/web/content?%s' % urlencode(params)
|
||||||
|
|
||||||
|
def _prepare_report_line(self, line):
|
||||||
|
name = line.investment_type_line_id.name if line.investment_type_line_id else ''
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'declaration_amount': line.declaration_amount,
|
||||||
|
'proof_amount': line.proof_amount,
|
||||||
|
'limit': line.limit,
|
||||||
|
'remarks': line.remarks,
|
||||||
|
'attachment_name': line.proof_name or _('Proof'),
|
||||||
|
'attachment_url': self._get_binary_link(line, 'proof', 'proof_name'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_report_sections(self):
|
||||||
|
self.ensure_one()
|
||||||
|
old_regime = self.tax_regime == 'old'
|
||||||
|
section_fields = [
|
||||||
|
(_('Past Employment'), 'past_employment_costings' if old_regime else 'past_employment_costings_new'),
|
||||||
|
(_('US 80C'), 'us80c_costings' if old_regime else 'us80c_costings_new'),
|
||||||
|
(_('US 80D'), {
|
||||||
|
'self_family': 'us80d_costings' if old_regime else 'us80d_costings_new',
|
||||||
|
'self_family_parent': 'us80d_costings_parents' if old_regime else 'us80d_costings_parents_new',
|
||||||
|
'self_family_senior_parent': 'us80d_costings_senior_parents' if old_regime else 'us80d_costings_senior_parents_new',
|
||||||
|
}[self.us80d_selection_type]),
|
||||||
|
(_('US 10'), 'us10_costings' if old_regime else 'us10_costings_new'),
|
||||||
|
(_('US 80G'), 'us80g_costings' if old_regime else 'us80g_costings_new'),
|
||||||
|
(_('Chapter VIA'), 'chapter_via_costings' if old_regime else 'chapter_via_costings_new'),
|
||||||
|
(_('US 17'), 'us17_costings' if old_regime else 'us17_costings_new'),
|
||||||
|
(_('Other Income / Loss'), 'other_il_costings' if old_regime else 'other_il_costings_new'),
|
||||||
|
(_('Other Declarations'), 'other_declaration_costings' if old_regime else 'other_declaration_costings_new'),
|
||||||
|
]
|
||||||
|
sections = []
|
||||||
|
for title, field_name in section_fields:
|
||||||
|
lines = self[field_name].filtered(lambda line: line.declaration_amount or line.proof_amount or line.remarks or line.proof)
|
||||||
|
if lines:
|
||||||
|
sections.append({
|
||||||
|
'title': title,
|
||||||
|
'lines': [self._prepare_report_line(line) for line in lines],
|
||||||
})
|
})
|
||||||
|
|
||||||
if inv_type.investment_type == 'past_employment':
|
if old_regime and self.house_rent_costings:
|
||||||
past_emp_costing_ids = [
|
sections.append({
|
||||||
self.env['past_employment.costing.type'].sudo().create({
|
'title': _('House Rent'),
|
||||||
'costing_type': investment_costing.id,
|
'house_rent': True,
|
||||||
'it_declaration_id': rec.id,
|
'lines': [{
|
||||||
'investment_type_line_id': investment_line.id,
|
'hra_exemption_type': dict(line._fields['hra_exemption_type'].selection).get(line.hra_exemption_type, ''),
|
||||||
'limit': investment_line.limit
|
'rent_amount': line.rent_amount,
|
||||||
}).id
|
'from_date': line.from_date,
|
||||||
for investment_line in inv_type.past_employment_ids
|
'to_date': line.to_date,
|
||||||
]
|
'landlord_pan_status': dict(line._fields['landlord_pan_status'].selection).get(line.landlord_pan_status, ''),
|
||||||
if inv_type.investment_type == 'us_80c':
|
'landlord_pan_no': line.landlord_pan_no,
|
||||||
|
'landlord_name_address': line.landlord_name_address,
|
||||||
|
'remarks': line.remarks,
|
||||||
|
'attachment_name': line.attachment_filename or _('Proof'),
|
||||||
|
'attachment_url': self._get_binary_link(line, 'attachment', 'attachment_filename'),
|
||||||
|
} for line in self.house_rent_costings],
|
||||||
|
})
|
||||||
|
return sections
|
||||||
|
|
||||||
us80c_costing_ids = [
|
def _get_report_extra_sections(self):
|
||||||
self.env['us80c.costing.type'].sudo().create({
|
self.ensure_one()
|
||||||
'costing_type': investment_costing.id,
|
extra_sections = []
|
||||||
'it_declaration_id': rec.id,
|
us80c_lines = self.us80c_costings if self.tax_regime == 'old' else self.us80c_costings_new
|
||||||
'investment_type_line_id': investment_line.id,
|
other_il_lines = self.other_il_costings if self.tax_regime == 'old' else self.other_il_costings_new
|
||||||
'limit': investment_line.limit
|
|
||||||
}).id
|
children_records = self.env['children.education'].sudo().search([('it_declaration_id', '=', self.id), ('us80c_id', 'in', us80c_lines.ids)])
|
||||||
for investment_line in inv_type.us80c_ids
|
if children_records:
|
||||||
]
|
extra_sections.append({
|
||||||
if inv_type.investment_type == 'us_80d':
|
'title': _('Children Education Details'),
|
||||||
us80d_costing_ids = [
|
'headers': [_('Child'), _('Name'), _('Class / Grade'), _('School / College'), _('Tuition Fee')],
|
||||||
self.env['us80d.costing.type'].sudo().create({
|
'rows': [[child.child_id, child.name, child.chile_class, child.organization, child.tuition_fee] for record in children_records for child in record.children_ids],
|
||||||
'costing_type': investment_costing.id,
|
})
|
||||||
'it_declaration_id': rec.id,
|
|
||||||
'investment_type_line_id': investment_line.id,
|
insurance_records = self.env['us80c.insurance.line'].sudo().search([('it_declaration_id', '=', self.id), ('us80c_id', 'in', us80c_lines.ids)])
|
||||||
'limit': investment_line.limit
|
if insurance_records:
|
||||||
}).id
|
extra_sections.append({
|
||||||
for investment_line in inv_type.us80d_ids
|
'title': _('Life Insurance Details'),
|
||||||
]
|
'headers': [_('Company'), _('Insured For'), _('Insured Name'), _('Policy No'), _('Premium'), _('Payment Date'), _('Exempt Amount')],
|
||||||
if inv_type.investment_type == 'us_10':
|
'rows': [[line.name_of_insurance_company, line.insured_in_favour_of, line.name_of_insured, line.policy_number, line.premium_amount, line.payment_date, line.exempt_amount] for record in insurance_records for line in record.life_insurance_ids],
|
||||||
us10_costing_ids = [
|
})
|
||||||
self.env['us10.costing.type'].sudo().create({
|
|
||||||
'costing_type': investment_costing.id,
|
nsc_records = self.env['nsc.declaration.line'].sudo().search([('it_declaration_id', '=', self.id), ('us80c_id', 'in', us80c_lines.ids)])
|
||||||
'it_declaration_id': rec.id,
|
if nsc_records:
|
||||||
'investment_type_line_id': investment_line.id,
|
extra_sections.append({
|
||||||
'limit': investment_line.limit
|
'title': _('NSC Declaration Details'),
|
||||||
}).id
|
'headers': [_('NSC Number'), _('Amount'), _('Payment Date')],
|
||||||
for investment_line in inv_type.us10_ids
|
'rows': [[line.nsc_number, line.nsc_amount, line.nsc_payment_date] for record in nsc_records for line in record.nsc_entry_ids],
|
||||||
]
|
})
|
||||||
if inv_type.investment_type == 'us_80g':
|
|
||||||
us80g_costing_ids = [
|
self_occupied_records = self.env['self.occupied.property'].sudo().search([('it_declaration_id', '=', self.id), ('other_il_id', 'in', other_il_lines.ids)])
|
||||||
self.env['us80g.costing.type'].sudo().create({
|
if self_occupied_records:
|
||||||
'costing_type': investment_costing.id,
|
extra_sections.append({
|
||||||
'it_declaration_id': rec.id,
|
'title': _('Self Occupied Property Details'),
|
||||||
'investment_type_line_id': investment_line.id,
|
'headers': [_('Address'), _('Period From'), _('Period To'), _('Interest Paid'), _('Income / Loss'), _('Lender'), _('Lender PAN')],
|
||||||
'limit': investment_line.limit
|
'rows': [[record.address, record.period_from, record.period_to, record.interest_paid_to, record.income_loss, record.lender_name, record.lender_pan] for record in self_occupied_records],
|
||||||
}).id
|
})
|
||||||
for investment_line in inv_type.us80g_ids
|
|
||||||
]
|
letout_records = self.env['letout.house.property'].sudo().search([('it_declaration_id', '=', self.id), ('other_il_id', 'in', other_il_lines.ids)])
|
||||||
if inv_type.investment_type == 'chapter_via':
|
if letout_records:
|
||||||
chapter_via_ids = [
|
extra_sections.append({
|
||||||
self.env['chapter.via.costing.type'].sudo().create({
|
'title': _('Let-out House Property Details'),
|
||||||
'costing_type': investment_costing.id,
|
'headers': [_('Address'), _('Rent'), _('Property Tax'), _('Water Tax'), _('Interest Paid'), _('Income / Loss'), _('Lender'), _('Lender PAN')],
|
||||||
'it_declaration_id': rec.id,
|
'rows': [[record.address, record.rent_received, record.property_tax, record.water_tax, record.interest_paid_to, record.income_loss, record.lender_name, record.lender_pan] for record in letout_records],
|
||||||
'investment_type_line_id': investment_line.id,
|
})
|
||||||
'limit': investment_line.limit
|
|
||||||
}).id
|
nsc_interest_records = self.env['nsc.interest.line'].sudo().search([('it_declaration_id', '=', self.id), ('other_il_id', 'in', other_il_lines.ids)])
|
||||||
for investment_line in inv_type.chapter_via_ids
|
if nsc_interest_records:
|
||||||
]
|
extra_sections.append({
|
||||||
if inv_type.investment_type == 'us_17':
|
'title': _('NSC Interest Details'),
|
||||||
us17_costing_ids = [
|
'headers': [_('NSC Number'), _('Amount'), _('Payment Date'), _('Interest Amount')],
|
||||||
self.env['us17.costing.type'].sudo().create({
|
'rows': [[line.nsc_number, line.nsc_amount, line.nsc_payment_date, line.nsc_interest_amount] for record in nsc_interest_records for line in record.nsc_entry_ids],
|
||||||
'costing_type': investment_costing.id,
|
})
|
||||||
'it_declaration_id': rec.id,
|
return [section for section in extra_sections if section['rows']]
|
||||||
'investment_type_line_id': investment_line.id,
|
|
||||||
'limit': investment_line.limit
|
|
||||||
}).id
|
|
||||||
for investment_line in inv_type.us17_ids
|
|
||||||
]
|
|
||||||
if inv_type.investment_type == 'other_i_or_l':
|
|
||||||
other_il_costing_ids = [
|
|
||||||
self.env['other.il.costing.type'].sudo().create({
|
|
||||||
'costing_type': investment_costing.id,
|
|
||||||
'it_declaration_id': rec.id,
|
|
||||||
'investment_type_line_id': investment_line.id,
|
|
||||||
'limit': investment_line.limit
|
|
||||||
}).id
|
|
||||||
for investment_line in inv_type.other_il_ids
|
|
||||||
]
|
|
||||||
if inv_type.investment_type == 'other_declaration':
|
|
||||||
other_declaration_costing_ids = [
|
|
||||||
self.env['other.declaration.costing.type'].sudo().create({
|
|
||||||
'costing_type': investment_costing.id,
|
|
||||||
'it_declaration_id': rec.id,
|
|
||||||
'investment_type_line_id': investment_line.id,
|
|
||||||
'limit': investment_line.limit
|
|
||||||
}).id
|
|
||||||
for investment_line in inv_type.other_declaration_ids
|
|
||||||
]
|
|
||||||
rec.costing_details_generated = True
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tools.safe_eval import safe_eval
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeePayslipDownloadWizard(models.TransientModel):
|
||||||
|
_name = 'employee.payslip.download.wizard'
|
||||||
|
_rec_name = 'employee_id'
|
||||||
|
_description = 'Employee Payslip Download Wizard'
|
||||||
|
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
required=True,
|
||||||
|
default=lambda self: self.env.user.employee_id.id,
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
download_type = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
('single', 'Single Payslip'),
|
||||||
|
('multi', 'Multiple Payslips'),
|
||||||
|
],
|
||||||
|
string='Download Option',
|
||||||
|
required=True,
|
||||||
|
default='single',
|
||||||
|
)
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string='Payroll Period',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
period_line = fields.Many2one(
|
||||||
|
'payroll.period.line',
|
||||||
|
string='Month',
|
||||||
|
domain="[('period_id', '=', period_id)]",
|
||||||
|
)
|
||||||
|
payslip_count = fields.Integer(
|
||||||
|
string='Available Payslips',
|
||||||
|
compute='_compute_payslip_count',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.onchange('download_type', 'period_id')
|
||||||
|
def _onchange_download_type_period_id(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.period_line = False
|
||||||
|
|
||||||
|
@api.depends('download_type', 'period_id', 'period_line')
|
||||||
|
def _compute_payslip_count(self):
|
||||||
|
for rec in self:
|
||||||
|
if not rec.period_id or (rec.download_type == 'single' and not rec.period_line):
|
||||||
|
rec.payslip_count = 0
|
||||||
|
else:
|
||||||
|
rec.payslip_count = len(rec._get_available_payslips())
|
||||||
|
|
||||||
|
def _get_current_employee(self):
|
||||||
|
employee = self.env.user.employee_id
|
||||||
|
if not employee:
|
||||||
|
raise UserError(_('No employee is linked to your user. Please contact HR.'))
|
||||||
|
return employee
|
||||||
|
|
||||||
|
def _get_date_range(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.download_type == 'single':
|
||||||
|
if not self.period_line:
|
||||||
|
raise UserError(_('Please select a month to download a single payslip.'))
|
||||||
|
return self.period_line.from_date, self.period_line.to_date
|
||||||
|
return self.period_id.from_date, self.period_id.to_date
|
||||||
|
|
||||||
|
def _get_available_payslips(self):
|
||||||
|
self.ensure_one()
|
||||||
|
employee = self._get_current_employee()
|
||||||
|
date_from, date_to = self._get_date_range()
|
||||||
|
if not date_from or not date_to:
|
||||||
|
return self.env['hr.payslip']
|
||||||
|
|
||||||
|
return self.env['hr.payslip'].sudo().search([
|
||||||
|
('employee_id', '=', employee.id),
|
||||||
|
('state', 'in', ['done', 'paid']),
|
||||||
|
('date_from', '>=', date_from),
|
||||||
|
('date_to', '<=', date_to),
|
||||||
|
('company_id', 'in', self.env.companies.ids),
|
||||||
|
], order='date_from asc, date_to asc, id asc')
|
||||||
|
|
||||||
|
def _get_payslips_for_download(self):
|
||||||
|
self.ensure_one()
|
||||||
|
payslips = self._get_available_payslips()
|
||||||
|
if not payslips:
|
||||||
|
raise UserError(_('No confirmed or paid payslip is available for the selected period.'))
|
||||||
|
if self.download_type == 'single' and len(payslips) > 1:
|
||||||
|
raise UserError(_('More than one payslip is available for this month. Please contact HR.'))
|
||||||
|
return payslips
|
||||||
|
|
||||||
|
def _get_pdf_content(self, payslip):
|
||||||
|
report = payslip.struct_id.report_id or self.env.ref('hr_payroll.action_report_payslip')
|
||||||
|
pdf_content, dummy = self.env['ir.actions.report'].sudo().with_context(
|
||||||
|
lang=payslip.employee_id.lang or self.env.lang,
|
||||||
|
)._render_qweb_pdf(report, payslip.id, data={'company_id': payslip.company_id})
|
||||||
|
return report, pdf_content
|
||||||
|
|
||||||
|
def _get_pdf_filename(self, payslip, report=False):
|
||||||
|
report = report or payslip.struct_id.report_id or self.env.ref('hr_payroll.action_report_payslip')
|
||||||
|
if report.print_report_name:
|
||||||
|
filename = safe_eval(report.print_report_name, {'object': payslip})
|
||||||
|
else:
|
||||||
|
filename = _('Payslip - %(employee)s - %(period)s', employee=payslip.employee_id.name, period=payslip.name)
|
||||||
|
return self._sanitize_filename(filename, 'payslip') + '.pdf'
|
||||||
|
|
||||||
|
def _get_zip_filename(self):
|
||||||
|
self.ensure_one()
|
||||||
|
employee = self._get_current_employee()
|
||||||
|
filename = _('Payslips - %(employee)s - %(period)s', employee=employee.name, period=self.period_id.name)
|
||||||
|
return self._sanitize_filename(filename, 'payslips') + '.zip'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _sanitize_filename(self, filename, fallback):
|
||||||
|
filename = re.sub(r'[\\/:*?"<>|]+', '-', str(filename or '')).strip(' .')
|
||||||
|
return filename or fallback
|
||||||
|
|
||||||
|
def action_download_payslips(self):
|
||||||
|
self.ensure_one()
|
||||||
|
self._get_payslips_for_download()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_url',
|
||||||
|
'url': '/employee_it_declaration/my_payslips/%s' % self.id,
|
||||||
|
'target': 'self',
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import re
|
||||||
|
|
||||||
class investmentCostings(models.Model):
|
class investmentCostings(models.Model):
|
||||||
_name = 'investment.costings'
|
_name = 'investment.costings'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_id'
|
_rec_name = 'investment_type_id'
|
||||||
|
|
||||||
investment_type_id = fields.Many2one('it.investment.type')
|
investment_type_id = fields.Many2one('it.investment.type')
|
||||||
|
|
@ -25,6 +26,7 @@ class investmentCostings(models.Model):
|
||||||
|
|
||||||
class pastEmpcostingType(models.Model):
|
class pastEmpcostingType(models.Model):
|
||||||
_name = 'past_employment.costing.type'
|
_name = 'past_employment.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -104,6 +106,7 @@ class pastEmpcostingType(models.Model):
|
||||||
|
|
||||||
class us80cCostingType(models.Model):
|
class us80cCostingType(models.Model):
|
||||||
_name = 'us80c.costing.type'
|
_name = 'us80c.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -140,6 +143,7 @@ class us80cCostingType(models.Model):
|
||||||
}
|
}
|
||||||
class us80dCostingType(models.Model):
|
class us80dCostingType(models.Model):
|
||||||
_name = 'us80d.costing.type'
|
_name = 'us80d.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -157,6 +161,7 @@ class us80dCostingType(models.Model):
|
||||||
|
|
||||||
class us10CostingType(models.Model):
|
class us10CostingType(models.Model):
|
||||||
_name = 'us10.costing.type'
|
_name = 'us10.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -173,6 +178,7 @@ class us10CostingType(models.Model):
|
||||||
|
|
||||||
class us80gCostingType(models.Model):
|
class us80gCostingType(models.Model):
|
||||||
_name = 'us80g.costing.type'
|
_name = 'us80g.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -189,6 +195,7 @@ class us80gCostingType(models.Model):
|
||||||
|
|
||||||
class chapterViaCostingType(models.Model):
|
class chapterViaCostingType(models.Model):
|
||||||
_name = 'chapter.via.costing.type'
|
_name = 'chapter.via.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -205,6 +212,7 @@ class chapterViaCostingType(models.Model):
|
||||||
|
|
||||||
class us17CostingType(models.Model):
|
class us17CostingType(models.Model):
|
||||||
_name = 'us17.costing.type'
|
_name = 'us17.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -219,6 +227,7 @@ class us17CostingType(models.Model):
|
||||||
|
|
||||||
class OtherILCostingType(models.Model):
|
class OtherILCostingType(models.Model):
|
||||||
_name = 'other.il.costing.type'
|
_name = 'other.il.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -256,6 +265,7 @@ class OtherILCostingType(models.Model):
|
||||||
|
|
||||||
class OtherDeclarationCostingType(models.Model):
|
class OtherDeclarationCostingType(models.Model):
|
||||||
_name = 'other.declaration.costing.type'
|
_name = 'other.declaration.costing.type'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_rec_name = 'investment_type_line_id'
|
_rec_name = 'investment_type_line_id'
|
||||||
|
|
||||||
costing_type = fields.Many2one('investment.costings')
|
costing_type = fields.Many2one('investment.costings')
|
||||||
|
|
@ -271,6 +281,7 @@ class OtherDeclarationCostingType(models.Model):
|
||||||
|
|
||||||
class HouseRentDeclaration(models.Model):
|
class HouseRentDeclaration(models.Model):
|
||||||
_name = 'house.rent.declaration'
|
_name = 'house.rent.declaration'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'House Rent Declaration'
|
_description = 'House Rent Declaration'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -302,8 +313,6 @@ class HouseRentDeclaration(models.Model):
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
# Auto-link applicant_id if context is passed correctly
|
# Auto-link applicant_id if context is passed correctly
|
||||||
if self.env.context.get('default_it_declaration_id'):
|
if self.env.context.get('default_it_declaration_id'):
|
||||||
import pdb
|
|
||||||
pdb.set_trace()
|
|
||||||
costing_id = self.env['investment.costings'].sudo().search([('id','=',self.env.context.get('it_declaration_id')),('investment_type_id.investment_type','=','house_rent')],limit=1)
|
costing_id = self.env['investment.costings'].sudo().search([('id','=',self.env.context.get('it_declaration_id')),('investment_type_id.investment_type','=','house_rent')],limit=1)
|
||||||
vals['costing_type'] = costing_id.id
|
vals['costing_type'] = costing_id.id
|
||||||
return super().create(vals)
|
return super().create(vals)
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
from odoo import models, fields
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
|
||||||
class ItInvestmentType(models.Model):
|
class ItInvestmentType(models.Model):
|
||||||
_name = 'it.investment.type'
|
_name = 'it.investment.type'
|
||||||
_rec_name = 'investment_type'
|
|
||||||
|
@api.depends('investment_type')
|
||||||
|
def _compute_display_name(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.investment_type:
|
||||||
|
rec.display_name = dict(
|
||||||
|
self._fields['investment_type'].selection
|
||||||
|
).get(rec.investment_type)
|
||||||
|
else:
|
||||||
|
rec.display_name = (
|
||||||
|
rec.investment_type.replace('_', ' ').title()
|
||||||
|
if rec.investment_type else ''
|
||||||
|
)
|
||||||
|
|
||||||
sequence = fields.Integer()
|
sequence = fields.Integer()
|
||||||
investment_type = fields.Selection(
|
investment_type = fields.Selection(
|
||||||
|
|
@ -11,6 +23,11 @@ class ItInvestmentType(models.Model):
|
||||||
('us_80g', 'US 80G'), ('chapter_via', 'CHAPTER VIA'), ('us_17', 'US 17'), ('house_rent', 'HOUSE RENT'),
|
('us_80g', 'US 80G'), ('chapter_via', 'CHAPTER VIA'), ('us_17', 'US 17'), ('house_rent', 'HOUSE RENT'),
|
||||||
('other_i_or_l', 'OTHER INCOME/LOSS'), ('other_declaration', 'OTHER DECLARATION')], string="Investment Type",
|
('other_i_or_l', 'OTHER INCOME/LOSS'), ('other_declaration', 'OTHER DECLARATION')], string="Investment Type",
|
||||||
required=True)
|
required=True)
|
||||||
|
regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime'),
|
||||||
|
('both', 'Both')
|
||||||
|
], string='Regime', required=True, default='both')
|
||||||
|
|
||||||
active = fields.Boolean(default=True)
|
active = fields.Boolean(default=True)
|
||||||
past_employment_ids = fields.One2many('past_employment.investment.type','investment_type')
|
past_employment_ids = fields.One2many('past_employment.investment.type','investment_type')
|
||||||
|
|
@ -22,6 +39,45 @@ class ItInvestmentType(models.Model):
|
||||||
us17_ids = fields.One2many('us17.investment.type', 'investment_type')
|
us17_ids = fields.One2many('us17.investment.type', 'investment_type')
|
||||||
other_il_ids = fields.One2many('other.il.investment.type', 'investment_type')
|
other_il_ids = fields.One2many('other.il.investment.type', 'investment_type')
|
||||||
other_declaration_ids = fields.One2many('other.declaration.investment.type', 'investment_type')
|
other_declaration_ids = fields.One2many('other.declaration.investment.type', 'investment_type')
|
||||||
|
period_ids = fields.Many2many(
|
||||||
|
'payroll.period',
|
||||||
|
'it_investment_type_payroll_period_rel',
|
||||||
|
'investment_type_id',
|
||||||
|
'period_id',
|
||||||
|
string='Periods'
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
self.env.cr.execute("""
|
||||||
|
INSERT INTO it_investment_type_payroll_period_rel (investment_type_id, period_id)
|
||||||
|
SELECT investment_type.id, period.id
|
||||||
|
FROM it_investment_type AS investment_type
|
||||||
|
JOIN payroll_period AS period ON TRUE
|
||||||
|
LEFT JOIN it_investment_type_payroll_period_rel AS rel
|
||||||
|
ON rel.investment_type_id = investment_type.id
|
||||||
|
WHERE investment_type.active = TRUE
|
||||||
|
AND rel.investment_type_id IS NULL
|
||||||
|
""")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_all_period_commands(self):
|
||||||
|
return [(6, 0, self.env['payroll.period'].search([]).ids)]
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if 'period_ids' not in vals and vals.get('active', True):
|
||||||
|
vals['period_ids'] = self._get_all_period_commands()
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
res = super().write(vals)
|
||||||
|
if vals.get('active') is True:
|
||||||
|
all_period_commands = self._get_all_period_commands()
|
||||||
|
self.filtered(lambda record: record.active and not record.period_ids).write({
|
||||||
|
'period_ids': all_period_commands,
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
|
||||||
class pastEmpInvestmentType(models.Model):
|
class pastEmpInvestmentType(models.Model):
|
||||||
_name = 'past_employment.investment.type'
|
_name = 'past_employment.investment.type'
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,13 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
# Inputs
|
# Inputs
|
||||||
employee_id = fields.Many2one('hr.employee', required=True, default=lambda self: self.env.user.employee_id.id)
|
employee_id = fields.Many2one('hr.employee', required=True, default=lambda self: self.env.user.employee_id.id)
|
||||||
emp_doj = fields.Date(related='employee_id.doj', store=True)
|
emp_doj = fields.Date(related='employee_id.doj', store=True)
|
||||||
|
is_general_tax_statement = fields.Boolean(default=True)
|
||||||
contract_id = fields.Many2one('hr.contract', related='employee_id.contract_id', required=True)
|
contract_id = fields.Many2one('hr.contract', related='employee_id.contract_id', required=True)
|
||||||
|
currency_id = fields.Many2one('res.currency', related='employee_id.company_id.currency_id')
|
||||||
|
|
||||||
period_id = fields.Many2one('payroll.period', required=True)
|
period_id = fields.Many2one('payroll.period', required=True)
|
||||||
period_line = fields.Many2one('payroll.period.line')
|
period_line = fields.Many2one('payroll.period.line',
|
||||||
|
domain="[('period_id', '=', period_id), ('to_date', '<', fields.Date.today())]")
|
||||||
|
|
||||||
# Taxpayer profile
|
# Taxpayer profile
|
||||||
taxpayer_name = fields.Char(related='employee_id.name')
|
taxpayer_name = fields.Char(related='employee_id.name')
|
||||||
|
|
@ -67,25 +70,76 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
ded_80G = fields.Float(string="Deduction under 80G", default=0.0)
|
ded_80G = fields.Float(string="Deduction under 80G", default=0.0)
|
||||||
ded_other = fields.Float(string="Other Deductions", default=0.0)
|
ded_other = fields.Float(string="Other Deductions", default=0.0)
|
||||||
|
|
||||||
def _get_applicable_slab(self, regime, age, residence_type):
|
comparison_available = fields.Boolean(default=False)
|
||||||
"""Get the applicable tax slab based on regime, age, and residence type"""
|
old_regime_taxable_income = fields.Float(
|
||||||
# Determine age category
|
string="Old Regime Taxable Income",
|
||||||
if age < 60:
|
readonly=True
|
||||||
age_category = 'below_60'
|
)
|
||||||
elif age < 80:
|
new_regime_taxable_income = fields.Float(
|
||||||
age_category = '60_to_80'
|
string="New Regime Taxable Income",
|
||||||
else:
|
readonly=True
|
||||||
age_category = 'above_80'
|
)
|
||||||
|
old_regime_tax_payable = fields.Float(
|
||||||
|
string="Old Regime Tax Payable",
|
||||||
|
readonly=True
|
||||||
|
)
|
||||||
|
new_regime_tax_payable = fields.Float(
|
||||||
|
string="New Regime Tax Payable",
|
||||||
|
readonly=True
|
||||||
|
)
|
||||||
|
tax_difference = fields.Float(
|
||||||
|
string="Tax Difference",
|
||||||
|
readonly=True
|
||||||
|
)
|
||||||
|
beneficial_regime = fields.Selection([
|
||||||
|
('old', 'Old Regime'),
|
||||||
|
('new', 'New Regime')
|
||||||
|
], string="Beneficial Regime", readonly=True)
|
||||||
|
|
||||||
# Search for slab master
|
def _get_age_category(self, age):
|
||||||
slab_master = self.env['it.slab.master'].search([
|
if age < 60:
|
||||||
|
return 'below_60'
|
||||||
|
elif age < 80:
|
||||||
|
return '60_to_80'
|
||||||
|
return 'above_80'
|
||||||
|
|
||||||
|
def _get_effective_period_start(self):
|
||||||
|
self.ensure_one()
|
||||||
|
period_start = self.period_id.from_date if self.period_id else False
|
||||||
|
if not period_start:
|
||||||
|
return False
|
||||||
|
if self.emp_doj and self.period_id.to_date and self.period_id.from_date <= self.emp_doj <= self.period_id.to_date:
|
||||||
|
return max(period_start, self.emp_doj.replace(day=1))
|
||||||
|
return period_start
|
||||||
|
|
||||||
|
def _get_effective_period_lines(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.period_id:
|
||||||
|
return self.env['payroll.period.line']
|
||||||
|
|
||||||
|
period_lines = self.period_id.period_line_ids.sorted('from_date')
|
||||||
|
effective_start = self._get_effective_period_start()
|
||||||
|
if not effective_start:
|
||||||
|
return period_lines
|
||||||
|
return period_lines.filtered(lambda line: line.to_date and line.to_date >= effective_start)
|
||||||
|
|
||||||
|
def _find_applicable_slab(self, regime, period_id, age, residence_type):
|
||||||
|
"""Find the applicable tax slab without forcing both regimes to exist."""
|
||||||
|
age_category = self._get_age_category(age)
|
||||||
|
residence_type = (residence_type or '').lower().replace('-', '_')
|
||||||
|
return self.env['it.slab.master'].search([
|
||||||
|
('period_id','=',period_id.id),
|
||||||
('regime', '=', regime),
|
('regime', '=', regime),
|
||||||
('age_category', '=', age_category),
|
('age_category', '=', age_category),
|
||||||
'|',
|
'|',
|
||||||
('residence_type', '=', residence_type.lower()),
|
('residence_type', '=', residence_type),
|
||||||
('residence_type', '=', 'both')
|
('residence_type', '=', 'both')
|
||||||
], limit=1)
|
], limit=1)
|
||||||
|
|
||||||
|
def _get_applicable_slab(self, regime, period_id, age, residence_type):
|
||||||
|
"""Get the applicable tax slab based on regime, age, and residence type"""
|
||||||
|
age_category = self._get_age_category(age)
|
||||||
|
slab_master = self._find_applicable_slab(regime, period_id, age, residence_type)
|
||||||
if not slab_master:
|
if not slab_master:
|
||||||
raise ValidationError(_(
|
raise ValidationError(_(
|
||||||
"No tax slab found for %s Regime with Age Category: %s and Residence Type: %s"
|
"No tax slab found for %s Regime with Age Category: %s and Residence Type: %s"
|
||||||
|
|
@ -93,174 +147,286 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
|
|
||||||
return slab_master
|
return slab_master
|
||||||
|
|
||||||
def _compute_tax_using_slab(self, taxable, slab_master):
|
@api.onchange('employee_id', 'period_id')
|
||||||
"""Compute tax using slab master rules"""
|
def _onchange_employee_id_period_id(self):
|
||||||
tax = 0.0
|
domain_by_record = {}
|
||||||
|
|
||||||
# Get rules sorted by min_income
|
|
||||||
rules = slab_master.rules.sorted('min_income')
|
|
||||||
|
|
||||||
for rule in rules:
|
|
||||||
if taxable <= rule.min_income:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Calculate amount in this bracket
|
|
||||||
bracket_max = rule.max_income if rule.max_income else float('inf')
|
|
||||||
amount_in_bracket = min(taxable, bracket_max) - rule.min_income
|
|
||||||
|
|
||||||
# Apply tax calculation based on rule structure
|
|
||||||
if rule.fixed_amount and rule.excess_threshold:
|
|
||||||
# Rule with fixed amount and excess threshold
|
|
||||||
excess_amount = max(0, taxable - rule.excess_threshold)
|
|
||||||
tax_for_bracket = rule.fixed_amount + (excess_amount * rule.tax_rate / 100)
|
|
||||||
else:
|
|
||||||
# Standard bracket calculation
|
|
||||||
tax_for_bracket = amount_in_bracket * rule.tax_rate / 100
|
|
||||||
|
|
||||||
tax += tax_for_bracket
|
|
||||||
|
|
||||||
return tax
|
|
||||||
|
|
||||||
@api.depends('employee_id', 'contract_id', 'period_id')
|
|
||||||
def _compute_salary_components(self):
|
|
||||||
"""Compute salary components from payroll data"""
|
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if not rec.employee_id or not rec.contract_id:
|
domain = [('period_id', '=', rec.period_id.id), ('to_date', '<', fields.Date.today())] if rec.period_id else []
|
||||||
|
if rec.emp_doj:
|
||||||
|
domain.append(('to_date', '>=', rec.emp_doj.replace(day=1)))
|
||||||
|
|
||||||
|
if rec.period_line and rec.period_line not in rec._get_effective_period_lines():
|
||||||
|
rec.period_line = False
|
||||||
|
domain_by_record[rec.id] = domain
|
||||||
|
if len(self) == 1:
|
||||||
|
return {'domain': {'period_line': domain_by_record.get(self.id, [])}}
|
||||||
|
|
||||||
|
def _get_standard_deduction(self, regime, slab_master=False):
|
||||||
|
if slab_master:
|
||||||
|
return slab_master.standard_deduction
|
||||||
|
return 75000.0 if regime == 'new' else 50000.0
|
||||||
|
|
||||||
|
def _compute_tax_using_slab(self, taxable, slab_master):
|
||||||
|
"""Compute tax using slab fixed amount logic"""
|
||||||
|
# Sort by sequence first, then by max_income for rules with same sequence
|
||||||
|
rules = slab_master.rules.sorted(lambda r: (r.sequence, r.max_income or float('inf')))
|
||||||
|
|
||||||
|
if not rules:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Find which slab the taxable income falls into
|
||||||
|
applicable_rule = None
|
||||||
|
for rule in rules:
|
||||||
|
min_income = rule.min_income or 0
|
||||||
|
max_income = rule.max_income if rule.max_income else float('inf')
|
||||||
|
|
||||||
|
if min_income < taxable <= max_income:
|
||||||
|
applicable_rule = rule
|
||||||
|
break
|
||||||
|
if not applicable_rule:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Get all rules with sequence less than applicable rule
|
||||||
|
# For rules with same sequence, we need to be careful
|
||||||
|
previous_rules = rules.filtered(
|
||||||
|
lambda r: r.sequence < applicable_rule.sequence or
|
||||||
|
(r.sequence == applicable_rule.sequence and
|
||||||
|
(r.max_income or float('inf')) < (applicable_rule.max_income or float('inf')))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the previous slab's max income (or 0 if first slab)
|
||||||
|
previous_max = 0
|
||||||
|
if previous_rules:
|
||||||
|
# Get the last rule from previous_rules (which is already sorted)
|
||||||
|
previous_max = previous_rules[-1].max_income or 0
|
||||||
|
|
||||||
|
# Calculate percentage tax for the current slab
|
||||||
|
taxable_in_current_slab = taxable - previous_max
|
||||||
|
current_tax = taxable_in_current_slab * (applicable_rule.tax_rate / 100)
|
||||||
|
|
||||||
|
# Sum fixed amounts from all previous slabs
|
||||||
|
previous_fixed_amounts = sum(previous_rules.mapped('fixed_amount'))
|
||||||
|
|
||||||
|
total_tax = current_tax + previous_fixed_amounts
|
||||||
|
|
||||||
|
return total_tax
|
||||||
|
|
||||||
|
@api.depends('employee_id', 'contract_id', 'period_id', 'period_line')
|
||||||
|
def _compute_salary_components(self):
|
||||||
|
"""Compute salary components from the same payroll source used by the report."""
|
||||||
|
for rec in self:
|
||||||
|
rec.basic_salary = 0.0
|
||||||
|
rec.hra_salary = 0.0
|
||||||
|
rec.lta_salary = 0.0
|
||||||
|
rec.special_allowance = 0.0
|
||||||
|
rec.gross_salary = 0.0
|
||||||
|
|
||||||
|
if not rec.employee_id or not rec.contract_id or not rec.period_line:
|
||||||
continue
|
continue
|
||||||
# Get payslip for the period
|
|
||||||
payslip = self.env['hr.payslip'].search([
|
|
||||||
('employee_id', '=', rec.employee_id.id),
|
|
||||||
('date_from', '>=', rec.period_line.from_date),
|
|
||||||
('date_to', '<=', rec.period_line.to_date),
|
|
||||||
('state', 'in', ['verify','done','paid'])
|
|
||||||
], limit=1)
|
|
||||||
|
|
||||||
if payslip:
|
components = rec._get_salary_components_for_period_line(rec.period_line)
|
||||||
# Extract salary components from payslip lines
|
rec.basic_salary = components['basic_salary']
|
||||||
rec.basic_salary = self._get_salary_rule_amount(payslip, 'BASIC')
|
rec.hra_salary = components['hra_salary']
|
||||||
rec.hra_salary = self._get_salary_rule_amount(payslip, 'HRA')
|
rec.lta_salary = components['lta_salary']
|
||||||
rec.lta_salary = self._get_salary_rule_amount(payslip, 'LTA')
|
rec.special_allowance = components['special_allowance']
|
||||||
rec.special_allowance = self._get_salary_rule_amount(payslip, 'SPA')
|
rec.gross_salary = components['gross_salary']
|
||||||
rec.gross_salary = self._get_salary_rule_amount(payslip, 'GROSS')
|
|
||||||
else:
|
|
||||||
# Fallback to contract values
|
|
||||||
rec.basic_salary = rec.contract_id.wage * 0.4 # Assuming 40% basic
|
|
||||||
rec.hra_salary = rec.contract_id.wage * 0.2 # Assuming 20% HRA
|
|
||||||
rec.lta_salary = rec.contract_id.wage * 0.1 # Assuming 10% LTA
|
|
||||||
rec.special_allowance = rec.contract_id.wage * 0.3 # Remaining as special allowance
|
|
||||||
rec.gross_salary = rec.contract_id.wage
|
|
||||||
|
|
||||||
|
def _get_valid_payslip_for_period_line(self, period_line):
|
||||||
|
payslip = self.env['hr.payslip'].search([
|
||||||
|
('employee_id', '=', self.employee_id.id),
|
||||||
|
('date_from', '>=', period_line.from_date),
|
||||||
|
('date_to', '<=', period_line.to_date),
|
||||||
|
('state', 'in', ['verify', 'done', 'paid'])
|
||||||
|
], limit=1)
|
||||||
|
if not payslip:
|
||||||
|
return payslip
|
||||||
|
|
||||||
|
refund_payslip = self.env['hr.payslip'].search([
|
||||||
|
('id', '!=', payslip.id),
|
||||||
|
('name', 'ilike', payslip.number),
|
||||||
|
('state', 'in', ['verify', 'done', 'paid'])
|
||||||
|
], limit=1)
|
||||||
|
return self.env['hr.payslip'] if refund_payslip else payslip
|
||||||
|
|
||||||
|
def _get_rule_amounts_for_period_line(self, period_line, rule_codes):
|
||||||
|
payslip = self._get_valid_payslip_for_period_line(period_line)
|
||||||
|
dummy_payslip = False
|
||||||
|
if not payslip:
|
||||||
|
dummy_payslip = self.env['hr.payslip'].sudo().create({
|
||||||
|
'name': 'Test Payslip',
|
||||||
|
'employee_id': self.employee_id.id,
|
||||||
|
'date_from': period_line.from_date,
|
||||||
|
'date_to': period_line.to_date
|
||||||
|
})
|
||||||
|
dummy_payslip.sudo().compute_sheet()
|
||||||
|
payslip = dummy_payslip
|
||||||
|
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
rule_code: self._get_salary_rule_amount(payslip, rule_code)
|
||||||
|
for rule_code in rule_codes
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
if dummy_payslip:
|
||||||
|
dummy_payslip.sudo().action_payslip_cancel()
|
||||||
|
dummy_payslip.sudo().unlink()
|
||||||
|
|
||||||
|
def _get_salary_components_for_period_line(self, period_line):
|
||||||
|
rule_codes = ['BASIC', 'HRA', 'LTA', 'SPA', 'GROSS', 'NET', 'ASSIG_SALARY', 'ATTACH_SALARY']
|
||||||
|
rule_amounts = self._get_rule_amounts_for_period_line(
|
||||||
|
period_line,
|
||||||
|
rule_codes
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
'basic_salary': rule_amounts['BASIC'],
|
||||||
|
'hra_salary': rule_amounts['HRA'],
|
||||||
|
'lta_salary': rule_amounts['LTA'],
|
||||||
|
'special_allowance': rule_amounts['SPA'],
|
||||||
|
'gross_salary': rule_amounts['GROSS'],
|
||||||
|
'net_salary': rule_amounts['NET'],
|
||||||
|
'salary_advance': rule_amounts['ASSIG_SALARY'],
|
||||||
|
'advance_recovery': rule_amounts['ATTACH_SALARY'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_other_payslip_components_for_period_line(self, period_line):
|
||||||
|
payslip = self._get_valid_payslip_for_period_line(period_line)
|
||||||
|
if not payslip:
|
||||||
|
return []
|
||||||
|
|
||||||
|
excluded_codes = {
|
||||||
|
'BASIC', 'HRA', 'LTA', 'SPA', 'GROSS', 'NET', 'PT', 'PFE', 'PF',
|
||||||
|
'ATTACH_SALARY',
|
||||||
|
}
|
||||||
|
grouped = {}
|
||||||
|
income_category_codes = {'BASIC', 'ALW', 'LEAVE'}
|
||||||
|
for line in payslip.line_ids.filtered(
|
||||||
|
lambda item: item.total
|
||||||
|
and (item.salary_rule_id.code or item.code) not in excluded_codes
|
||||||
|
and item.category_id.code in income_category_codes):
|
||||||
|
name = line.name or line.salary_rule_id.name or line.code
|
||||||
|
code = line.salary_rule_id.code or line.code
|
||||||
|
key = (code, name)
|
||||||
|
if key not in grouped:
|
||||||
|
grouped[key] = {
|
||||||
|
'code': code,
|
||||||
|
'name': name,
|
||||||
|
'actual': 0.0,
|
||||||
|
'projected': 0.0,
|
||||||
|
}
|
||||||
|
grouped[key]['actual'] += line.total
|
||||||
|
|
||||||
|
for input_line in payslip.input_line_ids.filtered(lambda item: item.amount and item.code not in excluded_codes):
|
||||||
|
name = input_line.name or input_line.input_type_id.name or input_line.code
|
||||||
|
key = (input_line.code, name)
|
||||||
|
if key in grouped:
|
||||||
|
continue
|
||||||
|
grouped[key] = {
|
||||||
|
'code': input_line.code,
|
||||||
|
'name': name,
|
||||||
|
'actual': input_line.amount,
|
||||||
|
'projected': 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
return list(grouped.values())
|
||||||
|
|
||||||
def fetch_salary_components(self):
|
def fetch_salary_components(self):
|
||||||
"""fetch salary components from payroll data"""
|
"""fetch salary components from payroll data"""
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if not rec.employee_id or not rec.contract_id:
|
|
||||||
continue
|
|
||||||
data = {
|
data = {
|
||||||
'basic_salary' : {'actual':[],'projected':[]},
|
'basic_salary' : {'actual':[],'projected':[]},
|
||||||
'hra_salary': {'actual': [], 'projected': []},
|
'hra_salary': {'actual': [], 'projected': []},
|
||||||
'lta_salary': {'actual': [], 'projected': []},
|
'lta_salary': {'actual': [], 'projected': []},
|
||||||
'special_allowance' : {'actual':[],'projected':[]},
|
'special_allowance' : {'actual':[],'projected':[]},
|
||||||
'gross_salary' : {'actual':[],'projected':[]}
|
'gross_salary' : {'actual':[],'projected':[]},
|
||||||
|
'net_salary' : {'actual':[],'projected':[]},
|
||||||
|
'salary_advance' : {'actual':[],'projected':[]},
|
||||||
|
'advance_recovery' : {'actual':[],'projected':[]},
|
||||||
|
'other_components': {},
|
||||||
}
|
}
|
||||||
period_lines = rec.period_id.period_line_ids
|
if not rec.employee_id or not rec.contract_id or not rec.period_id or not rec.period_line:
|
||||||
|
return data
|
||||||
|
period_lines = rec._get_effective_period_lines()
|
||||||
|
|
||||||
for line in period_lines:
|
for line in period_lines:
|
||||||
basic_salary = float()
|
components = rec._get_salary_components_for_period_line(line)
|
||||||
hra_salary = float()
|
if line.from_date and rec.period_line.from_date and line.from_date <= rec.period_line.from_date:
|
||||||
lta_salary = float()
|
data['basic_salary']['actual'].append(components['basic_salary'])
|
||||||
special_allowance = float()
|
data['hra_salary']['actual'].append(components['hra_salary'])
|
||||||
gross_salary = float()
|
data['lta_salary']['actual'].append(components['lta_salary'])
|
||||||
payslip = self.env['hr.payslip'].search([
|
data['special_allowance']['actual'].append(components['special_allowance'])
|
||||||
('employee_id', '=', rec.employee_id.id),
|
data['gross_salary']['actual'].append(components['gross_salary'])
|
||||||
('date_from', '>=', line.from_date),
|
data['net_salary']['actual'].append(components['net_salary'])
|
||||||
('date_to', '<=', line.to_date),
|
data['salary_advance']['actual'].append(components['salary_advance'])
|
||||||
('state', 'in', ['verify', 'done', 'paid'])
|
data['advance_recovery']['actual'].append(components['advance_recovery'])
|
||||||
], limit=1)
|
bucket = 'actual'
|
||||||
if payslip:
|
|
||||||
# Extract salary components from payslip lines
|
|
||||||
basic_salary = self._get_salary_rule_amount(payslip, 'BASIC')
|
|
||||||
hra_salary = self._get_salary_rule_amount(payslip, 'HRA')
|
|
||||||
lta_salary = self._get_salary_rule_amount(payslip, 'LTA')
|
|
||||||
special_allowance = self._get_salary_rule_amount(payslip, 'SPA')
|
|
||||||
gross_salary = self._get_salary_rule_amount(payslip, 'GROSS')
|
|
||||||
else:
|
else:
|
||||||
payslip = self.env['hr.payslip'].sudo().create({
|
data['basic_salary']['projected'].append(components['basic_salary'])
|
||||||
'name': 'Test Payslip',
|
data['hra_salary']['projected'].append(components['hra_salary'])
|
||||||
'employee_id': rec.employee_id.id,
|
data['lta_salary']['projected'].append(components['lta_salary'])
|
||||||
'date_from': line.from_date,
|
data['special_allowance']['projected'].append(components['special_allowance'])
|
||||||
'date_to': line.to_date
|
data['gross_salary']['projected'].append(components['gross_salary'])
|
||||||
})
|
data['net_salary']['projected'].append(components['net_salary'])
|
||||||
payslip.sudo().compute_sheet()
|
data['salary_advance']['projected'].append(components['salary_advance'])
|
||||||
|
data['advance_recovery']['projected'].append(components['advance_recovery'])
|
||||||
# Extract salary components from payslip lines
|
bucket = 'projected'
|
||||||
basic_salary = self._get_salary_rule_amount(payslip, 'BASIC')
|
for other_component in rec._get_other_payslip_components_for_period_line(line):
|
||||||
hra_salary = self._get_salary_rule_amount(payslip, 'HRA')
|
key = (other_component['code'], other_component['name'])
|
||||||
lta_salary = self._get_salary_rule_amount(payslip, 'LTA')
|
if key not in data['other_components']:
|
||||||
special_allowance = self._get_salary_rule_amount(payslip, 'SPA')
|
data['other_components'][key] = {
|
||||||
gross_salary = self._get_salary_rule_amount(payslip, 'GROSS')
|
'code': other_component['code'],
|
||||||
|
'name': other_component['name'],
|
||||||
payslip.sudo().action_payslip_cancel()
|
'actual': 0.0,
|
||||||
payslip.sudo().unlink()
|
'projected': 0.0,
|
||||||
|
}
|
||||||
if line.from_date <= rec.period_line.from_date:
|
data['other_components'][key][bucket] += other_component['actual']
|
||||||
data['basic_salary']['actual'].append(basic_salary)
|
|
||||||
data['hra_salary']['actual'].append(hra_salary)
|
|
||||||
data['lta_salary']['actual'].append(lta_salary)
|
|
||||||
data['special_allowance']['actual'].append(special_allowance)
|
|
||||||
data['gross_salary']['actual'].append(gross_salary)
|
|
||||||
else:
|
|
||||||
data['basic_salary']['projected'].append(basic_salary)
|
|
||||||
data['hra_salary']['projected'].append(hra_salary)
|
|
||||||
data['lta_salary']['projected'].append(lta_salary)
|
|
||||||
data['special_allowance']['projected'].append(special_allowance)
|
|
||||||
data['gross_salary']['projected'].append(gross_salary)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _get_salary_rule_amount(self, payslip, rule_code):
|
def _get_salary_rule_amount(self, payslip, rule_code):
|
||||||
"""Get amount for a specific salary rule from payslip"""
|
"""Get amount for a specific salary rule from payslip"""
|
||||||
line = payslip.line_ids.filtered(lambda l: l.salary_rule_id.code == rule_code)
|
line = payslip.line_ids.filtered(lambda l: l.salary_rule_id.code == rule_code)
|
||||||
return line.total if line else 0.0
|
return sum(line.mapped('total')) if line else 0.0
|
||||||
|
|
||||||
@api.depends('employee_id', 'contract_id', 'period_id', 'tax_regime')
|
@api.depends('employee_id', 'contract_id', 'period_id', 'period_line', 'tax_regime')
|
||||||
def _compute_deductions(self):
|
def _compute_deductions(self):
|
||||||
"""Compute deductions from payroll data"""
|
"""Compute deductions from payroll data"""
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if not rec.employee_id or not rec.contract_id:
|
rec.professional_tax = 0.0
|
||||||
|
rec.standard_deduction = 0.0
|
||||||
|
rec.nps_employer_contribution = 0.0
|
||||||
|
|
||||||
|
if not rec.employee_id or not rec.contract_id or not rec.period_id or not rec.period_line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get payslip for the period
|
deduction_data = rec.fetch_deduction_components()
|
||||||
payslip = self.env['hr.payslip'].search([
|
rec.professional_tax = (
|
||||||
('employee_id', '=', rec.employee_id.id),
|
sum(deduction_data['professional_tax']['actual']) +
|
||||||
('date_from', '>=', rec.period_id.from_date),
|
sum(deduction_data['professional_tax']['projected'])
|
||||||
('date_to', '<=', rec.period_id.to_date),
|
|
||||||
('state', 'in', ['verify', 'done', 'paid'])
|
|
||||||
], limit=1)
|
|
||||||
|
|
||||||
fy_start = self.period_id.from_date
|
|
||||||
fy_end = self.period_id.to_date
|
|
||||||
total_months = ((fy_end.year - fy_start.year) * 12 +
|
|
||||||
(fy_end.month - fy_start.month) + 1)
|
|
||||||
|
|
||||||
line_start = self.period_line.from_date
|
|
||||||
current_month_index = ((line_start.year - fy_start.year) * 12 +
|
|
||||||
(line_start.month - fy_start.month) + 1)
|
|
||||||
if payslip:
|
|
||||||
rec.professional_tax = (self._get_salary_rule_amount(payslip, 'PT'))*current_month_index
|
|
||||||
rec.nps_employer_contribution = self._get_salary_rule_amount(payslip, 'PFE')
|
|
||||||
else:
|
|
||||||
rec.professional_tax = 0.0
|
|
||||||
rec.nps_employer_contribution = 0.0
|
|
||||||
|
|
||||||
# Get standard deduction from slab master
|
|
||||||
if rec.tax_regime == 'new':
|
|
||||||
slab_master = self._get_applicable_slab('new', rec.taxpayer_age, rec.residential_status)
|
|
||||||
else:
|
|
||||||
slab_master = self._get_applicable_slab('old', rec.taxpayer_age, rec.residential_status)
|
|
||||||
|
|
||||||
rec.standard_deduction = slab_master.standard_deduction if slab_master else (
|
|
||||||
75000 if rec.tax_regime == 'new' else 50000
|
|
||||||
)
|
)
|
||||||
|
rec.nps_employer_contribution = (
|
||||||
|
sum(deduction_data['nps_employer_contribution']['actual']) +
|
||||||
|
sum(deduction_data['nps_employer_contribution']['projected'])
|
||||||
|
)
|
||||||
|
|
||||||
|
slab_master = rec._get_applicable_slab(
|
||||||
|
rec.tax_regime, rec.period_id, rec.taxpayer_age, rec.residential_status
|
||||||
|
)
|
||||||
|
rec.standard_deduction = rec._get_standard_deduction(rec.tax_regime, slab_master)
|
||||||
|
|
||||||
|
def fetch_deduction_components(self):
|
||||||
|
for rec in self:
|
||||||
|
data = {
|
||||||
|
'professional_tax': {'actual': [], 'projected': []},
|
||||||
|
'nps_employer_contribution': {'actual': [], 'projected': []},
|
||||||
|
}
|
||||||
|
if not rec.employee_id or not rec.contract_id or not rec.period_id or not rec.period_line:
|
||||||
|
return data
|
||||||
|
|
||||||
|
for line in rec._get_effective_period_lines():
|
||||||
|
rule_amounts = rec._get_rule_amounts_for_period_line(line, ['PT', 'PFE'])
|
||||||
|
bucket = 'actual' if line.from_date <= rec.period_line.from_date else 'projected'
|
||||||
|
data['professional_tax'][bucket].append(rule_amounts['PT'])
|
||||||
|
data['nps_employer_contribution'][bucket].append(rule_amounts['PFE'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
@api.onchange('employee_id')
|
@api.onchange('employee_id')
|
||||||
|
|
@ -272,6 +438,23 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
else:
|
else:
|
||||||
rec.taxpayer_age = rec.taxpayer_age or 0
|
rec.taxpayer_age = rec.taxpayer_age or 0
|
||||||
|
|
||||||
|
@api.onchange(
|
||||||
|
'employee_id', 'period_id', 'period_line', 'tax_regime', 'taxpayer_age',
|
||||||
|
'residential_status', 'other_income', 'hra_exemption',
|
||||||
|
'interest_home_loan_self', 'interest_home_loan_letout', 'rental_income',
|
||||||
|
'ded_80C', 'ded_80CCD1B', 'ded_80D_self', 'ded_80D_parents',
|
||||||
|
'ded_80G', 'ded_other'
|
||||||
|
)
|
||||||
|
def _onchange_reset_regime_comparison(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.comparison_available = False
|
||||||
|
rec.old_regime_taxable_income = 0.0
|
||||||
|
rec.new_regime_taxable_income = 0.0
|
||||||
|
rec.old_regime_tax_payable = 0.0
|
||||||
|
rec.new_regime_tax_payable = 0.0
|
||||||
|
rec.tax_difference = 0.0
|
||||||
|
rec.beneficial_regime = False
|
||||||
|
|
||||||
@api.onchange('basic_salary', 'hra_salary')
|
@api.onchange('basic_salary', 'hra_salary')
|
||||||
def onchange_hra_exemption(self):
|
def onchange_hra_exemption(self):
|
||||||
"""Calculate HRA exemption based on salary components"""
|
"""Calculate HRA exemption based on salary components"""
|
||||||
|
|
@ -319,7 +502,7 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
return min(60000.0, slab_tax)
|
return min(60000.0, slab_tax)
|
||||||
|
|
||||||
def _apply_surcharge_with_mr(self, slab_master, taxable, tax_after_rebate, regime):
|
def _apply_surcharge_with_mr(self, slab_master, taxable, tax_after_rebate, regime):
|
||||||
rules = slab_master.rules.sorted('min_income')
|
rules = slab_master.surcharges.sorted('min_income')
|
||||||
table = [(rule.min_income, rule.surcharge_rate) for rule in rules if rule.surcharge_rate > 0]
|
table = [(rule.min_income, rule.surcharge_rate) for rule in rules if rule.surcharge_rate > 0]
|
||||||
|
|
||||||
threshold = None
|
threshold = None
|
||||||
|
|
@ -338,9 +521,11 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
tax_with_surcharge = total_before_mr - mr
|
tax_with_surcharge = total_before_mr - mr
|
||||||
return surcharge, mr, tax_with_surcharge
|
return surcharge, mr, tax_with_surcharge
|
||||||
|
|
||||||
def _compute_tax_old_regime(self, taxable):
|
def _compute_tax_old_regime(self, taxable, slab_master=False):
|
||||||
# Get applicable slab
|
# Get applicable slab
|
||||||
slab_master = self._get_applicable_slab('old', self.taxpayer_age, self.residential_status)
|
slab_master = slab_master or self._get_applicable_slab(
|
||||||
|
'old', self.period_id, self.taxpayer_age, self.residential_status
|
||||||
|
)
|
||||||
|
|
||||||
# Compute slab tax
|
# Compute slab tax
|
||||||
slab_tax = self._compute_tax_using_slab(taxable, slab_master)
|
slab_tax = self._compute_tax_using_slab(taxable, slab_master)
|
||||||
|
|
@ -358,7 +543,9 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
rules = slab_master.rules.sorted('min_income')
|
rules = slab_master.rules.sorted('min_income')
|
||||||
|
|
||||||
cess_rate = [rule.cess_rate for rule in rules if rule.min_income<=taxable and rule.max_income >= taxable]
|
cess_rate = [rule.cess_rate for rule in rules if rule.min_income<=taxable and rule.max_income >= taxable]
|
||||||
cess = tax_with_surcharge * cess_rate[0]/100
|
cess = 0
|
||||||
|
if cess_rate and tax_with_surcharge:
|
||||||
|
cess = tax_with_surcharge * cess_rate[0]/100
|
||||||
|
|
||||||
total_tax = tax_with_surcharge + cess
|
total_tax = tax_with_surcharge + cess
|
||||||
|
|
||||||
|
|
@ -374,9 +561,11 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
'total_tax': total_tax
|
'total_tax': total_tax
|
||||||
}
|
}
|
||||||
|
|
||||||
def _compute_tax_new_regime(self, taxable):
|
def _compute_tax_new_regime(self, taxable, slab_master=False):
|
||||||
# Get applicable slab (new regime doesn't depend on age)
|
# Get applicable slab (new regime doesn't depend on age)
|
||||||
slab_master = self._get_applicable_slab('new', self.taxpayer_age, self.residential_status)
|
slab_master = slab_master or self._get_applicable_slab(
|
||||||
|
'new', self.period_id, self.taxpayer_age, self.residential_status
|
||||||
|
)
|
||||||
|
|
||||||
# Compute slab tax
|
# Compute slab tax
|
||||||
slab_tax = self._compute_tax_using_slab(taxable, slab_master)
|
slab_tax = self._compute_tax_using_slab(taxable, slab_master)
|
||||||
|
|
@ -393,7 +582,9 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
rules = slab_master.rules.sorted('min_income')
|
rules = slab_master.rules.sorted('min_income')
|
||||||
cess_rate = [rule.cess_rate for rule in rules if rule.min_income<=taxable and rule.max_income >= taxable]
|
cess_rate = [rule.cess_rate for rule in rules if rule.min_income<=taxable and rule.max_income >= taxable]
|
||||||
# Apply cess
|
# Apply cess
|
||||||
cess = tax_with_surcharge * cess_rate[0]/100
|
cess = 0
|
||||||
|
if cess_rate and tax_with_surcharge:
|
||||||
|
cess = tax_with_surcharge * cess_rate[0] / 100
|
||||||
total_tax = tax_with_surcharge + cess
|
total_tax = tax_with_surcharge + cess
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -421,59 +612,180 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
hp_income = -interest_allowed
|
hp_income = -interest_allowed
|
||||||
return hp_income
|
return hp_income
|
||||||
|
|
||||||
def _prepare_income_tax_data(self):
|
def _get_tax_base_values(self, include_comparison=False):
|
||||||
|
self.ensure_one()
|
||||||
|
selected_slab = self._get_applicable_slab(
|
||||||
|
self.tax_regime, self.period_id, self.taxpayer_age, self.residential_status
|
||||||
|
)
|
||||||
|
old_slab = selected_slab if self.tax_regime == 'old' else False
|
||||||
|
new_slab = selected_slab if self.tax_regime == 'new' else False
|
||||||
|
if include_comparison:
|
||||||
|
old_slab = old_slab or self._find_applicable_slab(
|
||||||
|
'old', self.period_id, self.taxpayer_age, self.residential_status
|
||||||
|
)
|
||||||
|
new_slab = new_slab or self._find_applicable_slab(
|
||||||
|
'new', self.period_id, self.taxpayer_age, self.residential_status
|
||||||
|
)
|
||||||
|
old_standard_deduction = self._get_standard_deduction('old', old_slab)
|
||||||
|
new_standard_deduction = self._get_standard_deduction('new', new_slab)
|
||||||
|
selected_standard_deduction = (
|
||||||
|
old_standard_deduction if self.tax_regime == 'old' else new_standard_deduction
|
||||||
|
)
|
||||||
|
|
||||||
|
salary_components_data = self.fetch_salary_components()
|
||||||
|
other_components_actual = sum(
|
||||||
|
component['actual'] for component in salary_components_data['other_components'].values()
|
||||||
|
)
|
||||||
|
other_components_projected = sum(
|
||||||
|
component['projected'] for component in salary_components_data['other_components'].values()
|
||||||
|
)
|
||||||
|
visible_gross_actual = (
|
||||||
|
sum(salary_components_data['basic_salary']['actual']) +
|
||||||
|
sum(salary_components_data['hra_salary']['actual']) +
|
||||||
|
sum(salary_components_data['lta_salary']['actual']) +
|
||||||
|
sum(salary_components_data['special_allowance']['actual']) +
|
||||||
|
other_components_actual
|
||||||
|
)
|
||||||
|
visible_gross_projected = (
|
||||||
|
sum(salary_components_data['basic_salary']['projected']) +
|
||||||
|
sum(salary_components_data['hra_salary']['projected']) +
|
||||||
|
sum(salary_components_data['lta_salary']['projected']) +
|
||||||
|
sum(salary_components_data['special_allowance']['projected']) +
|
||||||
|
other_components_projected
|
||||||
|
)
|
||||||
|
gross_salary_actual = max(sum(salary_components_data['gross_salary']['actual']), visible_gross_actual)
|
||||||
|
gross_salary_projected = max(
|
||||||
|
sum(salary_components_data['gross_salary']['projected']),
|
||||||
|
visible_gross_projected
|
||||||
|
)
|
||||||
|
annual_gross_salary = (
|
||||||
|
gross_salary_actual +
|
||||||
|
gross_salary_projected
|
||||||
|
)
|
||||||
|
annual_net_salary = (
|
||||||
|
sum(salary_components_data['net_salary']['actual']) +
|
||||||
|
sum(salary_components_data['net_salary']['projected'])
|
||||||
|
)
|
||||||
|
if not annual_net_salary or self.is_general_tax_statement:
|
||||||
|
annual_net_salary = annual_gross_salary
|
||||||
|
|
||||||
|
hp_income = self._compute_house_property_income()
|
||||||
|
old_deductions = (
|
||||||
|
old_standard_deduction
|
||||||
|
# self.hra_exemption +
|
||||||
|
# self.professional_tax +
|
||||||
|
# self.ded_80C +
|
||||||
|
# self.ded_80CCD1B +
|
||||||
|
# self.ded_80D_self +
|
||||||
|
# self.ded_80D_parents +
|
||||||
|
# self.ded_80G +
|
||||||
|
# self.ded_other +
|
||||||
|
# self.nps_employer_contribution
|
||||||
|
)
|
||||||
|
new_deductions = (
|
||||||
|
new_standard_deduction
|
||||||
|
# self.professional_tax +
|
||||||
|
# self.nps_employer_contribution
|
||||||
|
)
|
||||||
|
taxable_old = max(0.0, annual_gross_salary + self.other_income + hp_income - old_deductions - (-(self.professional_tax)))
|
||||||
|
taxable_new = max(0.0, annual_gross_salary + self.other_income + hp_income - new_deductions - (-(self.professional_tax)))
|
||||||
|
tax_result_old = self._compute_tax_old_regime(taxable_old, old_slab) if old_slab else False
|
||||||
|
tax_result_new = self._compute_tax_new_regime(taxable_new, new_slab) if new_slab else False
|
||||||
|
comparison_available = bool(tax_result_old and tax_result_new)
|
||||||
|
tax_savings = 0.0
|
||||||
|
beneficial_regime = False
|
||||||
|
if comparison_available:
|
||||||
|
tax_savings = abs(tax_result_old['total_tax'] - tax_result_new['total_tax'])
|
||||||
|
beneficial_regime = 'old' if tax_result_old['total_tax'] < tax_result_new['total_tax'] else 'new'
|
||||||
|
|
||||||
|
return {
|
||||||
|
'selected_slab': selected_slab,
|
||||||
|
'old_slab': old_slab,
|
||||||
|
'new_slab': new_slab,
|
||||||
|
'selected_standard_deduction': selected_standard_deduction,
|
||||||
|
'salary_components_data': salary_components_data,
|
||||||
|
'annual_gross_salary': annual_gross_salary,
|
||||||
|
'gross_salary_actual': gross_salary_actual,
|
||||||
|
'gross_salary_projected': gross_salary_projected,
|
||||||
|
'annual_net_salary': annual_net_salary,
|
||||||
|
'hp_income': hp_income,
|
||||||
|
'old_deductions': old_deductions,
|
||||||
|
'new_deductions': new_deductions,
|
||||||
|
'taxable_old': taxable_old,
|
||||||
|
'taxable_new': taxable_new,
|
||||||
|
'tax_result_old': tax_result_old,
|
||||||
|
'tax_result_new': tax_result_new,
|
||||||
|
'comparison_available': comparison_available,
|
||||||
|
'tax_savings': tax_savings,
|
||||||
|
'beneficial_regime': beneficial_regime,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _reset_regime_comparison(self):
|
||||||
|
self.write({
|
||||||
|
'comparison_available': False,
|
||||||
|
'old_regime_taxable_income': 0.0,
|
||||||
|
'new_regime_taxable_income': 0.0,
|
||||||
|
'old_regime_tax_payable': 0.0,
|
||||||
|
'new_regime_tax_payable': 0.0,
|
||||||
|
'tax_difference': 0.0,
|
||||||
|
'beneficial_regime': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
def action_check_regime_comparison(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.employee_id or not self.contract_id or not self.period_id or not self.period_line:
|
||||||
|
raise ValidationError(_("Select employee, period, and period line before checking comparison."))
|
||||||
|
|
||||||
|
values = self._get_tax_base_values(include_comparison=True)
|
||||||
|
if not values['comparison_available']:
|
||||||
|
self._reset_regime_comparison()
|
||||||
|
raise ValidationError(_("Tax comparison is available only when both old and new regime slabs are configured."))
|
||||||
|
|
||||||
|
self.write({
|
||||||
|
'comparison_available': True,
|
||||||
|
'old_regime_taxable_income': values['taxable_old'],
|
||||||
|
'new_regime_taxable_income': values['taxable_new'],
|
||||||
|
'old_regime_tax_payable': values['tax_result_old']['total_tax'],
|
||||||
|
'new_regime_tax_payable': values['tax_result_new']['total_tax'],
|
||||||
|
'tax_difference': values['tax_savings'],
|
||||||
|
'beneficial_regime': values['beneficial_regime'],
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': self._name,
|
||||||
|
'res_id': self.id,
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'current',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _prepare_income_tax_data(self, include_comparison=False):
|
||||||
"""Prepare data for the tax statement report"""
|
"""Prepare data for the tax statement report"""
|
||||||
today = date.today()
|
today = date.today()
|
||||||
fy_start = self.period_id.from_date
|
display_fy_start = self.period_id.from_date
|
||||||
fy_end = self.period_id.to_date
|
fy_end = self.period_id.to_date
|
||||||
total_months = ((fy_end.year - fy_start.year) * 12 +
|
effective_fy_start = self._get_effective_period_start() or display_fy_start
|
||||||
(fy_end.month - fy_start.month) + 1)
|
total_months = ((fy_end.year - effective_fy_start.year) * 12 +
|
||||||
|
(fy_end.month - effective_fy_start.month) + 1)
|
||||||
|
|
||||||
line_start = self.period_line.from_date
|
line_start = self.period_line.from_date
|
||||||
current_month_index = ((line_start.year - fy_start.year) * 12 +
|
current_month_index = ((line_start.year - effective_fy_start.year) * 12 +
|
||||||
(line_start.month - fy_start.month) + 1)
|
(line_start.month - effective_fy_start.month) + 1)
|
||||||
if today.month >= 4:
|
values = self._get_tax_base_values(include_comparison=include_comparison)
|
||||||
fy_start = date(today.year, 4, 1)
|
salary_components_data = values['salary_components_data']
|
||||||
fy_end = date(today.year + 1, 3, 31)
|
annual_gross_salary = values['annual_gross_salary']
|
||||||
else:
|
gross_salary_actual = values['gross_salary_actual']
|
||||||
fy_start = date(today.year - 1, 4, 1)
|
gross_salary_projected = values['gross_salary_projected']
|
||||||
fy_end = date(today.year, 3, 31)
|
annual_net_salary = values['annual_net_salary']
|
||||||
|
selected_standard_deduction = values['selected_standard_deduction']
|
||||||
# Calculate taxable income for both regimes
|
total_sec_16_deduction = values['selected_standard_deduction']+(-(self.professional_tax))
|
||||||
# Old regime
|
old_deductions = values['old_deductions']
|
||||||
old_deductions = (
|
new_deductions = values['new_deductions']
|
||||||
self.standard_deduction +
|
hp_income = values['hp_income']
|
||||||
self.hra_exemption +
|
taxable_old = values['taxable_old']
|
||||||
self.professional_tax +
|
taxable_new = values['taxable_new']
|
||||||
self.ded_80C +
|
tax_result_old = values['tax_result_old']
|
||||||
self.ded_80CCD1B +
|
tax_result_new = values['tax_result_new']
|
||||||
self.ded_80D_self +
|
|
||||||
self.ded_80D_parents +
|
|
||||||
self.ded_80G +
|
|
||||||
self.ded_other +
|
|
||||||
self.nps_employer_contribution
|
|
||||||
)
|
|
||||||
|
|
||||||
# House property income
|
|
||||||
hp_income = self._compute_house_property_income()
|
|
||||||
|
|
||||||
# Taxable income for old regime
|
|
||||||
taxable_old = max(0.0, (self.gross_salary * total_months) + self.other_income + hp_income - self.standard_deduction)
|
|
||||||
|
|
||||||
# New regime - fewer deductions
|
|
||||||
new_deductions = (
|
|
||||||
self.standard_deduction +
|
|
||||||
self.professional_tax +
|
|
||||||
self.nps_employer_contribution
|
|
||||||
)
|
|
||||||
|
|
||||||
# Taxable income for new regime
|
|
||||||
|
|
||||||
taxable_new = max(0.0, (self.gross_salary * total_months) + self.other_income + hp_income - self.standard_deduction)
|
|
||||||
|
|
||||||
# Compute tax for both regimes
|
|
||||||
tax_result_old = self._compute_tax_old_regime(taxable_old)
|
|
||||||
tax_result_new = self._compute_tax_new_regime(taxable_new)
|
|
||||||
|
|
||||||
# Determine which regime to use
|
# Determine which regime to use
|
||||||
if self.tax_regime == 'old':
|
if self.tax_regime == 'old':
|
||||||
|
|
@ -485,21 +797,25 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
taxable_income = taxable_new
|
taxable_income = taxable_new
|
||||||
chosen = 'new'
|
chosen = 'new'
|
||||||
|
|
||||||
# Calculate tax savings
|
if not tax_result:
|
||||||
tax_savings = abs(tax_result_old['total_tax'] - tax_result_new['total_tax'])
|
raise ValidationError(_("No tax calculation could be made for the selected regime."))
|
||||||
beneficial_regime = 'old' if tax_result_old['total_tax'] < tax_result_new['total_tax'] else 'new'
|
|
||||||
|
comparison_available = values['comparison_available']
|
||||||
|
tax_savings = values['tax_savings']
|
||||||
|
beneficial_regime = values['beneficial_regime']
|
||||||
|
|
||||||
# Prepare data structure matching screenshot format
|
# Prepare data structure matching screenshot format
|
||||||
# Financial year (period_id)
|
# Financial year (period_id)
|
||||||
fy_start = self.period_id.from_date
|
display_fy_start = self.period_id.from_date
|
||||||
fy_end = self.period_id.to_date
|
fy_end = self.period_id.to_date
|
||||||
total_months = ((fy_end.year - fy_start.year) * 12 +
|
effective_fy_start = self._get_effective_period_start() or display_fy_start
|
||||||
(fy_end.month - fy_start.month) + 1)
|
total_months = ((fy_end.year - effective_fy_start.year) * 12 +
|
||||||
|
(fy_end.month - effective_fy_start.month) + 1)
|
||||||
|
|
||||||
# Current month (period_line)
|
# Current month (period_line)
|
||||||
line_start = self.period_line.from_date
|
line_start = self.period_line.from_date
|
||||||
current_month_index = ((line_start.year - fy_start.year) * 12 +
|
current_month_index = ((line_start.year - effective_fy_start.year) * 12 +
|
||||||
(line_start.month - fy_start.month) + 1)
|
(line_start.month - effective_fy_start.month) + 1)
|
||||||
tax_result['roundoff_taxable_income'] = float(round(tax_result["taxable_income"] / 10) * 10)
|
tax_result['roundoff_taxable_income'] = float(round(tax_result["taxable_income"] / 10) * 10)
|
||||||
birthday = self.employee_id.birthday
|
birthday = self.employee_id.birthday
|
||||||
if birthday:
|
if birthday:
|
||||||
|
|
@ -508,9 +824,18 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
else:
|
else:
|
||||||
years_months = "N/A"
|
years_months = "N/A"
|
||||||
month_age = str(self.period_line.name)+ " / " + str(years_months)
|
month_age = str(self.period_line.name)+ " / " + str(years_months)
|
||||||
salary_components_data = self.fetch_salary_components()
|
other_salary_components = []
|
||||||
|
for component in salary_components_data['other_components'].values():
|
||||||
|
total = component['actual'] + component['projected']
|
||||||
|
if total:
|
||||||
|
other_salary_components.append({
|
||||||
|
'name': component['name'],
|
||||||
|
'actual': component['actual'],
|
||||||
|
'projected': component['projected'],
|
||||||
|
'total': total,
|
||||||
|
})
|
||||||
data = {
|
data = {
|
||||||
'financial_year': f"{fy_start.year}-{fy_end.year}",
|
'financial_year': f"{display_fy_start.year}-{fy_end.year}",
|
||||||
'assessment_year': fy_end.year + 1,
|
'assessment_year': fy_end.year + 1,
|
||||||
'report_time': today.strftime('%d-%m-%Y %H:%M'),
|
'report_time': today.strftime('%d-%m-%Y %H:%M'),
|
||||||
'user': 'ESS',
|
'user': 'ESS',
|
||||||
|
|
@ -539,14 +864,18 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
'special_allowance':{'actual': sum(salary_components_data['special_allowance']['actual']), 'projected': sum(salary_components_data['special_allowance']['projected']), 'total':sum(salary_components_data['special_allowance']['actual']) + sum(salary_components_data['special_allowance']['projected'])},
|
'special_allowance':{'actual': sum(salary_components_data['special_allowance']['actual']), 'projected': sum(salary_components_data['special_allowance']['projected']), 'total':sum(salary_components_data['special_allowance']['actual']) + sum(salary_components_data['special_allowance']['projected'])},
|
||||||
'perquisites': {'actual': 0 * current_month_index, 'projected': 0 * (total_months - current_month_index), 'total': 0 * total_months},
|
'perquisites': {'actual': 0 * current_month_index, 'projected': 0 * (total_months - current_month_index), 'total': 0 * total_months},
|
||||||
'reimbursement': {'actual': 0 * current_month_index, 'projected': 0 * (total_months - current_month_index), 'total': 0 * total_months},
|
'reimbursement': {'actual': 0 * current_month_index, 'projected': 0 * (total_months - current_month_index), 'total': 0 * total_months},
|
||||||
'gross_salary': {'actual': sum(salary_components_data['gross_salary']['actual']), 'projected': sum(salary_components_data['gross_salary']['projected']), 'total':sum(salary_components_data['gross_salary']['actual']) + sum(salary_components_data['gross_salary']['projected'])},
|
'gross_salary': {'actual': gross_salary_actual, 'projected': gross_salary_projected, 'total': annual_gross_salary},
|
||||||
'net_salary': {'actual': self.gross_salary * current_month_index, 'projected': self.gross_salary * (total_months - current_month_index),
|
'salary_advance': {'actual': sum(salary_components_data['salary_advance']['actual']), 'projected': sum(salary_components_data['salary_advance']['projected']), 'total': sum(salary_components_data['salary_advance']['actual']) + sum(salary_components_data['salary_advance']['projected'])},
|
||||||
'total': self.gross_salary * total_months}
|
'advance_recovery': {'actual': sum(salary_components_data['advance_recovery']['actual']), 'projected': sum(salary_components_data['advance_recovery']['projected']), 'total': sum(salary_components_data['advance_recovery']['actual']) + sum(salary_components_data['advance_recovery']['projected'])},
|
||||||
|
'other_components': other_salary_components,
|
||||||
|
'net_salary': {'actual': sum(salary_components_data['net_salary']['actual']), 'projected': sum(salary_components_data['net_salary']['projected']),
|
||||||
|
'total': annual_net_salary}
|
||||||
},
|
},
|
||||||
|
|
||||||
'deductions': {
|
'deductions': {
|
||||||
'professional_tax': self.professional_tax,
|
'professional_tax': self.professional_tax,
|
||||||
'standard_deduction': self.standard_deduction,
|
'standard_deduction': selected_standard_deduction,
|
||||||
|
'total_sec_16_deduction': total_sec_16_deduction,
|
||||||
'nps_employer': self.nps_employer_contribution,
|
'nps_employer': self.nps_employer_contribution,
|
||||||
'hra_exemption': self.hra_exemption,
|
'hra_exemption': self.hra_exemption,
|
||||||
'interest_home_loan': self.interest_home_loan_self + self.interest_home_loan_letout,
|
'interest_home_loan': self.interest_home_loan_self + self.interest_home_loan_letout,
|
||||||
|
|
@ -560,10 +889,10 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
},
|
},
|
||||||
|
|
||||||
'income_details': {
|
'income_details': {
|
||||||
'gross_salary': self.gross_salary,
|
'gross_salary': annual_gross_salary,
|
||||||
'other_income': self.other_income,
|
'other_income': self.other_income,
|
||||||
'house_property_income': hp_income,
|
'house_property_income': hp_income,
|
||||||
'gross_total_income': (self.gross_salary * total_months) + self.other_income + hp_income - self.standard_deduction,
|
'gross_total_income': (annual_gross_salary + self.other_income + hp_income) - total_sec_16_deduction,
|
||||||
},
|
},
|
||||||
|
|
||||||
'taxable_income': {
|
'taxable_income': {
|
||||||
|
|
@ -576,8 +905,11 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
'regime_used': chosen,
|
'regime_used': chosen,
|
||||||
|
|
||||||
'comparison': {
|
'comparison': {
|
||||||
'old_regime_tax': tax_result_old['total_tax'],
|
'available': comparison_available,
|
||||||
'new_regime_tax': tax_result_new['total_tax'],
|
'old_regime_tax': tax_result_old['total_tax'] if tax_result_old else 0.0,
|
||||||
|
'new_regime_tax': tax_result_new['total_tax'] if tax_result_new else 0.0,
|
||||||
|
'old_taxable_income': taxable_old if tax_result_old else 0.0,
|
||||||
|
'new_taxable_income': taxable_new if tax_result_new else 0.0,
|
||||||
'tax_savings': tax_savings,
|
'tax_savings': tax_savings,
|
||||||
'beneficial_regime': beneficial_regime,
|
'beneficial_regime': beneficial_regime,
|
||||||
}
|
}
|
||||||
|
|
@ -586,9 +918,19 @@ class ITTaxStatementWizard(models.TransientModel):
|
||||||
return {'data': data}
|
return {'data': data}
|
||||||
|
|
||||||
def action_generate_report(self):
|
def action_generate_report(self):
|
||||||
report_data = self._prepare_income_tax_data()
|
report_data = self._prepare_income_tax_data(include_comparison=False)
|
||||||
|
|
||||||
return self.env.ref('employee_it_declaration.income_tax_statement_action_report').report_action(
|
return self.env.ref('employee_it_declaration.income_tax_statement_action_report').report_action(
|
||||||
self,
|
self,
|
||||||
data={'report_data': report_data},
|
data={'report_data': report_data},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def action_generate_comparison_report(self):
|
||||||
|
report_data = self._prepare_income_tax_data(include_comparison=True)
|
||||||
|
if not report_data['data']['comparison']['available']:
|
||||||
|
raise ValidationError(_("Tax comparison is available only when both old and new regime slabs are configured."))
|
||||||
|
|
||||||
|
return self.env.ref('employee_it_declaration.income_tax_comparison_action_report').report_action(
|
||||||
|
self,
|
||||||
|
data={'report_data': report_data},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
from odoo import models, fields, api, _
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
from datetime import date, datetime, time
|
|
||||||
|
|
||||||
class ITTaxStatementWizard(models.TransientModel):
|
|
||||||
_name = 'it.tax.statement.wizard'
|
|
||||||
_description = 'Generate IT Tax Statement'
|
|
||||||
|
|
||||||
def _period_line_id_domain(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
employee_id = fields.Many2one('hr.employee', required=True, default=lambda self: self.env.user.employee_id.id)
|
|
||||||
emp_doj = fields.Date(related='employee_id.doj', store=True)
|
|
||||||
contract_id = fields.Many2one('hr.contract',related='employee_id.contract_id')
|
|
||||||
|
|
||||||
period_id = fields.Many2one('payroll.period', required=True)
|
|
||||||
period_line = fields.Many2one('payroll.period.line')
|
|
||||||
tax_regime = fields.Selection([
|
|
||||||
('new', 'New Regime'),
|
|
||||||
('old', 'Old Regime')
|
|
||||||
], string="Tax Regime", required=True, default='new')
|
|
||||||
|
|
||||||
def action_generate_report(self):
|
|
||||||
# declaration = self.env['emp.it.declaration'].search([
|
|
||||||
# ('employee_id', '=', self.employee_id.id),
|
|
||||||
# ('period_id', '=', self.period_id.id)
|
|
||||||
# ], limit=1)
|
|
||||||
#
|
|
||||||
# if not declaration:
|
|
||||||
# raise ValidationError("No IT Declaration found for the selected employee and period.")
|
|
||||||
|
|
||||||
# Prepare data for the report
|
|
||||||
report_data = self._prepare_income_tax_data()
|
|
||||||
|
|
||||||
# Return the report action
|
|
||||||
return {
|
|
||||||
'type': 'ir.actions.report',
|
|
||||||
'report_name': 'employee_it_declaration.income_tax_statement_report',
|
|
||||||
'report_type': 'qweb-pdf',
|
|
||||||
'data': {'model': 'it.tax.statement.wizard', 'ids': self.ids, 'report_data': report_data},
|
|
||||||
'context': {'active_model': 'it.tax.statement.wizard', 'active_id': self.id},
|
|
||||||
}
|
|
||||||
# return self.env.ref('your_module_name.action_report_it_tax_statement').report_action(declaration)
|
|
||||||
|
|
||||||
def _prepare_income_tax_data(self):
|
|
||||||
# Get fiscal year (April to March)
|
|
||||||
today = date.today()
|
|
||||||
if today.month >= 4:
|
|
||||||
fiscal_year_start = date(today.year, 4, 1)
|
|
||||||
fiscal_year_end = date(today.year + 1, 3, 31)
|
|
||||||
else:
|
|
||||||
fiscal_year_start = date(today.year - 1, 4, 1)
|
|
||||||
fiscal_year_end = date(today.year, 3, 31)
|
|
||||||
|
|
||||||
fiscal_year_str = f"{fiscal_year_start.year}-{fiscal_year_end.year}"
|
|
||||||
|
|
||||||
# Get company information
|
|
||||||
company = self.env.company
|
|
||||||
company_address = ', '.join(filter(None, [
|
|
||||||
company.street,
|
|
||||||
company.street2,
|
|
||||||
company.city,
|
|
||||||
company.state_id.name,
|
|
||||||
company.country_id.name,
|
|
||||||
company.zip
|
|
||||||
]))
|
|
||||||
|
|
||||||
# Get PAN and TAN (assuming they're stored in company)
|
|
||||||
pan_number = company.pan_number if hasattr(company, 'pan_number') else 'PAN1234567'
|
|
||||||
tan_number = company.tan_number if hasattr(company, 'tan_number') else 'TAN1234567'
|
|
||||||
|
|
||||||
# Prepare report data
|
|
||||||
data = {
|
|
||||||
'company_name': company.name,
|
|
||||||
'company_address': company_address,
|
|
||||||
'pan_number': pan_number,
|
|
||||||
'tan_number': tan_number,
|
|
||||||
'assessment_year': fiscal_year_end.year + 1,
|
|
||||||
'financial_year': fiscal_year_str,
|
|
||||||
'date': today.strftime('%d/%m/%Y'),
|
|
||||||
|
|
||||||
# Income details (simplified - you would query actual data)
|
|
||||||
'salary_income': 1200000.00,
|
|
||||||
'house_property_income': 300000.00,
|
|
||||||
'business_income': 500000.00,
|
|
||||||
'capital_gains': 200000.00,
|
|
||||||
'other_income': 100000.00,
|
|
||||||
'total_income': 2300000.00,
|
|
||||||
|
|
||||||
# Deductions
|
|
||||||
'section_80c': 150000.00,
|
|
||||||
'section_80d': 50000.00,
|
|
||||||
'section_80g': 10000.00,
|
|
||||||
'other_deductions': 30000.00,
|
|
||||||
'total_deductions': 240000.00,
|
|
||||||
|
|
||||||
# Tax computation
|
|
||||||
'taxable_income': 2060000.00,
|
|
||||||
'tax_payable': 450000.00,
|
|
||||||
'tax_paid': 400000.00,
|
|
||||||
'balance_tax': 50000.00,
|
|
||||||
|
|
||||||
# Additional details
|
|
||||||
'bank_name': 'State Bank of India',
|
|
||||||
'account_number': '1234567890',
|
|
||||||
'ifsc_code': 'SBIN0001234',
|
|
||||||
|
|
||||||
# For employee reports (if needed)
|
|
||||||
'employee_name': 'John Doe',
|
|
||||||
'employee_pan': 'ABCDE1234F',
|
|
||||||
'employee_address': '123, Main Street, Bangalore, Karnataka - 560001',
|
|
||||||
}
|
|
||||||
|
|
||||||
return {'data': data}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -17,6 +16,16 @@ class PayrollPeriod(models.Model):
|
||||||
name = fields.Char(string="Name", required=True)
|
name = fields.Char(string="Name", required=True)
|
||||||
period_line_ids = fields.One2many('payroll.period.line', 'period_id', string="Monthly Periods")
|
period_line_ids = fields.One2many('payroll.period.line', 'period_id', string="Monthly Periods")
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
periods = super().create(vals_list)
|
||||||
|
active_investment_types = self.env['it.investment.type'].search([('active', '=', True)])
|
||||||
|
if active_investment_types:
|
||||||
|
active_investment_types.write({
|
||||||
|
'period_ids': [(4, period.id) for period in periods],
|
||||||
|
})
|
||||||
|
return periods
|
||||||
|
|
||||||
@api.onchange('from_date', 'to_date')
|
@api.onchange('from_date', 'to_date')
|
||||||
def onchange_from_to_date(self):
|
def onchange_from_to_date(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,22 @@ class IncomeTaxSlabMaster(models.Model):
|
||||||
_sql_constraints = [
|
_sql_constraints = [
|
||||||
(
|
(
|
||||||
'unique_slab',
|
'unique_slab',
|
||||||
'unique(regime, age_category, residence_type)',
|
'unique(period_id, regime, age_category, residence_type)',
|
||||||
'Slab must be unique for the same Regime, Age Category, and Residence Type!'
|
'Slab must be unique for the same name, Regime, Age Category, and Residence Type!'
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
name = fields.Char(string="Slab Name", required=True)
|
name = fields.Char(string="Slab Name", required=True, copy=False, default="AY")
|
||||||
|
period_id = fields.Many2one('payroll.period', copy=False)
|
||||||
regime = fields.Selection([
|
regime = fields.Selection([
|
||||||
('old', 'Old Tax Regime'),
|
('old', 'Old Tax Regime'),
|
||||||
('new', 'New Tax Regime')
|
('new', 'New Tax Regime')
|
||||||
], required=True)
|
], required=True, default='old')
|
||||||
age_category = fields.Selection([
|
age_category = fields.Selection([
|
||||||
('below_60', 'Below 60 Years'),
|
('below_60', 'Below 60 Years'),
|
||||||
('60_to_80', '60-80 Years'),
|
('60_to_80', '60-80 Years'),
|
||||||
('above_80', 'Above 80 Years')
|
('above_80', 'Above 80 Years')
|
||||||
], required=True)
|
], required=True, default='below_60')
|
||||||
residence_type = fields.Selection([
|
residence_type = fields.Selection([
|
||||||
('resident', 'Resident'),
|
('resident', 'Resident'),
|
||||||
('non_resident', 'Non Resident'),
|
('non_resident', 'Non Resident'),
|
||||||
|
|
@ -31,7 +32,39 @@ class IncomeTaxSlabMaster(models.Model):
|
||||||
standard_deduction = fields.Float(string="Standard Deduction")
|
standard_deduction = fields.Float(string="Standard Deduction")
|
||||||
active = fields.Boolean(default=True)
|
active = fields.Boolean(default=True)
|
||||||
rules = fields.One2many('it.slab.master.rules','slab_id', string="Slab Rules")
|
rules = fields.One2many('it.slab.master.rules','slab_id', string="Slab Rules")
|
||||||
|
surcharges = fields.One2many('it.sur.charge.rules','slab_id', string="Surcharges Rules")
|
||||||
|
|
||||||
|
def copy(self, default=None):
|
||||||
|
"""Override copy to duplicate slab rules and surcharge rules"""
|
||||||
|
if default is None:
|
||||||
|
default = {}
|
||||||
|
|
||||||
|
# Check if we should duplicate from context
|
||||||
|
duplicate_rules = self.env.context.get('duplicate_slab_rules', True)
|
||||||
|
|
||||||
|
default.update({
|
||||||
|
'name': _("%s (copy)") % (self.name or 'Slab'),
|
||||||
|
'rules': [],
|
||||||
|
'surcharges': [],
|
||||||
|
})
|
||||||
|
|
||||||
|
new_slab = super(IncomeTaxSlabMaster, self).copy(default)
|
||||||
|
|
||||||
|
# Only duplicate if flag is True
|
||||||
|
if duplicate_rules:
|
||||||
|
if self.rules:
|
||||||
|
for rule in self.rules:
|
||||||
|
rule.copy({
|
||||||
|
'slab_id': new_slab.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.surcharges:
|
||||||
|
for surcharge in self.surcharges:
|
||||||
|
surcharge.copy({
|
||||||
|
'slab_id': new_slab.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return new_slab
|
||||||
class IncomeTaxSlabMasterRules(models.Model):
|
class IncomeTaxSlabMasterRules(models.Model):
|
||||||
_name = 'it.slab.master.rules'
|
_name = 'it.slab.master.rules'
|
||||||
_description = 'Income Tax slab rules'
|
_description = 'Income Tax slab rules'
|
||||||
|
|
@ -44,6 +77,10 @@ class IncomeTaxSlabMasterRules(models.Model):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
sequence = fields.Integer(
|
||||||
|
'Sequence',
|
||||||
|
help='Used to deduct the taxes based on order')
|
||||||
min_income = fields.Float(string="Min Income (₹)", required=True)
|
min_income = fields.Float(string="Min Income (₹)", required=True)
|
||||||
max_income = fields.Float(string="Max Income (₹)")
|
max_income = fields.Float(string="Max Income (₹)")
|
||||||
tax_rate = fields.Float(string="Tax Rate (%)", required=True)
|
tax_rate = fields.Float(string="Tax Rate (%)", required=True)
|
||||||
|
|
@ -55,6 +92,31 @@ class IncomeTaxSlabMasterRules(models.Model):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@api.constrains('min_income', 'max_income', 'slab_id')
|
||||||
|
def _check_overlap(self):
|
||||||
|
"""Ensure no overlapping or duplicate ranges within the same slab"""
|
||||||
|
for rule in self:
|
||||||
|
domain = [
|
||||||
|
('slab_id', '=', rule.slab_id.id),
|
||||||
|
('id', '!=', rule.id)
|
||||||
|
]
|
||||||
|
others = self.search(domain)
|
||||||
|
for other in others:
|
||||||
|
if not (rule.max_income and other.min_income >= rule.max_income) and \
|
||||||
|
not (other.max_income and rule.min_income >= other.max_income):
|
||||||
|
raise ValidationError(
|
||||||
|
f"Income ranges overlap with another slab rule: {other.min_income} - {other.max_income}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IncomeTaxSurchargeMasterRules(models.Model):
|
||||||
|
_name = 'it.sur.charge.rules'
|
||||||
|
|
||||||
|
min_income = fields.Float(string="Min Income (₹)", required=True)
|
||||||
|
max_income = fields.Float(string="Max Income (₹)")
|
||||||
|
surcharge_rate = fields.Float(string="Surcharge Rate (%)")
|
||||||
|
slab_id = fields.Many2one('it.slab.master')
|
||||||
|
|
||||||
@api.constrains('min_income', 'max_income', 'slab_id')
|
@api.constrains('min_income', 'max_income', 'slab_id')
|
||||||
def _check_overlap(self):
|
def _check_overlap(self):
|
||||||
"""Ensure no overlapping or duplicate ranges within the same slab"""
|
"""Ensure no overlapping or duplicate ranges within the same slab"""
|
||||||
|
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<odoo>
|
|
||||||
<template id="income_tax_statement_report">
|
|
||||||
<t t-call="web.external_layout">
|
|
||||||
<main class="page">
|
|
||||||
<t t-set="data" t-value="report_data['data']"/>
|
|
||||||
<div>
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="row text-center" style="margin-bottom: 20px; page-break-inside: avoid;">
|
|
||||||
<h2>INCOME TAX STATEMENT</h2>
|
|
||||||
<h4>Assessment Year: <span t-esc="data.get('assessment_year')"/></h4>
|
|
||||||
<h4>Financial Year: <span t-esc="data.get('financial_year')"/></h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Company Information -->
|
|
||||||
<div class="row" style="margin-bottom: 20px; page-break-inside: avoid;">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<strong>Company Name:</strong> <span t-esc="data.get('company_name')"/><br/>
|
|
||||||
<strong>Address:</strong> <span t-esc="data.get('company_address')"/><br/>
|
|
||||||
<strong>PAN:</strong> <span t-esc="data.get('pan_number')"/><br/>
|
|
||||||
<strong>TAN:</strong> <span t-esc="data.get('tan_number')"/><br/>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6 text-right">
|
|
||||||
<strong>Date:</strong> <span t-esc="data.get('date')"/><br/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Income Details -->
|
|
||||||
<div class="row" style="margin-bottom: 20px;">
|
|
||||||
<h4>Income Details</h4>
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Particulars</th>
|
|
||||||
<th class="text-right">Amount (₹)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Income from Salary</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('salary_income', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Income from House Property</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('house_property_income', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Income from Business/Profession</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('business_income', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Capital Gains</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('capital_gains', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Income from Other Sources</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('other_income', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr style="font-weight: bold;">
|
|
||||||
<td>Total Income</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('total_income', 0))"/>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Deductions -->
|
|
||||||
<div class="row" style="margin-bottom: 20px; page-break-inside: avoid;">
|
|
||||||
<h4>Deductions Under Chapter VI-A</h4>
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Section</th>
|
|
||||||
<th>Particulars</th>
|
|
||||||
<th class="text-right">Amount (₹)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>80C</td>
|
|
||||||
<td>Life Insurance, PF, PPF, etc.</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('section_80c', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>80D</td>
|
|
||||||
<td>Medical Insurance Premium</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('section_80d', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>80G</td>
|
|
||||||
<td>Donations to Charitable Institutions</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('section_80g', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Others</td>
|
|
||||||
<td>Other Deductions</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('other_deductions', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr style="font-weight: bold;">
|
|
||||||
<td colspan="2">Total Deductions</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('total_deductions', 0))"/>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tax Computation -->
|
|
||||||
<div class="row" style="margin-bottom: 20px; page-break-inside: avoid;">
|
|
||||||
<h4>Tax Computation</h4>
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Total Income</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('total_income', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Less: Deductions</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('total_deductions', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr style="font-weight: bold;">
|
|
||||||
<td>Taxable Income</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('taxable_income', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Tax Payable</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('tax_payable', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Less: Tax Paid (Advance/Self-assessment)</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('tax_paid', 0))"/>
|
|
||||||
</tr>
|
|
||||||
<tr style="font-weight: bold;">
|
|
||||||
<td>Balance Tax Payable</td>
|
|
||||||
<td class="text-right" t-esc="'{:,.2f}'.format(data.get('balance_tax', 0))"/>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
</t>
|
|
||||||
</template>
|
|
||||||
</odoo>
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
<t t-set="tax_employment" t-value="'{:,.0f}'.format(deductions.get('professional_tax', 0))"/>
|
<t t-set="tax_employment" t-value="'{:,.0f}'.format(deductions.get('professional_tax', 0))"/>
|
||||||
<t t-set="standard_deduction" t-value="'{:,.0f}'.format(deductions.get('standard_deduction', 0))"/>
|
<t t-set="standard_deduction" t-value="'{:,.0f}'.format(deductions.get('standard_deduction', 0))"/>
|
||||||
<t t-set="other_income" t-value="'{:,.0f}'.format(income_details.get('other_income', 0))"/>
|
<t t-set="other_income" t-value="'{:,.0f}'.format(income_details.get('other_income', 0))"/>
|
||||||
|
<t t-set="total_sec_16_deduction" t-value="'{:,.0f}'.format(deductions.get('total_sec_16_deduction', 0))"/>
|
||||||
<t t-set="gross_total_income" t-value="'{:,.0f}'.format(income_details.get('gross_total_income', 0))"/>
|
<t t-set="gross_total_income" t-value="'{:,.0f}'.format(income_details.get('gross_total_income', 0))"/>
|
||||||
<t t-set="taxable_income" t-value="'{:,.0f}'.format(tax_computation.get('taxable_income', 0))"/>
|
<t t-set="taxable_income" t-value="'{:,.0f}'.format(tax_computation.get('taxable_income', 0))"/>
|
||||||
<t t-set="roundoff_taxable_income" t-value="'{:,.0f}'.format(tax_computation.get('roundoff_taxable_income', 0))"/>
|
<t t-set="roundoff_taxable_income" t-value="'{:,.0f}'.format(tax_computation.get('roundoff_taxable_income', 0))"/>
|
||||||
|
|
@ -149,6 +150,15 @@
|
||||||
<td style="text-align: right;"
|
<td style="text-align: right;"
|
||||||
t-esc="'{:,.0f}'.format(salary_components.get('reimbursement', {}).get('total', 0))"/>
|
t-esc="'{:,.0f}'.format(salary_components.get('reimbursement', {}).get('total', 0))"/>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr t-foreach="salary_components.get('other_components', [])" t-as="component">
|
||||||
|
<td t-esc="component.get('name')"/>
|
||||||
|
<td style="text-align: right;"
|
||||||
|
t-esc="'{:,.0f}'.format(component.get('actual', 0))"/>
|
||||||
|
<td style="text-align: right;"
|
||||||
|
t-esc="'{:,.0f}'.format(component.get('projected', 0))"/>
|
||||||
|
<td style="text-align: right;"
|
||||||
|
t-esc="'{:,.0f}'.format(component.get('total', 0))"/>
|
||||||
|
</tr>
|
||||||
<tr style="border-top: 2px solid #ddd; font-weight: bold;">
|
<tr style="border-top: 2px solid #ddd; font-weight: bold;">
|
||||||
<td>Gross Salary</td>
|
<td>Gross Salary</td>
|
||||||
<td style="text-align: right;"
|
<td style="text-align: right;"
|
||||||
|
|
@ -158,6 +168,15 @@
|
||||||
<td style="text-align: right;"
|
<td style="text-align: right;"
|
||||||
t-esc="'{:,.0f}'.format(salary_components.get('gross_salary', {}).get('total', 0))"/>
|
t-esc="'{:,.0f}'.format(salary_components.get('gross_salary', {}).get('total', 0))"/>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr t-if="salary_components.get('advance_recovery', {}).get('total', 0)">
|
||||||
|
<td>Advance Recovery</td>
|
||||||
|
<td style="text-align: right;"
|
||||||
|
t-esc="'{:,.0f}'.format(salary_components.get('advance_recovery', {}).get('actual', 0))"/>
|
||||||
|
<td style="text-align: right;"
|
||||||
|
t-esc="'{:,.0f}'.format(salary_components.get('advance_recovery', {}).get('projected', 0))"/>
|
||||||
|
<td style="text-align: right;"
|
||||||
|
t-esc="'{:,.0f}'.format(salary_components.get('advance_recovery', {}).get('total', 0))"/>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Less: Exemption under section 10</td>
|
<td>Less: Exemption under section 10</td>
|
||||||
<td colspan="3"></td>
|
<td colspan="3"></td>
|
||||||
|
|
@ -180,17 +199,22 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="border-top: 2px solid #ddd; margin-bottom: 8px; margin-top: 10px;">
|
<tr style="border-top: 2px solid #ddd; margin-bottom: 8px; margin-top: 10px;">
|
||||||
<td style="padding-left: 30px;">Less: Standard Deduction for Salaried Employees</td>
|
<td style="padding-left: 30px;">Less: Standard Deduction for Salaried Employees</td>
|
||||||
|
<td style="text-align: right;">
|
||||||
|
-<span t-esc="standard_deduction"/>
|
||||||
|
</td>
|
||||||
<td colspan="2"></td>
|
<td colspan="2"></td>
|
||||||
<td style="text-align: right;"
|
|
||||||
t-esc="standard_deduction"/>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="border-top: 2px solid #ddd; margin-bottom: 8px; margin-top: 10px;">
|
<tr style="border-top: 2px solid #ddd; margin-bottom: 8px; margin-top: 10px;">
|
||||||
<td style="padding-left: 30px;">Total</td>
|
<td style="padding-left: 30px;">Total</td>
|
||||||
|
<td colspan="2"></td>
|
||||||
<td style="text-align: right;"
|
<td style="text-align: right;"
|
||||||
t-esc="tax_employment"/>
|
t-esc="total_sec_16_deduction"/>
|
||||||
<td colspan="1"></td>
|
</tr>
|
||||||
<td style="text-align: right;"
|
<tr style="border-top: 2px solid #ddd; margin-bottom: 8px; margin-top: 10px;">
|
||||||
t-esc="standard_deduction"/>
|
<td><strong>Less:</strong> Marginal Relief</td>
|
||||||
|
<td colspan="3"></td>
|
||||||
|
<td style="text-align: right;">0</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr style="border-top: 2px solid #ddd; font-weight: bold; margin-bottom: 8px; margin-top: 10px;">
|
<tr style="border-top: 2px solid #ddd; font-weight: bold; margin-bottom: 8px; margin-top: 10px;">
|
||||||
|
|
@ -267,7 +291,7 @@
|
||||||
<td style="width: 70%;">
|
<td style="width: 70%;">
|
||||||
Round off to nearest 10 Rupee:
|
Round off to nearest 10 Rupee:
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 30%; text-align: right;">
|
<td style="width: 30%; text-align: right; border-bottom: 1px solid #000; padding-bottom: 2px;">
|
||||||
<t t-esc="roundoff_taxable_income"/>
|
<t t-esc="roundoff_taxable_income"/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -414,6 +438,69 @@
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="generate_income_tax_comparison_rpt">
|
||||||
|
<t t-call="web.html_container">
|
||||||
|
<div class="page">
|
||||||
|
<t t-set="data" t-value="report_data['data']"/>
|
||||||
|
<t t-set="profile" t-value="data.get('profile')"/>
|
||||||
|
<t t-set="comparison" t-value="data.get('comparison')"/>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-bottom: 24px;">
|
||||||
|
<h2 style="font-weight: bold; margin-bottom: 5px;"><t t-esc="data.get('company_name')"/></h2>
|
||||||
|
<h3 style="font-weight: bold; margin-top: 0;">TAX REGIME COMPARISON</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-sm" style="width: 100%; margin-bottom: 24px;">
|
||||||
|
<tr>
|
||||||
|
<td><strong>Employee:</strong> <t t-esc="profile.get('name', '')"/></td>
|
||||||
|
<td><strong>Emp Code:</strong> <t t-esc="data.get('emp_code', '')"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Financial Year:</strong> <t t-esc="data.get('financial_year', '')"/></td>
|
||||||
|
<td><strong>Selected Regime:</strong> <t t-esc="data.get('regime_info', {}).get('tax_regime', '')"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="table table-bordered table-sm" style="width: 100%; margin-bottom: 20px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f8f9fa;">
|
||||||
|
<th style="font-weight: bold; text-align: left;">Particulars</th>
|
||||||
|
<th style="font-weight: bold; text-align: right;">Old Regime</th>
|
||||||
|
<th style="font-weight: bold; text-align: right;">New Regime</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Taxable Income</td>
|
||||||
|
<td style="text-align: right;" t-esc="'{:,.0f}'.format(comparison.get('old_taxable_income', 0))"/>
|
||||||
|
<td style="text-align: right;" t-esc="'{:,.0f}'.format(comparison.get('new_taxable_income', 0))"/>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tax Payable</td>
|
||||||
|
<td style="text-align: right;" t-esc="'{:,.0f}'.format(comparison.get('old_regime_tax', 0))"/>
|
||||||
|
<td style="text-align: right;" t-esc="'{:,.0f}'.format(comparison.get('new_regime_tax', 0))"/>
|
||||||
|
</tr>
|
||||||
|
<tr style="font-weight: bold;">
|
||||||
|
<td>Tax Difference</td>
|
||||||
|
<td colspan="2" style="text-align: right;" t-esc="'{:,.0f}'.format(comparison.get('tax_savings', 0))"/>
|
||||||
|
</tr>
|
||||||
|
<tr style="font-weight: bold;">
|
||||||
|
<td>Beneficial Regime</td>
|
||||||
|
<td colspan="2" style="text-align: right;">
|
||||||
|
<t t-esc="(comparison.get('beneficial_regime') or '').upper()"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div style="margin-top: 24px;">
|
||||||
|
<strong>Report Time:</strong>
|
||||||
|
<t t-esc="data.get('report_time', '')"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
<!-- <template id="generate_income_tax_statement_rpt">-->
|
<!-- <template id="generate_income_tax_statement_rpt">-->
|
||||||
<!-- <t t-call="web.basic_layout">-->
|
<!-- <t t-call="web.basic_layout">-->
|
||||||
<!-- <main class="page">-->
|
<!-- <main class="page">-->
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<report
|
<record id="action_report_it_tax_statement" model="ir.actions.report">
|
||||||
id="action_report_it_tax_statement"
|
<field name="name">IT Declaration Submission</field>
|
||||||
model="emp.it.declaration"
|
<field name="model">emp.it.declaration</field>
|
||||||
string="IT Tax Statement"
|
<field name="report_type">qweb-pdf</field>
|
||||||
report_type="qweb-pdf"
|
<field name="report_name">employee_it_declaration.report_it_tax_statement</field>
|
||||||
name="your_module_name.report_it_tax_statement"
|
<field name="report_file">employee_it_declaration.report_it_tax_statement</field>
|
||||||
file="your_module_name.report_it_tax_statement"
|
<field name="print_report_name">'IT Declaration - %s - %s' % (object.employee_id.name or '', object.period_id.name or '')</field>
|
||||||
print_report_name="'IT_Tax_Statement_%s' % (object.employee_id.name)"
|
<field name="binding_model_id" eval="False"/>
|
||||||
/>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ access_house_rent_declaration_user,access.house.rent.declaration.user,model_hous
|
||||||
|
|
||||||
access_it_tax_statement,it.tax.statement,model_it_tax_statement,base.group_user,1,0,0,0
|
access_it_tax_statement,it.tax.statement,model_it_tax_statement,base.group_user,1,0,0,0
|
||||||
access_it_tax_statement_wizard,it.tax.statement.wizard,model_it_tax_statement_wizard,base.group_user,1,0,0,0
|
access_it_tax_statement_wizard,it.tax.statement.wizard,model_it_tax_statement_wizard,base.group_user,1,0,0,0
|
||||||
|
access_employee_payslip_download_wizard,employee.payslip.download.wizard,model_employee_payslip_download_wizard,base.group_user,1,1,1,0
|
||||||
|
|
||||||
access_it_tax_statement_manager,it.tax.statement,model_it_tax_statement,hr.group_hr_manager,1,1,1,1
|
access_it_tax_statement_manager,it.tax.statement,model_it_tax_statement,hr.group_hr_manager,1,1,1,1
|
||||||
access_it_tax_statement_wizard_manager,it.tax.statement.wizard,model_it_tax_statement_wizard,hr.group_hr_manager,1,1,1,1
|
access_it_tax_statement_wizard_manager,it.tax.statement.wizard,model_it_tax_statement_wizard,hr.group_hr_manager,1,1,1,1
|
||||||
|
|
@ -63,3 +64,4 @@ access_it_tax_statement_wizard_manager,it.tax.statement.wizard,model_it_tax_stat
|
||||||
|
|
||||||
access_it_slab_master,it.slab.master,model_it_slab_master,base.group_user,1,1,1,1
|
access_it_slab_master,it.slab.master,model_it_slab_master,base.group_user,1,1,1,1
|
||||||
access_it_slab_master_rules,it.slab.master.rules,model_it_slab_master_rules,base.group_user,1,1,1,1
|
access_it_slab_master_rules,it.slab.master.rules,model_it_slab_master_rules,base.group_user,1,1,1,1
|
||||||
|
access_it_sur_charge_rules,it.sur.charge.rules.user,model_it_sur_charge_rules,base.group_user,1,1,1,1
|
||||||
|
|
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list>
|
<list>
|
||||||
|
|
||||||
|
<field name="employee_id"/>
|
||||||
<field name="period_id"/>
|
<field name="period_id"/>
|
||||||
<field name="total_investment"/>
|
<field name="total_investment"/>
|
||||||
<field name="tax_regime"/>
|
<field name="tax_regime"/>
|
||||||
|
<field name="state"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
@ -20,28 +22,60 @@
|
||||||
<field name="model">emp.it.declaration</field>
|
<field name="model">emp.it.declaration</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="IT Declaration">
|
<form string="IT Declaration">
|
||||||
|
<header>
|
||||||
|
<button name="action_submit"
|
||||||
|
string="Submit"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="state != 'draft' or not costing_details_generated"/>
|
||||||
|
<button name="action_download_submission_pdf"
|
||||||
|
string="Download PDF"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
icon="fa-download"
|
||||||
|
invisible="state != 'submitted'"/>
|
||||||
|
<button name="action_return_to_draft"
|
||||||
|
string="Return to Draft"
|
||||||
|
type="object"
|
||||||
|
class="btn-secondary"
|
||||||
|
invisible="state != 'submitted'"
|
||||||
|
groups="hr_payroll.group_hr_payroll_manager"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,submitted"/>
|
||||||
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
<div class="oe_title mb24">
|
<div class="oe_title mb24">
|
||||||
<div class="o_row">
|
<div class="o_row">
|
||||||
<field name="employee_id" widget="res_partner_many2one" placeholder="Employee Name..." readonly="costing_details_generated"/>
|
<field name="employee_id" widget="res_partner_many2one" placeholder="Employee Name..." readonly="costing_details_generated or state == 'submitted'"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
<field name="period_id" readonly="costing_details_generated"/>
|
<field name="period_id" readonly="costing_details_generated or state == 'submitted'"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="total_investment"/>
|
<field name="total_investment" readonly="state == 'submitted'"/>
|
||||||
|
<field name="return_reason" placeholder="Reason for returning the declaration to draft..." readonly="state != 'submitted' or not is_payroll_manager"/>
|
||||||
|
<field name="is_payroll_manager" invisible="1"/>
|
||||||
<field name="costing_details_generated" invisible="1" force_save="1"/>
|
<field name="costing_details_generated" invisible="1" force_save="1"/>
|
||||||
<field name="house_rent_costing_id"/>
|
<field name="house_rent_costing_id"/>
|
||||||
|
<field name="show_past_employment" invisible="1"/>
|
||||||
|
<field name="show_us_80c" invisible="1"/>
|
||||||
|
<field name="show_us_80d" invisible="1"/>
|
||||||
|
<field name="show_us_10" invisible="1"/>
|
||||||
|
<field name="show_us_80g" invisible="1"/>
|
||||||
|
<field name="show_chapter_via" invisible="1"/>
|
||||||
|
<field name="show_us_17" invisible="1"/>
|
||||||
|
<field name="show_house_rent" invisible="1"/>
|
||||||
|
<field name="show_other_i_or_l" invisible="1"/>
|
||||||
|
<field name="show_other_declaration" invisible="1"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}"/>
|
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}" readonly="state == 'submitted'"/>
|
||||||
<br/>
|
<br/>
|
||||||
<button name="generate_declarations" type="object" class="btn-primary" string="Generate" confirm="Upon Confirming you won't be able to change the Period & Employee" help="Generate Data to upload the declaration Costing" invisible="costing_details_generated"/>
|
<button name="generate_declarations" type="object" class="btn-primary" string="Generate" confirm="Upon Confirming you won't be able to change the Period & Employee" help="Generate Data to upload the declaration Costing" invisible="costing_details_generated or state == 'submitted'"/>
|
||||||
<field name="is_section_open" invisible="1"/> <!-- Store toggle state -->
|
<field name="is_section_open" invisible="1"/> <!-- Store toggle state -->
|
||||||
|
|
||||||
<group invisible="not costing_details_generated">
|
<group invisible="not costing_details_generated">
|
||||||
|
|
@ -61,7 +95,7 @@
|
||||||
<page string="Total Investment Costing">
|
<page string="Total Investment Costing">
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
<field name="investment_costing_ids" nolabel="1">
|
<field name="visible_investment_costing_ids" nolabel="1" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0" edit="0">
|
<list editable="bottom" create="0" delete="0" edit="0">
|
||||||
<field name="investment_type_id"/>
|
<field name="investment_type_id"/>
|
||||||
<field name="amount"/>
|
<field name="amount"/>
|
||||||
|
|
@ -90,9 +124,9 @@
|
||||||
<!-- </list>-->
|
<!-- </list>-->
|
||||||
<!-- </field>-->
|
<!-- </field>-->
|
||||||
<!-- </page>-->
|
<!-- </page>-->
|
||||||
<page name="past_employment_costings" string="PAST EMPLOYMENT">
|
<page name="past_employment_costings" string="PAST EMPLOYMENT" invisible="not show_past_employment">
|
||||||
|
|
||||||
<field name="past_employment_costings" invisible="tax_regime != 'old'">
|
<field name="past_employment_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -102,7 +136,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
<field name="past_employment_costings_new" invisible="tax_regime != 'new'">
|
<field name="past_employment_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -114,9 +148,25 @@
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
|
|
||||||
<page name="us_80c_costings" string="US 80C" invisible="tax_regime != 'old'">
|
<page name="us_80c_costings" string="US 80C" invisible="not show_us_80c">
|
||||||
|
|
||||||
<field name="us80c_costings">
|
<field name="us80c_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||||
|
<list editable="bottom" create="0" delete="0">
|
||||||
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
<field name="action_id" column_invisible="1"/>
|
||||||
|
<button name="open_action_wizard"
|
||||||
|
string="Action"
|
||||||
|
type="object"
|
||||||
|
icon="fa-external-link"
|
||||||
|
invisible="not action_id" width="60px"/>
|
||||||
|
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
|
||||||
|
<field name="remarks" width="200px"/>
|
||||||
|
<field name="proof" width="100px"/>
|
||||||
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
<field name="us80c_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -134,12 +184,12 @@
|
||||||
</field>
|
</field>
|
||||||
|
|
||||||
</page>
|
</page>
|
||||||
<page name="us_80d_costings" string="US 80D" invisible="tax_regime != 'old'">
|
<page name="us_80d_costings" string="US 80D" invisible="not show_us_80d">
|
||||||
<group>
|
<group>
|
||||||
<field name="us80d_selection_type" widget="radio" options="{'horizontal': true}" required="tax_regime == 'old' and costing_details_generated"/>
|
<field name="us80d_selection_type" widget="radio" options="{'horizontal': true}" required="tax_regime == 'old' and costing_details_generated" readonly="state == 'submitted'"/>
|
||||||
<field name="us80d_health_checkup"/>
|
<field name="us80d_health_checkup" readonly="state == 'submitted'"/>
|
||||||
</group>
|
</group>
|
||||||
<field name="us80d_costings" invisible="us80d_selection_type != 'self_family'">
|
<field name="us80d_costings" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -149,7 +199,17 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
<field name="us80d_costings_parents" invisible="us80d_selection_type != 'self_family_parent'">
|
<field name="us80d_costings_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family'" readonly="state == 'submitted'">
|
||||||
|
<list editable="bottom" create="0" delete="0">
|
||||||
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
|
||||||
|
<field name="remarks" width="250px"/>
|
||||||
|
<field name="proof" width="120px"/>
|
||||||
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
<field name="us80d_costings_parents" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family_parent'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -160,7 +220,29 @@
|
||||||
</list>
|
</list>
|
||||||
|
|
||||||
</field>
|
</field>
|
||||||
<field name="us80d_costings_senior_parents" invisible="us80d_selection_type != 'self_family_senior_parent'">
|
<field name="us80d_costings_parents_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family_parent'" readonly="state == 'submitted'">
|
||||||
|
<list editable="bottom" create="0" delete="0">
|
||||||
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
|
||||||
|
<field name="remarks" width="250px"/>
|
||||||
|
<field name="proof" width="120px"/>
|
||||||
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
|
</list>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
<field name="us80d_costings_senior_parents" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family_senior_parent'" readonly="state == 'submitted'">
|
||||||
|
<list editable="bottom" create="0" delete="0">
|
||||||
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
|
||||||
|
<field name="remarks" width="250px"/>
|
||||||
|
<field name="proof" width="120px"/>
|
||||||
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
|
</list>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
<field name="us80d_costings_senior_parents_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family_senior_parent'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -172,8 +254,18 @@
|
||||||
|
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page name="us_10_costing" string="US 10" invisible="tax_regime != 'old'">
|
<page name="us_10_costing" string="US 10" invisible="not show_us_10">
|
||||||
<field name="us10_costings">
|
<field name="us10_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||||
|
<list editable="bottom" create="0" delete="0">
|
||||||
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
|
||||||
|
<field name="remarks" width="250px"/>
|
||||||
|
<field name="proof" width="120px"/>
|
||||||
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
<field name="us10_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -184,8 +276,18 @@
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page name="us_80g_costing" string="US 80G" invisible="tax_regime != 'old'">
|
<page name="us_80g_costing" string="US 80G" invisible="not show_us_80g">
|
||||||
<field name="us80g_costings">
|
<field name="us80g_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||||
|
<list editable="bottom" create="0" delete="0">
|
||||||
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
|
||||||
|
<field name="remarks" width="250px"/>
|
||||||
|
<field name="proof" width="120px"/>
|
||||||
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
<field name="us80g_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -197,8 +299,8 @@
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
|
|
||||||
<page name="chapter_via_costings" string="CHAPTER VIA">
|
<page name="chapter_via_costings" string="CHAPTER VIA" invisible="not show_chapter_via">
|
||||||
<field name="chapter_via_costings" invisible="tax_regime != 'old'">
|
<field name="chapter_via_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -208,7 +310,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
<field name="chapter_via_costings_new" invisible="tax_regime != 'new'">
|
<field name="chapter_via_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -220,8 +322,18 @@
|
||||||
</field>
|
</field>
|
||||||
|
|
||||||
</page>
|
</page>
|
||||||
<page name="us_17_costings" string="US 17" invisible="tax_regime != 'old'">
|
<page name="us_17_costings" string="US 17" invisible="not show_us_17">
|
||||||
<field name="us17_costings">
|
<field name="us17_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||||
|
<list editable="bottom" create="0" delete="0">
|
||||||
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
|
||||||
|
<field name="remarks" width="250px"/>
|
||||||
|
<field name="proof" width="120px"/>
|
||||||
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
<field name="us17_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -232,9 +344,9 @@
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page name="house_rent_costings" string="HOUSE RENT" invisible="tax_regime != 'old'">
|
<page name="house_rent_costings" string="HOUSE RENT" invisible="not show_house_rent">
|
||||||
<!-- <field name="house_rent_costing_line_ids"/>-->
|
<!-- <field name="house_rent_costing_line_ids"/>-->
|
||||||
<field name="house_rent_costings" context="{
|
<field name="house_rent_costings" readonly="state == 'submitted'" context="{
|
||||||
'default_costing_type': house_rent_costing_id
|
'default_costing_type': house_rent_costing_id
|
||||||
}">
|
}">
|
||||||
<list string="House Rent Declarations">
|
<list string="House Rent Declarations">
|
||||||
|
|
@ -284,8 +396,8 @@
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page name="other_i_or_l_costings" string="OTHER INCOME/LOSS">
|
<page name="other_i_or_l_costings" string="OTHER INCOME/LOSS" invisible="not show_other_i_or_l">
|
||||||
<field name="other_il_costings" invisible="tax_regime != 'old'">
|
<field name="other_il_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -302,7 +414,7 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
<field name="other_il_costings_new" invisible="tax_regime != 'new'">
|
<field name="other_il_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
|
@ -319,12 +431,21 @@
|
||||||
<field name="limit" readonly="1" force_save="1"/>
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
|
|
||||||
</page>
|
</page>
|
||||||
<page name="other_declaration_costings" string="Other Declarations" invisible="tax_regime != 'old'">
|
<page name="other_declaration_costings" string="Other Declarations" invisible="not show_other_declaration">
|
||||||
<field name="other_declaration_costings">
|
<field name="other_declaration_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
|
||||||
<list editable="bottom" create="0" delete="0">
|
<list editable="bottom" create="0" delete="0">
|
||||||
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
|
<field name="declaration_amount" width="130px"/>
|
||||||
|
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
|
||||||
|
<field name="remarks" width="250px"/>
|
||||||
|
<field name="proof" width="120px"/>
|
||||||
|
<field name="limit" readonly="1" force_save="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
<field name="other_declaration_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
|
||||||
|
<list editable="bottom" create="0" delete="0">
|
||||||
|
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
|
||||||
<field name="declaration_amount" width="130px"/>
|
<field name="declaration_amount" width="130px"/>
|
||||||
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
|
<field name="proof_amount" width="100px" readonly="1" force_save="1"/>
|
||||||
<field name="remarks" width="250px"/>
|
<field name="remarks" width="250px"/>
|
||||||
|
|
@ -346,10 +467,13 @@
|
||||||
<field name="res_model">emp.it.declaration</field>
|
<field name="res_model">emp.it.declaration</field>
|
||||||
<field name="view_mode">list,form</field>
|
<field name="view_mode">list,form</field>
|
||||||
</record>
|
</record>
|
||||||
|
<menuitem id="menu_hr_payroll_emp_root" name="Payroll" sequence="190" web_icon="hr_payroll,static/description/icon.png" groups="base.group_user"/>
|
||||||
|
|
||||||
<menuitem id="menu_it_declarations" name="IT Declarations"
|
<menuitem id="menu_it_declarations" name="IT Declarations"
|
||||||
parent="hr_payroll.menu_hr_payroll_root"
|
parent="hr_payroll.menu_hr_payroll_root"
|
||||||
action="action_emp_it_declaration" sequence="99"/>
|
action="action_emp_it_declaration" sequence="99"/>
|
||||||
|
<menuitem id="menu_it_declarations_emp" name="IT Declarations"
|
||||||
|
parent="menu_hr_payroll_emp_root"
|
||||||
|
action="action_emp_it_declaration" sequence="1"/>
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_employee_payslip_download_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">employee.payslip.download.wizard.form</field>
|
||||||
|
<field name="model">employee.payslip.download.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Download Payslips">
|
||||||
|
<header>
|
||||||
|
<button name="action_download_payslips"
|
||||||
|
string="Download Payslip"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-download"
|
||||||
|
invisible="download_type != 'single'"/>
|
||||||
|
<button name="action_download_payslips"
|
||||||
|
string="Download ZIP"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-file-archive-o"
|
||||||
|
invisible="download_type != 'multi'"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="employee_id" options="{'no_edit': True, 'no_create': True}"/>
|
||||||
|
<field name="download_type" widget="radio" options="{'horizontal': true}"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="period_id" options="{'no_edit': True, 'no_create': True, 'no_open': True}"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="period_line"
|
||||||
|
force_save="1"
|
||||||
|
domain="[('period_id', '=', period_id)]"
|
||||||
|
required="download_type == 'single'"
|
||||||
|
invisible="download_type != 'single'"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="payslip_count" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_employee_payslip_download_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Download Payslips</field>
|
||||||
|
<field name="res_model">employee.payslip.download.wizard</field>
|
||||||
|
<field name="path">download-payslips</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">current</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_employee_payslip_download_wizard"
|
||||||
|
name="Salary Payslip"
|
||||||
|
parent="employee_it_declaration.menu_hr_payroll_emp_root"
|
||||||
|
action="action_employee_payslip_download_wizard"
|
||||||
|
sequence="2"
|
||||||
|
groups="base.group_user"/>
|
||||||
|
</odoo>
|
||||||
|
|
@ -22,7 +22,8 @@
|
||||||
<group>
|
<group>
|
||||||
<field name="sequence" invisible="1"/>
|
<field name="sequence" invisible="1"/>
|
||||||
<field name="investment_type"/>
|
<field name="investment_type"/>
|
||||||
|
<field name="regime"/>
|
||||||
|
<field name="period_ids" widget="many2many_tags"/>
|
||||||
<field name="active"/>
|
<field name="active"/>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<list>
|
<list>
|
||||||
<field name="employee_id"/>
|
<field name="employee_id"/>
|
||||||
<field name="period_id"/>
|
<field name="period_id"/>
|
||||||
<field name="period_line"/>
|
<field name="period_line" force_save="1"/>
|
||||||
<field name="tax_regime"/>
|
<field name="tax_regime"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
|
|
@ -26,8 +26,22 @@
|
||||||
type="object"
|
type="object"
|
||||||
class="oe_stat_button"
|
class="oe_stat_button"
|
||||||
icon="fa-file-text"/>
|
icon="fa-file-text"/>
|
||||||
|
<button name="action_check_regime_comparison"
|
||||||
|
string="Check Comparison"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-calculator"/>
|
||||||
|
<button name="action_generate_comparison_report"
|
||||||
|
string="Download Comparison"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-balance-scale"
|
||||||
|
invisible="not comparison_available"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
|
<field name="comparison_available" invisible="1"/>
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
|
<field name="is_general_tax_statement" invisible="1"/>
|
||||||
<group>
|
<group>
|
||||||
<field name="employee_id" options="{'no_edit': True, 'no_create': True}"/>
|
<field name="employee_id" options="{'no_edit': True, 'no_create': True}"/>
|
||||||
<field name="contract_id" readonly="1" force_save="1" invisible="0"/>
|
<field name="contract_id" readonly="1" force_save="1" invisible="0"/>
|
||||||
|
|
@ -43,13 +57,26 @@
|
||||||
<field name="period_id" options="{'no_edit': True, 'no_create': True, 'no_open': True}"/>
|
<field name="period_id" options="{'no_edit': True, 'no_create': True, 'no_open': True}"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="period_line" domain="[('period_id', '=', period_id),('to_date','<',datetime.datetime.now()),('from_date','>',emp_doj)]" options="{'no_edit': True, 'no_create': True, 'no_open': True}" invisible="not emp_doj"/>
|
<field name="period_line" force_save="1" domain="[('period_id', '=', period_id),('to_date','<',(context_today() + datetime.timedelta(days=30)).strftime('%Y-%m-%d')),('to_date','>',emp_doj)]"/>
|
||||||
<field name="period_line" domain="[('period_id', '=', period_id),('to_date','<',datetime.datetime.now())]" options="{'no_edit': True, 'no_create': True, 'no_open': True}" invisible="emp_doj"/>
|
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}" on_change="1"/>
|
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}" on_change="1"/>
|
||||||
</group>
|
</group>
|
||||||
|
<group string="Tax Regime Comparison" invisible="not comparison_available">
|
||||||
|
<group string="Old Regime">
|
||||||
|
<field name="old_regime_taxable_income" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||||
|
<field name="old_regime_tax_payable" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||||
|
</group>
|
||||||
|
<group string="New Regime">
|
||||||
|
<field name="new_regime_taxable_income" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||||
|
<field name="new_regime_tax_payable" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="beneficial_regime" readonly="1"/>
|
||||||
|
<field name="tax_difference" readonly="1" widget="monetary" options="{'currency_field': 'currency_id'}"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
|
|
@ -60,7 +87,7 @@
|
||||||
<field name="name">Generate Tax Statement</field>
|
<field name="name">Generate Tax Statement</field>
|
||||||
<field name="res_model">it.tax.statement.wizard</field>
|
<field name="res_model">it.tax.statement.wizard</field>
|
||||||
<field name="path">tax-statement</field>
|
<field name="path">tax-statement</field>
|
||||||
<field name="view_mode">list,form</field>
|
<field name="view_mode">form</field>
|
||||||
<field name="help" type="html">
|
<field name="help" type="html">
|
||||||
<p class="o_view_nocontent_smiling_face">
|
<p class="o_view_nocontent_smiling_face">
|
||||||
Create a new employment type
|
Create a new employment type
|
||||||
|
|
@ -69,8 +96,9 @@
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<menuitem id="menu_it_tax_statement_root" name="IT Tax Statement"
|
<menuitem id="menu_it_tax_statement_root" name="IT Tax Statement"
|
||||||
parent="hr_payroll.menu_hr_payroll_root"
|
parent="employee_it_declaration.menu_hr_payroll_emp_root"
|
||||||
action="action_it_tax_statement_wizard" sequence="99"/>
|
groups="base.group_user"
|
||||||
|
action="action_it_tax_statement_wizard" sequence="3"/>
|
||||||
|
|
||||||
<record id="it_statement_paper_format" model="report.paperformat">
|
<record id="it_statement_paper_format" model="report.paperformat">
|
||||||
<field name="name">A4 - statement</field>
|
<field name="name">A4 - statement</field>
|
||||||
|
|
@ -95,7 +123,19 @@
|
||||||
<field name="report_name">employee_it_declaration.generate_income_tax_statement_rpt</field>
|
<field name="report_name">employee_it_declaration.generate_income_tax_statement_rpt</field>
|
||||||
<field name="report_file">employee_it_declaration.generate_income_tax_statement_rpt</field>
|
<field name="report_file">employee_it_declaration.generate_income_tax_statement_rpt</field>
|
||||||
<field name="binding_model_id" ref="employee_it_declaration.model_it_tax_statement_wizard"/>
|
<field name="binding_model_id" ref="employee_it_declaration.model_it_tax_statement_wizard"/>
|
||||||
<field name="print_report_name">'INCOMETAX - %s' % (object.display_name)</field>
|
<field name="print_report_name">'%s - %s' % (object.employee_id.name or '', object.period_line.name or '')</field>
|
||||||
|
<field name="paperformat_id" ref="it_statement_paper_format"/>
|
||||||
|
<field name="binding_type">report</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="income_tax_comparison_action_report" model="ir.actions.report">
|
||||||
|
<field name="name">Download Tax Regime Comparison</field>
|
||||||
|
<field name="model">it.tax.statement.wizard</field>
|
||||||
|
<field name="report_type">qweb-pdf</field>
|
||||||
|
<field name="report_name">employee_it_declaration.generate_income_tax_comparison_rpt</field>
|
||||||
|
<field name="report_file">employee_it_declaration.generate_income_tax_comparison_rpt</field>
|
||||||
|
<field name="binding_model_id" ref="employee_it_declaration.model_it_tax_statement_wizard"/>
|
||||||
|
<field name="print_report_name">'%s - Tax Regime Comparison' % (object.employee_id.name or '')</field>
|
||||||
<field name="paperformat_id" ref="it_statement_paper_format"/>
|
<field name="paperformat_id" ref="it_statement_paper_format"/>
|
||||||
<field name="binding_type">report</field>
|
<field name="binding_type">report</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,158 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<template id="report_it_tax_statement">
|
<template id="report_it_tax_statement">
|
||||||
<t t-call="web.html_container">
|
<t t-call="web.html_container">
|
||||||
<t t-call="web.external_layout">
|
<t t-foreach="docs" t-as="doc">
|
||||||
<div class="page">
|
<t t-call="web.external_layout">
|
||||||
<h2>IT Tax Statement</h2>
|
<div class="page">
|
||||||
<p><strong>Employee:</strong> <t t-esc="doc.employee_id.name"/></p>
|
<style>
|
||||||
<p><strong>Period:</strong> <t t-esc="doc.period_id.name"/></p>
|
.it-title { text-align: center; margin-bottom: 18px; }
|
||||||
<p><strong>Tax Regime:</strong> <t t-esc="dict(doc._fields['tax_regime'].selection).get(doc.tax_regime)"/></p>
|
.it-title h2 { margin: 0; font-size: 21px; font-weight: 700; }
|
||||||
<table class="table table-sm">
|
.it-title p { margin: 4px 0 0; color: #555; }
|
||||||
<thead>
|
.it-section { margin-top: 18px; }
|
||||||
<tr><th>Section</th><th>Amount</th></tr>
|
.it-section h4 { font-size: 14px; font-weight: 700; border-bottom: 1px solid #999; padding-bottom: 4px; margin-bottom: 8px; }
|
||||||
</thead>
|
.it-table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
||||||
<tbody>
|
.it-table th { background: #f2f2f2; font-weight: 700; }
|
||||||
<t t-foreach="doc.investment_costing_ids" t-as="line">
|
.it-table th, .it-table td { border: 1px solid #ddd; padding: 5px; vertical-align: top; }
|
||||||
<tr>
|
.it-right { text-align: right; }
|
||||||
<td><t t-esc="line.investment_type_id.name"/></td>
|
.it-muted { color: #777; }
|
||||||
<td><t t-esc="line.amount"/></td>
|
</style>
|
||||||
</tr>
|
|
||||||
</t>
|
<div class="it-title">
|
||||||
</tbody>
|
<h2>IT Declaration Submission</h2>
|
||||||
</table>
|
<p>
|
||||||
<p><strong>Total:</strong> <t t-esc="sum(doc.investment_costing_ids.mapped('amount'))"/></p>
|
<span t-esc="doc.employee_id.company_id.name"/>
|
||||||
</div>
|
</p>
|
||||||
</t>
|
</div>
|
||||||
</t>
|
|
||||||
</template>
|
<table class="it-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Employee</th>
|
||||||
|
<td><span t-esc="doc.employee_id.name"/></td>
|
||||||
|
<th>Payroll Period</th>
|
||||||
|
<td><span t-esc="doc.period_id.name"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Tax Regime</th>
|
||||||
|
<td><span t-esc="doc._get_regime_label()"/></td>
|
||||||
|
<th>Status</th>
|
||||||
|
<td><span t-esc="dict(doc._fields['state'].selection).get(doc.state)"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Submitted On</th>
|
||||||
|
<td><span t-esc="doc.write_date"/></td>
|
||||||
|
<th>Total Investment</th>
|
||||||
|
<td class="it-right"><span t-esc="'{:,.2f}'.format(doc.total_investment or 0.0)"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr t-if="doc.return_reason">
|
||||||
|
<th>Return Reason</th>
|
||||||
|
<td colspan="3"><span t-esc="doc.return_reason"/></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<t t-set="sections" t-value="doc._get_report_sections()"/>
|
||||||
|
<t t-if="not sections">
|
||||||
|
<div class="it-section">
|
||||||
|
<p class="it-muted">No declaration lines are available for the selected regime.</p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-foreach="sections" t-as="section">
|
||||||
|
<div class="it-section">
|
||||||
|
<h4><span t-esc="section.get('title')"/></h4>
|
||||||
|
<t t-if="section.get('house_rent')">
|
||||||
|
<table class="it-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Rent Amount</th>
|
||||||
|
<th>From</th>
|
||||||
|
<th>To</th>
|
||||||
|
<th>Landlord PAN</th>
|
||||||
|
<th>Remarks</th>
|
||||||
|
<th>Proof</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr t-foreach="section.get('lines')" t-as="line">
|
||||||
|
<td><span t-esc="line.get('hra_exemption_type')"/></td>
|
||||||
|
<td class="it-right"><span t-esc="'{:,.2f}'.format(line.get('rent_amount') or 0.0)"/></td>
|
||||||
|
<td><span t-esc="line.get('from_date')"/></td>
|
||||||
|
<td><span t-esc="line.get('to_date')"/></td>
|
||||||
|
<td>
|
||||||
|
<span t-esc="line.get('landlord_pan_status')"/>
|
||||||
|
<br t-if="line.get('landlord_pan_no')"/>
|
||||||
|
<span t-if="line.get('landlord_pan_no')" t-esc="line.get('landlord_pan_no')"/>
|
||||||
|
</td>
|
||||||
|
<td><span t-esc="line.get('remarks')"/></td>
|
||||||
|
<td>
|
||||||
|
<a t-if="line.get('attachment_url')" t-att-href="line.get('attachment_url')">
|
||||||
|
<span t-esc="line.get('attachment_name')"/>
|
||||||
|
</a>
|
||||||
|
<span t-if="not line.get('attachment_url')" class="it-muted">No proof</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<table class="it-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Declaration Amount</th>
|
||||||
|
<th>Proof Amount</th>
|
||||||
|
<th>Limit</th>
|
||||||
|
<th>Remarks</th>
|
||||||
|
<th>Proof</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr t-foreach="section.get('lines')" t-as="line">
|
||||||
|
<td><span t-esc="line.get('name')"/></td>
|
||||||
|
<td class="it-right"><span t-esc="'{:,.2f}'.format(line.get('declaration_amount') or 0.0)"/></td>
|
||||||
|
<td class="it-right"><span t-esc="'{:,.2f}'.format(line.get('proof_amount') or 0.0)"/></td>
|
||||||
|
<td class="it-right"><span t-esc="'{:,.2f}'.format(line.get('limit') or 0.0)"/></td>
|
||||||
|
<td><span t-esc="line.get('remarks')"/></td>
|
||||||
|
<td>
|
||||||
|
<a t-if="line.get('attachment_url')" t-att-href="line.get('attachment_url')">
|
||||||
|
<span t-esc="line.get('attachment_name')"/>
|
||||||
|
</a>
|
||||||
|
<span t-if="not line.get('attachment_url')" class="it-muted">No proof</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-foreach="doc._get_report_extra_sections()" t-as="extra_section">
|
||||||
|
<div class="it-section">
|
||||||
|
<h4><span t-esc="extra_section.get('title')"/></h4>
|
||||||
|
<table class="it-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th t-foreach="extra_section.get('headers')" t-as="header">
|
||||||
|
<span t-esc="header"/>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr t-foreach="extra_section.get('rows')" t-as="row">
|
||||||
|
<td t-foreach="row" t-as="cell">
|
||||||
|
<span t-esc="cell"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list>
|
<list>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
|
<field name="period_id"/>
|
||||||
<field name="regime"/>
|
<field name="regime"/>
|
||||||
<field name="age_category"/>
|
<field name="age_category"/>
|
||||||
<field name="residence_type"/>
|
<field name="residence_type"/>
|
||||||
|
|
@ -42,6 +43,7 @@
|
||||||
<sheet>
|
<sheet>
|
||||||
<group>
|
<group>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
|
<field name="period_id"/>
|
||||||
<field name="regime"/>
|
<field name="regime"/>
|
||||||
<field name="age_category"/>
|
<field name="age_category"/>
|
||||||
<field name="residence_type"/>
|
<field name="residence_type"/>
|
||||||
|
|
@ -52,16 +54,27 @@
|
||||||
<page string="Slab Rules">
|
<page string="Slab Rules">
|
||||||
<field name="rules">
|
<field name="rules">
|
||||||
<list editable="bottom">
|
<list editable="bottom">
|
||||||
|
<field name="sequence" widget='handle'/>
|
||||||
<field name="min_income"/>
|
<field name="min_income"/>
|
||||||
<field name="max_income"/>
|
<field name="max_income"/>
|
||||||
<field name="tax_rate"/>
|
<field name="tax_rate"/>
|
||||||
<field name="fixed_amount"/>
|
<field name="fixed_amount"/>
|
||||||
<field name="excess_threshold"/>
|
<field name="excess_threshold" optional="hide"/>
|
||||||
<field name="surcharge_rate"/>
|
<field name="surcharge_rate" column_invisible="1" invisible="1"/>
|
||||||
<field name="cess_rate"/>
|
<field name="cess_rate"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
|
<page string="Surcharge Rules">
|
||||||
|
<field name="surcharges">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="min_income"/>
|
||||||
|
<field name="max_income"/>
|
||||||
|
<field name="surcharge_rate"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from . import hr_tds_calculation
|
||||||
from . import children_education_costing
|
from . import children_education_costing
|
||||||
from . import employee_life_insurance
|
from . import employee_life_insurance
|
||||||
from . import nsc_declaration
|
from . import nsc_declaration
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from odoo import models, fields, api
|
||||||
|
|
||||||
class ChildrenEducation(models.Model):
|
class ChildrenEducation(models.Model):
|
||||||
_name = "children.education"
|
_name = "children.education"
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = "Children Education"
|
_description = "Children Education"
|
||||||
_rec_name = 'it_declaration_id'
|
_rec_name = 'it_declaration_id'
|
||||||
|
|
||||||
|
|
@ -38,6 +39,7 @@ class ChildrenEducation(models.Model):
|
||||||
|
|
||||||
class ChildrenEducationCosting(models.Model):
|
class ChildrenEducationCosting(models.Model):
|
||||||
_name = 'children.education.costing'
|
_name = 'children.education.costing'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = "Children Education Costing"
|
_description = "Children Education Costing"
|
||||||
|
|
||||||
child_id = fields.Char('Child ID')
|
child_id = fields.Char('Child ID')
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from odoo import models, fields, api, _
|
||||||
|
|
||||||
class US80CInsuranceLine(models.Model):
|
class US80CInsuranceLine(models.Model):
|
||||||
_name = 'us80c.insurance.line'
|
_name = 'us80c.insurance.line'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'US80C Insurance Line'
|
_description = 'US80C Insurance Line'
|
||||||
|
|
||||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
||||||
|
|
@ -30,6 +31,7 @@ class US80CInsuranceLine(models.Model):
|
||||||
|
|
||||||
class EmployeeLifeInsurance(models.Model):
|
class EmployeeLifeInsurance(models.Model):
|
||||||
_name = 'employee.life.insurance'
|
_name = 'employee.life.insurance'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'Employee Life Insurance'
|
_description = 'Employee Life Insurance'
|
||||||
|
|
||||||
parent_id = fields.Many2one('us80c.insurance.line', string="Parent Line") # Link to parent
|
parent_id = fields.Many2one('us80c.insurance.line', string="Parent Line") # Link to parent
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,308 @@
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class HrTdsCalculation(models.TransientModel):
|
||||||
|
_inherit = 'l10n.in.tds.computation.wizard'
|
||||||
|
|
||||||
|
def default_get(self, fields):
|
||||||
|
res = super().default_get(fields)
|
||||||
|
# Remove the standard_deduction if it was set by parent
|
||||||
|
if 'standard_deduction' in res:
|
||||||
|
del res['standard_deduction']
|
||||||
|
return res
|
||||||
|
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", required=True, default='new')
|
||||||
|
|
||||||
|
standard_deduction = fields.Float(
|
||||||
|
string="Standard Deduction",
|
||||||
|
compute="_compute_standard_deduction",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _get_regular_pay_structure(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.payslip_id and self.payslip_id.struct_id:
|
||||||
|
return self.payslip_id.struct_id
|
||||||
|
if self.contract_id.structure_type_id.default_struct_id:
|
||||||
|
return self.contract_id.structure_type_id.default_struct_id
|
||||||
|
return self.env['hr.payroll.structure']
|
||||||
|
|
||||||
|
def _get_annualization_factor(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.contract_id:
|
||||||
|
return self.contract_id._get_salary_costs_factor()
|
||||||
|
return 12
|
||||||
|
|
||||||
|
def _get_monthly_gross_from_structure(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.contract_id or not self.contract_id.employee_id:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
structure = self._get_regular_pay_structure()
|
||||||
|
payslip = self.payslip_id
|
||||||
|
dummy_payslip = self.env['hr.payslip']
|
||||||
|
|
||||||
|
if not payslip:
|
||||||
|
today = fields.Date.today()
|
||||||
|
period_start = today.replace(day=1)
|
||||||
|
period_end = fields.Date.end_of(period_start, 'month')
|
||||||
|
dummy_payslip = self.env['hr.payslip'].sudo().create({
|
||||||
|
'name': 'TDS Gross Preview',
|
||||||
|
'employee_id': self.contract_id.employee_id.id,
|
||||||
|
'contract_id': self.contract_id.id,
|
||||||
|
'struct_id': structure.id or self.contract_id.structure_type_id.default_struct_id.id,
|
||||||
|
'date_from': period_start,
|
||||||
|
'date_to': period_end,
|
||||||
|
})
|
||||||
|
dummy_payslip.sudo().compute_sheet()
|
||||||
|
payslip = dummy_payslip
|
||||||
|
|
||||||
|
try:
|
||||||
|
gross_lines = payslip.line_ids.filtered(lambda line: line.salary_rule_id.code == 'GROSS')
|
||||||
|
if gross_lines:
|
||||||
|
return sum(gross_lines.mapped('total'))
|
||||||
|
return self.contract_id.wage or 0.0
|
||||||
|
finally:
|
||||||
|
if dummy_payslip:
|
||||||
|
dummy_payslip.sudo().action_payslip_cancel()
|
||||||
|
dummy_payslip.sudo().unlink()
|
||||||
|
|
||||||
|
def _get_current_payroll_period(self):
|
||||||
|
today = fields.Date.today()
|
||||||
|
return self.env['payroll.period'].search([
|
||||||
|
('from_date', '<=', today),
|
||||||
|
('to_date', '>=', today),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
def _get_age_category(self, age):
|
||||||
|
if age < 60:
|
||||||
|
return 'below_60'
|
||||||
|
if age < 80:
|
||||||
|
return '60_to_80'
|
||||||
|
return 'above_80'
|
||||||
|
|
||||||
|
def _get_employee_age(self):
|
||||||
|
self.ensure_one()
|
||||||
|
employee = self.contract_id.employee_id
|
||||||
|
if employee and employee.birthday:
|
||||||
|
return relativedelta(date.today(), employee.birthday).years
|
||||||
|
return 30
|
||||||
|
|
||||||
|
def _find_applicable_slab(self, regime):
|
||||||
|
self.ensure_one()
|
||||||
|
period = self._get_current_payroll_period()
|
||||||
|
if not period:
|
||||||
|
return self.env['it.slab.master']
|
||||||
|
|
||||||
|
age_category = self._get_age_category(self._get_employee_age())
|
||||||
|
return self.env['it.slab.master'].search([
|
||||||
|
('period_id', '=', period.id),
|
||||||
|
('regime', '=', regime),
|
||||||
|
('age_category', '=', age_category),
|
||||||
|
'|',
|
||||||
|
('residence_type', '=', 'resident'),
|
||||||
|
('residence_type', '=', 'both'),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
def _get_standard_deduction_amount(self, regime, slab_master=False):
|
||||||
|
if slab_master and slab_master.standard_deduction:
|
||||||
|
return slab_master.standard_deduction
|
||||||
|
return 75000.0 if regime == 'new' else 50000.0
|
||||||
|
|
||||||
|
def _compute_tax_using_slab(self, taxable_income, slab_master):
|
||||||
|
rules = slab_master.rules.sorted(lambda r: (r.sequence, r.max_income or float('inf')))
|
||||||
|
applicable_rule = False
|
||||||
|
previous_rules = self.env['it.slab.master.rules']
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
min_income = rule.min_income or 0.0
|
||||||
|
max_income = rule.max_income or float('inf')
|
||||||
|
if min_income < taxable_income <= max_income:
|
||||||
|
applicable_rule = rule
|
||||||
|
previous_rules = rules.filtered(
|
||||||
|
lambda r: r.sequence < rule.sequence or
|
||||||
|
(r.sequence == rule.sequence and (r.max_income or float('inf')) < max_income)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not applicable_rule:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
previous_max_income = previous_rules[-1].max_income if previous_rules else 0.0
|
||||||
|
current_tax = (taxable_income - previous_max_income) * (applicable_rule.tax_rate / 100.0)
|
||||||
|
previous_fixed_amounts = sum(previous_rules.mapped('fixed_amount'))
|
||||||
|
return current_tax + previous_fixed_amounts
|
||||||
|
|
||||||
|
def _compute_rebate(self, regime, taxable_income, slab_tax):
|
||||||
|
if regime == 'old':
|
||||||
|
if taxable_income <= 500000.0:
|
||||||
|
return min(12500.0, slab_tax)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
if taxable_income >= 1200000.0:
|
||||||
|
return max(0.0, slab_tax - (taxable_income - 1200000.0))
|
||||||
|
return min(60000.0, slab_tax)
|
||||||
|
|
||||||
|
def _compute_surcharge(self, slab_master, taxable_income, tax_after_rebate):
|
||||||
|
surcharge_rate = 0.0
|
||||||
|
for rule in slab_master.surcharges.sorted('min_income'):
|
||||||
|
max_income = rule.max_income or float('inf')
|
||||||
|
if rule.min_income < taxable_income <= max_income:
|
||||||
|
surcharge_rate = rule.surcharge_rate
|
||||||
|
return tax_after_rebate * (surcharge_rate / 100.0)
|
||||||
|
|
||||||
|
def _compute_cess(self, slab_master, taxable_income, tax_with_surcharge):
|
||||||
|
cess_rate = 4.0
|
||||||
|
for rule in slab_master.rules.sorted('min_income'):
|
||||||
|
max_income = rule.max_income or float('inf')
|
||||||
|
if rule.min_income < taxable_income <= max_income:
|
||||||
|
cess_rate = rule.cess_rate or 4.0
|
||||||
|
break
|
||||||
|
return tax_with_surcharge * (cess_rate / 100.0)
|
||||||
|
|
||||||
|
def _compute_tax_from_custom_slab(self, total_income, regime, slab_master):
|
||||||
|
taxable_income = max(total_income - self._get_standard_deduction_amount(regime, slab_master), 0.0)
|
||||||
|
slab_tax = self._compute_tax_using_slab(taxable_income, slab_master)
|
||||||
|
rebate = self._compute_rebate(regime, taxable_income, slab_tax)
|
||||||
|
total_tax_on_income = max(0.0, slab_tax - rebate)
|
||||||
|
surcharge = self._compute_surcharge(slab_master, taxable_income, total_tax_on_income)
|
||||||
|
tax_with_surcharge = total_tax_on_income + surcharge
|
||||||
|
cess = self._compute_cess(slab_master, taxable_income, tax_with_surcharge) if tax_with_surcharge else 0.0
|
||||||
|
return {
|
||||||
|
'taxable_income': taxable_income,
|
||||||
|
'tax_on_taxable_income': slab_tax,
|
||||||
|
'rebate': rebate,
|
||||||
|
'total_tax_on_income': total_tax_on_income,
|
||||||
|
'surcharge': surcharge,
|
||||||
|
'cess': cess,
|
||||||
|
'total_tax': tax_with_surcharge + cess,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _compute_tax_from_rule_parameters(self, total_income, standard_deduction):
|
||||||
|
rule_parameter = self.env['hr.rule.parameter']
|
||||||
|
tax_slabs = rule_parameter._get_parameter_from_code('l10n_in_tds_rate_chart')
|
||||||
|
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')
|
||||||
|
|
||||||
|
taxable_income = max(total_income - standard_deduction, 0.0)
|
||||||
|
tax = 0.0
|
||||||
|
for rate, (lower, upper) in tax_slabs:
|
||||||
|
if taxable_income <= lower:
|
||||||
|
break
|
||||||
|
taxable_amount = min(taxable_income, float(upper)) - lower
|
||||||
|
tax += round(taxable_amount * rate)
|
||||||
|
|
||||||
|
if taxable_income >= min_income_for_rebate:
|
||||||
|
marginal_income = taxable_income - min_income_for_rebate
|
||||||
|
rebate = max(tax - marginal_income, 0.0)
|
||||||
|
else:
|
||||||
|
rebate = tax
|
||||||
|
total_tax_on_income = tax - rebate
|
||||||
|
|
||||||
|
surcharge = 0.0
|
||||||
|
if taxable_income > min_income_for_surcharge:
|
||||||
|
for rate, amount in tax_slabs_for_surcharge:
|
||||||
|
if taxable_income <= float(amount[1]):
|
||||||
|
surcharge = 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.0, 0.0
|
||||||
|
for income, tax_amount, surcharge_rate in max_tax_slabs:
|
||||||
|
if taxable_income <= income:
|
||||||
|
break
|
||||||
|
max_taxable_income, max_tax, max_surcharge = income, tax_amount, surcharge_rate
|
||||||
|
|
||||||
|
excess_income = taxable_income - max_taxable_income
|
||||||
|
max_tax_with_surcharge = max_tax + max_surcharge
|
||||||
|
total_tax_with_surcharge = total_tax_on_income + surcharge
|
||||||
|
excess_tax = total_tax_with_surcharge - max_tax_with_surcharge
|
||||||
|
if excess_tax - excess_income > 0:
|
||||||
|
surcharge = max_tax_with_surcharge + taxable_income - max_taxable_income - total_tax_on_income
|
||||||
|
|
||||||
|
cess = (total_tax_on_income + surcharge) * 0.04
|
||||||
|
return {
|
||||||
|
'taxable_income': taxable_income,
|
||||||
|
'tax_on_taxable_income': tax,
|
||||||
|
'rebate': rebate,
|
||||||
|
'total_tax_on_income': total_tax_on_income,
|
||||||
|
'surcharge': surcharge,
|
||||||
|
'cess': cess,
|
||||||
|
'total_tax': total_tax_on_income + surcharge + cess,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.depends('contract_id', 'payslip_id', 'contract_id.structure_type_id', 'contract_id.wage')
|
||||||
|
def _compute_total_income(self):
|
||||||
|
for record in self:
|
||||||
|
if not record.contract_id:
|
||||||
|
record.total_income = 0.0
|
||||||
|
continue
|
||||||
|
monthly_gross = record._get_monthly_gross_from_structure()
|
||||||
|
|
||||||
|
rule_parameter = self.env['hr.rule.parameter']
|
||||||
|
pf = 0
|
||||||
|
if record.contract_id:
|
||||||
|
amounts = rule_parameter._get_parameter_from_code('l10n_in_professional_tax')
|
||||||
|
cost = record.contract_id.wage -1800
|
||||||
|
if cost >= 20000:
|
||||||
|
pf = amounts[0]
|
||||||
|
elif cost >= 15001 and cost < 20000:
|
||||||
|
pf = amounts[1]
|
||||||
|
else:
|
||||||
|
pf = 0
|
||||||
|
record.total_income = (monthly_gross * record._get_annualization_factor()) + (pf * 12)
|
||||||
|
|
||||||
|
@api.depends('contract_id', 'payslip_id', 'total_income')
|
||||||
|
def _compute_net_monthly(self):
|
||||||
|
for record in self:
|
||||||
|
factor = record._get_annualization_factor() if record.contract_id else 12
|
||||||
|
if record.payslip_id and record.payslip_id.net_wage:
|
||||||
|
record.net_monthly = record.payslip_id.net_wage
|
||||||
|
elif factor:
|
||||||
|
record.net_monthly = record.total_income / factor
|
||||||
|
else:
|
||||||
|
record.net_monthly = 0.0
|
||||||
|
|
||||||
|
@api.depends('tax_regime', 'contract_id')
|
||||||
|
def _compute_standard_deduction(self):
|
||||||
|
for record in self:
|
||||||
|
slab_master = record._find_applicable_slab(record.tax_regime) if record.tax_regime else False
|
||||||
|
record.standard_deduction = record._get_standard_deduction_amount(record.tax_regime or 'new', slab_master)
|
||||||
|
|
||||||
|
@api.depends('total_income', 'tax_regime', 'standard_deduction', 'contract_id')
|
||||||
|
def _compute_taxable_income(self):
|
||||||
|
for record in self:
|
||||||
|
if not record.total_income:
|
||||||
|
record.taxable_income = 0.0
|
||||||
|
record.tax_on_taxable_income = 0.0
|
||||||
|
record.rebate = 0.0
|
||||||
|
record.total_tax_on_income = 0.0
|
||||||
|
record.surcharge = 0.0
|
||||||
|
record.cess = 0.0
|
||||||
|
record.total_tax = 0.0
|
||||||
|
continue
|
||||||
|
|
||||||
|
slab_master = record._find_applicable_slab(record.tax_regime) if record.tax_regime else False
|
||||||
|
if slab_master:
|
||||||
|
values = record._compute_tax_from_custom_slab(record.total_income, record.tax_regime, slab_master)
|
||||||
|
else:
|
||||||
|
values = record._compute_tax_from_rule_parameters(record.total_income, record.standard_deduction)
|
||||||
|
|
||||||
|
record.taxable_income = values['taxable_income']
|
||||||
|
record.tax_on_taxable_income = values['tax_on_taxable_income']
|
||||||
|
record.rebate = values['rebate']
|
||||||
|
record.total_tax_on_income = values['total_tax_on_income']
|
||||||
|
record.surcharge = values['surcharge']
|
||||||
|
record.cess = values['cess']
|
||||||
|
record.total_tax = values['total_tax']
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="l10n_in_tds_computation_wizard_view_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">l10n.in.tds.computation.wizard.view.form</field>
|
||||||
|
<field name="model">l10n.in.tds.computation.wizard</field>
|
||||||
|
<field name="inherit_id" ref="l10n_in_hr_payroll.l10n_in_tds_computation_wizard_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//form/group/field[@name='total_income']" position="after">
|
||||||
|
<field name="tax_regime" widget="radio" options="{'horizontal': true}"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//form/group/field[@name='standard_deduction']" position="attributes">
|
||||||
|
<attribute name="force_save">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -3,6 +3,7 @@ import math
|
||||||
|
|
||||||
class LetoutHouseProperty(models.Model):
|
class LetoutHouseProperty(models.Model):
|
||||||
_name = 'letout.house.property'
|
_name = 'letout.house.property'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'Letout House Property Details'
|
_description = 'Letout House Property Details'
|
||||||
|
|
||||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from odoo import models, fields, api
|
||||||
|
|
||||||
class NSCDeclarationLine(models.Model):
|
class NSCDeclarationLine(models.Model):
|
||||||
_name = 'nsc.declaration.line'
|
_name = 'nsc.declaration.line'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'NSC Declaration Line'
|
_description = 'NSC Declaration Line'
|
||||||
|
|
||||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration", required=True)
|
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration", required=True)
|
||||||
|
|
@ -19,6 +20,7 @@ class NSCDeclarationLine(models.Model):
|
||||||
|
|
||||||
class NSCEntry(models.Model):
|
class NSCEntry(models.Model):
|
||||||
_name = 'nsc.entry'
|
_name = 'nsc.entry'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'NSC Entry'
|
_description = 'NSC Entry'
|
||||||
|
|
||||||
parent_id = fields.Many2one('nsc.declaration.line', string="NSC Declaration")
|
parent_id = fields.Many2one('nsc.declaration.line', string="NSC Declaration")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from odoo import models, fields, api
|
||||||
|
|
||||||
class NSCInterestLine(models.Model):
|
class NSCInterestLine(models.Model):
|
||||||
_name = 'nsc.interest.line'
|
_name = 'nsc.interest.line'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'NSC Interest Line'
|
_description = 'NSC Interest Line'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,6 +24,7 @@ class NSCInterestLine(models.Model):
|
||||||
|
|
||||||
class NSCEntry(models.Model):
|
class NSCEntry(models.Model):
|
||||||
_name = 'nsc.interest.entry'
|
_name = 'nsc.interest.entry'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'NSC Entry'
|
_description = 'NSC Entry'
|
||||||
|
|
||||||
parent_id = fields.Many2one('nsc.interest.line', string="NSC Interest")
|
parent_id = fields.Many2one('nsc.interest.line', string="NSC Interest")
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from odoo import models, fields, api
|
||||||
|
|
||||||
class SelfOccupiedProperty(models.Model):
|
class SelfOccupiedProperty(models.Model):
|
||||||
_name = 'self.occupied.property'
|
_name = 'self.occupied.property'
|
||||||
|
_inherit = ['it.declaration.submitted.lock.mixin']
|
||||||
_description = 'Self Occupied House Property Details'
|
_description = 'Self Occupied House Property Details'
|
||||||
|
|
||||||
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class website_hr_recruitment_applications_extended(website_hr_recruitment_applic
|
||||||
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||||
if not applicant.exists():
|
if not applicant.exists():
|
||||||
return request.not_found()
|
return request.not_found()
|
||||||
if applicant and applicant.send_post_onboarding_form:
|
if applicant:
|
||||||
if applicant.post_onboarding_form_status == 'done':
|
if applicant.post_onboarding_form_status == 'done':
|
||||||
return request.render("hr_recruitment_extended.thank_you_template", {
|
return request.render("hr_recruitment_extended.thank_you_template", {
|
||||||
'applicant': applicant
|
'applicant': applicant
|
||||||
|
|
@ -38,7 +38,7 @@ class website_hr_recruitment_applications_extended(website_hr_recruitment_applic
|
||||||
return f"Error: Applicant with ID {applicant_id} not found"
|
return f"Error: Applicant with ID {applicant_id} not found"
|
||||||
|
|
||||||
# Business logic check
|
# Business logic check
|
||||||
if not applicant.send_post_onboarding_form or applicant.post_onboarding_form_status != 'done':
|
if applicant.post_onboarding_form_status != 'done':
|
||||||
return f"Error: Applicant {applicant_id} does not meet the criteria for download"
|
return f"Error: Applicant {applicant_id} does not meet the criteria for download"
|
||||||
|
|
||||||
# Get the template
|
# Get the template
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from odoo import fields, api, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,8 +31,7 @@ class HREmployee(models.Model):
|
||||||
'recruitment_stage_id': self.env.ref('employee_jod.hired_stage8').id,
|
'recruitment_stage_id': self.env.ref('employee_jod.hired_stage8').id,
|
||||||
})
|
})
|
||||||
rec.applicant_id = application.id
|
rec.applicant_id = application.id
|
||||||
rec.sudo().applicant_id.send_post_onboarding_form = True
|
return rec.sudo().applicant_id.send_jod_form_to_employee()
|
||||||
return rec.sudo().applicant_id.send_post_onboarding_form_to_candidate()
|
|
||||||
|
|
||||||
|
|
||||||
class PostOnboardingAttachmentWizard(models.TransientModel):
|
class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
|
|
@ -44,11 +43,12 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
def _onchange_template_id(self):
|
def _onchange_template_id(self):
|
||||||
""" Update the email body and recipients based on the selected template. """
|
""" Update the email body and recipients based on the selected template. """
|
||||||
if self.template_id:
|
if self.template_id:
|
||||||
record_id = self.env.context.get('active_id')
|
record_id = self.env.context.get('applicant_id')
|
||||||
model = self.env.context.get('active_model')
|
model = self.env.context.get('active_model')
|
||||||
|
if model == 'applicant.request.forms':
|
||||||
if model == 'hr.applicant':
|
|
||||||
applicant = self.env['hr.applicant'].browse(record_id)
|
applicant = self.env['hr.applicant'].browse(record_id)
|
||||||
|
elif model == 'hr.applicant':
|
||||||
|
applicant = self.env['hr.applicant'].browse(self.env.context.get('active_id'))
|
||||||
else:
|
else:
|
||||||
if model == 'hr.employee':
|
if model == 'hr.employee':
|
||||||
applicant = self.env['hr.employee'].browse(record_id).applicant_id
|
applicant = self.env['hr.employee'].browse(record_id).applicant_id
|
||||||
|
|
@ -74,14 +74,30 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
context = self.env.context
|
context = self.env.context
|
||||||
active_id = context.get('active_id')
|
active_id = context.get('applicant_id')
|
||||||
model = context.get('active_model')
|
model = context.get('active_model')
|
||||||
|
request_token = False
|
||||||
|
request_upload_url = False
|
||||||
|
|
||||||
if model == 'hr.applicant':
|
if model == 'applicant.request.forms':
|
||||||
applicant = self.env['hr.applicant'].browse(active_id)
|
applicant = self.env['hr.applicant'].browse(active_id)
|
||||||
|
elif model == 'hr.applicant':
|
||||||
|
applicant = self.env['hr.applicant'].browse(context.get('active_id'))
|
||||||
|
elif model == 'hr.employee':
|
||||||
|
applicant = self.env['hr.employee'].browse(active_id).applicant_id
|
||||||
else:
|
else:
|
||||||
if model == 'hr.employee':
|
applicant = self.env['hr.applicant'].browse(active_id)
|
||||||
applicant = self.env['hr.employee'].browse(active_id).applicant_id
|
|
||||||
|
if rec.is_pre_onboarding_attachment_request and not rec.request_form_id:
|
||||||
|
raise UserError("A document request form is required before sending this email.")
|
||||||
|
|
||||||
|
if rec.request_form_id:
|
||||||
|
request_token = rec.request_form_id._issue_new_access_token()
|
||||||
|
base_url = self.get_base_url()
|
||||||
|
request_upload_url = (
|
||||||
|
f"{base_url}/FTPROTECH/DocRequests/"
|
||||||
|
f"{applicant.id}/{rec.request_form_id.id}?token={request_token}"
|
||||||
|
)
|
||||||
|
|
||||||
applicant.recruitment_attachments = [(4, attachment.id) for attachment in rec.req_attachment_ids]
|
applicant.recruitment_attachments = [(4, attachment.id) for attachment in rec.req_attachment_ids]
|
||||||
|
|
||||||
|
|
@ -93,22 +109,29 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
lambda a: a.attachment_type == 'previous_employer').mapped('name')
|
lambda a: a.attachment_type == 'previous_employer').mapped('name')
|
||||||
other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name')
|
other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name')
|
||||||
|
|
||||||
# Prepare context for the template
|
|
||||||
email_context = {
|
email_context = {
|
||||||
|
'applicant_request_form_id': rec.request_form_id.id,
|
||||||
|
'applicant_request_form_token': request_token,
|
||||||
|
'applicant_request_form_url': request_upload_url,
|
||||||
'personal_docs': personal_docs,
|
'personal_docs': personal_docs,
|
||||||
'education_docs': education_docs,
|
'education_docs': education_docs,
|
||||||
'previous_employer_docs': previous_employer_docs,
|
'previous_employer_docs': previous_employer_docs,
|
||||||
'other_docs': other_docs,
|
'other_docs': other_docs,
|
||||||
}
|
}
|
||||||
|
rendered_subject = template.with_context(**email_context)._render_field(
|
||||||
|
'subject', [applicant.id]
|
||||||
|
)[applicant.id]
|
||||||
|
rendered_body_html = template.with_context(**email_context)._render_field(
|
||||||
|
'body_html', [applicant.id], compute_lang=True
|
||||||
|
)[applicant.id]
|
||||||
email_values = {
|
email_values = {
|
||||||
'email_from': rec.email_from,
|
'email_from': rec.email_from,
|
||||||
'email_to': rec.email_to,
|
'email_to': rec.email_to,
|
||||||
'email_cc': rec.email_cc,
|
'email_cc': rec.email_cc,
|
||||||
'subject': rec.email_subject,
|
'subject': rendered_subject or rec.email_subject,
|
||||||
|
'body_html': rendered_body_html,
|
||||||
'attachment_ids': [(6, 0, rec.attachment_ids.ids)],
|
'attachment_ids': [(6, 0, rec.attachment_ids.ids)],
|
||||||
|
|
||||||
}
|
}
|
||||||
# Use 'with_context' to override the email template fields dynamically
|
|
||||||
if rec.send_mail:
|
if rec.send_mail:
|
||||||
template.sudo().with_context(default_body_html=rec.email_body,
|
template.sudo().with_context(default_body_html=rec.email_body,
|
||||||
**email_context).send_mail(applicant.id, email_values=email_values,
|
**email_context).send_mail(applicant.id, email_values=email_values,
|
||||||
|
|
@ -116,7 +139,7 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
base_url = self.get_base_url()
|
base_url = self.get_base_url()
|
||||||
|
|
||||||
if rec.is_pre_onboarding_attachment_request:
|
if rec.is_pre_onboarding_attachment_request:
|
||||||
applicant.doc_requests_form_status = 'email_sent_to_candidate'
|
rec.request_form_id.status = 'email_sent_to_candidate'
|
||||||
else:
|
else:
|
||||||
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
||||||
applicant.joining_form_link = '%s/FTPROTECH/JoiningForm/%s'%(base_url,applicant.id)
|
applicant.joining_form_link = '%s/FTPROTECH/JoiningForm/%s'%(base_url,applicant.id)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
<field name="model">hr.applicant</field>
|
<field name="model">hr.applicant</field>
|
||||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//field[@name='post_onboarding_form_status']" position="after">
|
<xpath expr="//field[@name='candidate_id']" position="after">
|
||||||
<field name="joining_form_link" force_save="1" readonly="1"/>
|
<field name="joining_form_link" force_save="1" readonly="1"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import main
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# from odoo import http
|
||||||
|
# from odoo.http import request
|
||||||
|
#
|
||||||
|
# class MyAPI(http.Controller):
|
||||||
|
#
|
||||||
|
# @http.route('/api/products', type='json', auth='user', methods=['GET'])
|
||||||
|
# def get_products(self):
|
||||||
|
# products = request.env['product.product'].search([])
|
||||||
|
# return [{"id": p.id, "name": p.name} for p in products]
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
|
|
||||||
|
class MobileArchiveController(http.Controller):
|
||||||
|
|
||||||
|
@http.route( '/mobile/archive_record',type='json',auth='user',methods=['POST'],csrf=False)
|
||||||
|
def archive_record(self, model=None, res_ids=None, archive=True):
|
||||||
|
|
||||||
|
if not model or not res_ids:
|
||||||
|
return {
|
||||||
|
'status': False,
|
||||||
|
'message': 'model and res_ids are required'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Ensure list
|
||||||
|
if not isinstance(res_ids, list):
|
||||||
|
return {
|
||||||
|
'status': False,
|
||||||
|
'message': 'res_ids must be a list'
|
||||||
|
}
|
||||||
|
|
||||||
|
records = request.env[model].sudo().browse(res_ids)
|
||||||
|
|
||||||
|
if not records.exists():
|
||||||
|
return {
|
||||||
|
'status': False,
|
||||||
|
'message': 'Records not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check active field exists
|
||||||
|
if 'active' not in records._fields:
|
||||||
|
return {
|
||||||
|
'status': False,
|
||||||
|
'message': 'Archive not supported for this model'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Archive / Unarchive
|
||||||
|
records.write({
|
||||||
|
'active': not archive
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': True,
|
||||||
|
'message': 'Records archived successfully'
|
||||||
|
if archive else
|
||||||
|
'Records unarchived successfully'
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'status': False,
|
||||||
|
'message': str(e)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
'name': 'Grace period Attendance',
|
||||||
|
'version': '18.0.1.0.0',
|
||||||
|
'category': 'Human Resources',
|
||||||
|
'summary': 'Attendances grace Period',
|
||||||
|
'author': 'Srivyn Platforms',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
|
||||||
|
'depends': [
|
||||||
|
'hr',
|
||||||
|
'hr_attendance',
|
||||||
|
'hr_holidays',
|
||||||
|
'resource',
|
||||||
|
'hr_attendance_extended',
|
||||||
|
'roster_management',
|
||||||
|
],
|
||||||
|
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/resource_calendar_weekday_data.xml',
|
||||||
|
'data/sequence.xml',
|
||||||
|
'views/resource_calendar_period.xml',
|
||||||
|
'views/attendance_data.xml',
|
||||||
|
'views/late_coming_request.xml',
|
||||||
|
'views/hr_employee_inherit.xml',
|
||||||
|
'views/late_coming_mail_template.xml',
|
||||||
|
'views/ot_request.xml',
|
||||||
|
'views/ot_mail_template.xml',
|
||||||
|
],
|
||||||
|
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<odoo noupdate="1">
|
||||||
|
|
||||||
|
<record id="weekday_monday"
|
||||||
|
model="resource.calendar.weekday">
|
||||||
|
<field name="name">Monday</field>
|
||||||
|
<field name="code">0</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="weekday_tuesday"
|
||||||
|
model="resource.calendar.weekday">
|
||||||
|
<field name="name">Tuesday</field>
|
||||||
|
<field name="code">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="weekday_wednesday"
|
||||||
|
model="resource.calendar.weekday">
|
||||||
|
<field name="name">Wednesday</field>
|
||||||
|
<field name="code">2</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="weekday_thursday"
|
||||||
|
model="resource.calendar.weekday">
|
||||||
|
<field name="name">Thursday</field>
|
||||||
|
<field name="code">3</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="weekday_friday"
|
||||||
|
model="resource.calendar.weekday">
|
||||||
|
<field name="name">Friday</field>
|
||||||
|
<field name="code">4</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="weekday_saturday"
|
||||||
|
model="resource.calendar.weekday">
|
||||||
|
<field name="name">Saturday</field>
|
||||||
|
<field name="code">5</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="weekday_sunday"
|
||||||
|
model="resource.calendar.weekday">
|
||||||
|
<field name="name">Sunday</field>
|
||||||
|
<field name="code">6</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="seq_late_coming_request"
|
||||||
|
model="ir.sequence">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
Late Coming Request
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="code">
|
||||||
|
late.coming.request
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="prefix">
|
||||||
|
LCR/
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="padding">
|
||||||
|
5
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from . import resource_calendar_period
|
||||||
|
from . import late_coming_request
|
||||||
|
from . import ot_request
|
||||||
|
from . import attendance_data
|
||||||
|
from . import hr_employee_inherit
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,45 @@
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class HREmployee(models.Model):
|
||||||
|
_inherit = 'hr.employee'
|
||||||
|
|
||||||
|
attendance_analytics_count = fields.Integer(
|
||||||
|
string="Attendance Count",
|
||||||
|
compute="_compute_attendance_analytics_count"
|
||||||
|
)
|
||||||
|
attendance_mode = fields.Selection(
|
||||||
|
[
|
||||||
|
('office', 'Office Based'),
|
||||||
|
('remote', 'Remote'),
|
||||||
|
('hybrid', 'Hybrid'),
|
||||||
|
('shift', 'Shift Based'),
|
||||||
|
],
|
||||||
|
string='Attendance Mode',
|
||||||
|
default='office',
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_attendance_analytics_count(self):
|
||||||
|
|
||||||
|
analytics = self.env['attendance.analytics']
|
||||||
|
|
||||||
|
for rec in self:
|
||||||
|
rec.attendance_analytics_count = analytics.search_count([
|
||||||
|
('employee_id', '=', rec.id)
|
||||||
|
])
|
||||||
|
|
||||||
|
def action_open_attendance_analytics(self):
|
||||||
|
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': 'Attendance Sheet',
|
||||||
|
'res_model': 'attendance.analytics',
|
||||||
|
'view_mode': 'list,pivot,graph',
|
||||||
|
'domain': [('employee_id', '=', self.id)],
|
||||||
|
'context': {
|
||||||
|
'search_default_employee_id': self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
from odoo import fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class LateComingRequest(models.Model):
|
||||||
|
_name = 'late.coming.request'
|
||||||
|
_description = 'Late Coming Request'
|
||||||
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
|
_rec_name = 'employee_id'
|
||||||
|
|
||||||
|
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
manager_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
related='employee_id.parent_id',
|
||||||
|
store=True,
|
||||||
|
string="Manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
department_id = fields.Many2one(
|
||||||
|
'hr.department',
|
||||||
|
related='employee_id.department_id',
|
||||||
|
store=True,
|
||||||
|
string="Department"
|
||||||
|
)
|
||||||
|
|
||||||
|
attendance_date = fields.Date(
|
||||||
|
string="Attendance Date"
|
||||||
|
)
|
||||||
|
|
||||||
|
check_in = fields.Datetime(
|
||||||
|
string="Check In"
|
||||||
|
)
|
||||||
|
|
||||||
|
worked_hours = fields.Float(
|
||||||
|
string="Worked Hours"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_check_in = fields.Float(
|
||||||
|
string="Expected Check In"
|
||||||
|
)
|
||||||
|
|
||||||
|
late_minutes = fields.Float(
|
||||||
|
string="Late Minutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
department_grace_period = fields.Integer(
|
||||||
|
string="Department Grace Period"
|
||||||
|
)
|
||||||
|
|
||||||
|
required_checkout_time = fields.Float(
|
||||||
|
string="Required Checkout Time"
|
||||||
|
)
|
||||||
|
|
||||||
|
compensation_pending = fields.Boolean(
|
||||||
|
string="Compensation Pending"
|
||||||
|
)
|
||||||
|
|
||||||
|
compensation_minutes = fields.Float(
|
||||||
|
string="Compensation Minutes"
|
||||||
|
)
|
||||||
|
|
||||||
|
reason = fields.Text(
|
||||||
|
string="Reason"
|
||||||
|
)
|
||||||
|
|
||||||
|
manager_comment = fields.Text(
|
||||||
|
string="Manager Comment"
|
||||||
|
)
|
||||||
|
|
||||||
|
status_message = fields.Char(
|
||||||
|
string="Status"
|
||||||
|
)
|
||||||
|
|
||||||
|
state = fields.Selection([
|
||||||
|
|
||||||
|
('draft', 'Draft'),
|
||||||
|
|
||||||
|
('submitted', 'Submitted'),
|
||||||
|
|
||||||
|
('approved', 'Approved'),
|
||||||
|
|
||||||
|
('rejected', 'Rejected')
|
||||||
|
|
||||||
|
],
|
||||||
|
string="Status",
|
||||||
|
default='draft'
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_submit(self):
|
||||||
|
|
||||||
|
for rec in self:
|
||||||
|
|
||||||
|
if not rec.reason:
|
||||||
|
raise UserError(
|
||||||
|
_("Please enter reason.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if not rec.manager_id:
|
||||||
|
raise UserError(
|
||||||
|
_("Manager not configured.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if not rec.manager_id.work_email:
|
||||||
|
raise UserError(
|
||||||
|
_("Manager work email not configured.")
|
||||||
|
)
|
||||||
|
|
||||||
|
rec.state = 'submitted'
|
||||||
|
|
||||||
|
template = self.env.ref(
|
||||||
|
'grace_period.email_template_late_coming_request'
|
||||||
|
)
|
||||||
|
|
||||||
|
template.send_mail(
|
||||||
|
rec.id,
|
||||||
|
force_send=True
|
||||||
|
)
|
||||||
|
rec.message_post(
|
||||||
|
body=_("Request Submitted.")
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_approve(self):
|
||||||
|
|
||||||
|
for rec in self:
|
||||||
|
|
||||||
|
if (
|
||||||
|
rec.manager_id.user_id != self.env.user
|
||||||
|
and not self.env.user.has_group(
|
||||||
|
'hr.group_hr_manager'
|
||||||
|
)
|
||||||
|
):
|
||||||
|
|
||||||
|
raise UserError(
|
||||||
|
_("Only Manager or HR can approve.")
|
||||||
|
)
|
||||||
|
|
||||||
|
rec.state = 'approved'
|
||||||
|
|
||||||
|
def action_reject(self):
|
||||||
|
|
||||||
|
for rec in self:
|
||||||
|
|
||||||
|
if (
|
||||||
|
rec.manager_id.user_id != self.env.user
|
||||||
|
and not self.env.user.has_group(
|
||||||
|
'hr.group_hr_manager'
|
||||||
|
)
|
||||||
|
):
|
||||||
|
|
||||||
|
raise UserError(
|
||||||
|
_("Only Manager or HR can reject.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if not rec.employee_id.work_email:
|
||||||
|
|
||||||
|
raise UserError(
|
||||||
|
_("Employee work email not configured.")
|
||||||
|
)
|
||||||
|
|
||||||
|
rec.state = 'rejected'
|
||||||
|
|
||||||
|
template = self.env.ref(
|
||||||
|
'grace_period.email_template_late_coming_rejected'
|
||||||
|
)
|
||||||
|
|
||||||
|
template.send_mail(
|
||||||
|
rec.id,
|
||||||
|
force_send=True
|
||||||
|
)
|
||||||
|
rec.message_post(
|
||||||
|
body=_("Request Rejected.")
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_reset_to_draft(self):
|
||||||
|
|
||||||
|
self.state = 'draft'
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
from odoo import fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class OvertimeRequest(models.Model):
|
||||||
|
_name = 'overtime.request'
|
||||||
|
_description = 'Overtime Request'
|
||||||
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
|
_rec_name = 'employee_id'
|
||||||
|
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
required=True,
|
||||||
|
tracking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
department_id = fields.Many2one(
|
||||||
|
'hr.department',
|
||||||
|
related='employee_id.department_id',
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
manager_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
related='employee_id.parent_id',
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
attendance_date = fields.Date(
|
||||||
|
tracking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
check_in = fields.Datetime()
|
||||||
|
|
||||||
|
check_out = fields.Datetime()
|
||||||
|
|
||||||
|
worked_hours = fields.Float()
|
||||||
|
|
||||||
|
hours_per_day = fields.Float()
|
||||||
|
|
||||||
|
allowed_ot_limit = fields.Float()
|
||||||
|
|
||||||
|
overtime_hours = fields.Float(
|
||||||
|
tracking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
reason = fields.Text(
|
||||||
|
tracking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
state = fields.Selection([
|
||||||
|
|
||||||
|
('draft', 'Draft'),
|
||||||
|
|
||||||
|
('submitted', 'Submitted'),
|
||||||
|
|
||||||
|
('approved', 'Approved'),
|
||||||
|
|
||||||
|
('rejected', 'Rejected')
|
||||||
|
|
||||||
|
], default='draft', tracking=True)
|
||||||
|
|
||||||
|
def action_submit(self):
|
||||||
|
|
||||||
|
for rec in self:
|
||||||
|
|
||||||
|
if not rec.reason:
|
||||||
|
raise UserError(
|
||||||
|
_("Please enter reason.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if not rec.manager_id:
|
||||||
|
raise UserError(
|
||||||
|
_("Manager not configured.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if not rec.manager_id.work_email:
|
||||||
|
raise UserError(
|
||||||
|
_("Manager email not configured.")
|
||||||
|
)
|
||||||
|
|
||||||
|
rec.state = 'submitted'
|
||||||
|
|
||||||
|
template = self.env.ref(
|
||||||
|
'grace_period.email_template_ot_request'
|
||||||
|
)
|
||||||
|
|
||||||
|
template.send_mail(
|
||||||
|
rec.id,
|
||||||
|
force_send=True
|
||||||
|
)
|
||||||
|
|
||||||
|
rec.message_post(
|
||||||
|
body=_(
|
||||||
|
"OT Request Submitted"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_approve(self):
|
||||||
|
|
||||||
|
for rec in self:
|
||||||
|
|
||||||
|
if (
|
||||||
|
rec.manager_id.user_id != self.env.user
|
||||||
|
and not self.env.user.has_group(
|
||||||
|
'hr.group_hr_manager'
|
||||||
|
)
|
||||||
|
):
|
||||||
|
|
||||||
|
raise UserError(
|
||||||
|
_("Only Manager or HR can approve.")
|
||||||
|
)
|
||||||
|
|
||||||
|
rec.state = 'approved'
|
||||||
|
|
||||||
|
rec.message_post(
|
||||||
|
body=_("Overtime Request Approved.")
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_reject(self):
|
||||||
|
|
||||||
|
for rec in self:
|
||||||
|
|
||||||
|
if (
|
||||||
|
rec.manager_id.user_id != self.env.user
|
||||||
|
and not self.env.user.has_group(
|
||||||
|
'hr.group_hr_manager'
|
||||||
|
)
|
||||||
|
):
|
||||||
|
|
||||||
|
raise UserError(
|
||||||
|
_("Only Manager or HR can reject.")
|
||||||
|
)
|
||||||
|
|
||||||
|
rec.state = 'rejected'
|
||||||
|
|
||||||
|
template = self.env.ref(
|
||||||
|
'grace_period.email_template_ot_rejected'
|
||||||
|
)
|
||||||
|
|
||||||
|
template.send_mail(
|
||||||
|
rec.id,
|
||||||
|
force_send=True
|
||||||
|
)
|
||||||
|
|
||||||
|
rec.message_post(
|
||||||
|
body=_("Overtime Request Rejected.")
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_reset_to_draft(self):
|
||||||
|
|
||||||
|
self.state = 'draft'
|
||||||
|
|
||||||
|
self.message_post(
|
||||||
|
body=_(
|
||||||
|
"Reset to Draft"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
from odoo import api, fields, models, tools
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceCalendar(models.Model):
|
||||||
|
_inherit = 'resource.calendar'
|
||||||
|
|
||||||
|
weekday_ids = fields.Many2many(
|
||||||
|
'resource.calendar.weekday',
|
||||||
|
'resource_calendar_weekday_rel',
|
||||||
|
'calendar_id',
|
||||||
|
'weekday_id',
|
||||||
|
string="Weekdays"
|
||||||
|
)
|
||||||
|
|
||||||
|
weekend_ids = fields.Many2many(
|
||||||
|
'resource.calendar.weekday',
|
||||||
|
'resource_calendar_weekend_rel',
|
||||||
|
'calendar_id',
|
||||||
|
'weekday_id',
|
||||||
|
string="Weekends"
|
||||||
|
)
|
||||||
|
|
||||||
|
shift_start_time = fields.Float(string="Start Time")
|
||||||
|
shift_end_time = fields.Float(string="End Time")
|
||||||
|
late_grace_period = fields.Integer(default=15)
|
||||||
|
early_out_grace_period = fields.Integer(default=0)
|
||||||
|
over_time_hrs = fields.Float(default=2)
|
||||||
|
department_grace_ids = fields.One2many(
|
||||||
|
'resource.calendar.department.grace',
|
||||||
|
'calendar_id',
|
||||||
|
string="Department Grace Periods"
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_generate_schedule(self):
|
||||||
|
for rec in self:
|
||||||
|
|
||||||
|
attendance_commands = [(5, 0, 0)]
|
||||||
|
|
||||||
|
added_days = []
|
||||||
|
|
||||||
|
for day in rec.weekday_ids:
|
||||||
|
|
||||||
|
# Avoid duplicate weekdays
|
||||||
|
if day.code in added_days:
|
||||||
|
continue
|
||||||
|
|
||||||
|
added_days.append(day.code)
|
||||||
|
|
||||||
|
attendance_commands.append((0, 0, {
|
||||||
|
'name': day.name,
|
||||||
|
'dayofweek': day.code,
|
||||||
|
'hour_from': rec.shift_start_time,
|
||||||
|
'hour_to': rec.shift_end_time,
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Single write operation
|
||||||
|
rec.write({
|
||||||
|
'attendance_ids': attendance_commands
|
||||||
|
})
|
||||||
|
|
||||||
|
# ------------------------------------
|
||||||
|
# Department Grace Period Generation
|
||||||
|
# ------------------------------------
|
||||||
|
|
||||||
|
department_commands = [(5, 0, 0)]
|
||||||
|
|
||||||
|
departments = self.env['hr.department'].search([])
|
||||||
|
|
||||||
|
for dept in departments:
|
||||||
|
department_commands.append((0, 0, {
|
||||||
|
'department_id': dept.id,
|
||||||
|
'grace_period': 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
rec.write({
|
||||||
|
'department_grace_ids': department_commands
|
||||||
|
})
|
||||||
|
class ResourceCalendarWeekday(models.Model):
|
||||||
|
_name = 'resource.calendar.weekday'
|
||||||
|
_description = 'Weekday Master'
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
|
||||||
|
code = fields.Selection([
|
||||||
|
('0', 'Monday'),
|
||||||
|
('1', 'Tuesday'),
|
||||||
|
('2', 'Wednesday'),
|
||||||
|
('3', 'Thursday'),
|
||||||
|
('4', 'Friday'),
|
||||||
|
('5', 'Saturday'),
|
||||||
|
('6', 'Sunday'),
|
||||||
|
], required=True)
|
||||||
|
|
||||||
|
class ResourceCalendarDepartmentGrace(models.Model):
|
||||||
|
_name = 'resource.calendar.department.grace'
|
||||||
|
_description = 'Department Wise Grace Period'
|
||||||
|
|
||||||
|
calendar_id = fields.Many2one(
|
||||||
|
'resource.calendar',
|
||||||
|
string="Working Schedule",
|
||||||
|
ondelete='cascade'
|
||||||
|
)
|
||||||
|
|
||||||
|
department_id = fields.Many2one(
|
||||||
|
'hr.department',
|
||||||
|
string="Department",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
grace_period = fields.Integer(
|
||||||
|
string="Grace Period (Minutes)",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_resource_calendar_weekday, resource_calendar_weekday,model_resource_calendar_weekday,base.group_user,1,1,1,1
|
||||||
|
access_attendance_analytics_user,attendance.analytics.user,model_attendance_analytics,base.group_user,1,0,0,0
|
||||||
|
access_attendance_analytics_manager,attendance.analytics.manager,model_attendance_analytics,hr.group_hr_manager,1,1,1,0
|
||||||
|
access_attendance_analytics_admin,attendance.analytics.admin,model_attendance_analytics,base.group_system,1,1,1,1
|
||||||
|
access_late_coming_request,attendance_late_coming_request,model_late_coming_request,base.group_user,1,1,1,1
|
||||||
|
access_resource_calendar_department_grace,resource_calendar_department_grace,model_resource_calendar_department_grace,base.group_user,1,1,1,1
|
||||||
|
access_overtime_request,overtime_request,model_overtime_request,base.group_user,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- ===================================================== -->
|
||||||
|
<!-- EMPLOYEE: Can see only own attendance -->
|
||||||
|
<!-- ===================================================== -->
|
||||||
|
|
||||||
|
<record id="attendance_analytics_employee_rule"
|
||||||
|
model="ir.rule">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
Attendance Analytics - Employee Own Records
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model_id"
|
||||||
|
ref="model_attendance_analytics"/>
|
||||||
|
|
||||||
|
<field name="domain_force">
|
||||||
|
[('employee_id.user_id', '=', user.id)]
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="groups"
|
||||||
|
eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ===================================================== -->
|
||||||
|
<!-- MANAGER: Can see subordinate employees -->
|
||||||
|
<!-- ===================================================== -->
|
||||||
|
|
||||||
|
<record id="attendance_analytics_manager_rule"
|
||||||
|
model="ir.rule">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
Attendance Analytics - Manager Records
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model_id"
|
||||||
|
ref="model_attendance_analytics"/>
|
||||||
|
|
||||||
|
<field name="domain_force">
|
||||||
|
|
||||||
|
[
|
||||||
|
'|',
|
||||||
|
|
||||||
|
('employee_id.user_id', '=', user.id),
|
||||||
|
|
||||||
|
('employee_id.parent_id.user_id', '=', user.id)
|
||||||
|
]
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="groups"
|
||||||
|
eval="[(4, ref('your_module_name.group_attendance_analytics_manager'))]"/>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ===================================================== -->
|
||||||
|
<!-- ADMIN: Full Access -->
|
||||||
|
<!-- ===================================================== -->
|
||||||
|
|
||||||
|
<record id="attendance_analytics_admin_rule"
|
||||||
|
model="ir.rule">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
Attendance Analytics - Admin Full Access
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model_id"
|
||||||
|
ref="model_attendance_analytics"/>
|
||||||
|
|
||||||
|
<field name="domain_force">
|
||||||
|
[(1, '=', 1)]
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="groups"
|
||||||
|
eval="[(4, ref('hr.group_hr_manager'))]"/>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ================================================= -->
|
||||||
|
<!-- EMPLOYEE -->
|
||||||
|
<!-- ================================================= -->
|
||||||
|
|
||||||
|
<record id="late_coming_request_employee_rule"
|
||||||
|
model="ir.rule">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
Late Coming Request Employee Rule
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model_id"
|
||||||
|
ref="model_late_coming_request"/>
|
||||||
|
|
||||||
|
<field name="domain_force">
|
||||||
|
[('employee_id.user_id', '=', user.id)]
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="groups"
|
||||||
|
eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
<record id="late_coming_request_manager_rule"
|
||||||
|
model="ir.rule">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
Late Coming Request Manager Rule
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model_id"
|
||||||
|
ref="model_late_coming_request"/>
|
||||||
|
|
||||||
|
<field name="domain_force">
|
||||||
|
|
||||||
|
[
|
||||||
|
'|',
|
||||||
|
('employee_id.user_id', '=', user.id),
|
||||||
|
('employee_id.parent_id.user_id', '=', user.id)
|
||||||
|
]
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="groups"
|
||||||
|
eval="[(4, ref('your_module.group_attendance_analytics_manager'))]"/>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ================================================= -->
|
||||||
|
<!-- HR MANAGER FULL ACCESS -->
|
||||||
|
<!-- ================================================= -->
|
||||||
|
|
||||||
|
<record id="late_coming_request_hr_rule"
|
||||||
|
model="ir.rule">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
Late Coming Request HR Rule
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model_id"
|
||||||
|
ref="model_late_coming_request"/>
|
||||||
|
|
||||||
|
<field name="domain_force">
|
||||||
|
[(1, '=', 1)]
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="groups"
|
||||||
|
eval="[(4, ref('hr.group_hr_manager'))]"/>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- =============================================== -->
|
||||||
|
<!-- TREE VIEW -->
|
||||||
|
<!-- =============================================== -->
|
||||||
|
<record id="view_attendance_analytics_list" model="ir.ui.view">
|
||||||
|
<field name="name">attendance.analytics.list</field>
|
||||||
|
<field name="model">attendance.analytics</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Attendance Analytics">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="min_check_in"/>
|
||||||
|
<field name="max_check_out"/>
|
||||||
|
<field name="worked_hours"/>
|
||||||
|
<field name="department_grace_period"/>
|
||||||
|
<field name="late_time"/>
|
||||||
|
<field name="early_out_time"/>
|
||||||
|
<field name="required_checkout_time" widget="float_time"/>
|
||||||
|
<field name="is_late"/>
|
||||||
|
<field name="is_early_out"/>
|
||||||
|
<field name="is_compensation_pending"/>
|
||||||
|
<field name="late_approved"/>
|
||||||
|
<field name="shift_id"/>
|
||||||
|
<!-- <field name="shift_name"/>-->
|
||||||
|
<field name="status"
|
||||||
|
widget="badge"
|
||||||
|
decoration-success="status == 'present'"
|
||||||
|
decoration-danger="status == 'absent'"
|
||||||
|
decoration-warning="status == 'late_in'"
|
||||||
|
decoration-info="status == 'half_day'"
|
||||||
|
decoration-primary="status == 'holiday'"/>
|
||||||
|
<button name="action_create_late_request"
|
||||||
|
type="object"
|
||||||
|
string="Request Approval"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="
|
||||||
|
is_holiday == True
|
||||||
|
or is_week_off == True
|
||||||
|
or late_approved == True
|
||||||
|
or (
|
||||||
|
late_minutes == 0
|
||||||
|
and early_out_minutes == 0
|
||||||
|
)
|
||||||
|
"/>
|
||||||
|
<button name="action_create_ot_request"
|
||||||
|
string="Apply OT"
|
||||||
|
type="object"
|
||||||
|
class="btn-warning"
|
||||||
|
invisible="ot_eligible == False"/>
|
||||||
|
<button name="action_create_shiftswap_request"
|
||||||
|
string="Swift Swap Request"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- =============================================== -->
|
||||||
|
<!-- CALENDAR VIEW -->
|
||||||
|
<!-- =============================================== -->
|
||||||
|
|
||||||
|
<record id="view_attendance_analytics_calendar" model="ir.ui.view">
|
||||||
|
<field name="name">attendance.analytics.calendar</field>
|
||||||
|
<field name="model">attendance.analytics</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<calendar string="Attendance Calendar"
|
||||||
|
date_start="date"
|
||||||
|
date_stop="date_end"
|
||||||
|
color="color"
|
||||||
|
mode="month"
|
||||||
|
quick_create="false"
|
||||||
|
event_open_popup="true">
|
||||||
|
<field name="display_label"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
</calendar>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- =============================================== -->
|
||||||
|
<!-- SEARCH VIEW -->
|
||||||
|
<!-- =============================================== -->
|
||||||
|
|
||||||
|
<record id="view_attendance_analytics_search" model="ir.ui.view">
|
||||||
|
<field name="name">attendance.analytics.search</field>
|
||||||
|
<field name="model">attendance.analytics</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Attendance Analytics">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="date"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Late"
|
||||||
|
name="late"
|
||||||
|
domain="[('is_late','=',True)]"/>
|
||||||
|
<filter string="Early Out"
|
||||||
|
name="early_out"
|
||||||
|
domain="[('is_early_out','=',True)]"/>
|
||||||
|
<filter string="Compensation Pending"
|
||||||
|
name="compensation_pending"
|
||||||
|
domain="[('is_compensation_pending','=',True)]"/>
|
||||||
|
<filter string="Holiday"
|
||||||
|
name="holiday"
|
||||||
|
domain="[('is_holiday','=',True)]"/>
|
||||||
|
<filter string="Week Off"
|
||||||
|
name="week_off"
|
||||||
|
domain="[('is_week_off','=',True)]"/>
|
||||||
|
<group expand="0"
|
||||||
|
string="Group By">
|
||||||
|
<filter string="Employee"
|
||||||
|
name="group_employee"
|
||||||
|
context="{'group_by':'employee_id'}"/>
|
||||||
|
<filter string="Department"
|
||||||
|
name="group_department"
|
||||||
|
context="{'group_by':'department_id'}"/>
|
||||||
|
<filter string="Date"
|
||||||
|
name="group_date"
|
||||||
|
context="{'group_by':'date'}"/>
|
||||||
|
<filter string="Status"
|
||||||
|
name="group_status"
|
||||||
|
context="{'group_by':'status'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="action_attendance_analytics" model="ir.actions.act_window">
|
||||||
|
<field name="name">Attendance Analytics</field>
|
||||||
|
<field name="res_model">attendance.analytics</field>
|
||||||
|
<field name="view_mode">list,calendar</field>
|
||||||
|
<field name="domain">[ '|', ('employee_id.user_id', '=', uid), ('employee_id.parent_id.user_id', '=', uid) ]
|
||||||
|
</field>
|
||||||
|
<field name="search_view_id" ref="view_attendance_analytics_search"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_attendance_analytics"
|
||||||
|
name="Attendance Data"
|
||||||
|
parent="hr_attendance.menu_hr_attendance_root"
|
||||||
|
action="action_attendance_analytics"
|
||||||
|
sequence="50"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_employee_form_inherit_attendance_analytics"
|
||||||
|
model="ir.ui.view">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
hr.employee.form.attendance.analytics
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model">hr.employee</field>
|
||||||
|
|
||||||
|
<field name="inherit_id"
|
||||||
|
ref="hr.view_employee_form"/>
|
||||||
|
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<xpath expr="//div[@name='button_box']"
|
||||||
|
position="inside">
|
||||||
|
|
||||||
|
<button name="action_open_attendance_analytics"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-calendar">
|
||||||
|
|
||||||
|
<field name="attendance_analytics_count"
|
||||||
|
string="Attendance"
|
||||||
|
widget="statinfo"/>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<xpath expr="//field[@name='department_id']"
|
||||||
|
position="after">
|
||||||
|
|
||||||
|
<field name="attendance_mode"/>
|
||||||
|
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
|
||||||
|
</record>
|
||||||
|
<record id="view_employee_filter_inherit_attendance_mode"
|
||||||
|
model="ir.ui.view">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
hr.employee.search.attendance.mode
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model">hr.employee</field>
|
||||||
|
|
||||||
|
<field name="inherit_id"
|
||||||
|
ref="hr.view_employee_filter"/>
|
||||||
|
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<xpath expr="//search" position="inside">
|
||||||
|
|
||||||
|
<filter string="Office"
|
||||||
|
name="office_employees"
|
||||||
|
domain="[('attendance_mode','=','office')]"/>
|
||||||
|
|
||||||
|
<filter string="Remote"
|
||||||
|
name="remote_employees"
|
||||||
|
domain="[('attendance_mode','=','remote')]"/>
|
||||||
|
|
||||||
|
<filter string="Hybrid"
|
||||||
|
name="hybrid_employees"
|
||||||
|
domain="[('attendance_mode','=','hybrid')]"/>
|
||||||
|
|
||||||
|
<filter string="Shift"
|
||||||
|
name="shift_employees"
|
||||||
|
domain="[('attendance_mode','=','shift')]"/>
|
||||||
|
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,447 @@
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="email_template_late_coming_request"
|
||||||
|
model="mail.template">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
Late Coming Request Submitted
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model_id"
|
||||||
|
ref="model_late_coming_request"/>
|
||||||
|
|
||||||
|
<field name="subject">
|
||||||
|
Late Coming Request - {{ object.employee_id.name }}
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="email_from">
|
||||||
|
{{ user.email }}
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="email_to">
|
||||||
|
{{ object.manager_id.work_email }}
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
|
||||||
|
<div style="font-family: Arial, sans-serif;">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Hello
|
||||||
|
<strong t-out="object.manager_id.name"/>,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Employee
|
||||||
|
<strong t-out="object.employee_id.name"/>
|
||||||
|
has submitted a Late Coming / Compensation Request.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table border="1"
|
||||||
|
cellpadding="6"
|
||||||
|
cellspacing="0"
|
||||||
|
style="border-collapse: collapse;">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Employee</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.employee_id.name"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Department</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.department_id.name"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Date</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.attendance_date"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Check In</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.check_in"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Late Minutes</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.late_minutes"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr t-if="object.department_grace_period">
|
||||||
|
<td>
|
||||||
|
<strong>Department Grace</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.department_grace_period"/>
|
||||||
|
Minutes
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr t-if="object.compensation_pending">
|
||||||
|
<td>
|
||||||
|
<strong>Compensation Required</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Yes
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr t-if="object.compensation_minutes">
|
||||||
|
<td>
|
||||||
|
<strong>Pending Minutes</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.compensation_minutes"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr t-if="object.required_checkout_time">
|
||||||
|
<td>
|
||||||
|
<strong>Required Checkout Time</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.required_checkout_time"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr t-if="object.status_message">
|
||||||
|
<td>
|
||||||
|
<strong>Status</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.status_message"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Reason:</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p t-if="object.reason">
|
||||||
|
<span t-out="object.reason"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p t-if="not object.reason">
|
||||||
|
No reason provided.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Please review and take appropriate action.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Thank You
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
<record id="email_template_late_coming_rejected"
|
||||||
|
model="mail.template">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
Late Coming Request Rejected
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model_id"
|
||||||
|
ref="model_late_coming_request"/>
|
||||||
|
|
||||||
|
<field name="subject">
|
||||||
|
Your Late Coming Request was Rejected
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="email_from">
|
||||||
|
{{ user.email }}
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="email_to">
|
||||||
|
{{ object.employee_id.work_email }}
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
|
||||||
|
<div style="font-family: Arial, sans-serif;">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Hello
|
||||||
|
<strong t-out="object.employee_id.name"/>,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Your Late Coming / Compensation Request has been
|
||||||
|
<strong style="color: red;">
|
||||||
|
Rejected
|
||||||
|
</strong>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table border="1"
|
||||||
|
cellpadding="6"
|
||||||
|
cellspacing="0"
|
||||||
|
style="border-collapse: collapse;">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Employee</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.employee_id.name"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Department</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.department_id.name"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Attendance Date</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.attendance_date"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Manager</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.manager_id.name"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Late Minutes</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.late_minutes"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr t-if="object.department_grace_period">
|
||||||
|
<td>
|
||||||
|
<strong>Department Grace</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.department_grace_period"/>
|
||||||
|
Minutes
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr t-if="object.compensation_pending">
|
||||||
|
<td>
|
||||||
|
<strong>Compensation Required</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Yes
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr t-if="object.compensation_minutes">
|
||||||
|
<td>
|
||||||
|
<strong>Pending Minutes</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.compensation_minutes"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr t-if="object.required_checkout_time">
|
||||||
|
<td>
|
||||||
|
<strong>Required Checkout Time</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.required_checkout_time"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr t-if="object.status_message">
|
||||||
|
<td>
|
||||||
|
<strong>Status</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.status_message"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Reason Submitted:</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p t-if="object.reason">
|
||||||
|
<span t-out="object.reason"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p t-if="not object.reason">
|
||||||
|
No reason provided.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Please contact your manager or HR for further clarification.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Thank You
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="email_template_absent_alert"
|
||||||
|
model="mail.template">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
Attendance Missing Alert
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model_id"
|
||||||
|
ref="model_attendance_analytics"/>
|
||||||
|
|
||||||
|
<field name="subject">
|
||||||
|
Attendance Punch Missing
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="email_from">
|
||||||
|
{{ user.email }}
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="email_to">
|
||||||
|
{{ object.employee_id.work_email }}
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
|
||||||
|
<div style="font-family: Arial, sans-serif;">
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Hello
|
||||||
|
<strong t-out="object.employee_id.name"/>,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Your attendance punch has not been recorded.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table border="1"
|
||||||
|
cellpadding="6"
|
||||||
|
cellspacing="0"
|
||||||
|
style="border-collapse: collapse;">
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Employee</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.employee_id.name"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Department</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.department_id.name"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Date</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span t-out="object.date"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Status</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Absent / No Attendance Recorded
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Please login your attendance immediately
|
||||||
|
or contact HR/Admin if already marked.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Thank You
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="ir_cron_send_absent_alert" model="ir.cron">
|
||||||
|
<field name="name">Send Absent Attendance Alert</field>
|
||||||
|
<field name="model_id" ref="model_attendance_analytics"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.action_send_absent_mail()</field>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="active">True</field>
|
||||||
|
<field name="nextcall"
|
||||||
|
eval="DateTime.now().replace(hour=10, minute=0, second=0)"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
<!-- ============================================= -->
|
||||||
|
<!-- LIST VIEW -->
|
||||||
|
<!-- ============================================= -->
|
||||||
|
<record id="view_late_coming_request_list" model="ir.ui.view">
|
||||||
|
<field name="name">late.coming.request.list</field>
|
||||||
|
<field name="model">late.coming.request</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="attendance_date"/>
|
||||||
|
<field name="late_minutes"/>
|
||||||
|
<field name="department_grace_period"/>
|
||||||
|
<field name="compensation_pending"/>
|
||||||
|
<field name="compensation_minutes"/>
|
||||||
|
<field name="state"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<!-- ============================================= -->
|
||||||
|
<!-- FORM VIEW -->
|
||||||
|
<!-- ============================================= -->
|
||||||
|
<record id="view_late_coming_request_form" model="ir.ui.view">
|
||||||
|
<field name="name">late.coming.request.form</field>
|
||||||
|
<field name="model">late.coming.request</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<form string="Permission Request">
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<button name="action_reset_to_draft"
|
||||||
|
string="Reset To Draft"
|
||||||
|
type="object"
|
||||||
|
invisible="state == 'draft' or state == 'approved'"
|
||||||
|
class="btn-secondary"/>
|
||||||
|
|
||||||
|
<button name="action_submit"
|
||||||
|
string="Submit"
|
||||||
|
type="object"
|
||||||
|
invisible="state != 'draft'"
|
||||||
|
class="btn-primary"/>
|
||||||
|
|
||||||
|
<button name="action_approve"
|
||||||
|
string="Approve"
|
||||||
|
type="object"
|
||||||
|
invisible="state != 'submitted'"
|
||||||
|
class="btn-success"/>
|
||||||
|
|
||||||
|
<button name="action_reject"
|
||||||
|
string="Reject"
|
||||||
|
type="object"
|
||||||
|
invisible="state != 'submitted'"
|
||||||
|
class="btn-danger"/>
|
||||||
|
<field name="state"
|
||||||
|
widget="statusbar"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="manager_id"/>
|
||||||
|
<field name="attendance_date"/>
|
||||||
|
<field name="worked_hours"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="late_minutes"/>
|
||||||
|
<field name="compensation_minutes"/>
|
||||||
|
<field name="department_grace_period"/>
|
||||||
|
<field name="required_checkout_time"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="status_message"
|
||||||
|
readonly="1"/>
|
||||||
|
<field name="reason"
|
||||||
|
placeholder="Enter reason here...Forgot punch/Client meeting/System issue/Travel/on-duty"
|
||||||
|
readonly="state in ('approved','rejected')"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="action_late_coming_request" model="ir.actions.act_window">
|
||||||
|
<field name="name">Permission Requests</field>
|
||||||
|
<field name="res_model">late.coming.request</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="domain">
|
||||||
|
[
|
||||||
|
'|',
|
||||||
|
('employee_id.user_id', '=', uid),
|
||||||
|
('employee_id.parent_id.user_id', '=', uid)
|
||||||
|
]
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<menuitem id="menu_late_coming_request"
|
||||||
|
name="Permission Requests"
|
||||||
|
parent="hr_attendance.menu_hr_attendance_root"
|
||||||
|
action="action_late_coming_request"
|
||||||
|
sequence="30"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="email_template_ot_request" model="mail.template">
|
||||||
|
<field name="name">Overtime Request Submitted</field>
|
||||||
|
<field name="model_id" ref="model_overtime_request"/>
|
||||||
|
<field name="subject">Overtime Request - {{ object.employee_id.name }}</field>
|
||||||
|
<field name="email_from">{{ user.email }}</field>
|
||||||
|
<field name="email_to">{{ object.manager_id.work_email }}</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Hello,
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Employee
|
||||||
|
<strong>
|
||||||
|
<t t-out="object.employee_id.name"/>
|
||||||
|
</strong>
|
||||||
|
submitted an Overtime Request.
|
||||||
|
</p>
|
||||||
|
<table border="1"
|
||||||
|
cellpadding="5"
|
||||||
|
cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Date</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<t t-out="object.attendance_date"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>Worked Hours</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<t t-out="object.worked_hours"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>OT Hours</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<t t-out="object.overtime_hours"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<br/>
|
||||||
|
<p>
|
||||||
|
<strong>Reason:</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<t t-out="object.reason"/>
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<p>
|
||||||
|
Please review the request.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="email_template_ot_rejected" model="mail.template">
|
||||||
|
<field name="name">Overtime Request Rejected</field>
|
||||||
|
<field name="model_id" ref="model_overtime_request"/>
|
||||||
|
<field name="subject">Overtime Request Rejected</field>
|
||||||
|
<field name="email_from">{{ user.email }}</field>
|
||||||
|
<field name="email_to">{{ object.employee_id.work_email }}</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Hello
|
||||||
|
<strong>
|
||||||
|
<t t-out="object.employee_id.name"/>
|
||||||
|
</strong>
|
||||||
|
,
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Your Overtime Request for
|
||||||
|
<strong>
|
||||||
|
<t t-out="object.attendance_date"/>
|
||||||
|
</strong>
|
||||||
|
has been
|
||||||
|
<strong style="color:red;">
|
||||||
|
Rejected
|
||||||
|
</strong>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Manager:</strong>
|
||||||
|
<t t-out="object.manager_id.name"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>OT Hours:</strong>
|
||||||
|
|
||||||
|
<t t-out="object.overtime_hours"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Reason Submitted:</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<t t-out="object.reason"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Please contact your manager.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Thank You
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_overtime_request_form" model="ir.ui.view">
|
||||||
|
<field name="name">overtime.request.form</field>
|
||||||
|
<field name="model">overtime.request</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="OT Request">
|
||||||
|
<header>
|
||||||
|
<button name="action_submit"
|
||||||
|
string="Submit"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="state != 'draft'"/>
|
||||||
|
|
||||||
|
<button name="action_approve"
|
||||||
|
string="Approve"
|
||||||
|
type="object"
|
||||||
|
class="btn-success"
|
||||||
|
invisible="state != 'submitted'"/>
|
||||||
|
|
||||||
|
<button name="action_reject"
|
||||||
|
string="Reject"
|
||||||
|
type="object"
|
||||||
|
class="btn-danger"
|
||||||
|
invisible="state != 'submitted'"/>
|
||||||
|
|
||||||
|
<button name="action_reset_to_draft"
|
||||||
|
string="Reset to Draft"
|
||||||
|
type="object"
|
||||||
|
invisible="state == 'draft'"/>
|
||||||
|
|
||||||
|
<field name="state"
|
||||||
|
widget="statusbar"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="attendance_date"/>
|
||||||
|
<field name="worked_hours"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="hours_per_day"/>
|
||||||
|
<field name="allowed_ot_limit"/>
|
||||||
|
<field name="overtime_hours"/>
|
||||||
|
<field name="check_in"/>
|
||||||
|
<field name="check_out"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="reason"
|
||||||
|
placeholder="Enter OT Reason..."
|
||||||
|
readonly="state in ('approved','rejected')"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
|
||||||
|
<chatter/>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
<record id="view_overtime_request_list" model="ir.ui.view">
|
||||||
|
<field name="name">overtime.request.list</field>
|
||||||
|
<field name="model">overtime.request</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="OT Request">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="attendance_date"/>
|
||||||
|
<field name="worked_hours"/>
|
||||||
|
<field name="hours_per_day"/>
|
||||||
|
<field name="overtime_hours"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="action_overtime_request" model="ir.actions.act_window">
|
||||||
|
<field name="name">Overtime Requests</field>
|
||||||
|
<field name="res_model">overtime.request</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="domain">
|
||||||
|
[
|
||||||
|
'|',
|
||||||
|
('employee_id.user_id', '=', uid),
|
||||||
|
('employee_id.parent_id.user_id', '=', uid)
|
||||||
|
]
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_overtime_request"
|
||||||
|
name="Overtime Requests"
|
||||||
|
parent="hr_attendance.menu_hr_attendance_root"
|
||||||
|
action="action_overtime_request"
|
||||||
|
sequence="35"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_resource_calendar_form_inherit_grace_period"
|
||||||
|
model="ir.ui.view">
|
||||||
|
|
||||||
|
<field name="name">
|
||||||
|
resource.calendar.form.inherit.grace.period
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="model">resource.calendar</field>
|
||||||
|
|
||||||
|
<field name="inherit_id"
|
||||||
|
ref="resource.resource_calendar_form"/>
|
||||||
|
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<xpath expr="//sheet/notebook"
|
||||||
|
position="before">
|
||||||
|
|
||||||
|
<group string="Attendance Configuration">
|
||||||
|
|
||||||
|
<group>
|
||||||
|
|
||||||
|
<field name="weekday_ids"
|
||||||
|
widget="many2many_tags"/>
|
||||||
|
|
||||||
|
<field name="weekend_ids"
|
||||||
|
widget="many2many_tags"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
|
||||||
|
<field name="shift_start_time"
|
||||||
|
widget="float_time"/>
|
||||||
|
|
||||||
|
<field name="shift_end_time"
|
||||||
|
widget="float_time"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
|
||||||
|
<field name="late_grace_period"/>
|
||||||
|
|
||||||
|
<field name="early_out_grace_period"/>
|
||||||
|
<field name="over_time_hrs"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<button name="action_generate_schedule"
|
||||||
|
type="object"
|
||||||
|
string="Generate Schedule"
|
||||||
|
class="oe_highlight oe_inline"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//sheet/notebook"
|
||||||
|
position="inside">
|
||||||
|
|
||||||
|
<page string="Department Grace Period">
|
||||||
|
|
||||||
|
<field name="department_grace_ids">
|
||||||
|
|
||||||
|
<list editable="bottom">
|
||||||
|
|
||||||
|
<field name="department_id"/>
|
||||||
|
<field name="grace_period"/>
|
||||||
|
|
||||||
|
</list>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
<!-- security.xml -->
|
<!--security.xml -->
|
||||||
<odoo>
|
<odoo>
|
||||||
<!-- security.xml -->
|
<!--security.xml -->
|
||||||
<odoo>
|
<odoo>
|
||||||
|
|
||||||
<record id="category_employee_appraisal" model="ir.module.category">
|
<record id="category_employee_appraisal" model="ir.module.category">
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@
|
||||||
'views/res_config_settings.xml',
|
'views/res_config_settings.xml',
|
||||||
'views/hr_employee.xml',
|
'views/hr_employee.xml',
|
||||||
'views/bank_details.xml',
|
'views/bank_details.xml',
|
||||||
'wizards/work_location_wizard.xml'
|
'wizards/work_location_wizard.xml',
|
||||||
|
'wizards/html_preview_wizard.xml'
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,18 @@ class EmployerHistory(models.Model):
|
||||||
last_working_day = fields.Date(string='Last Working Day')
|
last_working_day = fields.Date(string='Last Working Day')
|
||||||
ctc = fields.Char(string='CTC')
|
ctc = fields.Char(string='CTC')
|
||||||
employee_id = fields.Many2one('hr.employee')
|
employee_id = fields.Many2one('hr.employee')
|
||||||
|
summary = fields.Html(string='Summary')
|
||||||
|
|
||||||
|
def action_open_html_popup(self):
|
||||||
|
for rec in self:
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': 'Summary',
|
||||||
|
'res_model': 'html.preview.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'new',
|
||||||
|
'context': {
|
||||||
|
'default_html_content': rec.summary,
|
||||||
|
'active_id': rec.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
from . import html_preview_wizard
|
||||||
from . import work_location_wizard
|
from . import work_location_wizard
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class HtmlPreviewWizard(models.TransientModel):
|
||||||
|
_name = 'html.preview.wizard'
|
||||||
|
_description = 'Edit HTML Popup'
|
||||||
|
|
||||||
|
html_content = fields.Html()
|
||||||
|
|
||||||
|
def action_save_html(self):
|
||||||
|
active_id = self.env.context.get('active_id')
|
||||||
|
|
||||||
|
record = self.env['employer.history'].browse(active_id)
|
||||||
|
|
||||||
|
record.summary = self.html_content
|
||||||
|
|
||||||
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_html_preview_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">html.preview.wizard.form</field>
|
||||||
|
<field name="model">html.preview.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Summary">
|
||||||
|
<sheet>
|
||||||
|
<field name="html_content" widget="html"/>
|
||||||
|
</sheet>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button name="action_save_html"
|
||||||
|
string="Save"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"/>
|
||||||
|
|
||||||
|
<button string="Cancel"
|
||||||
|
special="cancel"
|
||||||
|
class="btn-secondary"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': "Payroll IT Declarations",
|
||||||
|
|
||||||
|
'summary': "Manage Income Tax Declarations for Employees in Payroll",
|
||||||
|
'description': """
|
||||||
|
Payroll IT Declarations
|
||||||
|
========================
|
||||||
|
|
||||||
|
This module allows HR and payroll departments to manage and track Income Tax (IT) declarations submitted by employees.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
---------
|
||||||
|
- Employee-wise tax declaration submission
|
||||||
|
- HR approval workflow for declarations
|
||||||
|
- Category-wise declaration limits (e.g. 80C, HRA, LTA, etc.)
|
||||||
|
- Auto-calculation of eligible deductions
|
||||||
|
- Integration with Odoo Payroll for accurate tax computation
|
||||||
|
- Attach supporting documents (PDFs, images)
|
||||||
|
- Employee self-service through portal
|
||||||
|
|
||||||
|
Built with usability and compliance in mind, this module streamlines the IT declaration process and ensures transparency and efficiency across the organization.
|
||||||
|
|
||||||
|
Developed by: Pranay
|
||||||
|
""",
|
||||||
|
|
||||||
|
'author': "Pranay",
|
||||||
|
'website': "https://www.ftprotech.com",
|
||||||
|
|
||||||
|
# Categories can be used to filter modules in modules listing
|
||||||
|
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
|
||||||
|
# for the full list
|
||||||
|
'category': 'Human Resources',
|
||||||
|
'version': '0.1',
|
||||||
|
|
||||||
|
# any module necessary for this one to work correctly
|
||||||
|
'depends': ['base','hr','hr_payroll','hr_employee_extended'],
|
||||||
|
|
||||||
|
# always loaded
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/payroll_periods.xml',
|
||||||
|
'views/emp_it_declaration.xml',
|
||||||
|
'views/it_investment_type.xml',
|
||||||
|
'views/it_investment_costing.xml'
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
from . import payroll_periods
|
||||||
|
from . import emp_it_declarations
|
||||||
|
from . import it_investment_type
|
||||||
|
from . import it_investment_costing
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import calendar
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
|
class empItDeclaration(models.Model):
|
||||||
|
_name = 'emp.it.declarations'
|
||||||
|
_rec_name = 'employee_id'
|
||||||
|
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
default=lambda self: self.env.user.employee_id,
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
total_investment = fields.Float(
|
||||||
|
string="Total Investment Amount",
|
||||||
|
required=True,
|
||||||
|
help="Declared investment amount eligible for tax exemption"
|
||||||
|
)
|
||||||
|
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", required=True, default='new')
|
||||||
|
|
||||||
|
investment_costing_ids = fields.One2many('it.investment.costing','it_declaration_id')
|
||||||
|
# # investment_type = fields.Selection([('past_employment','PAST EMPLOYMENT'),('us_80c','US 80C'),('us_80d','US 80D'),('chapter_via','CHAPTER VIA'),('us_17','US 17'),('house_rent','HOUSE RENT'),('other_i_or_l','OTHER INCOME/LOSS'),('other_declaration','OTHER DECLARATION')],string="Investment Type", required=True)
|
||||||
|
past_employment_costing_line_ids = fields.One2many('it.past.employment','it_declaration_id')
|
||||||
|
us_80c_costing_line_ids = fields.One2many('it.us80c.employment','it_declaration_id')
|
||||||
|
us_80d_costing_line_ids = fields.One2many('it.us80d.employment','it_declaration_id')
|
||||||
|
us_10_costing_line_ids = fields.One2many('it.us10.employment','it_declaration_id')
|
||||||
|
us_80g_costing_line_ids = fields.One2many('it.us80g.employment','it_declaration_id')
|
||||||
|
chapter_via_costing_line_ids = fields.One2many('it.chapter.via.employment','it_declaration_id')
|
||||||
|
us_17_costing_line_ids = fields.One2many('it.us17.employment','it_declaration_id')
|
||||||
|
house_rent_costing_line_ids = fields.One2many('it.house.rent.employment','it_declaration_id')
|
||||||
|
other_i_or_l_costing_line_ids = fields.One2many('it.income.loss.employment','it_declaration_id')
|
||||||
|
other_declaration_costing_line_ids = fields.One2many('it.other.declaration.employment','it_declaration_id')
|
||||||
|
|
||||||
|
@api.depends('employee_id','period_id')
|
||||||
|
def _compute_display_name(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.display_name = _('%s (%s)'%(rec.employee_id.name,rec.period_id.name))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_regime_declarations(self):
|
||||||
|
pass
|
||||||
|
# for rec in self:
|
||||||
|
# if not rec.tax_regime or not rec.period_id or not rec.employee_id:
|
||||||
|
# continue
|
||||||
|
# # Clear existing relations
|
||||||
|
# investment_costing = self.env['it.investment.costing']
|
||||||
|
# investment_costing_lines = self.env['it.investment.costing.line']
|
||||||
|
# rec.investment_costing_ids = [(5, 0, 0)]
|
||||||
|
# rec.past_employment_costing_line_ids = [(5, 0, 0)]
|
||||||
|
# available_costing_list = list()
|
||||||
|
# investment_types = self.env['it.investment.type'].search([])
|
||||||
|
#
|
||||||
|
# emp_investment_costing = investment_costing.search([('employee_id','=',rec.employee_id.id),('period_id','=',rec.period_id.id)])
|
||||||
|
#
|
||||||
|
# not_available_types = list(set(investment_types.ids) - set(emp_investment_costing.investment_type_id.ids))
|
||||||
|
#
|
||||||
|
# if not_available_types:
|
||||||
|
# for item in not_available_types:
|
||||||
|
# investment_costing.sudo().create({
|
||||||
|
# 'investment_type_id':item,
|
||||||
|
# 'employee_id': rec.employee_id.id,
|
||||||
|
# 'period_id': rec.period_id.id
|
||||||
|
# })
|
||||||
|
# emp_investment_costing = investment_costing.search([('employee_id','=',rec.employee_id.id),('period_id','=',rec.period_id.id)])
|
||||||
|
#
|
||||||
|
# for investment_costing in emp_investment_costing:
|
||||||
|
# investment_type_lines = self.env['it.investment.type.line'].search([('investment_type_id','=',investment_costing.investment_type_id.id)])
|
||||||
|
# emp_investment_costing_lines = investment_costing_lines.search([('investment_type_id','=',investment_costing.investment_type_id.id),('employee_id', '=', rec.employee_id.id), ('period_id', '=', rec.period_id.id)])
|
||||||
|
# not_available_line_types = list(set(investment_type_lines.ids) - set(emp_investment_costing_lines.investment_type_line_id.ids))
|
||||||
|
# if not_available_line_types:
|
||||||
|
# for item in not_available_line_types:
|
||||||
|
# investment_costing_lines.sudo().create({
|
||||||
|
# 'investment_type_line_id': item,
|
||||||
|
# 'investment_costing_id': investment_costing.id
|
||||||
|
# })
|
||||||
|
# emp_investment_costing_lines = investment_costing_lines.search([('investment_type_id','=',investment_costing.investment_type_id.id),('employee_id', '=', rec.employee_id.id), ('period_id', '=', rec.period_id.id)])
|
||||||
|
# for investment_costing_line in emp_investment_costing_lines:
|
||||||
|
# investment_costing_line.write({'investment_costing_id':investment_costing.id})
|
||||||
|
# investment_costing.write({'it_declaration_id': rec.id})
|
||||||
|
|
||||||
|
# @api.model
|
||||||
|
# def get_view(self, view_id=None, view_type='form', **options):
|
||||||
|
# rtn = super().get_view(view_id, view_type, **options)
|
||||||
|
# if view_type == 'form' and "arch" in rtn:
|
||||||
|
# doc = etree.fromstring(rtn['arch'])
|
||||||
|
# target_notebook = doc.xpath("//notebook")
|
||||||
|
#
|
||||||
|
# if target_notebook:
|
||||||
|
# investment_types = self.env['it.investment.type'].search([], order='sequence')
|
||||||
|
#
|
||||||
|
# for inv_type in investment_types:
|
||||||
|
# # Create a new <page> for each investment type
|
||||||
|
# page = etree.Element('page', {
|
||||||
|
# 'name': f'page_{inv_type.name}',
|
||||||
|
# 'string': inv_type.name
|
||||||
|
# })
|
||||||
|
#
|
||||||
|
# # Create a field with a domain specific to this investment type
|
||||||
|
# # domain_str = f"[('investment_type_id.id', '=', {inv_type.id})]"
|
||||||
|
# domain_str = "[('tax_regime','in',['new'])]"
|
||||||
|
# field = etree.Element('field', {
|
||||||
|
# 'name': 'investment_costing_line_ids',
|
||||||
|
# 'domain': domain_str,
|
||||||
|
# })
|
||||||
|
#
|
||||||
|
# # Add the field to the page
|
||||||
|
# page.append(field)
|
||||||
|
# # Add the page to the notebook
|
||||||
|
# target_notebook[0].append(page)
|
||||||
|
#
|
||||||
|
# # Update the arch
|
||||||
|
# rtn['arch'] = etree.tostring(doc, encoding='unicode')
|
||||||
|
# return rtn
|
||||||
|
|
@ -0,0 +1,405 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class ItInvestmentCosting(models.Model):
|
||||||
|
_name = 'it.investment.costing'
|
||||||
|
_description = 'IT Investment Costing'
|
||||||
|
_rec_name = 'investment_type_id'
|
||||||
|
|
||||||
|
investment_type_id = fields.Many2one('it.investment.type')
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
default=lambda self: self.env.user.employee_id,
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations')
|
||||||
|
amount = fields.Float(string='Amount')
|
||||||
|
# it_investment_costing_lines = fields.One2many('it.investment.costing.line','investment_costing_id')
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
investment_type = fields.Selection(string="Investment Type",required=True,related='investment_type_id.name')
|
||||||
|
it_past_employment_lines = fields.One2many('it.past.employment','investment_costing_id')
|
||||||
|
it_us80c_employment_lines = fields.One2many('it.us80c.employment','investment_costing_id')
|
||||||
|
it_us80d_employment_lines = fields.One2many('it.us80d.employment','investment_costing_id')
|
||||||
|
it_us10_employment_lines = fields.One2many('it.us10.employment','investment_costing_id')
|
||||||
|
it_us80g_employment_lines = fields.One2many('it.us80g.employment','investment_costing_id')
|
||||||
|
it_chapter_via_employment_lines = fields.One2many('it.chapter.via.employment','investment_costing_id')
|
||||||
|
it_us17_employment_lines = fields.One2many('it.us17.employment','investment_costing_id')
|
||||||
|
it_house_rent_employment_lines = fields.One2many('it.house.rent.employment','investment_costing_id')
|
||||||
|
it_income_loss_employment_lines = fields.One2many('it.income.loss.employment','investment_costing_id')
|
||||||
|
it_other_declaration_employment_lines = fields.One2many('it.other.declaration.employment','investment_costing_id')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class itPastEmployment(models.Model):
|
||||||
|
_name = 'it.past.employment'
|
||||||
|
|
||||||
|
investment_costing_id = fields.Many2one('it.investment.costing', required=True)
|
||||||
|
investment_type_line_id = fields.Many2one('it.investment.type.line','General', required=True,domain="[('investment_type','=','past_employment')]")
|
||||||
|
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type_id', store=True)
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", related='investment_type_line_id.tax_regime',store=True)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations',store=True)
|
||||||
|
declaration_amount = fields.Float(string='Declaration Amount')
|
||||||
|
need_action = fields.Boolean(string="Needs Action",related='investment_type_line_id.need_action')
|
||||||
|
action_id = fields.Many2one(
|
||||||
|
'ir.actions.actions',
|
||||||
|
string="Action",
|
||||||
|
related="investment_type_line_id.action_id",
|
||||||
|
help="Linked Odoo action if needed"
|
||||||
|
)
|
||||||
|
proof_amount = fields.Float(string='Proof Amount')
|
||||||
|
remarks = fields.Text(string='Remarks')
|
||||||
|
proof_file = fields.Binary(string='Proof')
|
||||||
|
proof_file_name = fields.Char()
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.period_id'
|
||||||
|
)
|
||||||
|
limit = fields.Integer(string='Limit',readonly=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.employee_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class itUS80CEmployment(models.Model):
|
||||||
|
_name = 'it.us80c.employment'
|
||||||
|
|
||||||
|
investment_costing_id = fields.Many2one('it.investment.costing', required=True)
|
||||||
|
investment_type_line_id = fields.Many2one('it.investment.type.line','General', required=True,domain="[('investment_type','=','us_80c')]")
|
||||||
|
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type_id', store=True)
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", related='investment_type_line_id.tax_regime',store=True)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations',store=True)
|
||||||
|
declaration_amount = fields.Float(string='Declaration Amount')
|
||||||
|
need_action = fields.Boolean(string="Needs Action",related='investment_type_line_id.need_action')
|
||||||
|
action_id = fields.Many2one(
|
||||||
|
'ir.actions.actions',
|
||||||
|
string="Action",
|
||||||
|
related="investment_type_line_id.action_id",
|
||||||
|
help="Linked Odoo action if needed"
|
||||||
|
)
|
||||||
|
proof_amount = fields.Float(string='Proof Amount')
|
||||||
|
remarks = fields.Text(string='Remarks')
|
||||||
|
proof_file = fields.Binary(string='Proof')
|
||||||
|
proof_file_name = fields.Char()
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.period_id'
|
||||||
|
)
|
||||||
|
limit = fields.Integer(string='Limit',readonly=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.employee_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class itUS80DEmployment(models.Model):
|
||||||
|
_name = 'it.us80d.employment'
|
||||||
|
|
||||||
|
investment_costing_id = fields.Many2one('it.investment.costing', required=True)
|
||||||
|
investment_type_line_id = fields.Many2one('it.investment.type.line','General', required=True,domain="[('investment_type','=','us_80d')]")
|
||||||
|
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type_id', store=True)
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", related='investment_type_line_id.tax_regime',store=True)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations',store=True)
|
||||||
|
declaration_amount = fields.Float(string='Declaration Amount')
|
||||||
|
need_action = fields.Boolean(string="Needs Action",related='investment_type_line_id.need_action')
|
||||||
|
action_id = fields.Many2one(
|
||||||
|
'ir.actions.actions',
|
||||||
|
string="Action",
|
||||||
|
related="investment_type_line_id.action_id",
|
||||||
|
help="Linked Odoo action if needed"
|
||||||
|
)
|
||||||
|
proof_amount = fields.Float(string='Proof Amount')
|
||||||
|
remarks = fields.Text(string='Remarks')
|
||||||
|
proof_file = fields.Binary(string='Proof')
|
||||||
|
proof_file_name = fields.Char()
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.period_id'
|
||||||
|
)
|
||||||
|
limit = fields.Integer(string='Limit',readonly=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.employee_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class itUS10Employment(models.Model):
|
||||||
|
_name = 'it.us10.employment'
|
||||||
|
|
||||||
|
investment_costing_id = fields.Many2one('it.investment.costing', required=True)
|
||||||
|
investment_type_line_id = fields.Many2one('it.investment.type.line','General', required=True,domain="[('investment_type','=','US10')]")
|
||||||
|
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type_id', store=True)
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", related='investment_type_line_id.tax_regime',store=True)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations',store=True)
|
||||||
|
declaration_amount = fields.Float(string='Declaration Amount')
|
||||||
|
need_action = fields.Boolean(string="Needs Action",related='investment_type_line_id.need_action')
|
||||||
|
action_id = fields.Many2one(
|
||||||
|
'ir.actions.actions',
|
||||||
|
string="Action",
|
||||||
|
related="investment_type_line_id.action_id",
|
||||||
|
help="Linked Odoo action if needed"
|
||||||
|
)
|
||||||
|
proof_amount = fields.Float(string='Proof Amount')
|
||||||
|
remarks = fields.Text(string='Remarks')
|
||||||
|
proof_file = fields.Binary(string='Proof')
|
||||||
|
proof_file_name = fields.Char()
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.period_id'
|
||||||
|
)
|
||||||
|
limit = fields.Integer(string='Limit',readonly=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.employee_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class itUS80GEmployment(models.Model):
|
||||||
|
_name = 'it.us80g.employment'
|
||||||
|
|
||||||
|
investment_costing_id = fields.Many2one('it.investment.costing', required=True)
|
||||||
|
investment_type_line_id = fields.Many2one('it.investment.type.line','General', required=True,domain="[('investment_type','=','us_80g')]")
|
||||||
|
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type_id', store=True)
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", related='investment_type_line_id.tax_regime',store=True)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations',store=True)
|
||||||
|
|
||||||
|
declaration_amount = fields.Float(string='Declaration Amount')
|
||||||
|
need_action = fields.Boolean(string="Needs Action",related='investment_type_line_id.need_action')
|
||||||
|
action_id = fields.Many2one(
|
||||||
|
'ir.actions.actions',
|
||||||
|
string="Action",
|
||||||
|
related="investment_type_line_id.action_id",
|
||||||
|
help="Linked Odoo action if needed"
|
||||||
|
)
|
||||||
|
proof_amount = fields.Float(string='Proof Amount')
|
||||||
|
remarks = fields.Text(string='Remarks')
|
||||||
|
proof_file = fields.Binary(string='Proof')
|
||||||
|
proof_file_name = fields.Char()
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.period_id'
|
||||||
|
)
|
||||||
|
limit = fields.Integer(string='Limit',readonly=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.employee_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class itChapterViaEmployment(models.Model):
|
||||||
|
_name = 'it.chapter.via.employment'
|
||||||
|
|
||||||
|
investment_costing_id = fields.Many2one('it.investment.costing', required=True)
|
||||||
|
investment_type_line_id = fields.Many2one('it.investment.type.line','General', required=True,domain="[('investment_type','=','chapter_via')]")
|
||||||
|
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type_id', store=True)
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", related='investment_type_line_id.tax_regime',store=True)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations',store=True)
|
||||||
|
|
||||||
|
declaration_amount = fields.Float(string='Declaration Amount')
|
||||||
|
need_action = fields.Boolean(string="Needs Action",related='investment_type_line_id.need_action')
|
||||||
|
action_id = fields.Many2one(
|
||||||
|
'ir.actions.actions',
|
||||||
|
string="Action",
|
||||||
|
related="investment_type_line_id.action_id",
|
||||||
|
help="Linked Odoo action if needed"
|
||||||
|
)
|
||||||
|
proof_amount = fields.Float(string='Proof Amount')
|
||||||
|
remarks = fields.Text(string='Remarks')
|
||||||
|
proof_file = fields.Binary(string='Proof')
|
||||||
|
proof_file_name = fields.Char()
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.period_id'
|
||||||
|
)
|
||||||
|
limit = fields.Integer(string='Limit',readonly=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.employee_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class itUS17Employment(models.Model):
|
||||||
|
_name = 'it.us17.employment'
|
||||||
|
|
||||||
|
investment_costing_id = fields.Many2one('it.investment.costing', required=True)
|
||||||
|
investment_type_line_id = fields.Many2one('it.investment.type.line','General', required=True,domain="[('investment_type','=','us_17')]")
|
||||||
|
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type_id', store=True)
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", related='investment_type_line_id.tax_regime',store=True)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations',store=True)
|
||||||
|
|
||||||
|
declaration_amount = fields.Float(string='Declaration Amount')
|
||||||
|
need_action = fields.Boolean(string="Needs Action",related='investment_type_line_id.need_action')
|
||||||
|
action_id = fields.Many2one(
|
||||||
|
'ir.actions.actions',
|
||||||
|
string="Action",
|
||||||
|
related="investment_type_line_id.action_id",
|
||||||
|
help="Linked Odoo action if needed"
|
||||||
|
)
|
||||||
|
proof_amount = fields.Float(string='Proof Amount')
|
||||||
|
remarks = fields.Text(string='Remarks')
|
||||||
|
proof_file = fields.Binary(string='Proof')
|
||||||
|
proof_file_name = fields.Char()
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.period_id'
|
||||||
|
)
|
||||||
|
limit = fields.Integer(string='Limit',readonly=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.employee_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class itHouseRentEmployment(models.Model):
|
||||||
|
_name = 'it.house.rent.employment'
|
||||||
|
|
||||||
|
|
||||||
|
investment_costing_id = fields.Many2one('it.investment.costing', required=True)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations',store=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.employee_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class itOtherIorLEmployment(models.Model):
|
||||||
|
_name = 'it.income.loss.employment'
|
||||||
|
|
||||||
|
investment_costing_id = fields.Many2one('it.investment.costing', required=True)
|
||||||
|
investment_type_line_id = fields.Many2one('it.investment.type.line','General', required=True,domain="[('investment_type','=','other_i_or_l')]")
|
||||||
|
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type_id', store=True)
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", related='investment_type_line_id.tax_regime',store=True)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations',store=True)
|
||||||
|
declaration_amount = fields.Float(string='Declaration Amount')
|
||||||
|
need_action = fields.Boolean(string="Needs Action",related='investment_type_line_id.need_action')
|
||||||
|
action_id = fields.Many2one(
|
||||||
|
'ir.actions.actions',
|
||||||
|
string="Action",
|
||||||
|
related="investment_type_line_id.action_id",
|
||||||
|
help="Linked Odoo action if needed"
|
||||||
|
)
|
||||||
|
proof_amount = fields.Float(string='Proof Amount')
|
||||||
|
remarks = fields.Text(string='Remarks')
|
||||||
|
proof_file = fields.Binary(string='Proof')
|
||||||
|
proof_file_name = fields.Char()
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.period_id'
|
||||||
|
)
|
||||||
|
limit = fields.Integer(string='Limit',readonly=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.employee_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class itOtherDeclarationEmployment(models.Model):
|
||||||
|
_name = 'it.other.declaration.employment'
|
||||||
|
|
||||||
|
investment_costing_id = fields.Many2one('it.investment.costing', required=True)
|
||||||
|
investment_type_line_id = fields.Many2one('it.investment.type.line','General', required=True,domain="[('investment_type','=','other_declaration')]")
|
||||||
|
investment_type_id = fields.Many2one('it.investment.type',related='investment_type_line_id.investment_type_id', store=True)
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", related='investment_type_line_id.tax_regime',store=True)
|
||||||
|
it_declaration_id = fields.Many2one('emp.it.declarations',store=True,)
|
||||||
|
declaration_amount = fields.Float(string='Declaration Amount')
|
||||||
|
need_action = fields.Boolean(string="Needs Action",related='investment_type_line_id.need_action')
|
||||||
|
action_id = fields.Many2one(
|
||||||
|
'ir.actions.actions',
|
||||||
|
string="Action",
|
||||||
|
related="investment_type_line_id.action_id",
|
||||||
|
help="Linked Odoo action if needed"
|
||||||
|
)
|
||||||
|
proof_amount = fields.Float(string='Proof Amount')
|
||||||
|
remarks = fields.Text(string='Remarks')
|
||||||
|
proof_file = fields.Binary(string='Proof')
|
||||||
|
proof_file_name = fields.Char()
|
||||||
|
period_id = fields.Many2one(
|
||||||
|
'payroll.period',
|
||||||
|
string="Payroll Period",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.period_id'
|
||||||
|
)
|
||||||
|
limit = fields.Integer(string='Limit',readonly=True)
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee',
|
||||||
|
string="Employee",
|
||||||
|
required=True,
|
||||||
|
related='investment_costing_id.employee_id'
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class ItInvestmentType(models.Model):
|
||||||
|
_name = 'it.investment.type'
|
||||||
|
_description = 'IT Investment Type'
|
||||||
|
_rec_name = 'name'
|
||||||
|
|
||||||
|
sequence = fields.Integer()
|
||||||
|
name = fields.Selection([('past_employment','PAST EMPLOYMENT'),('us_80c','US 80C'),('us_80d','US 80D'),('us_10','US 10'),('us_80g','US 80G'),('chapter_via','CHAPTER VIA'),('us_17','US 17'),('house_rent','HOUSE RENT'),('other_i_or_l','OTHER INCOME/LOSS'),('other_declaration','OTHER DECLARATION')],string="Investment Type", required=True)
|
||||||
|
line_ids = fields.One2many(
|
||||||
|
'it.investment.type.line',
|
||||||
|
'investment_type_id',
|
||||||
|
string="Investment Type Lines"
|
||||||
|
)
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ItInvestmentTypeLine(models.Model):
|
||||||
|
_name = 'it.investment.type.line'
|
||||||
|
_description = 'IT Investment Type Line'
|
||||||
|
_rec_name = 'name'
|
||||||
|
|
||||||
|
investment_type_id = fields.Many2one(
|
||||||
|
'it.investment.type',
|
||||||
|
string="Investment Type",
|
||||||
|
required=True,
|
||||||
|
ondelete='cascade'
|
||||||
|
)
|
||||||
|
investment_type = fields.Selection([('past_employment','PAST EMPLOYMENT'),('us_80c','US 80C'),('us_80d','US 80D'),('us_10','US 10'),('us_80g','US 80G'),('chapter_via','CHAPTER VIA'),('us_17','US 17'),('house_rent','HOUSE RENT'),('other_i_or_l','OTHER INCOME/LOSS'),('other_declaration','OTHER DECLARATION')],string="Investment Type", related='investment_type_id.name')
|
||||||
|
name = fields.Char(string="Line Name", required=True)
|
||||||
|
need_action = fields.Boolean(string="Needs Action")
|
||||||
|
action_id = fields.Many2one(
|
||||||
|
'ir.actions.actions',
|
||||||
|
string="Action",
|
||||||
|
help="Linked Odoo action if needed"
|
||||||
|
)
|
||||||
|
tax_regime = fields.Selection([
|
||||||
|
('new', 'New Regime'),
|
||||||
|
('old', 'Old Regime')
|
||||||
|
], string="Tax Regime", required=True)
|
||||||
|
sequence = fields.Integer()
|
||||||
|
active = fields.Boolean(default=True)
|
||||||
|
limit = fields.Integer()
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import calendar
|
||||||
|
|
||||||
|
|
||||||
|
class PayrollPeriod(models.Model):
|
||||||
|
_name = 'payroll.period'
|
||||||
|
_description = 'Payroll Period'
|
||||||
|
_rec_name = 'name'
|
||||||
|
_sql_constraints = [
|
||||||
|
('unique_name', 'unique(name)', 'The name must be unique.')
|
||||||
|
]
|
||||||
|
|
||||||
|
from_date = fields.Date(string="From Date", required=True)
|
||||||
|
to_date = fields.Date(string="To Date", required=True)
|
||||||
|
name = fields.Char(string="Name", required=True)
|
||||||
|
period_line_ids = fields.One2many('payroll.period.line', 'period_id', string="Monthly Periods")
|
||||||
|
|
||||||
|
@api.onchange('from_date', 'to_date')
|
||||||
|
def onchange_from_to_date(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.from_date and rec.to_date:
|
||||||
|
rec.name = f"{rec.from_date.year}-{rec.to_date.year}"
|
||||||
|
|
||||||
|
|
||||||
|
def action_generate_month_lines(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not (self.from_date and self.to_date):
|
||||||
|
raise ValidationError("Please set both From Date and To Date.")
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
current = self.from_date.replace(day=1)
|
||||||
|
while current <= self.to_date:
|
||||||
|
end_of_month = current.replace(day=calendar.monthrange(current.year, current.month)[1])
|
||||||
|
if end_of_month > self.to_date:
|
||||||
|
end_of_month = self.to_date
|
||||||
|
|
||||||
|
lines.append((0, 0, {
|
||||||
|
'from_date': current,
|
||||||
|
'to_date': end_of_month,
|
||||||
|
'name': f"{current.strftime('%b')} - {str(current.year)[-2:]}"
|
||||||
|
}))
|
||||||
|
|
||||||
|
if current.month == 12:
|
||||||
|
current = current.replace(year=current.year + 1, month=1)
|
||||||
|
else:
|
||||||
|
current = current.replace(month=current.month + 1)
|
||||||
|
|
||||||
|
self.period_line_ids = lines
|
||||||
|
|
||||||
|
|
||||||
|
class PayrollPeriodLine(models.Model):
|
||||||
|
_name = 'payroll.period.line'
|
||||||
|
_description = 'Payroll Period Line'
|
||||||
|
_rec_name = 'name'
|
||||||
|
|
||||||
|
period_id = fields.Many2one('payroll.period', string="Payroll Period", required=True, ondelete='cascade')
|
||||||
|
from_date = fields.Date(string="From Date")
|
||||||
|
to_date = fields.Date(string="To Date")
|
||||||
|
name = fields.Char(string="Name")
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_payroll_period_user,payroll.period,model_payroll_period,base.group_user,1,1,1,1
|
||||||
|
access_payroll_period_line_user,payroll.period.line,model_payroll_period_line,base.group_user,1,1,1,1
|
||||||
|
access_emp_it_declaration_user,emp.it.declarations,model_emp_it_declarations,base.group_user,1,1,1,1
|
||||||
|
access_it_investment_type,it.investment.type,model_it_investment_type,base.group_user,1,1,1,1
|
||||||
|
access_it_investment_type_line,it.investment.type.line,model_it_investment_type_line,base.group_user,1,1,1,1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
access_it_investment_costing,it.investment.costing.user,model_it_investment_costing,base.group_user,1,1,1,1
|
||||||
|
|
||||||
|
access_it_past_employment,it.past.employment,model_it_past_employment,base.group_user,1,1,1,1
|
||||||
|
access_it_us80c_employment,it.us80c.employment,model_it_us80c_employment,base.group_user,1,1,1,1
|
||||||
|
access_it_us80d_employment,it.us80d.employment,model_it_us80d_employment,base.group_user,1,1,1,1
|
||||||
|
access_it_us10_employment,it.us10.employment,model_it_us10_employment,base.group_user,1,1,1,1
|
||||||
|
access_it_us80g_employment,it.us80g.employment,model_it_us80g_employment,base.group_user,1,1,1,1
|
||||||
|
access_it_chapter_via_employment,it.chapter.via.employment,model_it_chapter_via_employment,base.group_user,1,1,1,1
|
||||||
|
access_it_us17_employment,it.us17.employment,model_it_us17_employment,base.group_user,1,1,1,1
|
||||||
|
access_it_house_rent_employment,it.house.rent.employment,model_it_house_rent_employment,base.group_user,1,1,1,1
|
||||||
|
access_it_income_loss_employment,it.income.loss.employment,model_it_income_loss_employment,base.group_user,1,1,1,1
|
||||||
|
access_it_other_declaration_employment,it.other.declaration.employment,model_it_other_declaration_employment,base.group_user,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_emp_it_declaration_form" model="ir.ui.view">
|
||||||
|
<field name="name">emp.it.declarations.form</field>
|
||||||
|
<field name="model">emp.it.declarations</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="IT Declaration">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title mb24">
|
||||||
|
<div class="o_row">
|
||||||
|
<!-- <label for="employee_id" string="Employee"/>-->
|
||||||
|
<field name="employee_id" widget="res_partner_many2one" placeholder="Employee Name..."/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="period_id"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="total_investment"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}"/>
|
||||||
|
<br/>
|
||||||
|
<!-- <button name="generate_regime_declarations" type="object" class="btn-primary"/>-->
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page name="investment_costings" string="Total Investment">
|
||||||
|
<field name="investment_costing_ids"/>
|
||||||
|
</page>
|
||||||
|
<page name="past_employment_costings" string="PAST EMPLOYMENT">
|
||||||
|
<field name="past_employment_costing_line_ids">
|
||||||
|
<list>
|
||||||
|
<field name="investment_costing_id" column_invisible="1"/>
|
||||||
|
<field name="investment_type_line_id"/>
|
||||||
|
<field name="declaration_amount"/>
|
||||||
|
<field name="proof_amount"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
<field name="proof_file"/>
|
||||||
|
<field name="limit"/>
|
||||||
|
<field name="tax_regime" column_invisible="1"/>
|
||||||
|
<field name="period_id" column_invisible="1"/>
|
||||||
|
<field name="proof_file_name" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<page name="us_80c_costings" string="US 80C">
|
||||||
|
<field name="us_80c_costing_line_ids">
|
||||||
|
<list>
|
||||||
|
<field name="investment_costing_id" column_invisible="1"/>
|
||||||
|
<field name="investment_type_line_id"/>
|
||||||
|
<field name="declaration_amount"/>
|
||||||
|
<field name="proof_amount"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
<field name="proof_file"/>
|
||||||
|
<field name="limit"/>
|
||||||
|
<field name="tax_regime" column_invisible="1"/>
|
||||||
|
<field name="period_id" column_invisible="1"/>
|
||||||
|
<field name="proof_file_name" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page name="us_80d_costings" string="US 80D">
|
||||||
|
<field name="us_80d_costing_line_ids">
|
||||||
|
<list>
|
||||||
|
<field name="investment_costing_id" column_invisible="1"/>
|
||||||
|
<field name="investment_type_line_id"/>
|
||||||
|
<field name="declaration_amount"/>
|
||||||
|
<field name="proof_amount"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
<field name="proof_file"/>
|
||||||
|
<field name="limit"/>
|
||||||
|
<field name="tax_regime" column_invisible="1"/>
|
||||||
|
<field name="period_id" column_invisible="1"/>
|
||||||
|
<field name="proof_file_name" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page name="us_10_costing" string="US 10">
|
||||||
|
<field name="us_10_costing_line_ids">
|
||||||
|
<list>
|
||||||
|
<field name="investment_costing_id" column_invisible="1"/>
|
||||||
|
<field name="investment_type_line_id"/>
|
||||||
|
<field name="declaration_amount"/>
|
||||||
|
<field name="proof_amount"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
<field name="proof_file"/>
|
||||||
|
<field name="limit"/>
|
||||||
|
<field name="tax_regime" column_invisible="1"/>
|
||||||
|
<field name="period_id" column_invisible="1"/>
|
||||||
|
<field name="proof_file_name" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page name="us_80g_costing" string="US 80G">
|
||||||
|
<field name="us_80g_costing_line_ids">
|
||||||
|
<list>
|
||||||
|
<field name="investment_costing_id" column_invisible="1"/>
|
||||||
|
<field name="investment_type_line_id"/>
|
||||||
|
<field name="declaration_amount"/>
|
||||||
|
<field name="proof_amount"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
<field name="proof_file"/>
|
||||||
|
<field name="limit"/>
|
||||||
|
<field name="tax_regime" column_invisible="1"/>
|
||||||
|
<field name="period_id" column_invisible="1"/>
|
||||||
|
<field name="proof_file_name" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<page name="chapter_via_costings" string="CHAPTER VIA">
|
||||||
|
<field name="chapter_via_costing_line_ids">
|
||||||
|
<list>
|
||||||
|
<field name="investment_costing_id" column_invisible="1"/>
|
||||||
|
<field name="investment_type_line_id"/>
|
||||||
|
<field name="declaration_amount"/>
|
||||||
|
<field name="proof_amount"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
<field name="proof_file"/>
|
||||||
|
<field name="limit"/>
|
||||||
|
<field name="tax_regime" column_invisible="1"/>
|
||||||
|
<field name="period_id" column_invisible="1"/>
|
||||||
|
<field name="proof_file_name" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page name="us_17_costings" string="US 17">
|
||||||
|
<field name="us_17_costing_line_ids">
|
||||||
|
<list>
|
||||||
|
<field name="investment_costing_id" column_invisible="1"/>
|
||||||
|
<field name="investment_type_line_id"/>
|
||||||
|
<field name="declaration_amount"/>
|
||||||
|
<field name="proof_amount"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
<field name="proof_file"/>
|
||||||
|
<field name="limit"/>
|
||||||
|
<field name="tax_regime" column_invisible="1"/>
|
||||||
|
<field name="period_id" column_invisible="1"/>
|
||||||
|
<field name="proof_file_name" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page name="house_rent_costings" string="HOUSE RENT">
|
||||||
|
<field name="house_rent_costing_line_ids"/>
|
||||||
|
</page>
|
||||||
|
<page name="other_i_or_l_costings" string="OTHER INCOME/LOSS">
|
||||||
|
<field name="other_i_or_l_costing_line_ids">
|
||||||
|
<list>
|
||||||
|
<field name="investment_costing_id" column_invisible="1"/>
|
||||||
|
<field name="investment_type_line_id"/>
|
||||||
|
<field name="declaration_amount"/>
|
||||||
|
<field name="proof_amount"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
<field name="proof_file"/>
|
||||||
|
<field name="limit"/>
|
||||||
|
<field name="tax_regime" column_invisible="1"/>
|
||||||
|
<field name="period_id" column_invisible="1"/>
|
||||||
|
<field name="proof_file_name" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page name="other_declaration_costings" string="Other Declarations">
|
||||||
|
<field name="other_declaration_costing_line_ids">
|
||||||
|
<list>
|
||||||
|
<field name="investment_costing_id" column_invisible="1"/>
|
||||||
|
<field name="investment_type_line_id"/>
|
||||||
|
<field name="declaration_amount"/>
|
||||||
|
<field name="proof_amount"/>
|
||||||
|
<field name="remarks"/>
|
||||||
|
<field name="proof_file"/>
|
||||||
|
<field name="limit"/>
|
||||||
|
<field name="tax_regime" column_invisible="1"/>
|
||||||
|
<field name="period_id" column_invisible="1"/>
|
||||||
|
<field name="proof_file_name" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_emp_it_declaration_tree" model="ir.ui.view">
|
||||||
|
<field name="name">emp.it.declarations.tree</field>
|
||||||
|
<field name="model">emp.it.declarations</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="period_id"/>
|
||||||
|
<field name="total_investment"/>
|
||||||
|
<field name="tax_regime"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_emp_it_declaration" model="ir.actions.act_window">
|
||||||
|
<field name="name">IT Declarations</field>
|
||||||
|
<field name="res_model">emp.it.declarations</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_it_declarations" name="IT Declarations"
|
||||||
|
parent="hr_payroll.menu_hr_payroll_root"
|
||||||
|
action="action_emp_it_declaration" sequence="99"/>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_tree_it_investment_costing" model="ir.ui.view">
|
||||||
|
<field name="name">it.investment.costing.tree</field>
|
||||||
|
<field name="model">it.investment.costing</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="investment_type_id"/>
|
||||||
|
<field name="amount"/>
|
||||||
|
<field name="period_id"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <record id="view_tree_it_investment_costing_line" model="ir.ui.view">-->
|
||||||
|
<!-- <field name="name">it.investment.costing.line.list</field>-->
|
||||||
|
<!-- <field name="model">it.investment.costing.line</field>-->
|
||||||
|
<!-- <field name="arch" type="xml">-->
|
||||||
|
<!-- <list>-->
|
||||||
|
<!-- <field name="investment_costing_id"/>-->
|
||||||
|
<!-- <field name="investment_type_line_id"/>-->
|
||||||
|
<!-- <field name="declaration_amount"/>-->
|
||||||
|
<!-- <field name="proof_amount"/>-->
|
||||||
|
<!-- <field name="remarks"/>-->
|
||||||
|
<!-- <field name="proof_file"/>-->
|
||||||
|
<!-- <field name="tax_regime"/>-->
|
||||||
|
<!-- <field name="period_id"/>-->
|
||||||
|
<!-- <field name="proof_file_name"/>-->
|
||||||
|
<!-- </list>-->
|
||||||
|
<!-- </field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
<!-- list View -->
|
||||||
|
<record id="view_it_investment_type_list" model="ir.ui.view">
|
||||||
|
<field name="name">it.investment.type.list</field>
|
||||||
|
<field name="model">it.investment.type</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="active" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_it_investment_type_form" model="ir.ui.view">
|
||||||
|
<field name="name">it.investment.type.form</field>
|
||||||
|
<field name="model">it.investment.type</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Investment Type">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
</group>
|
||||||
|
<field name="line_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="tax_regime"/>
|
||||||
|
<field name="need_action"/>
|
||||||
|
<field name="action_id" invisible="not need_action" readonly="not need_action" force_save="1"/>
|
||||||
|
<field name="limit"/>
|
||||||
|
<field name="active" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_it_investment_type" model="ir.actions.act_window">
|
||||||
|
<field name="name">Investment Types</field>
|
||||||
|
<field name="res_model">it.investment.type</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">Create your first IT Investment Type</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
|
<menuitem id="menu_it_investment_type" name="Investment Types"
|
||||||
|
parent="menu_it_payroll_declarations"
|
||||||
|
action="action_it_investment_type" sequence="20"/>
|
||||||
|
</odoo>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<odoo>
|
||||||
|
<record id="view_payroll_period_form" model="ir.ui.view">
|
||||||
|
<field name="name">payroll.period.form</field>
|
||||||
|
<field name="model">payroll.period</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Payroll Period">
|
||||||
|
<header>
|
||||||
|
<button name="action_generate_month_lines" type="object" string="Generate Periods" class="btn-primary"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="from_date"/>
|
||||||
|
<field name="to_date"/>
|
||||||
|
<field name="name" readonly="1" force_save="1"/>
|
||||||
|
</group>
|
||||||
|
<field name="period_line_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="from_date"/>
|
||||||
|
<field name="to_date"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_payroll_period_tree" model="ir.ui.view">
|
||||||
|
<field name="name">payroll.period.tree</field>
|
||||||
|
<field name="model">payroll.period</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="from_date"/>
|
||||||
|
<field name="to_date"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_payroll_period" model="ir.actions.act_window">
|
||||||
|
<field name="name">Payroll Periods</field>
|
||||||
|
<field name="res_model">payroll.period</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<menuitem id="menu_it_payroll_declarations" name="Payroll" parent="hr_payroll.menu_hr_payroll_configuration"/>
|
||||||
|
<menuitem id="menu_payroll_period" name="Payroll Periods" parent="menu_it_payroll_declarations" action="action_payroll_period"/>
|
||||||
|
</odoo>
|
||||||
|
|
@ -100,7 +100,7 @@ result_name = inputs['ATTACH_SALARY'].name
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="default_assignment_of_salary_rule" model="hr.salary.rule">
|
<record id="default_assignment_of_salary_rule" model="hr.salary.rule">
|
||||||
<field name="category_id" ref="hr_payroll.DED"/>
|
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||||
<field name="name">Assignment of Salary</field>
|
<field name="name">Assignment of Salary</field>
|
||||||
<field name="sequence">174</field>
|
<field name="sequence">174</field>
|
||||||
<field name="code">ASSIG_SALARY</field>
|
<field name="code">ASSIG_SALARY</field>
|
||||||
|
|
@ -108,7 +108,7 @@ result_name = inputs['ATTACH_SALARY'].name
|
||||||
<field name="condition_python">result = 'ASSIG_SALARY' in inputs</field>
|
<field name="condition_python">result = 'ASSIG_SALARY' in inputs</field>
|
||||||
<field name="amount_select">code</field>
|
<field name="amount_select">code</field>
|
||||||
<field name="amount_python_compute">
|
<field name="amount_python_compute">
|
||||||
result = -inputs['ASSIG_SALARY'].amount
|
result = inputs['ASSIG_SALARY'].amount
|
||||||
result_name = inputs['ASSIG_SALARY'].name
|
result_name = inputs['ASSIG_SALARY'].name
|
||||||
</field>
|
</field>
|
||||||
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
'name': 'Payroll Extended',
|
||||||
|
'category': 'Human Resources/Payroll',
|
||||||
|
'summary': 'Manage your employee payroll records and Extending the Payroll featuers',
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'depends': [
|
||||||
|
'hr_payroll'
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'data/hr_salary_advance_sequence.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/hr_salary_advance_views.xml',
|
||||||
|
'views/menus.xml'
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="seq_hr_salary_advance" model="ir.sequence">
|
||||||
|
<field name="name">Salary Advance</field>
|
||||||
|
<field name="code">hr.salary.advance</field>
|
||||||
|
<field name="prefix">SA/%(year)s/</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue