[IMP] hr_recruitment: UX improvements

Multi-Application Management and UX improvements.

Improve in Recruitment app Job Positions Kanban view;
It is more intuitive if list is applications default view; improve the applications form;

Task - 2404610

closes odoo/odoo#63946

Related: odoo/enterprise#15750
Signed-off-by: Kevin Baptiste <kba@odoo.com>
This commit is contained in:
sofiagvaladze
2021-07-30 13:15:02 +00:00
parent 4629caa530
commit 08ed9a0ebb
16 changed files with 159 additions and 63 deletions
+7
View File
@@ -27,6 +27,8 @@ class Job(models.Model):
new_application_count = fields.Integer(
compute='_compute_new_application_count', string="New Application",
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")
manager_id = fields.Many2one(
'hr.employee', related='department_id.manager_id', string="Department Manager",
readonly=True, store=True)
@@ -100,6 +102,11 @@ class Job(models.Model):
[("job_id", "=", job.id), ("stage_id", "=", job._get_first_stage().id)]
)
@api.depends('application_count', 'new_application_count')
def _compute_old_application_count(self):
for job in self:
job.old_application_count = job.application_count - job.new_application_count
def _alias_get_creation_values(self):
values = super(Job, self)._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get('hr.applicant').id
@@ -196,7 +196,7 @@ class Applicant(models.Model):
application_data_mapped = dict((data['email_from'], data['email_from_count']) for data in application_data)
applicants = self.filtered(lambda applicant: applicant.email_from)
for applicant in applicants:
applicant.application_count = application_data_mapped.get(applicant.email_from, 1) - 1
applicant.application_count = application_data_mapped.get(applicant.email_from, 1)
(self - applicants).application_count = False
@api.depends_context('lang')
@@ -3,7 +3,7 @@
&.o_kanban_ungrouped .o_kanban_record {
width: 350px;
&:not(.o_kanban_ghost) {
height: 180px;
height: 160px;
@include media-breakpoint-down(sm) {
height: 150px;
}
@@ -31,6 +31,10 @@
right: 30px;
}
}
.text_top_padding{
padding-top: 0.375rem;
}
}
.o_kanban_record_subtitle {
+8 -1
View File
@@ -24,6 +24,7 @@
<field name="manager_id"/>
<field name="state"/>
<field name="user_id"/>
<field name="application_count"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="#{!selection_mode ? kanban_color(record.color.raw_value) : ''}">
@@ -60,9 +61,14 @@
</t>
<t t-if="record.state.raw_value == 'open'">
<div class="row">
<div class="col-12 o_kanban_primary_left">
<div class="col-7 o_kanban_primary_left">
<button class="btn btn-secondary" name="set_recruit" type="object">Start Recruitment</button>
</div>
<t t-if="record.old_application_count.raw_value != 0">
<div class="col-5 text_top_padding">
<field name="old_application_count"/> Applications<br/>
</div>
</t>
</div>
</t>
<div name="kanban_boxes" class="row o_recruitment_kanban_boxes">
@@ -81,6 +87,7 @@
<div role="menuitem"><a t-if="record.state.raw_value == 'recruit'" name="set_open" type="object">Recruitment Done</a></div>
<div role="menuitem"><a t-if="record.state.raw_value == 'open'" name="set_recruit" type="object">Start recruitment</a></div>
<div role="menuitem"><a t-if="widget.editable" name="edit_job" type="edit">Edit</a></div>
<div role="menuitem" name="applications_action"><a t-if="record.state.raw_value == 'open' and record.application_count.raw_value != 0" name="%(action_hr_job_applications)d" type="action">Applications</a></div>
</div>
<div role="menuitem" aria-haspopup="true">
<ul class="oe_kanban_colorpicker" data-field="color" role="menu"/>
@@ -27,16 +27,18 @@
<tree string="Applicants" multi_edit="1" sample="1">
<field name="message_needaction" invisible="1"/>
<field name="last_stage_id" invisible="1"/>
<field name="create_date" readonly="1" widget="date" optional="show"/>
<field name="date_last_stage_update" invisible="1"/>
<field name="partner_name" readonly="1"/>
<field name="job_id"/>
<field name="name" readonly="1"/>
<field name="stage_id"/>
<field name="priority" widget="priority" optional="show"/>
<field name="partner_mobile" widget="phone" readonly="1" optional="show"/>
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
<field name="user_id" widget="many2one_avatar_user" optional="show"/>
<field name="create_date" readonly="1" widget="date" optional="show"/>
<field name="partner_phone" widget="phone" readonly="1" optional="hide"/>
<field name="email_from" readonly="1" optional="hide"/>
<field name="job_id"/>
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
<field name="priority" widget="priority" optional="show"/>
<field name="medium_id" optional="hide"/>
<field name="source_id" readonly="1" optional="hide"/>
<field name="salary_expected" optional="hide"/>
@@ -44,9 +46,7 @@
<field name="type_id" invisible="1"/>
<field name="availability" optional="hide"/>
<field name="department_id" invisible="context.get('invisible_department', True)" readonly="1"/>
<field name="user_id" widget="many2one_avatar_user" optional="show"/>
<field name="company_id" groups="base.group_multi_company" readonly="1" optional="hide"/>
<field name="stage_id"/>
</tree>
</field>
</record>
@@ -86,8 +86,8 @@
icon="fa-pencil"
type="object"
context="{'active_test': False}"
attrs="{'invisible': [('application_count', '=', 0)]}">
<field name="application_count" widget="statinfo" string="Other Applications"/>
attrs="{'invisible': [('application_count', '&lt;' , 2)]}">
<field name="application_count" widget="statinfo" string="Applications"/>
</button>
<button name="action_makeMeeting" class="oe_stat_button" icon="fa-calendar" type="object"
>
@@ -106,7 +106,7 @@
<field name="legend_done" invisible="1"/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
<h1><field name="name" placeholder="e.g. Sales Manager 2 year experience"/></h1>
<h2 class="o_row">
<div>
<label for="partner_name" class="oe_edit_only"/>
@@ -125,7 +125,7 @@
<field name="type_id" placeholder="Degree"/>
</group>
<group>
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
<field name="categ_ids" placeholder="Tags" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
<field name="user_id" domain="[('share', '=', False)]"/>
<field name="date_closed" attrs="{'invisible': [('date_closed', '=', False)]}" />
<field name="priority" widget="priority"/>
@@ -154,8 +154,11 @@
<field name="emp_id" invisible="1"/>
</group>
</group>
<separator string="Application Summary"/>
<field name="description" placeholder="Feedback of interviews..."/>
<notebook>
<page string="Application Summary">
<field name="description" placeholder="Motivations..."/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
@@ -390,7 +393,7 @@
<record model="ir.actions.act_window" id="action_hr_job_applications">
<field name="name">Applications</field>
<field name="res_model">hr.applicant</field>
<field name="view_mode">kanban,tree,form,graph,calendar,pivot</field>
<field name="view_mode">kanban,tree,form,graph,calendar,pivot,activity</field>
<field name="search_view_id" ref="hr_applicant_view_search_bis"/>
<field name="context">{'search_default_job_id': [active_id], 'default_job_id': active_id}</field>
<field name="help" type="html">
@@ -599,14 +602,14 @@
</record>
<record model="ir.actions.act_window.view" id="action_hr_sec_kanban_view_act_job">
<field name="sequence" eval="0"/>
<field name="sequence" eval="1"/>
<field name="view_mode">kanban</field>
<field name="view_id" ref="hr_kanban_view_applicant"/>
<field name="act_window_id" ref="crm_case_categ0_act_job"/>
</record>
<record model="ir.actions.act_window.view" id="action_hr_sec_tree_view_act_job">
<field name="sequence" eval="1"/>
<field name="sequence" eval="0"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="crm_case_tree_view_job"/>
<field name="act_window_id" ref="crm_case_categ0_act_job"/>
@@ -32,9 +32,10 @@
<field name="module_hr_recruitment_survey"/>
</div>
<div class="o_setting_right_pane">
<label for="module_hr_recruitment_survey" string="Interview Forms"/>
<label for="module_hr_recruitment_survey" string="Send Interview Survey"/>
<div class="text-muted">
Use interview forms during recruitment process
Send an Interview Survey to the applicant during
the recruitment process
</div>
<div class="content-group" id="interview_forms"/>
</div>
@@ -3,3 +3,4 @@
from . import hr_job
from . import hr_applicant
from . import survey_invite
@@ -1,6 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import fields, models, _
from odoo.exceptions import UserError
class Applicant(models.Model):
@@ -8,19 +9,26 @@ class Applicant(models.Model):
survey_id = fields.Many2one('survey.survey', related='job_id.survey_id', string="Survey", readonly=True)
response_id = fields.Many2one('survey.user_input', "Response", ondelete="set null")
def action_start_survey(self):
self.ensure_one()
# create a response and link it to this applicant
if not self.response_id:
response = self.survey_id._create_answer(partner=self.partner_id)
self.response_id = response.id
else:
response = self.response_id
# grab the token of the response and start surveying
return self.survey_id.action_start_survey(answer=response)
response_state = fields.Selection(related='response_id.state', readonly=True)
def action_print_survey(self):
""" If response is available then print this response otherwise print survey form (print template of the survey) """
self.ensure_one()
return self.survey_id.action_print_survey(answer=self.response_id)
def action_send_survey(self):
self.ensure_one()
# if an applicant does not already has associated partner_id create it
if not self.partner_id:
if not self.partner_name:
raise UserError(_('You must define a Contact Name for this applicant.'))
self.partner_id = self.env['res.partner'].create({
'is_company': False,
'type': 'private',
'name': self.partner_name,
'email': self.email_from,
'phone': self.partner_phone,
'mobile': self.partner_mobile
})
return self.survey_id.with_context(default_partner_ids=self.partner_id.ids, active_model='hr.applicant', active_id=self.id).action_send_survey()
@@ -0,0 +1,35 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
class SurveyInvite(models.TransientModel):
_inherit = "survey.invite"
applicant_id = fields.Many2one('hr.applicant', string='Applicant', default=lambda self: self.env.context.get('active_id', None))
def action_invite(self):
self.ensure_one()
if not self.applicant_id.response_id:
response = self.applicant_id.survey_id._create_answer(partner=self.applicant_id.partner_id)
self.applicant_id.response_id = response.id
body = _('The survey has been sent to "%s".', self.applicant_id.partner_name)
self.applicant_id.message_post(body=body)
return super().action_invite()
class SurveyUserInput(models.Model):
_inherit = "survey.user_input"
applicant_id = fields.One2many('hr.applicant', 'response_id', string='Applicant')
def _mark_done(self):
odoobot = self.env.ref('base.partner_root')
for user_input in self:
if user_input.applicant_id:
body = _('The applicant "%s" has finished the survey.', user_input.applicant_id.partner_name)
user_input.applicant_id.message_post(body=body, author_id=odoobot.id)
return super()._mark_done()
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
from odoo.tests import common, Form
class TestRecruitmentSurvey(common.SingleTransactionCase):
@@ -14,26 +14,50 @@ class TestRecruitmentSurvey(common.SingleTransactionCase):
cls.department_admins = cls.env['hr.department'].create({'name': 'Admins'})
cls.survey_sysadmin = cls.env['survey.survey'].create({'title': 'Questions for Sysadmin job offer'})
# We need this, so that when we send survey, we don't get an error
cls.question_ft = cls.env['survey.question'].create({
'title': 'Test Free Text',
'survey_id': cls.survey_sysadmin.id,
'sequence': 2,
'question_type': 'text_box',
})
cls.job = cls.env['hr.job'].create({
'name': 'Technical worker',
'survey_id': cls.survey_sysadmin.id,
})
cls.job_sysadmin = cls.env['hr.applicant'].create({
'name': 'Technical worker',
'partner_name': 'Jane Doe',
'email_from': 'customer@example.com',
'department_id': cls.department_admins.id,
'description': 'A nice Sys Admin job offer !',
'job_id': cls.job.id,
})
def test_start_survey(self):
def test_send_survey(self):
# We ensure that response is False because we don't know test order
self.job_sysadmin.response_id = False
action_start = self.job_sysadmin.action_start_survey()
self.assertEqual(action_start['type'], 'ir.actions.act_url')
Answer = self.env['survey.user_input']
answers = Answer.search([('survey_id', '=', self.survey_sysadmin.id)])
answers.unlink()
self.survey_sysadmin.write({'access_mode': 'public', 'users_login_required': False})
action = self.job_sysadmin.action_send_survey()
invite_form = Form(self.env[action['res_model']].with_context({
'active_id': self.job_sysadmin.id,
**action['context'],
}))
invite = invite_form.save()
invite.action_invite()
self.assertNotEqual(self.job_sysadmin.response_id.id, False)
self.assertIn(self.job_sysadmin.response_id.access_token, action_start['url'])
action_start_with_response = self.job_sysadmin.action_start_survey()
self.assertEqual(action_start_with_response, action_start)
answers = Answer.search([('survey_id', '=', self.survey_sysadmin.id)])
self.assertEqual(len(answers), 1)
self.assertEqual(
set(answers.mapped('email')),
set([self.job_sysadmin.email_from]))
def test_print_survey(self):
# We ensure that response is False because we don't know test order
@@ -17,19 +17,10 @@
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.hr_applicant_view_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='archive_applicant']" position="before">
<button name="action_send_survey" string="SEND INTERVIEW" type="object"/>
</xpath>
<xpath expr="//button[@name='action_makeMeeting']" position="after">
<button name="action_start_survey"
class="oe_stat_button"
icon="fa-user"
type="object"
help="Answer related job question"
context="{'survey_id': survey_id}"
attrs="{'invisible':[('survey_id','=',False)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Start</span>
<span class="o_stat_text">Interview</span>
</div>
</button>
<button name="action_print_survey"
class="oe_stat_button"
icon="fa-pencil-square-o"
@@ -45,6 +36,7 @@
<xpath expr="//field[@name='job_id']" position="before">
<field name="survey_id" invisible="1"/>
<field name="response_id" invisible="1"/>
<field name="response_state" invisible="1"/>
</xpath>
</field>
</record>
@@ -23,9 +23,11 @@
<xpath expr="//field[@name='manager_id']" position="after">
<field name="survey_id"/>
</xpath>
<xpath expr='//a[@name="edit_job"]' position="after">
<a t-if="record.survey_id.raw_value" name="action_print_survey" type="object" title="Display Interview Form">Preview Interview</a>
<a t-if="!record.survey_id.raw_value" name="action_new_survey" type="object" title="Create Interview Form">Create Interview Form</a>
<xpath expr='//div[@name="applications_action"]' position="after">
<div role="menuitem">
<a t-if="record.survey_id.raw_value" name="action_print_survey" type="object" title="Display Interview Form">Preview Interview</a>
<a t-if="!record.survey_id.raw_value" name="action_new_survey" type="object" title="Create Interview Form">Create Interview Form</a>
</div>
</xpath>
</field>
</record>
@@ -8,10 +8,10 @@
<div id="interview_forms" position="replace">
<div class="content-group">
<div class="mt8">
<button name="%(survey.action_survey_form)d" icon="fa-arrow-right" type="action" string="Interview Forms" class="btn-link"/>
<button name="%(survey.action_survey_form)d" icon="fa-arrow-right" type="action" string="Interview Survey" class="btn-link"/>
</div>
</div>
</div>
</div>
</field>
</record>
</odoo>
-1
View File
@@ -558,7 +558,6 @@ class Survey(http.Controller):
return self._redirect_with_error(access_data, access_data['validity_code'])
survey_sudo, answer_sudo = access_data['survey_sudo'], access_data['answer_sudo']
return request.render('survey.survey_page_print', {
'is_html_empty': is_html_empty,
'review': review,
@@ -3,7 +3,7 @@
from werkzeug import urls
from odoo import api, fields, models
from odoo import api, fields, models, _
from odoo.tools.translate import html_translate
@@ -31,7 +31,9 @@ class Applicant(models.Model):
def website_form_input_filter(self, request, values):
if 'partner_name' in values:
values.setdefault('name', '%s\'s Application' % values['partner_name'])
applicant_job = self.env['hr.job'].sudo().search([('id', '=', values['job_id'])]).name if 'job_id' in values else False
name = '%s - %s' % (values['partner_name'], applicant_job) if applicant_job else _("%s's Application", values['partner_name'])
values.setdefault('name', name)
if values.get('job_id'):
stage = self.env['hr.recruitment.stage'].sudo().search([
('fold', '=', False),
@@ -11,11 +11,22 @@
<span class="o_recruitment_purple">Published</span>
</div>
</xpath>
<xpath expr="//div[hasclass('o_primary')]" position="replace">
<field name="website_url" invisible="1"/>
<div class="o_primary col-11">
<span><a t-attf-href="#{record.website_url.raw_value}" class="mr-2">
<t t-esc="record.name.value"/></a>
</span>
</div>
</xpath>
<xpath expr="//div[@name='kanban_boxes']/div/div[1]" position="inside">
<field name="website_url" invisible="1"/>
<span>
<a t-attf-href="#{record.website_url.raw_value}" class="mr-2">Job Description</a>
<field name="is_published" widget='boolean_toggle'/>
<span><t t-if="record.no_of_recruitment.raw_value != 0">
<span class="mr-2">
<t t-if="record.is_published.raw_value">Published</t>
<t t-else="">Not published</t>
</span>
<field name="is_published" widget='boolean_toggle'/></t>
</span>
</xpath>
</field>
@@ -29,7 +40,7 @@
<xpath expr="//div[hasclass('o_link_trackers')]" position="replace">
<div class="o_link_trackers col-6">
<a role="button" name="%(hr_recruitment.action_hr_job_sources)d" type="action" class="btn btn-sm py-0">
<span title='Link Trackers'><i class='fa fa-lg fa-link' role="img" aria-label="Link Trackers"/></span>
<span title='Link Trackers'><i class='fa fa-lg fa-link' role="img" aria-label="Link Trackers"/></span>
</a>
</div>
</xpath>