[IMP] remover message_ids field
[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         'active': fields.boolean('Active', help="If the active field is set to false, it will allow you to hide the case without removing it."),
138         'description': fields.text('Description'),
139         'email_from': fields.char('Email', size=128, help="These people will receive email."),
140         '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"),
141         'probability': fields.float('Probability'),
142         'partner_id': fields.many2one('res.partner', 'Partner'),
143         'create_date': fields.datetime('Creation Date', readonly=True, select=True),
144         'write_date': fields.datetime('Update Date', readonly=True),
145         'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage'),
146         'state': fields.selection(AVAILABLE_STATES, 'State', size=16, readonly=True,
147                                   help='The state is set to \'Draft\', when a case is created.\
148                                   \nIf the case is in progress the state is set to \'Open\'.\
149                                   \nWhen the case is over, the state is set to \'Done\'.\
150                                   \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
151         'company_id': fields.many2one('res.company', 'Company'),
152         'user_id': fields.many2one('res.users', 'Responsible'),
153         # Applicant Columns
154         'date_closed': fields.datetime('Closed', readonly=True, select=True),
155         'date_open': fields.datetime('Opened', readonly=True, select=True),
156         'date': fields.datetime('Date'),
157         'date_action': fields.date('Next Action Date'),
158         'title_action': fields.char('Next Action', size=64),
159         'priority': fields.selection(AVAILABLE_PRIORITIES, 'Appreciation'),
160         'job_id': fields.many2one('hr.job', 'Applied Job'),
161         'salary_proposed_extra': fields.char('Proposed Salary Extra', size=100, help="Salary Proposed by the Organisation, extra advantages"),
162         'salary_expected_extra': fields.char('Expected Salary Extra', size=100, help="Salary Expected by Applicant, extra advantages"),
163         'salary_proposed': fields.float('Proposed Salary', help="Salary Proposed by the Organisation"),
164         'salary_expected': fields.float('Expected Salary', help="Salary Expected by Applicant"),
165         'availability': fields.integer('Availability (Days)'),
166         'partner_name': fields.char("Applicant's Name", size=64),
167         'partner_phone': fields.char('Phone', size=32),
168         'partner_mobile': fields.char('Mobile', size=32),
169         'type_id': fields.many2one('hr.recruitment.degree', 'Degree'),
170         'department_id': fields.many2one('hr.department', 'Department'),
171         'state': fields.selection(AVAILABLE_STATES, 'State', size=16, readonly=True),
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 create(self, cr, uid, vals, context=None):
393         obj_id = super(hr_applicant, self).create(cr, uid, vals, context=context)
394         self.create_send_note(cr, uid, [obj_id], context=context)
395         return obj_id
396
397     def case_open(self, cr, uid, ids, context=None):
398         """
399             open Request of the applicant for the hr_recruitment
400         """
401         res = super(hr_applicant, self).case_open(cr, uid, ids, context)
402         date = self.read(cr, uid, ids, ['date_open'])[0]
403         if not date['date_open']:
404             self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),})
405         return res
406
407     def case_close(self, cr, uid, ids, context=None):
408         res = super(hr_applicant, self).case_close(cr, uid, ids, context)
409         return res
410
411     def case_close_with_emp(self, cr, uid, ids, context=None):
412         if context is None:
413             context = {}
414         hr_employee = self.pool.get('hr.employee')
415         model_data = self.pool.get('ir.model.data')
416         act_window = self.pool.get('ir.actions.act_window')
417         emp_id = False
418         for applicant in self.browse(cr, uid, ids, context=context):
419             address_id = False
420             if applicant.partner_id:
421                 address_id = applicant.partner_id.address_get(['contact'])['contact']
422             if applicant.job_id:
423                 applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
424                 emp_id = hr_employee.create(cr,uid,{'name': applicant.partner_name or applicant.name,
425                                                      'job_id': applicant.job_id.id,
426                                                      'address_home_id': address_id,
427                                                      'department_id': applicant.department_id.id
428                                                      })
429                 self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
430                 self.case_close(cr, uid, [applicant.id], context)
431             else:
432                 raise osv.except_osv(_('Warning!'),_('You must define Applied Job for this applicant.'))
433
434         action_model, action_id = model_data.get_object_reference(cr, uid, 'hr', 'open_view_employee_list')
435         dict_act_window = act_window.read(cr, uid, action_id, [])
436         if emp_id:
437             dict_act_window['res_id'] = emp_id
438         dict_act_window['view_mode'] = 'form,tree'
439         return dict_act_window
440
441     def case_cancel(self, cr, uid, ids, context=None):
442         """Overrides cancel for crm_case for setting probability
443         """
444         res = super(hr_applicant, self).case_cancel(cr, uid, ids, context)
445         self.write(cr, uid, ids, {'probability' : 0.0})
446         return res
447
448     def case_pending(self, cr, uid, ids, context=None):
449         """Marks case as pending"""
450         res = super(hr_applicant, self).case_pending(cr, uid, ids, context)
451         self.write(cr, uid, ids, {'probability' : 0.0})
452         return res
453
454     def case_reset(self, cr, uid, ids, context=None):
455         """Resets case as draft
456         """
457         res = super(hr_applicant, self).case_reset(cr, uid, ids, context)
458         self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
459         return res
460
461     def set_priority(self, cr, uid, ids, priority, *args):
462         """Set applicant priority
463         """
464         return self.write(cr, uid, ids, {'priority' : priority})
465
466     def set_high_priority(self, cr, uid, ids, *args):
467         """Set applicant priority to high
468         """
469         return self.set_priority(cr, uid, ids, '1')
470
471     def set_normal_priority(self, cr, uid, ids, *args):
472         """Set applicant priority to normal
473         """
474         return self.set_priority(cr, uid, ids, '3')
475
476     def write(self, cr, uid, ids, vals, context=None):
477         if 'stage_id' in vals and vals['stage_id']:
478             stage = self.pool.get('hr.recruitment.stage').browse(cr, uid, vals['stage_id'], context=context)
479             self.message_append_note(cr, uid, ids, body=_("Stage changed to <b>%s</b>.") % stage.name, context=context)
480         return super(hr_applicant,self).write(cr, uid, ids, vals, context=context)
481
482     # -------------------------------------------------------
483     # OpenChatter methods and notifications
484     # -------------------------------------------------------
485     
486     def message_get_subscribers(self, cr, uid, ids, context=None):
487         sub_ids = self.message_get_subscribers_ids(cr, uid, ids, context=context);
488         for obj in self.browse(cr, uid, ids, context=context):
489             if obj.user_id:
490                 sub_ids.append(obj.user_id.id)
491         return self.pool.get('res.users').read(cr, uid, sub_ids, context=context)
492
493     def get_needaction_user_ids(self, cr, uid, ids, context=None):
494         result = dict.fromkeys(ids, [])
495         for obj in self.browse(cr, uid, ids, context=context):
496             if obj.state == 'draft' and obj.user_id:
497                 result[obj.id] = [obj.user_id.id]
498         return result
499     
500     def case_get_note_msg_prefix(self, cr, uid, id, context=None):
501                 return 'Applicant'
502
503     def case_open_send_note(self, cr, uid, ids, context=None):
504         message = _("Applicant has been set <b>in progress</b>.")
505         return self.message_append_note(cr, uid, ids, body=message, context=context)
506
507     def case_close_send_note(self, cr, uid, ids, context=None):
508         if context is None:
509             context = {}
510         for applicant in self.browse(cr, uid, ids, context=context):
511             if applicant.emp_id:
512                 message = _("Applicant has been <b>hired</b> and created as an employee.")
513                 self.message_append_note(cr, uid, [applicant.id], body=message, context=context)
514             else:
515                 message = _("Applicant has been <b>hired</b>.")
516                 self.message_append_note(cr, uid, [applicant.id], body=message, context=context)
517         return True
518
519     def case_cancel_send_note(self, cr, uid, ids, context=None):
520         msg = 'Applicant <b>refused</b>.'
521         return self.message_append_note(cr, uid, ids, body=msg, context=context)
522
523     def case_reset_send_note(self,  cr, uid, ids, context=None):
524         message =_("Applicant has been set as <b>new</b>.")
525         return self.message_append_note(cr, uid, ids, body=message, context=context)
526
527     def create_send_note(self, cr, uid, ids, context=None):
528         message = _("Applicant has been <b>created</b>.")
529         return self.message_append_note(cr, uid, ids, body=message, 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: