[FIX] Fixed to minor changes in messages for in progress and on refused.
[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 = ['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', 'Partner'),
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         'survey': fields.related('job_id', 'survey_id', type='many2one', relation='survey', string='Survey'),
173         'response': fields.integer("Response"),
174         'reference': fields.char('Refered By', size=128),
175         'source_id': fields.many2one('hr.recruitment.source', 'Source'),
176         'day_open': fields.function(_compute_day, string='Days to Open', \
177                                 multi='day_open', type="float", store=True),
178         'day_close': fields.function(_compute_day, string='Days to Close', \
179                                 multi='day_close', type="float", store=True),
180         'color': fields.integer('Color Index'),
181         'emp_id': fields.many2one('hr.employee', 'employee'),
182         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
183     }
184
185     _defaults = {
186         'active': lambda *a: 1,
187         'user_id':  lambda self, cr, uid, context: uid,
188         'email_from': crm.crm_case. _get_default_email,
189         'state': lambda *a: 'draft',
190         'priority': lambda *a: '',
191         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
192         'color': 0,
193     }
194
195     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
196         access_rights_uid = access_rights_uid or uid
197         stage_obj = self.pool.get('hr.recruitment.stage')
198         order = stage_obj._order
199         if read_group_order == 'stage_id desc':
200             # lame hack to allow reverting search, should just work in the trivial case
201             order = "%s desc" % order
202         stage_ids = stage_obj._search(cr, uid, ['|',('id','in',ids),('department_id','=',False)], order=order,
203                                       access_rights_uid=access_rights_uid, context=context)
204         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
205         # restore order of the search
206         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
207         return result
208
209     _group_by_full = {
210         'stage_id': _read_group_stage_ids
211     }
212
213
214     def onchange_job(self,cr, uid, ids, job, context=None):
215         result = {}
216
217         if job:
218             job_obj = self.pool.get('hr.job')
219             result['department_id'] = job_obj.browse(cr, uid, job, context=context).department_id.id
220             return {'value': result}
221         return {'value': {'department_id': False}}
222
223     def onchange_department_id(self, cr, uid, ids, department_id=False, context=None):
224         if not department_id:
225             return {'value': {'stage_id': False}}
226         obj_recru_stage = self.pool.get('hr.recruitment.stage')
227         stage_ids = obj_recru_stage.search(cr, uid, ['|',('department_id','=',department_id),('department_id','=',False)], context=context)
228         stage_id = stage_ids and stage_ids[0] or False
229         return {'value': {'stage_id': stage_id}}
230
231     def stage_previous(self, cr, uid, ids, context=None):
232         """This function computes previous stage for case from its current stage
233              using available stage for that case type
234         @param self: The object pointer
235         @param cr: the current row, from the database cursor,
236         @param uid: the current user’s ID for security checks,
237         @param ids: List of case IDs
238         @param context: A standard dictionary for contextual values"""
239         stage_obj = self.pool.get('hr.recruitment.stage')
240         for case in self.browse(cr, uid, ids, context=context):
241             department = (case.department_id.id or False)
242             st = case.stage_id.id  or False
243             stage_ids = stage_obj.search(cr, uid, ['|',('department_id','=',department),('department_id','=',False)], context=context)
244             if st and stage_ids.index(st):
245                 self.write(cr, uid, [case.id], {'stage_id': stage_ids[stage_ids.index(st)-1]}, context=context)
246             else:
247                 self.write(cr, uid, [case.id], {'stage_id': False}, context=context)
248         return True
249
250     def stage_next(self, cr, uid, ids, context=None):
251         """This function computes next stage for case from its current stage
252              using available stage for that case type
253         @param self: The object pointer
254         @param cr: the current row, from the database cursor,
255         @param uid: the current user’s ID for security checks,
256         @param ids: List of case IDs
257         @param context: A standard dictionary for contextual values"""
258         stage_obj = self.pool.get('hr.recruitment.stage')
259         for case in self.browse(cr, uid, ids, context=context):
260             department = (case.department_id.id or False)
261             st = case.stage_id.id  or False
262             stage_ids = stage_obj.search(cr, uid, ['|',('department_id','=',department),('department_id','=',False)], context=context)
263             val = False
264             if st and len(stage_ids) != stage_ids.index(st)+1:
265                 val = stage_ids[stage_ids.index(st)+1]
266             elif (not st) and stage_ids:
267                 val = stage_ids[0]
268             else:
269                 val = False
270             self.write(cr, uid, [case.id], {'stage_id': val}, context=context)
271         return True
272
273     def action_makeMeeting(self, cr, uid, ids, context=None):
274         """
275         This opens Meeting's calendar view to schedule meeting on current Opportunity
276         @param self: The object pointer
277         @param cr: the current row, from the database cursor,
278         @param uid: the current user’s ID for security checks,
279         @param ids: List of Opportunity to Meeting IDs
280         @param context: A standard dictionary for contextual values
281
282         @return: Dictionary value for created Meeting view
283         """
284         data_obj = self.pool.get('ir.model.data')
285         if context is None:
286             context = {}
287         value = {}
288         for opp in self.browse(cr, uid, ids, context=context):
289             # Get meeting views
290             result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
291             res = data_obj.read(cr, uid, result, ['res_id'], context=context)
292             id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
293             id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
294             id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
295             if id1:
296                 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
297             if id2:
298                 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
299             if id3:
300                 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
301
302             context = {
303                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
304                 'default_email_from': opp.email_from,
305                 'default_state': 'open',
306                 'default_name': opp.name
307             }
308             value = {
309                 'name': ('Meetings'),
310                 'domain': "[('user_id','=',%s)]" % (uid),
311                 'context': context,
312                 'view_type': 'form',
313                 'view_mode': 'calendar,form,tree',
314                 'res_model': 'crm.meeting',
315                 'view_id': False,
316                 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')],
317                 'type': 'ir.actions.act_window',
318                 'search_view_id': res['res_id'],
319                 'nodestroy': True
320             }
321         return value
322
323     def action_print_survey(self, cr, uid, ids, context=None):
324         """
325         If response is available then print this response otherwise print survey form(print template of the survey).
326
327         @param self: The object pointer
328         @param cr: the current row, from the database cursor,
329         @param uid: the current user’s ID for security checks,
330         @param ids: List of Survey IDs
331         @param context: A standard dictionary for contextual values
332         @return: Dictionary value for print survey form.
333         """
334         if context is None:
335             context = {}
336         record = self.browse(cr, uid, ids, context=context)
337         record = record and record[0]
338         context.update({'survey_id': record.survey.id, 'response_id': [record.response], 'response_no': 0, })
339         value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context)
340         return value
341
342     def message_new(self, cr, uid, msg, custom_values=None, context=None):
343         """Automatically called when new email message arrives"""
344         res_id = super(hr_applicant,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
345         subject = msg.get('subject') or _("No Subject")
346         body = msg.get('body_text')
347         msg_from = msg.get('from')
348         priority = msg.get('priority')
349         vals = {
350             'name': subject,
351             'email_from': msg_from,
352             'email_cc': msg.get('cc'),
353             'description': body,
354             'user_id': False,
355         }
356         if priority:
357             vals['priority'] = priority
358         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
359         self.write(cr, uid, [res_id], vals, context)
360         return res_id
361
362     def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
363         if isinstance(ids, (str, int, long)):
364             ids = [ids]
365         if vals is None:
366             vals = {}
367         msg_from = msg['from']
368         vals.update({
369             'description': msg['body_text']
370         })
371         if msg.get('priority', False):
372             vals['priority'] = msg.get('priority')
373
374         maps = {
375             'cost':'planned_cost',
376             'revenue': 'planned_revenue',
377             'probability':'probability'
378         }
379         vls = { }
380         for line in msg['body_text'].split('\n'):
381             line = line.strip()
382             res = tools.misc.command_re.match(line)
383             if res and maps.get(res.group(1).lower(), False):
384                 key = maps.get(res.group(1).lower())
385                 vls[key] = res.group(2).lower()
386
387         vals.update(vls)
388         res = self.write(cr, uid, ids, vals, context=context)
389         self.message_append_dict(cr, uid, ids, msg, context=context)
390         return res
391
392     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
393                 return 'Applicant'
394
395     def case_open_send_note(self, cr, uid, ids, context=None):
396         message = _("Applicant has been set <b>in progress</b>.")
397         return self.message_append_note(cr, uid, ids, _('System notification'),
398                         message, type='notification', context=context)
399
400     def case_close_send_note(self, cr, uid, ids, context=None):
401         if context is None:
402             context = {}
403         for applicant in self.browse(cr, uid, ids, context=context):
404             if applicant.emp_id:
405                 message = _("Applicant has been <b>hired</b> and created as an employee.")
406                 self.message_append_note(cr, uid, [applicant.id], _('System notification'),
407                         message, type='notification', context=context)
408             else:
409                 message = _("Applicant has been <b>hired</b>.")
410                 self.message_append_note(cr, uid, [applicant.id], _('System notification'),
411                         message, type='notification', context=context)
412         return True
413
414     def case_cancel_send_note(self, cr, uid, ids, context=None):
415         for id in ids:
416             msg = 'Applicant <b>refused</b>.'
417             self.message_append_note(cr, uid, [id], 'System Notification', msg, context=context)
418         return True
419
420     def case_reset_send_note(self,  cr, uid, ids, context=None):
421         message =_("Applicant has been set as <b>new</b>.")
422         return self.message_append_note(cr, uid, ids, _('System notification'),
423                         message, type='notification', context=context)
424
425     def message_get_subscribers(self, cr, uid, ids, context=None):
426         sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
427         for obj in self.browse(cr, uid, ids, context=context):
428             if obj.user_id:
429                 sub_ids.append(obj.user_id.id)
430         return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
431
432     def get_needaction_user_ids(self, cr, uid, ids, context=None):
433         result = dict.fromkeys(ids, [])
434         for obj in self.browse(cr, uid, ids, context=context):
435             if obj.state == 'draft' and obj.user_id:
436                 result[obj.id] = [obj.user_id.id]
437         return result
438
439     def create_send_note(self, cr, uid, ids, context=None):
440         message = _("Applicant has been <b>created</b>.")
441         return self.message_append_note(cr, uid, ids, _('System notification'),
442                         message, type='notification', context=context)
443
444     def create(self, cr, uid, vals, context=None):
445         obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
446         self.create_send_note(cr, uid, [obj_id], context=context)
447         return obj_id
448
449     def case_open(self, cr, uid, ids, context=None):
450         """
451             open Request of the applicant for the hr_recruitment
452         """
453         res = super(hr_applicant, self).case_open(cr, uid, ids, context)
454         date = self.read(cr, uid, ids, ['date_open'])[0]
455         if not date['date_open']:
456             self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),})
457         return res
458
459     def case_close(self, cr, uid, ids, context=None):
460         res = super(hr_applicant, self).case_close(cr, uid, ids, context)
461         return res
462
463     def case_close_with_emp(self, cr, uid, ids, context=None):
464         if context is None:
465             context = {}
466         hr_employee = self.pool.get('hr.employee')
467         model_data = self.pool.get('ir.model.data')
468         act_window = self.pool.get('ir.actions.act_window')
469         emp_id = False
470         for applicant in self.browse(cr, uid, ids, context=context):
471             address_id = False
472             if applicant.partner_id:
473                 address_id = applicant.partner_id.address_get(['contact'])['contact']
474             if applicant.job_id:
475                 applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
476                 emp_id = hr_employee.create(cr,uid,{'name': applicant.partner_name or applicant.name,
477                                                      'job_id': applicant.job_id.id,
478                                                      'address_home_id': address_id,
479                                                      'department_id': applicant.department_id.id
480                                                      })
481                 self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
482                 self.case_close(cr, uid, [applicant.id], context)
483             else:
484                 raise osv.except_osv(_('Warning!'),_('You must define Applied Job for this applicant.'))
485
486         action_model, action_id = model_data.get_object_reference(cr, uid, 'hr', 'open_view_employee_list')
487         dict_act_window = act_window.read(cr, uid, action_id, [])
488         if emp_id:
489             dict_act_window['res_id'] = emp_id
490         dict_act_window['view_mode'] = 'form,tree'
491         return dict_act_window
492
493     def case_cancel(self, cr, uid, ids, context=None):
494         """Overrides cancel for crm_case for setting probability
495         """
496         res = super(hr_applicant, self).case_cancel(cr, uid, ids, context)
497         self.write(cr, uid, ids, {'probability' : 0.0})
498         return res
499
500     def case_pending(self, cr, uid, ids, context=None):
501         """Marks case as pending"""
502         res = super(hr_applicant, self).case_pending(cr, uid, ids, context)
503         self.write(cr, uid, ids, {'probability' : 0.0})
504         return res
505
506     def case_reset(self, cr, uid, ids, context=None):
507         """Resets case as draft
508         """
509         res = super(hr_applicant, self).case_reset(cr, uid, ids, context)
510         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
511         return res
512
513     def set_priority(self, cr, uid, ids, priority, *args):
514         """Set applicant priority
515         """
516         return self.write(cr, uid, ids, {'priority' : priority})
517
518     def set_high_priority(self, cr, uid, ids, *args):
519         """Set applicant priority to high
520         """
521         return self.set_priority(cr, uid, ids, '1')
522
523     def set_normal_priority(self, cr, uid, ids, *args):
524         """Set applicant priority to normal
525         """
526         return self.set_priority(cr, uid, ids, '3')
527
528     def write(self, cr, uid, ids, vals, context=None):
529         if 'stage_id' in vals and vals['stage_id']:
530             stage = self.pool.get('hr.recruitment.stage').browse(cr, uid, vals['stage_id'], context=context)
531             self.message_append_note(cr, uid, ids, _('System notification'),
532                         _("changed stage to <b>%s</b>.") % stage.name, type='notification')
533         return super(hr_applicant,self).write(cr, uid, ids, vals, context=context)
534
535 hr_applicant()
536
537 class hr_job(osv.osv):
538     _inherit = "hr.job"
539     _name = "hr.job"
540     _columns = {
541         '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"),
542     }
543 hr_job()
544
545 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: