hr_job.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. import ast
  3. from collections import defaultdict
  4. from odoo import api, fields, models, SUPERUSER_ID, _
  5. class Job(models.Model):
  6. _name = "hr.job"
  7. _inherit = ["mail.alias.mixin", "hr.job"]
  8. _order = "sequence, name asc"
  9. @api.model
  10. def _default_address_id(self):
  11. last_used_address = self.env['hr.job'].search([('company_id', 'in', self.env.companies.ids)], order='id desc', limit=1)
  12. if last_used_address:
  13. return last_used_address.address_id
  14. else:
  15. return self.env.company.partner_id
  16. def _address_id_domain(self):
  17. return ['|', '&', '&', ('type', '!=', 'contact'), ('type', '!=', 'private'),
  18. ('id', 'in', self.sudo().env.companies.partner_id.child_ids.ids),
  19. ('id', 'in', self.sudo().env.companies.partner_id.ids)]
  20. def _get_default_favorite_user_ids(self):
  21. return [(6, 0, [self.env.uid])]
  22. address_id = fields.Many2one(
  23. 'res.partner', "Job Location", default=_default_address_id,
  24. domain=lambda self: self._address_id_domain(),
  25. help="Address where employees are working")
  26. application_ids = fields.One2many('hr.applicant', 'job_id', "Job Applications")
  27. application_count = fields.Integer(compute='_compute_application_count', string="Application Count")
  28. all_application_count = fields.Integer(compute='_compute_all_application_count', string="All Application Count")
  29. new_application_count = fields.Integer(
  30. compute='_compute_new_application_count', string="New Application",
  31. help="Number of applications that are new in the flow (typically at first step of the flow)")
  32. old_application_count = fields.Integer(
  33. compute='_compute_old_application_count', string="Old Application")
  34. applicant_hired = fields.Integer(compute='_compute_applicant_hired', string="Applicants Hired")
  35. manager_id = fields.Many2one(
  36. 'hr.employee', related='department_id.manager_id', string="Department Manager",
  37. readonly=True, store=True)
  38. user_id = fields.Many2one('res.users', "Recruiter", domain="[('share', '=', False), ('company_ids', 'in', company_id)]", tracking=True, help="The Recruiter will be the default value for all Applicants Recruiter's field in this job position. The Recruiter is automatically added to all meetings with the Applicant.")
  39. hr_responsible_id = fields.Many2one(
  40. 'res.users', "HR Responsible", tracking=True,
  41. help="Person responsible of validating the employee's contracts.")
  42. document_ids = fields.One2many('ir.attachment', compute='_compute_document_ids', string="Documents", readonly=True)
  43. documents_count = fields.Integer(compute='_compute_document_ids', string="Document Count")
  44. alias_id = fields.Many2one(
  45. 'mail.alias', "Alias", ondelete="restrict", required=True,
  46. help="Email alias for this job position. New emails will automatically create new applicants for this job position.")
  47. color = fields.Integer("Color Index")
  48. is_favorite = fields.Boolean(compute='_compute_is_favorite', inverse='_inverse_is_favorite')
  49. favorite_user_ids = fields.Many2many('res.users', 'job_favorite_user_rel', 'job_id', 'user_id', default=_get_default_favorite_user_ids)
  50. interviewer_ids = fields.Many2many('res.users', string='Interviewers', domain="[('share', '=', False), ('company_ids', 'in', company_id)]", help="The Interviewers set on the job position can see all Applicants in it. They have access to the information, the attachments, the meeting management and they can refuse him. You don't need to have Recruitment rights to be set as an interviewer.")
  51. extended_interviewer_ids = fields.Many2many('res.users', 'hr_job_extended_interviewer_res_users', compute='_compute_extended_interviewer_ids', store=True)
  52. activities_overdue = fields.Integer(compute='_compute_activities')
  53. activities_today = fields.Integer(compute='_compute_activities')
  54. @api.depends_context('uid')
  55. def _compute_activities(self):
  56. self.env.cr.execute("""
  57. SELECT
  58. app.job_id,
  59. COUNT(*) AS act_count,
  60. CASE
  61. WHEN %(today)s::date - act.date_deadline::date = 0 THEN 'today'
  62. WHEN %(today)s::date - act.date_deadline::date > 0 THEN 'overdue'
  63. END AS act_state
  64. FROM mail_activity act
  65. JOIN hr_applicant app ON app.id = act.res_id
  66. JOIN hr_recruitment_stage sta ON app.stage_id = sta.id
  67. WHERE act.user_id = %(user_id)s AND act.res_model = 'hr.applicant'
  68. AND act.date_deadline <= %(today)s::date AND app.active
  69. AND app.job_id IN %(job_ids)s
  70. AND sta.hired_stage IS NOT TRUE
  71. GROUP BY app.job_id, act_state
  72. """, {
  73. 'today': fields.Date.context_today(self),
  74. 'user_id': self.env.uid,
  75. 'job_ids': tuple(self.ids),
  76. })
  77. job_activities = defaultdict(dict)
  78. for activity in self.env.cr.dictfetchall():
  79. job_activities[activity['job_id']][activity['act_state']] = activity['act_count']
  80. for job in self:
  81. job.activities_overdue = job_activities[job.id].get('overdue', 0)
  82. job.activities_today = job_activities[job.id].get('today', 0)
  83. @api.depends('application_ids.interviewer_ids')
  84. def _compute_extended_interviewer_ids(self):
  85. # Use SUPERUSER_ID as the search_read is protected in hr_referral
  86. results_raw = self.env['hr.applicant'].with_user(SUPERUSER_ID).search_read([
  87. ('job_id', 'in', self.ids),
  88. ('interviewer_ids', '!=', False)
  89. ], ['interviewer_ids', 'job_id'])
  90. interviewers_by_job = defaultdict(set)
  91. for result_raw in results_raw:
  92. interviewers_by_job[result_raw['job_id'][0]] |= set(result_raw['interviewer_ids'])
  93. for job in self:
  94. job.extended_interviewer_ids = [(6, 0, list(interviewers_by_job[job.id]))]
  95. def _compute_is_favorite(self):
  96. for job in self:
  97. job.is_favorite = self.env.user in job.favorite_user_ids
  98. def _inverse_is_favorite(self):
  99. unfavorited_jobs = favorited_jobs = self.env['hr.job']
  100. for job in self:
  101. if self.env.user in job.favorite_user_ids:
  102. unfavorited_jobs |= job
  103. else:
  104. favorited_jobs |= job
  105. favorited_jobs.write({'favorite_user_ids': [(4, self.env.uid)]})
  106. unfavorited_jobs.write({'favorite_user_ids': [(3, self.env.uid)]})
  107. def _compute_document_ids(self):
  108. applicants = self.mapped('application_ids').filtered(lambda self: not self.emp_id)
  109. app_to_job = dict((applicant.id, applicant.job_id.id) for applicant in applicants)
  110. attachments = self.env['ir.attachment'].search([
  111. '|',
  112. '&', ('res_model', '=', 'hr.job'), ('res_id', 'in', self.ids),
  113. '&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', applicants.ids)])
  114. result = dict.fromkeys(self.ids, self.env['ir.attachment'])
  115. for attachment in attachments:
  116. if attachment.res_model == 'hr.applicant':
  117. result[app_to_job[attachment.res_id]] |= attachment
  118. else:
  119. result[attachment.res_id] |= attachment
  120. for job in self:
  121. job.document_ids = result.get(job.id, False)
  122. job.documents_count = len(job.document_ids)
  123. def _compute_all_application_count(self):
  124. read_group_result = self.env['hr.applicant'].with_context(active_test=False)._read_group([
  125. ('job_id', 'in', self.ids),
  126. '|',
  127. ('active', '=', True),
  128. '&',
  129. ('active', '=', False), ('refuse_reason_id', '!=', False),
  130. ], ['job_id'], ['job_id'])
  131. result = dict((data['job_id'][0], data['job_id_count']) for data in read_group_result)
  132. for job in self:
  133. job.all_application_count = result.get(job.id, 0)
  134. def _compute_application_count(self):
  135. read_group_result = self.env['hr.applicant']._read_group([('job_id', 'in', self.ids)], ['job_id'], ['job_id'])
  136. result = dict((data['job_id'][0], data['job_id_count']) for data in read_group_result)
  137. for job in self:
  138. job.application_count = result.get(job.id, 0)
  139. def _get_first_stage(self):
  140. self.ensure_one()
  141. return self.env['hr.recruitment.stage'].search([
  142. '|',
  143. ('job_ids', '=', False),
  144. ('job_ids', '=', self.id)], order='sequence asc', limit=1)
  145. def _compute_new_application_count(self):
  146. self.env.cr.execute(
  147. """
  148. WITH job_stage AS (
  149. SELECT DISTINCT ON (j.id) j.id AS job_id, s.id AS stage_id, s.sequence AS sequence
  150. FROM hr_job j
  151. LEFT JOIN hr_job_hr_recruitment_stage_rel rel
  152. ON rel.hr_job_id = j.id
  153. JOIN hr_recruitment_stage s
  154. ON s.id = rel.hr_recruitment_stage_id
  155. OR s.id NOT IN (
  156. SELECT "hr_recruitment_stage_id"
  157. FROM "hr_job_hr_recruitment_stage_rel"
  158. WHERE "hr_recruitment_stage_id" IS NOT NULL
  159. )
  160. WHERE j.id in %s
  161. ORDER BY 1, 3 asc
  162. )
  163. SELECT s.job_id, COUNT(a.id) AS new_applicant
  164. FROM hr_applicant a
  165. JOIN job_stage s
  166. ON s.job_id = a.job_id
  167. AND a.stage_id = s.stage_id
  168. AND a.active IS TRUE
  169. WHERE a.company_id in %s
  170. GROUP BY s.job_id
  171. """, [tuple(self.ids), tuple(self.env.companies.ids)]
  172. )
  173. new_applicant_count = dict(self.env.cr.fetchall())
  174. for job in self:
  175. job.new_application_count = new_applicant_count.get(job.id, 0)
  176. def _compute_applicant_hired(self):
  177. hired_stages = self.env['hr.recruitment.stage'].search([('hired_stage', '=', True)])
  178. hired_data = self.env['hr.applicant']._read_group([
  179. ('job_id', 'in', self.ids),
  180. ('stage_id', 'in', hired_stages.ids),
  181. ], ['job_id'], ['job_id'])
  182. job_hires = {data['job_id'][0]: data['job_id_count'] for data in hired_data}
  183. for job in self:
  184. job.applicant_hired = job_hires.get(job.id, 0)
  185. @api.depends('application_count', 'new_application_count')
  186. def _compute_old_application_count(self):
  187. for job in self:
  188. job.old_application_count = job.application_count - job.new_application_count
  189. def _alias_get_creation_values(self):
  190. values = super(Job, self)._alias_get_creation_values()
  191. values['alias_model_id'] = self.env['ir.model']._get('hr.applicant').id
  192. if self.id:
  193. values['alias_defaults'] = defaults = ast.literal_eval(self.alias_defaults or "{}")
  194. defaults.update({
  195. 'job_id': self.id,
  196. 'department_id': self.department_id.id,
  197. 'company_id': self.department_id.company_id.id if self.department_id else self.company_id.id,
  198. 'user_id': self.user_id.id,
  199. })
  200. return values
  201. @api.model_create_multi
  202. def create(self, vals_list):
  203. for vals in vals_list:
  204. vals['favorite_user_ids'] = vals.get('favorite_user_ids', []) + [(4, self.env.uid)]
  205. if vals.get('alias_name'):
  206. vals['alias_user_id'] = False
  207. jobs = super().create(vals_list)
  208. utm_linkedin = self.env.ref("utm.utm_source_linkedin", raise_if_not_found=False)
  209. if utm_linkedin:
  210. source_vals = [{
  211. 'source_id': utm_linkedin.id,
  212. 'job_id': job.id,
  213. } for job in jobs]
  214. self.env['hr.recruitment.source'].create(source_vals)
  215. jobs.sudo().interviewer_ids._create_recruitment_interviewers()
  216. return jobs
  217. def write(self, vals):
  218. old_interviewers = self.interviewer_ids
  219. if 'active' in vals and not vals['active']:
  220. self.application_ids.active = False
  221. res = super().write(vals)
  222. if 'interviewer_ids' in vals:
  223. interviewers_to_clean = old_interviewers - self.interviewer_ids
  224. interviewers_to_clean._remove_recruitment_interviewers()
  225. self.sudo().interviewer_ids._create_recruitment_interviewers()
  226. # Since the alias is created upon record creation, the default values do not reflect the current values unless
  227. # specifically rewritten
  228. # List of fields to keep synched with the alias
  229. alias_fields = {'department_id', 'user_id'}
  230. if any(field for field in alias_fields if field in vals):
  231. for job in self:
  232. alias_default_vals = job._alias_get_creation_values().get('alias_defaults', '{}')
  233. job.alias_defaults = alias_default_vals
  234. return res
  235. def _creation_subtype(self):
  236. return self.env.ref('hr_recruitment.mt_job_new')
  237. def action_open_attachments(self):
  238. return {
  239. 'type': 'ir.actions.act_window',
  240. 'res_model': 'ir.attachment',
  241. 'name': _('Documents'),
  242. 'context': {
  243. 'default_res_model': self._name,
  244. 'default_res_id': self.ids[0],
  245. 'show_partner_name': 1,
  246. },
  247. 'view_mode': 'tree',
  248. 'views': [
  249. (self.env.ref('hr_recruitment.ir_attachment_hr_recruitment_list_view').id, 'tree')
  250. ],
  251. 'search_view_id': self.env.ref('hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment').ids,
  252. 'domain': ['|',
  253. '&', ('res_model', '=', 'hr.job'), ('res_id', 'in', self.ids),
  254. '&', ('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.application_ids.ids),
  255. ],
  256. }
  257. def action_open_activities(self):
  258. action = self.env["ir.actions.actions"]._for_xml_id("hr_recruitment.action_hr_job_applications")
  259. views = ['activity'] + [view for view in action['view_mode'].split(',') if view != 'activity']
  260. action['view_mode'] = ','.join(views)
  261. action['views'] = [(False, view) for view in views]
  262. return action
  263. def action_open_late_activities(self):
  264. action = self.action_open_activities()
  265. action['context'] = {
  266. 'default_job_id': self.id,
  267. 'search_default_job_id': self.id,
  268. 'search_default_activities_overdue': True,
  269. 'search_default_running_applicant_activities': True,
  270. }
  271. return action
  272. def action_open_today_activities(self):
  273. action = self.action_open_activities()
  274. action['context'] = {
  275. 'default_job_id': self.id,
  276. 'search_default_job_id': self.id,
  277. 'search_default_activities_today': True,
  278. }
  279. return action
  280. def close_dialog(self):
  281. return {'type': 'ir.actions.act_window_close'}
  282. def edit_dialog(self):
  283. form_view = self.env.ref('hr.view_hr_job_form')
  284. return {
  285. 'name': _('Job'),
  286. 'res_model': 'hr.job',
  287. 'res_id': self.id,
  288. 'views': [(form_view.id, 'form'),],
  289. 'type': 'ir.actions.act_window',
  290. 'target': 'inline'
  291. }