393 lines
18 KiB
Python
393 lines
18 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import json
|
|
from markupsafe import Markup, escape
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import AccessError, UserError
|
|
from odoo.tools.misc import str2bool
|
|
|
|
REFERRAL_FIELDS = {
|
|
'sequence', 'partner_name', 'job_id', 'referral_points_ids', 'earned_points', 'max_points', 'active', 'response_id',
|
|
'shared_item_infos', 'referral_state', 'user_id', 'friend_id', 'write_date', 'ref_user_id', 'id',
|
|
}
|
|
|
|
|
|
class HrApplicant(models.Model):
|
|
_inherit = "hr.applicant"
|
|
|
|
ref_user_id = fields.Many2one('res.users', string='Referred By User', tracking=True,
|
|
compute='_compute_ref_user_id', readonly=False, store=True, copy=False)
|
|
source_id = fields.Many2one(compute="_compute_source_id", store=True, readonly=False)
|
|
referral_points_ids = fields.One2many('hr.referral.points', 'applicant_id', copy=False)
|
|
earned_points = fields.Integer(compute='_compute_earned_points')
|
|
referral_state = fields.Selection([
|
|
('progress', 'Ongoing'),
|
|
('hired', 'Hired'),
|
|
('closed', 'Not Hired')], required=True, default='progress')
|
|
shared_item_infos = fields.Text(compute="_compute_shared_item_infos")
|
|
max_points = fields.Integer(related="job_id.max_points")
|
|
friend_id = fields.Many2one('hr.referral.friend', copy=False)
|
|
last_valuable_stage_id = fields.Many2one('hr.recruitment.stage', "Last Valuable Stage")
|
|
is_accessible_to_current_user = fields.Boolean(
|
|
compute='_compute_is_accessible_to_current_user',
|
|
search='_search_is_accessible_to_current_user')
|
|
|
|
def _search_is_accessible_to_current_user(self, operator, value):
|
|
if operator not in ('in', 'not in'):
|
|
return NotImplemented
|
|
if self.env.user.has_group('hr_recruitment.group_hr_recruitment_user'):
|
|
return []
|
|
applications = self.env['hr.applicant'].with_context(active_test=False).search([
|
|
'|',
|
|
('job_id', 'any', [('interviewer_ids', 'in', self.env.user.id)]),
|
|
('interviewer_ids', 'in', self.env.user.id),
|
|
])
|
|
return [('id', operator, applications.ids)]
|
|
|
|
@api.depends_context('uid')
|
|
def _compute_is_accessible_to_current_user(self):
|
|
if self.env.user.has_group('hr_recruitment.group_hr_recruitment_user'):
|
|
self.is_accessible_to_current_user = True
|
|
else:
|
|
for applicant in self:
|
|
interviewers = applicant.interviewer_ids
|
|
corresponding_job_interviewers = applicant.job_id.interviewer_ids
|
|
applicant.is_accessible_to_current_user = self.env.user in interviewers or self.env.user in corresponding_job_interviewers
|
|
if not applicant.is_accessible_to_current_user:
|
|
raise AccessError(_("You are not allowed to access this application record because you're not one of the interviewers of this application. If you think that's an error, please contact your administrator."))
|
|
|
|
@api.depends('source_id')
|
|
def _compute_ref_user_id(self):
|
|
utm_source_referral = self.env.ref('utm.utm_source_referral', raise_if_not_found=False)
|
|
for applicant in self:
|
|
if applicant.source_id != utm_source_referral:
|
|
applicant.ref_user_id = False
|
|
|
|
@api.depends('ref_user_id')
|
|
def _compute_source_id(self):
|
|
utm_source_referral = self.env.ref('utm.utm_source_referral', raise_if_not_found=False)
|
|
for applicant in self:
|
|
if applicant.ref_user_id and utm_source_referral:
|
|
applicant.source_id = utm_source_referral.id
|
|
|
|
@api.model
|
|
@api.deprecated("Override of a deprecated method")
|
|
def check_field_access_rights(self, operation, field_names):
|
|
referral_fields = REFERRAL_FIELDS
|
|
|
|
result = super().check_field_access_rights(operation, field_names)
|
|
if field_names:
|
|
if (
|
|
not self.env.is_admin() and not self.env.user.has_group('hr_referral.group_hr_recruitment_referral_user')
|
|
and operation == 'read' and not set(field_names) - referral_fields
|
|
):
|
|
raise AccessError(_('You are not allowed to access applicant records.'))
|
|
return result
|
|
return [field_name for field_name in result if field_name not in referral_fields]
|
|
|
|
def _has_field_access(self, field, operation):
|
|
return super()._has_field_access(field, operation) and (
|
|
self.env.is_admin()
|
|
or self.env.user.has_group('hr_referral.group_hr_recruitment_referral_user')
|
|
or operation != 'read'
|
|
or field.name in REFERRAL_FIELDS
|
|
)
|
|
|
|
def _check_field_access(self, field, operation):
|
|
try:
|
|
return super()._check_field_access(field, operation)
|
|
except AccessError as e:
|
|
if field.name in REFERRAL_FIELDS:
|
|
raise AccessError(_('You are not allowed to access applicant records.')) from e
|
|
raise
|
|
|
|
@api.depends('referral_points_ids')
|
|
def _compute_shared_item_infos(self):
|
|
for applicant in self:
|
|
stages = self.env['hr.recruitment.stage'].search([('use_in_referral', '=', True), '|', ('job_ids', '=', False), ('job_ids', '=', applicant.job_id.id)])
|
|
infos = [{
|
|
'name': stage.name,
|
|
'points': stage.points,
|
|
'done': bool(len(applicant.referral_points_ids.filtered(lambda point: point.stage_id == stage)) % 2),
|
|
'seq': stage.sequence,
|
|
} for stage in stages]
|
|
applicant.shared_item_infos = json.dumps(infos)
|
|
|
|
def _compute_earned_points(self):
|
|
for applicant in self:
|
|
applicant.earned_points = sum(applicant.referral_points_ids.mapped('points'))
|
|
|
|
def write(self, vals):
|
|
res = super().write(vals)
|
|
if 'ref_user_id' in vals or 'stage_id' in vals or 'date_closed' in vals:
|
|
for applicant in self.filtered(lambda a: a.ref_user_id):
|
|
if 'ref_user_id' in vals:
|
|
applicant.referral_points_ids.unlink()
|
|
applicant.sudo()._update_points(applicant.stage_id.id, vals.get('last_stage_id', False))
|
|
if 'stage_id' in vals and vals['stage_id']:
|
|
applicant.last_valuable_stage_id = vals['stage_id']
|
|
if 'date_closed' in vals:
|
|
if not vals['date_closed'] and not applicant.stage_id.hired_stage:
|
|
applicant.referral_state = 'progress'
|
|
return res
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
applicants = super().create(vals_list)
|
|
for applicant in applicants:
|
|
if applicant.ref_user_id and applicant.stage_id:
|
|
applicant.sudo()._update_points(applicant.stage_id.id, False)
|
|
applicant.last_valuable_stage_id = applicant.stage_id
|
|
return applicants
|
|
|
|
def _send_notification(self, body, action_value='hr_referral.action_hr_applicant_employee_referral'):
|
|
if referrer := self.ref_user_id:
|
|
if self.partner_name:
|
|
subject = _('Referral: %(partner)s (%(applicant)s)', partner=self.partner_name, applicant=self.display_name)
|
|
else:
|
|
subject = _('Referral: %s', self.display_name)
|
|
action_url = f'/odoo/action-{action_value}?active_model={self._name}'
|
|
body = Markup("<a class='o_document_link' href=%s>%s</a><br>%s") % (action_url, subject, body)
|
|
odoobot = self.env.ref('base.partner_root')
|
|
# Do *not* notify on `self` as it will lead to unintended behavior.
|
|
# See opw-3285752 -> we attach it on the referrer's user
|
|
self.env['mail.thread'].sudo().message_notify(
|
|
model=referrer._name,
|
|
res_id=referrer.id,
|
|
subject=subject,
|
|
body=body,
|
|
author_id=odoobot.id,
|
|
partner_ids=[self.ref_user_id.partner_id.id],
|
|
email_layout_xmlid='mail.mail_notification_light',
|
|
)
|
|
|
|
def _update_points(self, new_state_id, old_state_id):
|
|
if not self.company_id:
|
|
raise UserError(_("Applicant must have a company."))
|
|
new_state = self.env['hr.recruitment.stage'].browse(new_state_id)
|
|
old_state = self.env['hr.recruitment.stage'].browse(old_state_id)
|
|
if old_state and old_state.use_in_referral:
|
|
old_state_sequence = old_state.sequence
|
|
elif old_state:
|
|
old_state_sequence = self.last_valuable_stage_id.sequence or -1
|
|
else:
|
|
old_state_sequence = -1
|
|
point_stage = []
|
|
|
|
# Decrease stage sequence
|
|
if new_state.sequence < old_state_sequence:
|
|
stages_to_remove = self.env['hr.referral.points']._read_group(
|
|
[
|
|
('stage_id.use_in_referral', '=', True),
|
|
('applicant_id', '=', self.id),
|
|
('stage_id.sequence', '<=', old_state_sequence),
|
|
('stage_id.sequence', '>', new_state.sequence)
|
|
], ['stage_id'], ['points:sum'])
|
|
for stage, point_sum in stages_to_remove:
|
|
point_stage.append({
|
|
'applicant_id': self.id,
|
|
'stage_id': stage.id,
|
|
'points': - point_sum,
|
|
'ref_user_id': self.ref_user_id.id,
|
|
'company_id': self.company_id.id
|
|
})
|
|
if not self.date_closed and not new_state.hired_stage:
|
|
self.referral_state = 'progress'
|
|
|
|
# Increase stage sequence
|
|
elif new_state.sequence > old_state_sequence:
|
|
stages_to_add = self.env['hr.recruitment.stage'].search([
|
|
('use_in_referral', '=', True),
|
|
('sequence', '>', old_state_sequence), ('sequence', '<=', new_state.sequence),
|
|
'|', ('job_ids', '=', False), ('job_ids', '=', self.job_id.id)])
|
|
gained_points = 0
|
|
available_points = sum(self.env['hr.referral.points'].search([('ref_user_id', '=', self.ref_user_id.id), ('company_id', '=', self.company_id.id)]).mapped('points'))
|
|
for stage in stages_to_add:
|
|
gained_points += stage.points
|
|
point_stage.append({
|
|
'applicant_id': self.id,
|
|
'stage_id': stage.id,
|
|
'points': stage.points,
|
|
'sequence_stage': stage.sequence,
|
|
'ref_user_id': self.ref_user_id.id,
|
|
'company_id': self.company_id.id
|
|
})
|
|
available_points += gained_points
|
|
additional_message = ''
|
|
if gained_points > 0:
|
|
additional_message = escape(_(
|
|
" You've gained {gained} points with this progress.{new_line}"
|
|
"It makes you a new total of {total} points. Visit {link1}this link{link2} to pick a gift!")).format(
|
|
gained=gained_points,
|
|
new_line=Markup('<br/>'),
|
|
total=available_points,
|
|
link1=Markup('<a href="/odoo/action-hr_referral.action_hr_referral_reward?active_model=hr.referral.reward">'),
|
|
link2=Markup('</a>'),
|
|
)
|
|
if new_state.use_in_referral:
|
|
if self.stage_id.hired_stage:
|
|
self.referral_state = 'hired'
|
|
self._send_notification(_("Your referrer is hired! %(message)s", message=additional_message))
|
|
else:
|
|
self._send_notification(_("Your referrer got a step further! %(message)s", message=additional_message))
|
|
|
|
self.env['hr.referral.points'].create(point_stage)
|
|
self.invalidate_recordset(['earned_points'])
|
|
|
|
def choose_a_friend(self, friend_id):
|
|
self.ensure_one()
|
|
self_sudo = self.sudo()
|
|
if not self.env.user:
|
|
return
|
|
if self_sudo.ref_user_id == self.env.user and not self_sudo.friend_id:
|
|
# Use sudo, user has normaly not the right to write on applicant
|
|
self_sudo.write({'friend_id': friend_id})
|
|
|
|
def _get_onboarding_steps(self):
|
|
return [{
|
|
'text': onboarding.text,
|
|
'image': onboarding.image
|
|
} for onboarding in self.env['hr.referral.onboarding'].search([])]
|
|
|
|
def _get_friends(self, applicant_names):
|
|
return [{
|
|
'id': friend.id,
|
|
'friend': applicant_names.get(friend.id, ''),
|
|
'name': applicant_names.get(friend.id, friend.name),
|
|
'position': friend.position,
|
|
'image': friend.image,
|
|
} for friend in self.env['hr.referral.friend'].search([]) if friend.id in applicant_names]
|
|
|
|
def _get_friends_head(self, applicant_names):
|
|
return [{
|
|
'id': friend.id,
|
|
'friend': applicant_names.get(friend.id, ''),
|
|
'name': friend.name,
|
|
'image': friend.image_head,
|
|
} for friend in self.env['hr.referral.friend'].search([])]
|
|
|
|
@api.model
|
|
def retrieve_referral_data(self):
|
|
return {
|
|
'show_grass': str2bool(self.env["ir.config_parameter"].sudo().get_param('hr_referral.show_grass')),
|
|
}
|
|
|
|
@api.model
|
|
def retrieve_referral_welcome_screen(self):
|
|
result = self.retrieve_referral_data()
|
|
user_id = self.env.user
|
|
|
|
result['id'] = user_id.id
|
|
if not user_id.hr_referral_onboarding_page:
|
|
result['onboarding_screen'] = True
|
|
result['onboarding'] = self._get_onboarding_steps()
|
|
return result
|
|
|
|
applicant = self.sudo().search([('ref_user_id', '=', user_id.id), ('company_id', 'in', self.env.companies.ids)])
|
|
applicants_hired = applicant.filtered(lambda r: r.referral_state == 'hired')
|
|
applicant_name = {applicant_hired.friend_id.id: applicant_hired.partner_name for applicant_hired in applicants_hired}
|
|
applicant_without_friend = applicants_hired.filtered(lambda r: not r.friend_id)
|
|
|
|
# If there are applicant hired without friend and available friends.
|
|
available_friend_count = self.env['hr.referral.friend'].search_count([])
|
|
if bool(applicant_without_friend) and (len(applicants_hired) - len(applicant_without_friend) < available_friend_count):
|
|
result['choose_new_friend'] = True
|
|
result['new_friend_name'] = applicant_without_friend[0].partner_name
|
|
result['new_friend_id'] = applicant_without_friend[0].id
|
|
|
|
result['friends'] = self._get_friends_head(applicant_name)
|
|
return result
|
|
|
|
result['friends'] = self._get_friends(applicant_name)
|
|
|
|
referal_points = self.env['hr.referral.points'].search([('ref_user_id', '=', user_id.id)])
|
|
result['point_received'] = sum(referal_points.filtered(lambda x: not x.hr_referral_reward_id).mapped('points'))
|
|
result['point_to_spend'] = sum(referal_points.mapped('points'))
|
|
|
|
# Employee comes for the first time on this app
|
|
if not user_id.hr_referral_level_id:
|
|
user_id.hr_referral_level_id = self.env['hr.referral.level'].search([], order='points asc', limit=1).id
|
|
|
|
current_level = user_id.hr_referral_level_id
|
|
next_level = self.env['hr.referral.level'].search([('points', '>', current_level.points)], order='points asc', limit=1)
|
|
|
|
result['level'] = {
|
|
'image': current_level.image,
|
|
'name': current_level.name,
|
|
'points': current_level.points
|
|
}
|
|
|
|
# Next referral levels
|
|
result['level_percentage'] = 100
|
|
if next_level:
|
|
if result['point_received'] >= next_level['points']:
|
|
result['reach_new_level'] = True
|
|
step_level = next_level['points'] - current_level['points']
|
|
result['level_percentage'] = round((min(result['point_received'], next_level['points']) - current_level['points']) * 100 / step_level)
|
|
|
|
result['referral'] = {
|
|
'all': len(applicant),
|
|
'hired': len(applicant.filtered(lambda r: r.referral_state == 'hired')),
|
|
'progress': len(applicant.filtered(lambda r: r.referral_state == 'progress')),
|
|
}
|
|
|
|
today = fields.Date.today()
|
|
messages = self.env['hr.referral.alert'].search([
|
|
('active', '=', True), ('dismissed_user_ids', 'not in', self.env.user.id),
|
|
'|', ('date_from', '<=', today), ('date_from', '=', False),
|
|
'|', ('date_to', '>', today), ('date_to', '=', False)])
|
|
|
|
result['message'] = []
|
|
for message in messages:
|
|
msg = {'id': message.id,
|
|
'text': message.name}
|
|
if message.onclick == 'url':
|
|
msg['url'] = message.url
|
|
elif message.onclick == 'all_jobs':
|
|
msg['url'] = '/odoo/action-hr_referral.action_hr_job_employee_referral'
|
|
result['message'].append(msg)
|
|
|
|
return result
|
|
|
|
@api.model
|
|
def upgrade_level(self):
|
|
if not self.env.user:
|
|
return
|
|
user_id = self.env.user
|
|
user_points = sum(self.env['hr.referral.points'].search([
|
|
('ref_user_id', '=', user_id.id),
|
|
('hr_referral_reward_id', '=', False)]).mapped('points'))
|
|
next_referral_level = self.env['hr.referral.level'].search([
|
|
('points', '>', user_id.hr_referral_level_id.points),
|
|
('points', '<=', user_points)
|
|
], order='points asc', limit=1)
|
|
if next_referral_level:
|
|
user_id.write({'hr_referral_level_id': next_referral_level.id})
|
|
|
|
@api.model
|
|
def default_get(self, fields):
|
|
# To set the source_id as "Referral" instead of utm_source that was generated for every individual referrer
|
|
values = super().default_get(fields)
|
|
utm_source_referral = self.env.ref('utm.utm_source_referral', raise_if_not_found=False)
|
|
if (
|
|
'ref_user_id' in fields
|
|
and (utm_source := values.get('source_id'))
|
|
and (user := self.env['res.users'].search([('utm_source_id', '=', utm_source)], limit=1))
|
|
):
|
|
values['ref_user_id'] = user.id
|
|
values['source_id'] = utm_source_referral.id if utm_source_referral else False
|
|
return values
|
|
|
|
def reset_applicant(self):
|
|
""" Reset the applicant state to progress """
|
|
super().reset_applicant()
|
|
self.write({'referral_state': 'progress'})
|
|
|
|
|
|
class HrRecruitmentStage(models.Model):
|
|
_inherit = "hr.recruitment.stage"
|
|
|
|
points = fields.Integer('Points', help="Amount of points that the referent will receive when the applicant will reach this stage")
|
|
use_in_referral = fields.Boolean('Show in Referrals', help="This option is used in app 'Referrals'. If checked, the stage is displayed in 'Referrals Dashboard' and points are given to the employee.")
|