302 lines
14 KiB
Python
302 lines
14 KiB
Python
from email.policy import default
|
|
|
|
from odoo import models, fields, api, _
|
|
from dateutil.relativedelta import relativedelta
|
|
from datetime import datetime
|
|
|
|
from odoo.exceptions import ValidationError
|
|
|
|
|
|
class HRApplicant(models.Model):
|
|
_inherit = 'hr.applicant'
|
|
_track_duration_field = 'recruitment_stage_id'
|
|
|
|
|
|
candidate_image = fields.Image(related='candidate_id.candidate_image', readonly=False, compute_sudo=True)
|
|
submitted_to_client = fields.Boolean(string="Submitted_to_client", default=False, readonly=True, tracking=True)
|
|
client_submission_date = fields.Datetime(string="Submission Date")
|
|
|
|
@api.model
|
|
def _read_group_recruitment_stage_ids(self, stages, domain):
|
|
# retrieve job_id from the context and write the domain: ids + contextual columns (job or default)
|
|
job_recruitment_id = self._context.get('default_hr_job_recruitment')
|
|
|
|
search_domain = []
|
|
if job_recruitment_id:
|
|
search_domain = [('job_recruitment_ids', '=', job_recruitment_id)] + search_domain
|
|
# if stages:
|
|
# search_domain = [('id', 'in', stages.ids)] + search_domain
|
|
|
|
stage_ids = stages.sudo()._search(search_domain, order=stages._order)
|
|
return stages.browse(stage_ids)
|
|
|
|
def write(self, vals):
|
|
# user_id change: update date_open
|
|
res = super().write(vals)
|
|
if vals.get('user_id'):
|
|
vals['date_open'] = fields.Datetime.now()
|
|
old_interviewers = self.interviewer_ids
|
|
# stage_id: track last stage before update
|
|
if 'recruitment_stage_id' in vals:
|
|
vals['date_last_stage_update'] = fields.Datetime.now()
|
|
if 'kanban_state' not in vals:
|
|
vals['kanban_state'] = 'normal'
|
|
for applicant in self:
|
|
vals['last_stage_id'] = applicant.recruitment_stage_id.id
|
|
|
|
return res
|
|
|
|
|
|
@api.depends('hr_job_recruitment')
|
|
def _compute_department(self):
|
|
for applicant in self:
|
|
applicant.department_id = applicant.hr_job_recruitment.department_id.id
|
|
|
|
@api.depends('hr_job_recruitment')
|
|
def _compute_stage(self):
|
|
for applicant in self:
|
|
if applicant.hr_job_recruitment:
|
|
if not applicant.recruitment_stage_id:
|
|
stage_ids = self.env['hr.recruitment.stage'].search([
|
|
'|',
|
|
('job_recruitment_ids', '=', False),
|
|
('job_recruitment_ids', '=', applicant.hr_job_recruitment.id),
|
|
('fold', '=', False)
|
|
], order='sequence asc', limit=1).ids
|
|
applicant.recruitment_stage_id = stage_ids[0] if stage_ids else False
|
|
else:
|
|
applicant.recruitment_stage_id = False
|
|
|
|
@api.depends('job_id')
|
|
def _compute_user(self):
|
|
for applicant in self:
|
|
applicant.user_id = applicant.hr_job_recruitment.user_id.id
|
|
|
|
|
|
def init(self):
|
|
super().init()
|
|
self.env.cr.execute("""
|
|
CREATE INDEX IF NOT EXISTS hr_applicant_job_id_recruitment_stage_id_idx
|
|
ON hr_applicant(job_id, recruitment_stage_id)
|
|
WHERE active IS TRUE
|
|
""")
|
|
|
|
|
|
refused_state = fields.Many2one('hr.recruitment.stage', readonly=True, force_save=True)
|
|
hr_job_recruitment = fields.Many2one('hr.job.recruitment')
|
|
job_id = fields.Many2one('hr.job', related='hr_job_recruitment.job_id', store=True)
|
|
|
|
recruitment_stage_id = fields.Many2one('hr.recruitment.stage', 'Stage', ondelete='restrict', tracking=True,
|
|
compute='_compute_recruitment_stage', store=True, readonly=False,
|
|
domain="[('job_recruitment_ids', '=', hr_job_recruitment)]",
|
|
copy=False, index=True,
|
|
group_expand='_read_group_recruitment_stage_ids')
|
|
stage_color = fields.Char(related="recruitment_stage_id.stage_color")
|
|
|
|
send_second_application_form = fields.Boolean(related='recruitment_stage_id.second_application_form')
|
|
second_application_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
|
send_post_onboarding_form = fields.Boolean(related='recruitment_stage_id.post_onboarding_form')
|
|
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
|
|
|
legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked')
|
|
legend_done = fields.Char(related='recruitment_stage_id.legend_done', string='Kanban Valid')
|
|
legend_normal = fields.Char(related='recruitment_stage_id.legend_normal', string='Kanban Ongoing')
|
|
# holding_offer = fields.HTML()
|
|
employee_code = fields.Char(related="employee_id.employee_id")
|
|
|
|
recruitment_attachments = fields.Many2many(
|
|
'recruitment.attachments',
|
|
string='Attachments Request')
|
|
|
|
joining_attachment_ids = fields.One2many('employee.recruitment.attachments','applicant_id',string="Attachments")
|
|
|
|
attachments_validation_status = fields.Selection([('pending', 'Pending'),
|
|
('validated', 'Validated')], default='pending')
|
|
|
|
approval_required = fields.Boolean(related='recruitment_stage_id.require_approval')
|
|
application_submitted = fields.Boolean(string="Application Submitted")
|
|
resume = fields.Binary(related='candidate_id.resume', readonly=False, compute_sudo=True)
|
|
|
|
|
|
def submit_to_client(self):
|
|
for rec in self:
|
|
submitted_count = len(self.sudo().search([('id','!=',rec.id),('submitted_to_client','=',True)]).ids)
|
|
if submitted_count >= rec.hr_job_recruitment.no_of_eligible_submissions:
|
|
raise ValidationError(_("Max no of submissions for this JD has been reached"))
|
|
rec.submitted_to_client = True
|
|
rec.client_submission_date = fields.Datetime.now()
|
|
|
|
def submit_for_approval(self):
|
|
for rec in self:
|
|
manager_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_manager')
|
|
if not manager_id:
|
|
raise ValidationError(_("Recruitment Manager is not selected please go into the Configuration->Settings and add the Manager"))
|
|
mail_template = self.env.ref('hr_recruitment_extended.email_template_candidate_approval')
|
|
# menu_id = self.env.ref('hr_recruitment.menu_crm_case_categ0_act_job').id
|
|
manager_id = self.env['res.users'].sudo().browse(int(manager_id))
|
|
render_ctx = dict(recruitment_manager=manager_id)
|
|
mail_template.with_context(render_ctx).send_mail(
|
|
self.id,
|
|
force_send=True,
|
|
email_layout_xmlid='mail.mail_notification_light')
|
|
rec.application_submitted = True
|
|
|
|
def approve_applicant(self):
|
|
for rec in self:
|
|
manager_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_manager')
|
|
if not manager_id:
|
|
raise ValidationError(
|
|
_("Recruitment Manager is not selected please go into the Configuration->Settings and add the Manager"))
|
|
mail_template = self.env.ref('hr_recruitment_extended.email_template_stage_approved')
|
|
# menu_id = self.env.ref('hr_recruitment.menu_crm_case_categ0_act_job').id
|
|
manager_id = self.env['res.users'].sudo().browse(int(manager_id))
|
|
render_ctx = dict(recruitment_manager=manager_id)
|
|
mail_template.with_context(render_ctx).send_mail(
|
|
self.id,
|
|
force_send=True,
|
|
email_layout_xmlid='mail.mail_notification_light')
|
|
rec.application_submitted = False
|
|
recruitment_stage_ids = rec.hr_job_recruitment.recruitment_stage_ids.ids
|
|
current_stage = self.env['hr.recruitment.stage'].browse(rec.recruitment_stage_id.id)
|
|
next_stage = self.env['hr.recruitment.stage'].search([
|
|
('id', 'in', recruitment_stage_ids),
|
|
('sequence', '>', current_stage.sequence)
|
|
], order='sequence asc', limit=1)
|
|
if next_stage:
|
|
rec.recruitment_stage_id = next_stage.id
|
|
|
|
def action_validate_attachments(self):
|
|
for rec in self:
|
|
if rec.employee_id and rec.joining_attachment_ids:
|
|
rec.joining_attachment_ids.write({'employee_id': rec.employee_id.id})
|
|
rec.attachments_validation_status = 'validated'
|
|
else:
|
|
raise ValidationError(_("No Data to Validate"))
|
|
|
|
def send_second_application_form_to_candidate(self):
|
|
"""Send the salary expectation and experience form to the candidate."""
|
|
template = self.env.ref('hr_recruitment_extended.email_template_second_application_form', raise_if_not_found=False)
|
|
for applicant in self:
|
|
if template and applicant.email_from:
|
|
template.send_mail(applicant.id, force_send=True)
|
|
applicant.second_application_form_status = 'email_sent_to_candidate'
|
|
|
|
def send_post_onboarding_form_to_candidate(self):
|
|
for rec in self:
|
|
if not rec.employee_id:
|
|
raise ValidationError(_('You must first create the employee before before Sending the Post Onboarding Form'))
|
|
|
|
elif not rec.employee_id.employee_id:
|
|
raise ValidationError(_('Employee Code for the Employee (%s) is missing')%(rec.employee_id.name))
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': 'Select Attachments',
|
|
'res_model': 'post.onboarding.attachment.wizard',
|
|
'view_mode': 'form',
|
|
'view_type': 'form',
|
|
'target': 'new',
|
|
'context': {'default_attachment_ids': []}
|
|
}
|
|
|
|
|
|
|
|
def _track_template(self, changes):
|
|
res = super(HRApplicant, self)._track_template(changes)
|
|
applicant = self[0]
|
|
# When applcant is unarchived, they are put back to the default stage automatically. In this case,
|
|
# don't post automated message related to the stage change.
|
|
if 'recruitment_stage_id' in changes and applicant.exists()\
|
|
and applicant.recruitment_stage_id.template_id\
|
|
and not applicant._context.get('just_moved')\
|
|
and not applicant._context.get('just_unarchived'):
|
|
res['recruitment_stage_id'] = (applicant.recruitment_stage_id.template_id, {
|
|
'auto_delete_keep_log': False,
|
|
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
|
'email_layout_xmlid': 'hr_recruitment.mail_notification_light_without_background'
|
|
})
|
|
return res
|
|
def _track_subtype(self, init_values):
|
|
record = self[0]
|
|
if 'recruitment_stage_id' in init_values and record.recruitment_stage_id:
|
|
return self.env.ref('hr_recruitment.mt_applicant_stage_changed')
|
|
return super(HRApplicant, self)._track_subtype(init_values)
|
|
|
|
def message_new(self, msg, custom_values=None):
|
|
stage = False
|
|
defaults = {}
|
|
if custom_values and 'hr_job_recruitment' in custom_values:
|
|
recruitment_stage_id = self.env['hr.job.recruitment'].browse(custom_values['hr_job_recruitment'])._get_first_stage()
|
|
if stage and stage.id:
|
|
defaults['recruitment_stage_id'] = recruitment_stage_id.id
|
|
res = super(HRApplicant, self).message_new(msg, custom_values=defaults)
|
|
return res
|
|
|
|
def reset_applicant(self):
|
|
""" Reinsert the applicant into the recruitment pipe in the first stage"""
|
|
default_stage = dict()
|
|
for hr_job_recruitment in self.mapped('hr_job_recruitment'):
|
|
default_stage[hr_job_recruitment.id] = self.env['hr.recruitment.stage'].search(
|
|
[
|
|
('job_recruitment_ids', '=', hr_job_recruitment.id),
|
|
('fold', '=', False)
|
|
], order='sequence asc', limit=1).id
|
|
for applicant in self:
|
|
applicant.write(
|
|
{'recruitment_stage_id': applicant.hr_job_recruitment.id and default_stage[applicant.hr_job_recruitment.id],
|
|
'refuse_reason_id': False})
|
|
|
|
@api.depends('recruitment_stage_id.hired_stage')
|
|
def _compute_date_closed(self):
|
|
for applicant in self:
|
|
if applicant.recruitment_stage_id and applicant.recruitment_stage_id.hired_stage and not applicant.date_closed:
|
|
applicant.date_closed = fields.datetime.now()
|
|
if not applicant.recruitment_stage_id.hired_stage:
|
|
applicant.date_closed = False
|
|
|
|
@api.depends('hr_job_recruitment')
|
|
def _compute_recruitment_stage(self):
|
|
for applicant in self:
|
|
if applicant.hr_job_recruitment:
|
|
if not applicant.recruitment_stage_id:
|
|
stage_ids = self.env['hr.recruitment.stage'].search([
|
|
'|',
|
|
('job_recruitment_ids', '=', False),
|
|
('job_recruitment_ids', '=', applicant.hr_job_recruitment.id),
|
|
('fold', '=', False)
|
|
], order='sequence asc', limit=1).ids
|
|
applicant.recruitment_stage_id = stage_ids[0] if stage_ids else False
|
|
else:
|
|
applicant.recruitment_stage_id = False
|
|
|
|
def _get_duration_from_tracking(self, trackings):
|
|
json = super()._get_duration_from_tracking(trackings)
|
|
now = datetime.now()
|
|
for applicant in self:
|
|
if applicant.refuse_reason_id and applicant.refuse_date:
|
|
json[applicant.recruitment_stage_id.id] -= (now - applicant.refuse_date).total_seconds()
|
|
return json
|
|
|
|
def create_employee_from_applicant(self):
|
|
self.ensure_one()
|
|
action = self.candidate_id.create_employee_from_candidate()
|
|
employee = self.env['hr.employee'].browse(action['res_id'])
|
|
employee.write({
|
|
'image_1920': self.candidate_image,
|
|
'job_id': self.job_id.id,
|
|
'job_title': self.job_id.name,
|
|
'department_id': self.department_id.id,
|
|
'work_email': self.department_id.company_id.email or self.email_from, # To have a valid email address by default
|
|
'work_phone': self.department_id.company_id.phone,
|
|
})
|
|
return action
|
|
|
|
class ApplicantGetRefuseReason(models.TransientModel):
|
|
_inherit = 'applicant.get.refuse.reason'
|
|
|
|
|
|
def action_refuse_reason_apply(self):
|
|
res = super(ApplicantGetRefuseReason, self).action_refuse_reason_apply()
|
|
refused_applications = self.applicant_ids
|
|
refused_applications.write({'refused_state': refused_applications.stage_id.id})
|
|
return res
|