1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from random import randint
from odoo import api, fields, models, tools, SUPERUSER_ID
from odoo.tools.translate import _
from odoo.exceptions import UserError
AVAILABLE_PRIORITIES = [
('0', 'Normal'),
('1', 'Good'),
('2', 'Very Good'),
('3', 'Excellent')
]
class RecruitmentSource(models.Model):
_name = "hr.recruitment.source"
_description = "Source of Applicants"
_inherits = {"utm.source": "source_id"}
source_id = fields.Many2one('utm.source', "Source", ondelete='cascade', required=True)
email = fields.Char(related='alias_id.display_name', string="Email", readonly=True)
job_id = fields.Many2one('hr.job', "Job", ondelete='cascade')
alias_id = fields.Many2one('mail.alias', "Alias ID")
def create_alias(self):
campaign = self.env.ref('hr_recruitment.utm_campaign_job')
medium = self.env.ref('utm.utm_medium_email')
for source in self:
vals = {
'alias_parent_thread_id': source.job_id.id,
'alias_model_id': self.env['ir.model']._get('hr.applicant').id,
'alias_parent_model_id': self.env['ir.model']._get('hr.job').id,
'alias_name': "%s+%s" % (source.job_id.alias_name or source.job_id.name, source.name),
'alias_defaults': {
'job_id': source.job_id.id,
'campaign_id': campaign.id,
'medium_id': medium.id,
'source_id': source.source_id.id,
},
}
source.alias_id = self.env['mail.alias'].create(vals)
source.name = source.source_id.name
class RecruitmentStage(models.Model):
_name = "hr.recruitment.stage"
_description = "Recruitment Stages"
_order = 'sequence'
name = fields.Char("Stage Name", required=True, translate=True)
sequence = fields.Integer(
"Sequence", default=10,
help="Gives the sequence order when displaying a list of stages.")
job_ids = fields.Many2many(
'hr.job', string='Job Specific',
help='Specific jobs that uses this stage. Other jobs will not use this stage.')
requirements = fields.Text("Requirements")
template_id = fields.Many2one(
'mail.template', "Email Template",
help="If set, a message is posted on the applicant using the template when the applicant is set to the stage.")
fold = fields.Boolean(
"Folded in Kanban",
help="This stage is folded in the kanban view when there are no records in that stage to display.")
legend_blocked = fields.Char(
'Red Kanban Label', default=lambda self: _('Blocked'), translate=True, required=True)
legend_done = fields.Char(
'Green Kanban Label', default=lambda self: _('Ready for Next Stage'), translate=True, required=True)
legend_normal = fields.Char(
'Grey Kanban Label', default=lambda self: _('In Progress'), translate=True, required=True)
@api.model
def default_get(self, fields):
if self._context and self._context.get('default_job_id') and not self._context.get('hr_recruitment_stage_mono', False):
context = dict(self._context)
context.pop('default_job_id')
self = self.with_context(context)
return super(RecruitmentStage, self).default_get(fields)
class RecruitmentDegree(models.Model):
_name = "hr.recruitment.degree"
_description = "Applicant Degree"
_sql_constraints = [
('name_uniq', 'unique (name)', 'The name of the Degree of Recruitment must be unique!')
]
name = fields.Char("Degree Name", required=True, translate=True)
sequence = fields.Integer("Sequence", default=1, help="Gives the sequence order when displaying a list of degrees.")
class Applicant(models.Model):
_name = "hr.applicant"
_description = "Applicant"
_order = "priority desc, id desc"
_inherit = ['mail.thread.cc', 'mail.activity.mixin', 'utm.mixin']
name = fields.Char("Subject / Application Name", required=True, help="Email subject for applications sent via email")
active = fields.Boolean("Active", default=True, help="If the active field is set to false, it will allow you to hide the case without removing it.")
description = fields.Text("Description")
email_from = fields.Char("Email", size=128, help="Applicant email", compute='_compute_partner_phone_email',
inverse='_inverse_partner_email', store=True)
probability = fields.Float("Probability")
partner_id = fields.Many2one('res.partner', "Contact", copy=False)
create_date = fields.Datetime("Creation Date", readonly=True, index=True)
stage_id = fields.Many2one('hr.recruitment.stage', 'Stage', ondelete='restrict', tracking=True,
compute='_compute_stage', store=True, readonly=False,
domain="['|', ('job_ids', '=', False), ('job_ids', '=', job_id)]",
copy=False, index=True,
group_expand='_read_group_stage_ids')
last_stage_id = fields.Many2one('hr.recruitment.stage', "Last Stage",
help="Stage of the applicant before being in the current stage. Used for lost cases analysis.")
categ_ids = fields.Many2many('hr.applicant.category', string="Tags")
company_id = fields.Many2one('res.company', "Company", compute='_compute_company', store=True, readonly=False, tracking=True)
user_id = fields.Many2one(
'res.users', "Recruiter", compute='_compute_user',
tracking=True, store=True, readonly=False)
date_closed = fields.Datetime("Closed", compute='_compute_date_closed', store=True, index=True)
date_open = fields.Datetime("Assigned", readonly=True, index=True)
date_last_stage_update = fields.Datetime("Last Stage Update", index=True, default=fields.Datetime.now)
priority = fields.Selection(AVAILABLE_PRIORITIES, "Appreciation", default='0')
job_id = fields.Many2one('hr.job', "Applied Job", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True)
salary_proposed_extra = fields.Char("Proposed Salary Extra", help="Salary Proposed by the Organisation, extra advantages", tracking=True)
salary_expected_extra = fields.Char("Expected Salary Extra", help="Salary Expected by Applicant, extra advantages", tracking=True)
salary_proposed = fields.Float("Proposed Salary", group_operator="avg", help="Salary Proposed by the Organisation", tracking=True)
salary_expected = fields.Float("Expected Salary", group_operator="avg", help="Salary Expected by Applicant", tracking=True)
availability = fields.Date("Availability", help="The date at which the applicant will be available to start working", tracking=True)
partner_name = fields.Char("Applicant's Name")
partner_phone = fields.Char("Phone", size=32, compute='_compute_partner_phone_email',
inverse='_inverse_partner_phone', store=True)
partner_mobile = fields.Char("Mobile", size=32, compute='_compute_partner_phone_email',
inverse='_inverse_partner_mobile', store=True)
type_id = fields.Many2one('hr.recruitment.degree', "Degree")
department_id = fields.Many2one(
'hr.department', "Department", compute='_compute_department', store=True, readonly=False,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True)
day_open = fields.Float(compute='_compute_day', string="Days to Open", compute_sudo=True)
day_close = fields.Float(compute='_compute_day', string="Days to Close", compute_sudo=True)
delay_close = fields.Float(compute="_compute_day", string='Delay to Close', readonly=True, group_operator="avg", help="Number of days to close", store=True)
color = fields.Integer("Color Index", default=0)
emp_id = fields.Many2one('hr.employee', string="Employee", help="Employee linked to the applicant.", copy=False)
user_email = fields.Char(related='user_id.email', string="User Email", readonly=True)
attachment_number = fields.Integer(compute='_get_attachment_number', string="Number of Attachments")
employee_name = fields.Char(related='emp_id.name', string="Employee Name", readonly=False, tracking=False)
attachment_ids = fields.One2many('ir.attachment', 'res_id', domain=[('res_model', '=', 'hr.applicant')], string='Attachments')
kanban_state = fields.Selection([
('normal', 'Grey'),
('done', 'Green'),
('blocked', 'Red')], string='Kanban State',
copy=False, default='normal', required=True)
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked')
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid')
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing')
application_count = fields.Integer(compute='_compute_application_count', help='Applications with the same email')
meeting_count = fields.Integer(compute='_compute_meeting_count', help='Meeting Count')
refuse_reason_id = fields.Many2one('hr.applicant.refuse.reason', string='Refuse Reason', tracking=True)
@api.depends('date_open', 'date_closed')
def _compute_day(self):
for applicant in self:
if applicant.date_open:
date_create = applicant.create_date
date_open = applicant.date_open
applicant.day_open = (date_open - date_create).total_seconds() / (24.0 * 3600)
else:
applicant.day_open = False
if applicant.date_closed:
date_create = applicant.create_date
date_closed = applicant.date_closed
applicant.day_close = (date_closed - date_create).total_seconds() / (24.0 * 3600)
applicant.delay_close = applicant.day_close - applicant.day_open
else:
applicant.day_close = False
applicant.delay_close = False
@api.depends('email_from')
def _compute_application_count(self):
application_data = self.env['hr.applicant'].with_context(active_test=False).read_group([
('email_from', 'in', list(set(self.mapped('email_from'))))], ['email_from'], ['email_from'])
application_data_mapped = dict((data['email_from'], data['email_from_count']) for data in application_data)
applicants = self.filtered(lambda applicant: applicant.email_from)
for applicant in applicants:
applicant.application_count = application_data_mapped.get(applicant.email_from, 1) - 1
(self - applicants).application_count = False
def _compute_meeting_count(self):
if self.ids:
meeting_data = self.env['calendar.event'].sudo().read_group(
[('applicant_id', 'in', self.ids)],
['applicant_id'],
['applicant_id']
)
mapped_data = {m['applicant_id'][0]: m['applicant_id_count'] for m in meeting_data}
else:
mapped_data = dict()
for applicant in self:
applicant.meeting_count = mapped_data.get(applicant.id, 0)
def _get_attachment_number(self):
read_group_res = self.env['ir.attachment'].read_group(
[('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.ids)],
['res_id'], ['res_id'])
attach_data = dict((res['res_id'], res['res_id_count']) for res in read_group_res)
for record in self:
record.attachment_number = attach_data.get(record.id, 0)
@api.model
def _read_group_stage_ids(self, stages, domain, order):
# retrieve job_id from the context and write the domain: ids + contextual columns (job or default)
job_id = self._context.get('default_job_id')
search_domain = [('job_ids', '=', False)]
if job_id:
search_domain = ['|', ('job_ids', '=', job_id)] + search_domain
if stages:
search_domain = ['|', ('id', 'in', stages.ids)] + search_domain
stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID)
return stages.browse(stage_ids)
@api.depends('job_id', 'department_id')
def _compute_company(self):
for applicant in self:
company_id = False
if applicant.department_id:
company_id = applicant.department_id.company_id.id
if not company_id and applicant.job_id:
company_id = applicant.job_id.company_id.id
applicant.company_id = company_id or self.env.company.id
@api.depends('job_id')
def _compute_department(self):
for applicant in self:
applicant.department_id = applicant.job_id.department_id.id
@api.depends('job_id')
def _compute_stage(self):
for applicant in self:
if applicant.job_id:
if not applicant.stage_id:
stage_ids = self.env['hr.recruitment.stage'].search([
'|',
('job_ids', '=', False),
('job_ids', '=', applicant.job_id.id),
('fold', '=', False)
], order='sequence asc', limit=1).ids
applicant.stage_id = stage_ids[0] if stage_ids else False
else:
applicant.stage_id = False
@api.depends('job_id')
def _compute_user(self):
for applicant in self:
applicant.user_id = applicant.job_id.user_id.id or self.env.uid
@api.depends('partner_id')
def _compute_partner_phone_email(self):
for applicant in self:
applicant.partner_phone = applicant.partner_id.phone
applicant.partner_mobile = applicant.partner_id.mobile
applicant.email_from = applicant.partner_id.email
def _inverse_partner_email(self):
for applicant in self.filtered(lambda a: a.partner_id and a.email_from and not a.partner_id.email):
applicant.partner_id.email = applicant.email_from
def _inverse_partner_phone(self):
for applicant in self.filtered(lambda a: a.partner_id and a.partner_phone and not a.partner_id.phone):
applicant.partner_id.phone = applicant.partner_phone
def _inverse_partner_mobile(self):
for applicant in self.filtered(lambda a: a.partner_id and a.partner_mobile and not a.partner_id.mobile):
applicant.partner_id.mobile = applicant.partner_mobile
@api.depends('stage_id')
def _compute_date_closed(self):
for applicant in self:
if applicant.stage_id and applicant.stage_id.fold:
applicant.date_closed = fields.datetime.now()
else:
applicant.date_closed = False
@api.model
def create(self, vals):
if vals.get('department_id') and not self._context.get('default_department_id'):
self = self.with_context(default_department_id=vals.get('department_id'))
if vals.get('user_id'):
vals['date_open'] = fields.Datetime.now()
if vals.get('email_from'):
vals['email_from'] = vals['email_from'].strip()
return super(Applicant, self).create(vals)
def write(self, vals):
# user_id change: update date_open
if vals.get('user_id'):
vals['date_open'] = fields.Datetime.now()
if vals.get('email_from'):
vals['email_from'] = vals['email_from'].strip()
# stage_id: track last stage before update
if 'stage_id' in vals:
vals['date_last_stage_update'] = fields.Datetime.now()
if 'kanban_state' not in vals:
vals['kanban_state'] = 'normal'
for applicant in self:
vals['last_stage_id'] = applicant.stage_id.id
res = super(Applicant, self).write(vals)
else:
res = super(Applicant, self).write(vals)
return res
def get_empty_list_help(self, help):
if 'active_id' in self.env.context and self.env.context.get('active_model') == 'hr.job':
alias_id = self.env['hr.job'].browse(self.env.context['active_id']).alias_id
else:
alias_id = False
nocontent_values = {
'help_title': _('No application yet'),
'para_1': _('Let people apply by email to save time.') ,
'para_2': _('Attachments, like resumes, get indexed automatically.'),
}
nocontent_body = """
<p class="o_view_nocontent_empty_folder">%(help_title)s</p>
<p>%(para_1)s<br/>%(para_2)s</p>"""
if alias_id and alias_id.alias_domain and alias_id.alias_name:
email = alias_id.display_name
email_link = "<a href='mailto:%s'>%s</a>" % (email, email)
nocontent_values['email_link'] = email_link
nocontent_body += """<p class="o_copy_paste_email">%(email_link)s</p>"""
return nocontent_body % nocontent_values
def action_makeMeeting(self):
""" This opens Meeting's calendar view to schedule meeting on current applicant
@return: Dictionary value for created Meeting view
"""
self.ensure_one()
partners = self.partner_id | self.user_id.partner_id | self.department_id.manager_id.user_id.partner_id
category = self.env.ref('hr_recruitment.categ_meet_interview')
res = self.env['ir.actions.act_window']._for_xml_id('calendar.action_calendar_event')
res['context'] = {
'default_applicant_id': self.id,
'default_partner_ids': partners.ids,
'default_user_id': self.env.uid,
'default_name': self.name,
'default_categ_ids': category and [category.id] or False,
}
return res
def action_get_attachment_tree_view(self):
action = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment')
action['context'] = {'default_res_model': self._name, 'default_res_id': self.ids[0]}
action['domain'] = str(['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)])
action['search_view_id'] = (self.env.ref('hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment').id, )
return action
def action_applications_email(self):
return {
'type': 'ir.actions.act_window',
'name': _('Applications'),
'res_model': self._name,
'view_mode': 'kanban,tree,form,pivot,graph,calendar,activity',
'domain': [('email_from', 'in', self.mapped('email_from'))],
'context': {
'active_test': False
},
}
def _track_template(self, changes):
res = super(Applicant, self)._track_template(changes)
applicant = self[0]
if 'stage_id' in changes and applicant.stage_id.template_id:
res['stage_id'] = (applicant.stage_id.template_id, {
'auto_delete_message': True,
'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
'email_layout_xmlid': 'mail.mail_notification_light'
})
return res
def _creation_subtype(self):
return self.env.ref('hr_recruitment.mt_applicant_new')
def _track_subtype(self, init_values):
record = self[0]
if 'stage_id' in init_values and record.stage_id:
return self.env.ref('hr_recruitment.mt_applicant_stage_changed')
return super(Applicant, self)._track_subtype(init_values)
def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
""" Override to set alias of applicants to their job definition if any. """
aliases = self.mapped('job_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None)
res = {app.id: aliases.get(app.job_id.id) for app in self}
leftover = self.filtered(lambda rec: not rec.job_id)
if leftover:
res.update(super(Applicant, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names))
return res
def _message_get_suggested_recipients(self):
recipients = super(Applicant, self)._message_get_suggested_recipients()
for applicant in self:
if applicant.partner_id:
applicant._message_add_suggested_recipient(recipients, partner=applicant.partner_id, reason=_('Contact'))
elif applicant.email_from:
email_from = applicant.email_from
if applicant.partner_name:
email_from = tools.formataddr((applicant.partner_name, email_from))
applicant._message_add_suggested_recipient(recipients, email=email_from, reason=_('Contact Email'))
return recipients
@api.model
def message_new(self, msg, custom_values=None):
""" Overrides mail_thread message_new that is called by the mailgateway
through message_process.
This override updates the document according to the email.
"""
# remove default author when going through the mail gateway. Indeed we
# do not want to explicitly set user_id to False; however we do not
# want the gateway user to be responsible if no other responsible is
# found.
self = self.with_context(default_user_id=False)
val = msg.get('from').split('<')[0]
defaults = {
'name': msg.get('subject') or _("No Subject"),
'partner_name': val,
'email_from': msg.get('from'),
'partner_id': msg.get('author_id', False),
}
if msg.get('priority'):
defaults['priority'] = msg.get('priority')
if custom_values:
defaults.update(custom_values)
return super(Applicant, self).message_new(msg, custom_values=defaults)
def _message_post_after_hook(self, message, msg_vals):
if self.email_from and not self.partner_id:
# we consider that posting a message with a specified recipient (not a follower, a specific one)
# on a document without customer means that it was created through the chatter using
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.email_from)
if new_partner:
if new_partner.create_date.date() == fields.Date.today():
new_partner.write({
'type': 'private',
'phone': self.partner_phone,
'mobile': self.partner_mobile,
})
self.search([
('partner_id', '=', False),
('email_from', '=', new_partner.email),
('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id})
return super(Applicant, self)._message_post_after_hook(message, msg_vals)
def create_employee_from_applicant(self):
""" Create an hr.employee from the hr.applicants """
employee = False
for applicant in self:
contact_name = False
if applicant.partner_id:
address_id = applicant.partner_id.address_get(['contact'])['contact']
contact_name = applicant.partner_id.display_name
else:
if not applicant.partner_name:
raise UserError(_('You must define a Contact Name for this applicant.'))
new_partner_id = self.env['res.partner'].create({
'is_company': False,
'type': 'private',
'name': applicant.partner_name,
'email': applicant.email_from,
'phone': applicant.partner_phone,
'mobile': applicant.partner_mobile
})
applicant.partner_id = new_partner_id
address_id = new_partner_id.address_get(['contact'])['contact']
if applicant.partner_name or contact_name:
employee_data = {
'default_name': applicant.partner_name or contact_name,
'default_job_id': applicant.job_id.id,
'default_job_title': applicant.job_id.name,
'address_home_id': address_id,
'default_department_id': applicant.department_id.id or False,
'default_address_id': applicant.company_id and applicant.company_id.partner_id
and applicant.company_id.partner_id.id or False,
'default_work_email': applicant.department_id and applicant.department_id.company_id
and applicant.department_id.company_id.email or False,
'default_work_phone': applicant.department_id.company_id.phone,
'form_view_initial_mode': 'edit',
'default_applicant_id': applicant.ids,
}
dict_act_window = self.env['ir.actions.act_window']._for_xml_id('hr.open_view_employee_list')
dict_act_window['context'] = employee_data
return dict_act_window
def archive_applicant(self):
return {
'type': 'ir.actions.act_window',
'name': _('Refuse Reason'),
'res_model': 'applicant.get.refuse.reason',
'view_mode': 'form',
'target': 'new',
'context': {'default_applicant_ids': self.ids, 'active_test': False},
'views': [[False, 'form']]
}
def reset_applicant(self):
""" Reinsert the applicant into the recruitment pipe in the first stage"""
default_stage = dict()
for job_id in self.mapped('job_id'):
default_stage[job_id.id] = self.env['hr.recruitment.stage'].search(
['|',
('job_ids', '=', False),
('job_ids', '=', job_id.id),
('fold', '=', False)
], order='sequence asc', limit=1).id
for applicant in self:
applicant.write(
{'stage_id': applicant.job_id.id and default_stage[applicant.job_id.id],
'refuse_reason_id': False})
def toggle_active(self):
res = super(Applicant, self).toggle_active()
applicant_active = self.filtered(lambda applicant: applicant.active)
if applicant_active:
applicant_active.reset_applicant()
applicant_inactive = self.filtered(lambda applicant: not applicant.active)
if applicant_inactive:
return applicant_inactive.archive_applicant()
return res
class ApplicantCategory(models.Model):
_name = "hr.applicant.category"
_description = "Category of applicant"
def _get_default_color(self):
return randint(1, 11)
name = fields.Char("Tag Name", required=True)
color = fields.Integer(string='Color Index', default=_get_default_color)
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
]
class ApplicantRefuseReason(models.Model):
_name = "hr.applicant.refuse.reason"
_description = 'Refuse Reason of Applicant'
name = fields.Char('Description', required=True, translate=True)
active = fields.Boolean('Active', default=True)
|