# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import warnings from datetime import datetime, timedelta from odoo import http, _ from odoo.addons.http_routing.models.ir_http import slug from odoo.osv.expression import AND from odoo.http import request from odoo.tools.misc import groupby class WebsiteHrRecruitment(http.Controller): _jobs_per_page = 12 def sitemap_jobs(env, rule, qs): if not qs or qs.lower() in '/jobs': yield {'loc': '/jobs'} @http.route([ '/jobs', '/jobs/page/', ], type='http', auth="public", website=True, sitemap=sitemap_jobs) def jobs(self, country_id=None, department_id=None, office_id=None, contract_type_id=None, is_remote=False, is_other_department=False, is_untyped=None, page=1, search=None, **kwargs): env = request.env(context=dict(request.env.context, show_address=True, no_tag_br=True)) Country = env['res.country'] Jobs = env['hr.job'] Department = env['hr.department'] country = Country.browse(int(country_id)) if country_id else None department = Department.browse(int(department_id)) if department_id else None office_id = int(office_id) if office_id else None contract_type_id = int(contract_type_id) if contract_type_id else None # Default search by user country if not (country or department or office_id or contract_type_id or kwargs.get('all_countries')): if request.geoip.country_code: countries_ = Country.search([('code', '=', request.geoip.country_code)]) country = countries_[0] if countries_ else None if country: country_count = Jobs.search_count(AND([ request.website.website_domain(), [('address_id.country_id', '=', country.id)] ])) if not country_count: country = False options = { 'displayDescription': True, 'allowFuzzy': not request.params.get('noFuzzy'), 'country_id': country.id if country else None, 'department_id': department.id if department else None, 'office_id': office_id, 'contract_type_id': contract_type_id, 'is_remote': is_remote, 'is_other_department': is_other_department, 'is_untyped': is_untyped, } total, details, fuzzy_search_term = request.website._search_with_fuzzy("jobs", search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc", options=options) # Browse jobs as superuser, because address is restricted jobs = details[0].get('results', Jobs).sudo() def sort(records_list, field_name): """ Sort records in the given collection according to the given field name, alphabetically. None values instead of records are placed at the end. :param list records_list: collection of records or None values :param str field_name: field on which to sort :return: sorted list """ return sorted( records_list, key=lambda item: (item is None, item and item[field_name] or ''), ) # Countries if country or is_remote: cross_country_options = options.copy() cross_country_options.update({ 'allowFuzzy': False, 'country_id': None, 'is_remote': False, }) cross_country_total, cross_country_details, _ = request.website._search_with_fuzzy("jobs", fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc", options=cross_country_options) # Browse jobs as superuser, because address is restricted cross_country_jobs = cross_country_details[0].get('results', Jobs).sudo() else: cross_country_total = total cross_country_jobs = jobs country_offices = set(j.address_id or None for j in cross_country_jobs) countries = sort(set(o and o.country_id or None for o in country_offices), 'name') count_per_country = {'all': cross_country_total} for c, jobs_list in groupby(cross_country_jobs, lambda job: job.address_id.country_id): count_per_country[c] = len(jobs_list) count_remote = len(cross_country_jobs.filtered(lambda job: not job.address_id)) if count_remote: count_per_country[None] = count_remote # Departments if department or is_other_department: cross_department_options = options.copy() cross_department_options.update({ 'allowFuzzy': False, 'department_id': None, 'is_other_department': False, }) cross_department_total, cross_department_details, _ = request.website._search_with_fuzzy("jobs", fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc", options=cross_department_options) cross_department_jobs = cross_department_details[0].get('results', Jobs) else: cross_department_total = total cross_department_jobs = jobs departments = sort(set(j.department_id or None for j in cross_department_jobs), 'name') count_per_department = {'all': cross_department_total} for d, jobs_list in groupby(cross_department_jobs, lambda job: job.department_id): count_per_department[d] = len(jobs_list) count_other_department = len(cross_department_jobs.filtered(lambda job: not job.department_id)) if count_other_department: count_per_department[None] = count_other_department # Offices if office_id or is_remote: cross_office_options = options.copy() cross_office_options.update({ 'allowFuzzy': False, 'office_id': None, 'is_remote': False, }) cross_office_total, cross_office_details, _ = request.website._search_with_fuzzy("jobs", fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc", options=cross_office_options) # Browse jobs as superuser, because address is restricted cross_office_jobs = cross_office_details[0].get('results', Jobs).sudo() else: cross_office_total = total cross_office_jobs = jobs offices = sort(set(j.address_id or None for j in cross_office_jobs), 'city') count_per_office = {'all': cross_office_total} for o, jobs_list in groupby(cross_office_jobs, lambda job: job.address_id): count_per_office[o] = len(jobs_list) count_remote = len(cross_office_jobs.filtered(lambda job: not job.address_id)) if count_remote: count_per_office[None] = count_remote # Employment types if contract_type_id or is_untyped: cross_type_options = options.copy() cross_type_options.update({ 'allowFuzzy': False, 'contract_type_id': None, 'is_untyped': False, }) cross_type_total, cross_type_details, _ = request.website._search_with_fuzzy("jobs", fuzzy_search_term or search, limit=1000, order="is_published desc, sequence, no_of_recruitment desc", options=cross_type_options) cross_type_jobs = cross_type_details[0].get('results', Jobs) else: cross_type_total = total cross_type_jobs = jobs employment_types = sort(set(j.contract_type_id for j in jobs if j.contract_type_id), 'name') count_per_employment_type = {'all': cross_type_total} for t, jobs_list in groupby(cross_type_jobs, lambda job: job.contract_type_id): count_per_employment_type[t] = len(jobs_list) count_untyped = len(cross_type_jobs.filtered(lambda job: not job.contract_type_id)) if count_untyped: count_per_employment_type[None] = count_untyped pager = request.website.pager( url=request.httprequest.path.partition('/page/')[0], url_args=request.httprequest.args, total=total, page=page, step=self._jobs_per_page, ) offset = pager['offset'] jobs = jobs[offset:offset + self._jobs_per_page] office = env['res.partner'].browse(int(office_id)) if office_id else None contract_type = env['hr.contract.type'].browse(int(contract_type_id)) if contract_type_id else None # Render page return request.render("website_hr_recruitment.index", { 'jobs': jobs, 'countries': countries, 'departments': departments, 'offices': offices, 'employment_types': employment_types, 'country_id': country, 'department_id': department, 'office_id': office, 'contract_type_id': contract_type, 'is_remote': is_remote, 'is_other_department': is_other_department, 'is_untyped': is_untyped, 'pager': pager, 'search': fuzzy_search_term or search, 'search_count': total, 'original_search': fuzzy_search_term and search, 'count_per_country': count_per_country, 'count_per_department': count_per_department, 'count_per_office': count_per_office, 'count_per_employment_type': count_per_employment_type, }) @http.route('/jobs/add', type='json', auth="user", website=True) def jobs_add(self, **kwargs): # avoid branding of website_description by setting rendering_bundle in context job = request.env['hr.job'].with_context(rendering_bundle=True).create({ 'name': _('Job Title'), }) return f"/jobs/{slug(job)}" @http.route('''/jobs/detail/''', type='http', auth="public", website=True, sitemap=True) def jobs_detail(self, job, **kwargs): redirect_url = f"/jobs/{slug(job)}" return request.redirect(redirect_url, code=301) @http.route('''/jobs/''', type='http', auth="public", website=True, sitemap=True) def job(self, job, **kwargs): return request.render("website_hr_recruitment.detail", { 'job': job, 'main_object': job, }) @http.route('''/jobs/apply/''', type='http', auth="public", website=True, sitemap=True) def jobs_apply(self, job, **kwargs): error = {} default = {} if 'website_hr_recruitment_error' in request.session: error = request.session.pop('website_hr_recruitment_error') default = request.session.pop('website_hr_recruitment_default') return request.render("website_hr_recruitment.apply", { 'job': job, 'error': error, 'default': default, }) # Compatibility routes @http.route([ '/jobs/country/', '/jobs/department/', '/jobs/country//department/', '/jobs/office/', '/jobs/country//office/', '/jobs/department//office/', '/jobs/country//department//office/', '/jobs/employment_type/', '/jobs/country//employment_type/', '/jobs/department//employment_type/', '/jobs/office//employment_type/', '/jobs/country//department//employment_type/', '/jobs/country//office//employment_type/', '/jobs/department//office//employment_type/', '/jobs/country//department//office//employment_type/', ], type='http', auth="public", website=True, sitemap=False) def jobs_compatibility(self, country=None, department=None, office_id=None, contract_type_id=None, **kwargs): """ Deprecated since Odoo 16.3: those routes are kept by compatibility. They should not be used in Odoo code anymore. """ warnings.warn( "This route is deprecated since Odoo 16.3: the jobs list is now available at /jobs or /jobs/page/XXX", DeprecationWarning ) return self.jobs( country_id=country.id if country else None, department_id=department.id if department else None, office_id=office_id, contract_type_id=contract_type_id, **kwargs ) @http.route('/website_hr_recruitment/check_recent_application', type='json', auth="public") def check_recent_application(self, email, job_id): date_limit = datetime.now() - timedelta(days=90) domain = [('email_from', '=ilike', email), ('create_date', '>=', date_limit)] recent_applications = http.request.env['hr.applicant'].sudo().search(domain) response = {'applied_same_job': any(a.job_id.id == int(job_id) for a in recent_applications), 'applied_other_job': bool(recent_applications)} return response