[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:
committed by
Yannick Tivisse
parent
f02b15dcc2
commit
63c4a9b951
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
@@ -0,0 +1,7 @@
|
||||
.o_applicant_interviewer_form {
|
||||
.o_ChatterTopbar_buttonSendMessage,
|
||||
.o_ChatterTopbar_buttonLogNote,
|
||||
.o_MessageList {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user