[MERGE] [IMP] hr, hr_recruitment: job position update and usability improvements

hr: hr.job
- no_of_recruitment now indicates the number of employees you want to hire in a given recruitment phase
- added no_of_hired_employee that is the number of employees already recruited in the current recruitment phase
- updated form view
- removed simplified form view
- the job in the employee form view is now clickable and redirects to the job form view

hr_recruitment
- new dependency: web_kanban_gauge
- installing this module adds the 'Job Positions' menu, and adds a new kanban view for hr.job
- adds fields on hr.job to track applicants and their documents, used in the kanban view
- added configuration for default jobs alias, available in the Human Resources settings
- updated applicant / job subtypes: following some subtypes on the job now makes automatically follow some subtypes on applicants, like salesteam/opportunities or project/tasks and issues

mail: mail_alias
- when migrating to alias, use mail_notrack to avoid performing the tracking; indeed we are in a transient state, and trying to browse and track value change is risky.
mail: mail_thread: empty list help: small tweak to try to find a default alias

misc
- sale_crm: update for the new gauge widget parameters

bzr revid: tde@openerp.com-20140211140149-l15qr876s5ykfww5
This commit is contained in:
Thibault Delavallée 2014-02-11 15:01:49 +01:00
commit 796d40b474
21 changed files with 588 additions and 146 deletions

View File

@ -21,16 +21,15 @@
import logging
from openerp import tools
from openerp.modules.module import get_module_resource
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp import tools
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
class hr_employee_category(osv.osv):
class hr_employee_category(osv.Model):
def name_get(self, cr, uid, ids, context=None):
if not ids:
@ -73,9 +72,9 @@ class hr_employee_category(osv.osv):
]
class hr_job(osv.osv):
class hr_job(osv.Model):
def _no_of_employee(self, cr, uid, ids, name, args, context=None):
def _get_nbr_employees(self, cr, uid, ids, name, args, context=None):
res = {}
for job in self.browse(cr, uid, ids, context=context):
nb_employees = len(job.employee_ids or [])
@ -93,59 +92,81 @@ class hr_job(osv.osv):
return res
_name = "hr.job"
_description = "Job Description"
_inherit = ['mail.thread']
_description = "Job Position"
_inherit = ['mail.thread', 'ir.needaction_mixin']
_columns = {
'name': fields.char('Job Name', size=128, required=True, select=True),
'expected_employees': fields.function(_no_of_employee, string='Total Forecasted Employees',
'expected_employees': fields.function(_get_nbr_employees, string='Total Forecasted Employees',
help='Expected number of employees for this job position after new recruitment.',
store = {
'hr.job': (lambda self,cr,uid,ids,c=None: ids, ['no_of_recruitment'], 10),
'hr.employee': (_get_job_position, ['job_id'], 10),
}, type='integer',
multi='no_of_employee'),
'no_of_employee': fields.function(_no_of_employee, string="Current Number of Employees",
multi='_get_nbr_employees'),
'no_of_employee': fields.function(_get_nbr_employees, string="Current Number of Employees",
help='Number of employees currently occupying this job position.',
store = {
'hr.employee': (_get_job_position, ['job_id'], 10),
}, type='integer',
multi='no_of_employee'),
'no_of_recruitment': fields.integer('Expected in Recruitment', help='Number of new employees you expect to recruit.'),
multi='_get_nbr_employees'),
'no_of_recruitment': fields.integer('Expected New Employees', help='Number of new employees you expect to recruit.'),
'no_of_hired_employee': fields.integer('Hired Employees', help='Number of hired employees for this job position during recruitment phase.'),
'employee_ids': fields.one2many('hr.employee', 'job_id', 'Employees', groups='base.group_user'),
'description': fields.text('Job Description'),
'requirements': fields.text('Requirements'),
'department_id': fields.many2one('hr.department', 'Department'),
'company_id': fields.many2one('res.company', 'Company'),
'state': fields.selection([('open', 'No Recruitment'), ('recruit', 'Recruitement in Progress')], 'Status', readonly=True, required=True,
help="By default 'In position', set it to 'In Recruitment' if recruitment process is going on for this job position."),
'state': fields.selection([('open', 'Recruitment Closed'), ('recruit', 'Recruitment in Progress')],
string='Status', readonly=True, required=True,
track_visibility='always',
help="By default 'Closed', set it to 'In Recruitment' if recruitment process is going on for this job position."),
'write_date': fields.datetime('Update Date', readonly=True),
}
_defaults = {
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'hr.job', context=c),
'no_of_recruitment': 0,
'company_id': lambda self, cr, uid, ctx=None: self.pool.get('res.company')._company_default_get(cr, uid, 'hr.job', context=ctx),
'state': 'open',
}
_sql_constraints = [
('name_company_uniq', 'unique(name, company_id, department_id)', 'The name of the job position must be unique per department in company!'),
('hired_employee_check', "CHECK ( no_of_hired_employee <= no_of_recruitment )", "Number of hired employee must be less than expected number of employee in recruitment."),
]
def on_change_expected_employee(self, cr, uid, ids, no_of_recruitment, no_of_employee, context=None):
if context is None:
context = {}
return {'value': {'expected_employees': no_of_recruitment + no_of_employee}}
def job_recruitement(self, cr, uid, ids, *args):
for job in self.browse(cr, uid, ids):
def set_recruit(self, cr, uid, ids, context=None):
for job in self.browse(cr, uid, ids, context=context):
no_of_recruitment = job.no_of_recruitment == 0 and 1 or job.no_of_recruitment
self.write(cr, uid, [job.id], {'state': 'recruit', 'no_of_recruitment': no_of_recruitment})
self.write(cr, uid, [job.id], {'state': 'recruit', 'no_of_recruitment': no_of_recruitment}, context=context)
return True
def job_open(self, cr, uid, ids, *args):
self.write(cr, uid, ids, {'state': 'open', 'no_of_recruitment': 0})
def set_open(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {
'state': 'open',
'no_of_recruitment': 0,
'no_of_hired_employee': 0
}, context=context)
return True
def copy(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
default.update({
'employee_ids': [],
'no_of_recruitment': 0,
'no_of_hired_employee': 0,
})
if 'name' in default:
job = self.browse(cr, uid, id, context=context)
default['name'] = _("%s (copy)") % (job.name)
return super(hr_job, self).copy(cr, uid, id, default=default, context=context)
# ----------------------------------------
# Compatibility methods
# ----------------------------------------
_no_of_employee = _get_nbr_employees # v7 compatibility
job_open = set_open # v7 compatibility
job_recruitment = set_recruit # v7 compatibility
class hr_employee(osv.osv):
_name = "hr.employee"

View File

@ -45,7 +45,7 @@
</group>
<group string="Position">
<field name="department_id" on_change="onchange_department_id(department_id)"/>
<field name="job_id" options='{"no_open": True}' domain="[('state','!=','old')]" context="{'form_view_ref': 'hr.view_hr_job_employee_form'}"/>
<field name="job_id"/>
<field name="parent_id"/>
<field name="coach_id"/>
</group>
@ -333,8 +333,8 @@
<field name="arch" type="xml">
<form string="Job" version="7.0">
<header>
<button name="job_recruitement" string="Launch Recruitement" states="open" type="object" class="oe_highlight" groups="base.group_user"/>
<button name="job_open" string="Stop Recruitment" states="recruit" type="object" class="oe_highlight" groups="base.group_user"/>
<button name="set_recruit" string="Launch Recruitment" states="open" type="object" class="oe_highlight" groups="base.group_user"/>
<button name="set_open" string="Stop Recruitment" states="recruit" type="object" class="oe_highlight" groups="base.group_user"/>
<field name="state" widget="statusbar" statusbar_visible="recruit,open"/>
</header>
<sheet>
@ -342,20 +342,20 @@
<label for="name" class="oe_edit_only"/>
<h1><field name="name" class="oe_inline"/></h1>
</div>
<group>
<group name="job_data">
<field name="no_of_employee" groups="base.group_user"/>
<field name="no_of_recruitment" on_change="on_change_expected_employee(no_of_recruitment,no_of_employee)"/>
<field name="expected_employees" groups="base.group_user"/>
<field name="company_id" widget="selection" groups="base.group_multi_company"/>
<field name="department_id"/>
</group>
<div class="oe_right" name="buttons"/>
<group name="employee_data">
<field name="department_id" class="oe_inline"/>
<label for="no_of_employee"/>no_of_recruitment
<div>
<field name="no_of_employee" class="oe_inline"/>
<p><field name="no_of_recruitment" groups="base.group_user" colspan="0" class="oe_inline" style="padding-top: 1px"/> new employee(s) expected</p>
</div>
</group>
<div>
<div attrs="{'invisible': [('state', '!=', 'recruit')]}">
<label for="description"/>
<field name="description"/>
</div>
<div>
<div attrs="{'invisible': [('state', '!=', 'recruit')]}">
<label for="requirements"/>
<field name="requirements"/>
</div>
@ -378,6 +378,7 @@
<field name="no_of_employee"/>
<field name="no_of_recruitment"/>
<field name="expected_employees"/>
<field name="no_of_hired_employee"/>
<field name="state"/>
</tree>
</field>
@ -389,34 +390,18 @@
<field name="arch" type="xml">
<search string="Jobs">
<field name="name" string="Job"/>
<filter icon="terp-camera_test" domain="[('state','=','open')]" string="In Position" help="In Position"/>
<filter icon="terp-personal+" domain="[('state','=','recruit')]" string="In Recruitment" help="In Recruitment"/>
<filter domain="[('state','=','open')]" string="In Position"/>
<filter domain="[('state','=','recruit')]" string="In Recruitment" name="in_recruitment"/>
<field name="department_id"/>
<group expand="0" string="Group By...">
<filter string="Department" icon="terp-personal+" domain="[]" context="{'group_by':'department_id'}"/>
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}"/>
<filter string="Company" icon="terp-go-home" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
<filter string="Department" domain="[]" context="{'group_by':'department_id'}"/>
<filter string="Status" domain="[]" context="{'group_by':'state'}"/>
<filter string="Company" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
</group>
</search>
</field>
</record>
<record id="view_hr_job_employee_form" model="ir.ui.view">
<field name="name">hr.job.employee.form</field>
<field name="model">hr.job</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<form string="Job" version="7.0">
<group col="4">
<field name="name"/>
<field name="department_id"/>
</group>
<label for="description"/>
<field name="description"/>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_hr_job">
<field name="name">Job Positions</field>
<field name="res_model">hr.job</field>
@ -441,7 +426,6 @@
</record>
<menuitem name="Recruitment" id="base.menu_crm_case_job_req_main" parent="menu_hr_root" groups="base.group_hr_user"/>
<menuitem parent="hr.menu_hr_configuration" id="menu_hr_job" action="action_hr_job" sequence="6"/>
<!-- hr.department -->
<record id="view_department_form" model="ir.ui.view">

View File

@ -35,7 +35,7 @@
<group name="recruitment_grp">
<label for="id" string="Talent Management"/>
<div name="recruitment">
<div>
<div name="hr_recruitment">
<field name="module_hr_recruitment" class="oe_inline"/>
<label for="module_hr_recruitment"/>
</div>

View File

@ -20,6 +20,7 @@
name: HR Officer
login: hro
password: hro
email: hro@example.com
-
I added groups for HR Officer.
-

View File

@ -15,10 +15,10 @@
- state == 'open'
- no_of_recruitment == 0
-
Now, Recruitement is started so I start recruitement of Job Postion of "Developer" Profile.
Now, Recruitment is started so I start recruitment of Job Postion of "Developer" Profile.
-
!python {model: hr.job}: |
self.job_recruitement(cr, uid, [ref('job_developer')])
self.job_recruitment(cr, uid, [ref('job_developer')])
-
I check 'state' and number of 'Expected in Recruitment' after initiating the recruitment
-

View File

@ -81,7 +81,7 @@
<group>
<group>
<field name="employee_id" on_change="onchange_employee_id(employee_id)"/>
<field name="job_id" context="{'form_view_ref': 'hr.view_hr_job_employee_form'}"/>
<field name="job_id"/>
</group>
<group>
<field name="type_id"/>

View File

@ -37,13 +37,14 @@ You can define the different phases of interviews and easily rate the applicant
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
'images': ['images/hr_recruitment_analysis.jpeg','images/hr_recruitment_applicants.jpeg'],
'images': ['images/hr_recruitment_analysis.jpeg','images/hr_recruitment_applicants.jpeg','static/src/img/down1.png'],
'depends': [
'decimal_precision',
'hr',
'survey',
'calendar',
'fetchmail',
'web_kanban_gauge',
],
'data': [
'wizard/hr_recruitment_create_partner_job_view.xml',
@ -58,7 +59,11 @@ You can define the different phases of interviews and easily rate the applicant
'hr_recruitment_data.xml',
],
'demo': ['hr_recruitment_demo.xml'],
'js': [
'static/src/js/job_position.js',
],
'test': ['test/recruitment_process.yml'],
'css':['static/src/css/job_position.css'],
'installable': True,
'auto_install': False,
'application': True,

View File

@ -19,12 +19,10 @@
#
##############################################################################
from openerp import tools
from datetime import datetime
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp.tools import html2plaintext
AVAILABLE_PRIORITIES = [
('', ''),
@ -246,7 +244,8 @@ class hr_applicant(osv.Model):
if job_id:
job_record = self.pool.get('hr.job').browse(cr, uid, job_id, context=context)
department_id = job_record and job_record.department_id and job_record.department_id.id or False
return {'value': {'department_id': department_id}}
user_id = job_record and job_record.user_id and job_record.user_id.id or False
return {'value': {'department_id': department_id, 'user_id': user_id}}
def onchange_department_id(self, cr, uid, ids, department_id=False, stage_id=False, context=None):
if not stage_id:
@ -331,19 +330,12 @@ class hr_applicant(osv.Model):
value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context)
return value
def action_get_attachment_tree_view(self, cr, uid, ids, context):
domain = ['&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', ids)]
return {
'name': _('Attachments'),
'domain': domain,
'res_model': 'ir.attachment',
'type': 'ir.actions.act_window',
'view_id': False,
'view_mode': 'tree,form',
'view_type': 'form',
'limit': 80,
'context': "{'default_res_model': '%s'}" % (self._name)
}
def action_get_attachment_tree_view(self, cr, uid, ids, context=None):
model, action_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'action_attachment')
action = self.pool.get(model).read(cr, uid, action_id, context=context)
action['context'] = {'default_res_model': self._name, 'default_res_id': ids[0]}
action['domain'] = str(['&', ('res_model', '=', self._name), ('res_id', 'in', ids)])
return action
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
recipients = super(hr_applicant, self).message_get_suggested_recipients(cr, uid, ids, context=context)
@ -364,7 +356,7 @@ class hr_applicant(osv.Model):
val = msg.get('from').split('<')[0]
defaults = {
'name': msg.get('subject') or _("No Subject"),
'partner_name':val,
'partner_name': val,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
@ -378,13 +370,20 @@ class hr_applicant(osv.Model):
def create(self, cr, uid, vals, context=None):
if context is None:
context = {}
context['mail_create_nolog'] = True
if vals.get('department_id') and not context.get('default_department_id'):
context['default_department_id'] = vals.get('department_id')
if vals.get('job_id') or context.get('default_job_id'):
job_id = vals.get('job_id') or context.get('default_job_id')
vals.update(self.onchange_job(cr, uid, [], job_id, context=context)['value'])
obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
applicant = self.browse(cr, uid, obj_id, context=context)
if applicant.job_id:
self.pool.get('hr.job').message_post(cr, uid, [applicant.job_id.id], body=_('Applicant <b>created</b>'), subtype="hr_recruitment.mt_job_new_applicant", context=context)
name = applicant.partner_name if applicant.partner_name else applicant.name
self.pool['hr.job'].message_post(
cr, uid, [applicant.job_id.id],
body=_('New application from %s') % name,
subtype="hr_recruitment.mt_job_applicant_new", context=context)
return obj_id
def write(self, cr, uid, ids, vals, context=None):
@ -404,6 +403,15 @@ class hr_applicant(osv.Model):
else:
res = super(hr_applicant, self).write(cr, uid, ids, vals, context=context)
# post processing: if job changed, post a message on the job
if vals.get('job_id'):
for applicant in self.browse(cr, uid, ids, context=None):
name = applicant.partner_name if applicant.partner_name else applicant.name
self.pool['hr.job'].message_post(
cr, uid, [vals['job_id']],
body=_('New application from %s') % name,
subtype="hr_recruitment.mt_job_applicant_new", context=context)
# post processing: if stage changed, post a message in the chatter
if vals.get('stage_id'):
stage = self.pool['hr.recruitment.stage'].browse(cr, uid, vals['stage_id'], context=context)
@ -444,7 +452,7 @@ class hr_applicant(osv.Model):
address_id = self.pool.get('res.partner').address_get(cr, uid, [applicant.partner_id.id], ['contact'])['contact']
contact_name = self.pool.get('res.partner').name_get(cr, uid, [applicant.partner_id.id])[0][1]
if applicant.job_id and (applicant.partner_name or contact_name):
applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
applicant.job_id.write({'no_of_hired_employee': applicant.job_id.no_of_hired_employee + 1}, context=context)
emp_id = hr_employee.create(cr, uid, {'name': applicant.partner_name or contact_name,
'job_id': applicant.job_id.id,
'address_home_id': address_id,
@ -454,6 +462,10 @@ class hr_applicant(osv.Model):
'work_phone': applicant.department_id and applicant.department_id.company_id and applicant.department_id.company_id.phone or False,
})
self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
self.pool['hr.job'].message_post(
cr, uid, [applicant.job_id.id],
body=_('New Employee %s Hired') % applicant.partner_name if applicant.partner_name else applicant.name,
subtype="hr_recruitment.mt_job_applicant_hired", context=context)
else:
raise osv.except_osv(_('Warning!'), _('You must define an Applied Job and a Contact Name for this applicant.'))
@ -490,16 +502,37 @@ class hr_job(osv.osv):
_inherit = "hr.job"
_name = "hr.job"
_inherits = {'mail.alias': 'alias_id'}
def _get_attached_docs(self, cr, uid, ids, field_name, arg, context=None):
res = {}
attachment_obj = self.pool.get('ir.attachment')
for job_id in ids:
applicant_ids = self.pool.get('hr.applicant').search(cr, uid, [('job_id', '=', job_id)], context=context)
res[job_id] = attachment_obj.search(
cr, uid, [
'|',
'&', ('res_model', '=', 'hr.job'), ('res_id', '=', job_id),
'&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', applicant_ids)
], context=context)
return res
_columns = {
'survey_id': fields.many2one('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"),
'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True,
help="Email alias for this job position. New emails will automatically "
"create new applicants for this job position."),
'address_id': fields.many2one('res.partner', 'Job Location', help="Address where employees are working"),
'application_ids': fields.one2many('hr.applicant', 'job_id', 'Applications'),
'manager_id': fields.related('department_id', 'manager_id', type='many2one', string='Department Manager', relation='hr.employee', readonly=True, store=True),
'document_ids': fields.function(_get_attached_docs, type='one2many', relation='ir.attachment', string='Applications'),
'user_id': fields.many2one('res.users', 'Recruitment Responsible', track_visibility='onchange'),
'color': fields.integer('Color Index'),
}
def _address_get(self, cr, uid, context=None):
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
return user.company_id.partner_id.id
_defaults = {
'address_id': _address_get
}
@ -541,6 +574,18 @@ class hr_job(osv.osv):
'nodestroy': True,
}
def action_get_attachment_tree_view(self, cr, uid, ids, context=None):
#open attachments of job and related applicantions.
model, action_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'action_attachment')
action = self.pool.get(model).read(cr, uid, action_id, context=context)
applicant_ids = self.pool.get('hr.applicant').search(cr, uid, [('job_id', 'in', ids)], context=context)
action['context'] = {'default_res_model': self._name, 'default_res_id': ids[0]}
action['domain'] = str(['|', '&', ('res_model', '=', 'hr.job'), ('res_id', 'in', ids), '&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', applicant_ids)])
return action
def action_set_no_of_recruitment(self, cr, uid, id, value, context=None):
return self.write(cr, uid, [id], {'no_of_recruitment': value}, context=context)
class applicant_category(osv.osv):
""" Category of applicant """

View File

@ -495,13 +495,9 @@
<field name="alias_name">jobs</field>
<field name="alias_model_id" ref="model_hr_applicant"/>
<field name="alias_user_id" ref="base.user_root"/>
<field name="alias_parent_model_id" ref="model_hr_job"/>
</record>
<!-- Job-related subtypes for messaging / Chatter -->
<record id="mt_job_new_applicant" model="mail.message.subtype">
<field name="name">New Applicant</field>
<field name="res_model">hr.job</field>
</record>
<!-- Applicant-related subtypes for messaging / Chatter -->
<record id="mt_applicant_new" model="mail.message.subtype">
<field name="name">New Applicant</field>
@ -515,12 +511,35 @@
<field name="default" eval="False"/>
<field name="description">Stage changed</field>
</record>
<record id="mt_applicant_employee" model="mail.message.subtype">
<record id="mt_applicant_hired" model="mail.message.subtype">
<field name="name">Applicant Hired</field>
<field name="res_model">hr.applicant</field>
<field name="default" eval="False"/>
<field name="description">Applicant hired</field>
</record>
<!-- Job-related subtypes for messaging / Chatter -->
<record id="mt_job_applicant_new" model="mail.message.subtype">
<field name="name">Applicant Created</field>
<field name="res_model">hr.job</field>
<field name="default" eval="False"/>
<field name="parent_id" eval="ref('mt_applicant_new')"/>
<field name="relation_field">job_id</field>
</record>
<record id="mt_job_applicant_stage_changed" model="mail.message.subtype">
<field name="name">Applicant Stage Changed</field>
<field name="res_model">hr.job</field>
<field name="default" eval="True"/>
<field name="parent_id" eval="ref('mt_applicant_stage_changed')"/>
<field name="relation_field">job_id</field>
</record>
<record id="mt_job_applicant_hired" model="mail.message.subtype">
<field name="name">Applicant Hired</field>
<field name="res_model">hr.job</field>
<field name="default" eval="True"/>
<field name="parent_id" eval="ref('mt_applicant_hired')"/>
<field name="relation_field">job_id</field>
</record>
<!-- Applicant Categories(Tag) -->
<record id="tag_applicant_reserve" model="hr.applicant_category">
<field name="name">Reserve</field>

