diff --git a/addons/hr/models/hr_job.py b/addons/hr/models/hr_job.py index a9f0eda96a4..065a696d5a8 100644 --- a/addons/hr/models/hr_job.py +++ b/addons/hr/models/hr_job.py @@ -11,13 +11,14 @@ class Job(models.Model): _inherit = ['mail.thread'] _order = 'sequence' + active = fields.Boolean(default=True) name = fields.Char(string='Job Position', required=True, index='trigram', translate=True) sequence = fields.Integer(default=10) expected_employees = fields.Integer(compute='_compute_employees', string='Total Forecasted Employees', store=True, help='Expected number of employees for this job position after new recruitment.') no_of_employee = fields.Integer(compute='_compute_employees', string="Current Number of Employees", store=True, help='Number of employees currently occupying this job position.') - no_of_recruitment = fields.Integer(string='Expected New Employees', copy=False, + no_of_recruitment = fields.Integer(string='Target', copy=False, help='Number of new employees you expect to recruit.', default=1) no_of_hired_employee = fields.Integer(string='Hired Employees', copy=False, help='Number of hired employees for this job position during recruitment phase.') @@ -26,10 +27,6 @@ class Job(models.Model): requirements = fields.Text('Requirements') department_id = fields.Many2one('hr.department', string='Department', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) - state = fields.Selection([ - ('recruit', 'Recruitment in Progress'), - ('open', 'Not Recruiting') - ], string='Status', readonly=True, required=True, tracking=True, copy=False, default='recruit', help="Set whether the recruitment process is open or closed for this job position.") _sql_constraints = [ ('name_company_uniq', 'unique(name, company_id, department_id)', 'The name of the job position must be unique per department in company!'), @@ -56,16 +53,3 @@ class Job(models.Model): if 'name' not in default: default['name'] = _("%s (copy)") % (self.name) return super(Job, self).copy(default=default) - - def set_recruit(self): - for record in self: - no_of_recruitment = 1 if record.no_of_recruitment == 0 else record.no_of_recruitment - record.write({'state': 'recruit', 'no_of_recruitment': no_of_recruitment}) - return True - - def set_open(self): - return self.write({ - 'state': 'open', - 'no_of_recruitment': 0, - 'no_of_hired_employee': 0 - }) diff --git a/addons/hr/tests/__init__.py b/addons/hr/tests/__init__.py index 17a4487628d..0fe90a1ae29 100644 --- a/addons/hr/tests/__init__.py +++ b/addons/hr/tests/__init__.py @@ -3,7 +3,6 @@ from . import test_hr_employee from . import test_channel -from . import test_hr_flow from . import test_self_user_access from . import test_multi_company from . import test_resource diff --git a/addons/hr/tests/test_hr_flow.py b/addons/hr/tests/test_hr_flow.py deleted file mode 100644 index 22edd44f11a..00000000000 --- a/addons/hr/tests/test_hr_flow.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.addons.hr.tests.common import TestHrCommon - - -class TestHrFlow(TestHrCommon): - - @classmethod - def setUpClass(cls): - super(TestHrFlow, cls).setUpClass() - cls.dep_rd = cls.env['hr.department'].create({ - 'name': 'Research & Development', - }) - cls.job_developer = cls.env['hr.job'].create({ - 'name': 'Experienced Developer', - 'department_id': cls.dep_rd.id, - 'no_of_recruitment': 5, - }) - cls.employee_niv = cls.env['hr.employee'].create({ - 'name': 'Sharlene Rhodes', - }) - cls.job_developer = cls.job_developer.with_user(cls.res_users_hr_officer.id) - cls.employee_niv = cls.employee_niv.with_user(cls.res_users_hr_officer.id) - - def test_open2recruit2close_job(self): - - """ Opening the job position for "Developer" and checking the job status and recruitment count. """ - self.job_developer.set_open() - self.assertEqual(self.job_developer.state, 'open', "Job position of 'Job Developer' is in 'open' state.") - self.assertEqual(self.job_developer.no_of_recruitment, 0, - "Wrong number of recruitment for the job 'Job Developer'(%s found instead of 0)." - % self.job_developer.no_of_recruitment) - - """ Recruiting employee "NIV" for the job position "Developer" and checking the job status and recruitment count. """ - self.job_developer.set_recruit() - self.assertEqual(self.job_developer.state, 'recruit', "Job position of 'Job Developer' is in 'recruit' state.") - self.assertEqual(self.job_developer.no_of_recruitment, 1, - "Wrong number of recruitment for the job 'Job Developer'(%s found instead of 1.0)." - % self.job_developer.no_of_recruitment) - - self.employee_niv.write({'job_id': self.job_developer.id}) - - """ Closing the recruitment for the job position "Developer" by marking it as open. """ - self.job_developer.set_open() - self.assertEqual(self.job_developer.state, 'open', "Job position of 'Job Developer' is in 'open' state.") - self.assertEqual(self.job_developer.no_of_recruitment, 0, - "Wrong number of recruitment for the job 'Job Developer'(%s found instead of 0)." - % self.job_developer.no_of_recruitment) diff --git a/addons/hr/views/hr_job_views.xml b/addons/hr/views/hr_job_views.xml index afe5da5dbf6..695a2f91a2c 100644 --- a/addons/hr/views/hr_job_views.xml +++ b/addons/hr/views/hr_job_views.xml @@ -7,11 +7,7 @@ hr.job
-
-
+
@@ -19,9 +15,6 @@

- - - @@ -29,10 +22,17 @@ - + + + +
@@ -50,13 +50,12 @@ - - - - + + + - + @@ -77,9 +76,6 @@
Vacancies : - - -
@@ -95,13 +91,12 @@ - - + + - diff --git a/addons/hr_recruitment/__manifest__.py b/addons/hr_recruitment/__manifest__.py index d97e6d503ec..80075d8b906 100644 --- a/addons/hr_recruitment/__manifest__.py +++ b/addons/hr_recruitment/__manifest__.py @@ -33,6 +33,7 @@ 'views/mail_activity_views.xml', 'views/digest_views.xml', 'wizard/applicant_refuse_reason_views.xml', + 'wizard/applicant_send_mail_views.xml', ], 'demo': [ 'data/hr_recruitment_demo.xml', diff --git a/addons/hr_recruitment/data/hr_recruitment_demo.xml b/addons/hr_recruitment/data/hr_recruitment_demo.xml index b82f048b5e3..e5b218820f4 100644 --- a/addons/hr_recruitment/data/hr_recruitment_demo.xml +++ b/addons/hr_recruitment/data/hr_recruitment_demo.xml @@ -1,42 +1,37 @@ - - - + - recruit 4 56 + - open 1 - open 1 + - recruit 1 17 + 1 - recruit 5 - recruit 3 2 + - recruit 6 @@ -85,8 +80,11 @@ 1 Enrique Jones + enrique.jones152@gmail.example.com 9963214587 - + + + Sales @@ -97,6 +95,7 @@ 1 Meldona Thang + thing.thang.thong@gmail.example.com 998655451 @@ -110,6 +109,7 @@ 3 Johan Duck + coincoin@gmail.example.com 8955545 @@ -122,6 +122,7 @@ 0 Kelly Wallant + kelly@wallant.example.com 879895515 @@ -135,6 +136,7 @@ 0 Cécile Donth + c-cile72@msn.example.com 98765411 @@ -147,6 +149,7 @@ 0 Ohen Rizome + 0h3n-rijaune@yahoo.example.com 654687987654 @@ -159,9 +162,12 @@ 2 Marie Justine + justinemarie@outlook.example.com 9988774455 6633225 + + Fresher @@ -172,6 +178,7 @@ 0 Jose + the.jose@gmail.example.com 999666735 @@ -183,6 +190,7 @@ Yin Lee + yin.lee@wechat.example.com @@ -193,6 +201,7 @@ Hubert Blank + st-hubertus@gmail.example.com 3 @@ -205,7 +214,10 @@ John Bruno + johnnyboy@gmail.example.com + + More than 5 yrs Experience in PHP @@ -216,7 +228,10 @@ Sandra Elvis + sandra.elvis.the.king25@gmail.example.com + + Finance Manager @@ -227,6 +242,7 @@ 1 David Armstrong + david.strongarm@gmail.example.com 33968745 @@ -239,7 +255,10 @@ 1 Joren Jacob + joren.jacob@outlook.example.com + + Trainee - MCA @@ -248,10 +267,13 @@ Tina Augustie + tina.turner@gmail.example.com 9898745745 6630125 + + Programmer @@ -261,10 +283,13 @@ Shane Williams + the.real.shane@gmail.example.com 9812398524 6630125 11000.0 + + Advertisement @@ -274,9 +299,12 @@ David Billy + billy.boy12@gmail.example.com 9988774455 11000.0 + + @@ -309,6 +337,7 @@ Regarding reference hr.applicant +

Hello!
I will surely refer to this application as it is by your reference and
@@ -460,6 +489,4 @@ - - diff --git a/addons/hr_recruitment/models/hr_job.py b/addons/hr_recruitment/models/hr_job.py index f308ac69656..38b2d2b7a0e 100644 --- a/addons/hr_recruitment/models/hr_job.py +++ b/addons/hr_recruitment/models/hr_job.py @@ -1,6 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import ast +from collections import defaultdict from odoo import api, fields, models, _ @@ -8,7 +9,7 @@ from odoo import api, fields, models, _ class Job(models.Model): _name = "hr.job" _inherit = ["mail.alias.mixin", "hr.job"] - _order = "sequence, state desc, name asc" + _order = "sequence, name asc" @api.model def _default_address_id(self): @@ -29,10 +30,11 @@ class Job(models.Model): 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") 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", tracking=True) + user_id = fields.Many2one('res.users', "Recruiter", domain="[('share', '=', False), ('company_ids', 'in', company_id)]", tracking=True) hr_responsible_id = fields.Many2one( 'res.users', "HR Responsible", tracking=True, help="Person responsible of validating the employee's contracts.") @@ -47,6 +49,39 @@ class Job(models.Model): interviewer_ids = fields.Many2many('res.users', string='Interviewers', domain="[('share', '=', False), ('company_ids', 'in', company_id)]") extended_interviewer_ids = fields.Many2many('res.users', 'hr_job_extended_interviewer_res_users', compute='_compute_extended_interviewer_ids', store=True) + activities_overdue = fields.Integer(compute='_compute_activities') + activities_today = fields.Integer(compute='_compute_activities') + + @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 + 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.job_id IN %(job_ids)s + AND sta.hired_stage IS NOT TRUE + GROUP BY app.job_id, act_state + """, { + 'today': fields.Date.context_today(self), + 'user_id': self.env.uid, + 'job_ids': tuple(self.ids), + }) + job_activities = defaultdict(dict) + for activity in self.env.cr.dictfetchall(): + job_activities[activity['job_id']][activity['act_state']] = 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) + @api.depends('application_ids.interviewer_id') def _compute_extended_interviewer_ids(self): for job in self: @@ -85,7 +120,13 @@ class Job(models.Model): job.documents_count = len(job.document_ids) def _compute_all_application_count(self): - read_group_result = self.env['hr.applicant'].with_context(active_test=False)._read_group([('job_id', 'in', self.ids)], ['job_id'], ['job_id']) + read_group_result = self.env['hr.applicant'].with_context(active_test=False)._read_group([ + ('job_id', 'in', self.ids), + '|', + ('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) for job in self: job.all_application_count = result.get(job.id, 0) @@ -109,6 +150,16 @@ class Job(models.Model): [("job_id", "=", job.id), ("stage_id", "=", job._get_first_stage().id)] ) + def _compute_applicant_hired(self): + hired_stages = self.env['hr.recruitment.stage'].search([('hired_stage', '=', True)]) + 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} + for job in self: + job.applicant_hired = job_hires.get(job.id, 0) + @api.depends('application_count', 'new_application_count') def _compute_old_application_count(self): for job in self: @@ -144,6 +195,8 @@ class Job(models.Model): def write(self, vals): old_interviewers = self.interviewer_ids + if 'active' in vals and not vals['active']: + self.application_ids.active = False res = super().write(vals) if 'interviewer_ids' in vals: interviewers_to_clean = old_interviewers - self.interviewer_ids @@ -163,14 +216,51 @@ class Job(models.Model): def _creation_subtype(self): return self.env.ref('hr_recruitment.mt_job_new') - def action_get_attachment_tree_view(self): - action = self.env["ir.actions.actions"]._for_xml_id("base.action_attachment") - action['context'] = { - 'default_res_model': self._name, - 'default_res_id': self.ids[0] + def action_open_attachments(self): + return { + 'type': 'ir.actions.act_window', + 'res_model': 'ir.attachment', + 'name': _('Documents'), + 'context': { + 'default_res_model': self._name, + '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.job'), ('res_id', 'in', self.ids), + '&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.application_ids.ids), + ], + } + + def action_open_activities(self): + action = self.env["ir.actions.actions"]._for_xml_id("hr_recruitment.action_hr_job_applications") + 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, + } + 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, } - action['search_view_id'] = (self.env.ref('hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment').id, ) - action['domain'] = ['|', '&', ('res_model', '=', 'hr.job'), ('res_id', 'in', self.ids), '&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.mapped('application_ids').ids)] return action def close_dialog(self): diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py index 77f2156dd00..85f6e52bdf0 100644 --- a/addons/hr_recruitment/models/hr_recruitment.py +++ b/addons/hr_recruitment/models/hr_recruitment.py @@ -10,6 +10,8 @@ from odoo.exceptions import AccessError, UserError from dateutil.relativedelta import relativedelta +from lxml import etree + AVAILABLE_PRIORITIES = [ ('0', 'Normal'), ('1', 'Good'), @@ -24,9 +26,13 @@ class RecruitmentSource(models.Model): _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") + 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') @@ -44,7 +50,16 @@ class RecruitmentSource(models.Model): }, } source.alias_id = self.env['mail.alias'].create(vals) - source.name = source.source_id.name + + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + res = super().fields_view_get(view_id, view_type, toolbar, submenu) + if view_type == 'tree' and not bool(self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")): + arch = etree.fromstring(res['arch']) + email = arch.xpath("//field[@name='email']")[0] + email.getparent().remove(email) + res['arch'] = etree.tostring(arch, encoding='unicode') + return res class RecruitmentStage(models.Model): _name = "hr.recruitment.stage" @@ -111,7 +126,7 @@ class Applicant(models.Model): _inherit = ['mail.thread.cc', 'mail.activity.mixin', 'utm.mixin'] _mailing_enabled = True - name = fields.Char("Subject / Application Name", required=True, help="Email subject for applications sent via email", index='trigram') + 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, help="Applicant email", compute='_compute_partner_phone_email', @@ -129,7 +144,7 @@ class Applicant(models.Model): 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', + '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) date_open = fields.Datetime("Assigned", readonly=True) @@ -178,6 +193,12 @@ class Applicant(models.Model): source_id = fields.Many2one(ondelete='set null') interviewer_id = fields.Many2one('res.users', string='Interviewer', domain="[('share', '=', False), ('company_ids', 'in', company_id)]") + @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: @@ -312,7 +333,7 @@ class Applicant(models.Model): for applicant in self: applicant.user_id = applicant.job_id.user_id.id or self.env.uid - @api.depends('partner_id') + @api.depends('partner_id', 'partner_id.email', 'partner_id.mobile', 'partner_id.phone') def _compute_partner_phone_email(self): for applicant in self: applicant.partner_phone = applicant.partner_id.phone @@ -448,12 +469,24 @@ class Applicant(models.Model): } return res - def action_get_attachment_tree_view(self): - action = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment') - action['context'] = {'default_res_model': self._name, 'default_res_id': self.ids[0]} - action['domain'] = str(['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)]) - action['search_view_id'] = (self.env.ref('hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment').id, ) - return action + def action_open_attachments(self): + return { + 'type': 'ir.actions.act_window', + 'res_model': 'ir.attachment', + 'name': _('Documents'), + 'context': { + 'default_res_model': 'hr.job', + '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): # Security rules will apply when loading the view, here we just fetch the ids @@ -479,6 +512,16 @@ class Applicant(models.Model): }, } + 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] @@ -520,6 +563,14 @@ class Applicant(models.Model): 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 @@ -646,6 +697,18 @@ class Applicant(models.Model): 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" diff --git a/addons/hr_recruitment/security/ir.model.access.csv b/addons/hr_recruitment/security/ir.model.access.csv index 91daed23899..e331a9a9ce1 100644 --- a/addons/hr_recruitment/security/ir.model.access.csv +++ b/addons/hr_recruitment/security/ir.model.access.csv @@ -17,3 +17,5 @@ access_hr_applicant_category_manager,hr.applicant_category,model_hr_applicant_ca access_calendar_event_type_hr_officer,calendar.event.type.officer,calendar.model_calendar_event_type,group_hr_recruitment_user,1,1,1,0 access_applicant_get_refuse_reason,access.applicant.get.refuse.reason,model_applicant_get_refuse_reason,hr_recruitment.group_hr_recruitment_user,1,1,1,0 access_applicant_get_refuse_reason_interviewer,access.applicant.get.refuse.reason.interviewer,model_applicant_get_refuse_reason,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,0 +access_applicant_send_mail,access.applicant.send.mail,model_applicant_send_mail,hr_recruitment.group_hr_recruitment_user,1,1,1,0 +access_applicant_send_mail_interviewer,access.applicant.send.mail.interviewer,model_applicant_send_mail,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,0 diff --git a/addons/hr_recruitment/static/src/js/recruitment.js b/addons/hr_recruitment/static/src/js/recruitment.js index e9758ddabf8..49a7fab691c 100644 --- a/addons/hr_recruitment/static/src/js/recruitment.js +++ b/addons/hr_recruitment/static/src/js/recruitment.js @@ -1,18 +1,56 @@ -odoo.define('job.update_kanban', function (require) { - 'use strict'; - var KanbanRecord = require('web.KanbanRecord'); +/** @odoo-module **/ +import KanbanRecord from 'web.KanbanRecord'; - KanbanRecord.include({ - /** - * @override - * @private - */ - _openRecord: function () { - if (this.modelName === 'hr.job' && this.$(".o_hr_job_boxes a").length) { - this.$(".o_hr_job_boxes a").first().click(); - } else { - this._super.apply(this, arguments); - } - } - }); +import fieldRegistry from 'web.field_registry'; +import { FieldChar, StateSelectionWidget } from 'web.basic_fields'; + +const RecruitmentStateSelectionWidget = StateSelectionWidget.extend({ + init() { + this._super(...arguments); + this.mode = 'edit'; + }, +}); + +const ApplicantChar = FieldChar.extend({ + events: _.extend({}, FieldChar.prototype.events, { + 'click': '_onClick', + }), + + _onClick(ev) { + if (this.recordData['res_id'] !== undefined && this.recordData['res_model'] === 'hr.applicant') { + ev.stopPropagation(); + + this.do_action({ + type: 'ir.actions.act_window', + res_model: 'hr.applicant', + res_id: this.recordData['res_id'], + views: [[false, "form"]], + view_mode: "form", + target: "current", + }); + } + }, + + _renderReadonly() { + this._super(...arguments); + this.$el.addClass('o_hr_applicant_widget'); + } +}); + +fieldRegistry + .add('recruitment_state_selection', RecruitmentStateSelectionWidget) + .add('applicant_char', ApplicantChar); + +KanbanRecord.include({ + /** + * @override + * @private + */ + _openRecord: function () { + if (this.modelName === 'hr.job' && this.$(".o_hr_job_boxes a").length) { + this.$(".o_hr_job_boxes a").first().click(); + } else { + this._super.apply(this, arguments); + } + } }); diff --git a/addons/hr_recruitment/static/src/scss/hr_applicant.scss b/addons/hr_recruitment/static/src/scss/hr_applicant.scss index fa2400084fe..701c394bb36 100644 --- a/addons/hr_recruitment/static/src/scss/hr_applicant.scss +++ b/addons/hr_recruitment/static/src/scss/hr_applicant.scss @@ -5,3 +5,19 @@ display: none; } } + +.o_applicant_form { + &.o_form_readonly { + .o_field_empty:empty { + min-height: unset; + } + } +} + +.o_hr_applicant_widget { + color: $link-color; + + &:hover { + color: $link-hover-color; + } +} diff --git a/addons/hr_recruitment/static/src/scss/hr_job.scss b/addons/hr_recruitment/static/src/scss/hr_job.scss index cd52a47b952..b6bee6b4b64 100644 --- a/addons/hr_recruitment/static/src/scss/hr_job.scss +++ b/addons/hr_recruitment/static/src/scss/hr_job.scss @@ -1,12 +1,9 @@ .o_kanban_view.o_kanban_dashboard { &.o_hr_recruitment_kanban { &.o_kanban_ungrouped .o_kanban_record { - width: 350px; + width: 450px; &:not(.o_kanban_ghost) { - height: 160px; - @include media-breakpoint-down(sm) { - height: 150px; - } + min-height: 220px; } } @@ -35,10 +32,30 @@ .text_top_padding{ padding-top: 0.375rem; } - } - .o_kanban_record_subtitle { - height: 1em; + .o_primary { + display: flex !important; + align-items: center; + justify-content: space-between; + padding-right: 24px !important; + + .fa { + margin-left: 0; + } + } + + .o_job_activities { + list-style-type: none; + + .to-recruit { + color: $o-enterprise-color; + font-weight: bold; + } + } + + .o_kanban_card_header_title { + min-height: 3rem; + } } .o_recruitment_kanban_boxes { @@ -69,6 +86,9 @@ .o_value { font-weight: 800; } + &.bottom_block { + background-color: unset !important; + } > a { font-weight: 500; @@ -95,6 +115,12 @@ .o_kanban_view { &.o_kanban_applicant { + .o_sample_data_disabled { + .ribbon { + display: none; + } + } + .ribbon { &::before, &::after { display: none; diff --git a/addons/hr_recruitment/views/hr_department_views.xml b/addons/hr_recruitment/views/hr_department_views.xml index d2225293f8f..26881994c11 100644 --- a/addons/hr_recruitment/views/hr_department_views.xml +++ b/addons/hr_recruitment/views/hr_department_views.xml @@ -42,5 +42,5 @@ + parent="menu_hr_recruitment_config_employees" action="action_hr_department"/> diff --git a/addons/hr_recruitment/views/hr_job_views.xml b/addons/hr_recruitment/views/hr_job_views.xml index 3c5857b10f1..088e07e58c4 100644 --- a/addons/hr_recruitment/views/hr_job_views.xml +++ b/addons/hr_recruitment/views/hr_job_views.xml @@ -11,7 +11,8 @@ hr.job.kanban hr.job - + + @@ -22,7 +23,6 @@ - @@ -30,15 +30,18 @@

+
- +
- -
- +
+ +
+
+
@@ -46,51 +49,73 @@
- -
-
- -
-
- Applications
- To Recruit -
+
+
+
- - -
-
- -
- -
- Applications
-
-
-
-
-
-
-
- +
+
+ - +
+
+ +
+ Send texts to your contacts +
+ +
+
diff --git a/addons/hr_recruitment/wizard/__init__.py b/addons/hr_recruitment/wizard/__init__.py index 6a456a24112..3622ba37619 100644 --- a/addons/hr_recruitment/wizard/__init__.py +++ b/addons/hr_recruitment/wizard/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import applicant_refuse_reason +from . import applicant_send_mail diff --git a/addons/hr_recruitment/wizard/applicant_refuse_reason.py b/addons/hr_recruitment/wizard/applicant_refuse_reason.py index 09c072541a9..3c503dd1ca7 100644 --- a/addons/hr_recruitment/wizard/applicant_refuse_reason.py +++ b/addons/hr_recruitment/wizard/applicant_refuse_reason.py @@ -8,7 +8,7 @@ class ApplicantGetRefuseReason(models.TransientModel): _name = 'applicant.get.refuse.reason' _description = 'Get Refuse Reason' - refuse_reason_id = fields.Many2one('hr.applicant.refuse.reason', 'Refuse Reason') + refuse_reason_id = fields.Many2one('hr.applicant.refuse.reason', 'Refuse Reason', required=True) applicant_ids = fields.Many2many('hr.applicant') send_mail = fields.Boolean("Send Email", compute='_compute_send_mail', store=True, readonly=False) template_id = fields.Many2one('mail.template', string='Email Template', diff --git a/addons/hr_recruitment/wizard/applicant_send_mail.py b/addons/hr_recruitment/wizard/applicant_send_mail.py new file mode 100644 index 00000000000..de3fc005851 --- /dev/null +++ b/addons/hr_recruitment/wizard/applicant_send_mail.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models, _ + + +class ApplicantSendMail(models.TransientModel): + _name = 'applicant.send.mail' + _inherit = 'mail.composer.mixin' + _description = 'Send mails to applicants' + + applicant_ids = fields.Many2many('hr.applicant', string='Applications', required=True) + author_id = fields.Many2one('res.partner', 'Author', required=True, default=lambda self: self.env.user.partner_id.id) + + @api.depends('subject') + def _compute_render_model(self): + self.render_model = 'hr.applicant' + + def action_send(self): + self.ensure_one() + + without_emails = self.applicant_ids.filtered(lambda a: not a.email_from or (a.partner_id and not a.partner_id.email)) + if without_emails: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'type': 'danger', + 'message': _("The following applicants are missing an email address: %s.", ', '.join(without_emails.mapped(lambda a: a.partner_name or a.name))), + } + } + + for applicant in self.applicant_ids: + if not applicant.partner_id: + applicant.partner_id = self.env['res.partner'].create({ + 'is_company': False, + 'type': 'private', + 'name': applicant.partner_name, + 'email': applicant.email_from, + 'phone': applicant.partner_phone, + 'mobile': applicant.partner_mobile, + }) + + applicant.message_post( + subject=self.subject, + body=self.body, + message_type='comment', + email_from=self.author_id.email, + email_layout_xmlid='mail.mail_notification_light', + partner_ids=applicant.partner_id.ids, + ) diff --git a/addons/hr_recruitment/wizard/applicant_send_mail_views.xml b/addons/hr_recruitment/wizard/applicant_send_mail_views.xml new file mode 100644 index 00000000000..043c9006b81 --- /dev/null +++ b/addons/hr_recruitment/wizard/applicant_send_mail_views.xml @@ -0,0 +1,24 @@ + + + + applicant.send.mail + + + + + + + + + + +
+
+ +
+
+
diff --git a/addons/hr_recruitment_survey/models/hr_job.py b/addons/hr_recruitment_survey/models/hr_job.py index 60e7557e550..897ad502d99 100644 --- a/addons/hr_recruitment_survey/models/hr_job.py +++ b/addons/hr_recruitment_survey/models/hr_job.py @@ -10,8 +10,9 @@ class Job(models.Model): 'survey.survey', "Interview Form", help="Choose an interview form for this job position and you will be able to print/answer this interview from all applicants who apply for this job") - def action_print_survey(self): - return self.survey_id.action_print_survey() + def action_test_survey(self): + self.ensure_one() + return self.survey_id.action_test_survey() def action_new_survey(self): self.ensure_one() diff --git a/addons/hr_recruitment_survey/views/hr_applicant_views.xml b/addons/hr_recruitment_survey/views/hr_applicant_views.xml index 7e178c40ce0..09abeac69d4 100644 --- a/addons/hr_recruitment_survey/views/hr_applicant_views.xml +++ b/addons/hr_recruitment_survey/views/hr_applicant_views.xml @@ -18,7 +18,7 @@ -
- + @@ -23,10 +23,14 @@ - -
- Preview Interview - Create Interview Form + + + + + diff --git a/addons/website_hr_recruitment/models/hr_recruitment.py b/addons/website_hr_recruitment/models/hr_recruitment.py index 3eec7c3d0f2..4940b8d2832 100644 --- a/addons/website_hr_recruitment/models/hr_recruitment.py +++ b/addons/website_hr_recruitment/models/hr_recruitment.py @@ -68,3 +68,8 @@ class Job(models.Model): def get_backend_menu_id(self): return self.env.ref('hr_recruitment.menu_hr_recruitment_root').id + + def open_website_url(self): + action = super().open_website_url() + action['target'] = 'new' + return action diff --git a/addons/website_hr_recruitment/views/hr_job_views.xml b/addons/website_hr_recruitment/views/hr_job_views.xml index 28f757fc16e..c83a52d325a 100644 --- a/addons/website_hr_recruitment/views/hr_job_views.xml +++ b/addons/website_hr_recruitment/views/hr_job_views.xml @@ -1,7 +1,7 @@ - hr.job.form.inherit + hr.job.kanban.inherit hr.job @@ -11,24 +11,19 @@ Published
- + -
- - - +
+
+ + + Published + Not published + +
+
- - - - - Published - Not published - - - - @@ -37,13 +32,29 @@ hr.job.view.kanban - + + + + + hr.job + + + + + + + + + + + diff --git a/addons/website_hr_recruitment/views/hr_recruitment_views.xml b/addons/website_hr_recruitment/views/hr_recruitment_views.xml index fe939d4596e..b535c7e0f5c 100644 --- a/addons/website_hr_recruitment/views/hr_recruitment_views.xml +++ b/addons/website_hr_recruitment/views/hr_recruitment_views.xml @@ -16,8 +16,8 @@ hr.recruitment.source - - + +
@@ -35,7 +35,7 @@
- + @@ -49,19 +49,22 @@ + + You can write here a short description of your Job Description that will be displayed on the main Jobs' list page. + hr.job.tree hr.job - + - - + + - - + + diff --git a/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml b/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml index 55a8699f5d8..129fc786b5b 100644 --- a/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml +++ b/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml @@ -467,7 +467,7 @@
-
+

Congratulations!

@@ -495,7 +495,7 @@

-
+
We usually reply between one and three days.
Feel free to contact him/her if you have further questions. @@ -504,7 +504,7 @@
-
+
In the meantime,

Look around on our website:

Continue To Our Website