Your Recruitment Process Lives in 6 Spreadsheets and 3 Inboxes. That Ends Today.
Here is the pattern we see at every mid-market company before they adopt a proper ATS: job descriptions live in Google Docs, resumes arrive in a shared inbox that three people monitor (and two ignore), interview feedback sits in Slack threads that nobody can search, and the "pipeline" is an Excel sheet updated once a week by whoever remembers. When a hiring manager asks "how many candidates are in stage 2 for the senior dev role?", the answer takes 20 minutes and is wrong.
Odoo 19's Recruitment module replaces this chaos with a single, integrated hiring pipeline that connects directly to your HR employee records, payroll configuration, and company website. This guide walks through the complete setup: configuring job positions, publishing online postings, tracking applicants through a Kanban pipeline, scheduling interviews with calendar integration, sending offer letters, and converting hired candidates into HR employees — all without leaving Odoo.
How to Configure Job Positions and Departments in Odoo 19 Recruitment
Job positions are the foundation of the recruitment module. Each position defines a role, links to a department, sets the expected number of hires, and determines the recruitment pipeline stages. Before posting anything externally, you need to get this structure right.
Step 1: Enable the Recruitment Module
Navigate to Apps and install the hr_recruitment module. This also pulls in hr (Employees) and calendar (for interview scheduling) as dependencies.
{
'name': 'Custom Recruitment Extensions',
'version': '19.0.1.0.0',
'depends': ['hr_recruitment', 'website_hr_recruitment'],
'data': [
'security/ir.model.access.csv',
'views/hr_applicant_views.xml',
],
'license': 'LGPL-3',
}Step 2: Create Department Structure
Go to Recruitment → Configuration → Departments. Each department should have a manager assigned — this person will be the default responsible for recruitment in that department. The hierarchy matters because interview scheduling and approval workflows reference it.
<odoo>
<data noupdate="1">
<record id="dept_engineering" model="hr.department">
<field name="name">Engineering</field>
<field name="manager_id" ref="base.user_admin"/>
</record>
<record id="dept_product" model="hr.department">
<field name="name">Product</field>
<field name="parent_id" ref="dept_engineering"/>
</record>
</data>
</odoo>Step 3: Create Job Positions
Navigate to Recruitment → Configuration → Job Positions. Each position represents a role you are actively (or will be) hiring for. Key fields:
| Field | Purpose | Best Practice |
|---|---|---|
| Job Position | Name displayed on careers page and in internal views | Use clear titles: "Senior Backend Developer" not "Dev III" |
| Department | Links to HR department hierarchy | Required for proper pipeline filtering |
| Expected New Employees | Target headcount for this position | Set to actual hiring target — pipeline reports use this as the denominator |
| Recruitment Responsible | HR person who owns this pipeline | Defaults to department manager; override for dedicated recruiters |
| Interview Form | Custom survey sent to interviewers for structured feedback | Create one per role type (technical, cultural, management) |
A Job Position is a recruitment-facing entity with a pipeline. A Job Title (on the employee record) is the internal label shown in the org chart. They can differ: the position "Senior Backend Developer (Python/Odoo)" recruits the candidate, but the employee record shows "Senior Developer". Keep position titles descriptive for search engines; keep job titles clean for internal use.
Publishing Job Postings on Your Odoo 19 Website and External Job Boards
The website_hr_recruitment module turns your Odoo website into a careers page. Each published job position gets a public URL, an application form, and automatic indexing in the recruitment pipeline.
Step 1: Install Website Recruitment
Install the website_hr_recruitment module from Apps. This adds a /jobs route to your website with a filterable list of open positions.
Step 2: Configure the Job Description
Open any job position and click "Go to Website". The website editor opens with a template for the job posting. You can edit the description, add requirements, benefits, and salary range using the website builder's drag-and-drop blocks.
<template id="custom_application_fields"
inherit_id="website_hr_recruitment.apply">
<xpath expr="//div[hasclass('o_website_hr_recruitment_form')]//div[@class='row'][last()]"
position="after">
<div class="row mb-3">
<div class="col-12">
<label for="x_portfolio_url" class="form-label">
Portfolio URL
</label>
<input type="url"
name="x_portfolio_url"
class="form-control"
placeholder="https://your-portfolio.com"/>
</div>
</div>
</xpath>
</template>Step 3: Publish and Manage Visibility
Each position has a "Published" toggle. When published, it appears on /jobs and accepts applications. When unpublished, existing applicants remain in the pipeline but new applications are blocked. Use the "Website Published" boolean in filters to track which positions are live.
# Example: Server action to push new postings to external API
# Triggered when a job position is published
for record in records:
if record.website_published:
payload = {
'title': record.name,
'department': record.department_id.name,
'description': record.description or '',
'location': record.address_id.city or 'Remote',
'apply_url': f'{{record.website_url}}/apply',
}
# POST to your job board aggregator API
import requests
requests.post(
'https://api.yourjobboard.com/v1/postings',
json=payload,
headers={'Authorization': f'Bearer {{env["ir.config_parameter"].sudo().get_param("jobboard.api_key")}}'},
timeout=30,
) Odoo 19 generates JobPosting structured data (JSON-LD) automatically for published positions when the website_hr_recruitment module is installed. Verify it with Google's Rich Results Test. If the structured data is missing salary or location info, override the _get_structured_data method on hr.job to include baseSalary and jobLocation fields — this dramatically improves visibility in Google for Jobs.
Applicant Tracking in Odoo 19: From Application to Interview in Under 5 Minutes
Every application — whether submitted through the website form, created manually, or imported via email alias — creates an hr.applicant record in the Kanban pipeline. Here's how to set up the tracking system so nothing falls through the cracks.
Step 1: Configure Email Aliases
Each job position can have a dedicated email alias (e.g., jobs-engineering@yourcompany.com). Any email sent to this alias automatically creates an applicant record with the email body as the description and attachments (resume, cover letter) linked to the record. Configure this in Recruitment → Configuration → Job Positions → [Position] → Email Alias.
# Server action: Auto-tag applicants based on source
# Model: hr.applicant | Trigger: On Creation
for applicant in records:
tags = []
# Tag by source channel
if applicant.source_id:
source_name = applicant.source_id.name.lower()
if 'linkedin' in source_name:
tags.append(env.ref('hr_recruitment.tag_linkedin'))
elif 'website' in source_name:
tags.append(env.ref('hr_recruitment.tag_website'))
# Tag by resume keywords (basic screening)
if applicant.description:
desc_lower = applicant.description.lower()
if 'odoo' in desc_lower or 'erp' in desc_lower:
tags.append(env.ref('my_module.tag_erp_experience'))
if 'python' in desc_lower:
tags.append(env.ref('my_module.tag_python'))
if tags:
applicant.categ_ids = [(4, t.id) for t in tags]Step 2: Configure Recruitment Stages
Stages define the columns in your Kanban pipeline. Odoo ships with defaults, but you should customize them to match your actual hiring process. Navigate to Recruitment → Configuration → Stages.
| Stage | Purpose | Automated Action | SLA Target |
|---|---|---|---|
| New | Application received, pending initial review | Auto-assign to recruiter via round-robin | <= 24 hours to first review |
| Initial Screening | Resume reviewed, phone screen scheduled | Send acknowledgement email template | <= 48 hours to phone screen |
| Technical Interview | Technical assessment or coding challenge | Notify hiring manager, create calendar event | <= 5 business days |
| Culture Fit | Team interview, values alignment | Send interview survey to panel | <= 3 business days after technical |
| Offer | Offer letter generated and sent | Generate offer from salary template | <= 2 business days after final interview |
| Hired | Offer accepted, employee record created | Create hr.employee, trigger onboarding | Same day as acceptance |
<odoo>
<data noupdate="1">
<record id="stage_new" model="hr.recruitment.stage">
<field name="name">New</field>
<field name="sequence">1</field>
<field name="fold">False</field>
<field name="template_id" ref="email_template_applicant_ack"/>
</record>
<record id="stage_screening" model="hr.recruitment.stage">
<field name="name">Initial Screening</field>
<field name="sequence">2</field>
<field name="requirements">Review resume. Schedule 15-min phone call.</field>
</record>
<record id="stage_technical" model="hr.recruitment.stage">
<field name="name">Technical Interview</field>
<field name="sequence">3</field>
<field name="requirements">Coding challenge completed and reviewed.</field>
</record>
<record id="stage_culture" model="hr.recruitment.stage">
<field name="name">Culture Fit</field>
<field name="sequence">4</field>
</record>
<record id="stage_offer" model="hr.recruitment.stage">
<field name="name">Offer</field>
<field name="sequence">5</field>
</record>
<record id="stage_hired" model="hr.recruitment.stage">
<field name="name">Hired</field>
<field name="sequence">6</field>
<field name="fold">True</field>
<field name="hired_stage">True</field>
</record>
</data>
</odoo>Interview Scheduling with Odoo 19 Calendar Integration
Odoo 19 integrates the recruitment module with the Calendar app so interviews are first-class calendar events — not email threads asking "does 3 PM Tuesday work for everyone?" The interviewer, the candidate (via email), and the recruiter all see the event on their calendars.
Step 1: Schedule Directly from the Applicant Record
Open any applicant and click "Schedule Interview". This opens a calendar event form pre-filled with the applicant name, the job position, and the responsible recruiter. Add interviewers as attendees, set the duration, and optionally include a video call link.
from odoo import models, fields, api
class HrApplicant(models.Model):
_inherit = 'hr.applicant'
def action_schedule_interview(self):
"""Override to auto-attach video meeting link."""
action = super().action_schedule_interview()
# Pre-fill context with default values
ctx = action.get('context', {})
ctx.update({
'default_videocall_location': self._generate_meet_url(),
'default_duration': 1.0, # 1 hour default
'default_partner_ids': [
(4, self.partner_id.id), # Candidate
(4, self.user_id.partner_id.id), # Recruiter
],
})
action['context'] = ctx
return action
def _generate_meet_url(self):
"""Generate a unique meeting URL."""
import uuid
base_url = self.env['ir.config_parameter'].sudo().get_param(
'recruitment.meet_base_url',
'https://meet.yourcompany.com',
)
return f'{{base_url}}/{{uuid.uuid4().hex[:12]}}'Step 2: Interview Feedback via Surveys
Odoo 19 supports interview evaluation forms built with the Survey module. Assign a survey template to each job position. After an interview, each panelist fills out the structured form — scores are aggregated on the applicant record for objective comparison.
class HrApplicant(models.Model):
_inherit = 'hr.applicant'
x_avg_interview_score = fields.Float(
string='Avg Interview Score',
compute='_compute_avg_score',
store=True,
)
@api.depends('survey_user_input_ids', 'survey_user_input_ids.scoring_percentage')
def _compute_avg_score(self):
for applicant in self:
inputs = applicant.survey_user_input_ids.filtered(
lambda i: i.state == 'done'
)
if inputs:
applicant.x_avg_interview_score = sum(
inputs.mapped('scoring_percentage')
) / len(inputs)
else:
applicant.x_avg_interview_score = 0.0Without structured forms, interview feedback devolves into "I liked them" or "not a culture fit" — subjective, legally risky, and impossible to compare across candidates. Odoo's survey integration forces interviewers to score specific competencies on a scale. This produces quantifiable, defensible hiring decisions and dramatically reduces unconscious bias. We configure every client's recruitment module with at least one scored survey per interview stage.
Mastering the Odoo 19 Recruitment Kanban: Filters, Colors, and Automation Rules
The Kanban view is where recruiters spend 80% of their time. Out of the box it works, but with a few customizations it becomes a real-time command center for your hiring pipeline.
Pipeline Metrics That Matter
Use Odoo's built-in reporting (Recruitment → Reporting) to track these KPIs:
| Metric | What It Measures | Healthy Benchmark | Odoo Field |
|---|---|---|---|
| Time to Hire | Days from application to offer accepted | <= 30 days for standard roles | date_closed - create_date |
| Stage Conversion Rate | % of applicants moving to next stage | Screen → Interview: 25-40% | Pipeline analysis report |
| Source Effectiveness | Which channels produce hires | Track cost-per-hire by source | source_id grouped by stage |
| Pipeline Velocity | Average days per stage | <= 5 days per stage | date_last_stage_update delta |
| Offer Acceptance Rate | % of offers that convert to hires | >= 85% | Offer stage → Hired stage ratio |
Automated Actions for Pipeline Hygiene
Stale applicants kill pipeline accuracy. Here are automated rules we configure for every client:
# Scheduled action: Run daily
# Model: hr.applicant
from datetime import timedelta
stale_threshold = fields.Date.today() - timedelta(days=14)
stale_applicants = env['hr.applicant'].search([
('stage_id.hired_stage', '=', False),
('stage_id.fold', '=', False),
('active', '=', True),
('date_last_stage_update', '<=', str(stale_threshold)),
])
for applicant in stale_applicants:
applicant.priority = '0' # Reset priority to flag review
applicant.message_post(
body='This applicant has been in the current stage for over 14 days. '
'Please review and take action.',
message_type='notification',
subtype_xmlid='mail.mt_note',
partner_ids=[applicant.user_id.partner_id.id],
)# Server action triggered when applicant is refused
# Model: hr.applicant | Trigger: On Stage Change (to refused)
for applicant in records:
if applicant.active is False: # Odoo archives on refuse
template = env.ref(
'hr_recruitment.email_template_applicant_refuse'
)
if template and applicant.partner_id.email:
template.send_mail(applicant.id, force_send=True)Use the priority stars on applicant cards to indicate urgency, not quality. One star = standard, two stars = urgent fill, three stars = critical role. Then use tags with colors for source tracking (green = referral, blue = website, orange = LinkedIn). This lets recruiters visually scan 50 cards and immediately spot urgent referrals that need attention.
Offer Letters, Employee Creation, and the Handoff to HR in Odoo 19
The final stretch of the recruitment pipeline is where most systems fall apart. The recruiter sends an offer in a Word doc, someone from HR manually creates the employee record two weeks later, and payroll discovers the new hire on their first day. Odoo 19 eliminates these handoff gaps.
Step 1: Generate Offer Letters from Templates
Configure offer templates in Recruitment → Configuration → Offer Templates. Each template uses Odoo's report engine with placeholders for salary, start date, job title, and department. When you move an applicant to the "Offer" stage, click "Generate Offer" to produce a PDF from the template.
<template id="report_offer_letter">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page">
<h2>Offer of Employment</h2>
<p>Dear <t t-out="o.partner_name"/>,</p>
<p>
We are pleased to offer you the position of
<strong><t t-out="o.job_id.name"/></strong>
in the <t t-out="o.department_id.name"/> department,
reporting to <t t-out="o.department_id.manager_id.name"/>.
</p>
<p>
<strong>Start Date:</strong>
<t t-out="o.x_start_date" t-options="{'widget': 'date'}"/>
</p>
<p>
<strong>Annual Salary:</strong>
<t t-out="o.salary_proposed"
t-options="{'widget': 'monetary',
'display_currency': o.company_id.currency_id}"/>
</p>
<p>
This offer is contingent upon successful completion of
background verification and reference checks.
</p>
<p>Please confirm your acceptance by
<t t-out="o.x_offer_deadline"
t-options="{'widget': 'date'}"/>.
</p>
</div>
</t>
</t>
</t>
</template>Step 2: Convert Applicant to Employee
When the candidate accepts, click "Create Employee" on the applicant record. Odoo 19 creates an hr.employee record and copies: name, email, phone, department, job position, job title, and the recruitment responsible as the employee's HR responsible. The applicant record links to the employee via the emp_id field for full audit trail.
class HrApplicant(models.Model):
_inherit = 'hr.applicant'
def create_employee_from_applicant(self):
"""Override to trigger onboarding plan after employee creation."""
result = super().create_employee_from_applicant()
for applicant in self:
if applicant.emp_id:
employee = applicant.emp_id
# Assign default onboarding plan
onboarding_plan = self.env.ref(
'my_module.default_onboarding_plan',
raise_if_not_found=False,
)
if onboarding_plan:
employee.sudo().write({
'plan_ids': [(4, onboarding_plan.id)],
})
# Launch onboarding activities
employee.sudo()._launch_plan(onboarding_plan)
return resultStep 3: The HR Handoff Checklist
Use Odoo's Onboarding Plans (configured in Employees → Configuration → Plans) to auto-generate activities for the new hire's first week: IT setup (email, laptop, VPN), payroll enrollment (tax forms, bank details), manager welcome (1:1, team norms, buddy assignment), compliance (handbook, NDA), and system access (Odoo user account, security groups).
Once the applicant converts to an employee, stop updating the applicant record. All subsequent data (contract, payroll, leaves, expenses) lives on the employee record. The applicant record becomes a historical artifact — useful for recruitment analytics but not for ongoing HR operations. We configure access rights so recruiters can read but not write to converted applicant records.
4 Recruitment Module Mistakes That Silently Sabotage Your Hiring Pipeline
Not Configuring Stage-Specific Email Templates
Odoo can send automated emails when an applicant moves between stages — but only if you assign an email template to each stage. Without this, candidates submit an application and hear nothing for two weeks. They assume you are not interested and accept another offer. Your pipeline shows 50 applicants in "Initial Screening" but half have already moved on.
At minimum, configure email templates for: Application Received (immediate acknowledgement), Interview Scheduled (with date/time/link), and Rejection (respectful, with timeline). The acknowledgement email alone reduces candidate drop-off by 30% in our experience.
Using a Single Pipeline for All Job Types
A senior engineering hire needs a coding challenge, system design review, and VP approval. A warehouse operator needs a skills test and a background check. If both go through the same 6-stage pipeline, warehouse applicants sit in "Technical Interview" and "Culture Fit" stages that don't apply to them, and recruiters waste time dragging cards past irrelevant columns.
Create department-specific stage sets. In Odoo 19, recruitment stages can be filtered by department using the department_ids many2many field on hr.recruitment.stage. Engineering sees 6 stages; Operations sees 4. Same module, different workflows.
Forgetting to Track Recruitment Sources
Without source tracking, you cannot answer the most important recruitment ROI question: "Which channel produces hires, and at what cost?" You spend $5,000/month on LinkedIn job slots and get 200 applications, but only 2 hires. Meanwhile, employee referrals produce 5 hires at zero ad spend. Without source data in Odoo, this insight is invisible.
Configure UTM sources for every channel: website, LinkedIn, Indeed, referrals, agencies, university fairs. Use source_id and medium_id on the applicant record. Add UTM parameters to external job post URLs so Odoo auto-captures the source on web applications. Run the Recruitment Analysis pivot report grouped by source and stage monthly.
No GDPR/Data Retention Policy for Applicant Records
Under GDPR (and similar regulations), you cannot keep applicant personal data indefinitely. A rejected candidate's resume, cover letter, and interview notes must be deleted or anonymized within a defined retention period — typically 6 to 24 months depending on jurisdiction. Odoo does not enforce this automatically. Your recruitment database grows forever, filled with personal data you have no legal basis to retain.
Create a scheduled action that runs monthly, finds refused/archived applicants older than your retention period, and anonymizes their records: replace partner_name with "Anonymized", clear email_from, and delete attached documents. Log the anonymization for audit compliance. We ship this as a standard module for EU-based clients.
What a Properly Configured Recruitment Pipeline Saves Your Business
Recruitment is one of the most expensive HR functions. Every day a position stays open costs the business in lost productivity, overtime for existing staff, and missed revenue. Here's what changes when you stop recruiting from spreadsheets:
Automated stage transitions, interview scheduling, and offer generation eliminate the manual delays that add 2-3 weeks to every hire.
Email templates, auto-tagging, and pipeline automation free recruiters to focus on candidate relationships instead of data entry.
UTM tracking and source-to-hire reporting shows exactly which channels produce quality hires — so you stop spending on channels that only produce volume.
The hidden ROI is candidate experience. Companies that respond within 24 hours and communicate at every stage win candidates over competitors who ghost them for two weeks. Odoo's automated templates and structured pipeline make this responsiveness the default.
Optimization Metadata
Complete guide to Odoo 19 Recruitment: configure job positions, publish careers pages, track applicants through Kanban, schedule interviews, send offers, and create employees.
1. "How to Configure Job Positions and Departments in Odoo 19 Recruitment" 2. "Applicant Tracking in Odoo 19" 3. "Mastering the Odoo 19 Recruitment Kanban" 4. "4 Recruitment Module Mistakes That Silently Sabotage Your Hiring Pipeline"