[IMP] [FIX] Cleaned code. Also fixed regression bugs introduced by simplifying loops...
[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         'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \
145                                  domain="[('partner_id','=',partner_id)]"),
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.selection(AVAILABLE_STATES, 'State', size=16, readonly=True,
150                                   help='The state is set to \'Draft\', when a case is created.\
151                                   \nIf the case is in progress the state is set to \'Open\'.\
152                                   \nWhen the case is over, the state is set to \'Done\'.\
153                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
154         'company_id': fields.many2one('res.company', 'Company'),
155         'user_id': fields.many2one('res.users', 'Responsible'),
156         # Applicant Columns
157         'date_closed': fields.datetime('Closed', readonly=True, select=True),
158         'date_open': fields.datetime('Opened', readonly=True, select=True),
159         'date': fields.datetime('Date'),
160         'date_action': fields.date('Next Action Date'),
161         'title_action': fields.char('Next Action', size=64),
162         'priority': fields.selection(AVAILABLE_PRIORITIES, 'Appreciation'),
163         'job_id': fields.many2one('hr.job', 'Applied Job'),
164         'salary_proposed_extra': fields.char('Proposed Salary Extra', size=100, help="Salary Proposed by the Organisation, extra advantages"),
165         'salary_expected_extra': fields.char('Expected Salary Extra', size=100, help="Salary Expected by Applicant, extra advantages"),
166         'salary_proposed': fields.float('Proposed Salary', help="Salary Proposed by the Organisation"),
167         'salary_expected': fields.float('Expected Salary', help="Salary Expected by Applicant"),
168         'availability': fields.integer('Availability (Days)'),
169         'partner_name': fields.char("Applicant's Name", size=64),
170         'partner_phone': fields.char('Phone', size=32),
171         'partner_mobile': fields.char('Mobile', size=32),
172         'type_id': fields.many2one('hr.recruitment.degree', 'Degree'),
173         'department_id': fields.many2one('hr.department', 'Department'),
174         'survey': fields.related('job_id', 'survey_id', type='many2one', relation='survey', string='Survey'),
175         'response': fields.integer("Response"),
176         'reference': fields.char('Refered By', size=128),
177         'source_id': fields.many2one('hr.recruitment.source', 'Source'),
178         'day_open': fields.function(_compute_day, string='Days to Open', \
179                                 multi='day_open', type="float", store=True),
180         'day_close': fields.function(_compute_day, string='Days to Close', \
181                                 multi='day_close', type="float", store=True),
182         'color': fields.integer('Color Index'),
183         'emp_id': fields.many2one('hr.employee', 'employee'),
184         'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
185     }
186
187     _defaults = {
188         'active': lambda *a: 1,
189         'user_id':  lambda self, cr, uid, context: uid,
190         'email_from': crm.crm_case. _get_default_email,
191         'state': lambda *a: 'draft',
192         'priority': lambda *a: '',
193         'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
194         'color': 0,
195     }
196
197     def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
198         access_rights_uid = access_rights_uid or uid
199         stage_obj = self.pool.get('hr.recruitment.stage')
200         order = stage_obj._order
201         if read_group_order == 'stage_id desc':
202             # lame hack to allow reverting search, should just work in the trivial case
203             order = "%s desc" % order
204         stage_ids = stage_obj._search(cr, uid, ['|',('id','in',ids),('department_id','=',False)], order=order,
205                                       access_rights_uid=access_rights_uid, context=context)
206         result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
207         # restore order of the search
208         result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
209         return result
210
211     _group_by_full = {
212         'stage_id': _read_group_stage_ids
213     }
214
215
216     def onchange_job(self,cr, uid, ids, job, context=None):
217         result = {}
218
219         if job:
220             job_obj = self.pool.get('hr.job')
221             result['department_id'] = job_obj.browse(cr, uid, job, context=context).department_id.id
222             return {'value': result}
223         return {'value': {'department_id': False}}
224
225     def onchange_department_id(self, cr, uid, ids, department_id=False, context=None):
226         if not department_id:
227             return {'value': {'stage_id': False}}
228         obj_recru_stage = self.pool.get('hr.recruitment.stage')
229         stage_ids = obj_recru_stage.search(cr, uid, ['|',('department_id','=',department_id),('department_id','=',False)], context=context)
230         stage_id = stage_ids and stage_ids[0] or False
231         return {'value': {'stage_id': stage_id}}
232
233     def stage_previous(self, cr, uid, ids, context=None):
234         """This function computes previous stage for case from its current stage
235              using available stage for that case type
236         @param self: The object pointer
237         @param cr: the current row, from the database cursor,
238         @param uid: the current user’s ID for security checks,
239         @param ids: List of case IDs
240         @param context: A standard dictionary for contextual values"""
241         stage_obj = self.pool.get('hr.recruitment.stage')
242         for case in self.browse(cr, uid, ids, context=context):
243             department = (case.department_id.id or False)
244             st = case.stage_id.id  or False
245             stage_ids = stage_obj.search(cr, uid, ['|',('department_id','=',department),('department_id','=',False)], context=context)
246             if st and stage_ids.index(st):
247                 self.write(cr, uid, [case.id], {'stage_id': stage_ids[stage_ids.index(st)-1]}, context=context)
248             else:
249                 self.write(cr, uid, [case.id], {'stage_id': False}, context=context)
250         return True
251
252     def stage_next(self, cr, uid, ids, context=None):
253         """This function computes next stage for case from its current stage
254              using available stage for that case type
255         @param self: The object pointer
256         @param cr: the current row, from the database cursor,
257         @param uid: the current user’s ID for security checks,
258         @param ids: List of case IDs
259         @param context: A standard dictionary for contextual values"""
260         stage_obj = self.pool.get('hr.recruitment.stage')
261         for case in self.browse(cr, uid, ids, context=context):
262             department = (case.department_id.id or False)
263             st = case.stage_id.id  or False
264             stage_ids = stage_obj.search(cr, uid, ['|',('department_id','=',department),('department_id','=',False)], context=context)
265             val = False
266             if st and len(stage_ids) != stage_ids.index(st)+1:
267                 val = stage_ids[stage_ids.index(st)+1]
268             elif (not st) and stage_ids:
269                 val = stage_ids[0]
270             else:
271                 val = False
272             self.write(cr, uid, [case.id], {'stage_id': val}, context=context)
273         return True
274
275     def action_makeMeeting(self, cr, uid, ids, context=None):
276         """
277         This opens Meeting's calendar view to schedule meeting on current Opportunity
278         @param self: The object pointer
279         @param cr: the current row, from the database cursor,
280         @param uid: the current user’s ID for security checks,
281         @param ids: List of Opportunity to Meeting IDs
282         @param context: A standard dictionary for contextual values
283
284         @return: Dictionary value for created Meeting view
285         """
286         data_obj = self.pool.get('ir.model.data')
287         if context is None:
288             context = {}
289         value = {}
290         for opp in self.browse(cr, uid, ids, context=context):
291             # Get meeting views
292             result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
293             res = data_obj.read(cr, uid, result, ['res_id'], context=context)
294             id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
295             id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
296             id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
297             if id1:
298                 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
299             if id2:
300                 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
301             if id3:
302                 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
303
304             context = {
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 case_get_note_msg_prefix(self, cr, uid, id, context=None):
395                 return 'Applicant'
396
397     def case_open_send_note(self, cr, uid, ids, context=None):
398         message = _("Applicant has been set <b>in Progress</b>.")
399         return self.message_append_note(cr, uid, ids, _('System notification'),
400                         message, type='notification', context=context)
401
402     def case_close_send_note(self, cr, uid, ids, context=None):
403         if context is None:
404             context = {}
405         for applicant in self.browse(cr, uid, ids, context=context):
406             if applicant.emp_id:
407                 message = _("Applicant has been <b>hired</b> and created as an employee.")
408                 self.message_append_note(cr, uid, [applicant.id], _('System notification'),
409                         message, type='notification', context=context)
410             else:
411                 message = _("Applicant has been <b>hired</b>.")
412                 self.message_append_note(cr, uid, [applicant.id], _('System notification'),
413                         message, type='notification', context=context)
414         return True
415
416     def case_reset_send_note(self,  cr, uid, ids, context=None):
417         message =_("Applicant has been set as <b>new</b>.")
418         return self.message_append_note(cr, uid, ids, _('System notification'),
419                         message, type='notification', context=context)
420
421     def message_get_subscribers(self, cr, uid, ids, context=None):
422         sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
423         for obj in self.browse(cr, uid, ids, context=context):
424             if obj.user_id:
425                 sub_ids.append(obj.user_id.id)
426         return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
427
428     def get_needaction_user_ids(self, cr, uid, ids, context=None):
429         result = dict.fromkeys(ids, [])
430         for obj in self.browse(cr, uid, ids, context=context):
431             if obj.state == 'draft' and obj.user_id:
432                 result[obj.id] = [obj.user_id.id]
433         return result
434
435     def create_send_note(self, cr, uid, ids, context=None):
436         message = _("Applicant has been <b>created</b>.")
437         return self.message_append_note(cr, uid, ids, _('System notification'),
438                         message, type='notification', context=context)
439
440     def create(self, cr, uid, vals, context=None):
441         obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
442         self.create_send_note(cr, uid, [obj_id], context=context)
443         return obj_id
444
445     def case_open(self, cr, uid, ids, context=None):
446         """
447             open Request of the applicant for the hr_recruitment
448         """
449         res = super(hr_applicant, self).case_open(cr, uid, ids, context)
450         date = self.read(cr, uid, ids, ['date_open'])[0]
451         if not date['date_open']:
452             self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),})
453         return res
454
455     def case_close(self, cr, uid, ids, context=None):
456         res = super(hr_applicant, self).case_close(cr, uid, ids, context)
457         return res
458
459     def case_close_with_emp(self, cr, uid, ids, context=None):
460         if context is None:
461             context = {}
462         hr_employee = self.pool.get('hr.employee')
463         model_data = self.pool.get('ir.model.data')
464         act_window = self.pool.get('ir.actions.act_window')
465         emp_id = False
466         for applicant in self.browse(cr, uid, ids, context=context):
467             address_id = False
468             if applicant.partner_id:
469                 address_id = applicant.partner_id.address_get(['contact'])['contact']
470             if applicant.job_id:
471                 applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
472                 emp_id = hr_employee.create(cr,uid,{'name': applicant.partner_name or applicant.name,
473                                                      'job_id': applicant.job_id.id,
474                                                      'address_home_id': address_id,
475                                                      'department_id': applicant.department_id.id
476                                                      })
477                 self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
478                 self.case_close(cr, uid, [applicant.id], context)
479             else:
480                 raise osv.except_osv(_('Warning!'),_('You must define Applied Job for this applicant.'))
481
482         action_model, action_id = model_data.get_object_reference(cr, uid, 'hr', 'open_view_employee_list')
483         dict_act_window = act_window.read(cr, uid, action_id, [])
484         if emp_id:
485             dict_act_window['res_id'] = emp_id
486         dict_act_window['view_mode'] = 'form,tree'
487         return dict_act_window
488
489     def case_cancel(self, cr, uid, ids, context=None):
490         """Overrides cancel for crm_case for setting probability
491         """
492         res = super(hr_applicant, self).case_cancel(cr, uid, ids, context)
493         self.write(cr, uid, ids, {'probability' : 0.0})
494         return res
495
496     def case_pending(self, cr, uid, ids, context=None):
497         """Marks case as pending"""
498         res = super(hr_applicant, self).case_pending(cr, uid, ids, context)
499         self.write(cr, uid, ids, {'probability' : 0.0})
500         return res
501
502     def case_reset(self, cr, uid, ids, context=None):
503         """Resets case as draft
504         """
505         res = super(hr_applicant, self).case_reset(cr, uid, ids, context)
506         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
507         return res
508
509     def set_priority(self, cr, uid, ids, priority, *args):
510         """Set applicant priority
511         """
512         return self.write(cr, uid, ids, {'priority' : priority})
513
514     def set_high_priority(self, cr, uid, ids, *args):
515         """Set applicant priority to high
516         """
517         return self.set_priority(cr, uid, ids, '1')
518
519     def set_normal_priority(self, cr, uid, ids, *args):
520         """Set applicant priority to normal
521         """
522         return self.set_priority(cr, uid, ids, '3')
523
524     def write(self, cr, uid, ids, vals, context=None):
525         if 'stage_id' in vals and vals['stage_id']:
526             stage = self.pool.get('hr.recruitment.stage').browse(cr, uid, vals['stage_id'], context=context)
527             self.message_append_note(cr, uid, ids, _('System notification'),
528                         _("changed stage to <b>%s</b>.") % stage.name, type='notification')
529         return super(hr_applicant,self).write(cr, uid, ids, vals, context=context)
530
531 hr_applicant()
532
533 class hr_job(osv.osv):
534     _inherit = "hr.job"
535     _name = "hr.job"
536     _columns = {
537         '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"),
538     }
539 hr_job()
540
541 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: