summaryrefslogtreecommitdiff
path: root/addons/hr_recruitment/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/hr_recruitment/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/hr_recruitment/models')
-rw-r--r--addons/hr_recruitment/models/__init__.py7
-rw-r--r--addons/hr_recruitment/models/calendar.py39
-rw-r--r--addons/hr_recruitment/models/digest.py29
-rw-r--r--addons/hr_recruitment/models/hr_department.py32
-rw-r--r--addons/hr_recruitment/models/hr_employee.py34
-rw-r--r--addons/hr_recruitment/models/hr_job.py153
-rw-r--r--addons/hr_recruitment/models/hr_recruitment.py553
-rw-r--r--addons/hr_recruitment/models/res_config_settings.py11
8 files changed, 858 insertions, 0 deletions
diff --git a/addons/hr_recruitment/models/__init__.py b/addons/hr_recruitment/models/__init__.py
new file mode 100644
index 00000000..6eca3380
--- /dev/null
+++ b/addons/hr_recruitment/models/__init__.py
@@ -0,0 +1,7 @@
+from . import hr_department
+from . import hr_recruitment
+from . import hr_employee
+from . import hr_job
+from . import res_config_settings
+from . import calendar
+from . import digest
diff --git a/addons/hr_recruitment/models/calendar.py b/addons/hr_recruitment/models/calendar.py
new file mode 100644
index 00000000..d97ee566
--- /dev/null
+++ b/addons/hr_recruitment/models/calendar.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class CalendarEvent(models.Model):
+ """ Model for Calendar Event """
+ _inherit = 'calendar.event'
+
+ @api.model
+ def default_get(self, fields):
+ if self.env.context.get('default_applicant_id'):
+ self = self.with_context(
+ default_res_model='hr.applicant', #res_model seems to be lost without this
+ default_res_model_id=self.env.ref('hr_recruitment.model_hr_applicant').id,
+ default_res_id=self.env.context['default_applicant_id']
+ )
+
+ defaults = super(CalendarEvent, self).default_get(fields)
+
+ # sync res_model / res_id to opportunity id (aka creating meeting from lead chatter)
+ if 'applicant_id' not in defaults:
+ res_model = defaults.get('res_model', False) or self.env.context.get('default_res_model')
+ res_model_id = defaults.get('res_model_id', False) or self.env.context.get('default_res_model_id')
+ if (res_model and res_model == 'hr.applicant') or (res_model_id and self.env['ir.model'].sudo().browse(res_model_id).model == 'hr.applicant'):
+ defaults['applicant_id'] = defaults.get('res_id', False) or self.env.context.get('default_res_id', False)
+
+ return defaults
+
+ def _compute_is_highlighted(self):
+ super(CalendarEvent, self)._compute_is_highlighted()
+ applicant_id = self.env.context.get('active_id')
+ if self.env.context.get('active_model') == 'hr.applicant' and applicant_id:
+ for event in self:
+ if event.applicant_id.id == applicant_id:
+ event.is_highlighted = True
+
+ applicant_id = fields.Many2one('hr.applicant', string="Applicant", index=True, ondelete='set null')
diff --git a/addons/hr_recruitment/models/digest.py b/addons/hr_recruitment/models/digest.py
new file mode 100644
index 00000000..c8d17019
--- /dev/null
+++ b/addons/hr_recruitment/models/digest.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models, _
+from odoo.exceptions import AccessError
+
+
+class Digest(models.Model):
+ _inherit = 'digest.digest'
+
+ kpi_hr_recruitment_new_colleagues = fields.Boolean('Employees')
+ kpi_hr_recruitment_new_colleagues_value = fields.Integer(compute='_compute_kpi_hr_recruitment_new_colleagues_value')
+
+ def _compute_kpi_hr_recruitment_new_colleagues_value(self):
+ if not self.env.user.has_group('hr_recruitment.group_hr_recruitment_user'):
+ raise AccessError(_("Do not have access, skip this data for user's digest email"))
+ for record in self:
+ start, end, company = record._get_kpi_compute_parameters()
+ new_colleagues = self.env['hr.employee'].search_count([
+ ('create_date', '>=', start),
+ ('create_date', '<', end),
+ ('company_id', '=', company.id)
+ ])
+ record.kpi_hr_recruitment_new_colleagues_value = new_colleagues
+
+ def _compute_kpis_actions(self, company, user):
+ res = super(Digest, self)._compute_kpis_actions(company, user)
+ res['kpi_hr_recruitment_new_colleagues'] = 'hr.open_view_employee_list_my&menu_id=%s' % self.env.ref('hr.menu_hr_root').id
+ return res
diff --git a/addons/hr_recruitment/models/hr_department.py b/addons/hr_recruitment/models/hr_department.py
new file mode 100644
index 00000000..66db1ca3
--- /dev/null
+++ b/addons/hr_recruitment/models/hr_department.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+
+from odoo import api, fields, models
+
+
+class HrDepartment(models.Model):
+ _inherit = 'hr.department'
+
+ new_applicant_count = fields.Integer(
+ compute='_compute_new_applicant_count', string='New Applicant')
+ new_hired_employee = fields.Integer(
+ compute='_compute_recruitment_stats', string='New Hired Employee')
+ expected_employee = fields.Integer(
+ compute='_compute_recruitment_stats', string='Expected Employee')
+
+ def _compute_new_applicant_count(self):
+ applicant_data = self.env['hr.applicant'].read_group(
+ [('department_id', 'in', self.ids), ('stage_id.sequence', '<=', '1')],
+ ['department_id'], ['department_id'])
+ result = dict((data['department_id'][0], data['department_id_count']) for data in applicant_data)
+ for department in self:
+ department.new_applicant_count = result.get(department.id, 0)
+
+ def _compute_recruitment_stats(self):
+ job_data = self.env['hr.job'].read_group(
+ [('department_id', 'in', self.ids)],
+ ['no_of_hired_employee', 'no_of_recruitment', 'department_id'], ['department_id'])
+ new_emp = dict((data['department_id'][0], data['no_of_hired_employee']) for data in job_data)
+ expected_emp = dict((data['department_id'][0], data['no_of_recruitment']) for data in job_data)
+ for department in self:
+ department.new_hired_employee = new_emp.get(department.id, 0)
+ department.expected_employee = expected_emp.get(department.id, 0)
diff --git a/addons/hr_recruitment/models/hr_employee.py b/addons/hr_recruitment/models/hr_employee.py
new file mode 100644
index 00000000..8640872d
--- /dev/null
+++ b/addons/hr_recruitment/models/hr_employee.py
@@ -0,0 +1,34 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from odoo.tools.translate import _
+from datetime import timedelta
+
+
+class HrEmployee(models.Model):
+ _inherit = "hr.employee"
+
+ newly_hired_employee = fields.Boolean('Newly hired employee', compute='_compute_newly_hired_employee',
+ search='_search_newly_hired_employee')
+ applicant_id = fields.One2many('hr.applicant', 'emp_id', 'Applicant')
+
+ def _compute_newly_hired_employee(self):
+ now = fields.Datetime.now()
+ for employee in self:
+ employee.newly_hired_employee = bool(employee.create_date > (now - timedelta(days=90)))
+
+ def _search_newly_hired_employee(self, operator, value):
+ employees = self.env['hr.employee'].search([
+ ('create_date', '>', fields.Datetime.now() - timedelta(days=90))
+ ])
+ return [('id', 'in', employees.ids)]
+
+ @api.model
+ def create(self, vals):
+ new_employee = super(HrEmployee, self).create(vals)
+ if new_employee.applicant_id:
+ new_employee.applicant_id.message_post_with_view(
+ 'hr_recruitment.applicant_hired_template',
+ values={'applicant': new_employee.applicant_id},
+ subtype_id=self.env.ref("hr_recruitment.mt_applicant_hired").id)
+ return new_employee
diff --git a/addons/hr_recruitment/models/hr_job.py b/addons/hr_recruitment/models/hr_job.py
new file mode 100644
index 00000000..2d446612
--- /dev/null
+++ b/addons/hr_recruitment/models/hr_job.py
@@ -0,0 +1,153 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import ast
+
+from odoo import api, fields, models, _
+
+
+class Job(models.Model):
+ _name = "hr.job"
+ _inherit = ["mail.alias.mixin", "hr.job"]
+ _order = "state desc, name asc"
+
+ @api.model
+ def _default_address_id(self):
+ return self.env.company.partner_id
+
+ def _get_default_favorite_user_ids(self):
+ return [(6, 0, [self.env.uid])]
+
+ address_id = fields.Many2one(
+ 'res.partner', "Job Location", default=_default_address_id,
+ domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
+ help="Address where employees are working")
+ application_ids = fields.One2many('hr.applicant', 'job_id', "Applications")
+ application_count = fields.Integer(compute='_compute_application_count', string="Application Count")
+ all_application_count = fields.Integer(compute='_compute_all_application_count', string="All Application Count")
+ new_application_count = fields.Integer(
+ compute='_compute_new_application_count', string="New Application",
+ help="Number of applications that are new in the flow (typically at first step of the flow)")
+ manager_id = fields.Many2one(
+ 'hr.employee', related='department_id.manager_id', string="Department Manager",
+ readonly=True, store=True)
+ user_id = fields.Many2one('res.users', "Recruiter", tracking=True)
+ hr_responsible_id = fields.Many2one(
+ 'res.users', "HR Responsible", tracking=True,
+ help="Person responsible of validating the employee's contracts.")
+ document_ids = fields.One2many('ir.attachment', compute='_compute_document_ids', string="Documents")
+ documents_count = fields.Integer(compute='_compute_document_ids', string="Document Count")
+ alias_id = fields.Many2one(
+ 'mail.alias', "Alias", ondelete="restrict", required=True,
+ help="Email alias for this job position. New emails will automatically create new applicants for this job position.")
+ color = fields.Integer("Color Index")
+ is_favorite = fields.Boolean(compute='_compute_is_favorite', inverse='_inverse_is_favorite')
+ favorite_user_ids = fields.Many2many('res.users', 'job_favorite_user_rel', 'job_id', 'user_id', default=_get_default_favorite_user_ids)
+
+ def _compute_is_favorite(self):
+ for job in self:
+ job.is_favorite = self.env.user in job.favorite_user_ids
+
+ def _inverse_is_favorite(self):
+ unfavorited_jobs = favorited_jobs = self.env['hr.job']
+ for job in self:
+ if self.env.user in job.favorite_user_ids:
+ unfavorited_jobs |= job
+ else:
+ favorited_jobs |= job
+ favorited_jobs.write({'favorite_user_ids': [(4, self.env.uid)]})
+ unfavorited_jobs.write({'favorite_user_ids': [(3, self.env.uid)]})
+
+ def _compute_document_ids(self):
+ applicants = self.mapped('application_ids').filtered(lambda self: not self.emp_id)
+ app_to_job = dict((applicant.id, applicant.job_id.id) for applicant in applicants)
+ attachments = self.env['ir.attachment'].search([
+ '|',
+ '&', ('res_model', '=', 'hr.job'), ('res_id', 'in', self.ids),
+ '&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', applicants.ids)])
+ result = dict.fromkeys(self.ids, self.env['ir.attachment'])
+ for attachment in attachments:
+ if attachment.res_model == 'hr.applicant':
+ result[app_to_job[attachment.res_id]] |= attachment
+ else:
+ result[attachment.res_id] |= attachment
+
+ for job in self:
+ job.document_ids = result.get(job.id, False)
+ job.documents_count = len(job.document_ids)
+
+ def _compute_all_application_count(self):
+ read_group_result = self.env['hr.applicant'].with_context(active_test=False).read_group([('job_id', 'in', self.ids)], ['job_id'], ['job_id'])
+ result = dict((data['job_id'][0], data['job_id_count']) for data in read_group_result)
+ for job in self:
+ job.all_application_count = result.get(job.id, 0)
+
+ def _compute_application_count(self):
+ read_group_result = self.env['hr.applicant'].read_group([('job_id', 'in', self.ids)], ['job_id'], ['job_id'])
+ result = dict((data['job_id'][0], data['job_id_count']) for data in read_group_result)
+ for job in self:
+ job.application_count = result.get(job.id, 0)
+
+ def _get_first_stage(self):
+ self.ensure_one()
+ return self.env['hr.recruitment.stage'].search([
+ '|',
+ ('job_ids', '=', False),
+ ('job_ids', '=', self.id)], order='sequence asc', limit=1)
+
+ def _compute_new_application_count(self):
+ for job in self:
+ job.new_application_count = self.env["hr.applicant"].search_count(
+ [("job_id", "=", job.id), ("stage_id", "=", job._get_first_stage().id)]
+ )
+
+ def _alias_get_creation_values(self):
+ values = super(Job, self)._alias_get_creation_values()
+ values['alias_model_id'] = self.env['ir.model']._get('hr.applicant').id
+ if self.id:
+ values['alias_defaults'] = defaults = ast.literal_eval(self.alias_defaults or "{}")
+ defaults.update({
+ 'job_id': self.id,
+ 'department_id': self.department_id.id,
+ 'company_id': self.department_id.company_id.id if self.department_id else self.company_id.id,
+ })
+ return values
+
+ @api.model
+ def create(self, vals):
+ vals['favorite_user_ids'] = vals.get('favorite_user_ids', []) + [(4, self.env.uid)]
+ new_job = super(Job, self).create(vals)
+ utm_linkedin = self.env.ref("utm.utm_source_linkedin", raise_if_not_found=False)
+ if utm_linkedin:
+ source_vals = {
+ 'source_id': utm_linkedin.id,
+ 'job_id': new_job.id,
+ }
+ self.env['hr.recruitment.source'].create(source_vals)
+ return new_job
+
+ def _creation_subtype(self):
+ return self.env.ref('hr_recruitment.mt_job_new')
+
+ def action_get_attachment_tree_view(self):
+ action = self.env["ir.actions.actions"]._for_xml_id("base.action_attachment")
+ action['context'] = {
+ 'default_res_model': self._name,
+ 'default_res_id': self.ids[0]
+ }
+ action['search_view_id'] = (self.env.ref('hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment').id, )
+ action['domain'] = ['|', '&', ('res_model', '=', 'hr.job'), ('res_id', 'in', self.ids), '&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.mapped('application_ids').ids)]
+ return action
+
+ def close_dialog(self):
+ return {'type': 'ir.actions.act_window_close'}
+
+ def edit_dialog(self):
+ form_view = self.env.ref('hr.view_hr_job_form')
+ return {
+ 'name': _('Job'),
+ 'res_model': 'hr.job',
+ 'res_id': self.id,
+ 'views': [(form_view.id, 'form'),],
+ 'type': 'ir.actions.act_window',
+ 'target': 'inline'
+ }
diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py
new file mode 100644
index 00000000..2e2fdbe5
--- /dev/null
+++ b/addons/hr_recruitment/models/hr_recruitment.py
@@ -0,0 +1,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)
diff --git a/addons/hr_recruitment/models/res_config_settings.py b/addons/hr_recruitment/models/res_config_settings.py
new file mode 100644
index 00000000..12b361a9
--- /dev/null
+++ b/addons/hr_recruitment/models/res_config_settings.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = ['res.config.settings']
+
+ module_website_hr_recruitment = fields.Boolean(string='Online Posting')
+ module_hr_recruitment_survey = fields.Boolean(string='Interview Forms')