View File

@ -1,7 +1,6 @@
<?xml version="1.0"?>
<openerp>
<data>
######################## JOB OPPORTUNITIES (menu) ###########################
<record model="ir.actions.act_window" id="crm_case_categ0_act_job">
<field name="name">Applications</field>
@ -9,16 +8,15 @@
<field name="view_mode">kanban,tree,form,graph,calendar</field>
<field name="view_id" eval="False"/>
<field name="search_view_id" ref="view_crm_case_jobs_filter"/>
<field name="context">{'empty_list_help_model': 'hr.job'}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to add a new job applicant.
<p>
OpenERP helps you track applicants in the recruitment
process and follow up all operations: meetings, interviews, etc.
</p><p>
OpenERP helps you track applicants in the recruitment process
and follow up all operations: meetings, interviews, etc.
Candidates and their cv's are automatically created when they
apply for a job. If you install the document management modules,
all resumes are indexed automatically, so that you can easily
search through their content in the recruitment menu.
Applicants and their attached CV are created automatically when an email is sent.
If you install the document management modules, all resumes are indexed automatically,
so that you can easily search through their content.
</p>
</field>
</record>
@ -58,15 +56,10 @@
sequence="1"/>
<!-- ALL JOBS REQUESTS -->
<menuitem parent="base.menu_crm_case_job_req_main" id="hr.menu_hr_job_position" action="action_hr_job" sequence="1"/>
<menuitem
name="Applications"
parent="base.menu_crm_case_job_req_main"
id="menu_crm_case_categ0_act_job" action="crm_case_categ0_act_job" sequence="1"/>
<menuitem parent="hr.menu_hr_configuration" id="hr.menu_hr_job" action="hr.action_hr_job" sequence="2"/>
id="menu_crm_case_categ0_act_job" action="crm_case_categ0_act_job" sequence="2"/>
</data>
</openerp>

View File

@ -34,7 +34,7 @@
</record>
<!-- Jobs -->
<!-- Applicants -->
<record model="ir.ui.view" id="crm_case_tree_view_job">
<field name="name">Applicants</field>
<field name="model">hr.applicant</field>
@ -91,10 +91,10 @@
<label for="partner_name" class="oe_edit_only"/>
<h2 style="display: inline-block;">
<field name="partner_name" class="oe_inline"/>
<button string="Create Employee" name="create_employee_from_applicant" type="object"
class="oe_link oe_inline" style="margin-left: 8px;"
attrs="{'invisible': [('emp_id', '!=', False)]}"/>
</h2>
<button string="Create Employee" name="create_employee_from_applicant" type="object"
class="oe_link oe_inline" style="margin-left: 8px;"
attrs="{'invisible': [('emp_id', '!=', False)]}"/>
</div>
<group>
<group>
@ -307,46 +307,190 @@
</field>
</record>
<!-- HR Job -->
<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</field>
<field name="context">{'search_default_job_id': [active_id], 'default_job_id': active_id, 'empty_list_help_model': 'hr.job'}</field>
<field name="help" type="html">
<p>
OpenERP helps you track applicants in the recruitment
process and follow up all operations: meetings, interviews, etc.
</p><p>
Applicants and their attached CV are created automatically when an email is sent.
If you install the document management modules, all resumes are indexed automatically,
so that you can easily search through their content.
</p>
</field>
</record>
<!-- Jobs -->
<record id="view_job_filter_recruitment" model="ir.ui.view">
<field name="name">Job</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr.view_job_filter"/>
<field name="arch" type="xml">
<field name="department_id" positon="after">
<separator/>
<filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
</field>
</field>
</record>
<record id="hr_job_survey" model="ir.ui.view">
<field name="name">hr.job.form1</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr.view_hr_job_form"/>
<field name="arch" type="xml">
<group name="job_data" position="inside">
<group name="employee_data" position="inside">
<label for="survey_id" groups="base.group_user"/>
<div groups="base.group_user">
<field name="survey_id" class="oe_inline" domain="[('type','=','Human Resources')]"/>
<button string="Print Interview" name="action_print_survey" type="object" attrs="{'invisible':[('survey_id','=',False)]}" class="oe_inline oe_link"/>
</div>
<label for="address_id"/>
<div>
<field name="address_id" context="{'show_address': 1}"/>
<span class="oe_grey">(empty = remote work)</span>
</div>
</group>
<field name="expected_employees" position="after">
<label for="survey_id" groups="base.group_user"/>
<div groups="base.group_user">
<field name="survey_id" class="oe_inline" domain="[('type','=','Human Resources')]"/>
<button class="oe_inline"
string="Interview"
name="action_print_survey" type="object"
attrs="{'invisible':[('survey_id','=',False)]}"/>
</div>
</field>
<xpath expr="//group[@name='job_data']" position="after">
<group name="group_alias"
attrs="{'invisible': [('alias_domain', '=', False)]}">
<label for="alias_name" string="Email Alias"/>
<div name="alias_def">
<xpath expr="//field[@name='department_id']" position="after">
<label for="alias_name" string="Specific Email Address" attrs="{'invisible': [('alias_domain', '=', False)]}" help ="Define a specific contact address for this job position. If you keep it empty, the default email address will be used which is in human resources settings"/>
<div name="alias_def" attrs="{'invisible': [('alias_domain', '=', False)]}">
<field name="alias_id" class="oe_read_only oe_inline"
string="Email Alias" required="0"/>
<div class="oe_edit_only oe_inline" name="edit_alias" style="display: inline;" >
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
</div>
</div>
<field name="alias_contact" class="oe_inline" string="Accept Emails From"/>
</group>
</xpath>
<xpath expr="//field[@name='department_id']" position="after">
<field name="user_id" class="oe_inline"/>
</xpath>
<div name="buttons" position="inside">
<button string="Applications" name="%(action_hr_job_applications)d" context="{'default_user_id': user_id}" type="action"/>
<button string="Documents" name="action_get_attachment_tree_view" type="object"/>
</div>
</field>
</record>
<record id="view_hr_job_kanban" model="ir.ui.view">
<field name="name">hr.job.kanban</field>
<field name="model">hr.job</field>
<field name="arch" type="xml">
<kanban version="7.0" class="oe_background_grey">
<field name="name"/>
<field name="department_id"/>
<field name="no_of_recruitment"/>
<field name="color"/>
<field name="application_ids"/>
<field name="document_ids"/>
<field name="no_of_hired_employee"/>
<field name="manager_id"/>
<field name="survey_id"/>
<field name="state"/>
<field name="user_id"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_job oe_kanban_card oe_kanban_global_click">
<div class="oe_dropdown_toggle oe_dropdown_kanban oe_custom">
<span class="oe_e">í</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('edit')">
<li><a type="edit">Edit...</a></li>
</t>
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
<li><ul class="oe_kanban_colorpicker" data-field="color"/></li>
</ul>
</div>
<div class = "oe_kanban_content">
<t t-if="record.user_id.raw_value">
<img t-att-src="kanban_image('res.users', 'image_medium', record.user_id.raw_value[0])" t-att-title="record.user_id.value" class="oe_kanban_avatar oe_job_avatar"/>
</t>
<t t-if="record.user_id.raw_value === false">
<img t-att-src='_s + "/base/static/src/img/avatar.png"' class="oe_kanban_avatar oe_job_avatar"/>
</t>
<div class="oe_job_detail">
<div class="oe_job oe_name oe_kanban_ellipsis">
<field name="name"/>
</div>
<div class="oe_job oe_department oe_kanban_ellipsis">
<field name="department_id"/>
<span t-if="record.manager_id.value" class="oe_manager_name">
(<t t-esc="record.manager_id.value"/>)
</span>
</div>
<div class="oe_job_alias oe_kanban_ellipsis" t-if=" record.alias_id.value and record.state.raw_value == 'recruit'">
<span class="oe_e">%%</span><small><field name="alias_id"/></small>
</div>
</div>
<t t-if="record.state.raw_value == 'recruit'">
<div class="oe_applications">
<a name="%(action_hr_job_applications)d" type="action">
<span t-if="record.application_ids.raw_value.length gt 1"><t t-esc="record.application_ids.raw_value.length"/> Applications</span>
<span t-if="record.application_ids.raw_value.length lt 2"><t t-esc="record.application_ids.raw_value.length"/> Application</span>
</a>
<br/>
<a t-if="record.document_ids.raw_value.length gt 0" name="action_get_attachment_tree_view" type="object">
<span t-if="record.document_ids.raw_value.length gt 1"><t t-esc="record.document_ids.raw_value.length"/> Documents</span>
<span t-if="record.document_ids.raw_value.length lt 2"><t t-esc="record.document_ids.raw_value.length"/> Document</span>
</a>
</div>
<div class="oe_job_justgage">
<field state="recruit" name="no_of_hired_employee" widget="gauge"
style="width:160px; height: 120px;"
options="{
'max_field': 'no_of_recruitment',
'label': 'Hired Employees',
'on_change': 'action_set_no_of_recruitment',
'on_click_label': 'employee(s) to recruit',
'force_set': False,
'gauge_value_field': 'no_of_recruitment',
}">
Hired Employees
</field>
</div>
</t>
<t t-if="record.state.raw_value == 'open'">
<div class="oe_start_recruitment">
<p><b>click here</b>, To start the recruitment</p>
<img src="/hr_recruitment/static/src/img/down1.png"/>
</div>
</t>
<div class="oe_launch_recruitment">
<a t-if="record.state.raw_value == 'open'" data-name="job_recruitment" data-type="object" class="oe_kanban_action">Launch Recruitment</a>
<a t-if="record.state.raw_value == 'recruit'" data-name="job_open" data-type="object" class="oe_kanban_action">Recruitment Done</a>
<a t-if="record.survey_id.raw_value"> | </a>
<a t-if="record.survey_id.raw_value" data-name="action_print_survey" data-type="object" class="oe_kanban_action">Print Interview</a>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- hr related job position menu action -->
<record model="ir.actions.act_window" id="action_hr_job">
<field name="name">Job Positions</field>
<field name="res_model">hr.job</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{'search_default_in_recruitment': 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new job or remove the filter on "In Recruitment" to recruit for an on hold job.
</p>
<p>
Define job position profile and manage recruitment in a context of a particular job: print interview survey, define number of expected new employees, and manage its recruitment pipe
</p>
</field>
</record>
<!-- Stage Tree View -->
<record model="ir.ui.view" id="hr_recruitment_stage_tree">
<field name="name">hr.recruitment.stage.tree</field>

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
# Copyright (C) 2004-Today OpenERP S.A. (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -19,9 +19,11 @@
#
##############################################################################
from openerp import SUPERUSER_ID
from openerp.osv import fields, osv
class hr_applicant_settings(osv.osv_memory):
class hr_applicant_settings(osv.TransientModel):
_name = 'hr.config.settings'
_inherit = ['hr.config.settings', 'fetchmail.config.settings']
@ -32,6 +34,44 @@ class hr_applicant_settings(osv.osv_memory):
'fetchmail_applicants': fields.boolean('Create applicants from an incoming email account',
fetchmail_model='hr.applicant', fetchmail_name='Incoming HR Applications',
help='Allow applicants to send their job application to an email address (jobs@mycompany.com), '
'and create automatically application documents in the system.'),
'and create automatically application documents in the system.',
deprecated='Will be removed with OpenERP v8, not applicable anymore. Use aliases instead.'),
'alias_prefix': fields.char('Default Alias Name for Jobs'),
'alias_domain': fields.char('Alias Domain'),
}
_defaults = {
'alias_domain': lambda self, cr, uid, context: self.pool['mail.alias']._get_alias_domain(cr, SUPERUSER_ID, [1], None, None)[1],
}
def _find_default_job_alias_id(self, cr, uid, context=None):
alias_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'hr_recruitment.mail_alias_jobs')
if not alias_id:
alias_ids = self.pool['mail.alias'].search(
cr, uid, [
('alias_model_id.model', '=', 'hr.applicant'),
('alias_force_thread_id', '=', 0),
('alias_parent_model_id.model', '=', 'hr.job'),
('alias_parent_thread_id', '=', 0),
('alias_defaults', '=', '{}')
], context=context)
alias_id = alias_ids and alias_ids[0] or False
return alias_id
def get_default_alias_prefix(self, cr, uid, ids, context=None):
alias_name = False
alias_id = self._find_default_job_alias_id(cr, uid, context=context)
if alias_id:
alias_name = self.pool['mail.alias'].browse(cr, uid, alias_id, context=context).alias_name
return {'alias_prefix': alias_name}
def set_default_alias_prefix(self, cr, uid, ids, context=None):
mail_alias = self.pool.get('mail.alias')
for record in self.browse(cr, uid, ids, context=context):
alias_id = self._find_default_job_alias_id(cr, uid, context=context)
if not alias_id:
create_ctx = dict(context, alias_model_name='hr.applicant', alias_parent_model_name='hr.job')
alias_id = self.pool['mail.alias'].create(cr, uid, {'alias_name': record.alias_prefix}, context=create_ctx)
else:
mail_alias.write(cr, uid, alias_id, {'alias_name': record.alias_prefix}, context=context)
return True

