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