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