Project, recruitment, payroll, bench management changes and updates
This commit is contained in:
parent
c2e33753bb
commit
19e5f1db80
|
|
@ -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,173 @@
|
|||
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)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
@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)
|
||||
)
|
||||
|
||||
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,250 @@
|
|||
<?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="1">
|
||||
<list create="0" delete="0" edit="0">
|
||||
<field name="project_id"/>
|
||||
<field name="status"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="job_id" optional="hide"/>
|
||||
</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="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>
|
||||
|
|
@ -51,10 +51,11 @@
|
|||
'wizards/children_education_costing.xml',
|
||||
'wizards/employee_life_insurance.xml',
|
||||
'wizards/nsc_declaration.xml',
|
||||
'wizards/self_occupied_property.xml',
|
||||
'wizards/letout_house_property.xml',
|
||||
'wizards/nsc_income_loss.xml',
|
||||
# 'views/it_investment_type.xml',
|
||||
# 'views/it_investment_costing.xml'
|
||||
],
|
||||
}
|
||||
'wizards/self_occupied_property.xml',
|
||||
'wizards/letout_house_property.xml',
|
||||
'wizards/nsc_income_loss.xml',
|
||||
'data/default_investment_types.xml',
|
||||
# 'views/it_investment_type.xml',
|
||||
# 'views/it_investment_costing.xml'
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,4 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import datetime, timedelta
|
||||
import calendar
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class EmpITDeclaration(models.Model):
|
||||
|
|
@ -42,103 +39,276 @@ class EmpITDeclaration(models.Model):
|
|||
('old', 'Old Regime')
|
||||
], string="Tax Regime", required=True, default='new')
|
||||
|
||||
total_investment = fields.Float(string='Total Investment')
|
||||
|
||||
costing_details_generated = fields.Boolean(default=False)
|
||||
|
||||
investment_costing_ids = fields.One2many('investment.costings','it_declaration_id')
|
||||
house_rent_costing_id = fields.Many2one('investment.costings', compute="_compute_investment_costing")
|
||||
total_investment = fields.Float(string='Total Investment')
|
||||
|
||||
costing_details_generated = fields.Boolean(default=False)
|
||||
|
||||
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")
|
||||
is_section_open = fields.Boolean()
|
||||
@api.depends('costing_details_generated','investment_costing_ids')
|
||||
def _compute_investment_costing(self):
|
||||
for rec in self:
|
||||
if rec.investment_costing_ids and rec.costing_details_generated:
|
||||
rec.house_rent_costing_id = rec.investment_costing_ids.filtered(
|
||||
@api.depends('costing_details_generated','investment_costing_ids')
|
||||
def _compute_investment_costing(self):
|
||||
for rec in self:
|
||||
if rec.investment_costing_ids and rec.costing_details_generated:
|
||||
rec.house_rent_costing_id = rec.investment_costing_ids.filtered(
|
||||
lambda e: e.investment_type_id.investment_type == 'house_rent'
|
||||
)[:1]
|
||||
else:
|
||||
rec.house_rent_costing_id = False
|
||||
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'])])
|
||||
us80c_costings = fields.One2many('us80c.costing.type','it_declaration_id')
|
||||
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_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_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_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)])
|
||||
else:
|
||||
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_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',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_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),('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||
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_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',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
|
||||
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_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',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')
|
||||
|
||||
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_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
|
||||
))
|
||||
|
||||
us10_costings = fields.One2many('us10.costing.type','it_declaration_id')
|
||||
us80g_costings = fields.One2many('us80g.costing.type','it_declaration_id')
|
||||
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'])])
|
||||
us17_costings = fields.One2many('us17.costing.type','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_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')
|
||||
|
||||
|
||||
def toggle_section_visibility(self):
|
||||
for rec in self:
|
||||
rec.is_section_open = not rec.is_section_open
|
||||
if rec.is_section_open:
|
||||
for investment_type in rec.investment_costing_ids:
|
||||
if investment_type.investment_type_id.investment_type == 'past_employment':
|
||||
if rec.tax_regime == 'old':
|
||||
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')
|
||||
def _onchange_tax_regime(self):
|
||||
if self.tax_regime:
|
||||
# res = super(empITDeclaration, self).fields_get(allfields, attributes)
|
||||
# self.fields_get()
|
||||
if self.tax_regime == 'new':
|
||||
domain = [('investment_type_line_id.tax_regime', 'in', ['new', 'both'])]
|
||||
def toggle_section_visibility(self):
|
||||
for rec in self:
|
||||
rec.is_section_open = not rec.is_section_open
|
||||
if rec.is_section_open:
|
||||
if rec.costing_details_generated:
|
||||
rec._ensure_investment_costing_records()
|
||||
rec._update_investment_amounts()
|
||||
|
||||
@api.onchange('tax_regime')
|
||||
def _onchange_tax_regime(self):
|
||||
if self.costing_details_generated:
|
||||
self._ensure_investment_costing_records()
|
||||
self._update_investment_amounts()
|
||||
if self.tax_regime:
|
||||
# res = super(empITDeclaration, self).fields_get(allfields, attributes)
|
||||
# self.fields_get()
|
||||
if self.tax_regime == 'new':
|
||||
domain = [('investment_type_line_id.tax_regime', 'in', ['new', 'both'])]
|
||||
elif self.tax_regime == 'old':
|
||||
domain = [('investment_type_line_id.tax_regime', 'in', ['old', 'both'])]
|
||||
else:
|
||||
|
|
@ -170,105 +340,9 @@ class EmpITDeclaration(models.Model):
|
|||
# 'past_employment_costings': [('investment_type_line_id.tax_regime', 'in', ['new','both'])]
|
||||
# }
|
||||
# }
|
||||
|
||||
def generate_declarations(self):
|
||||
for rec in self:
|
||||
investment_types = self.env['it.investment.type'].sudo().search([])
|
||||
for inv_type in investment_types:
|
||||
investment_costing = self.env['investment.costings'].sudo().create({
|
||||
'investment_type_id': inv_type.id,
|
||||
'it_declaration_id': rec.id,
|
||||
})
|
||||
|
||||
if inv_type.investment_type == 'past_employment':
|
||||
past_emp_costing_ids = [
|
||||
self.env['past_employment.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.past_employment_ids
|
||||
]
|
||||
if inv_type.investment_type == 'us_80c':
|
||||
|
||||
us80c_costing_ids = [
|
||||
self.env['us80c.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.us80c_ids
|
||||
]
|
||||
if inv_type.investment_type == 'us_80d':
|
||||
us80d_costing_ids = [
|
||||
self.env['us80d.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.us80d_ids
|
||||
]
|
||||
if inv_type.investment_type == 'us_10':
|
||||
us10_costing_ids = [
|
||||
self.env['us10.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.us10_ids
|
||||
]
|
||||
if inv_type.investment_type == 'us_80g':
|
||||
us80g_costing_ids = [
|
||||
self.env['us80g.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.us80g_ids
|
||||
]
|
||||
if inv_type.investment_type == 'chapter_via':
|
||||
chapter_via_ids = [
|
||||
self.env['chapter.via.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.chapter_via_ids
|
||||
]
|
||||
if inv_type.investment_type == 'us_17':
|
||||
us17_costing_ids = [
|
||||
self.env['us17.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.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
|
||||
|
||||
def generate_declarations(self):
|
||||
for rec in self:
|
||||
rec._ensure_investment_costing_records()
|
||||
rec._update_investment_amounts()
|
||||
rec.costing_details_generated = True
|
||||
|
|
|
|||
|
|
@ -302,8 +302,6 @@ class HouseRentDeclaration(models.Model):
|
|||
def create(self, vals):
|
||||
# Auto-link applicant_id if context is passed correctly
|
||||
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)
|
||||
vals['costing_type'] = costing_id.id
|
||||
return super().create(vals)
|
||||
|
|
@ -1,9 +1,21 @@
|
|||
from odoo import models, fields
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ItInvestmentType(models.Model):
|
||||
_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()
|
||||
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'),
|
||||
('other_i_or_l', 'OTHER INCOME/LOSS'), ('other_declaration', 'OTHER DECLARATION')], string="Investment Type",
|
||||
required=True)
|
||||
regime = fields.Selection([
|
||||
('new', 'New Regime'),
|
||||
('old', 'Old Regime'),
|
||||
('both', 'Both')
|
||||
], string='Regime', required=True, default='both')
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
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')
|
||||
other_il_ids = fields.One2many('other.il.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):
|
||||
_name = 'past_employment.investment.type'
|
||||
|
|
@ -217,4 +273,4 @@ class OtherDeclarationInvestmentType(models.Model):
|
|||
# ], string="Tax Regime", required=True)
|
||||
# sequence = fields.Integer()
|
||||
# active = fields.Boolean(default=True)
|
||||
# limit = fields.Integer()
|
||||
# limit = fields.Integer()
|
||||
|
|
|
|||
|
|
@ -14,10 +14,13 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
# Inputs
|
||||
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)
|
||||
is_general_tax_statement = fields.Boolean(default=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_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_name = fields.Char(related='employee_id.name')
|
||||
|
|
@ -67,25 +70,56 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
ded_80G = fields.Float(string="Deduction under 80G", default=0.0)
|
||||
ded_other = fields.Float(string="Other Deductions", default=0.0)
|
||||
|
||||
def _get_applicable_slab(self, regime, age, residence_type):
|
||||
"""Get the applicable tax slab based on regime, age, and residence type"""
|
||||
# Determine age category
|
||||
if age < 60:
|
||||
age_category = 'below_60'
|
||||
elif age < 80:
|
||||
age_category = '60_to_80'
|
||||
else:
|
||||
age_category = 'above_80'
|
||||
comparison_available = fields.Boolean(default=False)
|
||||
old_regime_taxable_income = fields.Float(
|
||||
string="Old Regime Taxable Income",
|
||||
readonly=True
|
||||
)
|
||||
new_regime_taxable_income = fields.Float(
|
||||
string="New Regime Taxable Income",
|
||||
readonly=True
|
||||
)
|
||||
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
|
||||
slab_master = self.env['it.slab.master'].search([
|
||||
def _get_age_category(self, age):
|
||||
if age < 60:
|
||||
return 'below_60'
|
||||
elif age < 80:
|
||||
return '60_to_80'
|
||||
return 'above_80'
|
||||
|
||||
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),
|
||||
('age_category', '=', age_category),
|
||||
'|',
|
||||
('residence_type', '=', residence_type.lower()),
|
||||
('residence_type', '=', residence_type),
|
||||
('residence_type', '=', 'both')
|
||||
], 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:
|
||||
raise ValidationError(_(
|
||||
"No tax slab found for %s Regime with Age Category: %s and Residence Type: %s"
|
||||
|
|
@ -93,174 +127,272 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
|
||||
return slab_master
|
||||
|
||||
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 master rules"""
|
||||
tax = 0.0
|
||||
"""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')))
|
||||
|
||||
# Get rules sorted by min_income
|
||||
rules = slab_master.rules.sorted('min_income')
|
||||
if not rules:
|
||||
return 0.0
|
||||
|
||||
# Find which slab the taxable income falls into
|
||||
applicable_rule = None
|
||||
for rule in rules:
|
||||
if taxable <= rule.min_income:
|
||||
continue
|
||||
min_income = rule.min_income or 0
|
||||
max_income = rule.max_income if rule.max_income else float('inf')
|
||||
|
||||
# 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
|
||||
if min_income < taxable <= max_income:
|
||||
applicable_rule = rule
|
||||
break
|
||||
if not applicable_rule:
|
||||
return 0.0
|
||||
|
||||
# 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
|
||||
# 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')))
|
||||
)
|
||||
|
||||
tax += tax_for_bracket
|
||||
# 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
|
||||
|
||||
return tax
|
||||
# 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)
|
||||
|
||||
@api.depends('employee_id', 'contract_id', 'period_id')
|
||||
# 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 payroll data"""
|
||||
"""Compute salary components from the same payroll source used by the report."""
|
||||
for rec in self:
|
||||
if not rec.employee_id or not rec.contract_id:
|
||||
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
|
||||
# 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:
|
||||
# Extract salary components from payslip lines
|
||||
rec.basic_salary = self._get_salary_rule_amount(payslip, 'BASIC')
|
||||
rec.hra_salary = self._get_salary_rule_amount(payslip, 'HRA')
|
||||
rec.lta_salary = self._get_salary_rule_amount(payslip, 'LTA')
|
||||
rec.special_allowance = self._get_salary_rule_amount(payslip, 'SPA')
|
||||
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
|
||||
components = rec._get_salary_components_for_period_line(rec.period_line)
|
||||
rec.basic_salary = components['basic_salary']
|
||||
rec.hra_salary = components['hra_salary']
|
||||
rec.lta_salary = components['lta_salary']
|
||||
rec.special_allowance = components['special_allowance']
|
||||
rec.gross_salary = components['gross_salary']
|
||||
|
||||
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
|
||||
|
||||
def fetch_salary_components(self):
|
||||
"""fetch salary components from payroll data"""
|
||||
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):
|
||||
"""fetch salary components from payroll data"""
|
||||
for rec in self:
|
||||
if not rec.employee_id or not rec.contract_id:
|
||||
continue
|
||||
data = {
|
||||
'basic_salary' : {'actual':[],'projected':[]},
|
||||
'hra_salary': {'actual': [], 'projected': []},
|
||||
'lta_salary': {'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': {},
|
||||
}
|
||||
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.period_id.period_line_ids
|
||||
|
||||
for line in period_lines:
|
||||
basic_salary = float()
|
||||
hra_salary = float()
|
||||
lta_salary = float()
|
||||
special_allowance = float()
|
||||
gross_salary = float()
|
||||
payslip = self.env['hr.payslip'].search([
|
||||
('employee_id', '=', rec.employee_id.id),
|
||||
('date_from', '>=', line.from_date),
|
||||
('date_to', '<=', line.to_date),
|
||||
('state', 'in', ['verify', 'done', 'paid'])
|
||||
], limit=1)
|
||||
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:
|
||||
payslip = self.env['hr.payslip'].sudo().create({
|
||||
'name': 'Test Payslip',
|
||||
'employee_id': rec.employee_id.id,
|
||||
'date_from': line.from_date,
|
||||
'date_to': line.to_date
|
||||
})
|
||||
payslip.sudo().compute_sheet()
|
||||
|
||||
# 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')
|
||||
|
||||
payslip.sudo().action_payslip_cancel()
|
||||
payslip.sudo().unlink()
|
||||
|
||||
if line.from_date <= rec.period_line.from_date:
|
||||
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
|
||||
components = rec._get_salary_components_for_period_line(line)
|
||||
if line.from_date and rec.period_line.from_date and line.from_date <= rec.period_line.from_date:
|
||||
data['basic_salary']['actual'].append(components['basic_salary'])
|
||||
data['hra_salary']['actual'].append(components['hra_salary'])
|
||||
data['lta_salary']['actual'].append(components['lta_salary'])
|
||||
data['special_allowance']['actual'].append(components['special_allowance'])
|
||||
data['gross_salary']['actual'].append(components['gross_salary'])
|
||||
data['net_salary']['actual'].append(components['net_salary'])
|
||||
data['salary_advance']['actual'].append(components['salary_advance'])
|
||||
data['advance_recovery']['actual'].append(components['advance_recovery'])
|
||||
bucket = 'actual'
|
||||
else:
|
||||
data['basic_salary']['projected'].append(components['basic_salary'])
|
||||
data['hra_salary']['projected'].append(components['hra_salary'])
|
||||
data['lta_salary']['projected'].append(components['lta_salary'])
|
||||
data['special_allowance']['projected'].append(components['special_allowance'])
|
||||
data['gross_salary']['projected'].append(components['gross_salary'])
|
||||
data['net_salary']['projected'].append(components['net_salary'])
|
||||
data['salary_advance']['projected'].append(components['salary_advance'])
|
||||
data['advance_recovery']['projected'].append(components['advance_recovery'])
|
||||
bucket = 'projected'
|
||||
for other_component in rec._get_other_payslip_components_for_period_line(line):
|
||||
key = (other_component['code'], other_component['name'])
|
||||
if key not in data['other_components']:
|
||||
data['other_components'][key] = {
|
||||
'code': other_component['code'],
|
||||
'name': other_component['name'],
|
||||
'actual': 0.0,
|
||||
'projected': 0.0,
|
||||
}
|
||||
data['other_components'][key][bucket] += other_component['actual']
|
||||
return data
|
||||
|
||||
def _get_salary_rule_amount(self, payslip, rule_code):
|
||||
"""Get amount for a specific salary rule from payslip"""
|
||||
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):
|
||||
"""Compute deductions from payroll data"""
|
||||
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
|
||||
|
||||
# Get payslip for the period
|
||||
payslip = self.env['hr.payslip'].search([
|
||||
('employee_id', '=', rec.employee_id.id),
|
||||
('date_from', '>=', rec.period_id.from_date),
|
||||
('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
|
||||
deduction_data = rec.fetch_deduction_components()
|
||||
rec.professional_tax = (
|
||||
sum(deduction_data['professional_tax']['actual']) +
|
||||
sum(deduction_data['professional_tax']['projected'])
|
||||
)
|
||||
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.period_id.period_line_ids:
|
||||
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')
|
||||
|
|
@ -272,6 +404,23 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
else:
|
||||
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')
|
||||
def onchange_hra_exemption(self):
|
||||
"""Calculate HRA exemption based on salary components"""
|
||||
|
|
@ -319,7 +468,7 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
return min(60000.0, slab_tax)
|
||||
|
||||
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]
|
||||
|
||||
threshold = None
|
||||
|
|
@ -338,9 +487,11 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
tax_with_surcharge = total_before_mr - mr
|
||||
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
|
||||
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
|
||||
slab_tax = self._compute_tax_using_slab(taxable, slab_master)
|
||||
|
|
@ -358,7 +509,9 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
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 = 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
|
||||
|
||||
|
|
@ -374,9 +527,11 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
'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)
|
||||
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
|
||||
slab_tax = self._compute_tax_using_slab(taxable, slab_master)
|
||||
|
|
@ -393,7 +548,9 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
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]
|
||||
# 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
|
||||
|
||||
return {
|
||||
|
|
@ -421,7 +578,154 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
hp_income = -interest_allowed
|
||||
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)
|
||||
taxable_new = max(0.0, annual_gross_salary + self.other_income + hp_income - new_deductions)
|
||||
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"""
|
||||
today = date.today()
|
||||
fy_start = self.period_id.from_date
|
||||
|
|
@ -439,41 +743,20 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
fy_start = date(today.year - 1, 4, 1)
|
||||
fy_end = date(today.year, 3, 31)
|
||||
|
||||
# Calculate taxable income for both regimes
|
||||
# Old regime
|
||||
old_deductions = (
|
||||
self.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
|
||||
)
|
||||
|
||||
# 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)
|
||||
values = self._get_tax_base_values(include_comparison=include_comparison)
|
||||
salary_components_data = values['salary_components_data']
|
||||
annual_gross_salary = values['annual_gross_salary']
|
||||
gross_salary_actual = values['gross_salary_actual']
|
||||
gross_salary_projected = values['gross_salary_projected']
|
||||
annual_net_salary = values['annual_net_salary']
|
||||
selected_standard_deduction = values['selected_standard_deduction']
|
||||
old_deductions = values['old_deductions']
|
||||
new_deductions = values['new_deductions']
|
||||
hp_income = values['hp_income']
|
||||
taxable_old = values['taxable_old']
|
||||
taxable_new = values['taxable_new']
|
||||
tax_result_old = values['tax_result_old']
|
||||
tax_result_new = values['tax_result_new']
|
||||
|
||||
# Determine which regime to use
|
||||
if self.tax_regime == 'old':
|
||||
|
|
@ -485,9 +768,12 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
taxable_income = taxable_new
|
||||
chosen = 'new'
|
||||
|
||||
# Calculate tax savings
|
||||
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'
|
||||
if not tax_result:
|
||||
raise ValidationError(_("No tax calculation could be made for the selected regime."))
|
||||
|
||||
comparison_available = values['comparison_available']
|
||||
tax_savings = values['tax_savings']
|
||||
beneficial_regime = values['beneficial_regime']
|
||||
|
||||
# Prepare data structure matching screenshot format
|
||||
# Financial year (period_id)
|
||||
|
|
@ -500,16 +786,25 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
line_start = self.period_line.from_date
|
||||
current_month_index = ((line_start.year - fy_start.year) * 12 +
|
||||
(line_start.month - fy_start.month) + 1)
|
||||
tax_result['roundoff_taxable_income'] = float(round(tax_result["taxable_income"] / 10) * 10)
|
||||
birthday = self.employee_id.birthday
|
||||
if birthday:
|
||||
diff = relativedelta(date.today(), birthday)
|
||||
years_months = f"{diff.years} years {diff.months} months"
|
||||
else:
|
||||
years_months = "N/A"
|
||||
month_age = str(self.period_line.name)+ " / " + str(years_months)
|
||||
salary_components_data = self.fetch_salary_components()
|
||||
data = {
|
||||
tax_result['roundoff_taxable_income'] = float(round(tax_result["taxable_income"] / 10) * 10)
|
||||
birthday = self.employee_id.birthday
|
||||
if birthday:
|
||||
diff = relativedelta(date.today(), birthday)
|
||||
years_months = f"{diff.years} years {diff.months} months"
|
||||
else:
|
||||
years_months = "N/A"
|
||||
month_age = str(self.period_line.name)+ " / " + str(years_months)
|
||||
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 = {
|
||||
'financial_year': f"{fy_start.year}-{fy_end.year}",
|
||||
'assessment_year': fy_end.year + 1,
|
||||
'report_time': today.strftime('%d-%m-%Y %H:%M'),
|
||||
|
|
@ -539,14 +834,17 @@ 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'])},
|
||||
'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},
|
||||
'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'])},
|
||||
'net_salary': {'actual': self.gross_salary * current_month_index, 'projected': self.gross_salary * (total_months - current_month_index),
|
||||
'total': self.gross_salary * total_months}
|
||||
},
|
||||
'gross_salary': {'actual': gross_salary_actual, 'projected': gross_salary_projected, 'total': annual_gross_salary},
|
||||
'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'])},
|
||||
'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': {
|
||||
'professional_tax': self.professional_tax,
|
||||
'standard_deduction': self.standard_deduction,
|
||||
'standard_deduction': selected_standard_deduction,
|
||||
'nps_employer': self.nps_employer_contribution,
|
||||
'hra_exemption': self.hra_exemption,
|
||||
'interest_home_loan': self.interest_home_loan_self + self.interest_home_loan_letout,
|
||||
|
|
@ -560,10 +858,10 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
},
|
||||
|
||||
'income_details': {
|
||||
'gross_salary': self.gross_salary,
|
||||
'gross_salary': annual_gross_salary,
|
||||
'other_income': self.other_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) - selected_standard_deduction,
|
||||
},
|
||||
|
||||
'taxable_income': {
|
||||
|
|
@ -576,8 +874,11 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
'regime_used': chosen,
|
||||
|
||||
'comparison': {
|
||||
'old_regime_tax': tax_result_old['total_tax'],
|
||||
'new_regime_tax': tax_result_new['total_tax'],
|
||||
'available': comparison_available,
|
||||
'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,
|
||||
'beneficial_regime': beneficial_regime,
|
||||
}
|
||||
|
|
@ -586,9 +887,19 @@ class ITTaxStatementWizard(models.TransientModel):
|
|||
return {'data': data}
|
||||
|
||||
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(
|
||||
self,
|
||||
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,10 +1,9 @@
|
|||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import datetime, timedelta
|
||||
import calendar
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
import calendar
|
||||
|
||||
|
||||
class PayrollPeriod(models.Model):
|
||||
class PayrollPeriod(models.Model):
|
||||
_name = 'payroll.period'
|
||||
_description = 'Payroll Period'
|
||||
_rec_name = 'name'
|
||||
|
|
@ -15,12 +14,22 @@ class PayrollPeriod(models.Model):
|
|||
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:
|
||||
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')
|
||||
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}"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ class IncomeTaxSlabMaster(models.Model):
|
|||
_sql_constraints = [
|
||||
(
|
||||
'unique_slab',
|
||||
'unique(regime, age_category, residence_type)',
|
||||
'Slab must be unique for the same Regime, Age Category, and Residence Type!'
|
||||
'unique(period_id, regime, age_category, residence_type)',
|
||||
'Slab must be unique for the same name, Regime, Age Category, and Residence Type!'
|
||||
)
|
||||
]
|
||||
|
||||
name = fields.Char(string="Slab Name", required=True)
|
||||
period_id = fields.Many2one('payroll.period')
|
||||
regime = fields.Selection([
|
||||
('old', 'Old Tax Regime'),
|
||||
('new', 'New Tax Regime')
|
||||
|
|
@ -31,6 +32,7 @@ class IncomeTaxSlabMaster(models.Model):
|
|||
standard_deduction = fields.Float(string="Standard Deduction")
|
||||
active = fields.Boolean(default=True)
|
||||
rules = fields.One2many('it.slab.master.rules','slab_id', string="Slab Rules")
|
||||
surcharges = fields.One2many('it.sur.charge.rules','slab_id', string="Surcharges Rules")
|
||||
|
||||
class IncomeTaxSlabMasterRules(models.Model):
|
||||
_name = 'it.slab.master.rules'
|
||||
|
|
@ -44,6 +46,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)
|
||||
max_income = fields.Float(string="Max Income (₹)")
|
||||
tax_rate = fields.Float(string="Tax Rate (%)", required=True)
|
||||
|
|
@ -70,3 +76,28 @@ class IncomeTaxSlabMasterRules(models.Model):
|
|||
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')
|
||||
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}"
|
||||
)
|
||||
|
|
@ -140,23 +140,41 @@
|
|||
<td style="text-align: right;"
|
||||
t-esc="'{:,.0f}'.format(salary_components.get('special_allowance', {}).get('total', 0))"/>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Reimbursement</td>
|
||||
<td style="text-align: right;"
|
||||
t-esc="'{:,.0f}'.format(salary_components.get('reimbursement', {}).get('actual', 0))"/>
|
||||
<tr>
|
||||
<td>Reimbursement</td>
|
||||
<td style="text-align: right;"
|
||||
t-esc="'{:,.0f}'.format(salary_components.get('reimbursement', {}).get('actual', 0))"/>
|
||||
<td style="text-align: right;"
|
||||
t-esc="'{:,.0f}'.format(salary_components.get('reimbursement', {}).get('projected', 0))"/>
|
||||
<td style="text-align: right;"
|
||||
t-esc="'{:,.0f}'.format(salary_components.get('reimbursement', {}).get('total', 0))"/>
|
||||
</tr>
|
||||
<tr style="border-top: 2px solid #ddd; font-weight: bold;">
|
||||
<td>Gross Salary</td>
|
||||
<td style="text-align: right;"
|
||||
<td style="text-align: right;"
|
||||
t-esc="'{:,.0f}'.format(salary_components.get('reimbursement', {}).get('total', 0))"/>
|
||||
</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;">
|
||||
<td>Gross Salary</td>
|
||||
<td style="text-align: right;"
|
||||
t-esc="'{:,.0f}'.format(salary_components.get('gross_salary', {}).get('actual', 0))"/>
|
||||
<td style="text-align: right;"
|
||||
t-esc="'{:,.0f}'.format(salary_components.get('gross_salary', {}).get('projected', 0))"/>
|
||||
<td style="text-align: right;"
|
||||
t-esc="'{:,.0f}'.format(salary_components.get('gross_salary', {}).get('total', 0))"/>
|
||||
</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('gross_salary', {}).get('total', 0))"/>
|
||||
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>
|
||||
<td>Less: Exemption under section 10</td>
|
||||
|
|
@ -267,7 +285,7 @@
|
|||
<td style="width: 70%;">
|
||||
Round off to nearest 10 Rupee:
|
||||
</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"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -333,9 +351,9 @@
|
|||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Tax Breakdown -->
|
||||
<h4 style="font-weight: bold; border-bottom: 2px solid #ddd; padding-bottom: 5px; margin-bottom: 15px;">
|
||||
Tax Deduction Details
|
||||
<!-- Tax Breakdown -->
|
||||
<h4 style="font-weight: bold; border-bottom: 2px solid #ddd; padding-bottom: 5px; margin-bottom: 15px;">
|
||||
Tax Deduction Details
|
||||
</h4>
|
||||
<table class="table table-bordered table-sm" style="width: 100%; margin-bottom: 20px;">
|
||||
<thead>
|
||||
|
|
@ -411,9 +429,72 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</div>
|
||||
</t>
|
||||
</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">-->
|
||||
<!-- <t t-call="web.basic_layout">-->
|
||||
<!-- <main class="page">-->
|
||||
|
|
@ -723,4 +804,4 @@
|
|||
<!-- </main>-->
|
||||
<!-- </t>-->
|
||||
<!-- </template>-->
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -63,3 +63,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_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
|
||||
|
|
|
@ -34,6 +34,16 @@
|
|||
<field name="total_investment"/>
|
||||
<field name="costing_details_generated" invisible="1" force_save="1"/>
|
||||
<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>
|
||||
|
||||
|
|
@ -61,7 +71,7 @@
|
|||
<page string="Total Investment Costing">
|
||||
<group>
|
||||
<group>
|
||||
<field name="investment_costing_ids" nolabel="1">
|
||||
<field name="visible_investment_costing_ids" nolabel="1">
|
||||
<list editable="bottom" create="0" delete="0" edit="0">
|
||||
<field name="investment_type_id"/>
|
||||
<field name="amount"/>
|
||||
|
|
@ -90,7 +100,7 @@
|
|||
<!-- </list>-->
|
||||
<!-- </field>-->
|
||||
<!-- </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'">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
|
|
@ -114,9 +124,25 @@
|
|||
</field>
|
||||
</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'">
|
||||
<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'">
|
||||
<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"/>
|
||||
|
|
@ -134,12 +160,12 @@
|
|||
</field>
|
||||
|
||||
</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>
|
||||
<field name="us80d_selection_type" widget="radio" options="{'horizontal': true}" required="tax_regime == 'old' and costing_details_generated"/>
|
||||
<field name="us80d_health_checkup"/>
|
||||
</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'">
|
||||
<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"/>
|
||||
|
|
@ -149,7 +175,17 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</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'">
|
||||
<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'">
|
||||
<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"/>
|
||||
|
|
@ -160,7 +196,29 @@
|
|||
</list>
|
||||
|
||||
</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'">
|
||||
<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'">
|
||||
<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'">
|
||||
<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"/>
|
||||
|
|
@ -172,8 +230,18 @@
|
|||
|
||||
</field>
|
||||
</page>
|
||||
<page name="us_10_costing" string="US 10" invisible="tax_regime != 'old'">
|
||||
<field name="us10_costings">
|
||||
<page name="us_10_costing" string="US 10" invisible="not show_us_10">
|
||||
<field name="us10_costings" invisible="tax_regime != 'old'">
|
||||
<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'">
|
||||
<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"/>
|
||||
|
|
@ -184,8 +252,18 @@
|
|||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page name="us_80g_costing" string="US 80G" invisible="tax_regime != 'old'">
|
||||
<field name="us80g_costings">
|
||||
<page name="us_80g_costing" string="US 80G" invisible="not show_us_80g">
|
||||
<field name="us80g_costings" invisible="tax_regime != 'old'">
|
||||
<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'">
|
||||
<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"/>
|
||||
|
|
@ -197,7 +275,7 @@
|
|||
</field>
|
||||
</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'">
|
||||
<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}"/>
|
||||
|
|
@ -220,8 +298,18 @@
|
|||
</field>
|
||||
|
||||
</page>
|
||||
<page name="us_17_costings" string="US 17" invisible="tax_regime != 'old'">
|
||||
<field name="us17_costings">
|
||||
<page name="us_17_costings" string="US 17" invisible="not show_us_17">
|
||||
<field name="us17_costings" invisible="tax_regime != 'old'">
|
||||
<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'">
|
||||
<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"/>
|
||||
|
|
@ -232,7 +320,7 @@
|
|||
</list>
|
||||
</field>
|
||||
</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_costings" context="{
|
||||
'default_costing_type': house_rent_costing_id
|
||||
|
|
@ -284,7 +372,7 @@
|
|||
</form>
|
||||
</field>
|
||||
</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'">
|
||||
<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}"/>
|
||||
|
|
@ -319,12 +407,21 @@
|
|||
<field name="limit" readonly="1" force_save="1"/>
|
||||
</list>
|
||||
</field>
|
||||
|
||||
</page>
|
||||
<page name="other_declaration_costings" string="Other Declarations" invisible="tax_regime != 'old'">
|
||||
<field name="other_declaration_costings">
|
||||
<page name="other_declaration_costings" string="Other Declarations" invisible="not show_other_declaration">
|
||||
<field name="other_declaration_costings" invisible="tax_regime != 'old'">
|
||||
<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'">
|
||||
<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"/>
|
||||
|
|
@ -352,4 +449,4 @@
|
|||
action="action_emp_it_declaration" sequence="99"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -19,12 +19,13 @@
|
|||
<field name="arch" type="xml">
|
||||
<form string="Investment Type">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="investment_type"/>
|
||||
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="investment_type"/>
|
||||
<field name="regime"/>
|
||||
<field name="period_ids" widget="many2many_tags"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Past Employment" invisible="investment_type != 'past_employment'">
|
||||
<field name="past_employment_ids">
|
||||
|
|
@ -153,4 +154,4 @@
|
|||
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<list>
|
||||
<field name="employee_id"/>
|
||||
<field name="period_id"/>
|
||||
<field name="period_line"/>
|
||||
<field name="period_line" force_save="1"/>
|
||||
<field name="tax_regime"/>
|
||||
</list>
|
||||
</field>
|
||||
|
|
@ -21,13 +21,27 @@
|
|||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_generate_report"
|
||||
string="Generate Tax Statement"
|
||||
type="object"
|
||||
<button name="action_generate_report"
|
||||
string="Generate Tax Statement"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
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-file-text"/>
|
||||
icon="fa-balance-scale"
|
||||
invisible="not comparison_available"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="comparison_available" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="is_general_tax_statement" invisible="1"/>
|
||||
<group>
|
||||
<field name="employee_id" options="{'no_edit': True, 'no_create': True}"/>
|
||||
<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}"/>
|
||||
</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" domain="[('period_id', '=', period_id),('to_date','<',datetime.datetime.now())]" options="{'no_edit': True, 'no_create': True, 'no_open': True}" invisible="emp_doj"/>
|
||||
<field name="period_line" force_save="1" domain="[('period_id', '=', period_id),('to_date','<',datetime.datetime.now())]"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}" on_change="1"/>
|
||||
</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>
|
||||
</form>
|
||||
</field>
|
||||
|
|
@ -95,7 +122,19 @@
|
|||
<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="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="binding_type">report</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="period_id"/>
|
||||
<field name="regime"/>
|
||||
<field name="age_category"/>
|
||||
<field name="residence_type"/>
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="period_id"/>
|
||||
<field name="regime"/>
|
||||
<field name="age_category"/>
|
||||
<field name="residence_type"/>
|
||||
|
|
@ -52,16 +54,27 @@
|
|||
<page string="Slab Rules">
|
||||
<field name="rules">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget='handle'/>
|
||||
<field name="min_income"/>
|
||||
<field name="max_income"/>
|
||||
<field name="tax_rate"/>
|
||||
<field name="fixed_amount"/>
|
||||
<field name="excess_threshold"/>
|
||||
<field name="surcharge_rate"/>
|
||||
<field name="excess_threshold" optional="hide"/>
|
||||
<field name="surcharge_rate" column_invisible="1" invisible="1"/>
|
||||
<field name="cess_rate"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Surcharge Rules">
|
||||
<field name="rules">
|
||||
<list editable="bottom">
|
||||
<field name="min_income"/>
|
||||
<field name="max_income"/>
|
||||
<field name="surcharge_rate"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -100,7 +100,7 @@ result_name = inputs['ATTACH_SALARY'].name
|
|||
</record>
|
||||
|
||||
<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="sequence">174</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="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
result = -inputs['ASSIG_SALARY'].amount
|
||||
result = inputs['ASSIG_SALARY'].amount
|
||||
result_name = inputs['ASSIG_SALARY'].name
|
||||
</field>
|
||||
<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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import hr_salary_advance
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.date_utils import start_of
|
||||
|
||||
|
||||
class HrSalaryAdvance(models.Model):
|
||||
_name = 'hr.salary.advance'
|
||||
_description = 'Salary Advance'
|
||||
_inherit = ['mail.thread']
|
||||
_order = 'date_advance desc, id desc'
|
||||
|
||||
name = fields.Char(default='New', copy=False, readonly=True)
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
string='Employee',
|
||||
required=True,
|
||||
tracking=True,
|
||||
domain=lambda self: [('company_id', 'in', self.env.companies.ids)],
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
|
||||
description = fields.Char(required=True, default='Salary Advance', tracking=True)
|
||||
date_advance = fields.Date(
|
||||
string='Advance Payslip Date',
|
||||
required=True,
|
||||
default=lambda self: start_of(fields.Date.today(), 'month'),
|
||||
tracking=True,
|
||||
)
|
||||
deduction_start_date = fields.Date(
|
||||
string='Deduction Start Date',
|
||||
required=True,
|
||||
default=lambda self: start_of(fields.Date.today() + relativedelta(months=1), 'month'),
|
||||
tracking=True,
|
||||
)
|
||||
advance_amount = fields.Monetary(required=True, tracking=True)
|
||||
deduction_period_months = fields.Integer(string='Deduction Period', required=True, default=1, tracking=True)
|
||||
monthly_deduction_amount = fields.Monetary(string='Monthly Deduction', required=True, tracking=True)
|
||||
date_estimated_end = fields.Date(string='Estimated Deduction End', compute='_compute_date_estimated_end')
|
||||
assignment_attachment_id = fields.Many2one(
|
||||
'hr.salary.attachment',
|
||||
string='Advance Salary Attachment',
|
||||
copy=False,
|
||||
readonly=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
deduction_attachment_id = fields.Many2one(
|
||||
'hr.salary.attachment',
|
||||
string='Deduction Salary Attachment',
|
||||
copy=False,
|
||||
readonly=True,
|
||||
ondelete='restrict',
|
||||
)
|
||||
attachment_count = fields.Integer(compute='_compute_attachment_count')
|
||||
state = fields.Selection(
|
||||
[
|
||||
('open', 'Running'),
|
||||
('close', 'Completed'),
|
||||
('cancel', 'Cancelled'),
|
||||
],
|
||||
string='Status',
|
||||
compute='_compute_state',
|
||||
store=True,
|
||||
readonly=False,
|
||||
default='open',
|
||||
required=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
@api.depends('assignment_attachment_id.state', 'deduction_attachment_id.state')
|
||||
def _compute_state(self):
|
||||
for advance in self:
|
||||
attachments = (advance.assignment_attachment_id | advance.deduction_attachment_id).exists()
|
||||
states = set(attachments.mapped('state'))
|
||||
if attachments and states == {'cancel'}:
|
||||
advance.state = 'cancel'
|
||||
elif attachments and states == {'close'}:
|
||||
advance.state = 'close'
|
||||
elif 'cancel' in states and 'open' not in states:
|
||||
advance.state = 'cancel'
|
||||
else:
|
||||
advance.state = 'open'
|
||||
|
||||
@api.depends('deduction_start_date', 'deduction_period_months')
|
||||
def _compute_date_estimated_end(self):
|
||||
for advance in self:
|
||||
if advance.deduction_start_date and advance.deduction_period_months:
|
||||
advance.date_estimated_end = start_of(
|
||||
advance.deduction_start_date + relativedelta(months=advance.deduction_period_months - 1),
|
||||
'month',
|
||||
)
|
||||
else:
|
||||
advance.date_estimated_end = False
|
||||
|
||||
@api.depends('assignment_attachment_id', 'deduction_attachment_id')
|
||||
def _compute_attachment_count(self):
|
||||
for advance in self:
|
||||
advance.attachment_count = len(
|
||||
(advance.assignment_attachment_id | advance.deduction_attachment_id).exists()
|
||||
)
|
||||
|
||||
@api.onchange('advance_amount', 'deduction_period_months')
|
||||
def _onchange_monthly_deduction_amount(self):
|
||||
for advance in self:
|
||||
if advance.advance_amount and advance.deduction_period_months:
|
||||
advance.monthly_deduction_amount = advance.advance_amount / advance.deduction_period_months
|
||||
|
||||
@api.constrains('advance_amount', 'monthly_deduction_amount', 'deduction_period_months')
|
||||
def _check_amounts(self):
|
||||
for advance in self:
|
||||
if advance.advance_amount <= 0:
|
||||
raise ValidationError(_('Advance amount must be strictly positive.'))
|
||||
if advance.monthly_deduction_amount <= 0:
|
||||
raise ValidationError(_('Monthly deduction must be strictly positive.'))
|
||||
if advance.deduction_period_months <= 0:
|
||||
raise ValidationError(_('Deduction period must be at least one month.'))
|
||||
if advance.monthly_deduction_amount * advance.deduction_period_months < advance.advance_amount:
|
||||
raise ValidationError(_('Monthly deduction does not cover the advance amount in the selected period.'))
|
||||
|
||||
@api.constrains('date_advance', 'deduction_start_date')
|
||||
def _check_dates(self):
|
||||
for advance in self:
|
||||
if advance.deduction_start_date < advance.date_advance:
|
||||
raise ValidationError(_('Deduction start date cannot be before the advance payslip date.'))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
advances = super().create(vals_list)
|
||||
for advance in advances:
|
||||
if advance.name == 'New':
|
||||
advance.name = self.env['ir.sequence'].next_by_code('hr.salary.advance') or _('Salary Advance')
|
||||
advance._create_salary_attachments()
|
||||
return advances
|
||||
|
||||
def write(self, vals):
|
||||
sync_fields = {
|
||||
'employee_id', 'company_id', 'description', 'date_advance', 'deduction_start_date',
|
||||
'advance_amount', 'monthly_deduction_amount',
|
||||
}
|
||||
if sync_fields.intersection(vals):
|
||||
for advance in self:
|
||||
advance._check_editable_attachments()
|
||||
result = super().write(vals)
|
||||
if sync_fields.intersection(vals):
|
||||
for advance in self.filtered(lambda rec: rec.state == 'open'):
|
||||
advance._sync_salary_attachments()
|
||||
return result
|
||||
|
||||
def _get_assignment_input_type(self):
|
||||
return self.env.ref('hr_payroll.input_assignment_salary')
|
||||
|
||||
def _get_deduction_input_type(self):
|
||||
return self.env.ref('hr_payroll.input_attachment_salary')
|
||||
|
||||
def _check_editable_attachments(self):
|
||||
self.ensure_one()
|
||||
attachments = self.assignment_attachment_id | self.deduction_attachment_id
|
||||
if any(attachments.mapped('has_done_payslip')):
|
||||
raise UserError(_('You cannot change this salary advance because one of its salary attachments already has a done payslip.'))
|
||||
|
||||
def _create_salary_attachments(self):
|
||||
for advance in self:
|
||||
if advance.assignment_attachment_id or advance.deduction_attachment_id:
|
||||
continue
|
||||
assignment = self.env['hr.salary.attachment'].create(advance._prepare_assignment_attachment_vals())
|
||||
deduction = self.env['hr.salary.attachment'].create(advance._prepare_deduction_attachment_vals())
|
||||
advance.write({
|
||||
'assignment_attachment_id': assignment.id,
|
||||
'deduction_attachment_id': deduction.id,
|
||||
})
|
||||
|
||||
def _sync_salary_attachments(self):
|
||||
for advance in self:
|
||||
if not advance.assignment_attachment_id or not advance.deduction_attachment_id:
|
||||
advance._create_salary_attachments()
|
||||
continue
|
||||
advance.assignment_attachment_id.write(advance._prepare_assignment_attachment_vals())
|
||||
advance.deduction_attachment_id.write(advance._prepare_deduction_attachment_vals())
|
||||
|
||||
def _prepare_common_attachment_vals(self, input_type):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'employee_ids': [(6, 0, self.employee_id.ids)],
|
||||
'company_id': self.company_id.id,
|
||||
'description': self.description,
|
||||
'other_input_type_id': input_type.id,
|
||||
}
|
||||
|
||||
def _prepare_assignment_attachment_vals(self):
|
||||
vals = self._prepare_common_attachment_vals(self._get_assignment_input_type())
|
||||
vals.update({
|
||||
'monthly_amount': self.advance_amount,
|
||||
'total_amount': self.advance_amount,
|
||||
'paid_amount': 0.0,
|
||||
'date_start': self.date_advance,
|
||||
})
|
||||
return vals
|
||||
|
||||
def _prepare_deduction_attachment_vals(self):
|
||||
vals = self._prepare_common_attachment_vals(self._get_deduction_input_type())
|
||||
vals.update({
|
||||
'monthly_amount': self.monthly_deduction_amount,
|
||||
'total_amount': self.advance_amount,
|
||||
'paid_amount': 0.0,
|
||||
'date_start': self.deduction_start_date,
|
||||
})
|
||||
return vals
|
||||
|
||||
def action_open_attachments(self):
|
||||
self.ensure_one()
|
||||
attachments = (self.assignment_attachment_id | self.deduction_attachment_id).exists()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Salary Attachments'),
|
||||
'res_model': 'hr.salary.attachment',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('id', 'in', attachments.ids)],
|
||||
}
|
||||
|
||||
def action_cancel(self):
|
||||
for advance in self:
|
||||
attachments = (advance.assignment_attachment_id | advance.deduction_attachment_id).filtered(
|
||||
lambda attachment: attachment.state == 'open'
|
||||
)
|
||||
for attachment in attachments:
|
||||
attachment.action_cancel()
|
||||
advance.state = 'cancel'
|
||||
|
||||
def action_open(self):
|
||||
for advance in self:
|
||||
attachments = (advance.assignment_attachment_id | advance.deduction_attachment_id).filtered(
|
||||
lambda attachment: attachment.state == 'cancel'
|
||||
)
|
||||
for attachment in attachments:
|
||||
attachment.action_open()
|
||||
advance.state = 'open'
|
||||
|
||||
def action_done(self):
|
||||
for advance in self:
|
||||
attachments = (advance.assignment_attachment_id | advance.deduction_attachment_id).filtered(
|
||||
lambda attachment: attachment.state == 'open'
|
||||
)
|
||||
for attachment in attachments:
|
||||
attachment.action_done()
|
||||
advance.state = 'close'
|
||||
|
||||
def unlink(self):
|
||||
for advance in self:
|
||||
attachments = advance.assignment_attachment_id | advance.deduction_attachment_id
|
||||
if any(attachment.state == 'open' for attachment in attachments):
|
||||
raise UserError(_('Cancel the salary advance before deleting it.'))
|
||||
return super().unlink()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_salary_advance_user,hr.salary.advance.user,model_hr_salary_advance,hr_payroll.group_hr_payroll_user,1,1,1,0
|
||||
access_hr_salary_advance_manager,hr.salary.advance.manager,model_hr_salary_advance,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_salary_advance_view_tree" model="ir.ui.view">
|
||||
<field name="name">hr.salary.advance.list</field>
|
||||
<field name="model">hr.salary.advance</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Salary Advances">
|
||||
<field name="name"/>
|
||||
<field name="employee_id" widget="many2one_avatar_employee"/>
|
||||
<field name="date_advance"/>
|
||||
<field name="deduction_start_date"/>
|
||||
<field name="advance_amount" widget="monetary" sum="Advance Amount"/>
|
||||
<field name="monthly_deduction_amount" widget="monetary" sum="Monthly Deduction"/>
|
||||
<field name="deduction_period_months"/>
|
||||
<field name="state" widget="badge" decoration-success="state == 'open'" decoration-info="state == 'close'" decoration-danger="state == 'cancel'"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_advance_view_form" model="ir.ui.view">
|
||||
<field name="name">hr.salary.advance.form</field>
|
||||
<field name="model">hr.salary.advance</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Salary Advance">
|
||||
<header>
|
||||
<button name="action_done" type="object" string="Mark as Completed" invisible="state != 'open'"/>
|
||||
<button name="action_open" type="object" string="Set to Running" invisible="state not in ['close', 'cancel']"/>
|
||||
<button name="action_cancel" type="object" string="Cancel" invisible="state != 'open'"/>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button type="object" name="action_open_attachments" class="oe_stat_button" icon="fa-files-o" invisible="attachment_count == 0">
|
||||
<field name="attachment_count" widget="statinfo" string="Attachments"/>
|
||||
</button>
|
||||
</div>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="attachment_count" invisible="1"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="employee_id" widget="many2one_avatar_employee" readonly="state != 'open'"/>
|
||||
<field name="description" readonly="state != 'open'"/>
|
||||
<field name="date_advance" readonly="state != 'open'"/>
|
||||
<field name="deduction_start_date" readonly="state != 'open'"/>
|
||||
<field name="date_estimated_end"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="advance_amount" readonly="state != 'open'"/>
|
||||
<field name="deduction_period_months" readonly="state != 'open'"/>
|
||||
<field name="monthly_deduction_amount" readonly="state != 'open'"/>
|
||||
<field name="assignment_attachment_id" readonly="1"/>
|
||||
<field name="deduction_attachment_id" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_advance_view_search" model="ir.ui.view">
|
||||
<field name="name">hr.salary.advance.search</field>
|
||||
<field name="model">hr.salary.advance</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Salary Advances">
|
||||
<field name="name"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="description"/>
|
||||
<filter string="Running" name="running" domain="[('state', '=', 'open')]"/>
|
||||
<filter string="Completed" name="completed" domain="[('state', '=', 'close')]"/>
|
||||
<filter string="Cancelled" name="cancelled" domain="[('state', '=', 'cancel')]"/>
|
||||
<filter string="Employee" name="employee" context="{'group_by': 'employee_id'}"/>
|
||||
<filter string="Status" name="status" context="{'group_by': 'state'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_salary_advance_action" model="ir.actions.act_window">
|
||||
<field name="name">Salary Advances</field>
|
||||
<field name="res_model">hr.salary.advance</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="context">{'search_default_running': 1}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<menuitem
|
||||
id="hr_payroll.menu_hr_payroll_employees_root"
|
||||
name="Contracts"
|
||||
parent="hr_payroll.menu_hr_payroll_root"
|
||||
sequence="1"
|
||||
action="hr_contract.action_hr_contract"
|
||||
groups="hr_contract.group_hr_contract_employee_manager"/>
|
||||
|
||||
<menuitem
|
||||
id="hr_payroll.hr_menu_all_contracts"
|
||||
name="Contracts"
|
||||
action="hr_contract.action_hr_contract"
|
||||
parent="hr_payroll.menu_hr_payroll_employees_root"
|
||||
active="0"
|
||||
sequence="90"/>
|
||||
<menuitem
|
||||
id="menu_hr_salary_advance_configuration"
|
||||
name="Salary Advance"
|
||||
parent="hr_payroll.menu_hr_payroll_root"
|
||||
sequence="75"
|
||||
groups="hr_payroll.group_hr_payroll_manager"
|
||||
action="hr_salary_advance_action"
|
||||
/>
|
||||
<menuitem
|
||||
id="hr_payroll.menu_hr_payroll_work_entries_root"
|
||||
name="Work Entries"
|
||||
parent="hr_payroll.menu_hr_payroll_configuration"
|
||||
sequence="100"
|
||||
groups="hr_payroll.group_hr_payroll_user"/>
|
||||
<menuitem
|
||||
id="hr_payroll.menu_hr_payroll_report"
|
||||
name="Reporting"
|
||||
parent="hr_payroll.menu_hr_payroll_root"
|
||||
sequence="79"
|
||||
groups="hr_payroll.group_hr_payroll_manager"/>
|
||||
|
||||
|
||||
|
||||
<menuitem
|
||||
id="hr_payroll.hr_menu_salary_attachments"
|
||||
name="Salary Attachments"
|
||||
action="hr_payroll.hr_salary_attachment_action"
|
||||
parent="hr_payroll.menu_hr_salary_configuration"
|
||||
groups="hr_payroll.group_hr_payroll_user"
|
||||
active="1"
|
||||
sequence="35"/>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "HR Recruitment Auto Document",
|
||||
"summary": "Parse resumes and auto-create recruitment records",
|
||||
"version": "1.0.0",
|
||||
"category": "Human Resources/Recruitment",
|
||||
"author": "Pranay",
|
||||
"website": "https://www.ftprotech.com",
|
||||
"license": "LGPL-3",
|
||||
"depends": [
|
||||
"document_parser",
|
||||
"hr_recruitment_extended",
|
||||
"hr_recruitment_skills",
|
||||
],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"wizard/hr_recruitment_auto_doc_wizard_views.xml",
|
||||
"views/hr_applicant_views.xml",
|
||||
"views/hr_candidate_views.xml",
|
||||
"views/hr_job_recruitment_views.xml",
|
||||
"views/hr_recruitment_actions.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"hr_recruitment_auto_doc/static/src/js/recruitment_auto_doc_list.js",
|
||||
"hr_recruitment_auto_doc/static/src/js/recruitment_auto_doc_kanban.js",
|
||||
"hr_recruitment_auto_doc/static/src/js/job_skill_transfer_field.js",
|
||||
"hr_recruitment_auto_doc/static/src/js/many2many_binary_dropzone_field.js",
|
||||
"hr_recruitment_auto_doc/static/src/scss/recruitment_auto_doc_widgets.scss",
|
||||
"hr_recruitment_auto_doc/static/src/xml/job_skill_transfer_field.xml",
|
||||
"hr_recruitment_auto_doc/static/src/xml/many2many_binary_dropzone_field.xml",
|
||||
"hr_recruitment_auto_doc/static/src/xml/recruitment_auto_doc_buttons.xml",
|
||||
],
|
||||
},
|
||||
"installable": True,
|
||||
"application": False,
|
||||
"auto_install": False,
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import hr_applicant
|
||||
from . import hr_candidate
|
||||
from . import hr_job_recruitment
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
from odoo import _, models
|
||||
|
||||
|
||||
class HrApplicant(models.Model):
|
||||
_inherit = "hr.applicant"
|
||||
|
||||
def action_open_auto_doc_wizard(self):
|
||||
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_applicant").read()[0]
|
||||
context = dict(self.env.context)
|
||||
if len(self) == 1 and self.hr_job_recruitment:
|
||||
context["default_job_recruitment_id"] = self.hr_job_recruitment.id
|
||||
action["context"] = context
|
||||
action["name"] = _("Parse Resumes")
|
||||
return action
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
from odoo import _, models
|
||||
|
||||
|
||||
class HrCandidate(models.Model):
|
||||
_inherit = "hr.candidate"
|
||||
|
||||
def action_open_auto_doc_wizard(self):
|
||||
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_candidate").read()[0]
|
||||
action["context"] = dict(self.env.context)
|
||||
action["name"] = _("Parse Resumes")
|
||||
return action
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
from odoo import _, models
|
||||
|
||||
|
||||
class HrJobRecruitment(models.Model):
|
||||
_inherit = "hr.job.recruitment"
|
||||
|
||||
def action_open_auto_doc_wizard(self):
|
||||
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_job_recruitment").read()[0]
|
||||
context = dict(self.env.context)
|
||||
if len(self) == 1:
|
||||
context["default_job_recruitment_id"] = self.id
|
||||
action["context"] = context
|
||||
action["name"] = _("Parse Job Description")
|
||||
return action
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_recruitment_auto_doc_wizard,hr.recruitment.auto.doc.wizard,model_hr_recruitment_auto_doc_wizard,base.group_user,1,1,1,1
|
||||
access_hr_recruitment_auto_doc_wizard_line,hr.recruitment.auto.doc.wizard.line,model_hr_recruitment_auto_doc_wizard_line,base.group_user,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
many2ManyTagsField,
|
||||
Many2ManyTagsField,
|
||||
} from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
|
||||
export class JobSkillTransferField extends Many2ManyTagsField {
|
||||
static template = "hr_recruitment_auto_doc.JobSkillTransferField";
|
||||
static props = {
|
||||
...Many2ManyTagsField.props,
|
||||
transferTarget: { type: String, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
}
|
||||
|
||||
get tags() {
|
||||
return this.props.record.data[this.props.name].records.map((record) => {
|
||||
const tag = this.getTagProps(record);
|
||||
return {
|
||||
...tag,
|
||||
draggable: !this.props.readonly && this.props.record.isInEdition,
|
||||
onDragStart: (ev) => this.onTagDragStart(ev, record),
|
||||
onDragEnd: () => this.onTagDragEnd(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onTagDragStart(ev, record) {
|
||||
if (this.props.readonly || !this.props.transferTarget) {
|
||||
return;
|
||||
}
|
||||
ev.dataTransfer.effectAllowed = "move";
|
||||
ev.dataTransfer.setData(
|
||||
"text/plain",
|
||||
JSON.stringify({
|
||||
resId: record.resId,
|
||||
fromField: this.props.name,
|
||||
targetField: this.props.transferTarget,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onTagDragEnd() {
|
||||
// noop, but keeps the template hook simple
|
||||
}
|
||||
|
||||
onDragOver(ev) {
|
||||
if (this.props.readonly || !this.props.transferTarget) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
|
||||
async onDrop(ev) {
|
||||
if (this.props.readonly || !this.props.transferTarget) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(ev.dataTransfer.getData("text/plain") || "{}");
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
const { resId, fromField } = payload;
|
||||
if (!resId || !fromField || fromField === this.props.name) {
|
||||
return;
|
||||
}
|
||||
const sourceList = this.props.record.data[fromField];
|
||||
const targetList = this.props.record.data[this.props.name];
|
||||
const sourceRecord = sourceList.records.find((record) => record.resId === resId);
|
||||
const alreadyExists = targetList.records.some((record) => record.resId === resId);
|
||||
if (!sourceRecord || alreadyExists) {
|
||||
return;
|
||||
}
|
||||
await targetList.addAndRemove({ add: [resId] });
|
||||
await sourceList.forget(sourceRecord);
|
||||
}
|
||||
}
|
||||
|
||||
export const jobSkillTransferField = {
|
||||
...many2ManyTagsField,
|
||||
component: JobSkillTransferField,
|
||||
displayName: _t("Tags With Transfer"),
|
||||
supportedOptions: [
|
||||
...(many2ManyTagsField.supportedOptions || []),
|
||||
{
|
||||
label: _t("Transfer target"),
|
||||
name: "transfer_target",
|
||||
type: "string",
|
||||
},
|
||||
],
|
||||
extractProps: ({ attrs, options, string }, dynamicInfo) => ({
|
||||
...(many2ManyTagsField.extractProps
|
||||
? many2ManyTagsField.extractProps({ attrs, options, string }, dynamicInfo)
|
||||
: {}),
|
||||
transferTarget: options.transfer_target,
|
||||
}),
|
||||
};
|
||||
|
||||
registry.category("fields").add("job_skill_transfer", jobSkillTransferField);
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useFileUploader } from "@web/core/utils/files";
|
||||
import { FileInput } from "@web/core/file_input/file_input";
|
||||
import { many2ManyBinaryField, Many2ManyBinaryField } from "@web/views/fields/many2many_binary/many2many_binary_field";
|
||||
|
||||
import { useState } from "@odoo/owl";
|
||||
|
||||
export class Many2ManyBinaryDropzoneField extends Many2ManyBinaryField {
|
||||
static template = "hr_recruitment_auto_doc.Many2ManyBinaryDropzoneField";
|
||||
static components = {
|
||||
...Many2ManyBinaryField.components,
|
||||
FileInput,
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.uploadFiles = useFileUploader();
|
||||
this.state = useState({
|
||||
isDragging: false,
|
||||
});
|
||||
}
|
||||
|
||||
get dropzoneText() {
|
||||
return _t("Drag and drop files here, or click to upload");
|
||||
}
|
||||
|
||||
onDragEnter(ev) {
|
||||
ev.preventDefault();
|
||||
this.state.isDragging = true;
|
||||
}
|
||||
|
||||
onDragOver(ev) {
|
||||
ev.preventDefault();
|
||||
this.state.isDragging = true;
|
||||
}
|
||||
|
||||
onDragLeave(ev) {
|
||||
ev.preventDefault();
|
||||
this.state.isDragging = false;
|
||||
}
|
||||
|
||||
async onFilesDropped(ev) {
|
||||
ev.preventDefault();
|
||||
this.state.isDragging = false;
|
||||
const droppedFiles = [...(ev.dataTransfer?.files || [])];
|
||||
if (!droppedFiles.length) {
|
||||
return;
|
||||
}
|
||||
const parsedFileData = await this.uploadFiles("/web/binary/upload_attachment", {
|
||||
csrf_token: odoo.csrf_token,
|
||||
ufile: droppedFiles,
|
||||
model: this.props.record.resModel,
|
||||
id: this.props.record.resId || 0,
|
||||
});
|
||||
if (parsedFileData) {
|
||||
await this.onFileUploaded(parsedFileData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const many2ManyBinaryDropzoneField = {
|
||||
...many2ManyBinaryField,
|
||||
component: Many2ManyBinaryDropzoneField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("many2many_binary_dropzone", many2ManyBinaryDropzoneField);
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
|
||||
export class RecruitmentAutoDocKanbanController extends KanbanController {
|
||||
static template = "hr_recruitment_auto_doc.KanbanView";
|
||||
|
||||
async openParseWizard() {
|
||||
const actionMap = {
|
||||
"hr.applicant": "hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_applicant",
|
||||
"hr.candidate": "hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_candidate",
|
||||
"hr.job.recruitment": "hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_job_recruitment",
|
||||
};
|
||||
const actionXmlId = actionMap[this.model.config.resModel];
|
||||
if (!actionXmlId) {
|
||||
return;
|
||||
}
|
||||
const activeIds = this.model.root.selection.map((record) => record.resId);
|
||||
await this.actionService.doAction(actionXmlId, {
|
||||
additionalContext: {
|
||||
active_model: this.model.config.resModel,
|
||||
active_ids: activeIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("views").add("hr_recruitment_auto_doc_kanban", {
|
||||
...kanbanView,
|
||||
buttonTemplate: "hr_recruitment_auto_doc.KanbanButtons",
|
||||
Controller: RecruitmentAutoDocKanbanController,
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
|
||||
export class RecruitmentAutoDocListController extends ListController {
|
||||
static template = "hr_recruitment_auto_doc.ListView";
|
||||
|
||||
async openParseWizard() {
|
||||
const actionMap = {
|
||||
"hr.applicant": "hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_applicant",
|
||||
"hr.candidate": "hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_candidate",
|
||||
"hr.job.recruitment": "hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_job_recruitment",
|
||||
};
|
||||
const actionXmlId = actionMap[this.model.config.resModel];
|
||||
if (!actionXmlId) {
|
||||
return;
|
||||
}
|
||||
const activeIds = this.model.root.selection.map((record) => record.resId);
|
||||
await this.actionService.doAction(actionXmlId, {
|
||||
additionalContext: {
|
||||
active_model: this.model.config.resModel,
|
||||
active_ids: activeIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("views").add("hr_recruitment_auto_doc_list", {
|
||||
...listView,
|
||||
buttonTemplate: "hr_recruitment_auto_doc.ListButtons",
|
||||
Controller: RecruitmentAutoDocListController,
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
.o_job_skill_transfer_tags .o_tag {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.o_job_skill_transfer_tags .o_tag:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.o_recruitment_dropzone_field {
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
padding: 18px;
|
||||
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.o_recruitment_dropzone_field_dragging {
|
||||
border-color: #0d6efd;
|
||||
background: #eff6ff;
|
||||
box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.08);
|
||||
}
|
||||
|
||||
.o_recruitment_dropzone_box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 18px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_recruitment_dropzone_icon {
|
||||
font-size: 30px;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.o_recruitment_dropzone_text {
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="hr_recruitment_auto_doc.JobSkillTransferField">
|
||||
<div class="o_field_tags d-inline-flex flex-wrap gap-1 mw-100 o_job_skill_transfer_tags"
|
||||
t-att-class="{'o_tags_input o_input': !props.readonly}"
|
||||
t-ref="many2ManyTagsField"
|
||||
t-on-dragover="onDragOver"
|
||||
t-on-drop="onDrop">
|
||||
<t t-foreach="tags" t-as="tag" t-key="tag.id or tag_index">
|
||||
<span
|
||||
class="o_tag position-relative d-inline-flex align-items-center user-select-none mw-100"
|
||||
t-att-class="{
|
||||
'o_badge badge rounded-pill lh-1': true,
|
||||
'cursor-pointer': tag.canEdit,
|
||||
}"
|
||||
t-attf-class="o_tag_color_{{ tag.colorIndex ? tag.colorIndex : '0' }}"
|
||||
tabindex="-1"
|
||||
t-att-data-color="tag.colorIndex"
|
||||
t-att-title="tag.text"
|
||||
t-att-draggable="tag.draggable ? 'true' : 'false'"
|
||||
t-on-dragstart="tag.onDragStart"
|
||||
t-on-dragend="tag.onDragEnd"
|
||||
t-on-keydown="tag.onKeydown">
|
||||
<div class="o_tag_badge_text text-truncate" t-out="tag.text"/>
|
||||
<a
|
||||
t-if="tag.onDelete"
|
||||
t-on-click.stop.prevent="(ev) => tag.onDelete and tag.onDelete(ev)"
|
||||
class="o_delete d-flex align-items-center ps-1 opacity-75"
|
||||
title="Delete"
|
||||
aria-label="Delete"
|
||||
tabIndex="-1"
|
||||
href="#">
|
||||
<i class="oi oi-close align-text-top"/>
|
||||
</a>
|
||||
</span>
|
||||
</t>
|
||||
<div t-if="showM2OSelectionField" class="o_field_many2many_selection d-inline-flex w-100" t-ref="autoComplete">
|
||||
<Many2XAutocomplete
|
||||
id="props.id"
|
||||
placeholder="tags.length ? '' : props.placeholder"
|
||||
resModel="relation"
|
||||
autoSelect="true"
|
||||
fieldString="string"
|
||||
activeActions="activeActions"
|
||||
update="update"
|
||||
quickCreate="activeActions.create ? quickCreate : null"
|
||||
context="props.context"
|
||||
getDomain.bind="getDomain"
|
||||
isToMany="true"
|
||||
nameCreateField="props.nameCreateField"
|
||||
noSearchMore="props.noSearchMore"
|
||||
getOptionClassnames.bind="getOptionClassnames"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="hr_recruitment_auto_doc.Many2ManyBinaryDropzoneField">
|
||||
<div t-attf-class="oe_fileupload o_recruitment_dropzone_field {{props.className ? props.className : ''}}"
|
||||
t-att-class="state.isDragging ? 'o_recruitment_dropzone_field_dragging' : ''"
|
||||
t-on-dragenter="onDragEnter"
|
||||
t-on-dragover="onDragOver"
|
||||
t-on-dragleave="onDragLeave"
|
||||
t-on-drop="onFilesDropped">
|
||||
<div class="o_recruitment_dropzone_box">
|
||||
<div class="o_recruitment_dropzone_icon fa fa-cloud-upload"/>
|
||||
<div class="o_recruitment_dropzone_text"><t t-esc="dropzoneText"/></div>
|
||||
<div class="o_recruitment_dropzone_action">
|
||||
<FileInput
|
||||
acceptedFileExtensions="props.acceptedFileExtensions"
|
||||
multiUpload="true"
|
||||
onUpload.bind="onFileUploaded"
|
||||
resModel="props.record.resModel"
|
||||
resId="props.record.resId or 0"
|
||||
>
|
||||
<button class="btn btn-primary" type="button">
|
||||
<span class="fa fa-paperclip me-1"/> <t t-esc="uploadText"/>
|
||||
</button>
|
||||
</FileInput>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_attachments mt-3">
|
||||
<t t-foreach="files" t-as="file" t-key="file_index">
|
||||
<t t-call="web.Many2ManyBinaryField.attachment_preview"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="hr_recruitment_auto_doc.ListButtons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary">
|
||||
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
t-on-click="openParseWizard">
|
||||
Upload / Parse Document
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_recruitment_auto_doc.ListView" t-inherit="web.ListView" t-inherit-mode="primary"/>
|
||||
|
||||
<t t-name="hr_recruitment_auto_doc.KanbanButtons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary">
|
||||
<xpath expr="//div[hasclass('o_cp_buttons')]" position="inside">
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
t-on-click="openParseWizard">
|
||||
Upload / Parse Document
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_recruitment_auto_doc.KanbanView" t-inherit="web.KanbanView" t-inherit-mode="primary"/>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_applicant_view_list_auto_doc_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.list.auto.doc.inherit</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.crm_case_tree_view_job"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="attributes">
|
||||
<attribute name="js_class">hr_recruitment_auto_doc_list</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_applicant_view_kanban_auto_doc_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.kanban.auto.doc.inherit</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_kanban_view_applicant"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//kanban" position="attributes">
|
||||
<attribute name="js_class">hr_recruitment_auto_doc_kanban</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_applicant_view_kanban_auto_doc_extended_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.kanban.auto.doc.extended.inherit</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_kanban_view_applicant_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//kanban" position="attributes">
|
||||
<attribute name="js_class">hr_recruitment_auto_doc_kanban</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_applicant_view_form_auto_doc_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.form.auto.doc.inherit</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_open_auto_doc_wizard"
|
||||
string="Parse Resumes"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_candidate_view_tree_auto_doc_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.candidate.view.tree.auto.doc.inherit</field>
|
||||
<field name="model">hr.candidate</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="attributes">
|
||||
<attribute name="js_class">hr_recruitment_auto_doc_list</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_candidate_view_kanban_auto_doc_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.candidate.view.kanban.auto.doc.inherit</field>
|
||||
<field name="model">hr.candidate</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//kanban" position="attributes">
|
||||
<attribute name="js_class">hr_recruitment_auto_doc_kanban</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_candidate_view_form_auto_doc_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.candidate.view.form.auto.doc.inherit</field>
|
||||
<field name="model">hr.candidate</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.hr_candidate_view_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_open_auto_doc_wizard"
|
||||
string="Parse Resumes"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_hr_job_recruitment_tree_auto_doc_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.job.recruitment.tree.auto.doc.inherit</field>
|
||||
<field name="model">hr.job.recruitment</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.view_hr_job_recruitment_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="attributes">
|
||||
<attribute name="js_class">hr_recruitment_auto_doc_list</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_hr_job_recruitment_kanban_auto_doc_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.job.recruitment.kanban.auto.doc.inherit</field>
|
||||
<field name="model">hr.job.recruitment</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.view_job_recruitment_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//kanban" position="attributes">
|
||||
<attribute name="js_class">hr_recruitment_auto_doc_kanban</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_hr_job_recruitment_form_auto_doc_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.job.recruitment.form.auto.doc.inherit</field>
|
||||
<field name="model">hr.job.recruitment</field>
|
||||
<field name="inherit_id" ref="hr_recruitment_extended.view_hr_job_recruitment_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_open_auto_doc_wizard"
|
||||
string="Parse JD"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='skill_ids']" position="replace">
|
||||
<field name="skill_ids"
|
||||
widget="job_skill_transfer"
|
||||
options="{'transfer_target': 'secondary_skill_ids', 'color_field': 'color'}"
|
||||
can_create="True"
|
||||
can_write="True"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='secondary_skill_ids']" position="replace">
|
||||
<field name="secondary_skill_ids"
|
||||
widget="job_skill_transfer"
|
||||
options="{'transfer_target': 'skill_ids', 'color_field': 'color'}"
|
||||
can_create="True"
|
||||
can_write="True"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="action_hr_recruitment_auto_doc_wizard_candidate" model="ir.actions.act_window">
|
||||
<field name="name">Parse Resumes</field>
|
||||
<field name="res_model">hr.recruitment.auto.doc.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_id" ref="view_hr_recruitment_auto_doc_wizard_form"/>
|
||||
<field name="binding_model_id" ref="hr_recruitment.model_hr_candidate"/>
|
||||
<field name="binding_type">action</field>
|
||||
<field name="binding_view_types">list,form,kanban</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hr_recruitment_auto_doc_wizard_applicant" model="ir.actions.act_window">
|
||||
<field name="name">Parse Resumes</field>
|
||||
<field name="res_model">hr.recruitment.auto.doc.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_id" ref="view_hr_recruitment_auto_doc_wizard_form"/>
|
||||
<field name="binding_model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||
<field name="binding_type">action</field>
|
||||
<field name="binding_view_types">list,form,kanban</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hr_recruitment_auto_doc_wizard_job_recruitment" model="ir.actions.act_window">
|
||||
<field name="name">Parse Job Description</field>
|
||||
<field name="res_model">hr.recruitment.auto.doc.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_id" ref="view_hr_recruitment_auto_doc_wizard_form"/>
|
||||
<field name="binding_model_id" ref="hr_recruitment_extended.model_hr_job_recruitment"/>
|
||||
<field name="binding_type">action</field>
|
||||
<field name="binding_view_types">list,form,kanban</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import hr_recruitment_auto_doc_wizard
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_hr_recruitment_auto_doc_wizard_form" model="ir.ui.view">
|
||||
<field name="name">hr.recruitment.auto.doc.wizard.form</field>
|
||||
<field name="model">hr.recruitment.auto.doc.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Parse Recruitment Documents" create="0" edit="1">
|
||||
<header>
|
||||
<button name="action_parse_documents" string="Parse Documents" type="object" class="btn-primary"/>
|
||||
<button string="Close" special="cancel" class="btn-secondary"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="target_model"/>
|
||||
<field name="job_recruitment_id"
|
||||
invisible="target_model != 'applicant'"
|
||||
required="target_model == 'applicant'"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="create_missing_skills" invisible="target_model == 'job_recruitment'"/>
|
||||
<field name="update_existing_candidates" invisible="target_model != 'candidate'"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Upload Documents">
|
||||
<field name="attachment_ids"
|
||||
widget="many2many_binary_dropzone"
|
||||
nolabel="1"
|
||||
class="w-100"
|
||||
options="{'preview_images': true}"/>
|
||||
</group>
|
||||
<group string="Uploaded Files">
|
||||
<field name="line_ids" nolabel="1" readonly="1">
|
||||
<kanban class="o_kanban_small_column">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click o_kanban_record">
|
||||
<div class="o_kanban_card_content">
|
||||
<div class="fw-bold">
|
||||
<field name="file_name"/>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<field name="state"/>
|
||||
</div>
|
||||
<div class="mt-2 small" t-if="record.message.raw_value">
|
||||
<field name="message"/>
|
||||
</div>
|
||||
<div class="mt-2 small text-muted" t-if="record.candidate_id.raw_value">
|
||||
Candidate: <field name="candidate_id"/>
|
||||
</div>
|
||||
<div class="small text-muted" t-if="record.applicant_id.raw_value">
|
||||
Applicant: <field name="applicant_id"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</group>
|
||||
<group string="Summary">
|
||||
<group>
|
||||
<field name="processed_count" readonly="1"/>
|
||||
<field name="created_count" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="updated_count" readonly="1"/>
|
||||
<field name="skipped_count" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Result">
|
||||
<field name="result_html" readonly="1" nolabel="1" widget="html"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -41,23 +41,26 @@
|
|||
'views/recruitment_attachments.xml',
|
||||
'views/hr_employee_education_employer_family.xml',
|
||||
'views/hr_recruitment_source.xml',
|
||||
'views/requisitions.xml',
|
||||
'views/skills.xml',
|
||||
'wizards/post_onboarding_attachment_wizard.xml',
|
||||
'wizards/applicant_refuse_reason.xml',
|
||||
'wizards/ats_invite_mail_template_wizard.xml',
|
||||
'wizards/client_submission_mail_template_wizard.xml',
|
||||
# 'views/resume_pearser.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'hr_recruitment_extended/static/src/img/pdf_icon.png',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
||||
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
|
||||
'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
||||
],
|
||||
}
|
||||
}
|
||||
'views/requisitions.xml',
|
||||
'views/skills.xml',
|
||||
'views/recruitment_matching_views.xml',
|
||||
'wizards/post_onboarding_attachment_wizard.xml',
|
||||
'wizards/applicant_refuse_reason.xml',
|
||||
'wizards/ats_invite_mail_template_wizard.xml',
|
||||
'wizards/client_submission_mail_template_wizard.xml',
|
||||
# 'views/resume_pearser.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'hr_recruitment_extended/static/src/img/pdf_icon.png',
|
||||
'hr_recruitment_extended/static/src/js/recruitment_match_panel.js',
|
||||
'hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss',
|
||||
],
|
||||
'web.assets_frontend': [
|
||||
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
||||
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
|
||||
'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,31 @@ import warnings
|
|||
from odoo.tools.mimetypes import guess_mimetype, fix_filename_extension
|
||||
|
||||
|
||||
class HRApplicant(models.Model):
|
||||
_inherit = 'hr.applicant'
|
||||
_track_duration_field = 'recruitment_stage_id'
|
||||
|
||||
|
||||
candidate_image = fields.Image(related='candidate_id.candidate_image', readonly=False, compute_sudo=True)
|
||||
class HRApplicant(models.Model):
|
||||
_inherit = 'hr.applicant'
|
||||
_track_duration_field = 'recruitment_stage_id'
|
||||
|
||||
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
|
||||
primary_skill_match_percentage = fields.Float(
|
||||
string="Primary Skill Match (%)",
|
||||
compute='_compute_skill_match_percentages',
|
||||
store=True,
|
||||
digits=(16, 2),
|
||||
)
|
||||
secondary_skill_match_percentage = fields.Float(
|
||||
string="Secondary Skill Match (%)",
|
||||
compute='_compute_skill_match_percentages',
|
||||
store=True,
|
||||
digits=(16, 2),
|
||||
)
|
||||
overall_skill_match_percentage = fields.Float(
|
||||
string="Overall Skill Match (%)",
|
||||
compute='_compute_skill_match_percentages',
|
||||
store=True,
|
||||
digits=(16, 2),
|
||||
)
|
||||
|
||||
candidate_image = fields.Image(related='candidate_id.candidate_image', readonly=False, compute_sudo=True)
|
||||
submitted_to_client = fields.Boolean(string="Submitted_to_client", default=False, readonly=True, tracking=True)
|
||||
client_submission_date = fields.Datetime(string="Submission Date")
|
||||
submitted_stage = fields.Many2one('hr.recruitment.stage')
|
||||
|
|
@ -22,12 +41,33 @@ class HRApplicant(models.Model):
|
|||
refused_comments = fields.Text(string='Reject Comments')
|
||||
is_on_hold = fields.Boolean(string="Is On Hold", default=False)
|
||||
|
||||
def hold_unhold_button(self):
|
||||
for rec in self:
|
||||
if rec.is_on_hold:
|
||||
rec.is_on_hold = False
|
||||
else:
|
||||
rec.is_on_hold = True
|
||||
def hold_unhold_button(self):
|
||||
for rec in self:
|
||||
if rec.is_on_hold:
|
||||
rec.is_on_hold = False
|
||||
else:
|
||||
rec.is_on_hold = True
|
||||
|
||||
def action_toggle_chatter_visibility(self):
|
||||
for record in self:
|
||||
record.hide_chatter_suggestion = not record.hide_chatter_suggestion
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
@api.depends('hr_job_recruitment.skill_ids', 'hr_job_recruitment.secondary_skill_ids', 'candidate_id.skill_ids')
|
||||
def _compute_skill_match_percentages(self):
|
||||
for applicant in self:
|
||||
percentages = {
|
||||
'primary_skill_match_percentage': 0.0,
|
||||
'secondary_skill_match_percentage': 0.0,
|
||||
'overall_skill_match_percentage': 0.0,
|
||||
}
|
||||
if applicant.hr_job_recruitment and applicant.candidate_id:
|
||||
percentages = applicant.hr_job_recruitment._get_skill_match_percentages(applicant.candidate_id.skill_ids)
|
||||
percentages = {
|
||||
key: value for key, value in percentages.items()
|
||||
if key in {'primary_skill_match_percentage', 'secondary_skill_match_percentage', 'overall_skill_match_percentage'}
|
||||
}
|
||||
applicant.update(percentages)
|
||||
@api.constrains('candidate_id','hr_job_recruitment')
|
||||
def hr_applicant_constrains(self):
|
||||
for rec in self:
|
||||
|
|
|
|||
|
|
@ -1,22 +1,34 @@
|
|||
from odoo import models, fields, api, _
|
||||
from datetime import date
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import timedelta
|
||||
import datetime
|
||||
from odoo import models, fields, api, _
|
||||
from datetime import date
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import timedelta
|
||||
import datetime
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
|
||||
class HRJobRecruitment(models.Model):
|
||||
class HRJobRecruitment(models.Model):
|
||||
_name = 'hr.job.recruitment'
|
||||
_description = 'Recruitment'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_inherits = {'hr.job': 'job_id'}
|
||||
_rec_name = 'recruitment_sequence'
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
active = fields.Boolean(default=True)
|
||||
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_recruitment_sequence', 'UNIQUE(recruitment_sequence)', 'Recruitment sequence must be unique!')
|
||||
]
|
||||
_sql_constraints = [
|
||||
('unique_recruitment_sequence', 'UNIQUE(recruitment_sequence)', 'Recruitment sequence must be unique!')
|
||||
]
|
||||
_SKILL_ALIAS_GROUPS = {
|
||||
'python': {'python', 'python3', 'py'},
|
||||
'postgresql': {'postgresql', 'postgres', 'postgre', 'pgsql', 'psql', 'pgadmin'},
|
||||
'javascript': {'javascript', 'js', 'nodejs', 'node'},
|
||||
'typescript': {'typescript', 'ts'},
|
||||
'react': {'react', 'reactjs'},
|
||||
'vue': {'vue', 'vuejs'},
|
||||
'angular': {'angular', 'angularjs'},
|
||||
}
|
||||
|
||||
def _get_first_stage(self):
|
||||
"""This function is used to fetch the starting stage"""
|
||||
|
|
@ -154,14 +166,208 @@ class HRJobRecruitment(models.Model):
|
|||
tracking=True, help="The Recruiter will be the default value for all Applicants in this job \
|
||||
position. The Recruiter is automatically added to all meetings with the Applicant.")
|
||||
interviewer_ids = fields.Many2many('res.users', string='Interviewers', domain="[('share', '=', False), ('company_ids', 'in', company_id)]", tracking=True, help="The Interviewers set on the job position can see all Applicants in it. They have access to the information, the attachments, the meeting management and they can refuse him. You don't need to have Recruitment rights to be set as an interviewer.")
|
||||
skill_ids = fields.Many2many('hr.skill','hr_job_recruitment_hr_primary_skill_rel','job_id', 'user_id', string="Primary Skills", tracking=True)
|
||||
skill_ids = fields.Many2many('hr.skill','hr_job_recruitment_hr_primary_skill_rel','job_id', 'user_id', string="Primary Skills", tracking=True)
|
||||
address_id = fields.Many2one(
|
||||
'res.partner', "Job Location", default=_default_address_id,
|
||||
domain="[('is_company','=',True),('contact_type','=',recruitment_type)]",
|
||||
help="Select the location where the applicant will work. Addresses listed here are defined on the company's contact information.", exportable=False, tracking=True)
|
||||
recruitment_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal', tracking=True)
|
||||
requested_by = fields.Many2one('res.partner', string="Requested By",
|
||||
default=lambda self: self.env.user.partner_id, domain="[('contact_type','=',recruitment_type)]", tracking=True)
|
||||
recruitment_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal', tracking=True)
|
||||
requested_by = fields.Many2one('res.partner', string="Requested By",
|
||||
default=lambda self: self.env.user.partner_id, domain="[('contact_type','=',recruitment_type)]", tracking=True)
|
||||
|
||||
def action_toggle_chatter_visibility(self):
|
||||
for record in self:
|
||||
record.hide_chatter_suggestion = not record.hide_chatter_suggestion
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
def _normalize_skill_name(self, skill_name):
|
||||
normalized_name = unicodedata.normalize('NFKD', skill_name or '')
|
||||
normalized_name = normalized_name.encode('ascii', 'ignore').decode('ascii').lower()
|
||||
normalized_name = re.sub(r'[^a-z0-9]+', '', normalized_name)
|
||||
if not normalized_name:
|
||||
return ''
|
||||
for canonical_name, aliases in self._SKILL_ALIAS_GROUPS.items():
|
||||
if normalized_name in aliases or any(alias in normalized_name for alias in aliases if len(alias) > 3):
|
||||
return canonical_name
|
||||
return normalized_name
|
||||
|
||||
def _get_normalized_skill_name_map(self, skill_names):
|
||||
normalized_map = {}
|
||||
for skill_name in skill_names:
|
||||
normalized_name = self._normalize_skill_name(skill_name)
|
||||
if normalized_name and normalized_name not in normalized_map:
|
||||
normalized_map[normalized_name] = skill_name
|
||||
return normalized_map
|
||||
|
||||
def _get_skill_match_percentages_from_names(self, primary_skill_names, secondary_skill_names, candidate_skill_names):
|
||||
self.ensure_one()
|
||||
candidate_skill_map = self._get_normalized_skill_name_map(candidate_skill_names)
|
||||
primary_skill_map = self._get_normalized_skill_name_map(primary_skill_names)
|
||||
secondary_skill_map = self._get_normalized_skill_name_map(secondary_skill_names)
|
||||
all_skill_map = {**primary_skill_map, **secondary_skill_map}
|
||||
|
||||
def _percentage(required_skill_map):
|
||||
if not required_skill_map:
|
||||
return 0.0
|
||||
return round(
|
||||
(len(set(required_skill_map) & set(candidate_skill_map)) / len(required_skill_map)) * 100,
|
||||
2,
|
||||
)
|
||||
|
||||
matching_skill_keys = set(all_skill_map) & set(candidate_skill_map)
|
||||
missing_skill_keys = set(all_skill_map) - set(candidate_skill_map)
|
||||
|
||||
return {
|
||||
'primary_skill_match_percentage': _percentage(primary_skill_map),
|
||||
'secondary_skill_match_percentage': _percentage(secondary_skill_map),
|
||||
'overall_skill_match_percentage': _percentage(all_skill_map),
|
||||
'matching_skill_names': [all_skill_map[key] for key in matching_skill_keys],
|
||||
'missing_skill_names': [all_skill_map[key] for key in missing_skill_keys],
|
||||
}
|
||||
|
||||
def _get_skill_match_percentages(self, candidate_skills, primary_skill_names=None, secondary_skill_names=None):
|
||||
self.ensure_one()
|
||||
primary_skill_names = primary_skill_names if primary_skill_names is not None else self.skill_ids.mapped('name')
|
||||
secondary_skill_names = secondary_skill_names if secondary_skill_names is not None else self.secondary_skill_ids.mapped('name')
|
||||
candidate_skill_names = candidate_skills.mapped('name')
|
||||
return self._get_skill_match_percentages_from_names(
|
||||
primary_skill_names,
|
||||
secondary_skill_names,
|
||||
candidate_skill_names,
|
||||
)
|
||||
|
||||
def _prepare_candidate_pool_match_payload(self, candidate, primary_skill_names, secondary_skill_names):
|
||||
self.ensure_one()
|
||||
percentages = self._get_skill_match_percentages(
|
||||
candidate.skill_ids,
|
||||
primary_skill_names=primary_skill_names,
|
||||
secondary_skill_names=secondary_skill_names,
|
||||
)
|
||||
return {
|
||||
'candidate_id': candidate.id,
|
||||
'candidate_name': candidate.partner_name or candidate.display_name,
|
||||
'candidate_sequence': getattr(candidate, 'candidate_sequence', False),
|
||||
'email_from': candidate.email_from,
|
||||
'partner_phone': candidate.partner_phone,
|
||||
'matching_skill_names': sorted(percentages['matching_skill_names']),
|
||||
'missing_skill_names': sorted(percentages['missing_skill_names']),
|
||||
'primary_skill_match_percentage': percentages['primary_skill_match_percentage'],
|
||||
'secondary_skill_match_percentage': percentages['secondary_skill_match_percentage'],
|
||||
'overall_skill_match_percentage': percentages['overall_skill_match_percentage'],
|
||||
}
|
||||
|
||||
def _prepare_applicant_match_payload(self, applicant, primary_skill_names, secondary_skill_names):
|
||||
self.ensure_one()
|
||||
candidate = applicant.candidate_id
|
||||
percentages = self._get_skill_match_percentages(
|
||||
candidate.skill_ids,
|
||||
primary_skill_names=primary_skill_names,
|
||||
secondary_skill_names=secondary_skill_names,
|
||||
) if candidate else {
|
||||
'primary_skill_match_percentage': 0.0,
|
||||
'secondary_skill_match_percentage': 0.0,
|
||||
'overall_skill_match_percentage': 0.0,
|
||||
'matching_skill_names': [],
|
||||
'missing_skill_names': [],
|
||||
}
|
||||
return {
|
||||
'applicant_id': applicant.id,
|
||||
'applicant_name': applicant.partner_name or applicant.display_name,
|
||||
'candidate_id': candidate.id if candidate else False,
|
||||
'candidate_sequence': getattr(candidate, 'candidate_sequence', False) if candidate else False,
|
||||
'email_from': applicant.email_from or (candidate.email_from if candidate else False),
|
||||
'partner_phone': applicant.partner_phone or (candidate.partner_phone if candidate else False),
|
||||
'recruitment_stage_name': applicant.recruitment_stage_id.display_name,
|
||||
'matching_skill_names': sorted(percentages['matching_skill_names']),
|
||||
'missing_skill_names': sorted(percentages['missing_skill_names']),
|
||||
'primary_skill_match_percentage': percentages['primary_skill_match_percentage'],
|
||||
'secondary_skill_match_percentage': percentages['secondary_skill_match_percentage'],
|
||||
'overall_skill_match_percentage': percentages['overall_skill_match_percentage'],
|
||||
}
|
||||
|
||||
def get_candidate_pool_matches_data(self, primary_skill_names=None, secondary_skill_names=None):
|
||||
self.ensure_one()
|
||||
primary_skill_names = primary_skill_names if primary_skill_names is not None else self.skill_ids.mapped('name')
|
||||
secondary_skill_names = secondary_skill_names if secondary_skill_names is not None else self.secondary_skill_ids.mapped('name')
|
||||
existing_candidate_ids = self.application_ids.candidate_id.ids
|
||||
candidates = self.env['hr.candidate'].search([
|
||||
('id', 'not in', existing_candidate_ids),
|
||||
])
|
||||
|
||||
ranked_candidates = []
|
||||
for candidate in candidates:
|
||||
ranked_candidates.append(
|
||||
self._prepare_candidate_pool_match_payload(candidate, primary_skill_names, secondary_skill_names)
|
||||
)
|
||||
|
||||
ranked_applicants = []
|
||||
for applicant in self.application_ids.sorted(lambda rec: (
|
||||
-(rec.overall_skill_match_percentage or 0.0),
|
||||
-(rec.primary_skill_match_percentage or 0.0),
|
||||
-(rec.secondary_skill_match_percentage or 0.0),
|
||||
rec.partner_name or rec.display_name or '',
|
||||
)):
|
||||
ranked_applicants.append(
|
||||
self._prepare_applicant_match_payload(applicant, primary_skill_names, secondary_skill_names)
|
||||
)
|
||||
|
||||
ranked_candidates.sort(
|
||||
key=lambda item: (
|
||||
item['overall_skill_match_percentage'],
|
||||
item['primary_skill_match_percentage'],
|
||||
item['secondary_skill_match_percentage'],
|
||||
item['candidate_name'] or '',
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
ranked_applicants.sort(
|
||||
key=lambda item: (
|
||||
item['overall_skill_match_percentage'],
|
||||
item['primary_skill_match_percentage'],
|
||||
item['secondary_skill_match_percentage'],
|
||||
item['applicant_name'] or '',
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
return {
|
||||
'job_recruitment_id': self.id,
|
||||
'job_recruitment_name': self.display_name,
|
||||
'primary_skill_names': list(self._get_normalized_skill_name_map(primary_skill_names).values()),
|
||||
'secondary_skill_names': list(self._get_normalized_skill_name_map(secondary_skill_names).values()),
|
||||
'candidate_count': len(ranked_candidates),
|
||||
'applicant_count': len(ranked_applicants),
|
||||
'candidates': ranked_candidates,
|
||||
'applicants': ranked_applicants,
|
||||
}
|
||||
|
||||
def action_add_candidate_to_recruitment(self, candidate_id):
|
||||
self.ensure_one()
|
||||
candidate = self.env['hr.candidate'].browse(candidate_id).exists()
|
||||
if not candidate:
|
||||
raise ValidationError(_("The selected candidate no longer exists."))
|
||||
|
||||
existing_applicant = self.application_ids.filtered(lambda applicant: applicant.candidate_id == candidate)[:1]
|
||||
if existing_applicant:
|
||||
return {
|
||||
'applicant_id': existing_applicant.id,
|
||||
'already_exists': True,
|
||||
}
|
||||
|
||||
applicant_vals = {
|
||||
'candidate_id': candidate.id,
|
||||
'partner_name': candidate.partner_name or candidate.display_name,
|
||||
'email_from': candidate.email_from,
|
||||
'partner_phone': candidate.partner_phone,
|
||||
'hr_job_recruitment': self.id,
|
||||
'user_id': self.user_id.id,
|
||||
'company_id': candidate.company_id.id or self.company_id.id,
|
||||
}
|
||||
applicant = self.env['hr.applicant'].create(applicant_vals)
|
||||
return {
|
||||
'applicant_id': applicant.id,
|
||||
'already_exists': False,
|
||||
}
|
||||
|
||||
@api.onchange('recruitment_type')
|
||||
def _onchange_recruitment_type(self):
|
||||
|
|
|
|||
|
|
@ -11,14 +11,15 @@ import datetime
|
|||
# hiring_history = fields.One2many('recruitment.status.history', 'job_id', string='History')
|
||||
|
||||
|
||||
class HrCandidate(models.Model):
|
||||
_inherit = "hr.candidate"
|
||||
class HrCandidate(models.Model):
|
||||
_inherit = "hr.candidate"
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_candidate_sequence', 'UNIQUE(candidate_sequence)', 'Candidate sequence must be unique!'),
|
||||
]
|
||||
#personal Details
|
||||
candidate_sequence = fields.Char(string='Candidate Sequence', readonly=False, default='/', copy=False)
|
||||
candidate_sequence = fields.Char(string='Candidate Sequence', readonly=False, default='/', copy=False)
|
||||
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
|
||||
|
||||
first_name = fields.Char(string='First Name',required=False, help="This is the person's first name, given at birth or during a naming ceremony. It’s the name people use to address you.")
|
||||
middle_name = fields.Char(string='Middle Name', help="This is an extra name that comes between the first name and last name. Not everyone has a middle name")
|
||||
|
|
@ -30,8 +31,13 @@ class HrCandidate(models.Model):
|
|||
resume_type = fields.Char()
|
||||
resume_name = fields.Char()
|
||||
|
||||
applications_stages_stat = fields.Many2many('application.stage.status',string="Applications History", compute="_compute_applications_stages_stat")
|
||||
# availability_status = fields.Selection([('available','Available'),('not_available','Not Available'),('hired','Hired'),('abscond','Abscond')])
|
||||
applications_stages_stat = fields.Many2many('application.stage.status',string="Applications History", compute="_compute_applications_stages_stat")
|
||||
# availability_status = fields.Selection([('available','Available'),('not_available','Not Available'),('hired','Hired'),('abscond','Abscond')])
|
||||
|
||||
def action_toggle_chatter_visibility(self):
|
||||
for record in self:
|
||||
record.hide_chatter_suggestion = not record.hide_chatter_suggestion
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
|
||||
@api.onchange('resume')
|
||||
|
|
@ -586,4 +592,4 @@ class ApplicationsStageStatus(models.Model):
|
|||
WHERE
|
||||
a.active = 't' or a.active = 'f'
|
||||
);
|
||||
""" % (self._table))
|
||||
""" % (self._table))
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ access_hr_recruitment_stage_hr,hr.recruitment.stage.hr,hr_recruitment.model_hr_r
|
|||
access_application_stage_status,application.stage.status,model_application_stage_status,base.group_user,1,1,1,1
|
||||
|
||||
|
||||
access_ats_invite_mail_template_wizard,ats.invite.mail.template.wizard.user,hr_recruitment_extended.model_ats_invite_mail_template_wizard,,1,1,1,1
|
||||
access_client_submission_mails_template_wizard,client.submission.mails.template.wizard.user,hr_recruitment_extended.model_client_submission_mails_template_wizard,,1,1,1,1
|
||||
access_hr_application_public,hr.applicant.public.access,hr_recruitment.model_hr_applicant,base.group_public,1,0,0,0
|
||||
access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0
|
||||
access_ats_invite_mail_template_wizard,ats.invite.mail.template.wizard.user,hr_recruitment_extended.model_ats_invite_mail_template_wizard,,1,1,1,1
|
||||
access_client_submission_mails_template_wizard,client.submission.mails.template.wizard.user,hr_recruitment_extended.model_client_submission_mails_template_wizard,,1,1,1,1
|
||||
access_hr_application_public,hr.applicant.public.access,hr_recruitment.model_hr_applicant,base.group_public,1,0,0,0
|
||||
access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0
|
||||
|
|
|
|||
|
|
|
@ -0,0 +1,463 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { onMounted, onPatched, onWillUnmount } from "@odoo/owl";
|
||||
import { RecruitmentFormController } from "@hr_recruitment/views/recruitment_form_controller";
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
patch(RecruitmentFormController.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.actionService = useService("action");
|
||||
this.notification = useService("notification");
|
||||
this.orm = useService("orm");
|
||||
this._matchPanelOpened = false;
|
||||
this._matchPanelLoading = false;
|
||||
this._matchPanelData = null;
|
||||
this._matchPanelSearchTerm = "";
|
||||
this._matchPanelActiveTab = "candidates";
|
||||
this._matchPanelAddingCandidateId = null;
|
||||
|
||||
onMounted(() => this._syncRecruitmentMatchPanel());
|
||||
onPatched(() => this._syncRecruitmentMatchPanel());
|
||||
onWillUnmount(() => this._removeRecruitmentMatchPanelArtifacts());
|
||||
},
|
||||
|
||||
_getRecruitmentMatchPanelButton() {
|
||||
return document.querySelector(`.o_hr_match_fab[data-controller-id="${this.__owl__.id}"]`);
|
||||
},
|
||||
|
||||
_getRecruitmentMatchPanel() {
|
||||
return document.querySelector(`.o_hr_match_panel[data-controller-id="${this.__owl__.id}"]`);
|
||||
},
|
||||
|
||||
_getCurrentSkillNames(fieldName) {
|
||||
const fieldValue = this.model.root.data[fieldName];
|
||||
if (!fieldValue || !Array.isArray(fieldValue.records)) {
|
||||
return [];
|
||||
}
|
||||
return fieldValue.records
|
||||
.map((record) => {
|
||||
const recordData = record.data || {};
|
||||
return recordData.name || recordData.display_name || record.display_name || "";
|
||||
})
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
async _fetchRecruitmentMatchPanelData() {
|
||||
const resId = this.model.root.resId;
|
||||
if (!resId) {
|
||||
this.notification.add(_t("Save the recruitment first to view candidate pool matches."), {
|
||||
type: "warning",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return this.orm.call("hr.job.recruitment", "get_candidate_pool_matches_data", [[resId], this._getCurrentSkillNames("skill_ids"), this._getCurrentSkillNames("secondary_skill_ids")]);
|
||||
},
|
||||
|
||||
async _openRecruitmentMatchPanel() {
|
||||
this._matchPanelOpened = true;
|
||||
this._ensureRecruitmentMatchPanel();
|
||||
await this._loadRecruitmentMatchPanelData();
|
||||
},
|
||||
|
||||
_closeRecruitmentMatchPanel() {
|
||||
this._matchPanelOpened = false;
|
||||
const panel = this._getRecruitmentMatchPanel();
|
||||
if (panel) {
|
||||
panel.classList.remove("o_hr_match_panel_open");
|
||||
}
|
||||
const button = this._getRecruitmentMatchPanelButton();
|
||||
if (button) {
|
||||
button.classList.remove("o_hr_match_fab_hidden");
|
||||
}
|
||||
},
|
||||
|
||||
_formatMatchPercentage(value) {
|
||||
const numericValue = Number(value || 0);
|
||||
return Number.isInteger(numericValue) ? String(numericValue) : numericValue.toFixed(2).replace(/\.00$/, "");
|
||||
},
|
||||
|
||||
_normalizeSearchValue(value) {
|
||||
return String(value || "").trim().toLowerCase();
|
||||
},
|
||||
|
||||
_recordMatchesSearch(record) {
|
||||
const searchTerm = this._normalizeSearchValue(this._matchPanelSearchTerm);
|
||||
if (!searchTerm) {
|
||||
return true;
|
||||
}
|
||||
const haystack = [
|
||||
record.candidate_name,
|
||||
record.applicant_name,
|
||||
record.candidate_sequence,
|
||||
record.email_from,
|
||||
record.partner_phone,
|
||||
record.recruitment_stage_name,
|
||||
...(record.matching_skill_names || []),
|
||||
...(record.missing_skill_names || []),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return haystack.includes(searchTerm);
|
||||
},
|
||||
|
||||
async _loadRecruitmentMatchPanelData() {
|
||||
if (!this._matchPanelOpened) {
|
||||
return;
|
||||
}
|
||||
this._matchPanelLoading = true;
|
||||
this._renderRecruitmentMatchPanel();
|
||||
try {
|
||||
this._matchPanelData = await this._fetchRecruitmentMatchPanelData();
|
||||
} catch (error) {
|
||||
this.notification.add(_t("Unable to load candidate pool matches."), { type: "danger" });
|
||||
this._matchPanelData = {
|
||||
candidates: [],
|
||||
primary_skill_names: [],
|
||||
secondary_skill_names: [],
|
||||
candidate_count: 0,
|
||||
};
|
||||
} finally {
|
||||
this._matchPanelLoading = false;
|
||||
this._renderRecruitmentMatchPanel();
|
||||
}
|
||||
},
|
||||
|
||||
async _addCandidateToRecruitment(candidateId) {
|
||||
if (!candidateId || this._matchPanelAddingCandidateId) {
|
||||
return;
|
||||
}
|
||||
this._matchPanelAddingCandidateId = candidateId;
|
||||
this._renderRecruitmentMatchPanel();
|
||||
try {
|
||||
const result = await this.orm.call("hr.job.recruitment", "action_add_candidate_to_recruitment", [[this.model.root.resId], candidateId]);
|
||||
this.notification.add(
|
||||
result.already_exists
|
||||
? _t("Candidate is already linked to this recruitment.")
|
||||
: _t("Candidate added to applicants."),
|
||||
{ type: result.already_exists ? "warning" : "success" }
|
||||
);
|
||||
this._matchPanelActiveTab = "applicants";
|
||||
await this._loadRecruitmentMatchPanelData();
|
||||
} catch (error) {
|
||||
this.notification.add(_t("Unable to add candidate to this recruitment."), { type: "danger" });
|
||||
} finally {
|
||||
this._matchPanelAddingCandidateId = null;
|
||||
this._renderRecruitmentMatchPanel();
|
||||
}
|
||||
},
|
||||
|
||||
_renderSkillTags(skillNames, className) {
|
||||
if (!skillNames.length) {
|
||||
return `<span class="o_hr_match_chip ${className}">${escapeHtml(_t("None"))}</span>`;
|
||||
}
|
||||
return skillNames
|
||||
.map((skillName) => `<span class="o_hr_match_chip ${className}">${escapeHtml(skillName)}</span>`)
|
||||
.join("");
|
||||
},
|
||||
|
||||
_renderEmptyState(title, description) {
|
||||
return `
|
||||
<div class="o_hr_match_empty">
|
||||
<h4>${escapeHtml(title)}</h4>
|
||||
<p>${escapeHtml(description)}</p>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
_renderMatchCard(record, index, type) {
|
||||
const isApplicant = type === "applicants";
|
||||
const title = isApplicant ? record.applicant_name : record.candidate_name;
|
||||
const identifier = isApplicant ? record.applicant_id : record.candidate_id;
|
||||
const actionLabel = isApplicant ? _t("Open Applicant") : _t("Add Applicant");
|
||||
const actionIcon = isApplicant ? "fa-external-link" : "fa-plus";
|
||||
const actionClass = isApplicant ? "o_hr_match_action_secondary" : "o_hr_match_action_primary";
|
||||
const metaLine = [record.candidate_sequence, isApplicant ? record.recruitment_stage_name : null]
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
return `
|
||||
<article class="o_hr_match_card" data-record-id="${identifier}" data-record-type="${type}">
|
||||
<div class="o_hr_match_card_main">
|
||||
<div class="o_hr_match_card_identity">
|
||||
<span class="o_hr_match_rank">#${index + 1}</span>
|
||||
<div class="o_hr_match_identity_copy">
|
||||
<h4>${escapeHtml(title || _t("Unnamed Candidate"))}</h4>
|
||||
<p>${escapeHtml(metaLine || _t("Profile available"))}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_hr_match_card_actions">
|
||||
<div class="o_hr_match_score_ring">
|
||||
<span>${escapeHtml(this._formatMatchPercentage(record.overall_skill_match_percentage))}%</span>
|
||||
<small>${escapeHtml(_t("match"))}</small>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn o_hr_match_inline_action ${actionClass}"
|
||||
data-action="${isApplicant ? "open-applicant" : "add-candidate"}"
|
||||
data-id="${identifier}"
|
||||
${!isApplicant && this._matchPanelAddingCandidateId === identifier ? "disabled" : ""}
|
||||
aria-label="${escapeHtml(actionLabel)}"
|
||||
>
|
||||
<i class="fa ${actionIcon}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_hr_match_score_grid">
|
||||
<div>
|
||||
<span>${escapeHtml(_t("Primary"))}</span>
|
||||
<strong>${escapeHtml(this._formatMatchPercentage(record.primary_skill_match_percentage))}%</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>${escapeHtml(_t("Secondary"))}</span>
|
||||
<strong>${escapeHtml(this._formatMatchPercentage(record.secondary_skill_match_percentage))}%</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>${escapeHtml(_t("Phone"))}</span>
|
||||
<strong class="o_hr_match_meta_value">${escapeHtml(record.partner_phone || "-")}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>${escapeHtml(_t("Email"))}</span>
|
||||
<strong class="o_hr_match_meta_value">${escapeHtml(record.email_from || "-")}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_hr_match_skill_split">
|
||||
<div class="o_hr_match_skill_section">
|
||||
<label>${escapeHtml(_t("Matching Skills"))}</label>
|
||||
<div class="o_hr_match_chip_row">
|
||||
${this._renderSkillTags(record.matching_skill_names || [], "o_hr_match_chip_good")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_hr_match_skill_section">
|
||||
<label>${escapeHtml(_t("Missing Skills"))}</label>
|
||||
<div class="o_hr_match_chip_row">
|
||||
${this._renderSkillTags(record.missing_skill_names || [], "o_hr_match_chip_warn")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
},
|
||||
|
||||
_renderRecruitmentMatchPanel() {
|
||||
const panel = this._getRecruitmentMatchPanel();
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
if (!this._matchPanelOpened) {
|
||||
panel.classList.remove("o_hr_match_panel_open");
|
||||
return;
|
||||
}
|
||||
|
||||
const activeElement = document.activeElement;
|
||||
const shouldRestoreSearchFocus = activeElement?.classList?.contains("o_hr_match_search_input");
|
||||
const searchSelectionStart = shouldRestoreSearchFocus ? activeElement.selectionStart : null;
|
||||
const searchSelectionEnd = shouldRestoreSearchFocus ? activeElement.selectionEnd : null;
|
||||
|
||||
const payload = this._matchPanelData || {
|
||||
candidates: [],
|
||||
applicants: [],
|
||||
primary_skill_names: [],
|
||||
secondary_skill_names: [],
|
||||
candidate_count: 0,
|
||||
applicant_count: 0,
|
||||
};
|
||||
const filteredCandidates = (payload.candidates || []).filter((record) => this._recordMatchesSearch(record));
|
||||
const filteredApplicants = (payload.applicants || []).filter((record) => this._recordMatchesSearch(record));
|
||||
const activeRecords = this._matchPanelActiveTab === "applicants" ? filteredApplicants : filteredCandidates;
|
||||
const activeCards = activeRecords.length
|
||||
? activeRecords
|
||||
.map((record, index) => this._renderMatchCard(record, index, this._matchPanelActiveTab))
|
||||
.join("")
|
||||
: this._renderEmptyState(
|
||||
this._matchPanelActiveTab === "applicants" ? _t("No applicants found") : _t("No candidates found"),
|
||||
this._matchPanelSearchTerm
|
||||
? _t("Try a different search term to widen the results.")
|
||||
: this._matchPanelActiveTab === "applicants"
|
||||
? _t("Applicants added to this recruitment will appear here with the same match insights.")
|
||||
: _t("Try refreshing after updating the recruitment skills or candidate pool.")
|
||||
);
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="o_hr_match_panel_backdropless">
|
||||
<header class="o_hr_match_panel_header">
|
||||
<div>
|
||||
<p>${escapeHtml(_t("Candidate Pool"))}</p>
|
||||
<h3>${escapeHtml(payload.job_recruitment_name || _t("Recruitment Matches"))}</h3>
|
||||
<span>${escapeHtml(`${payload.candidate_count || 0} ${_t("pool candidates")} • ${payload.applicant_count || 0} ${_t("applicants")}`)}</span>
|
||||
</div>
|
||||
<div class="o_hr_match_panel_actions">
|
||||
<button type="button" class="btn btn-light o_hr_match_refresh">${escapeHtml(_t("Refresh"))}</button>
|
||||
<button type="button" class="btn btn-dark o_hr_match_close" aria-label="${escapeHtml(_t("Close"))}">×</button>
|
||||
</div>
|
||||
</header>
|
||||
<section class="o_hr_match_panel_toolbar">
|
||||
<div class="o_hr_match_tab_row" role="tablist" aria-label="${escapeHtml(_t("Match Tabs"))}">
|
||||
<button type="button" class="o_hr_match_tab ${this._matchPanelActiveTab === "candidates" ? "o_hr_match_tab_active" : ""}" data-tab="candidates">
|
||||
${escapeHtml(_t("Candidates"))} <span>${escapeHtml(String(payload.candidate_count || 0))}</span>
|
||||
</button>
|
||||
<button type="button" class="o_hr_match_tab ${this._matchPanelActiveTab === "applicants" ? "o_hr_match_tab_active" : ""}" data-tab="applicants">
|
||||
${escapeHtml(_t("Applicants"))} <span>${escapeHtml(String(payload.applicant_count || 0))}</span>
|
||||
</button>
|
||||
</div>
|
||||
<label class="o_hr_match_search">
|
||||
<i class="fa fa-search"></i>
|
||||
<input
|
||||
type="search"
|
||||
class="o_hr_match_search_input"
|
||||
value="${escapeHtml(this._matchPanelSearchTerm)}"
|
||||
placeholder="${escapeHtml(_t("Search by name, email, phone, stage, or skill"))}"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
<section class="o_hr_match_panel_skill_summary">
|
||||
<div>
|
||||
<label>${escapeHtml(_t("Primary Skills"))}</label>
|
||||
<div class="o_hr_match_chip_row">
|
||||
${this._renderSkillTags(payload.primary_skill_names || [], "o_hr_match_chip_primary")}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>${escapeHtml(_t("Secondary Skills"))}</label>
|
||||
<div class="o_hr_match_chip_row">
|
||||
${this._renderSkillTags(payload.secondary_skill_names || [], "o_hr_match_chip_secondary")}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="o_hr_match_panel_body ${this._matchPanelLoading ? "o_hr_match_panel_body_loading" : ""}">
|
||||
${this._matchPanelLoading ? `<div class="o_hr_match_loading">${escapeHtml(_t("Refreshing matches..."))}</div>` : activeCards}
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
panel.classList.add("o_hr_match_panel_open");
|
||||
|
||||
panel.querySelector(".o_hr_match_close")?.addEventListener("click", () => this._closeRecruitmentMatchPanel());
|
||||
panel.querySelector(".o_hr_match_refresh")?.addEventListener("click", () => this._loadRecruitmentMatchPanelData());
|
||||
panel.querySelectorAll(".o_hr_match_tab").forEach((tabButton) => {
|
||||
tabButton.addEventListener("click", () => {
|
||||
this._matchPanelActiveTab = tabButton.dataset.tab || "candidates";
|
||||
this._renderRecruitmentMatchPanel();
|
||||
});
|
||||
});
|
||||
panel.querySelector(".o_hr_match_search_input")?.addEventListener("input", (event) => {
|
||||
this._matchPanelSearchTerm = event.target.value || "";
|
||||
this._renderRecruitmentMatchPanel();
|
||||
});
|
||||
panel.querySelectorAll('[data-action="add-candidate"]').forEach((button) => {
|
||||
button.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
const candidateId = Number(button.dataset.id);
|
||||
if (candidateId) {
|
||||
this._addCandidateToRecruitment(candidateId);
|
||||
}
|
||||
});
|
||||
});
|
||||
panel.querySelectorAll('[data-action="open-applicant"]').forEach((button) => {
|
||||
button.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
const applicantId = Number(button.dataset.id);
|
||||
if (applicantId) {
|
||||
this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "hr.applicant",
|
||||
res_id: applicantId,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
for (const card of panel.querySelectorAll(".o_hr_match_card")) {
|
||||
card.addEventListener("click", (event) => {
|
||||
const recordId = Number(event.currentTarget.dataset.recordId);
|
||||
const recordType = event.currentTarget.dataset.recordType;
|
||||
if (recordId && recordType === "applicants") {
|
||||
this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "hr.applicant",
|
||||
res_id: recordId,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
});
|
||||
} else if (recordId) {
|
||||
this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "hr.candidate",
|
||||
res_id: recordId,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const button = this._getRecruitmentMatchPanelButton();
|
||||
if (button) {
|
||||
button.classList.add("o_hr_match_fab_hidden");
|
||||
}
|
||||
|
||||
if (shouldRestoreSearchFocus) {
|
||||
const searchInput = panel.querySelector(".o_hr_match_search_input");
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
if (searchSelectionStart !== null && searchSelectionEnd !== null) {
|
||||
searchInput.setSelectionRange(searchSelectionStart, searchSelectionEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_ensureRecruitmentMatchPanel() {
|
||||
let panel = this._getRecruitmentMatchPanel();
|
||||
if (!panel) {
|
||||
panel = document.createElement("aside");
|
||||
panel.className = "o_hr_match_panel";
|
||||
panel.dataset.controllerId = this.__owl__.id;
|
||||
document.body.appendChild(panel);
|
||||
}
|
||||
this._renderRecruitmentMatchPanel();
|
||||
},
|
||||
|
||||
_ensureRecruitmentMatchButton() {
|
||||
if (!this.model.root.resId) {
|
||||
this._getRecruitmentMatchPanelButton()?.remove();
|
||||
return;
|
||||
}
|
||||
let button = this._getRecruitmentMatchPanelButton();
|
||||
if (!button) {
|
||||
button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "o_hr_match_fab";
|
||||
button.dataset.controllerId = this.__owl__.id;
|
||||
button.innerHTML = `
|
||||
<span class="o_hr_match_fab_icon"><i class="fa fa-users"></i></span>
|
||||
<span class="o_hr_match_fab_text">${escapeHtml(_t("Pool Matches"))}</span>
|
||||
`;
|
||||
button.addEventListener("click", () => this._openRecruitmentMatchPanel());
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
},
|
||||
|
||||
_syncRecruitmentMatchPanel() {
|
||||
this._ensureRecruitmentMatchButton();
|
||||
if (this._matchPanelOpened) {
|
||||
this._ensureRecruitmentMatchPanel();
|
||||
}
|
||||
},
|
||||
|
||||
_removeRecruitmentMatchPanelArtifacts() {
|
||||
this._getRecruitmentMatchPanelButton()?.remove();
|
||||
this._getRecruitmentMatchPanel()?.remove();
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
.o_hr_match_fab {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
z-index: 1090;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 14px 18px;
|
||||
background: linear-gradient(135deg, #0f3d3e 0%, #174a7c 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.28);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.o_hr_match_fab:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 24px 52px rgba(15, 23, 42, 0.34);
|
||||
}
|
||||
|
||||
.o_hr_match_fab_hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.o_hr_match_fab_icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.o_hr_match_fab_text {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.o_hr_match_panel {
|
||||
position: fixed;
|
||||
top: 88px;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
width: unquote("min(540px, calc(100vw - 48px))");
|
||||
z-index: 1085;
|
||||
pointer-events: none;
|
||||
transform: translateX(calc(100% + 24px));
|
||||
transition: transform 0.26s ease;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_open {
|
||||
pointer-events: auto;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.o_hr_match_panel_backdropless {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: 26px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(56, 189, 248, 0.12), transparent 32%),
|
||||
radial-gradient(circle at bottom left, rgba(45, 212, 191, 0.09), transparent 26%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.985), rgba(246, 248, 251, 0.985));
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 28px 60px rgba(15, 23, 42, 0.2);
|
||||
}
|
||||
|
||||
.o_hr_match_panel_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 20px 22px 16px;
|
||||
background: linear-gradient(135deg, #12263f, #1f5f72);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_header p,
|
||||
.o_hr_match_panel_header h3,
|
||||
.o_hr_match_panel_header span {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_header p {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_header h3 {
|
||||
margin-top: 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_header span {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
opacity: 0.84;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_toolbar {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px 22px 0;
|
||||
}
|
||||
|
||||
.o_hr_match_tab_row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.o_hr_match_tab {
|
||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||
border-radius: 999px;
|
||||
padding: 9px 14px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
|
||||
.o_hr_match_tab span {
|
||||
margin-left: 6px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.o_hr_match_tab_active {
|
||||
border-color: #1f5f72;
|
||||
background: linear-gradient(135deg, #10394a, #1f5f72);
|
||||
color: #fff;
|
||||
box-shadow: 0 12px 24px rgba(16, 57, 74, 0.2);
|
||||
}
|
||||
|
||||
.o_hr_match_tab_active span {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.o_hr_match_search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||
border-radius: 16px;
|
||||
padding: 11px 14px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.o_hr_match_search:focus-within {
|
||||
border-color: rgba(31, 95, 114, 0.5);
|
||||
box-shadow: 0 0 0 4px rgba(31, 95, 114, 0.08);
|
||||
}
|
||||
|
||||
.o_hr_match_search_input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.o_hr_match_close {
|
||||
min-width: 42px;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_skill_summary {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px 22px;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.2);
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
}
|
||||
|
||||
.o_hr_match_panel_skill_summary label,
|
||||
.o_hr_match_skill_section label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.o_hr_match_chip_row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.o_hr_match_chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.o_hr_match_chip_primary {
|
||||
background: #ddeefe;
|
||||
color: #2258a5;
|
||||
}
|
||||
|
||||
.o_hr_match_chip_secondary {
|
||||
background: #dbf7ef;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.o_hr_match_chip_good {
|
||||
background: #dff7ee;
|
||||
color: #106a57;
|
||||
}
|
||||
|
||||
.o_hr_match_chip_warn {
|
||||
background: #fff2d9;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_body {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
padding: 16px 22px 22px;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_body_loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.o_hr_match_loading,
|
||||
.o_hr_match_empty {
|
||||
padding: 28px 18px;
|
||||
text-align: center;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.o_hr_match_card {
|
||||
margin-bottom: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.o_hr_match_card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(31, 95, 114, 0.28);
|
||||
box-shadow: 0 16px 30px rgba(15, 23, 42, 0.1);
|
||||
}
|
||||
|
||||
.o_hr_match_card_main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.o_hr_match_card_identity {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.o_hr_match_identity_copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.o_hr_match_rank {
|
||||
display: inline-flex;
|
||||
border-radius: 999px;
|
||||
padding: 5px 9px;
|
||||
background: #e0f7f4;
|
||||
color: #0f766e;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.o_hr_match_identity_copy h4,
|
||||
.o_hr_match_identity_copy p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.o_hr_match_identity_copy h4 {
|
||||
color: #0f172a;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.o_hr_match_identity_copy p {
|
||||
margin-top: 4px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.o_hr_match_card_actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.o_hr_match_score_ring {
|
||||
min-width: 72px;
|
||||
min-height: 72px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at 30% 30%, #dcf4ef, #c4ece2 55%, #8fd6c5);
|
||||
color: #0f172a;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_hr_match_score_ring span {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.o_hr_match_score_ring small {
|
||||
margin-top: 3px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.o_hr_match_inline_action {
|
||||
width: 38px;
|
||||
min-width: 38px;
|
||||
height: 38px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.o_hr_match_action_primary {
|
||||
background: linear-gradient(135deg, #10394a, #1f5f72);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.o_hr_match_action_secondary {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.o_hr_match_inline_action:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.o_hr_match_score_grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.o_hr_match_score_grid div {
|
||||
border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.o_hr_match_score_grid span {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.o_hr_match_score_grid strong {
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.o_hr_match_meta_value {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.o_hr_match_skill_split {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.o_hr_match_skill_split {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.o_hr_match_panel {
|
||||
top: 72px;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
width: calc(100vw - 24px);
|
||||
}
|
||||
|
||||
.o_hr_match_fab {
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
|
||||
.o_hr_match_panel_toolbar,
|
||||
.o_hr_match_panel_skill_summary,
|
||||
.o_hr_match_panel_body {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.o_hr_match_card_main,
|
||||
.o_hr_match_card_actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.o_hr_match_card_actions {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.o_hr_match_inline_action {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.o_hr_match_score_grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,15 +4,18 @@
|
|||
<field name="name">hr.applicant.view.list</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.crm_case_tree_view_job"/>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||
<attribute name="column_invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="after">
|
||||
<field name="recruitment_stage_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||
<attribute name="column_invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="after">
|
||||
<field name="recruitment_stage_id"/>
|
||||
<field name="primary_skill_match_percentage" optional="hide"/>
|
||||
<field name="secondary_skill_match_percentage" optional="hide"/>
|
||||
<field name="overall_skill_match_percentage" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_applicant_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.form</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
|
|
@ -48,21 +51,29 @@
|
|||
<widget name="web_ribbon" title="Awaiting Approval" bg_color="text-bg-warning" invisible="not approval_required or not application_submitted"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='job_id']" position="before">
|
||||
<field name="hr_job_recruitment"/>
|
||||
<field name="approval_required" invisible="1"/>
|
||||
<field name="application_submitted" invisible="1"/>
|
||||
<field name="stage_color" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='job_id']" position="after">
|
||||
<field name="employee_id" invisible="1"/>
|
||||
<field name="send_second_application_form"/>
|
||||
<field name="second_application_form_status" readonly="not send_second_application_form"/>
|
||||
<xpath expr="//field[@name='job_id']" position="before">
|
||||
<field name="hr_job_recruitment"/>
|
||||
<field name="approval_required" invisible="1"/>
|
||||
<field name="application_submitted" invisible="1"/>
|
||||
<field name="stage_color" invisible="1"/>
|
||||
<field name="hide_chatter_suggestion" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='job_id']" position="after">
|
||||
<field name="employee_id" invisible="1"/>
|
||||
<field name="send_second_application_form"/>
|
||||
<field name="second_application_form_status" readonly="not send_second_application_form"/>
|
||||
|
||||
<field name="send_post_onboarding_form"/>
|
||||
<field name="post_onboarding_form_status" readonly="not send_post_onboarding_form"/>
|
||||
<field name="doc_requests_form_status" readonly="1"/>
|
||||
</xpath>
|
||||
<field name="send_post_onboarding_form"/>
|
||||
<field name="post_onboarding_form_status" readonly="not send_post_onboarding_form"/>
|
||||
<field name="doc_requests_form_status" readonly="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='application_details']/group[1]" position="after">
|
||||
<group string="Skill Matching">
|
||||
<field name="primary_skill_match_percentage" readonly="1"/>
|
||||
<field name="secondary_skill_match_percentage" readonly="1"/>
|
||||
<field name="overall_skill_match_percentage" readonly="1"/>
|
||||
</group>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
|
|
@ -141,18 +152,21 @@
|
|||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="after">
|
||||
<sheet invisible="application_status != 'refused'">
|
||||
<group invisible="application_status != 'refused'">
|
||||
<field name="refused_stage" readonly="1" force_save="1" invisible="application_status != 'refused'"/>
|
||||
<field name="refuse_date" string="Refused On" readonly="1" force_save="1" invisible="application_status != 'refused'"/>
|
||||
<xpath expr="//notebook" position="after">
|
||||
<sheet invisible="application_status != 'refused'">
|
||||
<group invisible="application_status != 'refused'">
|
||||
<field name="refused_stage" readonly="1" force_save="1" invisible="application_status != 'refused'"/>
|
||||
<field name="refuse_date" string="Refused On" readonly="1" force_save="1" invisible="application_status != 'refused'"/>
|
||||
<field name="refused_comments" invisible="application_status != 'refused'"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
</group>
|
||||
</sheet>
|
||||
</xpath>
|
||||
<xpath expr="//chatter" position="attributes">
|
||||
<attribute name="invisible">hide_chatter_suggestion</attribute>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_applicant_view_search_bis_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.applicant.view.search</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
|
|
@ -428,4 +442,4 @@
|
|||
|
||||
|
||||
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -57,14 +57,15 @@
|
|||
<!-- </form>-->
|
||||
<!-- Add the recruitment_sequence field into the form -->
|
||||
|
||||
<form string="Job" js_class="recruitment_form_view">
|
||||
<header>
|
||||
<button name="send_mail_to_recruiters" type="object" string="Send Recruiters Notification" class="oe_highlight" groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
<field name="recruitment_status" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}"/>
|
||||
</header> <!-- inherited in other module -->
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="company_id" invisible="1" on_change="1" can_create="True" can_write="True"/>
|
||||
<sheet>
|
||||
<form string="Job" js_class="recruitment_form_view">
|
||||
<header>
|
||||
<button name="send_mail_to_recruiters" type="object" string="Send Recruiters Notification" class="oe_highlight" groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
<field name="recruitment_status" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}"/>
|
||||
</header> <!-- inherited in other module -->
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="hide_chatter_suggestion" invisible="1"/>
|
||||
<field name="company_id" invisible="1" on_change="1" can_create="True" can_write="True"/>
|
||||
<sheet>
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button"
|
||||
icon="fa-pencil"
|
||||
|
|
@ -108,12 +109,12 @@
|
|||
<button name="buttion_view_applicants" type="object" class="oe_stat_button"
|
||||
string="Candidates" widget="statinfo" icon="fa-th-large"/>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="float-end">
|
||||
<field name="website_published" widget="boolean_toggle_labeled" nolabel="1"
|
||||
options="{'false_label': 'Not Published', 'true_label': 'Published'}" on_change="1"/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="float-end">
|
||||
<field name="website_published" widget="boolean_toggle_labeled" nolabel="1"
|
||||
options="{'false_label': 'Not Published', 'true_label': 'Published'}" on_change="1"/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
|
||||
<field name="recruitment_sequence" readonly="0" force_save="1"/>
|
||||
<group>
|
||||
|
|
@ -214,10 +215,10 @@
|
|||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter open_attachments="True"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<chatter open_attachments="True" invisible="hide_chatter_suggestion"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="view_job_recruitment_filter" model="ir.ui.view">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="action_toggle_hr_job_recruitment_chatter_visibility" model="ir.actions.server">
|
||||
<field name="name">Show/Hide Chatter Suggestions</field>
|
||||
<field name="model_id" ref="model_hr_job_recruitment"/>
|
||||
<field name="binding_model_id" ref="model_hr_job_recruitment"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = records.action_toggle_chatter_visibility()</field>
|
||||
</record>
|
||||
|
||||
<record id="action_toggle_hr_applicant_chatter_visibility" model="ir.actions.server">
|
||||
<field name="name">Show/Hide Chatter Suggestions</field>
|
||||
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||
<field name="binding_model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = records.action_toggle_chatter_visibility()</field>
|
||||
</record>
|
||||
|
||||
<record id="action_toggle_hr_candidate_chatter_visibility" model="ir.actions.server">
|
||||
<field name="name">Show/Hide Chatter Suggestions</field>
|
||||
<field name="model_id" ref="hr_recruitment.model_hr_candidate"/>
|
||||
<field name="binding_model_id" ref="hr_recruitment.model_hr_candidate"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = records.action_toggle_chatter_visibility()</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_candidate_view_form_chatter_toggle_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.candidate.view.form.chatter.toggle.inherit</field>
|
||||
<field name="model">hr.candidate</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='active']" position="after">
|
||||
<field name="hide_chatter_suggestion" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//chatter" position="attributes">
|
||||
<attribute name="invisible">hide_chatter_suggestion</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from . import post_onboarding_attachment_wizard
|
||||
from . import applicant_refuse_reason
|
||||
from . import ats_invite_mail_template_wizard
|
||||
from . import client_submission_mail_template_wizard
|
||||
from . import post_onboarding_attachment_wizard
|
||||
from . import applicant_refuse_reason
|
||||
from . import ats_invite_mail_template_wizard
|
||||
from . import client_submission_mail_template_wizard
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class MasterSwitcher(http.Controller):
|
||||
|
||||
@http.route('/switch/master/<string:code>', type='http', auth='user')
|
||||
def switch_master(self, code):
|
||||
|
||||
request.session['active_master'] = code
|
||||
|
||||
request.env['ir.ui.menu'].sudo().clear_caches()
|
||||
|
||||
return request.redirect('/web?reload=1')
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class MasterSwitcher(http.Controller):
|
||||
|
||||
@http.route('/switch/master/<string:code>', type='http', auth='user')
|
||||
def switch_master(self, code):
|
||||
master = request.env['master.control'].sudo().search([
|
||||
('code', '=', code),
|
||||
('user_ids', 'in', [request.env.user.id]),
|
||||
], limit=1)
|
||||
|
||||
if not master:
|
||||
return request.redirect('/web')
|
||||
|
||||
request.session['active_master'] = master.code
|
||||
request.session['active_user'] = request.env.user.id
|
||||
|
||||
request.env['ir.ui.menu'].sudo().clear_caches()
|
||||
|
||||
return request.redirect('/web?reload=1')
|
||||
|
|
|
|||
|
|
@ -1,37 +1,41 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, onWillStart, useState } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class ModuleSelector extends Component {
|
||||
|
||||
static template = "module_selector_sidebar.ModuleSelector";
|
||||
static props = {};
|
||||
|
||||
setup() {
|
||||
|
||||
this.orm = useService("orm");
|
||||
|
||||
this.state = useState({
|
||||
masters: [],
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
|
||||
const masters = await this.orm.searchRead(
|
||||
"master.control",
|
||||
[],
|
||||
["name", "code"]
|
||||
);
|
||||
|
||||
this.state.masters = masters;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registry.category("systray").add("module_selector_sidebar", {
|
||||
Component: ModuleSelector,
|
||||
});
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, onWillStart, useState } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { user } from "@web/core/user";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class ModuleSelector extends Component {
|
||||
|
||||
static template = "module_selector_sidebar.ModuleSelector";
|
||||
static props = {};
|
||||
|
||||
setup() {
|
||||
|
||||
this.orm = useService("orm");
|
||||
|
||||
this.state = useState({
|
||||
masters: [],
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
if (!user.userId) {
|
||||
this.state.masters = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const masters = await this.orm.searchRead(
|
||||
"master.control",
|
||||
[["user_ids", "in", [user.userId]]],
|
||||
["name", "code"]
|
||||
);
|
||||
|
||||
this.state.masters = masters;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registry.category("systray").add("module_selector_sidebar", {
|
||||
Component: ModuleSelector,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
parent="project.menu_project_management"
|
||||
action="action_project_dashboard_fullscreen"
|
||||
groups="project.group_project_manager"
|
||||
active="0"
|
||||
sequence="10"/>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -18,7 +18,6 @@ Bridge module for project
|
|||
'views/project_sharing_views.xml',
|
||||
'views/project_portal_project_task_templates.xml',
|
||||
],
|
||||
'demo': ['data/project_demo.xml'],
|
||||
'auto_install': True,
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
|
|
|
|||
|
|
@ -109,48 +109,48 @@
|
|||
</record>
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record id="task_type_backlog" model="project.task.type">
|
||||
<field name="sequence">100</field>
|
||||
<field name="name">Backlog</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_development" model="project.task.type">
|
||||
<field name="sequence">101</field>
|
||||
<field name="name">Development</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_code_review_and_merging" model="project.task.type">
|
||||
<field name="sequence">102</field>
|
||||
<field name="name">Code Review & Git Merging</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_testing" model="project.task.type">
|
||||
<field name="sequence">103</field>
|
||||
<field name="name">Testing</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_deployment" model="project.task.type">
|
||||
<field name="sequence">104</field>
|
||||
<field name="name">Deployment</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_completed" model="project.task.type">
|
||||
<field name="sequence">105</field>
|
||||
<field name="name">Completed</field>
|
||||
<field name="fold" eval="True"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_backlog" model="project.task.type">
|
||||
<field name="sequence">100</field>
|
||||
<field name="name">Backlog</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_development" model="project.task.type">
|
||||
<field name="sequence">101</field>
|
||||
<field name="name">Development</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_code_review_and_merging" model="project.task.type">
|
||||
<field name="sequence">102</field>
|
||||
<field name="name">Code Review & Git Merging</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_testing" model="project.task.type">
|
||||
<field name="sequence">103</field>
|
||||
<field name="name">Testing</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_deployment" model="project.task.type">
|
||||
<field name="sequence">104</field>
|
||||
<field name="name">Deployment</field>
|
||||
<field name="fold" eval="False"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<record id="task_type_completed" model="project.task.type">
|
||||
<field name="sequence">105</field>
|
||||
<field name="name">Completed</field>
|
||||
<field name="fold" eval="True"/>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="is_workflow_template" eval="True"/>
|
||||
</record>
|
||||
<!-- <record id="task_type_cancelled" model="project.task.type">-->
|
||||
<!-- <field name="sequence">106</field>-->
|
||||
<!-- <field name="name">Cancelled</field>-->
|
||||
|
|
@ -264,4 +264,4 @@
|
|||
|
||||
</data>
|
||||
<function model="project.task" name="_sync_all_involved_assignees_from_timelines"/>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ def post_init_hook(env):
|
|||
('user_ids', 'in', user.id),
|
||||
'&',
|
||||
('is_generic', '=', False),
|
||||
('user_ids', 'in', user.id),
|
||||
'|',
|
||||
('user_ids', 'in', user.id),
|
||||
('involved_user_ids', 'in', user.id),
|
||||
]
|
||||
"""
|
||||
})
|
||||
|
|
@ -174,4 +176,5 @@ def post_init_hook(env):
|
|||
task.sequence_name = project.task_sequence_id.next_by_id()
|
||||
|
||||
# Normalize task stages so each project owns its workflow configuration.
|
||||
env['project.project'].search([])._ensure_project_owned_task_stages()
|
||||
if env['project.project'].search([]):
|
||||
env['project.project'].search([])._ensure_project_owned_task_stages()
|
||||
|
|
|
|||
|
|
@ -27,6 +27,19 @@ class projectTask(models.Model):
|
|||
_inherit = 'project.task'
|
||||
_rec_name = 'name'
|
||||
|
||||
user_ids = fields.Many2many()
|
||||
|
||||
involved_user_ids = fields.Many2many(
|
||||
'res.users',
|
||||
'project_task_involved_user_rel',
|
||||
'task_id',
|
||||
'user_id',
|
||||
string='Involved Assignees',
|
||||
tracking=True,
|
||||
domain="[('id', 'in', assignee_domain_ids)]",
|
||||
help='Supporting users who can collaborate on the task without owning it in My Tasks.'
|
||||
)
|
||||
|
||||
deadline_status = fields.Selection([
|
||||
('overdue', 'Overdue'),
|
||||
('near', 'Near Deadline'),
|
||||
|
|
@ -34,6 +47,8 @@ class projectTask(models.Model):
|
|||
], compute='_compute_deadline_status')
|
||||
|
||||
model_id = fields.Many2one('project.module.source', string="Module")
|
||||
allocation_start_date = fields.Date(string="Allocation Start Date")
|
||||
allocation_end_date = fields.Date(string="Allocation End Date")
|
||||
|
||||
@api.depends('date_deadline')
|
||||
def _compute_deadline_status(self):
|
||||
|
|
@ -64,9 +79,19 @@ class projectTask(models.Model):
|
|||
def write(self, vals):
|
||||
# Allow stage update for multiple records
|
||||
if 'stage_id' in vals:
|
||||
return super(projectTask, self).write(vals)
|
||||
result = super(projectTask, self).write(vals)
|
||||
if any(field in vals for field in ['allocation_start_date', 'allocation_end_date']):
|
||||
self._sync_allocated_hours_from_allocation_dates()
|
||||
if any(field in vals for field in ['user_ids', 'is_generic']):
|
||||
self._sync_involved_assignees_from_timelines()
|
||||
return result
|
||||
|
||||
return super(projectTask, self).write(vals)
|
||||
result = super(projectTask, self).write(vals)
|
||||
if any(field in vals for field in ['allocation_start_date', 'allocation_end_date']):
|
||||
self._sync_allocated_hours_from_allocation_dates()
|
||||
if any(field in vals for field in ['user_ids', 'is_generic']):
|
||||
self._sync_involved_assignees_from_timelines()
|
||||
return result
|
||||
|
||||
#
|
||||
# @api.constrains('name')
|
||||
|
|
@ -112,7 +137,12 @@ class projectTask(models.Model):
|
|||
):
|
||||
raise UserError("Only Task Creator or Project Manager can edit Generic field.")
|
||||
|
||||
return super(projectTask, self).write(vals)
|
||||
result = super(projectTask, self).write(vals)
|
||||
if any(field in vals for field in ['allocation_start_date', 'allocation_end_date']):
|
||||
self._sync_allocated_hours_from_allocation_dates()
|
||||
if any(field in vals for field in ['user_ids', 'is_generic']):
|
||||
self._sync_involved_assignees_from_timelines()
|
||||
return result
|
||||
|
||||
@api.constrains('estimated_hours')
|
||||
def _check_estimated_hours(self):
|
||||
|
|
@ -152,6 +182,7 @@ class projectTask(models.Model):
|
|||
store=False,
|
||||
)
|
||||
|
||||
|
||||
@api.depends(
|
||||
'project_id',
|
||||
'project_id.privacy_visibility',
|
||||
|
|
@ -186,6 +217,7 @@ class projectTask(models.Model):
|
|||
@api.depends(
|
||||
'is_generic',
|
||||
'user_ids',
|
||||
'involved_user_ids',
|
||||
'project_id',
|
||||
'project_id.privacy_visibility',
|
||||
'project_id.message_partner_ids',
|
||||
|
|
@ -218,14 +250,22 @@ class projectTask(models.Model):
|
|||
|
||||
# NORMAL TASK: assignees and involved collaborators
|
||||
else:
|
||||
users = task.user_ids | task.involved_user_ids
|
||||
employees = (
|
||||
task.user_ids
|
||||
users
|
||||
.mapped('employee_id')
|
||||
.filtered(lambda e: e)
|
||||
)
|
||||
|
||||
task.allowed_employee_ids = employees
|
||||
|
||||
@api.onchange('involved_user_ids', 'is_generic')
|
||||
def _onchange_assignee_domain_from_involved(self):
|
||||
for task in self:
|
||||
if task.is_generic:
|
||||
return {'domain': {'user_ids': [('id', 'in', task.assignee_domain_ids.ids)]}}
|
||||
return {'domain': {'user_ids': [('id', 'in', task.involved_user_ids.ids)]}}
|
||||
|
||||
@api.onchange('user_ids')
|
||||
def _onchange_user_ids(self):
|
||||
if self.project_id and (self.project_id.user_id or self.project_id.project_lead):
|
||||
|
|
@ -460,7 +500,7 @@ class projectTask(models.Model):
|
|||
task.suggested_deadline.strftime('%Y-%m-%d %H:%M') if task.suggested_deadline else _('Not available')
|
||||
))
|
||||
|
||||
@api.depends("project_id")
|
||||
@api.depends("project_id", "stage_id")
|
||||
def _compute_has_supervisor_access(self):
|
||||
administrative_users = self.env['project.role'].search([
|
||||
('role_level', '=', 'administrative')
|
||||
|
|
@ -478,6 +518,12 @@ class projectTask(models.Model):
|
|||
|
||||
stages = project.type_ids.sorted("sequence")
|
||||
|
||||
if not stages:
|
||||
continue
|
||||
|
||||
first_stage = stages[0]
|
||||
|
||||
|
||||
if first_stage:
|
||||
create_access_users = first_stage.team_id.team_lead + first_stage.involved_user_ids + administrative_users.user_ids
|
||||
else:
|
||||
|
|
@ -498,6 +544,52 @@ class projectTask(models.Model):
|
|||
for task in self:
|
||||
task.actual_hours = sum(task.timesheet_ids.mapped('unit_amount'))
|
||||
|
||||
def _get_allocation_calendar(self):
|
||||
self.ensure_one()
|
||||
return self.company_id.resource_calendar_id or self.env.company.resource_calendar_id
|
||||
|
||||
def _get_allocated_hours_from_dates(self):
|
||||
self.ensure_one()
|
||||
start_date = fields.Date.to_date(self.allocation_start_date or self.allocation_end_date)
|
||||
end_date = fields.Date.to_date(self.allocation_end_date or self.allocation_start_date)
|
||||
if not start_date:
|
||||
return 0.0
|
||||
if end_date < start_date:
|
||||
raise ValidationError(_("Allocation End Date cannot be before Allocation Start Date."))
|
||||
|
||||
calendar = self._get_allocation_calendar()
|
||||
hours_per_day = calendar.hours_per_day if calendar and calendar.hours_per_day else 8.0
|
||||
attendance_days = set()
|
||||
if calendar:
|
||||
attendance_days = {
|
||||
int(attendance.dayofweek)
|
||||
for attendance in calendar.attendance_ids
|
||||
}
|
||||
|
||||
total_days = 0
|
||||
current_date = start_date
|
||||
while current_date <= end_date:
|
||||
weekday = current_date.weekday()
|
||||
if attendance_days:
|
||||
if weekday in attendance_days:
|
||||
total_days += 1
|
||||
elif weekday < 5:
|
||||
total_days += 1
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return total_days * hours_per_day
|
||||
|
||||
def _sync_allocated_hours_from_allocation_dates(self):
|
||||
for task in self:
|
||||
if task.allocation_start_date or task.allocation_end_date:
|
||||
task.allocated_hours = task._get_allocated_hours_from_dates()
|
||||
|
||||
@api.onchange('allocation_start_date', 'allocation_end_date')
|
||||
def _onchange_allocation_dates(self):
|
||||
for task in self:
|
||||
if task.allocation_start_date or task.allocation_end_date:
|
||||
task.allocated_hours = task._get_allocated_hours_from_dates()
|
||||
|
||||
def _post_to_project_channel(self, message_body, mention_partners=None):
|
||||
"""Post message to project's discuss channel with proper Odoo mention format"""
|
||||
for task in self:
|
||||
|
|
@ -784,7 +876,7 @@ class projectTask(models.Model):
|
|||
task.stage_id = n_stage
|
||||
task.approval_status = "approved"
|
||||
|
||||
activity_log = "%s: approved by %s and moved to %s" % (
|
||||
activity_log = "%s: ✅ approved by %s and moved to %s" % (
|
||||
current_stage.name,
|
||||
self.env.user.employee_id.name,
|
||||
n_stage.name)
|
||||
|
|
@ -825,9 +917,9 @@ class projectTask(models.Model):
|
|||
)
|
||||
else:
|
||||
task.approval_status = "approved"
|
||||
notes = "%s: Task approved and completed by %s" % (task.sequence_name, self.env.user.employee_id.name)
|
||||
notes = "%s: ✅ Task approved and completed by %s" % (task.sequence_name, self.env.user.employee_id.name)
|
||||
|
||||
activity_log = "%s: approved by %s" % (
|
||||
activity_log = "%s: ✅ approved by %s" % (
|
||||
current_stage.name,
|
||||
self.env.user.employee_id.name)
|
||||
|
||||
|
|
@ -863,9 +955,9 @@ class projectTask(models.Model):
|
|||
# Optional: find previous stage if you want to send back
|
||||
stage = task.assignees_timelines.filtered(lambda s: s.stage_id == task.stage_id)
|
||||
|
||||
notes = "%s: %s rejected by %s" % (task.sequence_name, current_stage.name, self.env.user.employee_id.name)
|
||||
notes = "%s: ❌ %s rejected by %s" % (task.sequence_name, current_stage.name, self.env.user.employee_id.name)
|
||||
|
||||
activity_log = "%s: rejected by %s: %s" % (
|
||||
activity_log = "%s: ❌ rejected by %s: %s" % (
|
||||
current_stage.name,
|
||||
self.env.user.employee_id.name,
|
||||
reason)
|
||||
|
|
@ -953,6 +1045,7 @@ class projectTask(models.Model):
|
|||
|
||||
if timeline_vals:
|
||||
self.env['project.task.time.lines'].create(timeline_vals)
|
||||
task._sync_involved_assignees_from_timelines()
|
||||
|
||||
# Post to project channel about timeline request
|
||||
channel_message = _("Timelines requested for task %s") % (task.sequence_name or task.name)
|
||||
|
|
@ -962,6 +1055,7 @@ class projectTask(models.Model):
|
|||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
tasks = super().create(vals_list)
|
||||
tasks._sync_allocated_hours_from_allocation_dates()
|
||||
# Group tasks by project to avoid creating multiple sequences for the same project
|
||||
project_dict = {}
|
||||
for task in tasks:
|
||||
|
|
@ -1026,6 +1120,14 @@ class projectTask(models.Model):
|
|||
involved_users = list((existing_user_ids | timeline_user_ids) - set(task.user_ids.ids))
|
||||
task.involved_user_ids = [(6, 0, involved_users)]
|
||||
|
||||
@api.model
|
||||
def _sync_all_involved_assignees_from_timelines(self):
|
||||
tasks = self.search([
|
||||
('is_generic', '=', False),
|
||||
('assignees_timelines', '!=', False),
|
||||
])
|
||||
tasks._sync_involved_assignees_from_timelines()
|
||||
return True
|
||||
def _fetch_planning_overlap(self, additional_domain=None):
|
||||
use_timeline_logic = any(
|
||||
t.timelines_requested and t.show_approval_flow and t.estimated_hours > 0
|
||||
|
|
@ -1497,6 +1599,23 @@ class projectTaskTimelines(models.Model):
|
|||
estimated_start_datetime = fields.Datetime(string="Estimated Start Date Time")
|
||||
estimated_end_datetime = fields.Datetime(string="Estimated End Date Time")
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
records.mapped('task_id')._sync_involved_assignees_from_timelines()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
tasks = self.mapped('task_id')
|
||||
result = super().write(vals)
|
||||
(tasks | self.mapped('task_id'))._sync_involved_assignees_from_timelines()
|
||||
return result
|
||||
|
||||
def unlink(self):
|
||||
tasks = self.mapped('task_id')
|
||||
result = super().unlink()
|
||||
tasks._sync_involved_assignees_from_timelines()
|
||||
return result
|
||||
# @api.constrains('estimated_start_datetime', 'estimated_end_datetime')
|
||||
# def _check_dates(self):
|
||||
# for rec in self:
|
||||
|
|
@ -1545,15 +1664,20 @@ class projectTaskTimelines(models.Model):
|
|||
task.actual_time = sum(
|
||||
task.task_id.timesheet_ids.filtered(lambda t: t.stage_id == stage).mapped('unit_amount'))
|
||||
|
||||
@api.depends('team_id', 'project_id')
|
||||
@api.depends('team_id', 'project_id', 'task_id.user_ids', 'task_id.involved_user_ids', 'responsible_lead', 'assigned_to')
|
||||
def _compute_team_members(self):
|
||||
for rec in self:
|
||||
members = self.env['res.users']
|
||||
task_users = rec.task_id.user_ids | rec.task_id.involved_user_ids
|
||||
if rec.team_id:
|
||||
valid_members = rec.team_id.all_members_ids.filtered(lambda u: u.exists())
|
||||
lead = rec.team_id.team_lead if rec.team_id.team_lead.exists() else False
|
||||
rec.team_all_member_ids = list(set(valid_members.ids + ([lead.id] if lead else [])))
|
||||
|
||||
elif task_users:
|
||||
extra_users = rec.responsible_lead | rec.assigned_to
|
||||
rec.team_all_member_ids = (task_users | extra_users).filtered(lambda u: u.exists()).ids
|
||||
|
||||
elif rec.project_id and rec.project_id.privacy_visibility == 'followers':
|
||||
project_members = rec.project_id.members_ids.filtered(lambda u: u.exists())
|
||||
partners = rec.project_id.message_partner_ids.mapped('user_ids').filtered(lambda u: u.exists())
|
||||
|
|
@ -1592,4 +1716,3 @@ class projectTaskTimelines(models.Model):
|
|||
allowed_teams |= team
|
||||
allowed_teams |= team.child_ids
|
||||
rec.allowed_team_ids = allowed_teams
|
||||
|
||||
|
|
|
|||
|
|
@ -1,161 +1,308 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="group_project_supervisor" model="res.groups">
|
||||
<field name="name">Manager</field>
|
||||
<field name="category_id" ref="base.module_category_services_project"/>
|
||||
<field name="implied_ids" eval="[(4, ref('project.group_project_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_project_lead" model="res.groups">
|
||||
<field name="name">Project Lead</field>
|
||||
<field name="category_id" ref="base.module_category_services_project"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="project.group_project_manager" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('project.group_project_user')),(4, ref('group_project_supervisor')), (4, ref('mail.group_mail_canned_response_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<data>
|
||||
<record id="portfolio_rule_company_projects" model="ir.rule">
|
||||
<field name="name">company: Own Company</field>
|
||||
<field name="model_id" ref="model_project_portfolio"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
<record id="project_rule_manager_own_projects" model="ir.rule">
|
||||
<field name="name">Manager: Own Projects</field>
|
||||
<field name="model_id" ref="project.model_project_project"/>
|
||||
<field name="groups" eval="[(4, ref('project_task_timesheet_extended.group_project_supervisor'))]"/>
|
||||
<field name="domain_force">[('user_id', '=', user.id)]</field>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="project_supervisor_all_project_tasks_rule">
|
||||
<field name="name">Project/Task: project supervisor: see all tasks linked to his assigned project or its own tasks</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
('project_id.user_id','=',user.id),
|
||||
'|', ('project_id', '!=', False),
|
||||
('user_ids', 'in', user.id),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4,ref('project_task_timesheet_extended.group_project_supervisor'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="project_users_project_tasks_rule">
|
||||
<field name="name">Project/Task: project users: don't see non generic tasks</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
'&', '&',
|
||||
('project_id', '!=', False),
|
||||
('is_generic', '=', False),
|
||||
('user_ids', 'not in', user.id),
|
||||
]
|
||||
</field>
|
||||
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="project_users_project_lead_rule">
|
||||
<field name="name">Project/Task: project lead: see all tasks</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
'&', '&', '&',
|
||||
('project_id', '!=', False),
|
||||
('project_id.project_lead', '=', user.id),
|
||||
'|', ('is_generic', '=', True), ('is_generic', '=', False),
|
||||
'|', ('user_ids', 'in', user.id), ('user_ids', 'not in', user.id)
|
||||
]
|
||||
</field>
|
||||
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="user_task_availability_project_lead_rule">
|
||||
<field name="name">Task Availability: project lead: see all user tasks</field>
|
||||
<field name="model_id" ref="model_user_task_availability"/>
|
||||
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
|
||||
<field name="domain_force">[
|
||||
'|', '|',
|
||||
('project_id.project_lead', '=', user.id),
|
||||
('user_id', '=', user.id),
|
||||
('project_id.user_id', '=', user.id),
|
||||
]
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- <record model="ir.rule" id="timesheet_users_normal_timesheets">-->
|
||||
<!-- <field name="name">timesheet: users: see own tasks</field>-->
|
||||
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
|
||||
<!-- <field name="domain_force">[-->
|
||||
<!-- '&','&', '&', '&', '&','&',-->
|
||||
<!-- ('project_id.privacy_visibility','=','followers'),-->
|
||||
<!-- ('task_id', '!=', False),-->
|
||||
<!-- ('project_id', '!=', False),-->
|
||||
<!-- ('project_id.project_lead', '!=', user.id),-->
|
||||
<!-- ('project_id.user_id', '!=', user.id),-->
|
||||
<!-- ('user_id','not in',[user.id]),-->
|
||||
<!-- '|',-->
|
||||
<!-- '&',-->
|
||||
<!-- ('task_id.is_generic', '=', False),-->
|
||||
<!-- ('task_id.user_ids', 'not in', [user.id]),-->
|
||||
<!-- '&',-->
|
||||
<!-- ('task_id.is_generic', '=', True),-->
|
||||
<!-- ('project_id.message_partner_ids', 'not in', [user.partner_id.id]),-->
|
||||
<!-- ]</field>-->
|
||||
<!-- <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user')),(4,ref('project_task_timesheet_extended.group_project_supervisor')),(4,ref('hr_timesheet.group_hr_timesheet_user')),(4,ref('hr_timesheet.group_hr_timesheet_approver'))]"/>-->
|
||||
<!-- <field name="perm_read" eval="0"/>-->
|
||||
<!-- <field name="perm_unlink" eval="1"/>-->
|
||||
<!-- <field name="perm_write" eval="1"/>-->
|
||||
<!-- <field name="perm_create" eval="1"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<!-- <record model="ir.rule" id="timesheet_users_non_generic_timesheets">-->
|
||||
<!-- <field name="name">timesheet: users: see own tasks</field>-->
|
||||
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
|
||||
<!-- <field name="domain_force">[-->
|
||||
<!-- ('project_id.privacy_visibility','!=','followers'),-->
|
||||
<!-- ('task_id', '!=', False),-->
|
||||
<!-- ('project_id', '!=', False),-->
|
||||
<!-- ('project_id.project_lead', '!=', user.id),-->
|
||||
<!-- ('project_id.user_id', '!=', user.id),-->
|
||||
<!-- ('task_id.is_generic', '=', False),-->
|
||||
<!-- ('task_id.user_ids', 'not in', [user.id]),-->
|
||||
<!-- ('user_id','not in',[user.id]),-->
|
||||
<!-- ]</field>-->
|
||||
<!-- <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user')),(4,ref('project_task_timesheet_extended.group_project_supervisor')),(4,ref('hr_timesheet.group_hr_timesheet_user')),(4,ref('hr_timesheet.group_hr_timesheet_approver'))]"/>-->
|
||||
<!-- <field name="perm_read" eval="0"/>-->
|
||||
<!-- <field name="perm_unlink" eval="1"/>-->
|
||||
<!-- <field name="perm_write" eval="1"/>-->
|
||||
<!-- <field name="perm_create" eval="1"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
|
||||
<!-- <record model="ir.rule" id="timesheet_team_lead_normal_timesheets">-->
|
||||
<!-- <field name="name">timesheet: Lead: see related tasks</field>-->
|
||||
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
|
||||
<!-- <field name="domain_force">[-->
|
||||
<!-- '&', '&', '&',-->
|
||||
<!-- ('project_id', '!=', False),-->
|
||||
<!-- ('project_id.project_lead', '=', user.id),-->
|
||||
<!-- '|', ('task_id.is_generic', '=', True), ('task_id.is_generic', '=', False),-->
|
||||
<!-- '|', ('task_id.user_ids', 'in', user.id), ('task_id.user_ids', 'not in', user.id)-->
|
||||
<!-- ]-->
|
||||
<!-- </field>-->
|
||||
<!-- <field name="perm_read" eval="1"/>-->
|
||||
<!-- <field name="perm_write" eval="1"/>-->
|
||||
<!-- <field name="perm_create" eval="1"/>-->
|
||||
<!-- <field name="perm_unlink" eval="0"/>-->
|
||||
<!-- </record>-->
|
||||
</data>
|
||||
<data>
|
||||
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="group_project_supervisor" model="res.groups">
|
||||
<field name="name">Manager</field>
|
||||
<field name="category_id" ref="base.module_category_services_project"/>
|
||||
<field name="implied_ids" eval="[(4, ref('project.group_project_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_project_lead" model="res.groups">
|
||||
<field name="name">Project Lead</field>
|
||||
<field name="category_id" ref="base.module_category_services_project"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="project.group_project_manager" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('project.group_project_user')),(4, ref('group_project_supervisor')), (4, ref('mail.group_mail_canned_response_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<data>
|
||||
<function name="write" model="ir.model.data">
|
||||
<function name="search" model="ir.model.data">
|
||||
<value eval="[('module', '=', 'project'), ('name', '=', 'project_public_members_rule')]"/>
|
||||
</function>
|
||||
<value eval="{'noupdate': False}"/>
|
||||
</function>
|
||||
|
||||
<record id="project.project_public_members_rule" model="ir.rule">
|
||||
<field name="domain_force">['|',
|
||||
('privacy_visibility', '!=', 'followers'),
|
||||
('message_ids', 'in', [user.id])
|
||||
]</field>
|
||||
</record>
|
||||
|
||||
<function name="write" model="ir.model.data">
|
||||
<function name="search" model="ir.model.data">
|
||||
<value eval="[('module', '=', 'project'), ('name', '=', 'project_public_members_rule')]"/>
|
||||
</function>
|
||||
<value eval="{'noupdate': True}"/>
|
||||
</function>
|
||||
|
||||
<record id="portfolio_rule_company_projects" model="ir.rule">
|
||||
<field name="name">company: Own Company</field>
|
||||
<field name="model_id" ref="model_project_portfolio"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
<record id="project_rule_manager_own_projects" model="ir.rule">
|
||||
<field name="name">Manager: Own Projects</field>
|
||||
<field name="model_id" ref="project.model_project_project"/>
|
||||
<field name="groups" eval="[(4, ref('project_task_timesheet_extended.group_project_supervisor'))]"/>
|
||||
<field name="domain_force">[('user_id', '=', user.id)]</field>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="project_supervisor_all_project_tasks_rule">
|
||||
<field name="name">Project/Task: project supervisor: see all tasks linked to his assigned project or its own tasks</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
('project_id.user_id','=',user.id),
|
||||
'|', ('project_id', '!=', False),
|
||||
('user_ids', 'in', user.id),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4,ref('project_task_timesheet_extended.group_project_supervisor'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="project_users_project_tasks_rule">
|
||||
<field name="name">Project/Task: project users: don't see non generic tasks</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
'&', '&', '&',
|
||||
('project_id', '!=', False),
|
||||
('is_generic', '=', False),
|
||||
('user_ids', 'not in', user.id),
|
||||
('involved_user_ids', 'not in', user.id),
|
||||
]
|
||||
</field>
|
||||
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
|
||||
<field name="perm_read" eval="0"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="project_users_project_lead_rule">
|
||||
<field name="name">Project/Task: project lead: see all tasks</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
'&', '&', '&',
|
||||
('project_id', '!=', False),
|
||||
('project_id.project_lead', '=', user.id),
|
||||
'|', ('is_generic', '=', True), ('is_generic', '=', False),
|
||||
'|', ('user_ids', 'in', user.id), ('user_ids', 'not in', user.id)
|
||||
]
|
||||
</field>
|
||||
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record model="ir.rule" id="project.ir_rule_private_task">
|
||||
<field name="domain_force">[
|
||||
'&',
|
||||
('project_id', '!=', False),
|
||||
('project_id.privacy_visibility', '!=', 'followers'),
|
||||
'|',
|
||||
'&',
|
||||
('is_generic', '=', True),
|
||||
'|', '|',
|
||||
('project_id', '!=', False),
|
||||
('parent_id', '!=', False),
|
||||
('user_ids', 'in', user.id),
|
||||
'&',
|
||||
('is_generic', '=', False),
|
||||
'|',
|
||||
('user_ids', 'in', user.id),
|
||||
('involved_user_ids', 'in', user.id),
|
||||
]</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="project.task_visibility_rule">
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
'&',
|
||||
('project_id', '!=', False),
|
||||
'|', '|',
|
||||
('is_generic', '=', True),
|
||||
('user_ids', 'in', user.id),
|
||||
('involved_user_ids', 'in', user.id),
|
||||
'&',
|
||||
('project_id', '=', False),
|
||||
'|',
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
('user_ids', 'in', user.id),
|
||||
]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="project.task_visibility_rule_project_user">
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
'&',
|
||||
('project_id', '!=', False),
|
||||
'|', '|',
|
||||
('is_generic', '=', True),
|
||||
('user_ids', 'in', user.id),
|
||||
('involved_user_ids', 'in', user.id),
|
||||
'&',
|
||||
('project_id', '=', False),
|
||||
'|',
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
('user_ids', 'in', user.id),
|
||||
]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="hr_timesheet.timesheet_line_rule_user">
|
||||
<field name="domain_force">[
|
||||
'&', '&',
|
||||
('user_id', '=', user.id),
|
||||
('project_id', '!=', False),
|
||||
'|', '|',
|
||||
('project_id.privacy_visibility', '!=', 'followers'),
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
'&',
|
||||
('task_id', '!=', False),
|
||||
'|',
|
||||
('task_id.user_ids', 'in', user.id),
|
||||
('task_id.involved_user_ids', 'in', user.id)
|
||||
]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="hr_timesheet.timesheet_line_rule_approver">
|
||||
<field name="domain_force">[
|
||||
'&', '&',
|
||||
('project_id', '!=', False),
|
||||
('task_id', '!=', False),
|
||||
'|',
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'followers'),
|
||||
'|',
|
||||
'|',
|
||||
('project_id.project_lead', '=', user.id),
|
||||
('project_id.user_id', '=', user.id),
|
||||
'|',
|
||||
'&',
|
||||
('task_id.is_generic', '=', False),
|
||||
'|',
|
||||
('user_id', 'in', 'task_id.user_ids'),
|
||||
('user_id', 'in', 'task_id.involved_user_ids'),
|
||||
'&',
|
||||
('task_id.is_generic', '=', True),
|
||||
('user_id.partner_id', 'in', 'project_id.message_partner_ids'),
|
||||
'&', '&',
|
||||
('project_id.privacy_visibility', '!=', 'followers'),
|
||||
('task_id.is_generic', '=', False),
|
||||
'|',
|
||||
('user_id', 'in', 'task_id.user_ids'),
|
||||
('user_id', 'in', 'task_id.involved_user_ids')
|
||||
]</field>
|
||||
</record>
|
||||
<record model="ir.rule" id="project_task_involved_assignee_rule">
|
||||
<field name="name">Project/Task: involved assignees see involved tasks</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[('involved_user_ids', 'in', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('project.group_project_user'))]"/>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="0"/>
|
||||
<field name="perm_unlink" eval="0"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="timesheet_line_involved_assignee_rule">
|
||||
<field name="name">Timesheets: involved assignees can manage own lines</field>
|
||||
<field name="model_id" ref="analytic.model_account_analytic_line"/>
|
||||
<field name="domain_force">[
|
||||
'&', '&', '&',
|
||||
('user_id', '=', user.id),
|
||||
('project_id', '!=', False),
|
||||
('task_id', '!=', False),
|
||||
('task_id.involved_user_ids', 'in', user.id)
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('hr_timesheet.group_hr_timesheet_user'))]"/>
|
||||
<field name="perm_read" eval="1"/>
|
||||
<field name="perm_write" eval="1"/>
|
||||
<field name="perm_create" eval="1"/>
|
||||
<field name="perm_unlink" eval="1"/>
|
||||
</record>
|
||||
<record model="ir.rule" id="user_task_availability_project_lead_rule">
|
||||
<field name="name">Task Availability: project lead: see all user tasks</field>
|
||||
<field name="model_id" ref="model_user_task_availability"/>
|
||||
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
|
||||
<field name="domain_force">[
|
||||
'|', '|',
|
||||
('project_id.project_lead', '=', user.id),
|
||||
('user_id', '=', user.id),
|
||||
('project_id.user_id', '=', user.id),
|
||||
]
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- <record model="ir.rule" id="timesheet_users_normal_timesheets">-->
|
||||
<!-- <field name="name">timesheet: users: see own tasks</field>-->
|
||||
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
|
||||
<!-- <field name="domain_force">[-->
|
||||
<!-- '&','&', '&', '&', '&','&',-->
|
||||
<!-- ('project_id.privacy_visibility','=','followers'),-->
|
||||
<!-- ('task_id', '!=', False),-->
|
||||
<!-- ('project_id', '!=', False),-->
|
||||
<!-- ('project_id.project_lead', '!=', user.id),-->
|
||||
<!-- ('project_id.user_id', '!=', user.id),-->
|
||||
<!-- ('user_id','not in',[user.id]),-->
|
||||
<!-- '|',-->
|
||||
<!-- '&',-->
|
||||
<!-- ('task_id.is_generic', '=', False),-->
|
||||
<!-- ('task_id.user_ids', 'not in', [user.id]),-->
|
||||
<!-- '&',-->
|
||||
<!-- ('task_id.is_generic', '=', True),-->
|
||||
<!-- ('project_id.message_partner_ids', 'not in', [user.partner_id.id]),-->
|
||||
<!-- ]</field>-->
|
||||
<!-- <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user')),(4,ref('project_task_timesheet_extended.group_project_supervisor')),(4,ref('hr_timesheet.group_hr_timesheet_user')),(4,ref('hr_timesheet.group_hr_timesheet_approver'))]"/>-->
|
||||
<!-- <field name="perm_read" eval="0"/>-->
|
||||
<!-- <field name="perm_unlink" eval="1"/>-->
|
||||
<!-- <field name="perm_write" eval="1"/>-->
|
||||
<!-- <field name="perm_create" eval="1"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<!-- <record model="ir.rule" id="timesheet_users_non_generic_timesheets">-->
|
||||
<!-- <field name="name">timesheet: users: see own tasks</field>-->
|
||||
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
|
||||
<!-- <field name="domain_force">[-->
|
||||
<!-- ('project_id.privacy_visibility','!=','followers'),-->
|
||||
<!-- ('task_id', '!=', False),-->
|
||||
<!-- ('project_id', '!=', False),-->
|
||||
<!-- ('project_id.project_lead', '!=', user.id),-->
|
||||
<!-- ('project_id.user_id', '!=', user.id),-->
|
||||
<!-- ('task_id.is_generic', '=', False),-->
|
||||
<!-- ('task_id.user_ids', 'not in', [user.id]),-->
|
||||
<!-- ('user_id','not in',[user.id]),-->
|
||||
<!-- ]</field>-->
|
||||
<!-- <field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user')),(4,ref('project_task_timesheet_extended.group_project_supervisor')),(4,ref('hr_timesheet.group_hr_timesheet_user')),(4,ref('hr_timesheet.group_hr_timesheet_approver'))]"/>-->
|
||||
<!-- <field name="perm_read" eval="0"/>-->
|
||||
<!-- <field name="perm_unlink" eval="1"/>-->
|
||||
<!-- <field name="perm_write" eval="1"/>-->
|
||||
<!-- <field name="perm_create" eval="1"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
|
||||
<!-- <record model="ir.rule" id="timesheet_team_lead_normal_timesheets">-->
|
||||
<!-- <field name="name">timesheet: Lead: see related tasks</field>-->
|
||||
<!-- <field name="model_id" ref="analytic.model_account_analytic_line"/>-->
|
||||
<!-- <field name="domain_force">[-->
|
||||
<!-- '&', '&', '&',-->
|
||||
<!-- ('project_id', '!=', False),-->
|
||||
<!-- ('project_id.project_lead', '=', user.id),-->
|
||||
<!-- '|', ('task_id.is_generic', '=', True), ('task_id.is_generic', '=', False),-->
|
||||
<!-- '|', ('task_id.user_ids', 'in', user.id), ('task_id.user_ids', 'not in', user.id)-->
|
||||
<!-- ]-->
|
||||
<!-- </field>-->
|
||||
<!-- <field name="perm_read" eval="1"/>-->
|
||||
<!-- <field name="perm_write" eval="1"/>-->
|
||||
<!-- <field name="perm_create" eval="1"/>-->
|
||||
<!-- <field name="perm_unlink" eval="0"/>-->
|
||||
<!-- </record>-->
|
||||
</data>
|
||||
<data>
|
||||
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@
|
|||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='user_ids']" position="attributes">
|
||||
<attribute name="domain">[('id', 'in', assignee_domain_ids)]</attribute>
|
||||
<attribute name="widget">involved_assignee_avatar_user</attribute>
|
||||
<attribute name="domain">[('id', 'in', involved_user_ids)]</attribute>
|
||||
<attribute name="options">{'no_create': True, 'no_quick_create': True, 'no_create_edit': True}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='timesheet_ids']" position="attributes">
|
||||
<attribute name="context">
|
||||
|
|
@ -58,6 +60,7 @@
|
|||
|
||||
|
||||
<xpath expr="//field[@name='user_ids']" position="after">
|
||||
<field name="involved_user_ids" widget="many2many_avatar_user" invisible="is_generic"/>
|
||||
<field name="is_generic" readonly="not has_supervisor_access"/>
|
||||
<field name="record_paused" invisible="1"/>
|
||||
<field name="model_id" readonly="not has_supervisor_access" options="{'no_open': True}"/>
|
||||
|
|
@ -69,8 +72,8 @@
|
|||
|
||||
<xpath expr="//sheet/notebook" position="inside">
|
||||
<page string="Assignees Timelines" invisible="not show_approval_flow">
|
||||
<button name="button_update_assignees" type="object" string="Update Assignees" class="oe_highlight"/>
|
||||
<button name="action_assign_approx_deadlines" type="object" string="Assign Approx Timeline" class="oe_highlight"/>
|
||||
<button name="action_assign_approx_deadlines" type="object" string="Assign Approx Timeline"
|
||||
class="oe_highlight"/>
|
||||
<field name="assignees_timelines" context="{'default_task_id': id}" options="{'no_open': True}">
|
||||
<list editable="bottom">
|
||||
<field name="stage_id" readonly="not has_edit_access"/>
|
||||
|
|
@ -159,8 +162,14 @@
|
|||
<field name="actual_hours" widget="timesheet_uom_no_toggle"/>
|
||||
<field name="is_suggested_deadline_warning" />
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='page_timesheets']/field[@name='timesheet_ids']/list/field[@name='name']" position="after">
|
||||
<field name="stage_id" required="0" readonly="readonly_timesheet" options="{'no_create': True,'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||
<xpath expr="//field[@name='allocated_hours']" position="after">
|
||||
<field name="allocation_start_date" nolabel="1" string="" widget="daterange" class="oe_inline o_allocated_time_calendar" options="{'end_date_field': 'allocation_end_date', 'always_range': '1'}"/>
|
||||
<field name="allocation_end_date" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='page_timesheets']/field[@name='timesheet_ids']/list/field[@name='name']"
|
||||
position="after">
|
||||
<field name="stage_id" required="0" readonly="readonly_timesheet"
|
||||
options="{'no_create': True,'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
@ -202,5 +211,19 @@
|
|||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<record id="project.action_view_my_task" model="ir.actions.act_window">
|
||||
<field name="domain">['|', ('user_ids', 'in', uid), ('involved_user_ids', 'in', uid)]</field>
|
||||
</record>
|
||||
|
||||
<record id="project_task_timesheet_extended_my_task_search_involved" model="ir.ui.view">
|
||||
<field name="name">project.task.search.my.tasks.involved</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_search_form_project_fsm_base"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='my_tasks']" position="attributes">
|
||||
<attribute name="domain">['|', ('user_ids', 'in', uid), ('involved_user_ids', 'in', uid)]</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -65,7 +65,7 @@ class UserTimelineEntry(models.Model):
|
|||
return f"""
|
||||
SELECT
|
||||
CONCAT('task-', task.id::varchar, '-', rel.user_id::varchar) AS source_key,
|
||||
COALESCE(task.sequence_name, task.name) AS name,
|
||||
COALESCE(task.name) AS name,
|
||||
employee.id AS employee_id,
|
||||
COALESCE(project.company_id, task.company_id, employee.company_id) AS company_id,
|
||||
rel.user_id AS user_id,
|
||||
|
|
|
|||
Loading…
Reference in New Issue