[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:
@@ -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
|
||||
|
@@ -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>
|
||||
Reference in New Issue
Block a user