[IMP] hr_recruitment(_skills/_survey): grant interviewer more rights

Purpose: In order to increase visibility of the interviewer group,
it is moved from hidden group, to recruitment group.

Moreover, we grant the interviewer additional rights in order to manage
the application process properly and be able to execute following actions
• Send an interview
• Check interview results
• Add/delete/modify skills
• Create a meeting

In order to allow above mentioned actions on skills to the interviewer,
we grant them all the rights to the hr_applicant_skill.

In order to let the interviewer send the survey we grant them access to
the private addresses and now recruitment-interviewer implies
survey-officer group as well.

Additionally, for the security purposes, we make a change, so that
the survey officer is not able to read recruitment interview results
unless they are also an interviewer for a specific applicant.

task - 3179332

closes odoo/odoo#118897

Related: odoo/enterprise#40696
Related: odoo/upgrade#4624
Signed-off-by: Kevin Baptiste <kba@odoo.com>
This commit is contained in:
sofiagvaladze
2023-06-27 17:33:02 +02:00
parent 0ecc280e7e
commit 3faad0a7a0
11 changed files with 88 additions and 27 deletions
+15 -5
View File
@@ -280,7 +280,7 @@ class Applicant(models.Model):
applicant.date_closed = False
def _check_interviewer_access(self):
if self.user_has_groups('hr_recruitment.group_hr_recruitment_interviewer'):
if self.user_has_groups('hr_recruitment.group_hr_recruitment_interviewer') and not self.user_has_groups('hr_recruitment.group_hr_recruitment_user'):
raise AccessError(_('You are not allowed to perform this action.'))
@api.model_create_multi
@@ -367,19 +367,20 @@ class Applicant(models.Model):
def _notify_get_recipients(self, message, msg_vals, **kwargs):
"""
Do not notify members of the Recruitment Interviewer group, as this
Do not notify members of the Recruitment Interviewer group that are not part of
Recruitment User group as well, as this
might leak some data they shouldn't have access to.
"""
recipients = super()._notify_get_recipients(message, msg_vals, **kwargs)
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']]
user_group = self.env.ref('hr_recruitment.group_hr_recruitment_user').id
return [recipient for recipient in recipients if not (interviewer_group in recipient['groups'] and user_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
"""
self.ensure_one()
if not self.partner_id:
if not self.partner_name:
raise UserError(_('You must define a Contact Name for this applicant.'))
@@ -392,11 +393,20 @@ class Applicant(models.Model):
'mobile': self.partner_mobile
})
partners = self.partner_id | self.user_id.partner_id | self.department_id.manager_id.user_id.partner_id
partners = self.partner_id | self.department_id.manager_id.user_id.partner_id
if self.user_has_groups('hr_recruitment.group_hr_recruitment_interviewer') and not self.user_has_groups('hr_recruitment.group_hr_recruitment_user'):
partners |= self.env.user.partner_id
else:
partners |= self.user_id.partner_id
category = self.env.ref('hr_recruitment.categ_meet_interview')
res = self.env['ir.actions.act_window']._for_xml_id('calendar.action_calendar_event')
# As we are redirected from the hr.applicant, calendar checks rules on "hr.applicant",
# in order to decide whether to allow creation of a meeting.
# As interviewer does not have create right on the hr.applicant, in order to allow them
# to create a meeting for an applicant, we pass 'create': True to the context.
res['context'] = {
'create': True,
'default_applicant_id': self.id,
'default_partner_ids': partners.ids,
'default_user_id': self.env.uid,
+3 -1
View File
@@ -9,6 +9,8 @@ class IrUiMenu(models.Model):
def _load_menus_blacklist(self):
res = super()._load_menus_blacklist()
if self.env.user.has_group('hr_recruitment.group_hr_recruitment_interviewer'):
if self.env.user.has_group('hr_recruitment.group_hr_recruitment_interviewer') and not self.env.user.has_group('hr_recruitment.group_hr_recruitment_user'):
res.append(self.env.ref('hr_recruitment.menu_hr_job_position').id)
elif self.env.user.has_group('hr_recruitment.group_hr_recruitment_user'):
res.append(self.env.ref('hr_recruitment.menu_hr_job_position_interviewer').id)
return res
@@ -2,7 +2,7 @@
<odoo noupdate="1">
<record model="ir.module.category" id="base.module_category_human_resources_recruitment">
<field name="description">The user interacting with the application as interviewer don't need any specific access. They'll have access thanks to their interviewer assignation.</field>
<field name="description">Interviewer right will give access to all job position/applications where the employee is defined. It will allow to refuse, plan meetings. Chatter content will not be available.</field>
<field name="sequence">11</field>
</record>
@@ -14,15 +14,15 @@
</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="name">Interviewer</field>
<field name="category_id" ref="base.module_category_human_resources_recruitment"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="group_hr_recruitment_user" model="res.groups">
<field name="name">Officer: Manage all applicants</field>
<field name="category_id" ref="base.module_category_human_resources_recruitment"/>
<field name="implied_ids" eval="[(4, ref('hr.group_hr_user'))]"/>
<field name="implied_ids" eval="[(4, ref('hr.group_hr_user')), (4, ref('group_hr_recruitment_interviewer'))]"/>
</record>
<record id="group_hr_recruitment_manager" model="res.groups">
@@ -59,6 +59,20 @@
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>
</record>
<record id="hr_applicant_user_rule" model="ir.rule">
<field name="name">User: All Applicants</field>
<field name="model_id" ref="model_hr_applicant"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_user'))]"/>
</record>
<record id="hr_job_user_rule" model="ir.rule">
<field name="name">User: All Applicants</field>
<field name="model_id" ref="model_hr_job"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_user'))]"/>
</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"/>
@@ -74,4 +88,11 @@
<field name="perm_unlink" eval="False"/>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>
</record>
<record id="mail_message_user_rule" model="ir.rule">
<field name="name">User: All Chatter</field>
<field name="model_id" ref="mail.model_mail_message"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_user'))]"/>
</record>
</odoo>
@@ -67,11 +67,6 @@ class TestRecruitmentInterviewer(MailCommon):
applicant.interviewer_ids = 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_ids = self.manager_user.ids
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',
@@ -11,4 +11,12 @@
]</field>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>
</record>
<record id="hr_applicant_skill_officer_rule" model="ir.rule">
<field name="name">Applicant Skill: Officer</field>
<field name="model_id" ref="model_hr_applicant_skill"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_user'))]"/>
</record>
</odoo>
@@ -1,3 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
hr_recruitment_skills.access_hr_applicant_skill,access_hr_applicant_skill,hr_recruitment_skills.model_hr_applicant_skill,hr_recruitment.group_hr_recruitment_user,1,1,1,1
hr_recruitment_skills.access_hr_applicant_skill_interviewer,access_hr_applicant_skill_interviewer,hr_recruitment_skills.model_hr_applicant_skill,hr_recruitment.group_hr_recruitment_interviewer,1,0,0,0
hr_recruitment_skills.access_hr_applicant_skill_interviewer,access_hr_applicant_skill_interviewer,hr_recruitment_skills.model_hr_applicant_skill,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 hr_recruitment_skills.access_hr_applicant_skill hr_recruitment_skills.access_hr_applicant_skill_interviewer access_hr_applicant_skill access_hr_applicant_skill_interviewer hr_recruitment_skills.model_hr_applicant_skill hr_recruitment.group_hr_recruitment_user hr_recruitment.group_hr_recruitment_interviewer 1 1 1 1
hr_recruitment_skills.access_hr_applicant_skill_interviewer access_hr_applicant_skill_interviewer hr_recruitment_skills.model_hr_applicant_skill hr_recruitment.group_hr_recruitment_interviewer 1 0 0 0
@@ -12,7 +12,7 @@
<field name="id" invisible="1"/>
<field name="is_interviewer" invisible="1"/>
<field mode="tree" nolabel="1" name="applicant_skill_ids" widget="skills_one2many"
context="{'default_applicant_id': id}" attrs="{'readonly': [('is_interviewer', '=', True)]}">
context="{'default_applicant_id': id}">
<tree>
<field name="skill_type_id" invisible="1"/>
<field name="skill_id"/>
@@ -18,12 +18,18 @@ class Applicant(models.Model):
.filtered(lambda i: i.survey_id == self.survey_id)\
.sorted(lambda i: i.create_date, reverse=True)
if not sorted_interviews:
return self.survey_id.action_print_survey()
action = self.survey_id.action_print_survey()
action['target'] = 'new'
return action
answered_interviews = sorted_interviews.filtered(lambda i: i.state == 'done')
if answered_interviews:
return self.survey_id.action_print_survey(answer=answered_interviews[0])
return self.survey_id.action_print_survey(answer=sorted_interviews[0])
action = self.survey_id.action_print_survey(answer=answered_interviews[0])
action['target'] = 'new'
return action
action = self.survey_id.action_print_survey(answer=sorted_interviews[0])
action['target'] = 'new'
return action
def action_send_survey(self):
self.ensure_one()
@@ -32,7 +38,7 @@ class Applicant(models.Model):
if not self.partner_id:
if not self.partner_name:
raise UserError(_('Please provide an applicant name.'))
self.partner_id = self.env['res.partner'].create({
self.partner_id = self.env['res.partner'].sudo().create({
'is_company': False,
'type': 'private',
'name': self.partner_name,
@@ -1,8 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="hr_recruitment.group_hr_recruitment_user" model="res.groups">
<field name="implied_ids" eval="[(4, ref('survey.group_survey_user'))]"/>
<record id="hr_recruitment.group_hr_recruitment_interviewer" model="res.groups">
<field name="implied_ids" eval="[Command.link(ref('survey.group_survey_user')), Command.link(ref('base.group_private_addresses'))]"/>
<field name="comment">It will also allow to send surveys and see the resume.</field>
</record>
<record id="survey.survey_user_input_rule_survey_user_read" model="ir.rule">
<field name="name">Survey user input: officer: read all non private survey answers</field>
<field name="domain_force">[('applicant_id', '=', False)]</field>
</record>
<record id="survey_user_input_rule_recruitment_interviewer" model="ir.rule">
<field name="name">Survey user input: recruitment interview: read survey answers for which they are set as interviewer</field>
<field name="model_id" ref="survey.model_survey_user_input"/>
<field name="domain_force">[
('applicant_id', '!=', False),
'|',
('applicant_id.interviewer_ids', 'in', user.id),
('applicant_id.job_id.interviewer_ids', 'in', user.id),
]</field>
<field name="groups" eval="[(4, ref('hr_recruitment.group_hr_recruitment_interviewer'))]"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
</data>
</odoo>
@@ -26,7 +26,7 @@
icon="fa-pencil-square-o"
type="object"
help="See interview report"
attrs="{'invisible':['|',('survey_id','=',False),('response_ids','=',False)]}">
attrs="{'invisible':['|',('survey_id','=',False),('response_ids','=',[])]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Consult</span>
<span class="o_stat_text">Interview</span>
@@ -31,8 +31,8 @@ class SurveyInvite(models.TransientModel):
survey = self.survey_id.with_context(clean_context(self.env.context))
if not self.applicant_id.response_ids.filtered(lambda res: res.survey_id.id == self.survey_id.id):
self.applicant_id.write({
'response_ids': (self.applicant_id.response_ids | survey._create_answer(partner=self.applicant_id.partner_id,
self.applicant_id.sudo().write({
'response_ids': (self.applicant_id.response_ids | survey.sudo()._create_answer(partner=self.applicant_id.partner_id,
**self._get_answers_values())).ids
})