View File

@ -19,6 +19,14 @@
<label for="module_document"/>
</div>
</div>
<xpath expr="//div[@name='hr_recruitment']" position="after">
<div attrs="{'invisible': ['|',('module_hr_recruitment','=',False),('alias_domain', '=', False)]}">
<label string="Default job email address"/>
<field name="alias_prefix" class="oe_inline" attrs="{'required': [('alias_domain', '!=', False)]}"/>
@
<field name="alias_domain" class="oe_inline" readonly="1"/>
</div>
</xpath>
</field>
</record>
</data>

View File

@ -0,0 +1,2 @@
job_position.css: job_position.sass
sass --trace -t expanded job_position.sass job_position.css

View File

@ -0,0 +1,86 @@
.openerp .oe_kanban_job{
width: 355px;
min-height: 165px !important;
}
.openerp .oe_job_alias{
margin: 3px;
}
.openerp .oe_job_detail{
height: 70px;
width: 308px
}
.openerp .oe_job_alias .oe_e {
font-size: 30px;
line-height: 6px;
vertical-align: top;
margin-right: 3px;
color: white;
text-shadow: 0px 0px 2px black;
float: left;
}
.openerp .oe_job {
font-size: 112%;
position: inline;
margin: 3px 3px;
color: #4c4c4c;
height: 16px;
}
.openerp img.oe_job_avatar {
position: absolute;
width: 24px;
height: 24px;
margin-left: 295px;
margin-top: -5px;
}
.openerp .oe_launch_recruitment{
float: left;
position: absolute;
bottom: 3px;
left: 10px;
}
.openerp div.oe_applications {
position: absolute;;
margin-top: 16px;
font-size: 14px;
}
.openerp .oe_applications > a > span:hover{
margin: 4px 0;
text-decoration: underline;
}
.openerp .oe_name {
font-size: 14px;
font-weight: bold;
}
.openerp .oe_manager_name {
width: 135px;
font-size: 11px;
color: gray;
}
.openerp .oe_job_justgage {
float: right;
margin-top: -40px;
margin-right: -58px;
width:200px;
height:130px;
}
.openerp .oe_department {
width: 350px;
}
.openerp .oe_start_recruitment {
padding-top: 10px;
}
.openerp .oe_start_recruitment p {
font-size: 14px;
color: gray;
padding-left: 50px;
}
.openerp .oe_start_recruitment img {
margin-top: -22px;
width: 32px;
height: 34px;
float: left;
padding-left: 12px;
}
.openerp .oe_job_messages{
margin-top: 40px !important;
}

View File

@ -0,0 +1,70 @@
.openerp
.oe_kanban_job
width: 355px
min-height: 165px !important
.oe_job_alias
margin: 3px
.oe_job_detail
height: 70px
width: 308px
.oe_job_alias .oe_e
font-size: 30px
line-height: 6px
vertical-align: top
margin-right: 3px
color: white
text-shadow: 0px 0px 2px black
float: left
.oe_job
font-size: 112%
position: inline
margin: 3px 3px
color: #4c4c4c
height: 16px
img.oe_job_avatar
position: absolute
width: 24px
height: 24px
margin-left: 295px
margin-top: -5px
.oe_launch_recruitment
float: left
position: absolute
bottom: 3px
left: 10px
div.oe_applications
position: absolute
margin-top: 16px
font-size: 14px
.oe_applications > a > span:hover
margin: 4px 0
text-decoration: underline
.oe_name
font-size: 14px
font-weight: bold
.oe_manager_name
width: 135px
font-size: 11px
color: gray
.oe_job_justgage
float: right
margin-top: -40px
margin-right: -58px
width: 200px
height: 130px
.oe_department
width: 350px
.oe_start_recruitment
padding-top: 10px
p
font-size: 14px
color: gray
padding-left: 50px
img
margin-top: -22px
width: 32px
height: 34px
float: left
padding-left: 12px
.oe_job_messages
margin-top: 40px !important

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,13 @@
openerp.hr_recruitment = function (openerp) {
openerp.web_kanban.KanbanRecord.include({
on_card_clicked: function() {
if (this.view.dataset.model === 'hr.job') {
this.$('.oe_applications a').first().click();
} else {
this._super.apply(this, arguments);
}
},
});
}

