Your Team Is Tracking Leave in a Spreadsheet. That Spreadsheet Is Lying to You.
We audit dozens of Odoo implementations every year. The pattern is always the same: Accounting runs on Odoo, Sales runs on Odoo, Inventory runs on Odoo — but leave management still lives in a shared Google Sheet that someone created during onboarding week. HR manually counts remaining days. Managers approve time off via email. Nobody trusts the balances because three people edited the same cell last month.
Odoo 19's Time Off module solves this completely, but only if you configure it correctly. The default installation gives you a single "Legal Leaves" type and a manual allocation workflow — barely better than the spreadsheet. The real power is in the leave type hierarchy, accrual plans, multi-level approval chains, public holiday calendars, and payroll integration that most teams never set up because the documentation assumes you already know what you want.
This guide walks you through the complete configuration — from defining leave types that match your country's labor code, to building accrual plans that auto-allocate days monthly, to wiring approval workflows that route through department heads and HR. Every step is production-tested across companies with 50 to 2,000 employees.
How to Configure Leave Types in Odoo 19 for Accurate Absence Tracking
Leave types are the foundation of the entire Time Off module. Every request, allocation, report, and payroll deduction traces back to a leave type. Getting these wrong means every downstream process is wrong too.
Navigate to the Configuration
Go to Time Off → Configuration → Leave Types. Odoo 19 ships with a few defaults, but you should replace them with types that match your actual policies. Here is a typical structure for a mid-size company:
| Leave Type | Requires Allocation | Approval | Accrual | Payroll Code |
|---|---|---|---|---|
| Annual Leave | Yes | Manager + HR | Monthly accrual | ANNUAL |
| Sick Leave | Yes (capped) | Manager only | Yearly reset | SICK |
| Unpaid Leave | No allocation | Manager + HR | None | UNPAID |
| Work From Home | No allocation | Manager only | None | — |
| Compensatory | Yes (manual) | HR only | None | COMP |
| Parental Leave | Yes (manual) | HR only | None | PARENTAL |
Key Leave Type Settings
Each leave type has configuration options that dramatically change behavior. Here are the ones that matter:
<record id="leave_type_annual" model="hr.leave.type">
<field name="name">Annual Leave</field>
<field name="requires_allocation">yes</field>
<field name="employee_requests">yes</field>
<field name="leave_validation_type">both</field>
<field name="allocation_validation_type">hr</field>
<field name="color">4</field>
<field name="request_unit">half_day</field>
<field name="company_id" ref="base.main_company"/>
</record>
<record id="leave_type_sick" model="hr.leave.type">
<field name="name">Sick Leave</field>
<field name="requires_allocation">yes</field>
<field name="employee_requests">no</field>
<field name="leave_validation_type">manager</field>
<field name="support_document">True</field>
<field name="color">2</field>
<field name="request_unit">day</field>
</record>
<record id="leave_type_unpaid" model="hr.leave.type">
<field name="name">Unpaid Leave</field>
<field name="requires_allocation">no</field>
<field name="leave_validation_type">both</field>
<field name="unpaid">True</field>
<field name="color">1</field>
</record>requires_allocation— set toyesfor any leave type with a balance (annual, sick). Set tonofor unlimited types (unpaid, WFH).leave_validation_type— controls the approval chain:no_validation(auto-approved),manager(single approval),both(manager then HR), orhr(HR only).request_unit— whether employees can request inday,half_day, orhourincrements. Half-day is the most common for annual leave.support_document— when enabled, employees can attach a doctor's certificate or supporting file to the leave request. Essential for sick leave compliance.unpaid— flags the leave type so payroll knows to deduct salary for these days. If you miss this flag, unpaid leave employees get paid anyway.
Odoo 19 uses the color field to differentiate leave types on the calendar view. Assign distinct colors (0–11) to each type. When a manager looks at the team calendar, they should instantly distinguish annual leave (green) from sick leave (red) from WFH (blue) without reading labels. This small detail dramatically improves adoption.
Setting Up Leave Allocation Rules in Odoo 19 for Automatic Balance Management
Allocations define how many days of each leave type an employee gets. Without allocations, employees with allocation-required leave types cannot submit requests. Odoo 19 offers three allocation strategies:
| Strategy | How It Works | Best For |
|---|---|---|
| Manual | HR creates allocation records one by one | Compensatory days, special grants |
| Bulk by Department | HR allocates N days to all employees in a department or tag at once | Fixed annual entitlements |
| Accrual Plan | Days accumulate automatically based on a schedule | Annual leave that accrues monthly |
Creating a Bulk Allocation
For companies that grant all annual leave on January 1st, bulk allocation is the fastest approach. Navigate to Time Off → Managers → Allocations → New:
# Server action: Allocate annual leave to all active employees
# Triggered by a scheduled action on January 1st each year
employees = env['hr.employee'].search([
('active', '=', True),
('company_id', '=', env.company.id),
])
leave_type = env.ref('hr_holidays.leave_type_annual')
for employee in employees:
# Check seniority for tiered allocation
years = (fields.Date.today() - employee.first_contract_date).days / 365.25
if years <= 2:
days = 20
elif years <= 5:
days = 23
elif years <= 10:
days = 25
else:
days = 28
env['hr.leave.allocation'].create({{
'name': f'Annual Leave {{fields.Date.today().year}} - {{employee.name}}',
'employee_id': employee.id,
'holiday_status_id': leave_type.id,
'number_of_days': days,
'date_from': fields.Date.today().replace(month=1, day=1),
'date_to': fields.Date.today().replace(month=12, day=31),
'state': 'validate',
}})
env.cr.commit() This script implements seniority-tiered allocations — employees with more tenure get more days. The date_from and date_to fields scope the allocation to the calendar year so balances reset automatically. Setting state to validate skips the approval step since this is an administrative bulk operation.
If your policy allows unused days to carry over, do not set date_to on the allocation. Instead, create a new allocation each year for the new entitlement and let the old one remain open. If you set date_to to December 31st and an employee has 5 unused days, those days vanish at midnight. For carryover with a cap (e.g., max 5 days), use an accrual plan with the carryover setting.
Configuring Accrual Plans in Odoo 19 for Automatic Monthly Leave Accumulation
Accrual plans are the most powerful allocation mechanism in Odoo 19. Instead of granting 24 days upfront, you grant 2 days per month — which means an employee who joins in July gets 12 days for the year, not 24. This is how most labor codes actually work, and it prevents the "new hire takes 3 weeks off in February" problem.
Navigate to Accrual Plan Configuration
Go to Time Off → Configuration → Accrual Plans and create a new plan:
<record id="accrual_plan_annual" model="hr.leave.accrual.plan">
<field name="name">Standard Annual Leave Accrual</field>
<field name="transition_mode">immediately</field>
<field name="carryover_date">year_start</field>
<field name="carryover_day">1</field>
<field name="carryover_month">jan</field>
</record>
<!-- Level 1: First 2 years of service -->
<record id="accrual_level_junior"
model="hr.leave.accrual.level">
<field name="accrual_plan_id"
ref="accrual_plan_annual"/>
<field name="start_count">0</field>
<field name="start_type">day</field>
<field name="added_value">1.67</field>
<field name="added_value_type">day</field>
<field name="frequency">monthly</field>
<field name="first_day">1</field>
<field name="cap_accrued_time">True</field>
<field name="maximum_leave">20</field>
</record>
<!-- Level 2: After 2 years -->
<record id="accrual_level_senior"
model="hr.leave.accrual.level">
<field name="accrual_plan_id"
ref="accrual_plan_annual"/>
<field name="start_count">730</field>
<field name="start_type">day</field>
<field name="added_value">2.08</field>
<field name="added_value_type">day</field>
<field name="frequency">monthly</field>
<field name="first_day">1</field>
<field name="cap_accrued_time">True</field>
<field name="maximum_leave">25</field>
</record>added_value: 1.67— this equals 20 days per year (1.67 × 12 = 20.04). The slight overshoot is harmless; themaximum_leavecap catches it.transition_mode: immediately— when an employee crosses the 730-day threshold, they immediately start accruing at the senior rate. The alternative,end_of_accrual, waits until the next accrual period.carryover_date: year_start— unused days carry over (or reset) on January 1st. Combined with a carryover cap on the allocation, this enforces "use it or lose it" policies.- Multi-level accrual — the plan has two levels: junior employees (0–2 years) accrue 20 days/year; senior employees (2+ years) accrue 25 days/year. Odoo automatically transitions between levels based on the employee's start date.
Linking an Accrual Plan to Allocations
An accrual plan does nothing until it is linked to an allocation. Create an allocation for each employee (or use a scheduled action to bulk-create them) and set the accrual_plan_id field:
# Scheduled action: Create accrual-based allocations
# for new employees who don't have one yet
employees = env['hr.employee'].search([
('active', '=', True),
('company_id', '=', env.company.id),
])
plan = env.ref('your_module.accrual_plan_annual')
leave_type = env.ref('hr_holidays.leave_type_annual')
existing = env['hr.leave.allocation'].search([
('accrual_plan_id', '=', plan.id),
('state', '=', 'validate'),
]).mapped('employee_id')
new_employees = employees - existing
for emp in new_employees:
env['hr.leave.allocation'].create({{
'name': f'Annual Accrual - {{emp.name}}',
'employee_id': emp.id,
'holiday_status_id': leave_type.id,
'number_of_days': 0,
'accrual_plan_id': plan.id,
'date_from': emp.first_contract_date or fields.Date.today(),
'state': 'validate',
}}) Odoo 19 runs the accrual cron (hr.leave.allocation.accrual.cron) once per day at midnight UTC. If your company is in UTC+8 and you expect accruals to update on the 1st of each month local time, they will actually update on the 1st at midnight UTC — which is 8 AM local. This is rarely an issue, but it confuses HR teams who check balances at 7 AM on the 1st and see "last month's" numbers. Adjust the cron schedule if needed via Settings → Technical → Automation → Scheduled Actions.
Building Multi-Level Leave Approval Workflows in Odoo 19
Approval workflows determine who can approve a leave request and in what order. Odoo 19 provides four validation modes, but the real complexity comes from combining them with employee hierarchy, department structure, and leave type rules.
| Validation Mode | Approval Chain | Use Case |
|---|---|---|
no_validation | Auto-approved immediately | Work From Home, training |
manager | Direct manager approves | Sick leave, short absences |
hr | HR Officer approves | Parental leave, compensatory |
both | Manager approves first, then HR confirms | Annual leave, extended absences |
Setting Up the Employee Hierarchy
The manager and both validation modes rely on the Leave Manager field on the employee record. If this field is empty, Odoo falls back to the department manager. If both are empty, the request sits in limbo forever — no one sees it in their approval queue.
# Run in shell or as a server action to find orphaned employees
orphaned = env['hr.employee'].search([
('active', '=', True),
('leave_manager_id', '=', False),
('department_id.manager_id', '=', False),
])
if orphaned:
names = ', '.join(orphaned.mapped('name'))
raise UserError(
f"These employees have no leave approver: {{names}}. "
"Set a Leave Manager on the employee form or assign "
"a manager to their department."
)Run this script before going live. We have seen companies go months without realizing that 15% of their workforce had no leave approver configured — those employees submitted requests that nobody ever saw.
Custom Approval Rules Based on Duration
A common policy: leave requests under 3 days need only manager approval; requests of 3 days or more need manager and HR. Odoo 19 doesn't support this natively, but you can implement it with a small module override:
from odoo import models, api
class HrLeave(models.Model):
_inherit = 'hr.leave'
@api.depends('number_of_days', 'holiday_status_id')
def _compute_validation_type(self):
"""Override validation type based on leave duration.
Requests >= 3 days require both manager and HR.
"""
for leave in self:
base_type = leave.holiday_status_id.leave_validation_type
if (base_type == 'manager'
and leave.number_of_days >= 3):
leave.validation_type = 'both'
else:
leave.validation_type = base_type By default, Odoo 19 prevents employees from approving their own leave requests. But what about department managers? They are their own "leave manager." If a department head submits annual leave with manager validation, Odoo lets them approve their own request. To prevent this, either set the department manager's leave_manager_id to their own manager or a senior HR officer, or enforce both validation for all leave types so HR always has the final say.
Managing Public Holidays and Company-Wide Closures in Odoo 19
Public holidays prevent the Time Off module from counting those days against an employee's balance. If Christmas is a public holiday, a leave request from December 23rd to 27th should deduct 3 working days, not 5. But this only works if the public holiday calendar is correctly configured and linked.
Configure Public Holidays
Navigate to Time Off → Configuration → Public Holidays. Create entries for each statutory holiday:
<record id="ph_2026_new_year"
model="resource.calendar.leaves">
<field name="name">New Year's Day</field>
<field name="date_from">2026-01-01 00:00:00</field>
<field name="date_to">2026-01-01 23:59:59</field>
<field name="resource_id" eval="False"/>
<field name="calendar_id"
ref="resource.resource_calendar_std"/>
</record>
<record id="ph_2026_christmas"
model="resource.calendar.leaves">
<field name="name">Christmas Day</field>
<field name="date_from">2026-12-25 00:00:00</field>
<field name="date_to">2026-12-25 23:59:59</field>
<field name="resource_id" eval="False"/>
<field name="calendar_id"
ref="resource.resource_calendar_std"/>
</record>
<!-- Company-wide closure: Dec 25-Jan 1 -->
<record id="ph_2026_winter_closure"
model="resource.calendar.leaves">
<field name="name">Winter Closure</field>
<field name="date_from">2026-12-25 00:00:00</field>
<field name="date_to">2027-01-01 23:59:59</field>
<field name="resource_id" eval="False"/>
<field name="calendar_id"
ref="resource.resource_calendar_std"/>
</record>resource_idset to False — this is the critical setting. Whenresource_idis empty, the leave applies to all employees using this work schedule. If you set a resource, it only applies to that specific employee.calendar_id— links the holiday to a specific work schedule. If you have multiple schedules (e.g., full-time 40h vs. part-time 20h), you may need to create public holidays on each calendar — or create them on all calendars via a scheduled action.
Multi-Country Public Holidays
For companies with employees in multiple countries, create one work schedule per country and attach country-specific public holidays to each. An employee in France gets French holidays; an employee in Canada gets Canadian ones. The work schedule is set on the employee's contract under Working Schedule.
Many countries have regional holidays (e.g., US state holidays, German Bundesland holidays, Canadian provincial holidays). The cleanest approach is to create work schedule variants per region: "Standard 40h — Ontario" and "Standard 40h — Quebec." This avoids the complexity of employee-level holiday overrides and keeps the calendar clean for managers reviewing team availability.
Calendar Integration and Leave Reporting in Odoo 19
The Time Off module shines when connected to the rest of Odoo. Calendar integration gives managers real-time visibility into team availability, and the reporting tools surface trends that HR needs for workforce planning.
Team Calendar View
Managers access the team calendar via Time Off → Managers → Overview. This shows all employees in the manager's department(s) on a Gantt-style timeline. Color-coded by leave type, it immediately answers the question "who's off next week?"
To make this view useful, ensure every leave type has a distinct color and that department hierarchies are correct. A manager only sees employees in departments they manage — if the org chart is wrong, the calendar is incomplete.
Outlook and Google Calendar Sync
Odoo 19 syncs approved leave requests to the employee's linked calendar (Google or Outlook) if the Calendar module is installed. The sync creates all-day events with the leave type as the title. This means colleagues outside Odoo can see when someone is off without logging into Odoo.
Key Reports
Navigate to Time Off → Reporting → Analysis for the pivot and graph views. The most useful analyses:
# Server action or Jupyter-style analysis
# Shows leave utilization rate per department
from collections import defaultdict
departments = env['hr.department'].search([])
report = []
for dept in departments:
employees = env['hr.employee'].search([
('department_id', '=', dept.id),
('active', '=', True),
])
if not employees:
continue
total_allocated = 0
total_taken = 0
for emp in employees:
allocations = env['hr.leave.allocation'].search([
('employee_id', '=', emp.id),
('state', '=', 'validate'),
('holiday_status_id.requires_allocation', '=', 'yes'),
])
total_allocated += sum(allocations.mapped('number_of_days'))
leaves = env['hr.leave'].search([
('employee_id', '=', emp.id),
('state', '=', 'validate'),
('date_from', '>=', '2026-01-01'),
('date_to', '<=', '2026-12-31'),
])
total_taken += sum(leaves.mapped('number_of_days'))
utilization = (total_taken / total_allocated * 100
if total_allocated else 0)
report.append({{
'department': dept.name,
'allocated': total_allocated,
'taken': total_taken,
'utilization': f'{{utilization:.1f}}%',
}})
# Sort by lowest utilization — these departments
# may have employees at risk of losing days
report.sort(key=lambda r: r['utilization'])This report highlights departments where employees are not using their leave — a compliance risk in many jurisdictions and a burnout signal in all of them. Run it quarterly and share results with department heads.
Payroll Integration
When the Payroll module is installed, approved leave requests automatically generate work entries that affect salary calculation:
- Paid leave (annual, sick) — creates a work entry with type "Leave" that does not reduce salary.
- Unpaid leave — creates a work entry with type "Unpaid" that reduces salary proportionally. The deduction formula is:
monthly_salary / working_days_in_month * unpaid_days. - Leave type → work entry type mapping — configured in Time Off → Configuration → Leave Types → Payroll tab. Each leave type maps to a work entry type which maps to a salary rule. If this mapping is missing, the leave won't affect payroll at all.
4 Time Off Configuration Mistakes That Create Payroll Errors and Compliance Gaps
Missing Work Schedule on Employee Contracts
The Time Off module calculates leave duration based on the employee's working schedule (resource calendar). If the contract has no working schedule, Odoo defaults to the company's standard 40-hour week. A part-time employee on a 3-day week who requests Monday to Friday off gets charged 5 days instead of 3 because the system doesn't know they only work Mon/Wed/Fri.
Create a working schedule for every employment pattern in your organization. Link it to the employee's active contract. Audit with: env['hr.employee'].search([('resource_calendar_id', '=', False)]).
Public Holidays Not Linked to All Work Schedules
You created Christmas as a public holiday on the "Standard 40h" calendar. But your part-time employees use "Part-Time 20h" — a different calendar. They request December 23–27 and get charged for Christmas Day because their calendar doesn't know it's a holiday. The fix is simple but tedious: public holidays must exist on every active work schedule.
Write a scheduled action that copies public holidays across all active calendars. Run it whenever you add new holidays or new work schedules. This prevents silent drift between calendars.
Accrual Plans Without a Maximum Cap
An accrual plan without cap_accrued_time enabled will keep adding days forever. An employee who never takes leave will accumulate 40, 60, 80+ days over time. When they eventually resign, your company owes a massive payout for unused leave. In most jurisdictions, accrued leave is a financial liability that must appear on your balance sheet.
Always set maximum_leave on accrual levels. For annual leave, cap at the annual entitlement (e.g., 25 days). For carryover policies, set a separate carryover maximum. Alert managers when employees exceed 80% of their balance without a planned vacation.
Leave Type "Unpaid" Flag Missing from Payroll Mapping
You created an "Unpaid Leave" type and set unpaid=True on the leave type. But in the Payroll module, the work entry type mapped to this leave type doesn't have the corresponding salary rule to deduct pay. Result: the employee is marked as "unpaid leave" in Time Off, but their payslip shows full salary. Nobody catches it until the quarterly audit.
After creating any leave type, immediately verify the full chain: Leave Type → Work Entry Type → Salary Rule. Generate a test payslip for an employee with that leave type and confirm the deduction appears. Document this chain in your implementation notes.
What Proper Leave Management Saves Your Organization
Time Off management isn't an HR convenience feature — it's a financial control. Misconfigured leave systems create real costs: overpayments, compliance fines, understaffed teams, and burned-out employees who never take vacation.
Accrual plans, auto-approval workflows, and self-service requests eliminate the back-and-forth emails. HR stops being a leave calculator and starts being strategic.
When leave types map correctly to work entries and salary rules, unpaid days deduct automatically. No manual payroll adjustments, no correction payslips.
Employees check balances, submit requests, and track approvals from the Odoo portal or mobile app. No emails to HR asking "how many days do I have left?"
The hidden ROI is compliance confidence. When leave balances, accruals, and payroll deductions are automated end-to-end, your year-end audit becomes a data export instead of a spreadsheet reconstruction exercise. In regulated industries, this alone justifies the configuration effort.
Optimization Metadata
Configure Odoo 19 Time Off management: leave types, accrual plans, multi-level approval workflows, public holidays, calendar sync, and payroll integration.
1. "How to Configure Leave Types in Odoo 19 for Accurate Absence Tracking"
2. "Setting Up Leave Allocation Rules in Odoo 19 for Automatic Balance Management"
3. "Configuring Accrual Plans in Odoo 19 for Automatic Monthly Leave Accumulation"
4. "Building Multi-Level Leave Approval Workflows in Odoo 19"
5. "4 Time Off Configuration Mistakes That Create Payroll Errors and Compliance Gaps"