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