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