mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-25 22:32:06 +02:00
19.0 vanilla
This commit is contained in:
parent
a1137a1456
commit
e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions
|
|
@ -1,11 +1,24 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import hr_department
|
||||
from . import hr_recruitment
|
||||
from . import hr_applicant
|
||||
from . import hr_applicant_category
|
||||
from . import hr_applicant_refuse_reason
|
||||
from . import hr_recruitment_degree
|
||||
from . import hr_recruitment_source
|
||||
from . import hr_recruitment_stage
|
||||
from . import hr_employee
|
||||
from . import hr_job
|
||||
from . import hr_talent_pool
|
||||
from . import res_config_settings
|
||||
from . import calendar
|
||||
from . import digest
|
||||
from . import utm_campaign
|
||||
from . import utm_source
|
||||
from . import res_company
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
from . import ir_attachment
|
||||
from . import ir_ui_menu
|
||||
from . import mail_activity_plan
|
||||
from . import hr_job_platform
|
||||
|
|
|
|||
|
|
@ -5,37 +5,57 @@ from odoo import api, fields, models
|
|||
|
||||
|
||||
class CalendarEvent(models.Model):
|
||||
""" Model for Calendar Event """
|
||||
_inherit = 'calendar.event'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
self_ctx = self
|
||||
if self.env.context.get('default_applicant_id'):
|
||||
self = self.with_context(
|
||||
default_res_model='hr.applicant', #res_model seems to be lost without this
|
||||
self_ctx = self.with_context(
|
||||
default_res_model='hr.applicant', # res_model seems to be lost without this
|
||||
default_res_model_id=self.env.ref('hr_recruitment.model_hr_applicant').id,
|
||||
default_res_id=self.env.context.get('default_applicant_id'),
|
||||
default_partner_ids=self.env.context.get('default_partner_ids'),
|
||||
default_name=self.env.context.get('default_name')
|
||||
)
|
||||
|
||||
defaults = super(CalendarEvent, self).default_get(fields)
|
||||
defaults = super(CalendarEvent, self_ctx).default_get(fields)
|
||||
|
||||
# sync res_model / res_id to opportunity id (aka creating meeting from lead chatter)
|
||||
if 'applicant_id' not in defaults:
|
||||
res_model = defaults.get('res_model', False) or self.env.context.get('default_res_model')
|
||||
res_model_id = defaults.get('res_model_id', False) or self.env.context.get('default_res_model_id')
|
||||
if (res_model and res_model == 'hr.applicant') or (res_model_id and self.env['ir.model'].sudo().browse(res_model_id).model == 'hr.applicant'):
|
||||
defaults['applicant_id'] = defaults.get('res_id', False) or self.env.context.get('default_res_id', False)
|
||||
res_model = defaults.get('res_model', False) or self_ctx.env.context.get('default_res_model')
|
||||
res_model_id = defaults.get('res_model_id', False) or self_ctx.env.context.get('default_res_model_id')
|
||||
if (res_model and res_model == 'hr.applicant') or (res_model_id and self_ctx.env['ir.model'].sudo().browse(res_model_id).model == 'hr.applicant'):
|
||||
defaults['applicant_id'] = defaults.get('res_id', False) or self_ctx.env.context.get('default_res_id', False)
|
||||
|
||||
return defaults
|
||||
|
||||
applicant_id = fields.Many2one('hr.applicant', string="Applicant", index='btree_not_null', ondelete='set null')
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
events = super().create(vals_list)
|
||||
if not self.env['hr.applicant'].has_access('read'):
|
||||
return events
|
||||
|
||||
attachments = False
|
||||
if "default_applicant_id" in self.env.context:
|
||||
attachments = self.env['hr.applicant'].browse(self.env.context['default_applicant_id']).attachment_ids
|
||||
|
||||
if attachments:
|
||||
self.env['ir.attachment'].create([{
|
||||
'name': att.name,
|
||||
'type': 'binary',
|
||||
'datas': att.datas,
|
||||
'res_model': event._name,
|
||||
'res_id': event.id
|
||||
} for event in events for att in attachments])
|
||||
return events
|
||||
|
||||
def _compute_is_highlighted(self):
|
||||
super(CalendarEvent, self)._compute_is_highlighted()
|
||||
super()._compute_is_highlighted()
|
||||
applicant_id = self.env.context.get('active_id')
|
||||
if self.env.context.get('active_model') == 'hr.applicant' and applicant_id:
|
||||
for event in self:
|
||||
if event.applicant_id.id == applicant_id:
|
||||
event.is_highlighted = True
|
||||
|
||||
applicant_id = fields.Many2one('hr.applicant', string="Applicant", index='btree_not_null', ondelete='set null')
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from odoo import fields, models, _
|
|||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class Digest(models.Model):
|
||||
class DigestDigest(models.Model):
|
||||
_inherit = 'digest.digest'
|
||||
|
||||
kpi_hr_recruitment_new_colleagues = fields.Boolean('New Employees')
|
||||
|
|
@ -14,16 +14,13 @@ class Digest(models.Model):
|
|||
def _compute_kpi_hr_recruitment_new_colleagues_value(self):
|
||||
if not self.env.user.has_group('hr_recruitment.group_hr_recruitment_user'):
|
||||
raise AccessError(_("Do not have access, skip this data for user's digest email"))
|
||||
for record in self:
|
||||
start, end, company = record._get_kpi_compute_parameters()
|
||||
new_colleagues = self.env['hr.employee'].search_count([
|
||||
('create_date', '>=', start),
|
||||
('create_date', '<', end),
|
||||
('company_id', '=', company.id)
|
||||
])
|
||||
record.kpi_hr_recruitment_new_colleagues_value = new_colleagues
|
||||
|
||||
self._calculate_company_based_kpi(
|
||||
'hr.employee',
|
||||
'kpi_hr_recruitment_new_colleagues_value',
|
||||
)
|
||||
|
||||
def _compute_kpis_actions(self, company, user):
|
||||
res = super(Digest, self)._compute_kpis_actions(company, user)
|
||||
res['kpi_hr_recruitment_new_colleagues'] = 'hr.open_view_employee_list_my&menu_id=%s' % self.env.ref('hr.menu_hr_root').id
|
||||
res = super()._compute_kpis_actions(company, user)
|
||||
res['kpi_hr_recruitment_new_colleagues'] = f"hr.open_view_employee_list_my?menu_id={self.env.ref('hr.menu_hr_root').id}"
|
||||
return res
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from random import randint
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrApplicantCategory(models.Model):
|
||||
_name = 'hr.applicant.category'
|
||||
_description = "Category of applicant"
|
||||
|
||||
def _get_default_color(self):
|
||||
return randint(1, 11)
|
||||
|
||||
name = fields.Char("Tag Name", required=True)
|
||||
color = fields.Integer(string='Color Index', default=_get_default_color)
|
||||
|
||||
_name_uniq = models.Constraint(
|
||||
'unique (name)',
|
||||
'Tag name already exists!',
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrApplicantRefuseReason(models.Model):
|
||||
_name = 'hr.applicant.refuse.reason'
|
||||
_description = 'Refuse Reason of Applicant'
|
||||
_order = 'sequence'
|
||||
|
||||
sequence = fields.Integer(copy=False, default=10)
|
||||
name = fields.Char('Description', required=True, translate=True)
|
||||
template_id = fields.Many2one('mail.template', string='Email Template', domain="[('model', '=', 'hr.applicant')]")
|
||||
active = fields.Boolean('Active', default=True)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrDepartment(models.Model):
|
||||
|
|
@ -17,8 +17,8 @@ class HrDepartment(models.Model):
|
|||
if self.env.user.has_group('hr_recruitment.group_hr_recruitment_interviewer'):
|
||||
applicant_data = self.env['hr.applicant']._read_group(
|
||||
[('department_id', 'in', self.ids), ('stage_id.sequence', '<=', '1')],
|
||||
['department_id'], ['department_id'])
|
||||
result = dict((data['department_id'][0], data['department_id_count']) for data in applicant_data)
|
||||
['department_id'], ['__count'])
|
||||
result = {department.id: count for department, count in applicant_data}
|
||||
for department in self:
|
||||
department.new_applicant_count = result.get(department.id, 0)
|
||||
else:
|
||||
|
|
@ -27,9 +27,9 @@ class HrDepartment(models.Model):
|
|||
def _compute_recruitment_stats(self):
|
||||
job_data = self.env['hr.job']._read_group(
|
||||
[('department_id', 'in', self.ids)],
|
||||
['no_of_hired_employee', 'no_of_recruitment', 'department_id'], ['department_id'])
|
||||
new_emp = dict((data['department_id'][0], data['no_of_hired_employee']) for data in job_data)
|
||||
expected_emp = dict((data['department_id'][0], data['no_of_recruitment']) for data in job_data)
|
||||
['department_id'], ['no_of_hired_employee:sum', 'no_of_recruitment:sum'])
|
||||
new_emp = {department.id: nb_employee for department, nb_employee, __ in job_data}
|
||||
expected_emp = {department.id: nb_recruitment for department, __, nb_recruitment in job_data}
|
||||
for department in self:
|
||||
department.new_hired_employee = new_emp.get(department.id, 0)
|
||||
department.expected_employee = expected_emp.get(department.id, 0)
|
||||
|
|
|
|||
|
|
@ -1,35 +1,27 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.translate import _
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
|
||||
newly_hired_employee = fields.Boolean('Newly hired employee', compute='_compute_newly_hired_employee',
|
||||
search='_search_newly_hired_employee')
|
||||
applicant_id = fields.One2many('hr.applicant', 'emp_id', 'Applicant')
|
||||
applicant_ids = fields.One2many('hr.applicant', 'employee_id', 'Applicants', groups="hr.group_hr_user")
|
||||
|
||||
def _compute_newly_hired_employee(self):
|
||||
now = fields.Datetime.now()
|
||||
for employee in self:
|
||||
employee.newly_hired_employee = bool(employee.create_date > (now - timedelta(days=90)))
|
||||
def _get_partner_count_depends(self):
|
||||
return super()._get_partner_count_depends() + ['applicant_ids']
|
||||
|
||||
def _search_newly_hired_employee(self, operator, value):
|
||||
employees = self.env['hr.employee'].search([
|
||||
('create_date', '>', fields.Datetime.now() - timedelta(days=90))
|
||||
])
|
||||
return [('id', 'in', employees.ids)]
|
||||
def _get_related_partners(self):
|
||||
partners = super()._get_related_partners()
|
||||
return partners | self.sudo().applicant_ids.partner_id
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
employees = super().create(vals_list)
|
||||
for employee in employees:
|
||||
if employee.applicant_id:
|
||||
employee.applicant_id._message_log_with_view(
|
||||
for employee_sudo in employees.sudo():
|
||||
if employee_sudo.applicant_ids:
|
||||
employee_sudo.applicant_ids._message_log_with_view(
|
||||
'hr_recruitment.applicant_hired_template',
|
||||
values={'applicant': employee.applicant_id},
|
||||
subtype_id=self.env.ref("hr_recruitment.mt_applicant_hired").id)
|
||||
render_values={'applicant': employee_sudo.applicant_ids}
|
||||
)
|
||||
return employees
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@
|
|||
import ast
|
||||
from collections import defaultdict
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo import api, fields, models, SUPERUSER_ID, _
|
||||
from odoo.tools import SQL
|
||||
from odoo.tools.convert import convert_file
|
||||
|
||||
|
||||
class Job(models.Model):
|
||||
_name = "hr.job"
|
||||
_inherit = ["mail.alias.mixin", "hr.job"]
|
||||
class HrJob(models.Model):
|
||||
_name = 'hr.job'
|
||||
_inherit = ["mail.alias.mixin", "hr.job", "mail.activity.mixin"]
|
||||
_order = "sequence, name asc"
|
||||
|
||||
@api.model
|
||||
|
|
@ -27,69 +30,102 @@ class Job(models.Model):
|
|||
def _get_default_favorite_user_ids(self):
|
||||
return [(6, 0, [self.env.uid])]
|
||||
|
||||
expected_employees = fields.Integer(groups="hr_recruitment.group_hr_recruitment_interviewer,hr.group_hr_user")
|
||||
no_of_employee = fields.Integer(groups="hr_recruitment.group_hr_recruitment_interviewer,hr.group_hr_user")
|
||||
requirements = fields.Text(groups="hr_recruitment.group_hr_recruitment_interviewer,hr.group_hr_user")
|
||||
user_id = fields.Many2one(groups="hr_recruitment.group_hr_recruitment_interviewer,hr.group_hr_user")
|
||||
|
||||
address_id = fields.Many2one(
|
||||
'res.partner', "Job Location", default=_default_address_id,
|
||||
domain=lambda self: self._address_id_domain(),
|
||||
help="Address where employees are working")
|
||||
application_ids = fields.One2many('hr.applicant', 'job_id', "Job Applications")
|
||||
application_count = fields.Integer(compute='_compute_application_count', string="Application Count")
|
||||
all_application_count = fields.Integer(compute='_compute_all_application_count', string="All Application Count")
|
||||
domain=lambda self: self._address_id_domain(), tracking=True,
|
||||
help="Select the location where the applicant will work. Addresses listed here are defined on the company's contact information.")
|
||||
application_ids = fields.One2many('hr.applicant', 'job_id', "Job Applications", groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
application_count = fields.Integer(compute='_compute_application_count', string="Application Count", groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
open_application_count = fields.Integer(compute='_compute_open_application_count', string="Open Application Count",
|
||||
groups="hr_recruitment.group_hr_recruitment_interviewer", help="Number of applications that are still ongoing (not hired or refused)")
|
||||
all_application_count = fields.Integer(compute='_compute_all_application_count', string="All Application Count",
|
||||
groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
new_application_count = fields.Integer(
|
||||
compute='_compute_new_application_count', string="New Application",
|
||||
compute='_compute_new_application_count', string="New Application", groups="hr_recruitment.group_hr_recruitment_interviewer",
|
||||
help="Number of applications that are new in the flow (typically at first step of the flow)")
|
||||
old_application_count = fields.Integer(
|
||||
compute='_compute_old_application_count', string="Old Application")
|
||||
applicant_hired = fields.Integer(compute='_compute_applicant_hired', string="Applicants Hired")
|
||||
compute='_compute_old_application_count', string="Old Application", groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
applicant_hired = fields.Integer(compute='_compute_applicant_hired', string="Applicants Hired", groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
manager_id = fields.Many2one(
|
||||
'hr.employee', related='department_id.manager_id', string="Department Manager",
|
||||
readonly=True, store=True)
|
||||
user_id = fields.Many2one('res.users', "Recruiter", domain="[('share', '=', False), ('company_ids', 'in', company_id)]", tracking=True, help="The Recruiter will be the default value for all Applicants Recruiter's field in this job position. The Recruiter is automatically added to all meetings with the Applicant.")
|
||||
hr_responsible_id = fields.Many2one(
|
||||
'res.users', "HR Responsible", tracking=True,
|
||||
help="Person responsible of validating the employee's contracts.")
|
||||
document_ids = fields.One2many('ir.attachment', compute='_compute_document_ids', string="Documents", readonly=True)
|
||||
documents_count = fields.Integer(compute='_compute_document_ids', string="Document Count")
|
||||
alias_id = fields.Many2one(
|
||||
'mail.alias', "Alias", ondelete="restrict", required=True,
|
||||
help="Email alias for this job position. New emails will automatically create new applicants for this job position.")
|
||||
readonly=True, store=True, groups="hr_recruitment.group_hr_recruitment_interviewer,hr.group_hr_user")
|
||||
document_ids = fields.One2many('ir.attachment', compute='_compute_document_ids', string="Documents", readonly=True, groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
documents_count = fields.Integer(compute='_compute_document_ids', string="Document Count", groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
employee_count = fields.Integer(compute='_compute_employee_count')
|
||||
alias_id = fields.Many2one(help="Email alias for this job position. New emails will automatically create new applicants for this job position.", groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
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)]", help="The Interviewers set on the job position can see all Applicants in it. They have access to the information, the attachments, the meeting management and they can refuse him. You don't need to have Recruitment rights to be set as an interviewer.")
|
||||
extended_interviewer_ids = fields.Many2many('res.users', 'hr_job_extended_interviewer_res_users', compute='_compute_extended_interviewer_ids', store=True)
|
||||
interviewer_ids = fields.Many2many(
|
||||
"res.users",
|
||||
domain="[('share', '=', False), ('company_ids', '=?', company_id)]",
|
||||
string="Interviewers",
|
||||
groups="hr_recruitment.group_hr_recruitment_interviewer",
|
||||
help="The Interviewers set on the job position can see all Applicants in it. They have access to the information, the attachments, the meeting management and they can refuse him. You don't need to have Recruitment rights to be set as an interviewer.",
|
||||
)
|
||||
extended_interviewer_ids = fields.Many2many('res.users', 'hr_job_extended_interviewer_res_users', compute='_compute_extended_interviewer_ids', store=True, groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
industry_id = fields.Many2one('res.partner.industry', 'Industry', tracking=True, groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
expected_degree = fields.Many2one("hr.recruitment.degree", groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
|
||||
activities_overdue = fields.Integer(compute='_compute_activities')
|
||||
activities_today = fields.Integer(compute='_compute_activities')
|
||||
activity_count = fields.Integer(compute='_compute_activities', groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
|
||||
job_properties = fields.Properties('Properties', definition='company_id.job_properties_definition', groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
|
||||
applicant_properties_definition = fields.PropertiesDefinition('Applicant Properties', groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
no_of_hired_employee = fields.Integer(
|
||||
compute='_compute_no_of_hired_employee',
|
||||
string='Hired', copy=False, groups="hr_recruitment.group_hr_recruitment_interviewer",
|
||||
help='Number of hired employees for this job position during recruitment phase.',
|
||||
store=True)
|
||||
|
||||
job_source_ids = fields.One2many('hr.recruitment.source', 'job_id', groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
|
||||
@api.depends('application_ids.date_closed')
|
||||
def _compute_no_of_hired_employee(self):
|
||||
counts = dict(self.env['hr.applicant']._read_group(
|
||||
domain=[
|
||||
('job_id', 'in', self.ids),
|
||||
('date_closed', '!=', False),
|
||||
'|',
|
||||
('active', '=', False),
|
||||
('active', '=', True),
|
||||
],
|
||||
groupby=['job_id'],
|
||||
aggregates=['__count']))
|
||||
for job in self:
|
||||
job.no_of_hired_employee = counts.get(job, 0)
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_activities(self):
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
app.job_id,
|
||||
COUNT(*) AS act_count,
|
||||
CASE
|
||||
WHEN %(today)s::date - act.date_deadline::date = 0 THEN 'today'
|
||||
WHEN %(today)s::date - act.date_deadline::date > 0 THEN 'overdue'
|
||||
END AS act_state
|
||||
COUNT(*) AS act_count
|
||||
FROM mail_activity act
|
||||
JOIN hr_applicant app ON app.id = act.res_id
|
||||
JOIN hr_recruitment_stage sta ON app.stage_id = sta.id
|
||||
WHERE act.user_id = %(user_id)s AND act.res_model = 'hr.applicant'
|
||||
AND act.date_deadline <= %(today)s::date AND app.active
|
||||
AND app.active
|
||||
AND app.job_id IN %(job_ids)s
|
||||
AND sta.hired_stage IS NOT TRUE
|
||||
GROUP BY app.job_id, act_state
|
||||
AND COALESCE(act.active, TRUE) = TRUE
|
||||
GROUP BY app.job_id
|
||||
""", {
|
||||
'today': fields.Date.context_today(self),
|
||||
'user_id': self.env.uid,
|
||||
'job_ids': tuple(self.ids),
|
||||
'job_ids': tuple(self.ids or [0]),
|
||||
# or [0] is used in case we only have newIds (web studio)
|
||||
})
|
||||
job_activities = defaultdict(dict)
|
||||
for activity in self.env.cr.dictfetchall():
|
||||
job_activities[activity['job_id']][activity['act_state']] = activity['act_count']
|
||||
job_activities[activity['job_id']] = activity['act_count']
|
||||
for job in self:
|
||||
job.activities_overdue = job_activities[job.id].get('overdue', 0)
|
||||
job.activities_today = job_activities[job.id].get('today', 0)
|
||||
job.activity_count = job_activities[job.id]
|
||||
|
||||
@api.depends('application_ids.interviewer_ids')
|
||||
def _compute_extended_interviewer_ids(self):
|
||||
|
|
@ -119,7 +155,7 @@ class Job(models.Model):
|
|||
unfavorited_jobs.write({'favorite_user_ids': [(3, self.env.uid)]})
|
||||
|
||||
def _compute_document_ids(self):
|
||||
applicants = self.mapped('application_ids').filtered(lambda self: not self.emp_id)
|
||||
applicants = self.mapped('application_ids').filtered(lambda self: not self.employee_id)
|
||||
app_to_job = dict((applicant.id, applicant.job_id.id) for applicant in applicants)
|
||||
attachments = self.env['ir.attachment'].search([
|
||||
'|',
|
||||
|
|
@ -143,17 +179,41 @@ class Job(models.Model):
|
|||
('active', '=', True),
|
||||
'&',
|
||||
('active', '=', False), ('refuse_reason_id', '!=', False),
|
||||
], ['job_id'], ['job_id'])
|
||||
result = dict((data['job_id'][0], data['job_id_count']) for data in read_group_result)
|
||||
], ['job_id'], ['__count'])
|
||||
result = {job.id: count for job, count in read_group_result}
|
||||
for job in self:
|
||||
job.all_application_count = result.get(job.id, 0)
|
||||
|
||||
def _compute_application_count(self):
|
||||
read_group_result = self.env['hr.applicant']._read_group([('job_id', 'in', self.ids)], ['job_id'], ['job_id'])
|
||||
result = dict((data['job_id'][0], data['job_id_count']) for data in read_group_result)
|
||||
read_group_result = self.env['hr.applicant']._read_group([('job_id', 'in', self.ids)], ['job_id'], ['__count'])
|
||||
result = {job.id: count for job, count in read_group_result}
|
||||
for job in self:
|
||||
job.application_count = result.get(job.id, 0)
|
||||
|
||||
def _compute_open_application_count(self):
|
||||
hired_stages = self.env['hr.recruitment.stage'].search([('hired_stage', '=', True)])
|
||||
result = dict(self.env['hr.applicant']._read_group([
|
||||
('job_id', 'in', self.ids),
|
||||
('stage_id', 'not in', hired_stages.ids),
|
||||
], ['job_id'], ['__count']))
|
||||
for job in self:
|
||||
job.open_application_count = result.get(job, 0)
|
||||
|
||||
def _compute_employee_count(self):
|
||||
res = {
|
||||
job.id: count
|
||||
for job, count in self.env['hr.employee'].sudo()._read_group(
|
||||
domain=[
|
||||
('job_id', 'in', self.ids),
|
||||
('company_id', 'in', self.env.companies.ids),
|
||||
],
|
||||
groupby=['job_id'],
|
||||
aggregates=['__count'],
|
||||
)
|
||||
}
|
||||
for job in self:
|
||||
job.employee_count = res.get(job.id, 0)
|
||||
|
||||
def _get_first_stage(self):
|
||||
self.ensure_one()
|
||||
return self.env['hr.recruitment.stage'].search([
|
||||
|
|
@ -185,9 +245,11 @@ class Job(models.Model):
|
|||
ON s.job_id = a.job_id
|
||||
AND a.stage_id = s.stage_id
|
||||
AND a.active IS TRUE
|
||||
WHERE a.company_id in %s
|
||||
WHERE a.company_id in %s
|
||||
OR a.company_id is NULL
|
||||
GROUP BY s.job_id
|
||||
""", [tuple(self.ids), tuple(self.env.companies.ids)]
|
||||
""", [tuple(self.ids or [0]), tuple(self.env.companies.ids)]
|
||||
# or [0] is used in case we only have newIds (web studio)
|
||||
)
|
||||
|
||||
new_applicant_count = dict(self.env.cr.fetchall())
|
||||
|
|
@ -199,8 +261,8 @@ class Job(models.Model):
|
|||
hired_data = self.env['hr.applicant']._read_group([
|
||||
('job_id', 'in', self.ids),
|
||||
('stage_id', 'in', hired_stages.ids),
|
||||
], ['job_id'], ['job_id'])
|
||||
job_hires = {data['job_id'][0]: data['job_id_count'] for data in hired_data}
|
||||
], ['job_id'], ['__count'])
|
||||
job_hires = {job.id: count for job, count in hired_data}
|
||||
for job in self:
|
||||
job.applicant_hired = job_hires.get(job.id, 0)
|
||||
|
||||
|
|
@ -210,14 +272,14 @@ class Job(models.Model):
|
|||
job.old_application_count = job.application_count - job.new_application_count
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
values = super(Job, self)._alias_get_creation_values()
|
||||
values = super()._alias_get_creation_values()
|
||||
values['alias_model_id'] = self.env['ir.model']._get('hr.applicant').id
|
||||
if self.id:
|
||||
values['alias_defaults'] = defaults = ast.literal_eval(self.alias_defaults or "{}")
|
||||
defaults.update({
|
||||
'job_id': self.id,
|
||||
'department_id': self.department_id.id,
|
||||
'company_id': self.department_id.company_id.id if self.department_id else self.company_id.id,
|
||||
'company_id': self.department_id.company_id.id or self.company_id.id,
|
||||
'user_id': self.user_id.id,
|
||||
})
|
||||
return values
|
||||
|
|
@ -225,22 +287,18 @@ class Job(models.Model):
|
|||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
vals['favorite_user_ids'] = vals.get('favorite_user_ids', []) + [(4, self.env.uid)]
|
||||
if vals.get('alias_name'):
|
||||
vals['alias_user_id'] = False
|
||||
vals["favorite_user_ids"] = vals.get("favorite_user_ids", [])
|
||||
jobs = super().create(vals_list)
|
||||
utm_linkedin = self.env.ref("utm.utm_source_linkedin", raise_if_not_found=False)
|
||||
if utm_linkedin:
|
||||
source_vals = [{
|
||||
'source_id': utm_linkedin.id,
|
||||
'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
|
||||
old_managers = {}
|
||||
old_recruiters = {}
|
||||
for job in self:
|
||||
old_managers[job] = job.manager_id
|
||||
old_recruiters[job] = job.user_id
|
||||
if 'active' in vals and not vals['active']:
|
||||
self.application_ids.active = False
|
||||
res = super().write(vals)
|
||||
|
|
@ -249,6 +307,24 @@ class Job(models.Model):
|
|||
interviewers_to_clean._remove_recruitment_interviewers()
|
||||
self.sudo().interviewer_ids._create_recruitment_interviewers()
|
||||
|
||||
# Subscribe the recruiter if it has changed.
|
||||
if "user_id" in vals:
|
||||
for job in self:
|
||||
to_unsubscribe = [
|
||||
partner
|
||||
for partner in old_recruiters[job].partner_id.ids
|
||||
if partner not in job.manager_id._get_related_partners().ids
|
||||
]
|
||||
job.message_unsubscribe(to_unsubscribe)
|
||||
application_ids = job.application_ids.filtered(
|
||||
lambda x:
|
||||
x.user_id == old_recruiters[job] and
|
||||
x.application_status == 'ongoing'
|
||||
)
|
||||
if application_ids:
|
||||
application_ids.message_unsubscribe(to_unsubscribe)
|
||||
application_ids.with_context(mail_auto_subscribe_no_notify=True).user_id = job.user_id
|
||||
|
||||
# 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
|
||||
|
|
@ -259,6 +335,16 @@ class Job(models.Model):
|
|||
job.alias_defaults = alias_default_vals
|
||||
return res
|
||||
|
||||
def _order_field_to_sql(self, alias, field_name, direction, nulls, query):
|
||||
if field_name == 'is_favorite':
|
||||
sql_field = SQL(
|
||||
"%s IN (SELECT job_id FROM job_favorite_user_rel WHERE user_id = %s)",
|
||||
SQL.identifier(alias, 'id'), self.env.uid,
|
||||
)
|
||||
return SQL("%s %s %s", sql_field, direction, nulls)
|
||||
|
||||
return super()._order_field_to_sql(alias, field_name, direction, nulls, query)
|
||||
|
||||
def _creation_subtype(self):
|
||||
return self.env.ref('hr_recruitment.mt_job_new')
|
||||
|
||||
|
|
@ -272,9 +358,9 @@ class Job(models.Model):
|
|||
'default_res_id': self.ids[0],
|
||||
'show_partner_name': 1,
|
||||
},
|
||||
'view_mode': 'tree',
|
||||
'view_mode': 'list',
|
||||
'views': [
|
||||
(self.env.ref('hr_recruitment.ir_attachment_hr_recruitment_list_view').id, 'tree')
|
||||
(self.env.ref('hr_recruitment.ir_attachment_hr_recruitment_list_view').id, 'list')
|
||||
],
|
||||
'search_view_id': self.env.ref('hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment').ids,
|
||||
'domain': ['|',
|
||||
|
|
@ -288,37 +374,47 @@ class Job(models.Model):
|
|||
views = ['activity'] + [view for view in action['view_mode'].split(',') if view != 'activity']
|
||||
action['view_mode'] = ','.join(views)
|
||||
action['views'] = [(False, view) for view in views]
|
||||
return action
|
||||
|
||||
def action_open_late_activities(self):
|
||||
action = self.action_open_activities()
|
||||
action['context'] = {
|
||||
'default_job_id': self.id,
|
||||
'search_default_job_id': self.id,
|
||||
'search_default_activities_overdue': True,
|
||||
'search_default_running_applicant_activities': True,
|
||||
}
|
||||
return action
|
||||
|
||||
def action_open_today_activities(self):
|
||||
action = self.action_open_activities()
|
||||
action['context'] = {
|
||||
'default_job_id': self.id,
|
||||
'search_default_job_id': self.id,
|
||||
'search_default_activities_today': True,
|
||||
}
|
||||
return action
|
||||
@api.model
|
||||
def _action_load_recruitment_scenario(self):
|
||||
|
||||
def close_dialog(self):
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
convert_file(
|
||||
self.sudo().env,
|
||||
"hr_recruitment",
|
||||
"data/scenarios/hr_recruitment_scenario.xml",
|
||||
None,
|
||||
mode="init",
|
||||
)
|
||||
|
||||
def edit_dialog(self):
|
||||
form_view = self.env.ref('hr.view_hr_job_form')
|
||||
return {
|
||||
'name': _('Job'),
|
||||
'res_model': 'hr.job',
|
||||
'res_id': self.id,
|
||||
'views': [(form_view.id, 'form'),],
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'inline'
|
||||
"type": "ir.actions.client",
|
||||
"tag": "reload",
|
||||
}
|
||||
|
||||
def action_open_employees(self):
|
||||
self.ensure_one()
|
||||
if self.env['hr.employee'].has_access('read'):
|
||||
res_model = "hr.employee"
|
||||
else:
|
||||
res_model = "hr.employee.public"
|
||||
|
||||
return {
|
||||
'name': _("Related Employees"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': res_model,
|
||||
'view_mode': 'list,kanban,form',
|
||||
'views': [(False, 'list'), (False, 'kanban'), (False, 'form')],
|
||||
'domain': [('company_id', 'in', self.env.companies.ids)],
|
||||
'context': {
|
||||
'default_job_id': self.id,
|
||||
'search_default_group_job': 1,
|
||||
'search_default_job_id': self.id,
|
||||
'expand': 1
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import email_normalize
|
||||
|
||||
|
||||
class HrJobPlatform(models.Model):
|
||||
_name = 'hr.job.platform'
|
||||
_description = 'Job Platforms'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
email = fields.Char(required=True, help="Applications received from this Email won't be linked to a contact."
|
||||
"There will be no email address set on the Applicant either.")
|
||||
regex = fields.Char(help="The regex facilitates to extract information from the subject or body "
|
||||
"of the received email to autopopulate the Applicant's name field")
|
||||
|
||||
_email_uniq = models.Constraint(
|
||||
'unique (email)',
|
||||
'The Email must be unique, this one already corresponds to another Job Platform.',
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals['email']:
|
||||
vals['email'] = email_normalize(vals['email']) or vals['email']
|
||||
platforms = super().create(vals_list)
|
||||
return platforms
|
||||
|
||||
def write(self, vals):
|
||||
if vals.get('email'):
|
||||
vals['email'] = email_normalize(vals['email']) or vals['email']
|
||||
return super().write(vals)
|
||||
|
|
@ -1,779 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from random import randint
|
||||
|
||||
from odoo import api, fields, models, tools, SUPERUSER_ID
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.tools import Query
|
||||
from odoo.tools.translate import _
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from lxml import etree
|
||||
|
||||
AVAILABLE_PRIORITIES = [
|
||||
('0', 'Normal'),
|
||||
('1', 'Good'),
|
||||
('2', 'Very Good'),
|
||||
('3', 'Excellent')
|
||||
]
|
||||
|
||||
|
||||
class RecruitmentSource(models.Model):
|
||||
_name = "hr.recruitment.source"
|
||||
_description = "Source of Applicants"
|
||||
_inherit = ['utm.source.mixin']
|
||||
|
||||
email = fields.Char(related='alias_id.display_name', string="Email", readonly=True)
|
||||
has_domain = fields.Char(compute='_compute_has_domain')
|
||||
job_id = fields.Many2one('hr.job', "Job", ondelete='cascade')
|
||||
alias_id = fields.Many2one('mail.alias', "Alias ID")
|
||||
medium_id = fields.Many2one('utm.medium', default=lambda self: self.env.ref('utm.utm_medium_website'))
|
||||
|
||||
def _compute_has_domain(self):
|
||||
self.has_domain = bool(self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain"))
|
||||
|
||||
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_model_id': self.env['ir.model']._get('hr.applicant').id,
|
||||
'alias_parent_model_id': self.env['ir.model']._get('hr.job').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,
|
||||
},
|
||||
}
|
||||
|
||||
# check that you can create source before to call mail.alias in sudo with known/controlled vals
|
||||
source.check_access_rights('create')
|
||||
source.check_access_rule('create')
|
||||
source.alias_id = self.env['mail.alias'].sudo().create(vals)
|
||||
|
||||
@api.model
|
||||
def _get_view(self, view_id=None, view_type='form', **options):
|
||||
arch, view = super()._get_view(view_id, view_type, **options)
|
||||
if view_type == 'tree' and not bool(self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")):
|
||||
email = arch.xpath("//field[@name='email']")[0]
|
||||
email.getparent().remove(email)
|
||||
return arch, view
|
||||
|
||||
class RecruitmentStage(models.Model):
|
||||
_name = "hr.recruitment.stage"
|
||||
_description = "Recruitment Stages"
|
||||
_order = 'sequence'
|
||||
|
||||
name = fields.Char("Stage Name", required=True, translate=True)
|
||||
sequence = fields.Integer(
|
||||
"Sequence", default=10)
|
||||
job_ids = fields.Many2many(
|
||||
'hr.job', string='Job Specific',
|
||||
help='Specific jobs that uses this stage. Other jobs will not use this stage.')
|
||||
requirements = fields.Text("Requirements")
|
||||
template_id = fields.Many2one(
|
||||
'mail.template', "Email Template",
|
||||
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 Kanban",
|
||||
help="This stage is folded in the kanban view when there are no records in that stage to display.")
|
||||
hired_stage = fields.Boolean('Hired Stage',
|
||||
help="If checked, this stage is used to determine the hire date of an applicant")
|
||||
legend_blocked = fields.Char(
|
||||
'Red Kanban Label', default=lambda self: _('Blocked'), translate=True, required=True)
|
||||
legend_done = fields.Char(
|
||||
'Green Kanban Label', default=lambda self: _('Ready for Next Stage'), translate=True, required=True)
|
||||
legend_normal = fields.Char(
|
||||
'Grey Kanban Label', default=lambda self: _('In Progress'), translate=True, required=True)
|
||||
is_warning_visible = fields.Boolean(compute='_compute_is_warning_visible')
|
||||
|
||||
@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)
|
||||
|
||||
@api.depends('hired_stage')
|
||||
def _compute_is_warning_visible(self):
|
||||
applicant_data = self.env['hr.applicant']._read_group([('stage_id', 'in', self.ids)], ['stage_id'], 'stage_id')
|
||||
applicants = dict((data['stage_id'][0], data['stage_id_count']) for data in applicant_data)
|
||||
for stage in self:
|
||||
if stage._origin.hired_stage and not stage.hired_stage and applicants.get(stage._origin.id):
|
||||
stage.is_warning_visible = True
|
||||
else:
|
||||
stage.is_warning_visible = False
|
||||
|
||||
class RecruitmentDegree(models.Model):
|
||||
_name = "hr.recruitment.degree"
|
||||
_description = "Applicant Degree"
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', 'The name of the Degree of Recruitment must be unique!')
|
||||
]
|
||||
|
||||
name = fields.Char("Degree Name", required=True, translate=True)
|
||||
sequence = fields.Integer("Sequence", default=1)
|
||||
|
||||
|
||||
class Applicant(models.Model):
|
||||
_name = "hr.applicant"
|
||||
_description = "Applicant"
|
||||
_order = "priority desc, id desc"
|
||||
_inherit = ['mail.thread.cc', 'mail.activity.mixin', 'utm.mixin']
|
||||
_mailing_enabled = True
|
||||
|
||||
name = fields.Char("Subject / Application", required=True, help="Email subject for applications sent via email", index='trigram')
|
||||
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.Html("Description")
|
||||
email_from = fields.Char("Email", size=128, compute='_compute_partner_phone_email',
|
||||
inverse='_inverse_partner_email', store=True)
|
||||
probability = fields.Float("Probability")
|
||||
partner_id = fields.Many2one('res.partner', "Contact", copy=False)
|
||||
create_date = fields.Datetime("Creation Date", readonly=True)
|
||||
stage_id = fields.Many2one('hr.recruitment.stage', 'Stage', ondelete='restrict', tracking=True,
|
||||
compute='_compute_stage', store=True, readonly=False,
|
||||
domain="['|', ('job_ids', '=', False), ('job_ids', '=', job_id)]",
|
||||
copy=False, index=True,
|
||||
group_expand='_read_group_stage_ids')
|
||||
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", compute='_compute_company', store=True, readonly=False, tracking=True)
|
||||
user_id = fields.Many2one(
|
||||
'res.users', "Recruiter", compute='_compute_user', domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
|
||||
tracking=True, store=True, readonly=False)
|
||||
date_closed = fields.Datetime("Hire Date", compute='_compute_date_closed', store=True, readonly=False, tracking=True, copy=False)
|
||||
date_open = fields.Datetime("Assigned", readonly=True)
|
||||
date_last_stage_update = fields.Datetime("Last Stage Update", index=True, default=fields.Datetime.now)
|
||||
priority = fields.Selection(AVAILABLE_PRIORITIES, "Evaluation", default='0')
|
||||
job_id = fields.Many2one('hr.job', "Applied Job", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True, index=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',
|
||||
inverse='_inverse_partner_phone', store=True)
|
||||
partner_mobile = fields.Char("Mobile", size=32, compute='_compute_partner_phone_email',
|
||||
inverse='_inverse_partner_mobile', store=True)
|
||||
type_id = fields.Many2one('hr.recruitment.degree', "Degree")
|
||||
department_id = fields.Many2one(
|
||||
'hr.department', "Department", compute='_compute_department', store=True, readonly=False,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True)
|
||||
day_open = fields.Float(compute='_compute_day', string="Days to Open", compute_sudo=True)
|
||||
day_close = fields.Float(compute='_compute_day', string="Days to Close", compute_sudo=True)
|
||||
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", help="Employee linked to the applicant.", copy=False)
|
||||
user_email = fields.Char(related='user_id.email', 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", readonly=False, tracking=False)
|
||||
attachment_ids = fields.One2many('ir.attachment', 'res_id', domain=[('res_model', '=', 'hr.applicant')], string='Attachments')
|
||||
kanban_state = fields.Selection([
|
||||
('normal', 'Grey'),
|
||||
('done', 'Green'),
|
||||
('blocked', 'Red')], string='Kanban State',
|
||||
copy=False, default='normal', required=True)
|
||||
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked')
|
||||
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid')
|
||||
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing')
|
||||
application_count = fields.Integer(compute='_compute_application_count', help='Applications with the same email or phone or mobile')
|
||||
refuse_reason_id = fields.Many2one('hr.applicant.refuse.reason', string='Refuse Reason', tracking=True)
|
||||
meeting_ids = fields.One2many('calendar.event', 'applicant_id', 'Meetings')
|
||||
meeting_display_text = fields.Char(compute='_compute_meeting_display')
|
||||
meeting_display_date = fields.Date(compute='_compute_meeting_display')
|
||||
# UTMs - enforcing the fact that we want to 'set null' when relation is unlinked
|
||||
campaign_id = fields.Many2one(ondelete='set null')
|
||||
medium_id = fields.Many2one(ondelete='set null')
|
||||
source_id = fields.Many2one(ondelete='set null')
|
||||
interviewer_ids = fields.Many2many('res.users', 'hr_applicant_res_users_interviewers_rel',
|
||||
string='Interviewers', index=True, tracking=True, copy=False,
|
||||
domain="[('share', '=', False), ('company_ids', 'in', company_id)]")
|
||||
linkedin_profile = fields.Char('LinkedIn Profile')
|
||||
application_status = fields.Selection([
|
||||
('ongoing', 'Ongoing'),
|
||||
('hired', 'Hired'),
|
||||
('refused', 'Refused'),
|
||||
], compute="_compute_application_status")
|
||||
|
||||
@api.onchange('job_id')
|
||||
def _onchange_job_id(self):
|
||||
for applicant in self:
|
||||
if applicant.job_id.name:
|
||||
applicant.name = applicant.job_id.name
|
||||
|
||||
@api.depends('date_open', 'date_closed')
|
||||
def _compute_day(self):
|
||||
for applicant in self:
|
||||
if applicant.date_open:
|
||||
date_create = applicant.create_date
|
||||
date_open = applicant.date_open
|
||||
applicant.day_open = (date_open - date_create).total_seconds() / (24.0 * 3600)
|
||||
else:
|
||||
applicant.day_open = False
|
||||
if applicant.date_closed:
|
||||
date_create = applicant.create_date
|
||||
date_closed = applicant.date_closed
|
||||
applicant.day_close = (date_closed - date_create).total_seconds() / (24.0 * 3600)
|
||||
applicant.delay_close = applicant.day_close - applicant.day_open
|
||||
else:
|
||||
applicant.day_close = False
|
||||
applicant.delay_close = False
|
||||
|
||||
@api.depends('email_from', 'partner_phone', 'partner_mobile')
|
||||
def _compute_application_count(self):
|
||||
self.flush_model(['email_from'])
|
||||
applicants = self.env['hr.applicant']
|
||||
for applicant in self:
|
||||
if applicant.email_from or applicant.partner_phone or applicant.partner_mobile:
|
||||
applicants |= applicant
|
||||
# Done via SQL since read_group does not support grouping by lowercase field
|
||||
if applicants.ids:
|
||||
query = Query(self.env.cr, self._table, self._table_query)
|
||||
query.add_where('hr_applicant.id in %s', [tuple(applicants.ids)])
|
||||
# Count into the companies that are selected from the multi-company widget
|
||||
company_ids = self.env.context.get('allowed_company_ids')
|
||||
if company_ids:
|
||||
query.add_where('other.company_id in %s', [tuple(company_ids)])
|
||||
self._apply_ir_rules(query)
|
||||
from_clause, where_clause, where_clause_params = query.get_sql()
|
||||
# In case the applicant phone or mobile is configured in wrong field
|
||||
query_str = """
|
||||
SELECT hr_applicant.id as appl_id,
|
||||
COUNT(other.id) as count
|
||||
FROM hr_applicant
|
||||
JOIN hr_applicant other ON LOWER(other.email_from) = LOWER(hr_applicant.email_from)
|
||||
OR other.partner_phone = hr_applicant.partner_phone OR other.partner_phone = hr_applicant.partner_mobile
|
||||
OR other.partner_mobile = hr_applicant.partner_mobile OR other.partner_mobile = hr_applicant.partner_phone
|
||||
%(where)s
|
||||
GROUP BY hr_applicant.id
|
||||
""" % {
|
||||
'where': ('WHERE %s' % where_clause) if where_clause else '',
|
||||
}
|
||||
self.env.cr.execute(query_str, where_clause_params)
|
||||
application_data_mapped = dict((data['appl_id'], data['count']) for data in self.env.cr.dictfetchall())
|
||||
else:
|
||||
application_data_mapped = dict()
|
||||
for applicant in applicants:
|
||||
applicant.application_count = application_data_mapped.get(applicant.id, 1) - 1
|
||||
(self - applicants).application_count = False
|
||||
|
||||
@api.depends_context('lang')
|
||||
@api.depends('meeting_ids', 'meeting_ids.start')
|
||||
def _compute_meeting_display(self):
|
||||
applicant_with_meetings = self.filtered('meeting_ids')
|
||||
(self - applicant_with_meetings).update({
|
||||
'meeting_display_text': _('No Meeting'),
|
||||
'meeting_display_date': ''
|
||||
})
|
||||
today = fields.Date.today()
|
||||
for applicant in applicant_with_meetings:
|
||||
count = len(applicant.meeting_ids)
|
||||
dates = applicant.meeting_ids.mapped('start')
|
||||
min_date, max_date = min(dates).date(), max(dates).date()
|
||||
if min_date >= today:
|
||||
applicant.meeting_display_date = min_date
|
||||
else:
|
||||
applicant.meeting_display_date = max_date
|
||||
if count == 1:
|
||||
applicant.meeting_display_text = _('1 Meeting')
|
||||
elif applicant.meeting_display_date >= today:
|
||||
applicant.meeting_display_text = _('Next Meeting')
|
||||
else:
|
||||
applicant.meeting_display_text = _('Last Meeting')
|
||||
|
||||
@api.depends('refuse_reason_id', 'date_closed')
|
||||
def _compute_application_status(self):
|
||||
for applicant in self:
|
||||
if applicant.refuse_reason_id:
|
||||
applicant.application_status = 'refused'
|
||||
elif applicant.date_closed:
|
||||
applicant.application_status = 'hired'
|
||||
else:
|
||||
applicant.application_status = 'ongoing'
|
||||
|
||||
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_ids', '=', False)]
|
||||
if job_id:
|
||||
search_domain = ['|', ('job_ids', '=', 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.depends('job_id', 'department_id')
|
||||
def _compute_company(self):
|
||||
for applicant in self:
|
||||
company_id = False
|
||||
if applicant.department_id:
|
||||
company_id = applicant.department_id.company_id.id
|
||||
if not company_id and applicant.job_id:
|
||||
company_id = applicant.job_id.company_id.id
|
||||
applicant.company_id = company_id or self.env.company.id
|
||||
|
||||
@api.depends('job_id')
|
||||
def _compute_department(self):
|
||||
for applicant in self:
|
||||
applicant.department_id = applicant.job_id.department_id.id
|
||||
|
||||
@api.depends('job_id')
|
||||
def _compute_stage(self):
|
||||
for applicant in self:
|
||||
if applicant.job_id:
|
||||
if not applicant.stage_id:
|
||||
stage_ids = self.env['hr.recruitment.stage'].search([
|
||||
'|',
|
||||
('job_ids', '=', False),
|
||||
('job_ids', '=', applicant.job_id.id),
|
||||
('fold', '=', False)
|
||||
], order='sequence asc', limit=1).ids
|
||||
applicant.stage_id = stage_ids[0] if stage_ids else False
|
||||
else:
|
||||
applicant.stage_id = False
|
||||
|
||||
@api.depends('job_id')
|
||||
def _compute_user(self):
|
||||
for applicant in self:
|
||||
applicant.user_id = applicant.job_id.user_id.id or self.env.uid
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_phone_email(self):
|
||||
for applicant in self:
|
||||
if applicant.partner_id:
|
||||
applicant.partner_phone = applicant.partner_id.phone
|
||||
applicant.partner_mobile = applicant.partner_id.mobile
|
||||
applicant.email_from = applicant.partner_id.email
|
||||
|
||||
def _inverse_partner_email(self):
|
||||
for applicant in self.filtered(lambda a: a.partner_id and a.email_from and not a.partner_id.email):
|
||||
applicant.partner_id.email = applicant.email_from
|
||||
|
||||
def _inverse_partner_phone(self):
|
||||
for applicant in self.filtered(lambda a: a.partner_id and a.partner_phone and not a.partner_id.phone):
|
||||
applicant.partner_id.phone = applicant.partner_phone
|
||||
|
||||
def _inverse_partner_mobile(self):
|
||||
for applicant in self.filtered(lambda a: a.partner_id and a.partner_mobile and not a.partner_id.mobile):
|
||||
applicant.partner_id.mobile = applicant.partner_mobile
|
||||
|
||||
@api.depends('stage_id.hired_stage')
|
||||
def _compute_date_closed(self):
|
||||
for applicant in self:
|
||||
if applicant.stage_id and applicant.stage_id.hired_stage and not applicant.date_closed:
|
||||
applicant.date_closed = fields.datetime.now()
|
||||
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:
|
||||
if vals.get('user_id'):
|
||||
vals['date_open'] = fields.Datetime.now()
|
||||
if vals.get('email_from'):
|
||||
vals['email_from'] = vals['email_from'].strip()
|
||||
applicants = super().create(vals_list)
|
||||
applicants.sudo().interviewer_ids._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'))
|
||||
category = self.env.ref('hr_recruitment.categ_meet_interview')
|
||||
for applicant in applicants:
|
||||
partners = applicant.partner_id | applicant.user_id.partner_id | applicant.department_id.manager_id.user_id.partner_id
|
||||
self.env['calendar.event'].sudo().with_context(default_applicant_id=applicant.id).create({
|
||||
'applicant_id': applicant.id,
|
||||
'partner_ids': [(6, 0, partners.ids)],
|
||||
'user_id': self.env.uid,
|
||||
'name': applicant.name,
|
||||
'categ_ids': [category.id],
|
||||
'start': deadline,
|
||||
'stop': deadline + relativedelta(minutes=30),
|
||||
})
|
||||
return applicants
|
||||
|
||||
def write(self, vals):
|
||||
# user_id change: update date_open
|
||||
if vals.get('user_id'):
|
||||
vals['date_open'] = fields.Datetime.now()
|
||||
if vals.get('email_from'):
|
||||
vals['email_from'] = vals['email_from'].strip()
|
||||
old_interviewers = self.interviewer_ids
|
||||
# stage_id: track last stage before update
|
||||
if '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.stage_id.id
|
||||
res = super(Applicant, self).write(vals)
|
||||
else:
|
||||
res = super(Applicant, self).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()
|
||||
if vals.get('emp_id'):
|
||||
self._update_employee_from_applicant()
|
||||
return res
|
||||
|
||||
def get_empty_list_help(self, help):
|
||||
if 'active_id' in self.env.context and self.env.context.get('active_model') == 'hr.job':
|
||||
alias_id = self.env['hr.job'].browse(self.env.context['active_id']).alias_id
|
||||
else:
|
||||
alias_id = False
|
||||
|
||||
nocontent_values = {
|
||||
'help_title': _("No application found. Let's create one !"),
|
||||
'para_1': _('People can also apply by email to save time.'),
|
||||
'para_2': _("You can search into attachment's content, like resumes, with the searchbar."),
|
||||
}
|
||||
nocontent_body = """
|
||||
<p class="o_view_nocontent_smiling_face">%(help_title)s</p>
|
||||
<p>%(para_1)s<br/>%(para_2)s</p>"""
|
||||
|
||||
if alias_id and alias_id.alias_domain and alias_id.alias_name:
|
||||
email = alias_id.display_name
|
||||
email_link = "<a href='mailto:%s'>%s</a>" % (email, email)
|
||||
nocontent_values['email_link'] = email_link
|
||||
nocontent_body += """<p class="o_copy_paste_email">%(email_link)s</p>"""
|
||||
|
||||
return nocontent_body % nocontent_values
|
||||
|
||||
@api.model
|
||||
def get_view(self, view_id=None, view_type='form', **options):
|
||||
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().get_view(view_id, view_type, **options)
|
||||
|
||||
def _notify_get_recipients(self, message, msg_vals, **kwargs):
|
||||
"""
|
||||
Do not notify members of the Recruitment Interviewer group, 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']]
|
||||
|
||||
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.'))
|
||||
self.partner_id = self.env['res.partner'].create({
|
||||
'is_company': False,
|
||||
'type': 'private',
|
||||
'name': self.partner_name,
|
||||
'email': self.email_from,
|
||||
'phone': self.partner_phone,
|
||||
'mobile': self.partner_mobile
|
||||
})
|
||||
|
||||
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'] = {
|
||||
'default_applicant_id': self.id,
|
||||
'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
|
||||
|
||||
def action_open_attachments(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'ir.attachment',
|
||||
'name': _('Documents'),
|
||||
'context': {
|
||||
'default_res_model': 'hr.applicant',
|
||||
'default_res_id': self.ids[0],
|
||||
'show_partner_name': 1,
|
||||
},
|
||||
'view_mode': 'tree,form',
|
||||
'views': [
|
||||
(self.env.ref('hr_recruitment.ir_attachment_hr_recruitment_list_view').id, 'tree'),
|
||||
(False, 'form'),
|
||||
],
|
||||
'search_view_id': self.env.ref('hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment').ids,
|
||||
'domain': [('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.ids), ],
|
||||
}
|
||||
|
||||
def action_applications_email(self):
|
||||
self.ensure_one()
|
||||
self.env.cr.execute("""
|
||||
SELECT other.id
|
||||
FROM hr_applicant
|
||||
JOIN hr_applicant other ON LOWER(other.email_from) = LOWER(hr_applicant.email_from)
|
||||
OR other.partner_phone = hr_applicant.partner_phone OR other.partner_phone = hr_applicant.partner_mobile
|
||||
OR other.partner_mobile = hr_applicant.partner_mobile OR other.partner_mobile = hr_applicant.partner_phone
|
||||
WHERE hr_applicant.id in %s
|
||||
""", (tuple(self.ids),)
|
||||
)
|
||||
ids = [res['id'] for res in self.env.cr.dictfetchall()]
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Job Applications'),
|
||||
'res_model': self._name,
|
||||
'view_mode': 'tree,kanban,form,pivot,graph,calendar,activity',
|
||||
'domain': [('id', 'in', ids)],
|
||||
'context': {
|
||||
'active_test': False,
|
||||
'search_default_stage': 1,
|
||||
},
|
||||
}
|
||||
|
||||
def action_open_employee(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Employee'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'hr.employee',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.emp_id.id,
|
||||
}
|
||||
|
||||
def _track_template(self, changes):
|
||||
res = super(Applicant, self)._track_template(changes)
|
||||
applicant = self[0]
|
||||
if 'stage_id' in changes and applicant.stage_id.template_id:
|
||||
res['stage_id'] = (applicant.stage_id.template_id, {
|
||||
'auto_delete_message': True,
|
||||
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
|
||||
'email_layout_xmlid': 'mail.mail_notification_light'
|
||||
})
|
||||
return res
|
||||
|
||||
def _creation_subtype(self):
|
||||
return self.env.ref('hr_recruitment.mt_applicant_new')
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
record = self[0]
|
||||
if 'stage_id' in init_values and record.stage_id:
|
||||
return self.env.ref('hr_recruitment.mt_applicant_stage_changed')
|
||||
return super(Applicant, self)._track_subtype(init_values)
|
||||
|
||||
def _notify_get_reply_to(self, default=None):
|
||||
""" Override to set alias of applicants to their job definition if any. """
|
||||
aliases = self.mapped('job_id')._notify_get_reply_to(default=default)
|
||||
res = {app.id: aliases.get(app.job_id.id) for app in self}
|
||||
leftover = self.filtered(lambda rec: not rec.job_id)
|
||||
if leftover:
|
||||
res.update(super(Applicant, leftover)._notify_get_reply_to(default=default))
|
||||
return res
|
||||
|
||||
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.sudo(), reason=_('Contact'))
|
||||
elif applicant.email_from:
|
||||
email_from = tools.email_normalize(applicant.email_from)
|
||||
if email_from and applicant.partner_name:
|
||||
email_from = tools.formataddr((applicant.partner_name, email_from))
|
||||
applicant._message_add_suggested_recipient(recipients, email=email_from, reason=_('Contact Email'))
|
||||
return recipients
|
||||
|
||||
def name_get(self):
|
||||
if self.env.context.get('show_partner_name'):
|
||||
return [
|
||||
(applicant.id, applicant.partner_name or applicant.name)
|
||||
for applicant in self
|
||||
]
|
||||
return super().name_get()
|
||||
|
||||
@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)
|
||||
stage = False
|
||||
if custom_values and 'job_id' in custom_values:
|
||||
stage = self.env['hr.job'].browse(custom_values['job_id'])._get_first_stage()
|
||||
partner_name, email_from = self.env['res.partner']._parse_partner_name(msg.get('from'))
|
||||
defaults = {
|
||||
'name': msg.get('subject') or _("No Subject"),
|
||||
'partner_name': partner_name or email_from,
|
||||
'email_from': email_from,
|
||||
'partner_id': msg.get('author_id', False),
|
||||
}
|
||||
if msg.get('priority'):
|
||||
defaults['priority'] = msg.get('priority')
|
||||
if stage and stage.id:
|
||||
defaults['stage_id'] = stage.id
|
||||
if custom_values:
|
||||
defaults.update(custom_values)
|
||||
return super(Applicant, self).message_new(msg, custom_values=defaults)
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
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.
|
||||
email_normalized = tools.email_normalize(self.email_from)
|
||||
new_partner = message.partner_ids.filtered(
|
||||
lambda partner: partner.email == self.email_from or (email_normalized and partner.email_normalized == email_normalized)
|
||||
)
|
||||
if new_partner:
|
||||
if new_partner[0].create_date.date() == fields.Date.today():
|
||||
new_partner[0].write({
|
||||
'type': 'private',
|
||||
'name': self.partner_name or self.email_from,
|
||||
'phone': self.partner_phone,
|
||||
'mobile': self.partner_mobile,
|
||||
})
|
||||
if new_partner[0].email_normalized:
|
||||
email_domain = ('email_from', 'in', [new_partner[0].email, new_partner[0].email_normalized])
|
||||
else:
|
||||
email_domain = ('email_from', '=', new_partner[0].email)
|
||||
self.search([
|
||||
('partner_id', '=', False), email_domain, ('stage_id.fold', '=', False)
|
||||
]).write({'partner_id': new_partner[0].id})
|
||||
return super(Applicant, self)._message_post_after_hook(message, msg_vals)
|
||||
|
||||
def create_employee_from_applicant(self):
|
||||
""" Create an employee from applicant """
|
||||
self.ensure_one()
|
||||
self._check_interviewer_access()
|
||||
|
||||
contact_name = False
|
||||
if self.partner_id:
|
||||
address_id = self.partner_id.address_get(['contact'])['contact']
|
||||
contact_name = self.partner_id.display_name
|
||||
else:
|
||||
if not self.partner_name:
|
||||
raise UserError(_('You must define a Contact Name for this applicant.'))
|
||||
new_partner_id = self.env['res.partner'].create({
|
||||
'is_company': False,
|
||||
'type': 'private',
|
||||
'name': self.partner_name,
|
||||
'email': self.email_from,
|
||||
'phone': self.partner_phone,
|
||||
'mobile': self.partner_mobile
|
||||
})
|
||||
self.partner_id = new_partner_id
|
||||
address_id = new_partner_id.address_get(['contact'])['contact']
|
||||
employee_data = {
|
||||
'default_name': self.partner_name or contact_name,
|
||||
'default_job_id': self.job_id.id,
|
||||
'default_job_title': self.job_id.name,
|
||||
'default_address_home_id': address_id,
|
||||
'default_department_id': self.department_id.id,
|
||||
'default_address_id': self.company_id.partner_id.id,
|
||||
'default_work_email': self.department_id.company_id.email or self.email_from, # To have a valid email address by default
|
||||
'default_work_phone': self.department_id.company_id.phone,
|
||||
'form_view_initial_mode': 'edit',
|
||||
'default_applicant_id': self.ids,
|
||||
}
|
||||
dict_act_window = self.env['ir.actions.act_window']._for_xml_id('hr.open_view_employee_list')
|
||||
dict_act_window['context'] = employee_data
|
||||
return dict_act_window
|
||||
|
||||
def _update_employee_from_applicant(self):
|
||||
# This method is to be overriden
|
||||
return
|
||||
|
||||
def archive_applicant(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Refuse Reason'),
|
||||
'res_model': 'applicant.get.refuse.reason',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {'default_applicant_ids': self.ids, 'active_test': False},
|
||||
'views': [[False, 'form']]
|
||||
}
|
||||
|
||||
def reset_applicant(self):
|
||||
""" Reinsert the applicant into the recruitment pipe in the first stage"""
|
||||
default_stage = dict()
|
||||
for job_id in self.mapped('job_id'):
|
||||
default_stage[job_id.id] = self.env['hr.recruitment.stage'].search(
|
||||
['|',
|
||||
('job_ids', '=', False),
|
||||
('job_ids', '=', job_id.id),
|
||||
('fold', '=', False)
|
||||
], order='sequence asc', limit=1).id
|
||||
for applicant in self:
|
||||
applicant.write(
|
||||
{'stage_id': applicant.job_id.id and default_stage[applicant.job_id.id],
|
||||
'refuse_reason_id': False})
|
||||
|
||||
def toggle_active(self):
|
||||
res = super(Applicant, self).toggle_active()
|
||||
applicant_active = self.filtered(lambda applicant: applicant.active)
|
||||
if applicant_active:
|
||||
applicant_active.reset_applicant()
|
||||
applicant_inactive = self.filtered(lambda applicant: not applicant.active)
|
||||
if applicant_inactive:
|
||||
return applicant_inactive.archive_applicant()
|
||||
return res
|
||||
|
||||
def action_send_email(self):
|
||||
return {
|
||||
'name': _('Send Email'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'applicant.send.mail',
|
||||
'context': {
|
||||
'default_applicant_ids': self.ids,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ApplicantCategory(models.Model):
|
||||
_name = "hr.applicant.category"
|
||||
_description = "Category of applicant"
|
||||
|
||||
def _get_default_color(self):
|
||||
return randint(1, 11)
|
||||
|
||||
name = fields.Char("Tag Name", required=True)
|
||||
color = fields.Integer(string='Color Index', default=_get_default_color)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
|
||||
|
||||
class ApplicantRefuseReason(models.Model):
|
||||
_name = "hr.applicant.refuse.reason"
|
||||
_description = 'Refuse Reason of Applicant'
|
||||
|
||||
name = fields.Char('Description', required=True, translate=True)
|
||||
template_id = fields.Many2one('mail.template', string='Email Template', domain="[('model', '=', 'hr.applicant')]")
|
||||
active = fields.Boolean('Active', default=True)
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrRecruitmentDegree(models.Model):
|
||||
_name = 'hr.recruitment.degree'
|
||||
_description = "Applicant Degree"
|
||||
|
||||
name = fields.Char("Degree Name", required=True, translate=True)
|
||||
score = fields.Float("Score", required=True, default=0)
|
||||
sequence = fields.Integer("Sequence", default=1)
|
||||
|
||||
_name_uniq = models.Constraint(
|
||||
'unique (name)',
|
||||
'The name of the Degree of Recruitment must be unique!',
|
||||
)
|
||||
_score_range = models.Constraint(
|
||||
'check(score >= 0 and score <= 1)',
|
||||
'Score should be between 0 and 100%',
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrRecruitmentSource(models.Model):
|
||||
_name = 'hr.recruitment.source'
|
||||
_description = "Source of Applicants"
|
||||
_inherit = ['utm.source.mixin']
|
||||
|
||||
email = fields.Char(related='alias_id.display_name', string="Email", readonly=True)
|
||||
has_domain = fields.Char(compute='_compute_has_domain')
|
||||
job_id = fields.Many2one('hr.job', "Job", index=True, ondelete='cascade')
|
||||
alias_id = fields.Many2one('mail.alias', "Alias ID", ondelete='restrict')
|
||||
medium_id = fields.Many2one('utm.medium', default=lambda self: self.env['utm.medium']._fetch_or_create_utm_medium('website'))
|
||||
campaign_id = fields.Many2one('utm.campaign')
|
||||
|
||||
def _compute_has_domain(self):
|
||||
for source in self:
|
||||
if source.alias_id:
|
||||
source.has_domain = bool(source.alias_id.alias_domain_id)
|
||||
else:
|
||||
source.has_domain = bool(source.job_id.company_id.alias_domain_id
|
||||
or self.env.company.alias_domain_id)
|
||||
|
||||
def create_alias(self):
|
||||
campaign = self.env.ref('hr_recruitment.utm_campaign_job')
|
||||
medium = self.env['utm.medium']._fetch_or_create_utm_medium('email')
|
||||
for source in self.filtered(lambda s: not s.alias_id):
|
||||
vals = {
|
||||
'alias_defaults': {
|
||||
'job_id': source.job_id.id,
|
||||
'campaign_id': campaign.id,
|
||||
'medium_id': medium.id,
|
||||
'source_id': source.source_id.id,
|
||||
},
|
||||
'alias_domain_id': source.job_id.company_id.alias_domain_id.id or self.env.company.alias_domain_id.id,
|
||||
'alias_model_id': self.env['ir.model']._get_id('hr.applicant'),
|
||||
'alias_name': f"{source.job_id.alias_name or source.job_id.name}+{source.name}",
|
||||
'alias_parent_thread_id': source.job_id.id,
|
||||
'alias_parent_model_id': self.env['ir.model']._get_id('hr.job'),
|
||||
}
|
||||
|
||||
# check that you can create source before to call mail.alias in sudo with known/controlled vals
|
||||
source.check_access('create')
|
||||
source.alias_id = self.env['mail.alias'].sudo().create(vals)
|
||||
|
||||
def create_and_get_alias(self):
|
||||
self.ensure_one()
|
||||
self.create_alias()
|
||||
return self.email
|
||||
|
||||
def unlink(self):
|
||||
""" Cascade delete aliases to avoid useless / badly configured aliases. """
|
||||
aliases = self.alias_id
|
||||
res = super().unlink()
|
||||
aliases.sudo().unlink()
|
||||
return res
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class HrRecruitmentStage(models.Model):
|
||||
_name = 'hr.recruitment.stage'
|
||||
_description = "Recruitment Stages"
|
||||
_order = 'sequence'
|
||||
|
||||
name = fields.Char("Stage Name", required=True, translate=True)
|
||||
sequence = fields.Integer(
|
||||
"Sequence", default=10)
|
||||
job_ids = fields.Many2many(
|
||||
'hr.job', string='Job Specific',
|
||||
help='Specific jobs that use this stage. Other jobs will not use this stage.')
|
||||
requirements = fields.Text("Requirements")
|
||||
template_id = fields.Many2one(
|
||||
'mail.template', "Email Template",
|
||||
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 Kanban",
|
||||
help="This stage is folded in the kanban view when there are no records in that stage to display.")
|
||||
hired_stage = fields.Boolean('Hired Stage',
|
||||
help="If checked, this stage is used to determine the hire date of an applicant")
|
||||
rotting_threshold_days = fields.Integer('Days to rot', default=0, help='Day count before applicants in this stage become stale. \
|
||||
Set to 0 to disable. Changing this parameter will not affect the rotting status/date of resources last updated before this change.')
|
||||
legend_blocked = fields.Char(
|
||||
'Red Kanban Label', default=lambda self: _('Blocked'), translate=True, required=True)
|
||||
legend_waiting = fields.Char(
|
||||
'Orange Kanban Label', default=lambda self: _('Waiting'), translate=True, required=True)
|
||||
legend_done = fields.Char(
|
||||
'Green Kanban Label', default=lambda self: _('Ready for Next Stage'), translate=True, required=True)
|
||||
legend_normal = fields.Char(
|
||||
'Grey Kanban Label', default=lambda self: _('In Progress'), translate=True, required=True)
|
||||
is_warning_visible = fields.Boolean(compute='_compute_is_warning_visible')
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
if self.env.context and self.env.context.get('default_job_id') and not self.env.context.get('hr_recruitment_stage_mono', False):
|
||||
context = dict(self.env.context)
|
||||
context.pop('default_job_id')
|
||||
self = self.with_context(context)
|
||||
return super().default_get(fields)
|
||||
|
||||
@api.depends('hired_stage')
|
||||
def _compute_is_warning_visible(self):
|
||||
applicant_data = self.env['hr.applicant']._read_group([('stage_id', 'in', self.ids)], ['stage_id'], ['__count'])
|
||||
applicants = {stage.id: count for stage, count in applicant_data}
|
||||
for stage in self:
|
||||
if stage._origin.hired_stage and not stage.hired_stage and applicants.get(stage._origin.id):
|
||||
stage.is_warning_visible = True
|
||||
else:
|
||||
stage.is_warning_visible = False
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from random import randint
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrTalentPool(models.Model):
|
||||
_name = "hr.talent.pool"
|
||||
_description = "Talent Pool"
|
||||
_inherit = ["mail.thread"]
|
||||
|
||||
def _get_default_color(self):
|
||||
return randint(1, 11)
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
name = fields.Char(string="Title", required=True, translate=True)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
tracking=True,
|
||||
)
|
||||
pool_manager = fields.Many2one(
|
||||
"res.users",
|
||||
"Pool Manager",
|
||||
default=lambda self: self.env.user,
|
||||
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
|
||||
tracking=True,
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
talent_ids = fields.Many2many(comodel_name="hr.applicant", string="Talent", groups="base.group_user")
|
||||
no_of_talents = fields.Integer(
|
||||
compute="_compute_talent_count",
|
||||
string="# Talents",
|
||||
help="The number of talents in this talent pool.",
|
||||
)
|
||||
description = fields.Html(string="Talent Pool Description")
|
||||
color = fields.Integer(string="Color", default=_get_default_color)
|
||||
categ_ids = fields.Many2many(
|
||||
comodel_name="hr.applicant.category",
|
||||
string="Tags",
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
def _compute_talent_count(self):
|
||||
talents = self.env["hr.applicant"]._read_group(
|
||||
domain=[("talent_pool_ids", "in", self.ids)], groupby=["talent_pool_ids"], aggregates=["__count"]
|
||||
)
|
||||
talent_data = {talent_pool.id: count for talent_pool, count in talents}
|
||||
for pool in self:
|
||||
pool.no_of_talents = talent_data.get(pool.id, 0)
|
||||
|
||||
def action_talent_pool_add_talents(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"name": self.env._("Create Talent"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.applicant",
|
||||
"views": [[False, "form"]],
|
||||
"context": {
|
||||
"default_talent_pool_ids": [self.id],
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.tools import SQL
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
def init(self):
|
||||
if self.env.registry.has_trigram:
|
||||
indexed_field = SQL('UNACCENT(index_content)') if self.env.registry.has_unaccent else SQL('index_content')
|
||||
|
||||
self.env.cr.execute(SQL('''
|
||||
CREATE INDEX IF NOT EXISTS ir_attachment_index_content_applicant_trgm_idx
|
||||
ON ir_attachment USING gin (%(indexed_field)s gin_trgm_ops)
|
||||
WHERE res_model = 'hr.applicant'
|
||||
''', indexed_field=indexed_field))
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
|
@ -9,7 +8,15 @@ class IrUiMenu(models.Model):
|
|||
|
||||
def _load_menus_blacklist(self):
|
||||
res = super()._load_menus_blacklist()
|
||||
job_menu = self.env.ref('hr_recruitment.menu_hr_job_position', raise_if_not_found=False)
|
||||
if job_menu and self.env.user.has_group('hr_recruitment.group_hr_recruitment_interviewer'):
|
||||
is_interviewer = self.env.user.has_group('hr_recruitment.group_hr_recruitment_interviewer')
|
||||
if not is_interviewer and (job_menu := self.env.ref('hr.menu_view_hr_job', raise_if_not_found=False)):
|
||||
res.append(job_menu.id)
|
||||
elif (
|
||||
is_interviewer
|
||||
and not self.env.user.has_group('hr_recruitment.group_hr_recruitment_user')
|
||||
and (pos_menu := self.env.ref('hr_recruitment.menu_hr_job_position', raise_if_not_found=False))
|
||||
):
|
||||
res.append(pos_menu.id)
|
||||
elif int_menu := self.env.ref('hr_recruitment.menu_hr_job_position_interviewer', raise_if_not_found=False):
|
||||
res.append(int_menu.id)
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class MailActivityPlan(models.Model):
|
||||
_inherit = 'mail.activity.plan'
|
||||
|
||||
def _compute_department_assignable(self):
|
||||
super()._compute_department_assignable()
|
||||
for plan in self:
|
||||
if not plan.department_assignable:
|
||||
plan.department_assignable = plan.res_model == 'hr.applicant'
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
job_properties_definition = fields.PropertiesDefinition("Job Properties")
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = ['res.config.settings']
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
module_website_hr_recruitment = fields.Boolean(string='Online Posting')
|
||||
module_hr_recruitment_survey = fields.Boolean(string='Interview Forms')
|
||||
group_applicant_cv_display = fields.Boolean(implied_group="hr_recruitment.group_applicant_cv_display")
|
||||
module_hr_recruitment_extract = fields.Boolean(string='Send CV to OCR to fill applications')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = "res.partner"
|
||||
|
||||
applicant_ids = fields.One2many("hr.applicant", "partner_id", string="Applicants")
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
|
|
@ -12,9 +13,9 @@ class ResUsers(models.Model):
|
|||
interviewer_group = self.env.ref('hr_recruitment.group_hr_recruitment_interviewer')
|
||||
recruitment_group = self.env.ref('hr_recruitment.group_hr_recruitment_user')
|
||||
|
||||
interviewers = self - recruitment_group.users
|
||||
interviewers = self - recruitment_group.all_user_ids
|
||||
interviewers.sudo().write({
|
||||
'groups_id': [(4, interviewer_group.id)]
|
||||
'group_ids': [(4, interviewer_group.id)]
|
||||
})
|
||||
|
||||
def _remove_recruitment_interviewers(self):
|
||||
|
|
@ -23,14 +24,14 @@ class ResUsers(models.Model):
|
|||
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}
|
||||
job_interviewers = self.env['hr.job']._read_group([('interviewer_ids', 'in', self.ids)], ['interviewer_ids'])
|
||||
user_ids = {interviewer.id for [interviewer] in job_interviewers}
|
||||
|
||||
application_interviewers = self.env['hr.applicant']._read_group([('interviewer_ids', 'in', self.ids)], ['interviewer_ids'], ['interviewer_ids'])
|
||||
user_ids |= {a['interviewer_ids'][0] for a in application_interviewers}
|
||||
application_interviewers = self.env['hr.applicant']._read_group([('interviewer_ids', 'in', self.ids)], ['interviewer_ids'])
|
||||
user_ids |= {interviewer.id for [interviewer] 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))
|
||||
users_to_remove = set(self.ids) - (user_ids | set(recruitment_group.all_user_ids.ids))
|
||||
self.env['res.users'].browse(users_to_remove).sudo().write({
|
||||
'groups_id': [(3, interviewer_group.id)]
|
||||
'group_ids': [(3, interviewer_group.id)]
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue