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 _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
131 access_rights_uid = access_rights_uid or uid
132 stage_obj = self.pool.get('hr.recruitment.stage')
133 order = stage_obj._order
134 # lame hack to allow reverting search, should just work in the trivial case
135 if read_group_order == 'stage_id desc':
136 order = "%s desc" % order
137 # retrieve section_id from the context and write the domain
138 # - ('id', 'in', 'ids'): add columns that should be present
139 # - OR ('department_id', '=', False), ('fold', '=', False): add default columns that are not folded
140 # - OR ('department_id', 'in', department_id), ('fold', '=', False) if department_id: add department columns that are not folded
141 department_id = self._resolve_department_id_from_context(cr, uid, context=context)
144 search_domain += ['|', ('department_id', '=', department_id)]
145 search_domain += ['|', ('id', 'in', ids), ('department_id', '=', False)]
146 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
147 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
148 # restore order of the search
149 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
152 for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
153 fold[stage.id] = stage.fold or False
156 def _compute_day(self, cr, uid, ids, fields, args, context=None):
158 @param cr: the current row, from the database cursor,
159 @param uid: the current user’s ID for security checks,
160 @param ids: List of Openday’s IDs
161 @return: difference between current date and log date
162 @param context: A standard dictionary for contextual values
165 for issue in self.browse(cr, uid, ids, context=context):
172 if field in ['day_open']:
174 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
175 date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
176 ans = date_open - date_create
178 elif field in ['day_close']:
179 if issue.date_closed:
180 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
181 date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
182 ans = date_close - date_create
184 duration = float(ans.days)
185 res[issue.id][field] = abs(float(duration))
189 'name': fields.char('Subject', size=128, required=True),
190 'active': fields.boolean('Active', help="If the active field is set to false, it will allow you to hide the case without removing it."),
191 'description': fields.text('Description'),
192 'email_from': fields.char('Email', size=128, help="These people will receive email."),
193 '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"),
194 'probability': fields.float('Probability'),
195 'partner_id': fields.many2one('res.partner', 'Contact'),
196 'create_date': fields.datetime('Creation Date', readonly=True, select=True),
197 'write_date': fields.datetime('Update Date', readonly=True),
198 'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage', track_visibility='onchange',
199 domain="['|', ('department_id', '=', department_id), ('department_id', '=', False)]"),
200 'state': fields.related('stage_id', 'state', type="selection", store=True,
201 selection=AVAILABLE_STATES, string="Status", readonly=True,
202 help='The status is set to \'Draft\', when a case is created.\
203 If the case is in progress the status is set to \'Open\'.\
204 When the case is over, the status is set to \'Done\'.\
205 If the case needs to be reviewed then the status is \
206 set to \'Pending\'.'),
207 'categ_ids': fields.many2many('hr.applicant_category', string='Tags'),
208 'company_id': fields.many2one('res.company', 'Company'),
209 'user_id': fields.many2one('res.users', 'Responsible', track_visibility='onchange'),
211 'date_closed': fields.datetime('Closed', readonly=True, select=True),
212 'date_open': fields.datetime('Opened', readonly=True, select=True),
213 'date': fields.datetime('Date'),
214 'date_action': fields.date('Next Action Date'),
215 'title_action': fields.char('Next Action', size=64),
216 'priority': fields.selection(AVAILABLE_PRIORITIES, 'Appreciation'),
217 'job_id': fields.many2one('hr.job', 'Applied Job'),
218 'salary_proposed_extra': fields.char('Proposed Salary Extra', size=100, help="Salary Proposed by the Organisation, extra advantages"),
219 'salary_expected_extra': fields.char('Expected Salary Extra', size=100, help="Salary Expected by Applicant, extra advantages"),
220 'salary_proposed': fields.float('Proposed Salary', help="Salary Proposed by the Organisation"),
221 'salary_expected': fields.float('Expected Salary', help="Salary Expected by Applicant"),
222 'availability': fields.integer('Availability'),
223 'partner_name': fields.char("Applicant's Name", size=64),
224 'partner_phone': fields.char('Phone', size=32),
225 'partner_mobile': fields.char('Mobile', size=32),
226 'type_id': fields.many2one('hr.recruitment.degree', 'Degree'),
227 'department_id': fields.many2one('hr.department', 'Department'),
228 'survey': fields.related('job_id', 'survey_id', type='many2one', relation='survey', string='Survey'),
229 'response': fields.integer("Response"),
230 'reference': fields.char('Referred By', size=128),
231 'source_id': fields.many2one('hr.recruitment.source', 'Source'),
232 'day_open': fields.function(_compute_day, string='Days to Open', \
233 multi='day_open', type="float", store=True),
234 'day_close': fields.function(_compute_day, string='Days to Close', \
235 multi='day_close', type="float", store=True),
236 'color': fields.integer('Color Index'),
237 'emp_id': fields.many2one('hr.employee', 'employee'),
238 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
242 'active': lambda *a: 1,
243 'user_id': lambda s, cr, uid, c: uid,
244 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
245 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
246 'department_id': lambda s, cr, uid, c: s._get_default_department_id(cr, uid, c),
247 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'hr.applicant', context=c),
252 'stage_id': _read_group_stage_ids
255 def onchange_job(self, cr, uid, ids, job, context=None):
259 job_obj = self.pool.get('hr.job')
260 result['department_id'] = job_obj.browse(cr, uid, job, context=context).department_id.id
261 return {'value': result}
262 return {'value': {'department_id': False}}
264 def onchange_department_id(self, cr, uid, ids, department_id=False, context=None):
265 obj_recru_stage = self.pool.get('hr.recruitment.stage')
266 stage_ids = obj_recru_stage.search(cr, uid, ['|',('department_id','=',department_id),('department_id','=',False)], context=context)
267 stage_id = stage_ids and stage_ids[0] or False
268 return {'value': {'stage_id': stage_id}}
270 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
271 data = {'partner_phone': False,
272 'partner_mobile': False,
275 addr = self.pool.get('res.partner').browse(cr, uid, partner_id, context)
276 data.update({'partner_phone': addr.phone,
277 'partner_mobile': addr.mobile,
278 'email_from': addr.email})
279 return {'value': data}
281 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
282 """ Override of the base.stage method
283 Parameter of the stage search taken from the lead:
284 - department_id: if set, stages must belong to this section or
287 if isinstance(cases, (int, long)):
288 cases = self.browse(cr, uid, cases, context=context)
289 # collect all section_ids
292 department_ids.append(section_id)
294 if case.department_id:
295 department_ids.append(case.department_id.id)
296 # OR all section_ids and OR with case_default
299 search_domain += ['|', ('department_id', 'in', department_ids)]
300 search_domain.append(('department_id', '=', False))
301 # AND with the domain in parameter
302 search_domain += list(domain)
303 # perform search, return the first found
304 stage_ids = self.pool.get('hr.recruitment.stage').search(cr, uid, search_domain, order=order, context=context)
309 def action_makeMeeting(self, cr, uid, ids, context=None):
310 """ This opens Meeting's calendar view to schedule meeting on current applicant
311 @return: Dictionary value for created Meeting view
313 applicant = self.browse(cr, uid, ids[0], context)
314 category = self.pool.get('ir.model.data').get_object(cr, uid, 'hr_recruitment', 'categ_meet_interview', context)
315 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
317 'default_partner_ids': applicant.partner_id and [applicant.partner_id.id] or False,
318 'default_user_id': uid,
319 'default_name': applicant.name,
320 'default_categ_ids': category and [category.id] or False,
324 def action_print_survey(self, cr, uid, ids, context=None):
326 If response is available then print this response otherwise print survey form(print template of the survey).
328 @param self: The object pointer
329 @param cr: the current row, from the database cursor,
330 @param uid: the current user’s ID for security checks,
331 @param ids: List of Survey IDs
332 @param context: A standard dictionary for contextual values
333 @return: Dictionary value for print survey form.
337 record = self.browse(cr, uid, ids, context=context)
338 record = record and record[0]
339 context.update({'survey_id': record.survey.id, 'response_id': [record.response], 'response_no': 0, })
340 value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context)
343 def message_get_suggested_recipients(self, cr, uid, ids, context=None):
344 recipients = super(hr_applicant, self).message_get_suggested_recipients(cr, uid, ids, context=context)
345 for applicant in self.browse(cr, uid, ids, context=context):
346 if applicant.partner_id:
347 self._message_add_suggested_recipient(cr, uid, recipients, applicant, partner=applicant.partner_id, reason=_('Contact'))
348 elif applicant.email_from:
349 self._message_add_suggested_recipient(cr, uid, recipients, applicant, email=applicant.email_from, reason=_('Contact Email'))
352 def message_new(self, cr, uid, msg, custom_values=None, context=None):
353 """ Overrides mail_thread message_new that is called by the mailgateway
354 through message_process.
355 This override updates the document according to the email.
357 if custom_values is None: custom_values = {}
358 val = msg.get('from').split('<')[0]
359 desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
361 'name': msg.get('subject') or _("No Subject"),
364 'email_from': msg.get('from'),
365 'email_cc': msg.get('cc'),
367 'partner_id': msg.get('author_id', False),
369 if msg.get('priority'):
370 defaults['priority'] = msg.get('priority')
371 defaults.update(custom_values)
372 return super(hr_applicant,self).message_new(cr, uid, msg, custom_values=defaults, context=context)
374 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
375 """ Override mail_thread message_update that is called by the mailgateway
376 through message_process.
377 This method updates the document according to the email.
379 if isinstance(ids, (str, int, long)):
381 if update_vals is None:
385 'email_from': msg.get('from'),
386 'email_cc': msg.get('cc'),
388 if msg.get('priority'):
389 update_vals['priority'] = msg.get('priority')
392 'cost': 'planned_cost',
393 'revenue': 'planned_revenue',
394 'probability': 'probability',
396 for line in msg.get('body', '').split('\n'):
398 res = tools.command_re.match(line)
399 if res and maps.get(res.group(1).lower(), False):
400 key = maps.get(res.group(1).lower())
401 update_vals[key] = res.group(2).lower()
403 return super(hr_applicant, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
405 def create(self, cr, uid, vals, context=None):
406 obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
407 applicant = self.browse(cr, uid, obj_id, context=context)
409 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)
412 def case_open(self, cr, uid, ids, context=None):
414 open Request of the applicant for the hr_recruitment
416 res = super(hr_applicant, self).case_open(cr, uid, ids, context)
417 date = self.read(cr, uid, ids, ['date_open'])[0]
418 if not date['date_open']:
419 self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),})
422 def case_close(self, cr, uid, ids, context=None):
423 res = super(hr_applicant, self).case_close(cr, uid, ids, context)
426 def case_close_with_emp(self, cr, uid, ids, context=None):
429 hr_employee = self.pool.get('hr.employee')
430 model_data = self.pool.get('ir.model.data')
431 act_window = self.pool.get('ir.actions.act_window')
433 for applicant in self.browse(cr, uid, ids, context=context):
434 address_id = contact_name = False
435 if applicant.partner_id:
436 address_id = self.pool.get('res.partner').address_get(cr,uid,[applicant.partner_id.id],['contact'])['contact']
437 contact_name = self.pool.get('res.partner').name_get(cr,uid,[applicant.partner_id.id])[0][1]
438 if applicant.job_id and (applicant.partner_name or contact_name):
439 applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
440 emp_id = hr_employee.create(cr,uid,{'name': applicant.partner_name or contact_name,
441 'job_id': applicant.job_id.id,
442 'address_home_id': address_id,
443 'department_id': applicant.department_id.id
445 self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
446 self.case_close(cr, uid, [applicant.id], context)
448 raise osv.except_osv(_('Warning!'), _('You must define an Applied Job and a Contact Name for this applicant.'))
450 action_model, action_id = model_data.get_object_reference(cr, uid, 'hr', 'open_view_employee_list')
451 dict_act_window = act_window.read(cr, uid, action_id, [])
453 dict_act_window['res_id'] = emp_id
454 dict_act_window['view_mode'] = 'form,tree'
455 return dict_act_window
457 def case_cancel(self, cr, uid, ids, context=None):
458 """Overrides cancel for crm_case for setting probability
460 res = super(hr_applicant, self).case_cancel(cr, uid, ids, context)
461 self.write(cr, uid, ids, {'probability': 0.0})
464 def case_pending(self, cr, uid, ids, context=None):
465 """Marks case as pending"""
466 res = super(hr_applicant, self).case_pending(cr, uid, ids, context)
467 self.write(cr, uid, ids, {'probability': 0.0})
470 def case_reset(self, cr, uid, ids, context=None):
471 """Resets case as draft
473 res = super(hr_applicant, self).case_reset(cr, uid, ids, context)
474 self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
477 def set_priority(self, cr, uid, ids, priority, *args):
478 """Set applicant priority
480 return self.write(cr, uid, ids, {'priority': priority})
482 def set_high_priority(self, cr, uid, ids, *args):
483 """Set applicant priority to high
485 return self.set_priority(cr, uid, ids, '1')
487 def set_normal_priority(self, cr, uid, ids, *args):
488 """Set applicant priority to normal
490 return self.set_priority(cr, uid, ids, '3')
492 def get_empty_list_help(self, cr, uid, help, context=None):
493 context['empty_list_help_model'] = 'hr.job'
494 context['empty_list_help_id'] = context.get('default_job_id', None)
495 context['empty_list_help_document_name'] = _("job applicants")
496 return super(hr_applicant, self).get_empty_list_help(cr, uid, help, context=context)
499 class hr_job(osv.osv):
502 _inherits = {'mail.alias': 'alias_id'}
504 '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"),
505 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", required=True,
506 help="Email alias for this job position. New emails will automatically "
507 "create new applicants for this job position."),
510 def _auto_init(self, cr, context=None):
511 """Installation hook to create aliases for all jobs and avoid constraint errors."""
512 return self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(hr_job, self)._auto_init,
513 'hr.applicant', self._columns['alias_id'], 'name', alias_prefix='job+', alias_defaults={'job_id': 'id'}, context=context)
515 def create(self, cr, uid, vals, context=None):
516 alias_context = dict(context, alias_model_name='hr.applicant', alias_parent_model_name=self._name)
517 job_id = super(hr_job, self).create(cr, uid, vals, context=alias_context)
518 job = self.browse(cr, uid, job_id, context=context)
519 self.pool.get('mail.alias').write(cr, uid, [job.alias_id.id], {'alias_parent_thread_id': job_id, "alias_defaults": {'job_id': job_id}}, context)
522 def unlink(self, cr, uid, ids, context=None):
523 # Cascade-delete mail aliases as well, as they should not exist without the job position.
524 mail_alias = self.pool.get('mail.alias')
525 alias_ids = [job.alias_id.id for job in self.browse(cr, uid, ids, context=context) if job.alias_id]
526 res = super(hr_job, self).unlink(cr, uid, ids, context=context)
527 mail_alias.unlink(cr, uid, alias_ids, context=context)
530 def action_print_survey(self, cr, uid, ids, context=None):
534 record = self.browse(cr, uid, ids, context=context)[0]
536 datas['ids'] = [record.survey_id.id]
537 datas['model'] = 'survey.print'
538 context.update({'response_id': [0], 'response_no': 0})
540 'type': 'ir.actions.report.xml',
541 'report_name': 'survey.form',
548 class applicant_category(osv.osv):
549 """ Category of applicant """
550 _name = "hr.applicant_category"
551 _description = "Category of applicant"
553 'name': fields.char('Name', size=64, required=True, translate=True),
556 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: