flectra/addons/hr_recruitment/models/hr_recruitment.py

460 lines
22 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2018-01-16 11:34:37 +01:00
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from datetime import datetime
2018-01-16 11:34:37 +01:00
from flectra import api, fields, models, tools, SUPERUSER_ID
from flectra.tools.translate import _
from flectra.exceptions import UserError
AVAILABLE_PRIORITIES = [
('0', 'Normal'),
('1', 'Good'),
('2', 'Very Good'),
('3', 'Excellent')
]
class RecruitmentSource(models.Model):
_name = "hr.recruitment.source"
_description = "Source of Applicants"
_inherits = {"utm.source": "source_id"}
source_id = fields.Many2one('utm.source', "Source", ondelete='cascade', required=True)
email = fields.Char(related='alias_id.display_name', string="Email", readonly=True)
job_id = fields.Many2one('hr.job', "Job ID")
alias_id = fields.Many2one('mail.alias', "Alias ID")
@api.multi
def create_alias(self):
campaign = self.env.ref('hr_recruitment.utm_campaign_job')
medium = self.env.ref('utm.utm_medium_email')
for source in self:
vals = {
'alias_parent_thread_id': source.job_id.id,
'alias_name': "%s+%s" % (source.job_id.alias_name or source.job_id.name, source.name),
'alias_defaults': {
'job_id': source.job_id.id,
'campaign_id': campaign.id,
'medium_id': medium.id,
'source_id': source.source_id.id,
},
}
source.alias_id = self.with_context(alias_model_name='hr.applicant', alias_parent_model_name='hr.job').env['mail.alias'].create(vals)
source.name = source.source_id.name
class RecruitmentStage(models.Model):
_name = "hr.recruitment.stage"
_description = "Stage of Recruitment"
_order = 'sequence'
name = fields.Char("Stage name", required=True, translate=True)
sequence = fields.Integer(
"Sequence", default=10,
help="Gives the sequence order when displaying a list of stages.")
job_id = fields.Many2one('hr.job', string='Job Specific',
ondelete='cascade',
help='Specific job that uses this stage. Other jobs will not use this stage.')
requirements = fields.Text("Requirements")
template_id = fields.Many2one(
'mail.template', "Automated Email",
help="If set, a message is posted on the applicant using the template when the applicant is set to the stage.")
fold = fields.Boolean(
"Folded in Recruitment Pipe",
help="This stage is folded in the kanban view when there are no records in that stage to display.")
@api.model
def default_get(self, fields):
if self._context and self._context.get('default_job_id') and not self._context.get('hr_recruitment_stage_mono', False):
context = dict(self._context)
context.pop('default_job_id')
self = self.with_context(context)
return super(RecruitmentStage, self).default_get(fields)
class RecruitmentDegree(models.Model):
_name = "hr.recruitment.degree"
_description = "Degree of Recruitment"
_sql_constraints = [
('name_uniq', 'unique (name)', 'The name of the Degree of Recruitment must be unique!')
]
name = fields.Char("Degree", required=True, translate=True)
sequence = fields.Integer("Sequence", default=1, help="Gives the sequence order when displaying a list of degrees.")
class Applicant(models.Model):
_name = "hr.applicant"
_description = "Applicant"
_order = "priority desc, id desc"
_inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin']
def _default_stage_id(self):
if self._context.get('default_job_id'):
ids = self.env['hr.recruitment.stage'].search([
'|',
('job_id', '=', False),
('job_id', '=', self._context['default_job_id']),
('fold', '=', False)
], order='sequence asc', limit=1).ids
if ids:
return ids[0]
return False
def _default_company_id(self):
company_id = False
if self._context.get('default_department_id'):
department = self.env['hr.department'].browse(self._context['default_department_id'])
company_id = department.company_id.id
if not company_id:
company_id = self.env['res.company']._company_default_get('hr.applicant')
return company_id
name = fields.Char("Subject / Application Name", required=True)
active = fields.Boolean("Active", default=True, help="If the active field is set to false, it will allow you to hide the case without removing it.")
description = fields.Text("Description")
email_from = fields.Char("Email", size=128, help="These people will receive email.")
email_cc = fields.Text("Watchers Emails", size=252,
help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma")
probability = fields.Float("Probability")
partner_id = fields.Many2one('res.partner', "Contact")
create_date = fields.Datetime("Creation Date", readonly=True, index=True)
write_date = fields.Datetime("Update Date", readonly=True)
stage_id = fields.Many2one('hr.recruitment.stage', 'Stage', track_visibility='onchange',
domain="['|', ('job_id', '=', False), ('job_id', '=', job_id)]",
copy=False, index=True,
group_expand='_read_group_stage_ids',
default=_default_stage_id)
last_stage_id = fields.Many2one('hr.recruitment.stage', "Last Stage",
help="Stage of the applicant before being in the current stage. Used for lost cases analysis.")
categ_ids = fields.Many2many('hr.applicant.category', string="Tags")
company_id = fields.Many2one('res.company', "Company", default=_default_company_id)
user_id = fields.Many2one('res.users', "Responsible", track_visibility="onchange", default=lambda self: self.env.uid)
date_closed = fields.Datetime("Closed", readonly=True, index=True)
date_open = fields.Datetime("Assigned", readonly=True, index=True)
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")
salary_proposed_extra = fields.Char("Proposed Salary Extra", help="Salary Proposed by the Organisation, extra advantages")
salary_expected_extra = fields.Char("Expected Salary Extra", help="Salary Expected by Applicant, extra advantages")
salary_proposed = fields.Float("Proposed Salary", group_operator="avg", help="Salary Proposed by the Organisation")
salary_expected = fields.Float("Expected Salary", group_operator="avg", help="Salary Expected by Applicant")
availability = fields.Date("Availability", help="The date at which the applicant will be available to start working")
partner_name = fields.Char("Applicant's Name")
partner_phone = fields.Char("Phone", size=32)
partner_mobile = fields.Char("Mobile", size=32)
type_id = fields.Many2one('hr.recruitment.degree', "Degree")
department_id = fields.Many2one('hr.department', "Department")
reference = fields.Char("Referred By")
day_open = fields.Float(compute='_compute_day', string="Days to Open")
day_close = fields.Float(compute='_compute_day', string="Days to Close")
delay_close = fields.Float(compute="_compute_day", string='Delay to Close', readonly=True, group_operator="avg", help="Number of days to close", store=True)
color = fields.Integer("Color Index", default=0)
emp_id = fields.Many2one('hr.employee', string="Employee", track_visibility="onchange", help="Employee linked to the applicant.")
user_email = fields.Char(related='user_id.email', type="char", string="User Email", readonly=True)
attachment_number = fields.Integer(compute='_get_attachment_number', string="Number of Attachments")
employee_name = fields.Char(related='emp_id.name', string="Employee Name")
attachment_ids = fields.One2many('ir.attachment', 'res_id', domain=[('res_model', '=', 'hr.applicant')], string='Attachments')
@api.depends('date_open', 'date_closed')
@api.one
def _compute_day(self):
if self.date_open:
date_create = datetime.strptime(self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_open = datetime.strptime(self.date_open, tools.DEFAULT_SERVER_DATETIME_FORMAT)
self.day_open = (date_open - date_create).total_seconds() / (24.0 * 3600)
if self.date_closed:
date_create = datetime.strptime(self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_closed = datetime.strptime(self.date_closed, tools.DEFAULT_SERVER_DATETIME_FORMAT)
self.day_close = (date_closed - date_create).total_seconds() / (24.0 * 3600)
self.delay_close = self.day_close - self.day_open
@api.multi
def _get_attachment_number(self):
read_group_res = self.env['ir.attachment'].read_group(
[('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.ids)],
['res_id'], ['res_id'])
attach_data = dict((res['res_id'], res['res_id_count']) for res in read_group_res)
for record in self:
record.attachment_number = attach_data.get(record.id, 0)
@api.model
def _read_group_stage_ids(self, stages, domain, order):
# retrieve job_id from the context and write the domain: ids + contextual columns (job or default)
job_id = self._context.get('default_job_id')
search_domain = [('job_id', '=', False)]
if job_id:
search_domain = ['|', ('job_id', '=', job_id)] + search_domain
if stages:
search_domain = ['|', ('id', 'in', stages.ids)] + search_domain
stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID)
return stages.browse(stage_ids)
@api.onchange('job_id')
def onchange_job_id(self):
vals = self._onchange_job_id_internal(self.job_id.id)
self.department_id = vals['value']['department_id']
self.user_id = vals['value']['user_id']
self.stage_id = vals['value']['stage_id']
def _onchange_job_id_internal(self, job_id):
department_id = False
user_id = False
stage_id = self.stage_id.id
if job_id:
job = self.env['hr.job'].browse(job_id)
department_id = job.department_id.id
user_id = job.user_id.id
if not self.stage_id:
stage_ids = self.env['hr.recruitment.stage'].search([
'|',
('job_id', '=', False),
('job_id', '=', job.id),
('fold', '=', False)
], order='sequence asc', limit=1).ids
stage_id = stage_ids[0] if stage_ids else False
return {'value': {
'department_id': department_id,
'user_id': user_id,
'stage_id': stage_id
}}
@api.onchange('partner_id')
def onchange_partner_id(self):
self.partner_phone = self.partner_id.phone
self.partner_mobile = self.partner_id.mobile
self.email_from = self.partner_id.email
@api.onchange('stage_id')
def onchange_stage_id(self):
vals = self._onchange_stage_id_internal(self.stage_id.id)
if vals['value'].get('date_closed'):
self.date_closed = vals['value']['date_closed']
def _onchange_stage_id_internal(self, stage_id):
if not stage_id:
return {'value': {}}
stage = self.env['hr.recruitment.stage'].browse(stage_id)
if stage.fold:
return {'value': {'date_closed': fields.datetime.now()}}
return {'value': {'date_closed': False}}
@api.model
def create(self, vals):
if vals.get('department_id') and not self._context.get('default_department_id'):
self = self.with_context(default_department_id=vals.get('department_id'))
if vals.get('job_id') or self._context.get('default_job_id'):
job_id = vals.get('job_id') or self._context.get('default_job_id')
for key, value in self._onchange_job_id_internal(job_id)['value'].items():
if key not in vals:
vals[key] = value
if vals.get('user_id'):
vals['date_open'] = fields.Datetime.now()
if 'stage_id' in vals:
vals.update(self._onchange_stage_id_internal(vals.get('stage_id'))['value'])
return super(Applicant, self.with_context(mail_create_nolog=True)).create(vals)
@api.multi
def write(self, vals):
# user_id change: update date_open
if vals.get('user_id'):
vals['date_open'] = fields.Datetime.now()
# stage_id: track last stage before update
if 'stage_id' in vals:
vals['date_last_stage_update'] = fields.Datetime.now()
vals.update(self._onchange_stage_id_internal(vals.get('stage_id'))['value'])
for applicant in self:
vals['last_stage_id'] = applicant.stage_id.id
res = super(Applicant, self).write(vals)
else:
res = super(Applicant, self).write(vals)
return res
@api.model
def get_empty_list_help(self, help):
return super(Applicant, self.with_context(empty_list_help_model='hr.job',
empty_list_help_id=self.env.context.get('default_job_id'),
empty_list_help_document_name=_("job applicants"))).get_empty_list_help(help)
@api.multi
def action_get_created_employee(self):
self.ensure_one()
action = self.env['ir.actions.act_window'].for_xml_id('hr', 'open_view_employee_list')
action['res_id'] = self.mapped('emp_id').ids[0]
return action
@api.multi
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()
partners = self.partner_id | self.user_id.partner_id | self.department_id.manager_id.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')
res['context'] = {
'search_default_partner_ids': self.partner_id.name,
'default_partner_ids': partners.ids,
'default_user_id': self.env.uid,
'default_name': self.name,
'default_categ_ids': category and [category.id] or False,
}
return res
@api.multi
def action_get_attachment_tree_view(self):
attachment_action = self.env.ref('base.action_attachment')
action = attachment_action.read()[0]
action['context'] = {'default_res_model': self._name, 'default_res_id': self.ids[0]}
action['domain'] = str(['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)])
action['search_view_id'] = (self.env.ref('hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment').id, )
return action
@api.multi
def _track_template(self, tracking):
res = super(Applicant, self)._track_template(tracking)
applicant = self[0]
changes, dummy = tracking[applicant.id]
if 'stage_id' in changes and applicant.stage_id.template_id:
res['stage_id'] = (applicant.stage_id.template_id, {'composition_mode': 'mass_mail'})
return res
@api.multi
def _track_subtype(self, init_values):
record = self[0]
if 'emp_id' in init_values and record.emp_id and record.emp_id.active:
return 'hr_recruitment.mt_applicant_hired'
elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence <= 1:
return 'hr_recruitment.mt_applicant_new'
elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence > 1:
return 'hr_recruitment.mt_applicant_stage_changed'
return super(Applicant, self)._track_subtype(init_values)
@api.model
def message_get_reply_to(self, ids, default=None):
""" Override to get the reply_to of the parent project. """
applicants = self.sudo().browse(ids)
aliases = self.env['hr.job'].message_get_reply_to(applicants.mapped('job_id').ids, default=default)
return dict((applicant.id, aliases.get(applicant.job_id and applicant.job_id.id or 0, False)) for applicant in applicants)
@api.multi
def message_get_suggested_recipients(self):
recipients = super(Applicant, self).message_get_suggested_recipients()
for applicant in self:
if applicant.partner_id:
applicant._message_add_suggested_recipient(recipients, partner=applicant.partner_id, reason=_('Contact'))
elif applicant.email_from:
applicant._message_add_suggested_recipient(recipients, email=applicant.email_from, reason=_('Contact Email'))
return recipients
@api.model
def message_new(self, msg, custom_values=None):
""" Overrides mail_thread message_new that is called by the mailgateway
through message_process.
This override updates the document according to the email.
"""
# remove default author when going through the mail gateway. Indeed we
# do not want to explicitly set user_id to False; however we do not
# want the gateway user to be responsible if no other responsible is
# found.
self = self.with_context(default_user_id=False)
val = msg.get('from').split('<')[0]
defaults = {
'name': msg.get('subject') or _("No Subject"),
'partner_name': val,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'partner_id': msg.get('author_id', False),
}
if msg.get('priority'):
defaults['priority'] = msg.get('priority')
if custom_values:
defaults.update(custom_values)
return super(Applicant, self).message_new(msg, custom_values=defaults)
def _message_post_after_hook(self, message):
if self.email_from and not self.partner_id:
# we consider that posting a message with a specified recipient (not a follower, a specific one)
# on a document without customer means that it was created through the chatter using
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.email_from)
if new_partner:
self.search([
('partner_id', '=', False),
('email_from', '=', new_partner.email),
('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id})
return super(Applicant, self)._message_post_after_hook(message)
@api.multi
def create_employee_from_applicant(self):
""" Create an hr.employee from the hr.applicants """
employee = False
for applicant in self:
contact_name = False
if applicant.partner_id:
address_id = applicant.partner_id.address_get(['contact'])['contact']
contact_name = applicant.partner_id.name_get()[0][1]
else :
new_partner_id = self.env['res.partner'].create({
'is_company': False,
'name': applicant.partner_name,
'email': applicant.email_from,
'phone': applicant.partner_phone,
'mobile': applicant.partner_mobile
})
address_id = new_partner_id.address_get(['contact'])['contact']
if applicant.job_id and (applicant.partner_name or contact_name):
applicant.job_id.write({'no_of_hired_employee': applicant.job_id.no_of_hired_employee + 1})
employee = self.env['hr.employee'].create({
'name': applicant.partner_name or contact_name,
'job_id': applicant.job_id.id,
'address_home_id': address_id,
'department_id': applicant.department_id.id or False,
'address_id': applicant.company_id and applicant.company_id.partner_id
and applicant.company_id.partner_id.id or False,
'work_email': applicant.department_id and applicant.department_id.company_id
and applicant.department_id.company_id.email or False,
'work_phone': applicant.department_id and applicant.department_id.company_id
and applicant.department_id.company_id.phone or False})
applicant.write({'emp_id': employee.id})
applicant.job_id.message_post(
body=_('New Employee %s Hired') % applicant.partner_name if applicant.partner_name else applicant.name,
subtype="hr_recruitment.mt_job_applicant_hired")
employee._broadcast_welcome()
else:
raise UserError(_('You must define an Applied Job and a Contact Name for this applicant.'))
employee_action = self.env.ref('hr.open_view_employee_list')
dict_act_window = employee_action.read([])[0]
if employee:
dict_act_window['res_id'] = employee.id
dict_act_window['view_mode'] = 'form,tree'
return dict_act_window
@api.multi
def archive_applicant(self):
self.write({'active': False})
@api.multi
def reset_applicant(self):
""" Reinsert the applicant into the recruitment pipe in the first stage"""
default_stage_id = self._default_stage_id()
self.write({'active': True, 'stage_id': default_stage_id})
class ApplicantCategory(models.Model):
_name = "hr.applicant.category"
_description = "Category of applicant"
name = fields.Char("Name", required=True)
color = fields.Integer(string='Color Index', default=10)
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
]