Odoo's time-off module (hr.leave) is deceptively deep. On the surface it is a request-and-approval form. Under the hood it ties together allocation policies, multi-level approval, carry-over rules, public holidays, work schedules, and payroll integration - any of which can silently produce wrong results if misconfigured.
This guide covers everything an integrator needs to set up time-off correctly from scratch, including the non-obvious constraints that support questions never surface until after go-live.
Core data model#
Three models do most of the work:
| Model | Purpose |
|---|---|
hr.leave.type | Leave type definition: annual leave, sick leave, unpaid, etc. |
hr.leave.allocation | How many days an employee is entitled to for a given type |
hr.leave | Individual leave request (an absence) |
A leave request (hr.leave) cannot be validated unless the employee has a validated allocation (hr.leave.allocation) with enough remaining days for that leave type - unless the type is configured to allow negative balance or no allocation at all.
Leave type configuration#
Navigate to Time Off → Configuration → Activity Types (or in Odoo 17: Time Off → Configuration → Time Off Types).
The fields that matter most:
Approval#
validation_type controls the approval chain:
| Value | Behaviour |
|---|---|
no_validation | Auto-confirmed on submission |
hr | Requires one approval (HR manager or direct manager depending on settings) |
both | Two-step: employee's manager approves first, then HR validates |
manager | Direct manager only |
set | No allocation required; unlimited |
Two-step approval (both) is correct for annual leave in most companies. Single-step (manager) works for sick leave where HR retroactively records rather than approves.
Allocation mode#
allocation_type determines whether employees need a formal allocation record:
- Fixed by HR (
fixed by hr): HR creates allocations manually. Common for annual leave with specific entitlements. - Allocated by employee (
no): No allocation required. Suitable for unpaid leave or special leave where you track time off but not a balance. - Fixed by HR (accrual): Allocation accrues over time via an accrual plan (see below).
Leave validation from payroll#
If you have Odoo Payroll, set Leave Validation to sync absences as payroll inputs automatically. This generates a hr.payslip.worked_days line when a leave is validated - do not configure this without confirming the payroll structure handles it.
Work schedules (critical dependency)#
Time-off days are calculated against an employee's work schedule. A 5-day leave request on an employee with a 4-day work week (Mon–Thu) results in 4 days deducted, not 5.
Before setting up leave types, confirm every employee has:
- A work schedule assigned (
resource.calendar) via the employee form → Work Information tab - The correct schedule for their contract period
Common mistakes:
- Using the default 40-hour schedule for part-time employees, causing over-deduction
- Not setting a work schedule at all (Odoo falls back to the company default)
- Changing a schedule mid-year without accounting for leaves already validated under the old schedule
Public holidays#
Public holidays are defined as hr.leave.type records with is_public_holiday = True (in Odoo 16+) or as resource.calendar.leaves with a leave type flag. They are globally time-off days that apply to all employees.
Create them under Time Off → Configuration → Public Holidays. Link each holiday to a country if you have multi-country operations - Odoo applies only country-matching holidays to each employee.
Public holidays do not automatically reduce leave deduction. If an employee takes a week off that spans a public holiday and the public holiday is properly configured in their work schedule, the deduction will be 4 working days, not 5. This is correct behaviour but surprises users who manually count days.
Allocations in depth#
Manual allocation#
HR creates hr.leave.allocation records individually or in bulk (Configuration → Allocation Requests or through the employee form). Each allocation has:
- Employee: can be an individual or a department
- Leave type
- Number of days (or hours, if the leave type uses hours)
- Validity dates: start and optional end date; expired allocations cannot be used
- Carry-over: whether unused days roll over to the next allocation period
Bulk allocation#
For annual leave, create allocations in bulk:
- Go to Time Off → Managers → Allocation Requests
- Click New and set Mode to By department or By employee
- Set the number of days and validity dates
This creates one allocation record per employee. Validate them in batch with the Action → Validate menu.
Accrual plans#
Accrual plans (hr.leave.accrual.plan) accumulate allocation days over time. Configure them under Time Off → Configuration → Accrual Plans.
Key fields in an accrual plan:
| Field | Meaning |
|---|---|
| Level added per | Accrual frequency: daily, weekly, twice a month, monthly |
| Rate | Days earned per accrual period |
| Cap | Maximum accrued days (leave balance ceiling) |
| Carry over | At year-end: all days, up to a maximum, or none |
| Milestone | Time-in-service threshold to unlock higher accrual rate |
A typical setup for 20 days/year with monthly accrual: frequency = monthly, rate = 1.67 days/month (20 / 12), cap = 20, carry-over = max 5.
After creating the accrual plan, assign it when creating an allocation: set Accrual Plan on the allocation record and validate it. Odoo runs a cron job nightly to add accrued days.
Leave request flow#
The employee lifecycle for a leave request:
- Draft: Employee submits (or HR submits on behalf)
- Confirmed: Awaiting manager approval
- Validated (first approval): Manager approved; awaiting HR if two-step
- Validated: Fully approved; balance deducted and absence shown on calendar
- Refused: Rejected at any step
States map to hr.leave.state field values: draft, confirm, validate1, validate, refuse.
Programmatic approval#
To approve or refuse leaves in code (e.g., an automated action):
leave = env['hr.leave'].browse(leave_id)
leave.action_validate() # approve
leave.action_refuse() # refuse
leave.action_draft() # reset to draftaction_validate() requires the calling user to have the correct group (hr.group_hr_manager or group_hr_user depending on validation_type). Use sudo() carefully - it bypasses the approval chain entirely.
Calendar integration#
Validated leaves automatically appear on the employee's calendar and on the team calendar view. In Odoo 16+, they also appear in the Gantt view of Project/Planning if the employee is assigned to tasks during the absence period.
The hr.leave record creates a corresponding resource.calendar.leaves entry for the employee's work schedule, which is what prevents task scheduling during that period.
Reporting#
Key reports under Time Off → Reporting:
- By Employee: leave days per employee per type, current year
- By Department: summary table; useful for HR budget planning
- Allocation Analysis: outstanding allocations, expiry dates, remaining balance
The Analysis view (hr.leave.report) joins leaves and allocations into one flat view, which is useful for exports. Filter by status = Validated to see only confirmed absences.
Payroll integration notes#
If you have Odoo Payroll enabled, validated leaves automatically appear in Worked Days on payslips for leave types with payroll sync enabled. Verify:
- The leave type has a Work Entry Type configured (
hr.work.entry.type) - The work entry type has the correct Time Off flag
- The payroll structure includes a rule that handles that work entry type
Missing the work entry type is the most common payroll integration bug - absences disappear from payslips silently.
Common configuration mistakes#
| Mistake | Symptom | Fix |
|---|---|---|
| No work schedule on employee | All leave requests deduct 0 days | Assign schedule in employee Work Information tab |
| Allocation expired before leave taken | "Insufficient allocation" on a valid request | Extend validity end date on the allocation |
| Wrong validation_type | Leaves auto-approve or need unnecessary steps | Match validation_type to the company policy |
| Accrual not running | Balance does not grow | Confirm the nightly cron (Accrual Plans cron job) is active in Settings → Technical → Automation |
| Public holiday not in work schedule | Holidays count against leave balance | Ensure public holidays are registered AND linked to the company/country |
For HR payroll configuration see the Odoo HR and payroll guide. For multi-company leave management see the multi-company setup guide.

