[IMP] hr_recruitment: add Interviewer access rights

Create a new Interviewer access rights.

This right is automatically given to users that are added as
Interviewers on a Job Position or on an Application.

An interviewer can only access applications they are interviewer of the
job or the application itself, the chatter is disabled for them as it might
contain some sensitive information (salary details, contract link, etc.)

closes odoo/odoo#78622

Taskid: 2669730
Related: odoo/enterprise#21778
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Kevin Baptiste
2022-01-24 11:09:50 +00:00
committed by Yannick Tivisse
parent f02b15dcc2
commit 63c4a9b951
13 changed files with 318 additions and 17 deletions
+1 -1
View File
@@ -42,7 +42,7 @@
'application': True,
'assets': {
'web.assets_backend': [
'hr_recruitment/static/src/scss/hr_job.scss',
'hr_recruitment/static/src/scss/*.scss',
'hr_recruitment/static/src/js/recruitment.js',
'hr_recruitment/static/src/js/tours/hr_recruitment.js',
],
+2
View File
@@ -7,3 +7,5 @@ from . import calendar
from . import digest
from . import utm_campaign
from . import utm_source
from . import res_users
from . import ir_ui_menu
+14
View File
@@ -44,6 +44,13 @@ class Job(models.Model):
color = fields.Integer("Color Index")
is_favorite = fields.Boolean(compute='_compute_is_favorite', inverse='_inverse_is_favorite')
favorite_user_ids = fields.Many2many('res.users', 'job_favorite_user_rel', 'job_id', 'user_id', default=_get_default_favorite_user_ids)
interviewer_ids = fields.Many2many('res.users', string='Interviewers', domain="[('share', '=', False), ('company_ids', 'in', company_id)]")
extended_interviewer_ids = fields.Many2many('res.users', 'hr_job_extended_interviewer_res_users', compute='_compute_extended_interviewer_ids', store=True)
@api.depends('application_ids.interviewer_id')
def _compute_extended_interviewer_ids(self):
for job in self:
job.extended_interviewer_ids = job.application_ids.interviewer_id
def _compute_is_favorite(self):
for job in self:
@@ -132,10 +139,17 @@ class Job(models.Model):
'job_id': job.id,
} for job in jobs]
self.env['hr.recruitment.source'].create(source_vals)
jobs.sudo().interviewer_ids._create_recruitment_interviewers()
return jobs
def write(self, vals):
old_interviewers = self.interviewer_ids
res = super().write(vals)
if 'interviewer_ids' in vals:
interviewers_to_clean = old_interviewers - self.interviewer_ids
interviewers_to_clean._remove_recruitment_interviewers()
self.sudo().interviewer_ids._create_recruitment_interviewers()
# Since the alias is created upon record creation, the default values do not reflect the current values unless
# specifically rewritten
# List of fields to keep synched with the alias
+33 -7
View File
@@ -6,7 +6,7 @@ from random import randint
from odoo import api, fields, models, tools, SUPERUSER_ID
from odoo.osv.query import Query
from odoo.tools.translate import _
from odoo.exceptions import UserError
from odoo.exceptions import AccessError, UserError
from dateutil.relativedelta import relativedelta
@@ -137,10 +137,10 @@ class Applicant(models.Model):
date_last_stage_update = fields.Datetime("Last Stage Update", index=True, default=fields.Datetime.now)
priority = fields.Selection(AVAILABLE_PRIORITIES, "Appreciation", default='0')
job_id = fields.Many2one('hr.job', "Applied Job", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True)
salary_proposed_extra = fields.Char("Proposed Salary Extra", help="Salary Proposed by the Organisation, extra advantages", tracking=True)
salary_expected_extra = fields.Char("Expected Salary Extra", help="Salary Expected by Applicant, extra advantages", tracking=True)
salary_proposed = fields.Float("Proposed Salary", group_operator="avg", help="Salary Proposed by the Organisation", tracking=True)
salary_expected = fields.Float("Expected Salary", group_operator="avg", help="Salary Expected by Applicant", tracking=True)
salary_proposed_extra = fields.Char("Proposed Salary Extra", help="Salary Proposed by the Organisation, extra advantages", tracking=True, groups="hr_recruitment.group_hr_recruitment_user")
salary_expected_extra = fields.Char("Expected Salary Extra", help="Salary Expected by Applicant, extra advantages", tracking=True, groups="hr_recruitment.group_hr_recruitment_user")
salary_proposed = fields.Float("Proposed Salary", group_operator="avg", help="Salary Proposed by the Organisation", tracking=True, groups="hr_recruitment.group_hr_recruitment_user")
salary_expected = fields.Float("Expected Salary", group_operator="avg", help="Salary Expected by Applicant", tracking=True, groups="hr_recruitment.group_hr_recruitment_user")
availability = fields.Date("Availability", help="The date at which the applicant will be available to start working", tracking=True)
partner_name = fields.Char("Applicant's Name")
partner_phone = fields.Char("Phone", size=32, compute='_compute_partner_phone_email',
@@ -177,6 +177,7 @@ class Applicant(models.Model):
campaign_id = fields.Many2one(ondelete='set null')
medium_id = fields.Many2one(ondelete='set null')
source_id = fields.Many2one(ondelete='set null')
interviewer_id = fields.Many2one('res.users', string='Interviewer', domain="[('share', '=', False), ('company_ids', 'in', company_id)]")
@api.depends('date_open', 'date_closed')
def _compute_day(self):
@@ -256,7 +257,6 @@ class Applicant(models.Model):
else:
applicant.meeting_display_text = _('Last Meeting')
def _get_attachment_number(self):
read_group_res = self.env['ir.attachment'].read_group(
[('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.ids)],
@@ -340,6 +340,10 @@ class Applicant(models.Model):
if not applicant.stage_id.hired_stage:
applicant.date_closed = False
def _check_interviewer_access(self):
if self.user_has_groups('hr_recruitment.group_hr_recruitment_interviewer'):
raise AccessError(_('You are not allowed to perform this action.'))
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
@@ -348,6 +352,7 @@ class Applicant(models.Model):
if vals.get('email_from'):
vals['email_from'] = vals['email_from'].strip()
applicants = super().create(vals_list)
applicants.sudo().interviewer_id._create_recruitment_interviewers()
# Record creation through calendar, creates the calendar event directly, it will also create the activity.
if 'default_activity_date_deadline' in self.env.context:
deadline = fields.Datetime.to_datetime(self.env.context.get('default_activity_date_deadline'))
@@ -371,6 +376,7 @@ class Applicant(models.Model):
vals['date_open'] = fields.Datetime.now()
if vals.get('email_from'):
vals['email_from'] = vals['email_from'].strip()
old_interviewers = self.interviewer_id
# stage_id: track last stage before update
if 'stage_id' in vals:
vals['date_last_stage_update'] = fields.Datetime.now()
@@ -381,6 +387,10 @@ class Applicant(models.Model):
res = super(Applicant, self).write(vals)
else:
res = super(Applicant, self).write(vals)
if 'interviewer_id' in vals:
interviewers_to_clean = old_interviewers - self.interviewer_id
interviewers_to_clean._remove_recruitment_interviewers()
self.sudo().interviewer_id._create_recruitment_interviewers()
return res
def get_empty_list_help(self, help):
@@ -406,6 +416,21 @@ class Applicant(models.Model):
return nocontent_body % nocontent_values
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
if view_type == 'form' and self.user_has_groups('hr_recruitment.group_hr_recruitment_interviewer'):
view_id = self.env.ref('hr_recruitment.hr_applicant_view_form_interviewer').id
return super().fields_view_get(view_id, view_type, toolbar, submenu)
def _notify_compute_recipients(self, message, msg_vals):
"""
Do not notify members of the Recruitment Interviewer group, as this
might leak some data they shouldn't have access to.
"""
recipients = super()._notify_compute_recipients(message, msg_vals)
interviewer_group = self.env.ref('hr_recruitment.group_hr_recruitment_interviewer').id
return [recipient for recipient in recipients if interviewer_group not in recipient['groups']]
def action_makeMeeting(self):
""" This opens Meeting's calendar view to schedule meeting on current applicant
@return: Dictionary value for created Meeting view
@@ -546,7 +571,8 @@ class Applicant(models.Model):
def create_employee_from_applicant(self):
""" Create an hr.employee from the hr.applicants """
employee = False
self._check_interviewer_access()
for applicant in self:
contact_name = False
if applicant.partner_id:
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class IrUiMenu(models.Model):
_inherit = 'ir.ui.menu'
def _load_menus_blacklist(self):
res = super()._load_menus_blacklist()
if self.env.user.has_group('hr_recruitment.group_hr_recruitment_interviewer'):
res.append(self.env.ref('hr_recruitment.menu_hr_job_position').id)
return res
+39
View File
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ResUsers(models.Model):
_inherit = 'res.users'
def _create_recruitment_interviewers(self):
if not self:
return
interviewer_group = self.env.ref('hr_recruitment.group_hr_recruitment_interviewer')
recruitment_group = self.env.ref('hr_recruitment.group_hr_recruitment_user')
interviewer_group.sudo().write({
'users': [
(4, user.id) for user in self - recruitment_group.users
]
})
def _remove_recruitment_interviewers(self):
if not self:
return
interviewer_group = self.env.ref('hr_recruitment.group_hr_recruitment_interviewer')
recruitment_group = self.env.ref('hr_recruitment.group_hr_recruitment_user')
job_interviewers = self.env['hr.job'].read_group([('interviewer_ids', 'in', self.ids)], ['interviewer_ids'], ['interviewer_ids'])
user_ids = {j['interviewer_ids'][0] for j in job_interviewers}
application_interviewers = self.env['hr.applicant'].read_group([('interviewer_id', 'in', self.ids)], ['interviewer_id'], ['interviewer_id'])
user_ids |= {a['interviewer_id'][0] for a in application_interviewers}
# Remove users that are no longer interviewers on at least a job or an application
users_to_remove = set(self.ids) - (user_ids | set(recruitment_group.users.ids))
interviewer_group.sudo().write({
'users': [
(3, user_id) for user_id in users_to_remove
]
})
@@ -13,6 +13,12 @@
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="group_hr_recruitment_interviewer" model="res.groups">
<field name="name">Recruitment Interviewer</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="group_hr_recruitment_user" model="res.groups">
<field name="name">Officer</field>
<field name="category_id" ref="base.module_category_human_resources_recruitment"/>
@@ -30,4 +36,27 @@
<field name="groups_id" eval="[(4,ref('hr_recruitment.group_hr_recruitment_manager'))]"/>
</record>
<!-- Interviewer Access Rules -->
<record id="hr_applicant_interviewer_rule" model="ir.rule">
<field name="name">Applicant Interviewer</field>
<field name="model_id" ref="model_hr_applicant"/>
<field name="domain_force">[
'|',
('job_id.interviewer_ids', 'in', user.id),
('interviewer_id', '=', user.id),
]</field>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>
</record>
<record id="mail_message_interviewer_rule" model="ir.rule">
<field name="name">Interviewer: No Applicant Chatter</field>
<field name="model_id" ref="mail.model_mail_message"/>
<field name="domain_force">[('model', '!=', 'hr.applicant')]</field>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>
</record>
</odoo>
@@ -1,8 +1,12 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_job_interviewer,hr.job.interviewer,hr.model_hr_job,group_hr_recruitment_interviewer,1,0,0,0
access_hr_applicant_interviewer,hr.applicant.interviewer,model_hr_applicant,group_hr_recruitment_interviewer,1,1,0,0
access_hr_applicant_user,hr.applicant.user,model_hr_applicant,group_hr_recruitment_user,1,1,1,1
access_hr_recruitment_stage_interviewer,hr.recruitment.stage.interviewer,model_hr_recruitment_stage,group_hr_recruitment_interviewer,1,0,0,0
access_hr_recruitment_stage_user,hr.recruitment.stage.user,model_hr_recruitment_stage,group_hr_recruitment_user,1,0,0,0
access_hr_recruitment_stage_manager,hr.recruitment.stage.manager,model_hr_recruitment_stage,group_hr_recruitment_manager,1,1,1,1
access_hr_recruitment_degree,hr.recruitment.degree,model_hr_recruitment_degree,group_hr_recruitment_user,1,1,1,1
access_hr_recruitment_refuse_reason_interviewer,hr.applicant.refuse.reason.interviewer,model_hr_applicant_refuse_reason,group_hr_recruitment_interviewer,1,0,0,0
access_hr_recruitment_refuse_reason,hr.applicant.refuse.reason,model_hr_applicant_refuse_reason,group_hr_recruitment_user,1,1,1,1
access_res_partner_hr_user,res.partner.user,base.model_res_partner,group_hr_recruitment_user,1,1,1,1
access_calendar_event_hruser,calendar.event.hruser,calendar.model_calendar_event,group_hr_recruitment_user,1,1,1,1
@@ -12,3 +16,4 @@ access_hr_applicant_category,hr.applicant_category,model_hr_applicant_category,,
access_hr_applicant_category_manager,hr.applicant_category,model_hr_applicant_category,group_hr_recruitment_user,1,1,1,1
access_calendar_event_type_hr_officer,calendar.event.type.officer,calendar.model_calendar_event_type,group_hr_recruitment_user,1,1,1,0
access_applicant_get_refuse_reason,access.applicant.get.refuse.reason,model_applicant_get_refuse_reason,hr_recruitment.group_hr_recruitment_user,1,1,1,0
access_applicant_get_refuse_reason_interviewer,access.applicant.get.refuse.reason.interviewer,model_applicant_get_refuse_reason,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_job_interviewer hr.job.interviewer hr.model_hr_job group_hr_recruitment_interviewer 1 0 0 0
3 access_hr_applicant_interviewer hr.applicant.interviewer model_hr_applicant group_hr_recruitment_interviewer 1 1 0 0
4 access_hr_applicant_user hr.applicant.user model_hr_applicant group_hr_recruitment_user 1 1 1 1
5 access_hr_recruitment_stage_interviewer hr.recruitment.stage.interviewer model_hr_recruitment_stage group_hr_recruitment_interviewer 1 0 0 0
6 access_hr_recruitment_stage_user hr.recruitment.stage.user model_hr_recruitment_stage group_hr_recruitment_user 1 0 0 0
7 access_hr_recruitment_stage_manager hr.recruitment.stage.manager model_hr_recruitment_stage group_hr_recruitment_manager 1 1 1 1
8 access_hr_recruitment_degree hr.recruitment.degree model_hr_recruitment_degree group_hr_recruitment_user 1 1 1 1
9 access_hr_recruitment_refuse_reason_interviewer hr.applicant.refuse.reason.interviewer model_hr_applicant_refuse_reason group_hr_recruitment_interviewer 1 0 0 0
10 access_hr_recruitment_refuse_reason hr.applicant.refuse.reason model_hr_applicant_refuse_reason group_hr_recruitment_user 1 1 1 1
11 access_res_partner_hr_user res.partner.user base.model_res_partner group_hr_recruitment_user 1 1 1 1
12 access_calendar_event_hruser calendar.event.hruser calendar.model_calendar_event group_hr_recruitment_user 1 1 1 1
16 access_hr_applicant_category_manager hr.applicant_category model_hr_applicant_category group_hr_recruitment_user 1 1 1 1
17 access_calendar_event_type_hr_officer calendar.event.type.officer calendar.model_calendar_event_type group_hr_recruitment_user 1 1 1 0
18 access_applicant_get_refuse_reason access.applicant.get.refuse.reason model_applicant_get_refuse_reason hr_recruitment.group_hr_recruitment_user 1 1 1 0
19 access_applicant_get_refuse_reason_interviewer access.applicant.get.refuse.reason.interviewer model_applicant_get_refuse_reason hr_recruitment.group_hr_recruitment_interviewer 1 1 1 0
@@ -0,0 +1,7 @@
.o_applicant_interviewer_form {
.o_ChatterTopbar_buttonSendMessage,
.o_ChatterTopbar_buttonLogNote,
.o_MessageList {
display: none;
}
}
+1
View File
@@ -4,3 +4,4 @@
from . import test_recruitment_process
from . import test_recruitment
from . import test_utm
from . import test_recruitment_interviewer
@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import AccessError
from odoo.tests.common import new_test_user
from odoo.addons.mail.tests.common import MailCommon
class TestRecruitmentInterviewer(MailCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.simple_user = new_test_user(cls.env, 'smp',
groups='base.group_user', name='Simple User', email='smp@example.com')
cls.interviewer_user = new_test_user(cls.env, 'itw',
groups='base.group_user,hr_recruitment.group_hr_recruitment_interviewer',
name='Recruitment Interviewer', email='itw@example.com')
cls.manager_user = new_test_user(cls.env, 'mng',
groups='base.group_user,hr_recruitment.group_hr_recruitment_manager',
name='Recruitment Manager', email='mng@example.com')
cls.job = cls.env['hr.job'].create({
'name': 'super nice job',
'user_id': cls.manager_user.id,
})
def test_interviewer_group(self):
"""
Test that adding a user as interviewer to a job / applicant adds
that user in the Interviewer group. Also checks that removing the
user will remove them when they are no longer required (e.g. no
longer interviewer of any job/applicant).
"""
interviewer_group = self.env.ref('hr_recruitment.group_hr_recruitment_interviewer')
self.assertFalse(interviewer_group.id in self.simple_user.groups_id.ids, "Simple User should not be interviewer")
self.job.interviewer_ids = self.simple_user.ids
self.assertTrue(interviewer_group.id in self.simple_user.groups_id.ids, "Simple User should be added as interviewer")
self.job.write({'interviewer_ids': [(5, 0, 0)]})
self.assertFalse(interviewer_group.id in self.simple_user.groups_id.ids, "Simple User should be removed from interviewer")
applicant = self.env['hr.applicant'].create({
'name': 'toto',
'partner_name': 'toto',
'job_id': self.job.id,
'interviewer_id': self.simple_user.id,
})
self.assertTrue(interviewer_group.id in self.simple_user.groups_id.ids, "Simple User should be added as interviewer")
applicant.interviewer_id = False
self.assertFalse(interviewer_group.id in self.simple_user.groups_id.ids, "Simple User should be removed from interviewer")
self.job.interviewer_ids = self.simple_user.ids
applicant.interviewer_id = self.simple_user.id
self.assertTrue(interviewer_group.id in self.simple_user.groups_id.ids, "Simple User should be added as interviewer")
applicant.interviewer_id = False
self.assertTrue(interviewer_group.id in self.simple_user.groups_id.ids, "Simple User should stay interviewer")
self.job.write({'interviewer_ids': [(5, 0, 0)]})
applicant.interviewer_id = self.simple_user.id
self.assertTrue(interviewer_group.id in self.simple_user.groups_id.ids, "Simple User should stay interviewer")
applicant.interviewer_id = False
self.assertFalse(interviewer_group.id in self.simple_user.groups_id.ids, "Simple User should be removed from interviewer")
# A Manager should not be added to the Interviewer group
self.assertFalse(interviewer_group.id in self.manager_user.groups_id.ids, "Manager User should not be interviewer")
applicant.interviewer_id = self.manager_user.id
self.assertFalse(interviewer_group.id in self.manager_user.groups_id.ids, "Manager User should not be added in Interviewer group")
def test_interviewer_access_rights(self):
applicant = self.env['hr.applicant'].create({
'name': 'toto',
'partner_name': 'toto',
'job_id': self.job.id,
})
with self.assertRaises(AccessError):
applicant.with_user(self.interviewer_user).read()
applicant = self.env['hr.applicant'].create({
'name': 'toto',
'partner_name': 'toto',
'job_id': self.job.id,
'interviewer_id': self.interviewer_user.id,
})
applicant.with_user(self.interviewer_user).read()
self.job.interviewer_ids = self.interviewer_user.ids
applicant = self.env['hr.applicant'].create({
'name': 'toto',
'partner_name': 'toto',
'job_id': self.job.id,
})
applicant.with_user(self.interviewer_user).read()
# An interviewer can change the interviewers
applicant.with_user(self.interviewer_user).interviewer_id = self.simple_user.id
self.assertEqual(self.simple_user, applicant.interviewer_id)
with self.assertRaises(AccessError):
applicant.with_user(self.interviewer_user).create_employee_from_applicant()
def test_interviewer_chatter(self):
self.manager_user.notification_type = 'email'
self.interviewer_user.notification_type = 'email'
applicant = self.env['hr.applicant'].create({
'name': 'toto',
'partner_name': 'toto',
'job_id': self.job.id,
'interviewer_id': self.interviewer_user.id,
})
with self.mock_mail_gateway():
message = applicant.message_post(body='A super secret message', message_type='comment', subtype_xmlid='mail.mt_comment')
with self.assertRaises(AccessError):
message.with_user(self.interviewer_user).read()
try:
self._find_mail_mail_wpartners(self.interviewer_user.partner_id, False)
except AssertionError:
pass
else:
raise AssertionError('No mail.mail should be sent to members of Interviewer group')
self.assertSentEmail(self.env.user.partner_id, [self.manager_user.partner_id])
+22 -3
View File
@@ -42,7 +42,7 @@
</div>
</div>
<div class="o_kanban_manage_button_section">
<a class="o_kanban_manage_toggle_button" href="#"><i class="fa fa-ellipsis-v" role="img" aria-label="Manage" title="Manage"/></a>
<a class="o_kanban_manage_toggle_button" href="#" groups="hr_recruitment.group_hr_recruitment_user"><i class="fa fa-ellipsis-v" role="img" aria-label="Manage" title="Manage"/></a>
</div>
</div>
<div class="container o_kanban_card_content mt-0 mt-sm-3">
@@ -71,7 +71,7 @@
</t>
</div>
</t>
<div name="kanban_boxes" class="row o_recruitment_kanban_boxes">
<div name="kanban_boxes" class="row o_recruitment_kanban_boxes" groups="hr_recruitment.group_hr_recruitment_user">
<div class="o_recruitment_kanban_box o_kanban_primary_bottom bottom_block" style="padding-left:8px;">
<div class="col-6"></div>
<div class="col-6 o_link_trackers">
@@ -129,5 +129,24 @@
</p>
</field>
</record>
<menuitem name="By Job Positions" parent="menu_crm_case_categ0_act_job" id="menu_hr_job_position" action="action_hr_job" sequence="1"/>
<menuitem name="By Job Positions" parent="menu_crm_case_categ0_act_job"
id="menu_hr_job_position" action="action_hr_job"
sequence="1" groups="hr_recruitment.group_hr_recruitment_user" />
<!-- Job Positions filtered for interviewers -->
<record model="ir.actions.act_window" id="action_hr_job_interviewer">
<field name="name">Job Positions</field>
<field name="res_model">hr.job</field>
<field name="view_mode">kanban,form</field>
<field name="view_id" ref="hr_recruitment.view_hr_job_kanban"/>
<field name="context">{'create': False}</field>
<field name="domain">[
'|',
('interviewer_ids', 'in', uid),
('extended_interviewer_ids', 'in', uid),
]</field>
</record>
<menuitem name="By Job Positions" parent="menu_crm_case_categ0_act_job"
id="menu_hr_job_position_interviewer" action="action_hr_job_interviewer"
sequence="1" groups="hr_recruitment.group_hr_recruitment_interviewer" />
</odoo>
@@ -72,8 +72,9 @@
<field name="model">hr.applicant</field>
<field name="arch" type="xml">
<form string="Jobs - Recruitment Form" class="o_applicant_form">
<field name="emp_id" invisible="1"/>
<header>
<button string="Create Employee" name="create_employee_from_applicant" type="object" data-hotkey="v"
<button string="Create Employee" name="create_employee_from_applicant" type="object" data-hotkey="v" groups="hr_recruitment.group_hr_recruitment_user"
class="oe_highlight o_create_employee" attrs="{'invisible': ['|',('emp_id', '!=', False),('active', '=', False)]}"/>
<button string="Refuse" name="archive_applicant" type="object" attrs="{'invisible': [('active', '=', False)]}" data-hotkey="x"/>
<button string="Restore" name="toggle_active" type="object" attrs="{'invisible': [('active', '=', True)]}" data-hotkey="z"/>
@@ -126,6 +127,7 @@
</group>
<group>
<field name="categ_ids" placeholder="Tags" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
<field name="interviewer_id" options="{'no_create': True, 'no_create_edit': True}" />
<field name="user_id" domain="[('share', '=', False)]"/>
<field name="date_closed" attrs="{'invisible': [('date_closed', '=', False)]}" />
<field name="priority" widget="priority"/>
@@ -137,7 +139,7 @@
<field name="department_id"/>
<field name="company_id" groups="base.group_multi_company" options='{"no_open":True}' />
</group>
<group string="Contract">
<group string="Contract" name="recruitment_contract">
<label for="salary_expected"/>
<div class="o_row">
<field name="salary_expected"/>
@@ -151,7 +153,6 @@
<field name="salary_proposed_extra" placeholder="Extra advantages..."/>
</div>
<field name="availability"/>
<field name="emp_id" invisible="1"/>
</group>
</group>
<notebook>
@@ -169,6 +170,19 @@
</field>
</record>
<record id="hr_applicant_view_form_interviewer" model="ir.ui.view">
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_applicant_view_form"/>
<field name="priority">50</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//form" position="attributes">
<attribute name="class">o_applicant_form o_applicant_interviewer_form</attribute>
</xpath>
<xpath expr="//group[@name='recruitment_contract']" position="replace"/>
</field>
</record>
<record model="ir.ui.view" id="crm_case_pivot_view_job">
<field name="name">Jobs - Recruitment</field>
<field name="model">hr.applicant</field>
@@ -489,6 +503,7 @@
</div>
</xpath>
<field name="no_of_recruitment" position="after">
<field name="interviewer_ids" widget="many2many_tags" options="{'no_create': True, 'no_create_edit': True}" />
<field name="user_id" domain="[('share', '=', False)]"/>
</field>
<div name="button_box" position="inside">
@@ -640,11 +655,11 @@
name="Recruitment"
id="menu_hr_recruitment_root"
web_icon="hr_recruitment,static/description/icon.png"
groups="hr_recruitment.group_hr_recruitment_user"
groups="hr_recruitment.group_hr_recruitment_user,hr_recruitment.group_hr_recruitment_interviewer"
sequence="210"/>
<menuitem id="menu_hr_recruitment_configuration" name="Configuration" parent="menu_hr_recruitment_root"
sequence="100"/>
groups="group_hr_recruitment_user" sequence="100"/>
<!-- ALL JOBS REQUESTS -->
<menuitem parent="menu_hr_recruitment_configuration" id="menu_hr_job_position_config" action="action_hr_job_config" sequence="10"/>
@@ -1069,7 +1084,7 @@ action = act
</p>
</field>
</record>
<menuitem name="Reporting" id="report_hr_recruitment" parent="menu_hr_recruitment_root"
<menuitem name="Reporting" id="report_hr_recruitment" parent="menu_hr_recruitment_root" groups="group_hr_recruitment_user"
sequence="99"/>
<menuitem name="Recruitment Analysis" id="hr_applicant_report_menu" parent="report_hr_recruitment"