1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
23 from openerp import tools
25 from openerp.addons.base_status.base_stage import base_stage
26 from datetime import datetime
27 from openerp.osv import fields, osv
28 from openerp.tools.translate import _
29 from openerp.tools import html2plaintext
33 ('cancel', 'Refused'),
34 ('open', 'In Progress'),
35 ('pending', 'Pending'),
39 AVAILABLE_PRIORITIES = [
48 class hr_recruitment_source(osv.osv):
49 """ Sources of HR Recruitment """
50 _name = "hr.recruitment.source"
51 _description = "Source of Applicants"
53 'name': fields.char('Source Name', size=64, required=True, translate=True),
56 class hr_recruitment_stage(osv.osv):
57 """ Stage of HR Recruitment """
58 _name = "hr.recruitment.stage"
59 _description = "Stage of Recruitment"
62 'name': fields.char('Name', size=64, required=True, translate=True),
63 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of stages."),
64 'department_id':fields.many2one('hr.department', 'Specific to a Department', help="Stages of the recruitment process may be different per department. If this stage is common to all departments, keep this field empty."),
65 'state': fields.selection(AVAILABLE_STATES, 'Status', required=True, help="The related status for the stage. The status of your document will automatically change according to the selected stage. Example, a stage is related to the status 'Close', when your document reach this stage, it will be automatically closed."),
66 'fold': fields.boolean('Hide in views if empty', help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
67 'requirements': fields.text('Requirements'),
75 class hr_recruitment_degree(osv.osv):
76 """ Degree of HR Recruitment """
77 _name = "hr.recruitment.degree"
78 _description = "Degree of Recruitment"
80 'name': fields.char('Name', size=64, required=True, translate=True),
81 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of degrees."),
87 ('name_uniq', 'unique (name)', 'The name of the Degree of Recruitment must be unique!')
90 class hr_applicant(base_stage, osv.Model):
91 _name = "hr.applicant"
92 _description = "Applicant"
94 _inherit = ['mail.thread', 'ir.needaction_mixin']
97 'hr_recruitment.mt_applicant_hired': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done',
98 'hr_recruitment.mt_applicant_refused': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'cancel',
101 'hr_recruitment.mt_stage_changed': lambda self, cr, uid, obj, ctx=None: obj['state'] not in ['done', 'cancel'],
105 def _get_default_department_id(self, cr, uid, context=None):
106 """ Gives default department by checking if present in the context """
107 return (self._resolve_department_id_from_context(cr, uid, context=context) or False)
109 def _get_default_stage_id(self, cr, uid, context=None):
110 """ Gives default stage_id """
111 department_id = self._get_default_department_id(cr, uid, context=context)
112 return self.stage_find(cr, uid, [], department_id, [('state', '=', 'draft')], context=context)
114 def _resolve_department_id_from_context(self, cr, uid, context=None):
115 """ Returns ID of department based on the value of 'default_department_id'
116 context key, or None if it cannot be resolved to a single
121 if type(context.get('default_department_id')) in (int, long):
122 return context.get('default_department_id')
123 if isinstance(context.get('default_department_id'), basestring):
124 department_name = context['default_department_id']
125 department_ids = self.pool.get('hr.department').name_search(cr, uid, name=department_name, context=context)
126 if len(department_ids) == 1:
127 return int(department_ids[0][0])
130 def _get_default_company_id(self, cr, uid, department_id=None, context=None):
133 department = self.pool['hr.department'].browse(cr, uid, department_id, context=context)
134 company_id = department.company_id.id if department and department.company_id else False
136 company_id = self.pool['res.company']._company_default_get(cr, uid, 'hr.applicant', context=context)
139 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
140 access_rights_uid = access_rights_uid or uid
141 stage_obj = self.pool.get('hr.recruitment.stage')
142 order = stage_obj._order
143 # lame hack to allow reverting search, should just work in the trivial case
144 if read_group_order == 'stage_id desc':
145 order = "%s desc" % order
146 # retrieve section_id from the context and write the domain
147 # - ('id', 'in', 'ids'): add columns that should be present
148 # - OR ('department_id', '=', False), ('fold', '=', False): add default columns that are not folded
149 # - OR ('department_id', 'in', department_id), ('fold', '=', False) if department_id: add department columns that are not folded
150 department_id = self._resolve_department_id_from_context(cr, uid, context=context)
153 search_domain += ['|', ('department_id', '=', department_id)]
154 search_domain += ['|', ('id', 'in', ids), ('department_id', '=', False)]
155 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
156 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
157 # restore order of the search
158 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
161 for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
162 fold[stage.id] = stage.fold or False
165 def _compute_day(self, cr, uid, ids, fields, args, context=None):
167 @param cr: the current row, from the database cursor,
168 @param uid: the current user’s ID for security checks,
169 @param ids: List of Openday’s IDs
170 @return: difference between current date and log date
171 @param context: A standard dictionary for contextual values
174 for issue in self.browse(cr, uid, ids, context=context):
181 if field in ['day_open']:
183 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
184 date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
185 ans = date_open - date_create
187 elif field in ['day_close']:
188 if issue.date_closed:
189 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
190 date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
191 ans = date_close - date_create
193 duration = float(ans.days)
194 res[issue.id][field] = abs(float(duration))
198 'name': fields.char('Subject', size=128, required=True),
199 'active': fields.boolean('Active', help="If the active field is set to false, it will allow you to hide the case without removing it."),
200 'description': fields.text('Description'),
201 'email_from': fields.char('Email', size=128, help="These people will receive email."),
202 'email_cc': fields.text('Watchers Emails', size=252, help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
203 'probability': fields.float('Probability'),
204 'partner_id': fields.many2one('res.partner', 'Contact'),
205 'create_date': fields.datetime('Creation Date', readonly=True, select=True),
206 'write_date': fields.datetime('Update Date', readonly=True),
207 'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage', track_visibility='onchange',
208 domain="['&', ('fold', '=', False), '|', ('department_id', '=', department_id), ('department_id', '=', False)]"),
209 'state': fields.related('stage_id', 'state', type="selection", store=True,
210 selection=AVAILABLE_STATES, string="Status", readonly=True,
211 help='The status is set to \'Draft\', when a case is created.\
212 If the case is in progress the status is set to \'Open\'.\
213 When the case is over, the status is set to \'Done\'.\
214 If the case needs to be reviewed then the status is \
215 set to \'Pending\'.'),
216 'categ_ids': fields.many2many('hr.applicant_category', string='Tags'),
217 'company_id': fields.many2one('res.company', 'Company'),
218 'user_id': fields.many2one('res.users', 'Responsible', track_visibility='onchange'),
220 'date_closed': fields.datetime('Closed', readonly=True, select=True),
221 'date_open': fields.datetime('Opened', readonly=True, select=True),
222 'date': fields.datetime('Date'),
223 'date_action': fields.date('Next Action Date'),
224 'title_action': fields.char('Next Action', size=64),
225 'priority': fields.selection(AVAILABLE_PRIORITIES, 'Appreciation'),
226 'job_id': fields.many2one('hr.job', 'Applied Job'),
227 'salary_proposed_extra': fields.char('Proposed Salary Extra', size=100, help="Salary Proposed by the Organisation, extra advantages"),
228 'salary_expected_extra': fields.char('Expected Salary Extra', size=100, help="Salary Expected by Applicant, extra advantages"),
229 'salary_proposed': fields.float('Proposed Salary', help="Salary Proposed by the Organisation"),
230 'salary_expected': fields.float('Expected Salary', help="Salary Expected by Applicant"),
231 'availability': fields.integer('Availability'),
232 'partner_name': fields.char("Applicant's Name", size=64),
233 'partner_phone': fields.char('Phone', size=32),
234 'partner_mobile': fields.char('Mobile', size=32),
235 'type_id': fields.many2one('hr.recruitment.degree', 'Degree'),
236 'department_id': fields.many2one('hr.department', 'Department'),
237 'survey': fields.related('job_id', 'survey_id', type='many2one', relation='survey', string='Survey'),
238 'response': fields.integer("Response"),
239 'reference': fields.char('Referred By', size=128),
240 'source_id': fields.many2one('hr.recruitment.source', 'Source'),
241 'day_open': fields.function(_compute_day, string='Days to Open', \
242 multi='day_open', type="float", store=True),
243 'day_close': fields.function(_compute_day, string='Days to Close', \
244 multi='day_close', type="float", store=True),
245 'color': fields.integer('Color Index'),
246 'emp_id': fields.many2one('hr.employee', 'employee'),
247 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
251 'active': lambda *a: 1,
252 'user_id': lambda s, cr, uid, c: uid,
253 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
254 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
255 'department_id': lambda s, cr, uid, c: s._get_default_department_id(cr, uid, c),
256 'company_id': lambda s, cr, uid, c: s._get_default_company_id(cr, uid, s._get_default_department_id(cr, uid, c), c),
261 'stage_id': _read_group_stage_ids
264 def onchange_job(self, cr, uid, ids, job, context=None):
266 job_record = self.pool.get('hr.job').browse(cr, uid, job, context=context)
267 if job_record and job_record.department_id:
268 return {'value': {'department_id': job_record.department_id.id}}
271 def onchange_department_id(self, cr, uid, ids, department_id=False, context=None):
272 obj_recru_stage = self.pool.get('hr.recruitment.stage')
273 stage_ids = obj_recru_stage.search(cr, uid, ['|',('department_id','=',department_id),('department_id','=',False)], context=context)
274 stage_id = stage_ids and stage_ids[0] or False
275 return {'value': {'stage_id': stage_id}}
277 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
278 data = {'partner_phone': False,
279 'partner_mobile': False,
282 addr = self.pool.get('res.partner').browse(cr, uid, partner_id, context)
283 data.update({'partner_phone': addr.phone,
284 'partner_mobile': addr.mobile,
285 'email_from': addr.email})
286 return {'value': data}
288 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
289 """ Override of the base.stage method
290 Parameter of the stage search taken from the lead:
291 - department_id: if set, stages must belong to this section or
294 if isinstance(cases, (int, long)):
295 cases = self.browse(cr, uid, cases, context=context)
296 # collect all section_ids
299 department_ids.append(section_id)
301 if case.department_id:
302 department_ids.append(case.department_id.id)
303 # OR all section_ids and OR with case_default
306 search_domain += ['|', ('department_id', 'in', department_ids)]
307 search_domain.append(('department_id', '=', False))
308 # AND with the domain in parameter
309 search_domain += list(domain)
310 # perform search, return the first found
311 stage_ids = self.pool.get('hr.recruitment.stage').search(cr, uid, search_domain, order=order, context=context)
316 def action_makeMeeting(self, cr, uid, ids, context=None):
317 """ This opens Meeting's calendar view to schedule meeting on current applicant
318 @return: Dictionary value for created Meeting view
320 applicant = self.browse(cr, uid, ids[0], context)
321 category = self.pool.get('ir.model.data').get_object(cr, uid, 'hr_recruitment', 'categ_meet_interview', context)
322 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
324 'default_partner_ids': applicant.partner_id and [applicant.partner_id.id] or False,
325 'default_user_id': uid,
326 'default_name': applicant.name,
327 'default_categ_ids': category and [category.id] or False,
331 def action_print_survey(self, cr, uid, ids, context=None):
333 If response is available then print this response otherwise print survey form(print template of the survey).
335 @param self: The object pointer
336 @param cr: the current row, from the database cursor,
337 @param uid: the current user’s ID for security checks,
338 @param ids: List of Survey IDs
339 @param context: A standard dictionary for contextual values
340 @return: Dictionary value for print survey form.
344 record = self.browse(cr, uid, ids, context=context)
345 record = record and record[0]
346 context.update({'survey_id': record.survey.id, 'response_id': [record.response], 'response_no': 0, })
347 value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context)
350 def message_get_suggested_recipients(self, cr, uid, ids, context=None):
351 recipients = super(hr_applicant, self).message_get_suggested_recipients(cr, uid, ids, context=context)
352 for applicant in self.browse(cr, uid, ids, context=context):
353 if applicant.partner_id:
354 self._message_add_suggested_recipient(cr, uid, recipients, applicant, partner=applicant.partner_id, reason=_('Contact'))
355 elif applicant.email_from:
356 self._message_add_suggested_recipient(cr, uid, recipients, applicant, email=applicant.email_from, reason=_('Contact Email'))
359 def message_new(self, cr, uid, msg, custom_values=None, context=None):
360 """ Overrides mail_thread message_new that is called by the mailgateway
361 through message_process.
362 This override updates the document according to the email.
364 if custom_values is None:
366 desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
368 'name': msg.get('subject') or _("No Subject"),
370 'email_from': msg.get('from'),
371 'email_cc': msg.get('cc'),
373 'partner_id': msg.get('author_id', False),
375 if msg.get('priority'):
376 defaults['priority'] = msg.get('priority')
377 defaults.update(custom_values)
378 return super(hr_applicant, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
380 def create(self, cr, uid, vals, context=None):
383 if vals.get('department_id') and not context.get('default_department_id'):
384 context['default_department_id'] = vals.get('department_id')
386 obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
387 applicant = self.browse(cr, uid, obj_id, context=context)
389 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)
392 def case_open(self, cr, uid, ids, context=None):
394 open Request of the applicant for the hr_recruitment
396 res = super(hr_applicant, self).case_open(cr, uid, ids, context)
397 date = self.read(cr, uid, ids, ['date_open'])[0]
398 if not date['date_open']:
399 self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),})
402 def case_close(self, cr, uid, ids, context=None):
403 res = super(hr_applicant, self).case_close(cr, uid, ids, context)
406 def case_close_with_emp(self, cr, uid, ids, context=None):
409 hr_employee = self.pool.get('hr.employee')
410 model_data = self.pool.get('ir.model.data')
411 act_window = self.pool.get('ir.actions.act_window')
413 for applicant in self.browse(cr, uid, ids, context=context):
415 if applicant.partner_id:
416 address_id = self.pool.get('res.partner').address_get(cr,uid,[applicant.partner_id.id],['contact'])['contact']
418 applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
419 emp_id = hr_employee.create(cr,uid,{'name': applicant.partner_name or applicant.name,
420 'job_id': applicant.job_id.id,
421 'address_home_id': address_id,
422 'department_id': applicant.department_id.id
424 self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
425 self.case_close(cr, uid, [applicant.id], context)
427 raise osv.except_osv(_('Warning!'), _('You must define Applied Job for this applicant.'))
429 action_model, action_id = model_data.get_object_reference(cr, uid, 'hr', 'open_view_employee_list')
430 dict_act_window = act_window.read(cr, uid, action_id, [])
432 dict_act_window['res_id'] = emp_id
433 dict_act_window['view_mode'] = 'form,tree'
434 return dict_act_window
436 def case_cancel(self, cr, uid, ids, context=None):
437 """Overrides cancel for crm_case for setting probability
439 res = super(hr_applicant, self).case_cancel(cr, uid, ids, context)
440 self.write(cr, uid, ids, {'probability': 0.0})
443 def case_pending(self, cr, uid, ids, context=None):
444 """Marks case as pending"""
445 res = super(hr_applicant, self).case_pending(cr, uid, ids, context)
446 self.write(cr, uid, ids, {'probability': 0.0})
449 def case_reset(self, cr, uid, ids, context=None):
450 """Resets case as draft
452 res = super(hr_applicant, self).case_reset(cr, uid, ids, context)
453 self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
456 def set_priority(self, cr, uid, ids, priority, *args):
457 """Set applicant priority
459 return self.write(cr, uid, ids, {'priority': priority})
461 def set_high_priority(self, cr, uid, ids, *args):
462 """Set applicant priority to high
464 return self.set_priority(cr, uid, ids, '1')
466 def set_normal_priority(self, cr, uid, ids, *args):
467 """Set applicant priority to normal
469 return self.set_priority(cr, uid, ids, '3')
472 class hr_job(osv.osv):
475 _inherits = {'mail.alias': 'alias_id'}
477 '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"),
478 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True,
479 help="Email alias for this job position. New emails will automatically "
480 "create new applicants for this job position."),
483 'alias_domain': False, # always hide alias during creation
486 def _auto_init(self, cr, context=None):
487 """Installation hook to create aliases for all jobs and avoid constraint errors."""
490 alias_context = dict(context, alias_model_name='hr.applicant')
491 return self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(hr_job, self)._auto_init,
492 self._columns['alias_id'], 'name', alias_prefix='job+', alias_defaults={'job_id': 'id'}, context=alias_context)
494 def create(self, cr, uid, vals, context=None):
495 mail_alias = self.pool.get('mail.alias')
496 if not vals.get('alias_id'):
497 vals.pop('alias_name', None) # prevent errors during copy()
498 alias_id = mail_alias.create_unique_alias(cr, uid,
499 # Using '+' allows using subaddressing for those who don't
500 # have a catchall domain setup.
501 {'alias_name': 'jobs+'+vals['name']},
502 model_name="hr.applicant",
504 vals['alias_id'] = alias_id
505 res = super(hr_job, self).create(cr, uid, vals, context)
506 mail_alias.write(cr, uid, [vals['alias_id']], {"alias_defaults": {'job_id': res}}, context)
509 def unlink(self, cr, uid, ids, context=None):
510 # Cascade-delete mail aliases as well, as they should not exist without the job position.
511 mail_alias = self.pool.get('mail.alias')
512 alias_ids = [job.alias_id.id for job in self.browse(cr, uid, ids, context=context) if job.alias_id]
513 res = super(hr_job, self).unlink(cr, uid, ids, context=context)
514 mail_alias.unlink(cr, uid, alias_ids, context=context)
517 def action_print_survey(self, cr, uid, ids, context=None):
521 record = self.browse(cr, uid, ids, context=context)[0]
523 datas['ids'] = [record.survey_id.id]
524 datas['model'] = 'survey.print'
525 context.update({'response_id': [0], 'response_no': 0,})
527 'type': 'ir.actions.report.xml',
528 'report_name': 'survey.form',
534 class applicant_category(osv.osv):
535 """ Category of applicant """
536 _name = "hr.applicant_category"
537 _description = "Category of applicant"
539 'name': fields.char('Name', size=64, required=True, translate=True),
542 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: