[FIX] fixed error in selection
[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
61 class hr_recruitment_stage(osv.osv):
62     """ Stage of HR Recruitment """
63     _name = "hr.recruitment.stage"
64     _description = "Stage of Recruitment"
65     _order = 'sequence'
66     _columns = {
67         'name': fields.char('Name', size=64, required=True, translate=True),
68         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of stages."),
69         '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."),
70         '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."),
71         'fold': fields.boolean('Hide in views if empty', help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
72         'requirements': fields.text('Requirements'),
73     }
74     _defaults = {
75         'sequence': 1,
76         'state': 'draft',
77         'fold': False,
78     }
79
80 class hr_recruitment_degree(osv.osv):
81     """ Degree of HR Recruitment """
82     _name = "hr.recruitment.degree"
83     _description = "Degree of Recruitment"
84     _columns = {
85         'name': fields.char('Name', size=64, required=True, translate=True),
86         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of degrees."),
87     }
88     _defaults = {
89         'sequence': 1,
90     }
91     _sql_constraints = [
92         ('name_uniq', 'unique (name)', 'The name of the Degree of Recruitment must be unique!')
93     ]
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', '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.related('stage_id', 'state', type="selection", store=True,
148                 selection=AVAILABLE_STATES, string="State", readonly=True,
149                 help='The state is set to \'Draft\', when a case is created.\
150                       If the case is in progress the state is set to \'Open\'.\
151                       When the case is over, the state is set to \'Done\'.\
152                       If the case needs to be reviewed then the state is \
153                       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': '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     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_find(self, cr, uid, section_id, domain=[], order='sequence', context=None):
233         domain = list(domain)
234         if section_id:
235             domain.append(('department_id', '=', section_id))
236         stage_ids = self.pool.get('hr.recruitment.stage').search(cr, uid, domain, order=order, context=context)
237         if stage_ids:
238             return stage_ids[0]
239         return False
240
241     def stage_previous(self, cr, uid, ids, context=None):
242         """ This function computes previous stage for case from its current stage
243             using available stage for that case type
244         """
245         stage_obj = self.pool.get('hr.recruitment.stage')
246         for case in self.browse(cr, uid, ids, context=context):
247             department = (case.department_id.id or False)
248             st = case.stage_id.id  or False
249             stage_ids = stage_obj.search(cr, uid, ['|',('department_id','=',department),('department_id','=',False)], context=context)
250             if st and stage_ids.index(st):
251                 self.write(cr, uid, [case.id], {'stage_id': stage_ids[stage_ids.index(st)-1]}, context=context)
252             else:
253                 self.write(cr, uid, [case.id], {'stage_id': False}, context=context)
254         return True
255
256     def stage_next(self, cr, uid, ids, context=None):
257         """This function computes next stage for case from its current stage
258              using available stage for that case type
259         @param self: The object pointer
260         @param cr: the current row, from the database cursor,
261         @param uid: the current user’s ID for security checks,
262         @param ids: List of case IDs
263         @param context: A standard dictionary for contextual values"""
264         stage_obj = self.pool.get('hr.recruitment.stage')
265         for case in self.browse(cr, uid, ids, context=context):
266             department = (case.department_id.id or False)
267             st = case.stage_id.id  or False
268             stage_ids = stage_obj.search(cr, uid, ['|',('department_id','=',department),('department_id','=',False)], context=context)
269             val = False
270             if st and len(stage_ids) != stage_ids.index(st)+1:
271                 val = stage_ids[stage_ids.index(st)+1]
272             elif (not st) and stage_ids:
273                 val = stage_ids[0]
274             else:
275                 val = False
276             self.write(cr, uid, [case.id], {'stage_id': val}, context=context)
277         return True
278
279     def action_makeMeeting(self, cr, uid, ids, context=None):
280         """
281         This opens Meeting's calendar view to schedule meeting on current Opportunity
282         @param self: The object pointer
283         @param cr: the current row, from the database cursor,
284         @param uid: the current user’s ID for security checks,
285         @param ids: List of Opportunity to Meeting IDs
286         @param context: A standard dictionary for contextual values
287
288         @return: Dictionary value for created Meeting view
289         """
290         data_obj = self.pool.get('ir.model.data')
291         if context is None:
292             context = {}
293         value = {}
294         for opp in self.browse(cr, uid, ids, context=context):
295             # Get meeting views
296             result = data_obj._get_id(cr, uid, 'crm', 'view_crm_case_meetings_filter')
297             res = data_obj.read(cr, uid, result, ['res_id'], context=context)
298             id1 = data_obj._get_id(cr, uid, 'crm', 'crm_case_calendar_view_meet')
299             id2 = data_obj._get_id(cr, uid, 'crm', 'crm_case_form_view_meet')
300             id3 = data_obj._get_id(cr, uid, 'crm', 'crm_case_tree_view_meet')
301             if id1:
302                 id1 = data_obj.browse(cr, uid, id1, context=context).res_id
303             if id2:
304                 id2 = data_obj.browse(cr, uid, id2, context=context).res_id
305             if id3:
306                 id3 = data_obj.browse(cr, uid, id3, context=context).res_id
307
308             context = {
309                 'default_partner_id': opp.partner_id and opp.partner_id.id or False,
310                 'default_email_from': opp.email_from,
311                 'default_state': 'open',
312                 'default_name': opp.name
313             }
314             value = {
315                 'name': ('Meetings'),
316                 'domain': "[('user_id','=',%s)]" % (uid),
317                 'context': context,
318                 'view_type': 'form',
319                 'view_mode': 'calendar,form,tree',
320                 'res_model': 'crm.meeting',
321                 'view_id': False,
322                 'views': [(id1, 'calendar'), (id2, 'form'), (id3, 'tree')],
323                 'type': 'ir.actions.act_window',
324                 'search_view_id': res['res_id'],
325                 'nodestroy': True
326             }
327         return value
328
329     def action_print_survey(self, cr, uid, ids, context=None):
330         """
331         If response is available then print this response otherwise print survey form(print template of the survey).
332
333         @param self: The object pointer
334         @param cr: the current row, from the database cursor,
335         @param uid: the current user’s ID for security checks,
336         @param ids: List of Survey IDs
337         @param context: A standard dictionary for contextual values
338         @return: Dictionary value for print survey form.
339         """
340         if context is None:
341             context = {}
342         record = self.browse(cr, uid, ids, context=context)
343         record = record and record[0]
344         context.update({'survey_id': record.survey.id, 'response_id': [record.response], 'response_no': 0, })
345         value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context)
346         return value
347
348     def message_new(self, cr, uid, msg, custom_values=None, context=None):
349         """Automatically called when new email message arrives"""
350         res_id = super(hr_applicant,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
351         subject = msg.get('subject') or _("No Subject")
352         body = msg.get('body_text')
353         msg_from = msg.get('from')
354         priority = msg.get('priority')
355         vals = {
356             'name': subject,
357             'email_from': msg_from,
358             'email_cc': msg.get('cc'),
359             'description': body,
360             'user_id': False,
361         }
362         if priority:
363             vals['priority'] = priority
364         vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
365         self.write(cr, uid, [res_id], vals, context)
366         return res_id
367
368     def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
369         if isinstance(ids, (str, int, long)):
370             ids = [ids]
371         if vals is None:
372             vals = {}
373         msg_from = msg['from']
374         vals.update({
375             'description': msg['body_text']
376         })
377         if msg.get('priority', False):
378             vals['priority'] = msg.get('priority')
379
380         maps = {
381             'cost':'planned_cost',
382             'revenue': 'planned_revenue',
383             'probability':'probability'
384         }
385         vls = { }
386         for line in msg['body_text'].split('\n'):
387             line = line.strip()
388             res = tools.misc.command_re.match(line)
389             if res and maps.get(res.group(1).lower(), False):
390                 key = maps.get(res.group(1).lower())
391                 vls[key] = res.group(2).lower()
392
393         vals.update(vls)
394         res = self.write(cr, uid, ids, vals, context=context)
395         self.message_append_dict(cr, uid, ids, msg, context=context)
396         return res
397
398     def create(self, cr, uid, vals, context=None):
399         obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
400         self.create_send_note(cr, uid, [obj_id], context=context)
401         return obj_id
402
403     def case_open(self, cr, uid, ids, context=None):
404         """
405             open Request of the applicant for the hr_recruitment
406         """
407         res = super(hr_applicant, self).case_open(cr, uid, ids, context)
408         date = self.read(cr, uid, ids, ['date_open'])[0]
409         if not date['date_open']:
410             self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),})
411         return res
412
413     def case_close(self, cr, uid, ids, context=None):
414         res = super(hr_applicant, self).case_close(cr, uid, ids, context)
415         return res
416
417     def case_close_with_emp(self, cr, uid, ids, context=None):
418         if context is None:
419             context = {}
420         hr_employee = self.pool.get('hr.employee')
421         model_data = self.pool.get('ir.model.data')
422         act_window = self.pool.get('ir.actions.act_window')
423         emp_id = False
424         for applicant in self.browse(cr, uid, ids, context=context):
425             address_id = False
426             if applicant.partner_id:
427                 address_id = applicant.partner_id.address_get(['contact'])['contact']
428             if applicant.job_id:
429                 applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
430                 emp_id = hr_employee.create(cr,uid,{'name': applicant.partner_name or applicant.name,
431                                                      'job_id': applicant.job_id.id,
432                                                      'address_home_id': address_id,
433                                                      'department_id': applicant.department_id.id
434                                                      })
435                 self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
436                 self.case_close(cr, uid, [applicant.id], context)
437             else:
438                 raise osv.except_osv(_('Warning!'),_('You must define Applied Job for this applicant.'))
439
440         action_model, action_id = model_data.get_object_reference(cr, uid, 'hr', 'open_view_employee_list')
441         dict_act_window = act_window.read(cr, uid, action_id, [])
442         if emp_id:
443             dict_act_window['res_id'] = emp_id
444         dict_act_window['view_mode'] = 'form,tree'
445         return dict_act_window
446
447     def case_cancel(self, cr, uid, ids, context=None):
448         """Overrides cancel for crm_case for setting probability
449         """
450         res = super(hr_applicant, self).case_cancel(cr, uid, ids, context)
451         self.write(cr, uid, ids, {'probability' : 0.0})
452         return res
453
454     def case_pending(self, cr, uid, ids, context=None):
455         """Marks case as pending"""
456         res = super(hr_applicant, self).case_pending(cr, uid, ids, context)
457         self.write(cr, uid, ids, {'probability' : 0.0})
458         return res
459
460     def case_reset(self, cr, uid, ids, context=None):
461         """Resets case as draft
462         """
463         res = super(hr_applicant, self).case_reset(cr, uid, ids, context)
464         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
465         return res
466
467     def set_priority(self, cr, uid, ids, priority, *args):
468         """Set applicant priority
469         """
470         return self.write(cr, uid, ids, {'priority' : priority})
471
472     def set_high_priority(self, cr, uid, ids, *args):
473         """Set applicant priority to high
474         """
475         return self.set_priority(cr, uid, ids, '1')
476
477     def set_normal_priority(self, cr, uid, ids, *args):
478         """Set applicant priority to normal
479         """
480         return self.set_priority(cr, uid, ids, '3')
481
482     def write(self, cr, uid, ids, vals, context=None):
483         if 'stage_id' in vals and vals['stage_id']:
484             stage = self.pool.get('hr.recruitment.stage').browse(cr, uid, vals['stage_id'], context=context)
485             self.message_append_note(cr, uid, ids, body=_("Stage changed to <b>%s</b>.") % stage.name, context=context)
486         return super(hr_applicant,self).write(cr, uid, ids, vals, context=context)
487
488     # -------------------------------------------------------
489     # OpenChatter methods and notifications
490     # -------------------------------------------------------
491     
492     def message_get_subscribers(self, cr, uid, ids, context=None):
493         sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
494         for obj in self.browse(cr, uid, ids, context=context):
495             if obj.user_id:
496                 sub_ids.append(obj.user_id.id)
497         return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
498
499     def get_needaction_user_ids(self, cr, uid, ids, context=None):
500         result = dict.fromkeys(ids, [])
501         for obj in self.browse(cr, uid, ids, context=context):
502             if obj.state == 'draft' and obj.user_id:
503                 result[obj.id] = [obj.user_id.id]
504         return result
505     
506     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
507                 return 'Applicant'
508
509     def case_open_send_note(self, cr, uid, ids, context=None):
510         message = _("Applicant has been set <b>in progress</b>.")
511         return self.message_append_note(cr, uid, ids, body=message, context=context)
512
513     def case_close_send_note(self, cr, uid, ids, context=None):
514         if context is None:
515             context = {}
516         for applicant in self.browse(cr, uid, ids, context=context):
517             if applicant.emp_id:
518                 message = _("Applicant has been <b>hired</b> and created as an employee.")
519                 self.message_append_note(cr, uid, [applicant.id], body=message, context=context)
520             else:
521                 message = _("Applicant has been <b>hired</b>.")
522                 self.message_append_note(cr, uid, [applicant.id], body=message, context=context)
523         return True
524
525     def case_cancel_send_note(self, cr, uid, ids, context=None):
526         msg = 'Applicant <b>refused</b>.'
527         return self.message_append_note(cr, uid, ids, body=msg, context=context)
528
529     def case_reset_send_note(self,  cr, uid, ids, context=None):
530         message =_("Applicant has been set as <b>new</b>.")
531         return self.message_append_note(cr, uid, ids, body=message, context=context)
532
533     def create_send_note(self, cr, uid, ids, context=None):
534         message = _("Applicant has been <b>created</b>.")
535         return self.message_append_note(cr, uid, ids, body=message, context=context)
536
537
538 class hr_job(osv.osv):
539     _inherit = "hr.job"
540     _name = "hr.job"
541     _columns = {
542         '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"),
543     }
544
545
546 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: