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 ##############################################################################
25 from base_status.base_stage import base_stage
26 from datetime import datetime
27 from osv import fields, osv
28 from tools.translate import _
32 ('cancel', 'Refused'),
33 ('open', 'In Progress'),
34 ('pending', 'Pending'),
38 AVAILABLE_PRIORITIES = [
47 class hr_recruitment_source(osv.osv):
48 """ Sources of HR Recruitment """
49 _name = "hr.recruitment.source"
50 _description = "Source of Applicants"
52 'name': fields.char('Source Name', size=64, required=True, translate=True),
55 class hr_recruitment_stage(osv.osv):
56 """ Stage of HR Recruitment """
57 _name = "hr.recruitment.stage"
58 _description = "Stage of Recruitment"
61 'name': fields.char('Name', size=64, required=True, translate=True),
62 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of stages."),
63 '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."),
64 'state': fields.selection(AVAILABLE_STATES, 'State', required=True, help="The related state for the stage. The state of your document will automatically change according to the selected stage. Example, a stage is related to the state 'Close', when your document reach this stage, it will be automatically closed."),
65 '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."),
66 'requirements': fields.text('Requirements'),
74 class hr_recruitment_degree(osv.osv):
75 """ Degree of HR Recruitment """
76 _name = "hr.recruitment.degree"
77 _description = "Degree of Recruitment"
79 'name': fields.char('Name', size=64, required=True, translate=True),
80 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of degrees."),
86 ('name_uniq', 'unique (name)', 'The name of the Degree of Recruitment must be unique!')
89 class hr_applicant(base_stage, osv.Model):
90 _name = "hr.applicant"
91 _description = "Applicant"
93 _inherit = ['mail.thread', 'ir.needaction_mixin']
95 def _get_default_department_id(self, cr, uid, context=None):
96 """ Gives default department by checking if present in the context """
97 return (self._resolve_department_id_from_context(cr, uid, context=context) or False)
99 def _get_default_stage_id(self, cr, uid, context=None):
100 """ Gives default stage_id """
101 department_id = self._get_default_department_id(cr, uid, context=context)
102 return self.stage_find(cr, uid, [], department_id, [('state', '=', 'draft')], context=context)
104 def _resolve_department_id_from_context(self, cr, uid, context=None):
105 """ Returns ID of department based on the value of 'default_department_id'
106 context key, or None if it cannot be resolved to a single
111 if type(context.get('default_department_id')) in (int, long):
112 return context.get('default_department_id')
113 if isinstance(context.get('default_department_id'), basestring):
114 department_name = context['default_department_id']
115 department_ids = self.pool.get('hr.department').name_search(cr, uid, name=department_name, context=context)
116 if len(department_ids) == 1:
117 return int(department_ids[0][0])
120 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
121 access_rights_uid = access_rights_uid or uid
122 stage_obj = self.pool.get('hr.recruitment.stage')
123 order = stage_obj._order
124 # lame hack to allow reverting search, should just work in the trivial case
125 if read_group_order == 'stage_id desc':
126 order = "%s desc" % order
127 # retrieve section_id from the context and write the domain
128 # - ('id', 'in', 'ids'): add columns that should be present
129 # - OR ('department_id', '=', False), ('fold', '=', False): add default columns that are not folded
130 # - OR ('department_id', 'in', department_id), ('fold', '=', False) if department_id: add department columns that are not folded
131 department_id = self._resolve_department_id_from_context(cr, uid, context=context)
134 search_domain += ['|', '&', ('department_id', '=', department_id), ('fold', '=', False)]
135 search_domain += ['|', ('id', 'in', ids), '&', ('department_id', '=', False), ('fold', '=', False)]
136 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
137 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
138 # restore order of the search
139 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
142 def _compute_day(self, cr, uid, ids, fields, args, context=None):
144 @param cr: the current row, from the database cursor,
145 @param uid: the current user’s ID for security checks,
146 @param ids: List of Openday’s IDs
147 @return: difference between current date and log date
148 @param context: A standard dictionary for contextual values
151 for issue in self.browse(cr, uid, ids, context=context):
158 if field in ['day_open']:
160 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
161 date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
162 ans = date_open - date_create
164 elif field in ['day_close']:
165 if issue.date_closed:
166 date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
167 date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
168 ans = date_close - date_create
170 duration = float(ans.days)
171 res[issue.id][field] = abs(float(duration))
175 'name': fields.char('Subject', size=128, required=True),
176 'active': fields.boolean('Active', help="If the active field is set to false, it will allow you to hide the case without removing it."),
177 'description': fields.text('Description'),
178 'email_from': fields.char('Email', size=128, help="These people will receive email."),
179 '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"),
180 'probability': fields.float('Probability'),
181 'partner_id': fields.many2one('res.partner', 'Contact'),
182 'create_date': fields.datetime('Creation Date', readonly=True, select=True),
183 'write_date': fields.datetime('Update Date', readonly=True),
184 'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage',
185 domain="['|', ('department_id', '=', department_id), ('department_id', '=', False)]"),
186 'state': fields.related('stage_id', 'state', type="selection", store=True,
187 selection=AVAILABLE_STATES, string="State", readonly=True,
188 help='The state is set to \'Draft\', when a case is created.\
189 If the case is in progress the state is set to \'Open\'.\
190 When the case is over, the state is set to \'Done\'.\
191 If the case needs to be reviewed then the state is \
192 set to \'Pending\'.'),
193 'categ_ids': fields.many2many('hr.applicant_category', string='Categories'),
194 'company_id': fields.many2one('res.company', 'Company'),
195 'user_id': fields.many2one('res.users', 'Responsible'),
197 'date_closed': fields.datetime('Closed', readonly=True, select=True),
198 'date_open': fields.datetime('Opened', readonly=True, select=True),
199 'date': fields.datetime('Date'),
200 'date_action': fields.date('Next Action Date'),
201 'title_action': fields.char('Next Action', size=64),
202 'priority': fields.selection(AVAILABLE_PRIORITIES, 'Appreciation'),
203 'job_id': fields.many2one('hr.job', 'Applied Job'),
204 'salary_proposed_extra': fields.char('Proposed Salary Extra', size=100, help="Salary Proposed by the Organisation, extra advantages"),
205 'salary_expected_extra': fields.char('Expected Salary Extra', size=100, help="Salary Expected by Applicant, extra advantages"),
206 'salary_proposed': fields.float('Proposed Salary', help="Salary Proposed by the Organisation"),
207 'salary_expected': fields.float('Expected Salary', help="Salary Expected by Applicant"),
208 'availability': fields.integer('Availability'),
209 'partner_name': fields.char("Applicant's Name", size=64),
210 'partner_phone': fields.char('Phone', size=32),
211 'partner_mobile': fields.char('Mobile', size=32),
212 'type_id': fields.many2one('hr.recruitment.degree', 'Degree'),
213 'department_id': fields.many2one('hr.department', 'Department'),
214 'survey': fields.related('job_id', 'survey_id', type='many2one', relation='survey', string='Survey'),
215 'response': fields.integer("Response"),
216 'reference': fields.char('Referred By', size=128),
217 'source_id': fields.many2one('hr.recruitment.source', 'Source'),
218 'day_open': fields.function(_compute_day, string='Days to Open', \
219 multi='day_open', type="float", store=True),
220 'day_close': fields.function(_compute_day, string='Days to Close', \
221 multi='day_close', type="float", store=True),
222 'color': fields.integer('Color Index'),
223 'emp_id': fields.many2one('hr.employee', 'employee'),
224 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
228 'active': lambda *a: 1,
229 'user_id': lambda s, cr, uid, c: uid,
230 'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
231 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
232 'department_id': lambda s, cr, uid, c: s._get_default_department_id(cr, uid, c),
233 'priority': lambda *a: '',
234 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'hr.applicant', context=c),
239 'stage_id': _read_group_stage_ids
242 def onchange_job(self,cr, uid, ids, job, context=None):
246 job_obj = self.pool.get('hr.job')
247 result['department_id'] = job_obj.browse(cr, uid, job, context=context).department_id.id
248 return {'value': result}
249 return {'value': {'department_id': False}}
251 def onchange_department_id(self, cr, uid, ids, department_id=False, context=None):
252 obj_recru_stage = self.pool.get('hr.recruitment.stage')
253 stage_ids = obj_recru_stage.search(cr, uid, ['|',('department_id','=',department_id),('department_id','=',False)], context=context)
254 stage_id = stage_ids and stage_ids[0] or False
255 return {'value': {'stage_id': stage_id}}
257 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
258 """ Override of the base.stage method
259 Parameter of the stage search taken from the lead:
260 - department_id: if set, stages must belong to this section or
263 if isinstance(cases, (int, long)):
264 cases = self.browse(cr, uid, cases, context=context)
265 # collect all section_ids
268 department_ids.append(section_id)
270 if case.department_id:
271 department_ids.append(case.department_id.id)
272 # OR all section_ids and OR with case_default
275 search_domain += ['|', ('department_id', 'in', department_ids)]
276 search_domain.append(('department_id', '=', False))
277 # AND with the domain in parameter
278 search_domain += list(domain)
279 # perform search, return the first found
280 stage_ids = self.pool.get('hr.recruitment.stage').search(cr, uid, search_domain, order=order, context=context)
285 def action_makeMeeting(self, cr, uid, ids, context=None):
286 """ This opens Meeting's calendar view to schedule meeting on current applicant
287 @return: Dictionary value for created Meeting view
289 applicant = self.browse(cr, uid, ids[0], context)
290 category = self.pool.get('ir.model.data').get_object(cr, uid, 'hr_recruitment', 'categ_meet_interview', context)
291 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'base_calendar', 'action_crm_meeting', context)
293 'default_partner_ids': applicant.partner_id and [applicant.partner_id.id] or False,
294 'default_user_id': uid,
295 'default_state': 'open',
296 'default_name': applicant.name,
297 'default_categ_ids': category and [category.id] or False,
301 def action_print_survey(self, cr, uid, ids, context=None):
303 If response is available then print this response otherwise print survey form(print template of the survey).
305 @param self: The object pointer
306 @param cr: the current row, from the database cursor,
307 @param uid: the current user’s ID for security checks,
308 @param ids: List of Survey IDs
309 @param context: A standard dictionary for contextual values
310 @return: Dictionary value for print survey form.
314 record = self.browse(cr, uid, ids, context=context)
315 record = record and record[0]
316 context.update({'survey_id': record.survey.id, 'response_id': [record.response], 'response_no': 0, })
317 value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context)
320 def message_new(self, cr, uid, msg, custom_values=None, context=None):
321 """ Overrides mail_thread message_new that is called by the mailgateway
322 through message_process.
323 This override updates the document according to the email.
325 if custom_values is None: custom_values = {}
326 custom_values.update({
327 'name': msg.get('subject') or _("No Subject"),
328 'description': msg.get('body'),
329 'email_from': msg.get('from'),
330 'email_cc': msg.get('cc'),
333 if msg.get('priority'):
334 custom_values['priority'] = msg.get('priority')
335 return super(hr_applicant,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
337 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
338 """ Override mail_thread message_update that is called by the mailgateway
339 through message_process.
340 This method updates the document according to the email.
342 if isinstance(ids, (str, int, long)):
344 if update_vals is None: vals = {}
347 'description': msg.get('body'),
348 'email_from': msg.get('from'),
349 'email_cc': msg.get('cc'),
351 if msg.get('priority'):
352 update_vals['priority'] = msg.get('priority')
355 'cost': 'planned_cost',
356 'revenue': 'planned_revenue',
357 'probability': 'probability',
359 for line in msg.get('body', '').split('\n'):
361 res = tools.misc.command_re.match(line)
362 if res and maps.get(res.group(1).lower(), False):
363 key = maps.get(res.group(1).lower())
364 update_vals[key] = res.group(2).lower()
366 return super(hr_applicant, self).message_update(cr, uids, ids, update_vals=update_vals, context=context)
368 def create(self, cr, uid, vals, context=None):
369 obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
370 self.create_send_note(cr, uid, [obj_id], context=context)
373 def case_open(self, cr, uid, ids, context=None):
375 open Request of the applicant for the hr_recruitment
377 res = super(hr_applicant, self).case_open(cr, uid, ids, context)
378 date = self.read(cr, uid, ids, ['date_open'])[0]
379 if not date['date_open']:
380 self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),})
383 def case_close(self, cr, uid, ids, context=None):
384 res = super(hr_applicant, self).case_close(cr, uid, ids, context)
387 def case_close_with_emp(self, cr, uid, ids, context=None):
390 hr_employee = self.pool.get('hr.employee')
391 model_data = self.pool.get('ir.model.data')
392 act_window = self.pool.get('ir.actions.act_window')
394 for applicant in self.browse(cr, uid, ids, context=context):
396 if applicant.partner_id:
397 address_id = applicant.partner_id.address_get(['contact'])['contact']
399 applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
400 emp_id = hr_employee.create(cr,uid,{'name': applicant.partner_name or applicant.name,
401 'job_id': applicant.job_id.id,
402 'address_home_id': address_id,
403 'department_id': applicant.department_id.id
405 self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
406 self.case_close(cr, uid, [applicant.id], context)
408 raise osv.except_osv(_('Warning!'),_('You must define Applied Job for this applicant.'))
410 action_model, action_id = model_data.get_object_reference(cr, uid, 'hr', 'open_view_employee_list')
411 dict_act_window = act_window.read(cr, uid, action_id, [])
413 dict_act_window['res_id'] = emp_id
414 dict_act_window['view_mode'] = 'form,tree'
415 return dict_act_window
417 def case_cancel(self, cr, uid, ids, context=None):
418 """Overrides cancel for crm_case for setting probability
420 res = super(hr_applicant, self).case_cancel(cr, uid, ids, context)
421 self.write(cr, uid, ids, {'probability' : 0.0})
424 def case_pending(self, cr, uid, ids, context=None):
425 """Marks case as pending"""
426 res = super(hr_applicant, self).case_pending(cr, uid, ids, context)
427 self.write(cr, uid, ids, {'probability' : 0.0})
430 def case_reset(self, cr, uid, ids, context=None):
431 """Resets case as draft
433 res = super(hr_applicant, self).case_reset(cr, uid, ids, context)
434 self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
437 def set_priority(self, cr, uid, ids, priority, *args):
438 """Set applicant priority
440 return self.write(cr, uid, ids, {'priority' : priority})
442 def set_high_priority(self, cr, uid, ids, *args):
443 """Set applicant priority to high
445 return self.set_priority(cr, uid, ids, '1')
447 def set_normal_priority(self, cr, uid, ids, *args):
448 """Set applicant priority to normal
450 return self.set_priority(cr, uid, ids, '3')
452 # -------------------------------------------------------
453 # OpenChatter methods and notifications
454 # -------------------------------------------------------
456 def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
457 """ Override of the (void) default notification method. """
458 if not stage_id: return True
459 stage_name = self.pool.get('hr.recruitment.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
460 return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
462 def case_get_note_msg_prefix(self, cr, uid, id, context=None):
465 def case_open_send_note(self, cr, uid, ids, context=None):
466 message = _("Applicant has been set <b>in progress</b>.")
467 return self.message_post(cr, uid, ids, body=message, context=context)
469 def case_close_send_note(self, cr, uid, ids, context=None):
472 for applicant in self.browse(cr, uid, ids, context=context):
474 message = _("Applicant has been <b>hired</b> and created as an employee.")
475 self.message_post(cr, uid, [applicant.id], body=message, context=context)
477 message = _("Applicant has been <b>hired</b>.")
478 self.message_post(cr, uid, [applicant.id], body=message, context=context)
481 def case_cancel_send_note(self, cr, uid, ids, context=None):
482 msg = 'Applicant <b>refused</b>.'
483 return self.message_post(cr, uid, ids, body=msg, context=context)
485 def case_reset_send_note(self, cr, uid, ids, context=None):
486 message =_("Applicant has been set as <b>new</b>.")
487 return self.message_post(cr, uid, ids, body=message, context=context)
489 def create_send_note(self, cr, uid, ids, context=None):
490 message = _("Applicant has been <b>created</b>.")
491 return self.message_post(cr, uid, ids, body=message, context=context)
494 class hr_job(osv.osv):
497 _inherits = {'mail.alias': 'alias_id'}
499 '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"),
500 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", required=True,
501 help="Email alias for this job position. New emails will automatically "
502 "create new applicants for this job position."),
506 'alias_domain': False, # always hide alias during creation
509 def _auto_init(self, cr, context=None):
510 """Installation hook to create aliases for all jobs and avoid constraint errors."""
511 self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(hr_job,self)._auto_init,
512 self._columns['alias_id'], 'name', alias_prefix='job+', alias_defaults={'job_id': 'id'}, context=context)
514 def create(self, cr, uid, vals, context=None):
515 mail_alias = self.pool.get('mail.alias')
516 if not vals.get('alias_id'):
517 vals.pop('alias_name', None) # prevent errors during copy()
518 alias_id = mail_alias.create_unique_alias(cr, uid,
519 # Using '+' allows using subaddressing for those who don't
520 # have a catchall domain setup.
521 {'alias_name': 'jobs+'+vals['name']},
522 model_name="hr.applicant",
524 vals['alias_id'] = alias_id
525 res = super(hr_job, self).create(cr, uid, vals, context)
526 mail_alias.write(cr, uid, [vals['alias_id']], {"alias_defaults": {'job_id': res}}, context)
529 def unlink(self, cr, uid, ids, context=None):
530 # Cascade-delete mail aliases as well, as they should not exist without the job position.
531 mail_alias = self.pool.get('mail.alias')
532 alias_ids = [job.alias_id.id for job in self.browse(cr, uid, ids, context=context) if job.alias_id]
533 res = super(hr_job, self).unlink(cr, uid, ids, context=context)
534 mail_alias.unlink(cr, uid, alias_ids, context=context)
537 def action_print_survey(self, cr, uid, ids, context=None):
541 record = self.browse(cr, uid, ids, context=context)[0]
543 datas['ids'] = [record.survey_id.id]
544 datas['model'] = 'survey.print'
545 context.update({'response_id': [0], 'response_no': 0,})
547 'type': 'ir.actions.report.xml',
548 'report_name': 'survey.form',
554 class applicant_category(osv.osv):
555 """ Category of applicant """
556 _name = "hr.applicant_category"
557 _description = "Category of applicant"
559 'name': fields.char('Name', size=64, required=True, translate=True),
562 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: