diff --git a/addons/hr_recruitment_skills/__init__.py b/addons/hr_recruitment_skills/__init__.py new file mode 100644 index 00000000000..2ae6446f9dc --- /dev/null +++ b/addons/hr_recruitment_skills/__init__.py @@ -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 diff --git a/addons/hr_recruitment_skills/__manifest__.py b/addons/hr_recruitment_skills/__manifest__.py new file mode 100644 index 00000000000..22edffe4962 --- /dev/null +++ b/addons/hr_recruitment_skills/__manifest__.py @@ -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', +} diff --git a/addons/hr_recruitment_skills/models/__init__.py b/addons/hr_recruitment_skills/models/__init__.py new file mode 100644 index 00000000000..c8c748f8c46 --- /dev/null +++ b/addons/hr_recruitment_skills/models/__init__.py @@ -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 diff --git a/addons/hr_recruitment_skills/models/hr_applicant.py b/addons/hr_recruitment_skills/models/hr_applicant.py new file mode 100644 index 00000000000..84a36bae4de --- /dev/null +++ b/addons/hr_recruitment_skills/models/hr_applicant.py @@ -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 diff --git a/addons/hr_recruitment_skills/models/hr_applicant_skill.py b/addons/hr_recruitment_skills/models/hr_applicant_skill.py new file mode 100644 index 00000000000..9dc9c059092 --- /dev/null +++ b/addons/hr_recruitment_skills/models/hr_applicant_skill.py @@ -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 diff --git a/addons/hr_recruitment_skills/security/ir.model.access.csv b/addons/hr_recruitment_skills/security/ir.model.access.csv new file mode 100644 index 00000000000..8d8fc3ef6bf --- /dev/null +++ b/addons/hr_recruitment_skills/security/ir.model.access.csv @@ -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 \ No newline at end of file diff --git a/addons/hr_recruitment_skills/static/src/js/hr_applicant_kanban_view.js b/addons/hr_recruitment_skills/static/src/js/hr_applicant_kanban_view.js new file mode 100644 index 00000000000..4902f8488fd --- /dev/null +++ b/addons/hr_recruitment_skills/static/src/js/hr_applicant_kanban_view.js @@ -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; +}); diff --git a/addons/hr_recruitment_skills/static/src/xml/hr_applicant_kanban_view.xml b/addons/hr_recruitment_skills/static/src/xml/hr_applicant_kanban_view.xml new file mode 100644 index 00000000000..d9bc679010c --- /dev/null +++ b/addons/hr_recruitment_skills/static/src/xml/hr_applicant_kanban_view.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/addons/hr_recruitment_skills/views/hr_applicant_skill_views.xml b/addons/hr_recruitment_skills/views/hr_applicant_skill_views.xml new file mode 100644 index 00000000000..691d3b1d8b5 --- /dev/null +++ b/addons/hr_recruitment_skills/views/hr_applicant_skill_views.xml @@ -0,0 +1,38 @@ + + + + hr.applicant.skill.view.form + hr.applicant.skill + +
+ + + + + + + + + + + +
+
+
+
diff --git a/addons/hr_recruitment_skills/views/hr_applicant_views.xml b/addons/hr_recruitment_skills/views/hr_applicant_views.xml new file mode 100644 index 00000000000..b308e82167c --- /dev/null +++ b/addons/hr_recruitment_skills/views/hr_applicant_views.xml @@ -0,0 +1,60 @@ + + + + hr.applicant.view.form.inherit.hr.recruitment.skills + hr.applicant + + + + +
+
+ + + + + + + + + +
+
+
+
+
+
+ + + hr.applicant.view.search.inherit.skills.bis + hr.applicant + + + + + + + + + + hr.applicant.view.search.inherit.skills + hr.applicant + + + + + + + + + + hr.applicant.view.kanban.inherit.skills + hr.applicant + + + + hr_applicant_kanban + + + +
diff --git a/addons/hr_recruitment_skills/wizard/__init__.py b/addons/hr_recruitment_skills/wizard/__init__.py new file mode 100644 index 00000000000..eb20c541672 --- /dev/null +++ b/addons/hr_recruitment_skills/wizard/__init__.py @@ -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 diff --git a/addons/hr_recruitment_skills/wizard/hr_recruitment_populate_wizard.py b/addons/hr_recruitment_skills/wizard/hr_recruitment_populate_wizard.py new file mode 100644 index 00000000000..ade6774fdc4 --- /dev/null +++ b/addons/hr_recruitment_skills/wizard/hr_recruitment_populate_wizard.py @@ -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 diff --git a/addons/hr_recruitment_skills/wizard/hr_recruitment_populate_wizard_views.xml b/addons/hr_recruitment_skills/wizard/hr_recruitment_populate_wizard_views.xml new file mode 100644 index 00000000000..37ca3ec32c8 --- /dev/null +++ b/addons/hr_recruitment_skills/wizard/hr_recruitment_populate_wizard_views.xml @@ -0,0 +1,38 @@ + + + + hr.recruitment.populate.wizard.view.form + hr.recruitment.populate.wizard + +
+ +
+ 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. +
+ + + + + + + + + +
+
+
+
+
+
+ + + Populate Job Position + hr.recruitment.populate.wizard + form + new + +