66b3bac67c23fc3647a8a4f3ffe134f6b0ecd6df
[odoo/odoo.git] / addons / hr_recruitment / hr_recruitment.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import time
23 from datetime import datetime, timedelta
24
25 from osv import fields, osv
26 from crm import crm
27 import tools
28 import collections
29 import binascii
30 import tools
31 from tools.translate import _
32 from crm import wizard
33
34 wizard.mail_compose_message.SUPPORTED_MODELS.append('hr.applicant')
35
36 AVAILABLE_STATES = [
37     ('draft', 'New'),
38     ('cancel', 'Refused'),
39     ('open', 'In Progress'),
40     ('pending', 'Pending'),
41     ('done', 'Hired')
42 ]
43
44 AVAILABLE_PRIORITIES = [
45     ('', ''),
46     ('5', 'Not Good'),
47     ('4', 'On Average'),
48     ('3', 'Good'),
49     ('2', 'Very Good'),
50     ('1', 'Excellent')
51 ]
52
53 class hr_recruitment_source(osv.osv):
54     """ Sources of HR Recruitment """
55     _name = "hr.recruitment.source"
56     _description = "Source of Applicants"
57     _columns = {
58         'name': fields.char('Source Name', size=64, required=True, translate=True),
59     }
60 hr_recruitment_source()
61
62
63 class hr_recruitment_stage(osv.osv):
64     """ Stage of HR Recruitment """
65     _name = "hr.recruitment.stage"
66     _description = "Stage of Recruitment"
67     _order = 'sequence'
68     _columns = {
69         'name': fields.char('Name', size=64, required=True, translate=True),
70         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of stages."),
71         '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 tempy this field."),
72         'state': fields.selection(AVAILABLE_STATES, 'State', required=True, help="The related state for the stage. The state of your document will automatically change regarding the selected stage. Example, a stage is related to the state 'Close', when your document reach this stage, it will be automatically closed."),
73         'requirements': fields.text('Requirements')
74     }
75     _defaults = {
76         'sequence': 1,
77         'state': 'draft',
78     }
79 hr_recruitment_stage()
80
81 class hr_recruitment_degree(osv.osv):
82     """ Degree of HR Recruitment """
83     _name = "hr.recruitment.degree"
84     _description = "Degree of Recruitment"
85     _columns = {
86         'name': fields.char('Name', size=64, required=True, translate=True),
87         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of degrees."),
88     }
89     _defaults = {
90         'sequence': 1,
91     }
92     _sql_constraints = [
93         ('name_uniq', 'unique (name)', 'The name of the Degree of Recruitment must be unique!')
94     ]
95 hr_recruitment_degree()
96
97 class hr_applicant(crm.crm_case, osv.osv):
98     _name = "hr.applicant"
99     _description = "Applicant"
100     _order = "id desc"
101     _inherit = ['ir.needaction_mixin', 'mail.thread']
102
103     def _compute_day(self, cr, uid, ids, fields, args, context=None):
104         """
105         @param cr: the current row, from the database cursor,
106         @param uid: the current user’s ID for security checks,
107         @param ids: List of Openday’s IDs
108         @return: difference between current date and log date
109         @param context: A standard dictionary for contextual values
110         """
111         res = {}
112         for issue in self.browse(cr, uid, ids, context=context):
113             for field in fields:
114                 res[issue.id] = {}
115                 duration = 0
116                 ans = False
117                 hours = 0
118
119                 if field in ['day_open']:
120                     if issue.date_open:
121                         date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
122                         date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
123                         ans = date_open - date_create
124                         date_until = issue.date_open
125
126                 elif field in ['day_close']:
127                     if issue.date_closed:
128                         date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
129                         date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
130                         date_until = issue.date_closed
131                         ans = date_close - date_create
132                 if ans:
133                     duration = float(ans.days)
134                     res[issue.id][field] = abs(float(duration))
135         return res
136
137     _columns = {
138         'name': fields.char('Name', size=128, required=True),
139         'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
140         'active': fields.boolean('Active', help="If the active field is set to false, it will allow you to hide the case without removing it."),
141         'description': fields.text('Description'),
142         'email_from': fields.char('Email', size=128, help="These people will receive email."),
143         '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"),
144         'probability': fields.float('Probability'),
145         'partner_id': fields.many2one('res.partner', 'Partner'),
146         'create_date': fields.datetime('Creation Date', readonly=True, select=True),
147         'write_date': fields.datetime('Update Date', readonly=True),
148         'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage'),
149         'state': fields.related('stage_id', 'state', type="selection", store=True,
150                 selection=hr_recruitment.AVAILABLE_STATES, string="State", readonly=True,
151                 help='The state is set to \'Draft\', when a case is created.\
152                       If the case is in progress the state is set to \'Open\'.\
153                       When the case is over, the state is set to \'Done\'.\
154                       If the case needs to be reviewed then the state is \
155                       set to \'Pending\'.'),
156         'company_id': fields.many2one('res.company', 'Company'),
157         'user_id': fields.many2one('res.users', 'Responsible'),
158         # Applicant Columns
159         'date_closed': fields.datetime('Closed', readonly=True, select=True),
160         'date_open': fields.datetime('Opened', readonly=True, select=True),
161         'date': fields.datetime('Date'),
162         'date_action': fields.date('Next Action Date'),
163         'title_action': fields.char('Next Action', size=64),
164         'priority': fields.selection(AVAILABLE_PRIORITIES, 'Appreciation'),
165         'job_id': fields.many2one('hr.job', 'Applied Job'),
166         'salary_proposed_extra': fields.char('Proposed Salary Extra', size=100, help="Salary Proposed by the Organisation, extra advantages"),
167         'salary_expected_extra': fields.char('Expected Salary Extra', size=100, help="Salary Expected by Applicant, extra advantages"),
168         'salary_proposed': fields.float('Proposed Salary', help="Salary Proposed by the Organisation"),
169         'salary_expected': fields.float('Expected Salary', help="Salary Expected by Applicant"),
170         'availability': fields.integer('Availability (Days)'),
171         'partner_name': fields.char("Applicant's Name", size=64),
172         'partner_phone': fields.char('Phone', size=32),
173         'partner_mobile': fields.char('Mobile', size=32),
174         'type_id': fields.many2one('hr.recruitment.degree', 'Degree'),
175         'department_id': fields.many2one('hr.department', 'Department'),
176         'survey': fields.related('job_id', 'survey_id', type='many2one', relation='survey', string='Survey'),
177         'response': fields.integer("Response"),
178         'reference': fields.char('Refered By', size=128),
179         'source_id': fields.many2one('hr.recruitment.source', 'Source'),
180         'day_open': fields.function(_compute_day, string='Days to Open', \
181                                 multi='day_open', type="float", store=True),
182         'day_close': fields.function(_compute_day, string='Days to Close', \
183                                 multi='day_close', type="float", store=True),
184         'color': fields.integer('Color Index'),
185         'emp_id': fields.many2one('hr.employee', 'employee'),
186         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
187     }
188
189     _defaults = {
190         'active': lambda *a: 1,
191         'user_id':  lambda self, cr, uid, context: uid,
192         'email_from': crm.crm_case. _get_default_email,
193         'state': 'draft',
194         'priority': lambda *a: '',
195         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
196         'color': 0,
197     }
198
199     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
200         access_rights_uid = access_rights_uid or uid
201         stage_obj = self.pool.get('hr.recruitment.stage')
202         order = stage_obj._order
203         if read_group_order == 'stage_id desc':
204             # lame hack to allow reverting search, should just work in the trivial case
205             order = "%s desc" % order
206         stage_ids = stage_obj._search(cr, uid, ['|',('id','in',ids),('department_id','=',False)], order=order,
207                                       access_rights_uid=access_rights_uid, context=context)
208         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
209         # restore order of the search
210         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
211         return result
212
213     _group_by_full = {
214         'stage_id': _read_group_stage_ids
215     }
216
217     def onchange_job(self,cr, uid, ids, job, context=None):
218         result = {}
219
220         if job:
221             job_obj = self.pool.get('hr.job')
222             result['department_id'] = job_obj.browse(cr, uid, job, context=context).department_id.id
223             return {'value': result}
224         return {'value': {'department_id': False}}
225
226     def onchange_department_id(self, cr, uid, ids, department_id=False, context=None):
227         if not department_id:
228             return {'value': {'stage_id': False}}
229         obj_recru_stage = self.pool.get('hr.recruitment.stage')
230         stage_ids = obj_recru_stage.search(cr, uid, ['|',('department_id','=',department_id),('department_id','=',False)], context=context)
231         stage_id = stage_ids and stage_ids[0] or False
232         return {'value': {'stage_id': stage_id}}
233
234     def stage_previous(self, cr, uid, ids, context=None):
235         """This function computes previous stage for case from its current stage
236              using available stage for that case type
237         @param self: The object pointer
238         @param cr: the current row, from the database cursor,
239         @param uid: the current user’s ID for security checks,
240         @param ids: List of case IDs
241         @param context: A standard dictionary for contextual values"""
242         stage_obj = self.pool.get('hr.recruitment.stage')
243         for case in self.browse(cr, uid, ids, context=context):
244             department = (case.department_id.id or False)
245             st = case.stage_id.id  or False
246             stage_ids = stage_obj.search(cr, uid, ['|',('department_id','=',department),('department_id','=',False)], context=context)
247             if st and stage_ids.index(st):
248                 self.write(cr, uid, [case.id], {'stage_id': stage_ids[stage_ids.index(st)-1]}, context=context)
249             else:
250                 self.write(cr, uid, [case.id], {'stage_id': False}, context=context)
251         return True
252
253     def stage_next(self, cr, uid, ids, context=None):
254         """This function computes next stage for case from its current stage
255              using available stage for that case type
256         @param self: The object pointer
257         @param cr: the current row, from the database cursor,
258         @param uid: the current user’s ID for security checks,
259         @param ids: List of case IDs
260         @param context: A standard dictionary for contextual values"""
261         stage_obj = self.pool.get('hr.recruitment.stage')
262         for case in self.browse(cr, uid, ids, context=context):
263             department = (case.department_id.id or False)
264             st = case.stage_id.id  or False
265             stage_ids = stage_obj.search(cr, uid, ['|',('department_id','=',department),('department_id','=',False)], context=context)
266             val = False
267             if st and len(stage_ids) != stage_ids.index(st)+1:
268                 val = stage_ids[stage_ids.index(st)+1]
269             elif (not st) and stage_ids:
270                 val = stage_ids[0]
271             else:
272                 val = False
273             self.write(cr, uid, [case.id], {'stage_id': val}, context=context)
274         return True
275
276     def action_makeMeeting(self, cr, uid, ids, context=None):
277         """
278         This opens Meeting's calendar view to schedule meeting on current Opportunity
279         @param self: The object pointer
280         @param cr: the current row, from the database cursor,
281         @param uid: the current user’s ID for security checks,
282         @param ids: List of Opportunity to Meeting IDs
283         @param context: A standard dictionary for contextual values
284
285         @return: Dictionary value for created Meeting view
286         """
287         data_obj = self.pool.get('ir.model.data')
288         if context is None:
289             context = {}
290         value = {}
291         for opp in self.browse(cr, uid, ids, context=context):
292             # Get meeting views
293             result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
294             res = data_obj.read(cr, uid, result, ['res_id'], context=context)
295             id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
296             id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
297             id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
298             if id1:
299                 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
300             if id2:
301                 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
302             if id3:
303                 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
304
305             context = {
306                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
307                 'default_email_from': opp.email_from,
308                 'default_state': 'open',
309                 'default_name': opp.name
310             }
311             value = {
312                 'name': ('Meetings'),
313                 'domain': "[('user_id','=',%s)]" % (uid),
314                 'context': context,
315                 'view_type': 'form',
316                 'view_mode': 'calendar,form,tree',
317                 'res_model': 'crm.meeting',
318                 'view_id': False,
319                 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')],
320                 'type': 'ir.actions.act_window',
321                 'search_view_id': res['res_id'],
322                 'nodestroy': True
323             }
324         return value
325
326     def action_print_survey(self, cr, uid, ids, context=None):
327         """
328         If response is available then print this response otherwise print survey form(print template of the survey).
329
330         @param self: The object pointer
331         @param cr: the current row, from the database cursor,
332         @param uid: the current user’s ID for security checks,
333         @param ids: List of Survey IDs
334         @param context: A standard dictionary for contextual values
335         @return: Dictionary value for print survey form.
336         """
337         if context is None:
338             context = {}
339         record = self.browse(cr, uid, ids, context=context)
340         record = record and record[0]
341         context.update({'survey_id': record.survey.id, 'response_id': [record.response], 'response_no': 0, })
342         value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context)
343         return value
344
345     def message_new(self, cr, uid, msg, custom_values=None, context=None):
346         """Automatically called when new email message arrives"""
347         res_id = super(hr_applicant,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
348         subject = msg.get('subject') or _("No Subject")
349         body = msg.get('body_text')
350         msg_from = msg.get('from')
351         priority = msg.get('priority')
352         vals = {
353             'name': subject,
354             'email_from': msg_from,
355             'email_cc': msg.get('cc'),
356             'description': body,
357             'user_id': False,
358         }
359         if priority:
360             vals['priority'] = priority
361         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
362         self.write(cr, uid, [res_id], vals, context)
363         return res_id
364
365     def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
366         if isinstance(ids, (str, int, long)):
367             ids = [ids]
368         if vals is None:
369             vals = {}
370         msg_from = msg['from']
371         vals.update({
372             'description': msg['body_text']
373         })
374         if msg.get('priority', False):
375             vals['priority'] = msg.get('priority')
376
377         maps = {
378             'cost':'planned_cost',
379             'revenue': 'planned_revenue',
380             'probability':'probability'
381         }
382         vls = { }
383         for line in msg['body_text'].split('\n'):
384             line = line.strip()
385             res = tools.misc.command_re.match(line)
386             if res and maps.get(res.group(1).lower(), False):
387                 key = maps.get(res.group(1).lower())
388                 vls[key] = res.group(2).lower()
389
390         vals.update(vls)
391         res = self.write(cr, uid, ids, vals, context=context)
392         self.message_append_dict(cr, uid, ids, msg, context=context)
393         return res
394
395     def create(self, cr, uid, vals, context=None):
396         obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
397         self.create_send_note(cr, uid, [obj_id], context=context)
398         return obj_id
399
400     def case_open(self, cr, uid, ids, context=None):
401         """
402             open Request of the applicant for the hr_recruitment
403         """
404         res = super(hr_applicant, self).case_open(cr, uid, ids, context)
405         date = self.read(cr, uid, ids, ['date_open'])[0]
406         if not date['date_open']:
407             self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),})
408         return res
409
410     def case_close(self, cr, uid, ids, context=None):
411         res = super(hr_applicant, self).case_close(cr, uid, ids, context)
412         return res
413
414     def case_close_with_emp(self, cr, uid, ids, context=None):
415         if context is None:
416             context = {}
417         hr_employee = self.pool.get('hr.employee')
418         model_data = self.pool.get('ir.model.data')
419         act_window = self.pool.get('ir.actions.act_window')
420         emp_id = False
421         for applicant in self.browse(cr, uid, ids, context=context):
422             address_id = False
423             if applicant.partner_id:
424                 address_id = applicant.partner_id.address_get(['contact'])['contact']
425             if applicant.job_id:
426                 applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
427                 emp_id = hr_employee.create(cr,uid,{'name': applicant.partner_name or applicant.name,
428                                                      'job_id': applicant.job_id.id,
429                                                      'address_home_id': address_id,
430                                                      'department_id': applicant.department_id.id
431                                                      })
432                 self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
433                 self.case_close(cr, uid, [applicant.id], context)
434             else:
435                 raise osv.except_osv(_('Warning!'),_('You must define Applied Job for this applicant.'))
436
437         action_model, action_id = model_data.get_object_reference(cr, uid, 'hr', 'open_view_employee_list')
438         dict_act_window = act_window.read(cr, uid, action_id, [])
439         if emp_id:
440             dict_act_window['res_id'] = emp_id
441         dict_act_window['view_mode'] = 'form,tree'
442         return dict_act_window
443
444     def case_cancel(self, cr, uid, ids, context=None):
445         """Overrides cancel for crm_case for setting probability
446         """
447         res = super(hr_applicant, self).case_cancel(cr, uid, ids, context)
448         self.write(cr, uid, ids, {'probability' : 0.0})
449         return res
450
451     def case_pending(self, cr, uid, ids, context=None):
452         """Marks case as pending"""
453         res = super(hr_applicant, self).case_pending(cr, uid, ids, context)
454         self.write(cr, uid, ids, {'probability' : 0.0})
455         return res
456
457     def case_reset(self, cr, uid, ids, context=None):
458         """Resets case as draft
459         """
460         res = super(hr_applicant, self).case_reset(cr, uid, ids, context)
461         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
462         return res
463
464     def set_priority(self, cr, uid, ids, priority, *args):
465         """Set applicant priority
466         """
467         return self.write(cr, uid, ids, {'priority' : priority})
468
469     def set_high_priority(self, cr, uid, ids, *args):
470         """Set applicant priority to high
471         """
472         return self.set_priority(cr, uid, ids, '1')
473
474     def set_normal_priority(self, cr, uid, ids, *args):
475         """Set applicant priority to normal
476         """
477         return self.set_priority(cr, uid, ids, '3')
478
479     def write(self, cr, uid, ids, vals, context=None):
480         if 'stage_id' in vals and vals['stage_id']:
481             stage = self.pool.get('hr.recruitment.stage').browse(cr, uid, vals['stage_id'], context=context)
482             self.message_append_note(cr, uid, ids, body=_("Stage changed to <b>%s</b>.") % stage.name, context=context)
483         return super(hr_applicant,self).write(cr, uid, ids, vals, context=context)
484
485     # -------------------------------------------------------
486     # OpenChatter methods and notifications
487     # -------------------------------------------------------
488     
489     def message_get_subscribers(self, cr, uid, ids, context=None):
490         sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
491         for obj in self.browse(cr, uid, ids, context=context):
492             if obj.user_id:
493                 sub_ids.append(obj.user_id.id)
494         return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
495
496     def get_needaction_user_ids(self, cr, uid, ids, context=None):
497         result = dict.fromkeys(ids, [])
498         for obj in self.browse(cr, uid, ids, context=context):
499             if obj.state == 'draft' and obj.user_id:
500                 result[obj.id] = [obj.user_id.id]
501         return result
502     
503     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
504                 return 'Applicant'
505
506     def case_open_send_note(self, cr, uid, ids, context=None):
507         message = _("Applicant has been set <b>in progress</b>.")
508         return self.message_append_note(cr, uid, ids, body=message, context=context)
509
510     def case_close_send_note(self, cr, uid, ids, context=None):
511         if context is None:
512             context = {}
513         for applicant in self.browse(cr, uid, ids, context=context):
514             if applicant.emp_id:
515                 message = _("Applicant has been <b>hired</b> and created as an employee.")
516                 self.message_append_note(cr, uid, [applicant.id], body=message, context=context)
517             else:
518                 message = _("Applicant has been <b>hired</b>.")
519                 self.message_append_note(cr, uid, [applicant.id], body=message, context=context)
520         return True
521
522     def case_cancel_send_note(self, cr, uid, ids, context=None):
523         msg = 'Applicant <b>refused</b>.'
524         return self.message_append_note(cr, uid, ids, body=msg, context=context)
525
526     def case_reset_send_note(self,  cr, uid, ids, context=None):
527         message =_("Applicant has been set as <b>new</b>.")
528         return self.message_append_note(cr, uid, ids, body=message, context=context)
529
530     def create_send_note(self, cr, uid, ids, context=None):
531         message = _("Applicant has been <b>created</b>.")
532         return self.message_append_note(cr, uid, ids, body=message, context=context)
533
534 hr_applicant()
535
536 class hr_job(osv.osv):
537     _inherit = "hr.job"
538     _name = "hr.job"
539     _columns = {
540         '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"),
541     }
542 hr_job()
543
544 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: