mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-27 16: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
|
|
@ -3,3 +3,4 @@
|
|||
|
||||
from . import hr_applicant
|
||||
from . import hr_applicant_skill
|
||||
from . import hr_job
|
||||
|
|
|
|||
|
|
@ -1,48 +1,166 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
from ast import literal_eval
|
||||
|
||||
from odoo import fields, models, Command, api
|
||||
|
||||
|
||||
class HrApplicant(models.Model):
|
||||
_inherit = 'hr.applicant'
|
||||
_inherit = "hr.applicant"
|
||||
|
||||
applicant_skill_ids = fields.One2many('hr.applicant.skill', 'applicant_id', string="Skills")
|
||||
skill_ids = fields.Many2many('hr.skill', compute='_compute_skill_ids', store=True)
|
||||
is_interviewer = fields.Boolean(compute='_compute_is_interviewer')
|
||||
applicant_skill_ids = fields.One2many(
|
||||
"hr.applicant.skill", "applicant_id", string="Skills", copy=True
|
||||
)
|
||||
current_applicant_skill_ids = fields.One2many(
|
||||
comodel_name="hr.applicant.skill",
|
||||
inverse_name="applicant_id",
|
||||
compute="_compute_current_applicant_skill_ids",
|
||||
readonly=False,
|
||||
)
|
||||
skill_ids = fields.Many2many("hr.skill", compute="_compute_skill_ids", store=True)
|
||||
matching_skill_ids = fields.Many2many(
|
||||
comodel_name="hr.skill",
|
||||
string="Matching Skills",
|
||||
compute="_compute_matching_skill_ids",
|
||||
)
|
||||
missing_skill_ids = fields.Many2many(
|
||||
comodel_name="hr.skill",
|
||||
string="Missing Skills",
|
||||
compute="_compute_matching_skill_ids",
|
||||
)
|
||||
matching_score = fields.Integer(string="Matching Score", compute="_compute_matching_skill_ids")
|
||||
|
||||
@api.depends_context('uid')
|
||||
@api.depends('interviewer_ids', 'job_id.interviewer_ids')
|
||||
def _compute_is_interviewer(self):
|
||||
is_recruiter = self.user_has_groups('hr_recruitment.group_hr_recruitment_user')
|
||||
@api.depends("applicant_skill_ids")
|
||||
def _compute_current_applicant_skill_ids(self):
|
||||
current_applicant_skill_by_applicant = self.applicant_skill_ids._get_current_skills_by_applicant()
|
||||
for applicant in self:
|
||||
applicant.is_interviewer = not is_recruiter and self.env.user in (applicant.interviewer_ids | applicant.job_id.interviewer_ids)
|
||||
applicant.current_applicant_skill_ids = current_applicant_skill_by_applicant[applicant.id]
|
||||
|
||||
@api.depends('applicant_skill_ids.skill_id')
|
||||
@api.depends("applicant_skill_ids.skill_id")
|
||||
def _compute_skill_ids(self):
|
||||
for applicant in self:
|
||||
applicant.skill_ids = applicant.applicant_skill_ids.skill_id
|
||||
|
||||
def create_employee_from_applicant(self):
|
||||
self.ensure_one()
|
||||
action = super().create_employee_from_applicant()
|
||||
action['context']['default_employee_skill_ids'] = [(0, 0, {
|
||||
'skill_id': applicant_skill.skill_id.id,
|
||||
'skill_level_id': applicant_skill.skill_level_id.id,
|
||||
'skill_type_id': applicant_skill.skill_type_id.id,
|
||||
}) for applicant_skill in self.applicant_skill_ids]
|
||||
@api.depends_context("matching_job_id")
|
||||
@api.depends("current_applicant_skill_ids", "type_id", "job_id", "job_id.job_skill_ids", "job_id.expected_degree")
|
||||
def _compute_matching_skill_ids(self):
|
||||
matching_job_id = self.env.context.get("matching_job_id")
|
||||
matching_job = self.env["hr.job"].browse(matching_job_id)
|
||||
for applicant in self:
|
||||
job = matching_job or applicant.job_id
|
||||
if not job or not (job.job_skill_ids or job.expected_degree):
|
||||
applicant.matching_skill_ids = False
|
||||
applicant.missing_skill_ids = False
|
||||
applicant.matching_score = False
|
||||
continue
|
||||
job_skills = job.job_skill_ids
|
||||
job_degree = job.expected_degree.sudo().score * 100
|
||||
job_total = sum(job_skills.mapped("level_progress")) + job_degree
|
||||
job_skill_map = {js.skill_id: js.level_progress for js in job_skills}
|
||||
|
||||
matching_applicant_skills = applicant.current_applicant_skill_ids.filtered(
|
||||
lambda a: a.skill_id in job_skill_map,
|
||||
)
|
||||
applicant_degree = applicant.type_id.score * 100 if job_degree > 1 else 0
|
||||
applicant_total = (
|
||||
sum(min(skill.level_progress, job_skill_map[skill.skill_id] * 2) for skill in matching_applicant_skills)
|
||||
+ applicant_degree
|
||||
)
|
||||
|
||||
matching_skill_ids = matching_applicant_skills.mapped("skill_id")
|
||||
missing_skill_ids = job_skills.mapped("skill_id") - matching_applicant_skills.mapped("skill_id")
|
||||
matching_score = round(applicant_total / job_total * 100) if job_total else 0
|
||||
|
||||
applicant.matching_skill_ids = matching_skill_ids
|
||||
applicant.missing_skill_ids = missing_skill_ids
|
||||
applicant.matching_score = matching_score
|
||||
|
||||
def _get_employee_create_vals(self):
|
||||
vals = super()._get_employee_create_vals()
|
||||
vals["employee_skill_ids"] = [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"skill_id": applicant_skill.skill_id.id,
|
||||
"skill_level_id": applicant_skill.skill_level_id.id,
|
||||
"skill_type_id": applicant_skill.skill_type_id.id,
|
||||
},
|
||||
)
|
||||
for applicant_skill in self.applicant_skill_ids
|
||||
]
|
||||
return vals
|
||||
|
||||
def _map_applicant_skill_ids_to_talent_skill_ids(self, vals):
|
||||
"""
|
||||
The applicant_skills_ids contains a list of ORM tuples i.e (command, record ID, {values})
|
||||
The challenge lies in the uniqueness of the record ID in this tuple. Each skill (e.g., 'arabic')
|
||||
has a distinct ID per applicant, i.e arabic in applicant 1 will have a different id from arabic in
|
||||
applicant 2. This means the content of applicant_skills_ids is unique for each record and attempting
|
||||
to pass it directly (e.g., applicant.pool_applicant_id.write(vals)) won't yield results so we must
|
||||
update each tuple to have the correct command and record ID for the talent pool applicant
|
||||
|
||||
:param vals: list of CREATE, WRITE or UNLINK commands with skill_ids relevant to the applicant
|
||||
:return: list of CREATE, WRITE or UNLINK commands with skill_ids relevant to the pool_applicant
|
||||
"""
|
||||
applicant_skills = {a.id: a.skill_id.id for a in self.applicant_skill_ids}
|
||||
applicant_skills_type = {a.id: a.skill_type_id.id for a in self.applicant_skill_ids}
|
||||
talent_skills = {a.skill_id.id: a.id for a in self.pool_applicant_id.applicant_skill_ids}
|
||||
mapped_commands = []
|
||||
for command in vals.get("applicant_skill_ids"):
|
||||
command_number = command[0]
|
||||
record_id = command[1]
|
||||
if command_number == Command.UPDATE:
|
||||
values = command[2]
|
||||
if applicant_skills[record_id] in talent_skills:
|
||||
mapped_command = Command.update(talent_skills[applicant_skills[record_id]], values)
|
||||
mapped_commands.append(mapped_command)
|
||||
else:
|
||||
mapped_command = Command.create(
|
||||
{
|
||||
"skill_id": applicant_skills[record_id],
|
||||
"skill_type_id": applicant_skills_type[record_id],
|
||||
"skill_level_id": values["skill_level_id"],
|
||||
},
|
||||
)
|
||||
mapped_commands.append(mapped_command)
|
||||
elif command_number == Command.DELETE:
|
||||
if applicant_skills[record_id] in talent_skills:
|
||||
mapped_command = Command.delete(talent_skills[applicant_skills[record_id]])
|
||||
mapped_commands.append(mapped_command)
|
||||
else:
|
||||
mapped_commands.append(command)
|
||||
return mapped_commands
|
||||
|
||||
def action_add_to_job(self):
|
||||
self.with_context(just_moved=True).write(
|
||||
{
|
||||
"job_id": self.env["hr.job"].browse(self.env.context.get("matching_job_id")).id,
|
||||
"stage_id": self.env.ref("hr_recruitment.stage_job0").id,
|
||||
}
|
||||
)
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_recruitment.action_hr_job_applications")
|
||||
action["context"] = literal_eval(action["context"].replace("active_id", str(self.job_id.id)))
|
||||
return action
|
||||
|
||||
def _update_employee_from_applicant(self):
|
||||
vals_list = []
|
||||
for applicant in self:
|
||||
existing_skills = applicant.emp_id.employee_skill_ids.skill_id
|
||||
skills_to_create = applicant.applicant_skill_ids.skill_id - existing_skills
|
||||
vals_list.extend([{
|
||||
'employee_id': applicant.emp_id.id,
|
||||
'skill_id': skill.id,
|
||||
'skill_level_id': applicant.applicant_skill_ids.filtered(lambda s: s.skill_id == skill).skill_level_id.id,
|
||||
'skill_type_id': skill.skill_type_id.id,
|
||||
} for skill in skills_to_create])
|
||||
self.env['hr.employee.skill'].create(vals_list)
|
||||
return super()._update_employee_from_applicant()
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if not self:
|
||||
# This is required for the talent pool mechanism to work. Duplicating an hr.applicant record without this
|
||||
# check will cause the skills to not be duplicated or disappear randomly.
|
||||
for vals in vals_list:
|
||||
vals["applicant_skill_ids"] = vals.pop("current_applicant_skill_ids", []) + vals.get("applicant_skill_ids", [])
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
if "current_applicant_skill_ids" in vals or "applicant_skill_ids" in vals:
|
||||
skills = vals.pop("current_applicant_skill_ids", []) + vals.get("applicant_skill_ids", [])
|
||||
original_vals = vals.copy()
|
||||
original_vals["applicant_skill_ids"] = skills
|
||||
vals["applicant_skill_ids"] = self.env["hr.applicant.skill"]._get_transformed_commands(skills, self)
|
||||
for applicant in self:
|
||||
# Modify the skill values for the talent if it exists
|
||||
if applicant.pool_applicant_id and (not applicant.is_pool_applicant):
|
||||
mapped_skills = applicant._map_applicant_skill_ids_to_talent_skill_ids(original_vals)
|
||||
applicant.pool_applicant_id.write({"applicant_skill_ids": mapped_skills})
|
||||
return super().write(vals)
|
||||
|
|
|
|||
|
|
@ -1,67 +1,36 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ApplicantSkill(models.Model):
|
||||
class HrApplicantSkill(models.Model):
|
||||
_name = 'hr.applicant.skill'
|
||||
_inherit = "hr.individual.skill.mixin"
|
||||
_description = "Skill level for an applicant"
|
||||
_rec_name = 'skill_id'
|
||||
_order = "skill_level_id"
|
||||
_order = "skill_type_id, skill_level_id desc"
|
||||
|
||||
applicant_id = fields.Many2one(
|
||||
comodel_name='hr.applicant',
|
||||
comodel_name="hr.applicant",
|
||||
required=True,
|
||||
ondelete='cascade')
|
||||
skill_id = fields.Many2one(
|
||||
comodel_name='hr.skill',
|
||||
compute='_compute_skill_id',
|
||||
store=True,
|
||||
domain="[('skill_type_id', '=', skill_type_id)]",
|
||||
readonly=False,
|
||||
required=True)
|
||||
skill_level_id = fields.Many2one(
|
||||
comodel_name='hr.skill.level',
|
||||
compute='_compute_skill_level_id',
|
||||
domain="[('skill_type_id', '=', skill_type_id)]",
|
||||
store=True,
|
||||
readonly=False,
|
||||
required=True)
|
||||
skill_type_id = fields.Many2one(
|
||||
comodel_name='hr.skill.type',
|
||||
required=True)
|
||||
level_progress = fields.Integer(
|
||||
related='skill_level_id.level_progress')
|
||||
index=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('_unique_skill', 'unique (applicant_id, skill_id)', "Two levels for the same skill is not allowed"),
|
||||
]
|
||||
def _linked_field_name(self):
|
||||
return "applicant_id"
|
||||
|
||||
@api.constrains('skill_id', 'skill_type_id')
|
||||
def _check_skill_type(self):
|
||||
for applicant in self:
|
||||
if applicant.skill_id not in applicant.skill_type_id.skill_ids:
|
||||
raise ValidationError(_("The skill %(name)s and skill type %(type)s doesn't match", name=applicant.skill_id.name, type=applicant.skill_type_id.name))
|
||||
|
||||
@api.constrains('skill_type_id', 'skill_level_id')
|
||||
def _check_skill_level(self):
|
||||
for applicant in self:
|
||||
if applicant.skill_level_id not in applicant.skill_type_id.skill_level_ids:
|
||||
raise ValidationError(_("The skill level %(level)s is not valid for skill type: %(type)s", level=applicant.skill_level_id.name, type=applicant.skill_type_id.name))
|
||||
|
||||
@api.depends('skill_type_id')
|
||||
def _compute_skill_id(self):
|
||||
for applicant in self:
|
||||
if applicant.skill_id.skill_type_id != applicant.skill_type_id:
|
||||
applicant.skill_id = False
|
||||
|
||||
@api.depends('skill_id')
|
||||
def _compute_skill_level_id(self):
|
||||
for applicant in self:
|
||||
if not applicant.skill_id:
|
||||
applicant.skill_level_id = False
|
||||
else:
|
||||
skill_levels = applicant.skill_type_id.skill_level_ids
|
||||
applicant.skill_level_id = skill_levels.filtered('default_level') or skill_levels[0] if skill_levels else False
|
||||
def _get_current_skills_by_applicant(self):
|
||||
applicant_skill_grouped = self.grouped(lambda a_s: (a_s.applicant_id, a_s.skill_id))
|
||||
result_dict = defaultdict(lambda: self.env["hr.applicant.skill"])
|
||||
for (applicant, skill), applicant_skills in applicant_skill_grouped.items():
|
||||
filtered_applicant_skills = applicant_skills.filtered(
|
||||
lambda a_s: not a_s.valid_to or a_s.valid_to >= fields.Date.today(),
|
||||
)
|
||||
if skill.skill_type_id.is_certification and not filtered_applicant_skills:
|
||||
most_recent_certification = max(applicant_skills, key=lambda a_s: a_s.valid_to)
|
||||
result_dict[applicant.id] += most_recent_certification
|
||||
continue
|
||||
result_dict[applicant.id] += filtered_applicant_skills
|
||||
return result_dict
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
from ast import literal_eval
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrJob(models.Model):
|
||||
_inherit = "hr.job"
|
||||
|
||||
applicant_matching_score = fields.Float(string="Matching Score(%)", compute="_compute_applicant_matching_score",
|
||||
groups="hr_recruitment.group_hr_recruitment_interviewer")
|
||||
|
||||
@api.depends_context("active_applicant_id")
|
||||
def _compute_applicant_matching_score(self):
|
||||
active_applicant_id = self.env.context.get("active_applicant_id")
|
||||
if not active_applicant_id:
|
||||
for job in self:
|
||||
job.applicant_matching_score = False
|
||||
return
|
||||
|
||||
applicant = self.env["hr.applicant"].browse(active_applicant_id)
|
||||
for job in self:
|
||||
if not job.job_skill_ids:
|
||||
job.applicant_matching_score = False
|
||||
continue
|
||||
job_skills = job.job_skill_ids
|
||||
job_degree = job.expected_degree.score * 100
|
||||
job_total = sum(job.job_skill_ids.mapped("level_progress")) + job_degree
|
||||
job_skill_map = {js.skill_id.id: js.level_progress for js in job_skills}
|
||||
|
||||
matching_applicant_skills = applicant.current_applicant_skill_ids.filtered(
|
||||
lambda a: a.skill_id.id in job_skill_map,
|
||||
)
|
||||
applicant_degree = applicant.type_id.score * 100 if job_degree > 1 else 0
|
||||
applicant_total = (
|
||||
sum(
|
||||
min(skill.level_progress, job_skill_map[skill.skill_id.id] * 2)
|
||||
for skill in matching_applicant_skills
|
||||
)
|
||||
+ applicant_degree
|
||||
)
|
||||
|
||||
job.applicant_matching_score = applicant_total / job_total * 100
|
||||
|
||||
def action_search_matching_applicants(self):
|
||||
self.ensure_one()
|
||||
help_message_1 = self.env._("No Matching Applicants")
|
||||
help_message_2 = self.env._("We do not have any applicants who meet the skill requirements for this job position in the database at the moment.")
|
||||
action = self.env['ir.actions.actions']._for_xml_id('hr_recruitment.crm_case_categ0_act_job')
|
||||
context = literal_eval(action['context'])
|
||||
context['matching_job_id'] = self.id
|
||||
action.update({
|
||||
'name': self.env._("Matching Applicants"),
|
||||
'views': [
|
||||
(self.env.ref('hr_recruitment_skills.crm_case_tree_view_inherit_hr_recruitment_skills').id, 'list'),
|
||||
(False, 'form'),
|
||||
],
|
||||
'context': context,
|
||||
'domain': [
|
||||
('job_id', '!=', self.id),
|
||||
('skill_ids', 'in', self.job_skill_ids.skill_id.ids),
|
||||
],
|
||||
'help': Markup("<p class='o_view_nocontent_empty_folder'>%s</p><p>%s</p>") % (help_message_1, help_message_2),
|
||||
})
|
||||
return action
|
||||
Loading…
Add table
Add a link
Reference in a new issue