Recruitment changes, Menu Contral Center xml change, attachment preview changes
This commit is contained in:
parent
ad5967d420
commit
4b85cc0f59
|
|
@ -24,4 +24,4 @@ class CandidateExperience(models.Model):
|
|||
@api.depends('experience_code', 'experience_from', 'experience_to')
|
||||
def _compute_display_name(self):
|
||||
for template in self:
|
||||
template.display_name = False if not template.experience_code else f"{template.experience_code} ({template.experience_from} - {template.experience_to} Years)"
|
||||
template.display_name = False if not template.experience_code else (f"{template.experience_code} ({template.experience_from} - {template.experience_to} Years)" if template.experience_to > 0 else f"{template.experience_code}")
|
||||
|
|
@ -7,6 +7,7 @@ import datetime
|
|||
|
||||
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'
|
||||
|
|
@ -233,6 +234,12 @@ class HRJobRecruitment(models.Model):
|
|||
|
||||
recruitment_status = fields.Selection([('open','Open'),('closed','Closed'),('hold','Hold'),('modified','Modified'),('cancelled','Cancelled')], default='open', tracking=True)
|
||||
|
||||
@api.onchange('recruitment_status')
|
||||
def _onchange_recruitment_status(self):
|
||||
for rec in self:
|
||||
if rec.recruitment_status != 'open':
|
||||
rec.website_published == False
|
||||
|
||||
@api.onchange('job_id','job_category')
|
||||
def onchange_job_id(self):
|
||||
for rec in self:
|
||||
|
|
|
|||
|
|
@ -6,19 +6,52 @@ from odoo.exceptions import ValidationError
|
|||
class RecruitmentRequisition(models.Model):
|
||||
_inherit = 'recruitment.requisition'
|
||||
|
||||
hr_job_recruitment = fields.Many2one('hr.job.recruitment')
|
||||
hr_job_recruitment = fields.Many2one('hr.job.recruitment', tracking=True)
|
||||
position_title = fields.Char(string="Position Title", required=False,related='job_id.name')
|
||||
job_category = fields.Many2one('job.category', tracking=True)
|
||||
job_priority = fields.Selection([
|
||||
('low', 'Low'),
|
||||
('medium', 'Medium'),
|
||||
('high', 'High')
|
||||
], string='Priority', tracking=True)
|
||||
experience = fields.Many2one('candidate.experience', string="Experience", tracking=True)
|
||||
recruitment_status = fields.Selection([('open','Open'),('closed','Closed'),('hold','Hold'),('modified','Modified'),('cancelled','Cancelled')],related='hr_job_recruitment.recruitment_status',readonly=False, tracking=True)
|
||||
|
||||
def button_create_jd(self):
|
||||
self.hr_job_recruitment = self.env['hr.job.recruitment'].create({
|
||||
'job_id': self.job_id.id,
|
||||
'department_id': self.department_id.id,
|
||||
'no_of_recruitment':self.number_of_positions,
|
||||
'description':self.job_description,
|
||||
'skill_ids': [(6, 0, self.primary_skill_ids.ids)],
|
||||
'secondary_skill_ids': [(6, 0, self.secondary_skill_ids.ids)],
|
||||
'requested_by': self.requested_by.partner_id.id,
|
||||
'user_id': self.assign_to.id
|
||||
})
|
||||
@api.onchange('recruitment_status')
|
||||
def _onchange_recruitment_status(self):
|
||||
for rec in self:
|
||||
if rec.recruitment_status != 'open':
|
||||
rec.hr_job_recruitment.website_published = False
|
||||
|
||||
@api.onchange('assign_to')
|
||||
def onchange_assign_to(self):
|
||||
for rec in self:
|
||||
if rec.hr_job_recruitment and rec.state == 'jd_created':
|
||||
rec.hr_job_recruitment.user_id = rec.assign_to.id
|
||||
|
||||
def action_final_approve(self):
|
||||
res = super().action_final_approve()
|
||||
for rec in self:
|
||||
if not rec.hr_job_recruitment:
|
||||
rec.hr_job_recruitment = self.env['hr.job.recruitment'].create({
|
||||
'job_id': rec.job_id.id if rec.job_id else False,
|
||||
'job_category': rec.job_category.id if rec.job_category else False,
|
||||
'no_of_recruitment': rec.number_of_positions,
|
||||
'description': rec.job_description,
|
||||
'skill_ids': [(6, 0, rec.primary_skill_ids.ids)],
|
||||
'secondary_skill_ids': [(6, 0, rec.secondary_skill_ids.ids)],
|
||||
'requested_by': rec.requested_by.partner_id.id if rec.recruitment_type == 'internal' else rec.client_id.id,
|
||||
'recruitment_type': rec.recruitment_type,
|
||||
'contract_type_id': rec.contract_type_id.id if rec.contract_type_id else False,
|
||||
'address_id': rec.client_id.id if rec.client_id.company_type == 'company' else (rec.client_id.parent_id.id if rec.client_id.parent_id else False),
|
||||
'user_id': rec.assign_to.id,
|
||||
'address_id': rec.requested_by.company_id.partner_id.id if rec.requested_by.company_id else '',
|
||||
'job_priority': rec.job_priority,
|
||||
'experience': rec.experience.id if rec.experience else False,
|
||||
'budget':rec.budget,
|
||||
'target_from': rec.target_startdate if rec.target_startdate else fields.Date.today(),
|
||||
'target_to': rec.target_deadline if rec.target_deadline else False,
|
||||
})
|
||||
|
||||
return res
|
||||
|
||||
self.state ='done'
|
||||
|
|
|
|||
|
|
@ -31,5 +31,4 @@ access_application_stage_status,application.stage.status,model_application_stage
|
|||
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_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<field name="recruitment_sequence"/>
|
||||
<field name="job_id"/>
|
||||
<field name="recruitment_type" optional="hide"/>
|
||||
<field name="department_id"/>
|
||||
<field name="requested_by"/>
|
||||
<field name="no_of_recruitment"/>
|
||||
<field name="no_of_eligible_submissions" optional="hide"/>
|
||||
<field name="application_count" string="Applications"
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
name="%(hr_recruitment.action_hr_job_sources)d" icon="fa-bar-chart-o"
|
||||
context="{'default_job_recruitment_id': id}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Trackers</span>
|
||||
<span class="o_stat_text">Job Publish Channels</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
<button class="oe_stat_button" type="action" name="233" icon="fa-bar-chart-o"
|
||||
context="{'default_job_recruitment_id': id}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Trackers</span>
|
||||
<span class="o_stat_text">Job Publish Channels</span>
|
||||
</div>
|
||||
</button>
|
||||
<field name="is_published" widget="website_redirect_button" on_change="1"/>
|
||||
|
|
@ -236,6 +236,7 @@
|
|||
<field name="requested_by"/>
|
||||
<separator/>
|
||||
<filter string="Published Records" name="published_records" domain="[('website_published','=',True)]"/>
|
||||
<filter string="UnPublished Records" name="unpublished_records" domain="[('website_published','=',False)]"/>
|
||||
<separator/>
|
||||
<filter string="My Assignments" name="my_assignments" domain="['|',('user_id', '=', uid), ('interviewer_ids','in',uid)]"/>
|
||||
<separator/>
|
||||
|
|
@ -261,9 +262,9 @@
|
|||
domain="[('recruitment_status','=', 'closed')]"/>
|
||||
<filter name="hold_status" string="Hold"
|
||||
domain="[('recruitment_status','=', 'hold')]"/>
|
||||
<filter name="modified_status" string="Open"
|
||||
<filter name="modified_status" string="Modified"
|
||||
domain="[('recruitment_status','=', 'modified')]"/>
|
||||
<filter name="cancelled_status" string="Open"
|
||||
<filter name="cancelled_status" string="Cancelled"
|
||||
domain="[('recruitment_status','=', 'cancelled')]"/>
|
||||
|
||||
<separator/>
|
||||
|
|
@ -429,12 +430,28 @@
|
|||
|
||||
|
||||
|
||||
<record id="action_hr_job_recruitment" model="ir.actions.act_window">
|
||||
<field name="name">Job Positions Recruitment</field>
|
||||
<record id="action_hr_job_recruitment_awaiting_published" model="ir.actions.act_window">
|
||||
<field name="name">UnPublished Recruitments</field>
|
||||
<field name="res_model">hr.job.recruitment</field>
|
||||
<field name="view_mode">kanban,list,form,search</field>
|
||||
<field name="search_view_id" ref="view_job_recruitment_filter"/>
|
||||
<field name="context">{"search_default_Current":1,"search_default_my_assignments":1,"search_default_published_records":1,'no_of_eligible_submissions': 0}</field>
|
||||
<field name="context">{"search_default_open_status":1,"search_default_my_assignments":1,"search_default_unpublished_records":1,'no_of_eligible_submissions': 0}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Ready to recruit more efficiently?
|
||||
</p>
|
||||
<p>
|
||||
Let's create a job position Recruitment Requests.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_hr_job_recruitment_published" model="ir.actions.act_window">
|
||||
<field name="name">Published Recruitments</field>
|
||||
<field name="res_model">hr.job.recruitment</field>
|
||||
<field name="view_mode">kanban,list,form,search</field>
|
||||
<field name="search_view_id" ref="view_job_recruitment_filter"/>
|
||||
<field name="context">{"search_default_open_status":1,"search_default_my_assignments":1,"search_default_published_records":1,'no_of_eligible_submissions': 0}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Ready to recruit more efficiently?
|
||||
|
|
@ -454,13 +471,26 @@
|
|||
active="0"
|
||||
sequence="2"/>
|
||||
|
||||
<menuitem name="JD"
|
||||
id="menu_hr_job_descriptions"
|
||||
parent="hr_recruitment.menu_hr_recruitment_root"
|
||||
sequence="1"
|
||||
groups="base.group_user"/>
|
||||
|
||||
<menuitem
|
||||
name="Job Description"
|
||||
id="menu_hr_job_recruitment_interviewer"
|
||||
parent="hr_recruitment.menu_hr_recruitment_root"
|
||||
action="action_hr_job_recruitment"
|
||||
name="Awaiting Publication"
|
||||
id="menu_hr_job_recruitment_awaiting_publication"
|
||||
parent="menu_hr_job_descriptions"
|
||||
action="action_hr_job_recruitment_awaiting_published"
|
||||
sequence="1"
|
||||
groups="base.group_user"/>
|
||||
<menuitem
|
||||
name="Published"
|
||||
id="menu_hr_job_recruitment_published"
|
||||
parent="menu_hr_job_descriptions"
|
||||
action="action_hr_job_recruitment_published"
|
||||
sequence="2"
|
||||
groups="base.group_user"/>
|
||||
|
||||
<menuitem
|
||||
name="Job Positions"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,15 @@
|
|||
<attribute name="required">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='job_id']" position="after">
|
||||
<field name="hr_job_recruitment" readonly="1" force_save="1"/>
|
||||
<field name="job_category" required="1" readonly="state not in ['draft']"/>
|
||||
<field name="hr_job_recruitment" string="Job Recruitment ID" readonly="not is_hr or state == 'jd_created'" force_save="1" invisible="state not in ['final','jd_created']" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||
<field name="recruitment_status" widget="statusbar" readonly="1" force_save="1" invisible="not hr_job_recruitment" options="{'clickable': '1', 'fold_field': 'fold'}"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='requested_on']" position="after">
|
||||
<field name="job_priority" readonly="state in ['jd_created']"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='secondary_skill_ids']" position="after">
|
||||
<field name="experience" readonly="state in ['jd_created']"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@
|
|||
'security/ir.model.access.csv',
|
||||
'data/data.xml',
|
||||
'views/masters.xml',
|
||||
'views/groups.xml',
|
||||
# 'views/groups.xml',
|
||||
'views/login.xml',
|
||||
'views/menu_access_control_views.xml',
|
||||
],
|
||||
# 'assets': {
|
||||
# 'web.assets_backend': [
|
||||
# 'menu_control_center/static/src/js/menu_service.js',
|
||||
# 'menu_control_center/static/src/js/login.js',
|
||||
# ],
|
||||
# },
|
||||
'installable': True,
|
||||
|
|
|
|||
|
|
@ -1,28 +1,53 @@
|
|||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
from odoo.addons.web.controllers.home import Home
|
||||
import werkzeug
|
||||
|
||||
|
||||
class CustomMasterLogin(Home):
|
||||
|
||||
@http.route()
|
||||
def web_login(self, *args, **kw):
|
||||
# Call the original Odoo login
|
||||
request.env['ir.ui.menu'].sudo().clear_caches()
|
||||
master_selected = kw.get('master_select')
|
||||
|
||||
response = super(CustomMasterLogin, self).web_login(*args, **kw)
|
||||
|
||||
# We only modify the QWeb response (GET request)
|
||||
if response.is_qweb:
|
||||
# load your masters
|
||||
masters = request.env['master.control'].sudo().search([])
|
||||
response.qcontext['masters'] = masters
|
||||
response.qcontext['masters'] = request.env['master.control'].sudo().search([])
|
||||
request.env['ir.ui.menu'].sudo().clear_caches()
|
||||
request.env['ir.ui.menu'].sudo()._visible_menu_ids()
|
||||
|
||||
# After successful login
|
||||
if request.session.uid and master_selected:
|
||||
request.session['active_master'] = master_selected
|
||||
user = request.env.user
|
||||
master = request.env['master.control'].sudo().search(
|
||||
[('code', '=', master_selected)], limit=1
|
||||
)
|
||||
|
||||
if master.exists() and master.access_group_ids:
|
||||
if not (user.groups_id & master.access_group_ids):
|
||||
request.session.logout(keep_db=True)
|
||||
|
||||
# Create a response with JavaScript alert
|
||||
html = f"""
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
alert("{_("You don't have access to login to '%s'. Please contact the administrator.") % master.display_name}");
|
||||
window.location.href = '/web/login';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return http.Response(html, content_type='text/html')
|
||||
# request.session.uid = None
|
||||
# request.params['login_success'] = False
|
||||
# response.qcontext['error'] = _(
|
||||
# "You don't have access to login to '%s'. "
|
||||
# "Please contact the administrator."
|
||||
# ) % master.display_name
|
||||
# return response
|
||||
|
||||
request.session['active_master'] = master.code
|
||||
|
||||
return response
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from . import masters
|
||||
from . import ir_http
|
||||
from . import groups
|
||||
# from . import groups
|
||||
from . import models
|
||||
from . import menu
|
||||
|
|
@ -7,8 +7,7 @@ class MasterControl(models.Model):
|
|||
sequence = fields.Integer()
|
||||
name = fields.Char(string='Master Name', required=True)
|
||||
code = fields.Char(string='Code', required=True)
|
||||
default_show = fields.Boolean(default=True)
|
||||
access_group_ids = fields.One2many('group.access.line','master_control_id',string='Roles')
|
||||
access_group_ids = fields.Many2many('res.groups',string='Roles')
|
||||
|
||||
@api.depends('name', 'code')
|
||||
def _compute_display_name(self):
|
||||
|
|
@ -18,87 +17,87 @@ class MasterControl(models.Model):
|
|||
else:
|
||||
record.display_name = False
|
||||
|
||||
def action_generate_groups(self):
|
||||
"""Generate category → groups list"""
|
||||
for rec in self:
|
||||
# def action_generate_groups(self):
|
||||
# """Generate category → groups list"""
|
||||
# for rec in self:
|
||||
#
|
||||
# # Clear old groups
|
||||
# rec.access_group_ids.unlink()
|
||||
#
|
||||
# groups = self.env['res.groups'].sudo().search([],order='category_id')
|
||||
# show_group = True if rec.default_show else False
|
||||
# print(show_group)
|
||||
# for grp in groups:
|
||||
# self.env['group.access.line'].create({
|
||||
# 'master_control_id': rec.id,
|
||||
# 'group_id': grp.id,
|
||||
# 'show_group': show_group,
|
||||
# })
|
||||
#
|
||||
# return {
|
||||
# 'type': 'ir.actions.client',
|
||||
# 'tag': 'display_notification',
|
||||
# 'params': {
|
||||
# 'title': _('Success'),
|
||||
# 'message': _('Groups generated successfully!'),
|
||||
# 'type': 'success',
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# # -----------------------------------------
|
||||
# # UPDATE GROUPS (Detect new groups)
|
||||
# # -----------------------------------------
|
||||
# def action_update_groups(self):
|
||||
# import pdb
|
||||
# pdb.set_trace()
|
||||
# for rec in self:
|
||||
# created_count = 0
|
||||
#
|
||||
# existing_ids = set(rec.access_group_ids.mapped('group_id.id'))
|
||||
#
|
||||
# categories = self.env['ir.module.category'].search([])
|
||||
#
|
||||
# for category in categories:
|
||||
# groups = self.env['res.groups'].search([
|
||||
# ('category_id', '=', category.id)
|
||||
# ])
|
||||
#
|
||||
# for grp in groups:
|
||||
# # create only missing group
|
||||
# if grp.id not in existing_ids:
|
||||
# rec.access_group_ids.create({
|
||||
# 'master_control_id': rec.id,
|
||||
# 'category_id': category.id,
|
||||
# 'group_id': grp.id,
|
||||
# 'show_group': True if rec.default_show else False,
|
||||
# })
|
||||
# created_count += 1
|
||||
# existing_ids.add(grp.id)
|
||||
#
|
||||
# if created_count:
|
||||
# message = f"Added {created_count} new groups."
|
||||
# msg_type = "success"
|
||||
# else:
|
||||
# message = "No new groups found."
|
||||
# msg_type = "info"
|
||||
#
|
||||
# return {
|
||||
# 'type': 'ir.actions.client',
|
||||
# 'tag': 'display_notification',
|
||||
# 'params': {
|
||||
# 'title': _('Group Update'),
|
||||
# 'message': _(message),
|
||||
# 'type': msg_type,
|
||||
# }
|
||||
# }
|
||||
|
||||
# Clear old groups
|
||||
rec.access_group_ids.unlink()
|
||||
|
||||
groups = self.env['res.groups'].sudo().search([],order='category_id')
|
||||
show_group = True if rec.default_show else False
|
||||
print(show_group)
|
||||
for grp in groups:
|
||||
self.env['group.access.line'].create({
|
||||
'master_control_id': rec.id,
|
||||
'group_id': grp.id,
|
||||
'show_group': show_group,
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Success'),
|
||||
'message': _('Groups generated successfully!'),
|
||||
'type': 'success',
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------
|
||||
# UPDATE GROUPS (Detect new groups)
|
||||
# -----------------------------------------
|
||||
def action_update_groups(self):
|
||||
import pdb
|
||||
pdb.set_trace()
|
||||
for rec in self:
|
||||
created_count = 0
|
||||
|
||||
existing_ids = set(rec.access_group_ids.mapped('group_id.id'))
|
||||
|
||||
categories = self.env['ir.module.category'].search([])
|
||||
|
||||
for category in categories:
|
||||
groups = self.env['res.groups'].search([
|
||||
('category_id', '=', category.id)
|
||||
])
|
||||
|
||||
for grp in groups:
|
||||
# create only missing group
|
||||
if grp.id not in existing_ids:
|
||||
rec.access_group_ids.create({
|
||||
'master_control_id': rec.id,
|
||||
'category_id': category.id,
|
||||
'group_id': grp.id,
|
||||
'show_group': True if rec.default_show else False,
|
||||
})
|
||||
created_count += 1
|
||||
existing_ids.add(grp.id)
|
||||
|
||||
if created_count:
|
||||
message = f"Added {created_count} new groups."
|
||||
msg_type = "success"
|
||||
else:
|
||||
message = "No new groups found."
|
||||
msg_type = "info"
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Group Update'),
|
||||
'message': _(message),
|
||||
'type': msg_type,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GroupsAccessLine(models.Model):
|
||||
_name = 'group.access.line'
|
||||
_description = 'Group Access Line'
|
||||
_rec_name = 'group_id'
|
||||
|
||||
category_id = fields.Many2one('ir.module.category', related='group_id.category_id')
|
||||
group_id = fields.Many2one('res.groups', string="Role")
|
||||
show_group = fields.Boolean(string="Show", default=True)
|
||||
master_control_id = fields.Many2one('master.control')
|
||||
#
|
||||
# class GroupsAccessLine(models.Model):
|
||||
# _name = 'group.access.line'
|
||||
# _description = 'Group Access Line'
|
||||
# _rec_name = 'group_id'
|
||||
#
|
||||
# category_id = fields.Many2one('ir.module.category', related='group_id.category_id')
|
||||
# group_id = fields.Many2one('res.groups', string="Role")
|
||||
# show_group = fields.Boolean(string="Show", default=True)
|
||||
# master_control_id = fields.Many2one('master.control')
|
||||
|
|
@ -53,119 +53,143 @@ class MenuAccessControl(models.Model):
|
|||
all_subs |= self._get_all_submenus(sm)
|
||||
return all_subs
|
||||
|
||||
def _get_all_menus_sql(self):
|
||||
"""
|
||||
Fetch all active menus with hierarchy using SQL
|
||||
Returns list of dicts:
|
||||
[
|
||||
{id, parent_id}
|
||||
]
|
||||
"""
|
||||
self.env.cr.execute("""
|
||||
WITH RECURSIVE menu_tree AS (
|
||||
SELECT id, parent_id
|
||||
FROM ir_ui_menu
|
||||
WHERE parent_id IS NULL
|
||||
AND active = true
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT m.id, m.parent_id
|
||||
FROM ir_ui_menu m
|
||||
JOIN menu_tree mt ON mt.id = m.parent_id
|
||||
WHERE m.active = true
|
||||
)
|
||||
SELECT id, parent_id
|
||||
FROM menu_tree
|
||||
ORDER BY parent_id NULLS FIRST, id
|
||||
""")
|
||||
return self.env.cr.dictfetchall()
|
||||
|
||||
def action_generate_menus(self):
|
||||
"""
|
||||
Generate main menus and all submenus (recursive),
|
||||
Generate main menus and all submenus (SQL-based),
|
||||
and set access_line_id for every submenu.
|
||||
"""
|
||||
for rec in self:
|
||||
MenuLine = self.env['menu.access.line']
|
||||
|
||||
# clear old menus
|
||||
for rec in self:
|
||||
# Clear old menus
|
||||
rec.access_menu_line_ids.unlink()
|
||||
rec.access_sub_menu_line_ids.unlink()
|
||||
|
||||
active_menus = self.env['ir.ui.menu'].search([
|
||||
('parent_id', '=', False),
|
||||
('active', '=', True)
|
||||
])
|
||||
menus = rec._get_all_menus_sql()
|
||||
|
||||
for menu in active_menus:
|
||||
# Map: parent_id -> children
|
||||
children_map = {}
|
||||
for m in menus:
|
||||
children_map.setdefault(m['parent_id'], []).append(m)
|
||||
|
||||
# 1️⃣ Create main menu line
|
||||
main_line = self.env['menu.access.line'].create({
|
||||
# ---------- Main menus ----------
|
||||
for main in children_map.get(None, []):
|
||||
|
||||
main_line = MenuLine.create({
|
||||
'access_control_id': rec.id,
|
||||
'menu_id': menu.id,
|
||||
'menu_id': main['id'],
|
||||
'is_main_menu': True,
|
||||
})
|
||||
|
||||
# 2️⃣ Fetch all recursive submenus
|
||||
submenus = self._get_all_submenus(menu)
|
||||
|
||||
# 3️⃣ Create submenu lines with correct parent
|
||||
for sm in submenus:
|
||||
self.env['menu.access.line'].create({
|
||||
# ---------- Submenus ----------
|
||||
stack = children_map.get(main['id'], [])
|
||||
while stack:
|
||||
sm = stack.pop()
|
||||
MenuLine.create({
|
||||
'access_control_id': rec.id,
|
||||
'menu_id': sm.id,
|
||||
'menu_id': sm['id'],
|
||||
'is_main_menu': True,
|
||||
'access_line_id': main_line.id, # important
|
||||
'access_line_id': main_line.id,
|
||||
})
|
||||
stack.extend(children_map.get(sm['id'], []))
|
||||
|
||||
def action_update_menus(self):
|
||||
line = self.env['menu.access.line']
|
||||
menu = self.env['ir.ui.menu']
|
||||
MenuLine = self.env['menu.access.line']
|
||||
|
||||
for rec in self:
|
||||
created_count = 0
|
||||
|
||||
# All existing menu IDs across BOTH One2manys
|
||||
# Existing menu IDs
|
||||
existing_menu_ids = set(
|
||||
rec.access_menu_line_ids.mapped('menu_id.id') +
|
||||
rec.access_sub_menu_line_ids.mapped('menu_id.id')
|
||||
)
|
||||
|
||||
# ---------- Step 1: Ensure all top-level menus exist ----------
|
||||
top_menus = menu.search([
|
||||
('parent_id', '=', False),
|
||||
('active', '=', True),
|
||||
])
|
||||
menus = rec._get_all_menus_sql()
|
||||
|
||||
for menu in top_menus:
|
||||
# Map parent -> children
|
||||
children_map = {}
|
||||
for m in menus:
|
||||
children_map.setdefault(m['parent_id'], []).append(m)
|
||||
|
||||
# Create missing MAIN MENU
|
||||
main_line = line.search([
|
||||
# ---------- Main menus ----------
|
||||
for main in children_map.get(None, []):
|
||||
|
||||
main_line = MenuLine.search([
|
||||
('access_control_id', '=', rec.id),
|
||||
('menu_id', '=', menu.id),
|
||||
('access_line_id', '=', False)
|
||||
('menu_id', '=', main['id']),
|
||||
('access_line_id', '=', False),
|
||||
], limit=1)
|
||||
|
||||
if not main_line:
|
||||
main_line = line.create({
|
||||
main_line = MenuLine.create({
|
||||
'access_control_id': rec.id,
|
||||
'menu_id': menu.id,
|
||||
'menu_id': main['id'],
|
||||
'is_main_menu': True,
|
||||
})
|
||||
created_count += 1
|
||||
existing_menu_ids.add(menu.id)
|
||||
existing_menu_ids.add(main['id'])
|
||||
|
||||
# ---------- Step 2: Ensure all SUBMENUS exist ----------
|
||||
submenus = rec._get_all_submenus(menu)
|
||||
# ---------- Submenus ----------
|
||||
stack = children_map.get(main['id'], [])
|
||||
while stack:
|
||||
sm = stack.pop()
|
||||
|
||||
for sm in submenus:
|
||||
|
||||
# If submenu is missing → create it
|
||||
if sm.id not in existing_menu_ids:
|
||||
line.create({
|
||||
if sm['id'] not in existing_menu_ids:
|
||||
MenuLine.create({
|
||||
'access_control_id': rec.id,
|
||||
'menu_id': sm.id,
|
||||
'menu_id': sm['id'],
|
||||
'is_main_menu': True,
|
||||
'access_line_id': main_line.id,
|
||||
})
|
||||
created_count += 1
|
||||
existing_menu_ids.add(sm.id)
|
||||
existing_menu_ids.add(sm['id'])
|
||||
|
||||
stack.extend(children_map.get(sm['id'], []))
|
||||
|
||||
# ---------- Notification ----------
|
||||
if created_count:
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Success'),
|
||||
'message': _('Added %s new menu(s) (including submenus)') % created_count,
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Info'),
|
||||
'message': _('No new menus found to add.'),
|
||||
'type': 'info',
|
||||
'sticky': False,
|
||||
}
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Success') if created_count else _('Info'),
|
||||
'message': (
|
||||
_('Added %s new menu(s) (including submenus)') % created_count
|
||||
if created_count else
|
||||
_('No new menus found to add.')
|
||||
),
|
||||
'type': 'success' if created_count else 'info',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MenuAccessLine(models.Model):
|
||||
|
|
|
|||
|
|
@ -4,4 +4,3 @@ access_menu_access_line,access.menu.access.line,model_menu_access_line,hr.group_
|
|||
access_menu_control_units,access.menu.control.units,model_menu_control_units,hr.group_hr_manager,1,1,1,1
|
||||
access_master_control_public,master.control.public,model_master_control,base.group_public,1,0,0,0
|
||||
access_master_control_hr,master.control.hr,model_master_control,hr.group_hr_manager,1,1,1,1
|
||||
group_access_line_access,group_access_line_access,model_group_access_line,,1,1,1,1
|
||||
|
|
|
|||
|
|
|
@ -0,0 +1,27 @@
|
|||
odoo.define('menu_control_center.login', function (require) {
|
||||
"use strict";
|
||||
|
||||
var WebClient = require('web.WebClient');
|
||||
|
||||
WebClient.include({
|
||||
show_application: function() {
|
||||
var self = this;
|
||||
var res = this._super.apply(this, arguments);
|
||||
|
||||
// Check for flash messages in session
|
||||
var flash_msg = sessionStorage.getItem('flash_message');
|
||||
var flash_type = sessionStorage.getItem('flash_type');
|
||||
|
||||
if (flash_msg) {
|
||||
// Show notification
|
||||
this.do_notify(flash_msg, '', true);
|
||||
|
||||
// Clear stored messages
|
||||
sessionStorage.removeItem('flash_message');
|
||||
sessionStorage.removeItem('flash_type');
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -18,34 +18,28 @@
|
|||
<field name="model">master.control</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_generate_groups"
|
||||
type="object"
|
||||
string="Generate Groups"
|
||||
class="btn-primary"/>
|
||||
<!-- <header>-->
|
||||
<!-- <button name="action_generate_groups"-->
|
||||
<!-- type="object"-->
|
||||
<!-- string="Generate Groups"-->
|
||||
<!-- class="btn-primary"/>-->
|
||||
|
||||
<button name="action_update_groups"
|
||||
type="object"
|
||||
string="Update Groups"
|
||||
class="btn-secondary"/>
|
||||
</header>
|
||||
<!-- <button name="action_update_groups"-->
|
||||
<!-- type="object"-->
|
||||
<!-- string="Update Groups"-->
|
||||
<!-- class="btn-secondary"/>-->
|
||||
<!-- </header>-->
|
||||
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="default_show" help="Upon Generating this value be placed in Show Option"/>
|
||||
<!-- <field name="default_show" help="Upon Generating this value be placed in Show Option"/>-->
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Roles">
|
||||
<field name="access_group_ids" widget="one2many_search">
|
||||
<list editable="bottom">
|
||||
<field name="category_id"/>
|
||||
<field name="group_id"/>
|
||||
<field name="show_group"/>
|
||||
</list>
|
||||
</field>
|
||||
<field name="access_group_ids" widget="one2many_search"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@
|
|||
</group>
|
||||
<notebook>
|
||||
<page name="main_menus" string="Menus">
|
||||
<field name="access_menu_line_ids">
|
||||
<field name="access_menu_line_ids" widget="one2many_search">
|
||||
<list editable="bottom">
|
||||
<field name="menu_id"/>
|
||||
<field name="is_main_menu"/>
|
||||
|
|
@ -106,8 +106,8 @@
|
|||
</field>
|
||||
</page>
|
||||
<page name="sub_menus" string="Sub Menus">
|
||||
<field name="access_sub_menu_line_ids">
|
||||
<list create="0" default_group_by="parent_menu">
|
||||
<field name="access_sub_menu_line_ids" widget="one2many_search">
|
||||
<list create="0" default_group_by="parent_menu" editable="bottom">
|
||||
<field name="menu_id" readonly="1" force_save="1"/>
|
||||
<field name="parent_menu"/>
|
||||
<field name="is_main_menu" string="Show Menu"/>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
'website': "https://www.ftprotech.com",
|
||||
'category': 'Recruitment',
|
||||
'version': '0.1',
|
||||
'depends': ['hr_recruitment', 'mail','base','hr'],
|
||||
'depends': ['hr_recruitment', 'mail','base','hr','account'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/ir_sequence.xml',
|
||||
|
|
|
|||
|
|
@ -12,26 +12,46 @@
|
|||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
Dear <t t-out="object.hr_manager_id.name or ''">John Doe</t>,
|
||||
<br /><br />
|
||||
<br/>
|
||||
<br/>
|
||||
A new requisition has been submitted for your review:
|
||||
<ul>
|
||||
<li><strong>Requisition Name:</strong> <t t-out="object.name or ''">New</t></li>
|
||||
<li><strong>Department:</strong> <t t-out="object.department_id.name or ''">Sales</t></li>
|
||||
<li><strong>Position Title:</strong> <t t-out="object.position_title or ''">Sales Manager</t></li>
|
||||
<li><strong>Number of Positions:</strong> <t t-out="object.number_of_positions or 0">3</t></li>
|
||||
<li><strong>Requested By:</strong> <t t-out="object.requested_by.name or ''">Emily Clark</t></li>
|
||||
<li><strong>Requested On:</strong> <t t-out="object.requested_on or ''">2024-12-31</t></li>
|
||||
<li>
|
||||
<strong>Requisition Name:</strong>
|
||||
<t t-out="object.name or ''">New</t>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Position Title:</strong>
|
||||
<t t-out="object.position_title or ''">Sales Manager</t>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Number of Positions:</strong>
|
||||
<t t-out="object.number_of_positions or 0">3</t>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Requested By:</strong>
|
||||
<t t-out="object.requested_by.name or ''">Emily Clark</t>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Requested On:</strong>
|
||||
<t t-out="object.requested_on or ''">2024-12-31</t>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<br/>
|
||||
<t t-if="object.notes">
|
||||
<strong>Notes:</strong> <t t-out="object.notes or ''">Requirement</t>
|
||||
<br />
|
||||
<strong>Notes:</strong>
|
||||
<t t-out="object.notes or ''">Requirement</t>
|
||||
<br/>
|
||||
</t>
|
||||
<br />
|
||||
Please <a t-att-href="'%s/web#id=%d&model=recruitment.requisition&view_type=form' % (object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id)" target="_blank">click here</a> to view the requisition details.
|
||||
<br /><br />
|
||||
<br/>
|
||||
Please <a
|
||||
t-att-href="'%s/web#id=%d&model=recruitment.requisition&view_type=form' % (object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id)"
|
||||
target="_blank">click here
|
||||
</a> to view the requisition details.
|
||||
<br/>
|
||||
<br/>
|
||||
Regards,
|
||||
<br />
|
||||
<br/>
|
||||
<t t-out="object.requested_by.name or ''">Emily Clark</t>
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -40,8 +60,6 @@
|
|||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="mail_template_recruitment_requisition_cancellation" model="mail.template">
|
||||
<field name="name">Recruitment Requisition Cancellation</field>
|
||||
<field name="model_id" ref="requisitions.model_recruitment_requisition"/>
|
||||
|
|
@ -50,23 +68,216 @@
|
|||
<field name="subject">Requisition Cancelled: {{ object.name }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>Dear <t t-out="object.hr_manager_id.name or ''">HR Manager</t>,</p>
|
||||
<p>The requisition <strong><t t-out="object.name or ''">New</t></strong> has been cancelled for the following reason:</p>
|
||||
<p><t t-out="object.notes or 'No reason provided.'"/></p>
|
||||
<p>Dear <t t-out="object.hr_manager_id.name or ''">HR Manager</t>,
|
||||
</p>
|
||||
<p>The requisition
|
||||
<strong>
|
||||
<t t-out="object.name or ''">New</t>
|
||||
</strong>
|
||||
has been cancelled for the following reason:
|
||||
</p>
|
||||
<p>
|
||||
<t t-out="object.notes or 'No reason provided.'"/>
|
||||
</p>
|
||||
<p>
|
||||
You can view the requisition details by clicking the link below:
|
||||
<br/>
|
||||
<a t-att-href="'/web#id=%d&model=recruitment.requisition&view_type=form' % object.id" style="color: #1a73e8; text-decoration: none;">
|
||||
<a t-att-href="'/web#id=%d&model=recruitment.requisition&view_type=form' % object.id"
|
||||
style="color: #1a73e8; text-decoration: none;">
|
||||
View Requisition
|
||||
</a>
|
||||
</p>
|
||||
<p>Regards,</p>
|
||||
<p><t t-out="object.requested_by.name or ''">Requested By</t></p>
|
||||
<p>
|
||||
<t t-out="object.requested_by.name or ''">Requested By</t>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
|
||||
<record id="mail_template_recruitment_requisition_finance" model="mail.template">
|
||||
<field name="name">Recruitment Requisition: Sent to Finance</field>
|
||||
<field name="model_id" ref="requisitions.model_recruitment_requisition"/>
|
||||
<field name="email_from">{{ object.hr_manager_id.email_formatted or user.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.finance_manager_id.email }}</field>
|
||||
<field name="subject">Requisition Awaiting Finance Approval: {{ object.name }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>Dear
|
||||
<t t-out="object.finance_manager_id.name"/>
|
||||
,
|
||||
</p>
|
||||
|
||||
<p>The following recruitment requisition has been reviewed by HR and is now awaiting your
|
||||
approval:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Requisition:</strong>
|
||||
<t t-out="object.name"/>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Position:</strong>
|
||||
<t t-out="object.job_id.name"/>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Positions Required:</strong>
|
||||
<t t-out="object.number_of_positions"/>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Requested By:</strong>
|
||||
<t t-out="object.requested_by.name"/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<a t-att-href="'%s/web#id=%d&model=recruitment.requisition&view_type=form' % (
|
||||
object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id)"
|
||||
target="_blank">
|
||||
View Requisition
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>Regards,<br/>HR Team
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_template_recruitment_requisition_final" model="mail.template">
|
||||
<field name="name">Recruitment Requisition: Finance Approved</field>
|
||||
<field name="model_id" ref="requisitions.model_recruitment_requisition"/>
|
||||
<field name="email_from">{{ object.finance_manager_id.email_formatted or user.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.hr_manager_id.email }}</field>
|
||||
<field name="subject">Finance Approved: {{ object.name }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>Dear
|
||||
<t t-out="object.hr_manager_id.name"/>
|
||||
,
|
||||
</p>
|
||||
|
||||
<p>The finance team has approved the following recruitment requisition:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Requisition:</strong>
|
||||
<t t-out="object.name"/>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Position:</strong>
|
||||
<t t-out="object.job_id.name"/>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Finance Comments:</strong>
|
||||
<t t-out="object.finance_notes or '—'"/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Please proceed with final approval.</p>
|
||||
|
||||
<p>
|
||||
<a t-att-href="'%s/web#id=%d&model=recruitment.requisition&view_type=form' % (
|
||||
object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id)"
|
||||
target="_blank">
|
||||
Review Requisition
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>Regards,<br/>Finance Team
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="mail_template_recruitment_requisition_finance_reject" model="mail.template">
|
||||
<field name="name">Recruitment Requisition: Finance Rejected</field>
|
||||
<field name="model_id" ref="requisitions.model_recruitment_requisition"/>
|
||||
<field name="email_from">{{ object.finance_manager_id.email_formatted or user.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.hr_manager_id.email }}</field>
|
||||
<field name="subject">Finance Rejected: {{ object.name }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>Dear
|
||||
<t t-out="object.hr_manager_id.name"/>
|
||||
,
|
||||
</p>
|
||||
|
||||
<p>The finance team has Rejected the following recruitment requisition:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Requisition:</strong>
|
||||
<t t-out="object.name"/>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Position:</strong>
|
||||
<t t-out="object.job_id.name"/>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Finance Comments:</strong>
|
||||
<t t-out="object.finance_notes or '—'"/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<a t-att-href="'%s/web#id=%d&model=recruitment.requisition&view_type=form' % (
|
||||
object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id)"
|
||||
target="_blank">
|
||||
Review Requisition
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>Regards,<br/>Finance Team
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_template_recruitment_requisition_jd" model="mail.template">
|
||||
<field name="name">Recruitment Requisition: Approved & JD Creation</field>
|
||||
<field name="model_id" ref="requisitions.model_recruitment_requisition"/>
|
||||
<field name="email_from">{{ user.email_formatted }}</field>
|
||||
<field name="email_to">{{ object.assign_to.email }}</field>
|
||||
<field name="subject">JD Creation Required: {{ object.name }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>Dear
|
||||
<t t-out="object.assign_to.name"/>
|
||||
,
|
||||
</p>
|
||||
|
||||
<p>The following recruitment requisition has received final approval.</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Requisition:</strong>
|
||||
<t t-out="object.name"/>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Position:</strong>
|
||||
<t t-out="object.job_id.name"/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Please proceed with Job Description creation.</p>
|
||||
|
||||
<p>
|
||||
<a t-att-href="'%s/web#id=%d&model=recruitment.requisition&view_type=form' % (
|
||||
object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id)"
|
||||
target="_blank">
|
||||
Open Requisition
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>Regards,<br/>HR Team
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
|
||||
class RecruitmentRequisition(models.Model):
|
||||
|
|
@ -8,75 +7,133 @@ class RecruitmentRequisition(models.Model):
|
|||
_description = 'Recruitment Requisition'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
name = fields.Char(string="Requisition Name", required=True, default="New")
|
||||
department_id = fields.Many2one('hr.department', string="Department", required=True)
|
||||
requested_by = fields.Many2one('res.users', string="Requested By", default=lambda self: self.env.user)
|
||||
requested_on = fields.Date(string='Requested On')
|
||||
jd_file = fields.Binary(string="JD")
|
||||
jd_file_name = fields.Char(string="JD")
|
||||
name = fields.Char(string="Requisition Name", required=True, default="New", tracking=True)
|
||||
requested_by = fields.Many2one('res.users', string="Requested By", default=lambda self: self.env.user, tracking=True)
|
||||
client_id = fields.Many2one('res.partner', string="Client", default=lambda self: self.env.user.company_id.partner_id, tracking=True)
|
||||
recruitment_type = fields.Selection([('internal','In-House'),('external','Client-Side')], default='internal')
|
||||
requested_on = fields.Date(string='Requested On', tracking=True)
|
||||
hr_manager_id = fields.Many2one('res.users', string="HR Manager", compute='_compute_hr_manager')
|
||||
hr_job = fields.Many2one('')
|
||||
position_title = fields.Char(string="Position Title", required=True)
|
||||
number_of_positions = fields.Integer(string="Number of Positions", required=True)
|
||||
finance_manager_id = fields.Many2one('res.users',string="Finance Manager", compute="_compute_finance_manager")
|
||||
is_hr = fields.Boolean(compute="_compute_check_access")
|
||||
is_finance_manager = fields.Boolean(compute="_compute_check_access")
|
||||
|
||||
@api.onchange('client_id')
|
||||
def onchange_client_id(self):
|
||||
for rec in self:
|
||||
if rec.client_id != rec.requested_by.company_id.partner_id and rec.client_id != rec.requested_by.partner_id:
|
||||
rec.recruitment_type = 'external'
|
||||
else:
|
||||
rec.recruitment_type = 'internal'
|
||||
|
||||
@api.depends('hr_manager_id','finance_manager_id')
|
||||
def _compute_check_access(self):
|
||||
for rec in self:
|
||||
rec.is_hr = False
|
||||
rec.is_finance_manager = False
|
||||
if (rec.hr_manager_id and rec.hr_manager_id == self.env.user) or self.env.user.id == 2:
|
||||
rec.is_hr = True
|
||||
if (rec.finance_manager_id and rec.finance_manager_id == self.env.user) or self.env.user.id == 2:
|
||||
rec.is_finance_manager = True
|
||||
|
||||
|
||||
hr_job = fields.Many2one('hr.job', tracking=True)
|
||||
number_of_positions = fields.Integer(string="Number of Positions", required=True, tracking=True)
|
||||
job_description = fields.Html(string="Job Summary")
|
||||
job_id = fields.Many2one('hr.job',"JD ID")
|
||||
job_id = fields.Many2one('hr.job',"Job Position", tracking=True)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('waiting_approval', 'Waiting Approval'),
|
||||
('approved', 'Approved'),
|
||||
('done', 'Done'),
|
||||
('hr', 'HR Manager'),
|
||||
('finance', 'Finance'),
|
||||
('final', 'Final Approval'),
|
||||
('jd_created', 'JD Created'),
|
||||
('rejected', 'Rejected'),
|
||||
('cancel', 'Cancelled')
|
||||
], default='draft', track_visibility='onchange')
|
||||
notes = fields.Text(string="Notes")
|
||||
], default='draft', tracking=True)
|
||||
notes = fields.Text(string="Notes", tracking=True)
|
||||
primary_skill_ids = fields.Many2many('hr.skill', "recruitment_requisition_primary_hr_skill_rel",
|
||||
'hr_job_id', 'hr_skill_id', "Primary Skills", required=True)
|
||||
'hr_job_id', 'hr_skill_id', "Primary Skills", required=True, tracking=True)
|
||||
secondary_skill_ids = fields.Many2many('hr.skill', "recruitment_requisition_secondary_hr_skill_rel",
|
||||
'hr_job_id', 'hr_skill_id', "Secondary Skills")
|
||||
'hr_job_id', 'hr_skill_id', "Secondary Skills", tracking=True)
|
||||
assign_to = fields.Many2one('res.users', domain=lambda self: [
|
||||
('groups_id', 'in', self.env.ref('hr_recruitment.group_hr_recruitment_user').id)])
|
||||
('groups_id', 'in', self.env.ref('hr_recruitment.group_hr_recruitment_user').id)], tracking=True)
|
||||
finance_notes = fields.Text(string="Finance Comments", tracking=True)
|
||||
budget = fields.Char(string="Budget",tracking=True)
|
||||
requested_deadline = fields.Date(string="Required Before",tracking=True)
|
||||
target_deadline = fields.Date(string="Target Deadline",tracking=True)
|
||||
target_startdate = fields.Date(string="Start Date",tracking=True)
|
||||
contract_type_id = fields.Many2one('hr.contract.type',string="Employment Type")
|
||||
|
||||
def action_finance_reject(self):
|
||||
for rec in self:
|
||||
if not rec.finance_notes:
|
||||
raise UserError(_("Please enter rejection comments before rejecting."))
|
||||
|
||||
rec.state = 'hr'
|
||||
template = self.env.ref(
|
||||
'requisitions.mail_template_recruitment_requisition_finance_reject',
|
||||
raise_if_not_found=False
|
||||
)
|
||||
if template:
|
||||
template.send_mail(rec.id, force_send=True)
|
||||
|
||||
@api.depends('requested_by')
|
||||
def _compute_hr_manager(self):
|
||||
hr_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_hr_id')
|
||||
self.hr_manager_id = self.env['res.users'].sudo().browse(int(hr_id)) if hr_id else False
|
||||
|
||||
def action_submit(self):
|
||||
@api.depends('requested_by')
|
||||
def _compute_finance_manager(self):
|
||||
finance_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_finance_manager_id')
|
||||
self.finance_manager_id = self.env['res.users'].sudo().browse(int(finance_id)) if finance_id else False
|
||||
|
||||
def action_submit_hr(self):
|
||||
self.name = self.env['ir.sequence'].next_by_code('hr.requisitions')
|
||||
self.requested_on = fields.Date.today()
|
||||
self.state = 'waiting_approval'
|
||||
self.state = 'hr'
|
||||
if self.requested_deadline:
|
||||
self.target_deadline = self.requested_deadline
|
||||
|
||||
template = self.env.ref('requisitions.mail_template_recruitment_requisition_notification') # Replace `module_name` with your module name
|
||||
if template:
|
||||
template.send_mail(self.id, force_send=True)
|
||||
|
||||
def action_approve(self):
|
||||
def action_send_finance(self):
|
||||
self.state = 'finance'
|
||||
template = self.env.ref('requisitions.mail_template_recruitment_requisition_finance', raise_if_not_found=False)
|
||||
if template:
|
||||
template.send_mail(self.id, force_send=True)
|
||||
|
||||
def action_send_hr(self):
|
||||
self.state = 'final'
|
||||
template = self.env.ref('requisitions.mail_template_recruitment_requisition_final', raise_if_not_found=False)
|
||||
if template:
|
||||
template.send_mail(self.id, force_send=True)
|
||||
|
||||
def action_final_approve(self):
|
||||
for rec in self:
|
||||
if not rec.assign_to:
|
||||
raise ValidationError(_("Please Assign a recruitment user"))
|
||||
rec.state = 'approved'
|
||||
rec.button_create_jd()
|
||||
raise ValidationError(_("Please assign a recruitment user."))
|
||||
|
||||
rec.state = 'jd_created'
|
||||
template = self.env.ref(
|
||||
'requisitions.mail_template_recruitment_requisition_jd',
|
||||
raise_if_not_found=False
|
||||
)
|
||||
if template:
|
||||
template.send_mail(rec.id, force_send=True)
|
||||
|
||||
def action_cancel(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'recruitment.requisition.cancel.wizard',
|
||||
'name': _('Cancellation Reason'),
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {'active_id': self.id},
|
||||
}
|
||||
|
||||
def button_create_jd(self):
|
||||
self.job_id = self.env['hr.job'].create({
|
||||
'name': self.position_title,
|
||||
'department_id': self.department_id.id,
|
||||
'no_of_recruitment':self.number_of_positions,
|
||||
'description':self.job_description,
|
||||
'skill_ids': [(6, 0, self.primary_skill_ids.ids)],
|
||||
'secondary_skill_ids': [(6, 0, self.secondary_skill_ids.ids)],
|
||||
'requested_by': self.requested_by.partner_id.id,
|
||||
'user_id': self.assign_to.id
|
||||
})
|
||||
|
||||
self.state ='done'
|
||||
|
||||
|
||||
|
||||
class HRJob(models.Model):
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ class ResConfigSettings(models.TransientModel):
|
|||
|
||||
requisition_hr_id = fields.Many2one('res.users',config_parameter='requisitions.requisition_hr_id', string='Requisition HR',
|
||||
domain=lambda self: [
|
||||
('groups_id', 'in', self.env.ref('hr_recruitment.group_hr_recruitment_manager').id)])
|
||||
('groups_id', 'in', self.env.ref('hr.group_hr_manager').id)])
|
||||
|
||||
# requisition_md_id = fields.Many2one('res.users', string='Requisition MD')
|
||||
requisition_finance_manager = fields.Many2one('res.users',config_parameter='requisitions.requisition_finance_manager_id', string='Requisition Finance Manager',
|
||||
domain=lambda self: [
|
||||
('groups_id', 'in',
|
||||
self.env.ref('account.group_account_manager').id)])
|
||||
|
|
@ -1,28 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="view_hr_job_form" model="ir.ui.view">
|
||||
<field name="name">view.hr.job.form</field>
|
||||
<field name="model">hr.job</field>
|
||||
<field name="inherit_id" ref="hr.view_hr_job_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='department_id']" position="before">
|
||||
<field name="requested_by"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_requisition_form" model="ir.ui.view">
|
||||
<field name="name">requisition.form</field>
|
||||
<field name="model">recruitment.requisition</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Recruitment Requisition">
|
||||
<header>
|
||||
<button string="Submit" type="object" name="action_submit" invisible=" state != 'draft'"/>
|
||||
<button string="Approve & Create JD" type="object" name="action_approve" invisible=" state != 'waiting_approval'" groups="hr_recruitment.group_hr_recruitment_manager"/>
|
||||
<button name="button_create_jd" type="object" string="Create JD" invisible=" state != 'approved'"/>
|
||||
<button string="Cancel" type="object" name="action_cancel" invisible=" state in ('draft','done','cancel')"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,waiting_approval,done" readonly="1"/>
|
||||
<button string="Submit" type="object" name="action_submit_hr" invisible=" state != 'draft'"/>
|
||||
<button string="Send to FM" type="object" name="action_send_finance" invisible=" state != 'hr' or not is_hr"/>
|
||||
<button string="Approve & Proceed" type="object" name="action_send_hr" invisible=" state != 'finance' or not is_finance_manager"/>
|
||||
<button string="Reject" type="object" name="action_finance_reject" invisible="state != 'finance' or not is_finance_manager" confirm="Are you sure you want to reject? Please ensure Finance comments are added."/>
|
||||
<button string="Approve" type="object" name="action_final_approve" invisible=" state != 'final'"/>
|
||||
<button string="Cancel" type="object" name="action_cancel" invisible="state in ('draft','jd_created','cancel') or not is_hr"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,hr,finance,final,jd_created" readonly="1"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="row justify-content-between position-relative w-100 m-0 mb-2">
|
||||
|
|
@ -34,25 +24,45 @@
|
|||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="department_id" readonly="state != 'draft'"/>
|
||||
<field name="requested_by" readonly="1" widget="many2one_avatar_user"/>
|
||||
<field name="requested_on" invisible="requested_on == False"/>
|
||||
<field name="requested_by" readonly="not is_hr" widget="many2one_avatar_user"/>
|
||||
<field name="client_id" readonly="not is_hr" invisible="not is_hr" force_save="1"/>
|
||||
<field name="recruitment_type" readonly="not is_hr" force_save="1"/>
|
||||
<field name="requested_on" invisible="requested_on == False" readonly="not is_hr" force_save="1"/>
|
||||
<field name="requested_deadline" readonly="state != 'draft'" invisible="state != 'draft' and not requested_deadline"/>
|
||||
<field name="hr_manager_id" widget="many2one_avatar_user"/>
|
||||
<field name="finance_manager_id" widget="many2one_avatar_user"/>
|
||||
<field name="jd_file" filename="jd_file_name"
|
||||
widget="binary" force_save="1"/>
|
||||
<field name="jd_file_name" invisible="1" force_save="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="position_title" readonly="state != 'draft'"/>
|
||||
<field name="number_of_positions" readonly="state != 'draft'"/>
|
||||
<field name="job_id" invisible="job_id == False" readonly="1" force_save="1" options="{'no_quick_create':True,'no_open':True}"/>
|
||||
<field name="number_of_positions" readonly="(state != 'draft' and not is_hr) or state == 'jd_created'"/>
|
||||
<field name="primary_skill_ids" widget="many2many_tags" options="{'color_field': 'color'}" readonly="state != 'draft'"/>
|
||||
<field name="secondary_skill_ids" widget="many2many_tags" options="{'color_field': 'color'}" readonly="state != 'draft'"/>
|
||||
|
||||
<field name="assign_to" invisible="state not in ['approved','waiting_approval','done']" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||
<field name="job_id" invisible="job_id == False" readonly="1" force_save="1"/>
|
||||
<field name="contract_type_id" readonly="state in ['jd_created']"/>
|
||||
<field name="is_hr" invisible="1" force_save="1"/>
|
||||
<field name="is_finance_manager" invisible="1" force_save="1"/>
|
||||
<field name="assign_to" invisible="state not in ['final','jd_created']" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||
<field name="target_startdate" invisible="1" readonly="state in ['jd_created']"/>
|
||||
<field name="target_deadline" widget="daterange" string="Target Deadline" options="{'start_date_field': 'target_startdate'}" on_change="1" readonly="state in ['jd_created']"/>
|
||||
</group>
|
||||
<field name="notes" placeholder="Remarks" readonly="state == 'done'"/>
|
||||
</group>
|
||||
<group string="Job Summary">
|
||||
<field name="job_description" readonly="state == 'done'" nolabel="1" placeholder="Please enter the JD Hear"/>
|
||||
<field name="notes" placeholder="Remarks" readonly="state == 'jd_created'" invisible="not notes"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Job Summary" name="job_summary">
|
||||
<field name="job_description" readonly="state == 'jd_created'" nolabel="1" placeholder="Please enter the JD Hear"/>
|
||||
</page>
|
||||
<page name="finance_notes" string="Finance Comments" invisible="state == 'draft' or (not is_hr and not is_finance_manager)">
|
||||
<group>
|
||||
<field name="budget" placeholder="15.5 - 17.5LPA"/>
|
||||
</group>
|
||||
<group string="Comments">
|
||||
<field name="finance_notes" nolabel="1" string="Comments" placeholder="Enter the Finance Comments in Hear..."/>
|
||||
</group>
|
||||
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter reload_on_follower="True"/>
|
||||
</form>
|
||||
|
|
@ -65,7 +75,6 @@
|
|||
<field name="arch" type="xml">
|
||||
<list string="Recruitment Requisitions">
|
||||
<field name="name"/>
|
||||
<field name="department_id"/>
|
||||
<field name="requested_by"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
|
|
@ -77,6 +86,13 @@
|
|||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_recruitment_requisition" name="Recruitment Requisition" active='0' parent="hr_recruitment.menu_hr_recruitment_root"/>
|
||||
<menuitem
|
||||
name="Recruitment"
|
||||
id="menu_hr_recruitment_root"
|
||||
web_icon="hr_recruitment,static/description/icon.png"
|
||||
groups="hr_recruitment.group_hr_recruitment_user,hr_recruitment.group_hr_recruitment_interviewer"
|
||||
sequence="210"/>
|
||||
|
||||
<menuitem id="menu_recruitment_requisition" name="Recruitment Requisition" active='1' sequence="0" parent="hr_recruitment.menu_hr_recruitment_root"/>
|
||||
<menuitem id="menu_recruitment_requisition_main" name="Requisitions" parent="menu_recruitment_requisition" action="action_recruitment_requisition"/>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,16 @@
|
|||
<xpath expr="//block[@name='recruitment_in_app_purchases']" position="after">
|
||||
<block title="Requisition Access Control" name="requisition_access_block">
|
||||
<setting string="Requisition HR Approval Access"
|
||||
help="Select the HR responsible for approving the recruitment requisitions."
|
||||
help="Select the HR responsible for Requisitions."
|
||||
id="requisition_hr_access_control">
|
||||
<field name="requisition_hr_id" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||
</setting>
|
||||
<setting string="Requisition Finance Approval Access"
|
||||
help="Select the Finance Manager responsible for Requisitions."
|
||||
id="requisition_finance_access_control">
|
||||
<field name="requisition_finance_manager" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
|
||||
</setting>
|
||||
|
||||
</block>
|
||||
</xpath>
|
||||
</field>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<form string="Cancel Requisition">
|
||||
<group>
|
||||
<field name="cancellation_reason" placeholder="Enter the reason for cancellation..."/>
|
||||
<field name="cancellation_reason" nolabel="1" placeholder="Enter the reason for cancellation..."/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Submit" type="object" name="submit_cancellation" class="btn-primary"/>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@
|
|||
|
||||
<t t-name="universal_attachment_preview.PopupPreview">
|
||||
<Dialog title="props.filename" size="'lg'">
|
||||
<p class="text-muted mt-2">
|
||||
<t t-esc="props.mimetype"/>
|
||||
</p>
|
||||
|
||||
<t t-if="props.mimetype.endsWith('pdf')">
|
||||
<iframe t-att-src="props.url"
|
||||
style="width:100%; height:500px;"
|
||||
frameborder="0">
|
||||
</iframe>
|
||||
|
||||
</t>
|
||||
<t t-elif="props.mimetype.startsWith('image')">
|
||||
<img t-att-src="props.url"
|
||||
|
|
|
|||
Loading…
Reference in New Issue