View File

@ -154,6 +154,12 @@ class mail_alias(osv.Model):
sequence = (sequence + 1) if sequence else 2
return new_name
def _clean_and_make_unique(self, cr, uid, name, context=None):
# when an alias name appears to already be an email, we keep the local part only
name = remove_accents(name).lower().split('@')[0]
name = re.sub(r'[^\w+.]+', '-', name)
return self._find_unique(cr, uid, name, context=context)
def migrate_to_alias(self, cr, child_model_name, child_table_name, child_model_auto_init_fct,
alias_model_name, alias_id_column, alias_key, alias_prefix='', alias_force_key='', alias_defaults={},
alias_generate_name=False, context=None):
@ -199,7 +205,7 @@ class mail_alias(osv.Model):
alias_vals['alias_parent_thread_id'] = obj_data['id']
alias_create_ctx = dict(context, alias_model_name=alias_model_name, alias_parent_model_name=child_model_name)
alias_id = mail_alias.create(cr, SUPERUSER_ID, alias_vals, context=alias_create_ctx)
child_class_model.write(cr, SUPERUSER_ID, obj_data['id'], {'alias_id': alias_id})
child_class_model.write(cr, SUPERUSER_ID, obj_data['id'], {'alias_id': alias_id}, context={'mail_notrack': True})
_logger.info('Mail alias created for %s %s (id %s)', child_model_name, obj_data[alias_key], obj_data['id'])
# Finally attempt to reinstate the missing constraint
@ -227,11 +233,7 @@ class mail_alias(osv.Model):
model_name = context.get('alias_model_name')
parent_model_name = context.get('alias_parent_model_name')
if vals.get('alias_name'):
# when an alias name appears to already be an email, we keep the local part only
alias_name = remove_accents(vals['alias_name']).lower().split('@')[0]
alias_name = re.sub(r'[^\w+.]+', '-', alias_name)
alias_name = self._find_unique(cr, uid, alias_name, context=context)
vals['alias_name'] = alias_name
vals['alias_name'] = self._clean_and_make_unique(cr, uid, vals.get('alias_name'), context=context)
if model_name:
model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', model_name)], context=context)[0]
vals['alias_model_id'] = model_id
@ -240,6 +242,12 @@ class mail_alias(osv.Model):
vals['alias_parent_model_id'] = model_id
return super(mail_alias, self).create(cr, uid, vals, context=context)
def write(self, cr, uid, ids, vals, context=None):
""""give uniqe alias name if given alias name is allready assigned"""
if vals.get('alias_name'):
vals['alias_name'] = self._clean_and_make_unique(cr, uid, vals.get('alias_name'), context=context)
return super(mail_alias, self).write(cr, uid, ids, vals, context=context)
def open_document(self, cr, uid, ids, context=None):
alias = self.browse(cr, uid, ids, context=context)[0]
if not alias.alias_model_id or not alias.alias_force_thread_id:

View File

@ -113,9 +113,9 @@ class mail_thread(osv.AbstractModel):
object_id.alias_id.alias_model_id.model == self._name and \
object_id.alias_id.alias_force_thread_id == 0:
alias = object_id.alias_id
elif catchall_domain and model: # no specific res_id given -> generic help message, take an example alias (i.e. alias of some section_id)
if not alias and catchall_domain and model: # no res_id or res_id not linked to an alias -> generic help message, take a generic alias of the model
alias_obj = self.pool.get('mail.alias')
alias_ids = alias_obj.search(cr, uid, [("alias_parent_model_id.model", "=", model), ("alias_name", "!=", False), ('alias_force_thread_id', '=', False)], context=context, order='id ASC')
alias_ids = alias_obj.search(cr, uid, [("alias_parent_model_id.model", "=", model), ("alias_name", "!=", False), ('alias_force_thread_id', '=', False), ('alias_parent_thread_id', '=', False)], context=context, order='id ASC')
if alias_ids and len(alias_ids) == 1:
alias = alias_obj.browse(cr, uid, alias_ids[0], context=context)
@ -384,7 +384,10 @@ class mail_thread(osv.AbstractModel):
track_ctx = dict(context)
if 'lang' not in track_ctx:
track_ctx['lang'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).lang
tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
if not context.get('mail_notrack'):
tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
else:
tracked_fields = []
if tracked_fields:
records = self.browse(cr, uid, ids, context=track_ctx)
initial_values = dict((this.id, dict((key, getattr(this, key)) for key in tracked_fields.keys())) for this in records)

View File

@ -301,7 +301,7 @@
<field name="monthly_invoiced" widget="gauge" style="width:160px; height: 120px; cursor: pointer;"
options="{'max_field': 'invoiced_target'}">Invoiced</field>
<field name="invoiced_forecast" widget="gauge" style="width:160px; height: 120px; cursor: pointer;"
options="{'max_field': 'invoiced_target', 'action_change': 'action_forecast'}">Forecast</field>
options="{'max_field': 'invoiced_target', 'on_change': 'action_forecast'}">Forecast</field>
</div>
<div class="oe_center oe_salesteams_help" style="color:#bbbbbb;" t-if="!record.invoiced_target.raw_value">
<br/>Define an invoicing target in the sales team settings to see the period's achievement and forecast at a glance.