[ADD] hr_recruitment_skills: Manage skills for applicants

This commit also add a wizard on the applicant kanban view to
search applicants in the reserve based on their skills, degree,
tags and availability and move them on the recruitment pipe.

closes odoo/odoo#93126

Taskid: 2877672
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Yannick Tivisse
2022-06-09 15:26:22 +02:00
parent 16ee5235e0
commit abef5a7fb2
13 changed files with 368 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import wizard
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Recruitment - Skills Management',
'category': 'Human Resources/Recruitment',
'sequence': 270,
'version': '1.0',
'summary': 'Manage skills of your employees',
'description': """""",
'depends': ['hr_skills', 'hr_recruitment'],
'data': [
'views/hr_applicant_views.xml',
'views/hr_applicant_skill_views.xml',
'security/ir.model.access.csv',
'wizard/hr_recruitment_populate_wizard_views.xml',
],
'demo': [
],
'installable': True,
'application': False,
'auto_install': True,
'assets': {
'web.assets_backend': [
'hr_recruitment_skills/static/src/**/*.js',
],
'web.assets_qweb': [
'hr_recruitment_skills/static/src/xml/**/*',
'hr_recruitment_skills/static/src/**/*.xml',
],
},
'license': 'LGPL-3',
}
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_applicant
from . import hr_applicant_skill
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class HrApplicant(models.Model):
_inherit = 'hr.applicant'
applicant_skill_ids = fields.One2many('hr.applicant.skill', 'applicant_id', string="Skills")
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]
return action
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class ApplicantSkill(models.Model):
_name = 'hr.applicant.skill'
_description = "Skill level for an applicant"
_rec_name = 'skill_id'
_order = "skill_level_id"
applicant_id = fields.Many2one(
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')
_sql_constraints = [
('_unique_skill', 'unique (applicant_id, skill_id)', "Two levels for the same skill is not allowed"),
]
@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
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
hr_recruitment_skills.access_hr_applicant_skill,access_hr_applicant_skill,hr_recruitment_skills.model_hr_applicant_skill,hr_recruitment.group_hr_recruitment_user,1,1,1,1
hr_recruitment_skills.access_hr_recruitment_populate_wizard,access_hr_recruitment_populate_wizard,hr_recruitment_skills.model_hr_recruitment_populate_wizard,hr_recruitment.group_hr_recruitment_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 hr_recruitment_skills.access_hr_applicant_skill access_hr_applicant_skill hr_recruitment_skills.model_hr_applicant_skill hr_recruitment.group_hr_recruitment_user 1 1 1 1
3 hr_recruitment_skills.access_hr_recruitment_populate_wizard access_hr_recruitment_populate_wizard hr_recruitment_skills.model_hr_recruitment_populate_wizard hr_recruitment.group_hr_recruitment_user 1 1 1 0
@@ -0,0 +1,41 @@
odoo.define('hr.applicant.view.kanban', function (require) {
"use strict";
var core = require('web.core');
var KanbanController = require('web.KanbanController');
var KanbanView = require('web.KanbanView');
var viewRegistry = require('web.view_registry');
var QWeb = core.qweb;
var ApplicantKanbanController = KanbanController.extend({
/**
* Extends the renderButtons function of KanbanView by adding a button
* on the applicant view.
*
* @override
*/
renderButtons: function () {
this._super.apply(this, arguments);
this.$buttons.append($(QWeb.render("ApplicantKanbanView.populate_button", this)));
var self = this;
this.$buttons.on('click', '.o_button_recruitment_populate', function () {
self.do_action('hr_recruitment_skills.hr_recruitment_populate_wizard_action', {
additional_context: {active_id: self.model.get(self.handle).context.active_id},
on_close: function () {
self.trigger_up('reload');
}
});
});
}
});
var PayslipKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
Controller: ApplicantKanbanController,
}),
});
viewRegistry.add('hr_applicant_kanban', PayslipKanbanView);
return ApplicantKanbanController;
});
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="ApplicantKanbanView.populate_button">
<button type="button" class="btn btn-secondary o_button_recruitment_populate">
Populate
</button>
</t>
</templates>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_applicant_skill_view_form" model="ir.ui.view">
<field name="name">hr.applicant.skill.view.form</field>
<field name="model">hr.applicant.skill</field>
<field name="arch" type="xml">
<form string="Skills">
<sheet>
<group>
<group>
<field name="applicant_id" invisible="1"/>
<field name="skill_type_id" widget="radio" />
</group>
<group>
<field name="skill_id" options="{'no_open': True, 'no_create_edit': True}"
context="{'default_skill_type_id': skill_type_id}"
domain="[('skill_type_id', '=', skill_type_id)]"
attrs="{'invisible': [('skill_type_id', '=', False)]}"/>
<label for="skill_level_id"
attrs="{'invisible': ['|', ('skill_id', '=', False), ('skill_type_id', '=', False)]}"/>
<div class="o_row"
attrs="{'invisible': ['|', ('skill_id', '=', False), ('skill_type_id', '=', False)]}">
<span class="col-4 pl-0">
<field name="skill_level_id"
attrs="{'readonly': [('skill_id', '=', False)]}"
context="{'from_skill_level_dropdown': True}" />
</span>
<span class="col-8">
<field name="level_progress" widget="progressbar" class="o_hr_skills_progress" attrs="{'invisible': [('skill_level_id', '=', False)]}" />
</span>
</div>
</group>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_applicant_view_form" model="ir.ui.view">
<field name="name">hr.applicant.view.form.inherit.hr.recruitment.skills</field>
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.hr_applicant_view_form"/>
<field name="arch" type="xml">
<notebook position="inside">
<page string="Skills">
<div class="row">
<div class="o_hr_skills_editable o_hr_skills_group o_group_skills col-lg-5 d-flex flex-column">
<field name="id" invisible="1"/>
<field mode="tree" nolabel="1" name="applicant_skill_ids" widget="hr_skills" context="{'default_applicant_id': id}">
<tree>
<field name="skill_type_id" invisible="1"/>
<field name="skill_id"/>
<field name="skill_level_id"/>
<field name="level_progress" widget="progressbar"/>
</tree>
</field>
</div>
</div>
</page>
</notebook>
</field>
</record>
<record id="hr_applicant_view_search_bis" model="ir.ui.view">
<field name="name">hr.applicant.view.search.inherit.skills.bis</field>
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.hr_applicant_view_search_bis"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='email_from']" position="after">
<field name="applicant_skill_ids"/>
</xpath>
</field>
</record>
<record id="hr_applicant_view_search" model="ir.ui.view">
<field name="name">hr.applicant.view.search.inherit.skills</field>
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.hr_applicant_view_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='user_id']" position="after">
<field name="applicant_skill_ids"/>
</xpath>
</field>
</record>
<record id="hr_applicant_view_kanban" model="ir.ui.view">
<field name="name">hr.applicant.view.kanban.inherit.skills</field>
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.hr_kanban_view_applicant"/>
<field name="arch" type="xml">
<xpath expr="//kanban" position="attributes">
<attribute name="js_class">hr_applicant_kanban</attribute>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_recruitment_populate_wizard
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import fields, models
from odoo.osv.expression import AND
class HrRecruitmentPopulateWizard(models.TransientModel):
_name = 'hr.recruitment.populate.wizard'
_description = 'Populate Job Position Wizard'
job_id = fields.Many2one('hr.job', default=lambda self: self.env.context.get('active_id'))
skill_ids = fields.Many2many('hr.skill', string="Skills")
categ_ids = fields.Many2many('hr.applicant.category', string="Tags")
degree_id = fields.Many2one('hr.recruitment.degree', "Degree") # on hr.applicant: type_id
availability = fields.Selection(
selection=[
('now', 'Immediately'),
('quarter', 'In the next 3 months'),
('year', 'In the coming year'),
],
required=True,
default='now')
def action_validate(self):
self.ensure_one()
domain = [('job_id', '=', False)]
if self.skill_ids:
domain = AND([domain, [('applicant_skill_ids.skill_id', 'in', self.skill_ids.ids)]])
if self.categ_ids:
domain = AND([domain, [('categ_ids', 'in', self.categ_ids.ids)]])
if self.degree_id:
domain = AND([domain, [('type_id', '=', self.degree_id.id)]])
today = fields.Date.today()
if self.availability == 'now':
domain = AND([domain, ['|', ('availability', '=', False), ('availability', '<=', today)]])
elif self.availability == 'quarter':
domain = AND([domain, ['|', ('availability', '=', False), ('availability', '<=', today + relativedelta(months=3))]])
else:
domain = AND([domain, ['|', ('availability', '=', False), ('availability', '<=', today + relativedelta(years=1))]])
reserve = self.env['hr.applicant'].search(domain)
reserve.job_id = self.job_id
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="hr_recruitment_populate_wizard_view_form" model="ir.ui.view">
<field name="name">hr.recruitment.populate.wizard.view.form</field>
<field name="model">hr.recruitment.populate.wizard</field>
<field name="arch" type="xml">
<form string="Populate Job Position">
<sheet>
<div class="text-muted mb-2">
Search among all the available applicants in the reserve pool
based on one or more criteria.
The applicants will be automatically assigned to this job position.
</div>
<group>
<group>
<field name="job_id" invisible="1"/>
<field name="skill_ids" widget="many2many_tags"/>
<field name="categ_ids" widget="many2many_tags"/>
<field name="degree_id"/>
<field name="availability"/>
</group>
</group>
</sheet>
<footer>
<button string="Populate" name="action_validate" type="object" class="btn-primary"/>
<button special="cancel" string="Close" type="object" class="btn-primary"/>
</footer>
</form>
</field>
</record>
<record id="hr_recruitment_populate_wizard_action" model="ir.actions.act_window">
<field name="name">Populate Job Position</field>
<field name="res_model">hr.recruitment.populate.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>