Your Shift Schedule Lives in a Spreadsheet. And Everyone Hates It.
We have audited dozens of mid-size operations running Odoo for inventory, accounting, and CRM while still managing employee shifts in a shared Google Sheet. The spreadsheet has 14 tabs, conditional formatting nobody understands, and a change history that reveals at least three overwrite conflicts per week. When someone calls in sick at 6 AM, the operations manager texts five people to find a replacement while cross-referencing a separate leave tracker that may or may not be current.
Odoo 19's Planning module eliminates this. It provides a visual Gantt-based scheduler with shift templates, open shift self-assignment, automatic conflict detection, employee availability rules, and deep integration with the Time Off module. But most teams either don't know it exists or install it and never move past the default configuration.
This guide walks through a complete Planning module setup from scratch: installing and configuring the module, building reusable shift templates, allocating resources across roles, publishing open shifts, detecting scheduling conflicts, reading the Gantt view effectively, and wiring it all to Time Off so approved leave automatically blocks scheduling. Every step is production-tested across retail, manufacturing, and field-service clients.
Installing and Configuring the Odoo 19 Planning Module for Workforce Scheduling
The Planning module (planning) is a standalone app in Odoo 19 Enterprise. It depends on hr and resource but does not require Project or Manufacturing. You can run it purely for shift scheduling without any project management overhead.
Step 1: Install the Module
Navigate to Apps, search for "Planning", and click Install. Alternatively, install via the command line:
# Install via CLI (stop-after-init for non-interactive)
./odoo-bin -c /etc/odoo/odoo.conf \
-d your_database \
-i planning \
--stop-after-initStep 2: Configure Planning Settings
Go to Planning → Configuration → Settings. The key toggles you need:
| Setting | Default | Recommended | Why |
|---|---|---|---|
| Allow Open Shifts | Off | On | Lets you create shifts without pre-assigning an employee. Staff self-assign via the portal. |
| Allow Shift Switching | Off | On | Enables employees to swap assigned shifts with eligible colleagues. |
| Planning Roles | Off | On | Required for role-based resource allocation. Without this, every employee is interchangeable. |
Step 3: Define Planning Roles
Roles are the backbone of intelligent scheduling. Navigate to Planning → Configuration → Roles and create entries for each function:
# Example: seed planning roles via a data file
# my_module/data/planning_roles.xml
<odoo>
<data noupdate="1">
<record id="role_cashier" model="planning.role">
<field name="name">Cashier</field>
<field name="color">1</field>
</record>
<record id="role_floor_manager" model="planning.role">
<field name="name">Floor Manager</field>
<field name="color">4</field>
</record>
<record id="role_warehouse_picker" model="planning.role">
<field name="name">Warehouse Picker</field>
<field name="color">5</field>
</record>
<record id="role_delivery_driver" model="planning.role">
<field name="name">Delivery Driver</field>
<field name="color">10</field>
</record>
</data>
</odoo>Step 4: Assign Roles to Employees
On each employee's form (Employees → Employee → Work Information tab), set their Default Planning Role and optionally add additional roles they can cover. This is critical: when you create a shift for "Cashier," Odoo will only suggest employees tagged with that role.
In Odoo 19, employees can hold multiple planning roles. A seasoned retail associate who can work as both a Cashier and a Floor Manager should have both roles assigned. When you create an open shift for either role, they become eligible. This dramatically increases scheduling flexibility in small teams.
Building Reusable Shift Templates in Odoo 19 to Eliminate Repetitive Scheduling
Without templates, managers create 50+ individual shift slots every week by hand. Shift templates let you define a schedule pattern once and stamp it across any date range. This alone saves 2-3 hours of scheduling time per week for a 30-person operation.
Creating a Shift Template
Navigate to Planning → Configuration → Shift Templates. Each template defines:
| Field | Example | Purpose |
|---|---|---|
| Name | Morning Cashier | Descriptive label visible in the Gantt view. |
| Role | Cashier | Only employees with this role will be suggested. |
| Start Hour | 08:00 | Shift start time (in the company timezone). |
| Duration | 8 hours | Calculated end time. Avoids manual errors. |
| Recurrence | Weekly, Mon-Fri | Auto-generates shifts for selected days. |
# Create shift templates via server action or migration script
env = self.env
# Morning shift template
env['planning.slot.template'].create({
'name': 'Morning Cashier (08-16)',
'role_id': env.ref('my_module.role_cashier').id,
'start_time': 8.0, # 08:00
'duration': 8.0, # 8 hours
})
# Evening shift template
env['planning.slot.template'].create({
'name': 'Evening Floor (16-00)',
'role_id': env.ref('my_module.role_floor_manager').id,
'start_time': 16.0, # 16:00
'duration': 8.0, # 8 hours
})
# Short Saturday shift
env['planning.slot.template'].create({
'name': 'Saturday Picker (09-13)',
'role_id': env.ref('my_module.role_warehouse_picker').id,
'start_time': 9.0, # 09:00
'duration': 4.0, # 4 hours
})Applying Templates to Generate a Weekly Schedule
Once templates exist, go to Planning → Schedule by Employee, click Auto Plan or use the Copy Previous Week button. Odoo generates slots for the selected period using your templates. You can then drag-and-drop to adjust individual assignments.
Copy Previous Week duplicates last week's actual schedule, including employee assignments. Auto Plan with templates creates fresh slots from templates and lets Odoo auto-assign based on availability. Use Copy Previous for stable teams with fixed rotations. Use Auto Plan when you want the system to optimize around constraints like leave, overtime limits, and role coverage.
Resource Allocation in Odoo 19: Matching Employees to Shifts by Role, Skill, and Availability
Resource allocation is where Planning moves beyond a simple calendar. Odoo 19 considers three dimensions when suggesting employees for a shift:
- Role eligibility — does the employee hold the required planning role?
- Working schedule — is the shift within the employee's contracted hours (defined on their
resource.calendar)? - Existing commitments — does the employee already have another shift, approved leave, or a public holiday overlapping this slot?
How Resource Calendars Drive Allocation
Every employee in Odoo has a Working Schedule (resource calendar) defined on their contract or employee form. This calendar tells Planning when the employee is contractually available. A part-time employee on a Mon/Wed/Fri schedule will not appear as a suggestion for a Tuesday shift.
# How Odoo internally checks resource availability
from datetime import datetime
slot_start = datetime(2026, 3, 16, 8, 0) # Monday 08:00
slot_end = datetime(2026, 3, 16, 16, 0) # Monday 16:00
role = env.ref('my_module.role_cashier')
# Get employees with the Cashier role
eligible = env['hr.employee'].search([
('planning_role_ids', 'in', role.id),
('company_id', '=', env.company.id),
])
# Filter by working schedule availability
available = eligible.filtered(
lambda e: e.resource_id._check_availability(
slot_start, slot_end
)
)
# Further filter: no overlapping planning slots
for emp in available:
conflicts = env['planning.slot'].search([
('employee_id', '=', emp.id),
('start_datetime', '<', slot_end),
('end_datetime', '>', slot_start),
('state', '!=', 'cancelled'),
])
if conflicts:
available -= empAllocation Percentage and Capacity
Each planning slot has an Allocated Percentage field (default 100%). This is essential for employees who split time across projects or departments. A developer who spends 50% of their time on support and 50% on feature work can have two simultaneous slots at 50% each. Odoo tracks the total allocation per employee and flags when it exceeds 100% in the Gantt view with a visual warning.
Allocation percentage is relative to the employee's working schedule, not absolute hours. A part-time employee working 20h/week at 100% allocation means all 20 hours are booked. A full-time employee at 50% allocation means 20 of their 40 hours are committed. Always check the "Allocated Hours" computed field to see the actual hour impact.
Open Shifts in Odoo 19: Let Employees Self-Assign and Reduce Manager Overhead
Open shifts are planning slots with no employee assigned. Instead of the manager picking who works each shift, the shift is published and eligible employees can claim it themselves. This is transformative for retail, hospitality, and healthcare operations where staff preferences and availability change weekly.
Creating and Publishing Open Shifts
When creating a planning slot, simply leave the Employee field empty and set the Role. Then use the Send button (or schedule auto-send) to notify eligible employees. They receive an email with a link to the Planning portal where they can view and claim available shifts.
# Create open shifts for next week
from datetime import datetime, timedelta
next_monday = datetime(2026, 3, 16, 0, 0)
role_cashier = env.ref('my_module.role_cashier')
open_shifts = env['planning.slot']
for day_offset in range(5): # Mon-Fri
shift_date = next_monday + timedelta(days=day_offset)
open_shifts |= env['planning.slot'].create({
'role_id': role_cashier.id,
'employee_id': False, # No employee = open shift
'start_datetime': shift_date.replace(hour=8),
'end_datetime': shift_date.replace(hour=16),
'state': 'draft',
})
# Publish all open shifts at once
open_shifts.action_send()Self-Assignment via the Employee Portal
Once published, employees log into My Planning on the portal. They see a calendar of their assigned shifts and a separate section for Available Open Shifts filtered by their roles. Claiming a shift is one click. The system immediately checks for conflicts; if the employee already has a shift at that time, the claim is rejected with a clear message.
| Open Shift Feature | Manager Action | Employee Action |
|---|---|---|
| Create | Creates slot with no employee, sets role | - |
| Publish | Clicks Send to notify eligible staff | Receives email notification |
| Claim | - | Clicks "I'll take it" on the portal |
| Confirm | Auto-confirmed or requires approval (configurable) | Sees shift on their calendar |
| Unclaim | Can reassign or reopen | Can release shift back to open pool (if allowed) |
When 10 employees receive an open shift notification simultaneously, multiple may try to claim it. Odoo 19 handles this with an optimistic locking mechanism on the planning.slot record. The first successful write wins; subsequent attempts get a concurrency error and a user-friendly "This shift has already been taken" message. No double-bookings.
Automatic Conflict Detection and Employee Availability Rules in Odoo 19 Planning
Scheduling conflicts are the #1 source of operational friction in shift-based businesses. An employee scheduled for two overlapping shifts, a shift assigned during approved vacation, or a night shift followed by a morning shift with insufficient rest — each of these causes call-outs, resentment, and sometimes labor law violations.
What Odoo 19 Detects Automatically
The Planning module runs conflict checks in real-time as you create or modify slots:
| Conflict Type | Detection Method | Visual Indicator |
|---|---|---|
| Overlapping shifts | Same employee, overlapping start_datetime / end_datetime | Red warning banner on the slot |
| Approved time off | Cross-references hr.leave records in "validate" state | Hatched/grayed-out area on Gantt |
| Public holidays | Checks the employee's resource calendar for global leaves | Gray column in Gantt view |
| Outside working hours | Compares slot to the employee's resource.calendar attendance lines | Orange warning indicator |
| Over-allocation (>100%) | Sums allocated percentages across concurrent slots | Red progress bar exceeding 100% |
# Add minimum rest period enforcement (e.g., 11 hours between shifts)
# my_module/models/planning_slot.py
from odoo import models, api
from odoo.exceptions import ValidationError
from datetime import timedelta
class PlanningSlot(models.Model):
_inherit = 'planning.slot'
MIN_REST_HOURS = 11 # EU Working Time Directive
@api.constrains('employee_id', 'start_datetime', 'end_datetime')
def _check_minimum_rest_period(self):
for slot in self.filtered('employee_id'):
# Find the previous shift ending
prev_slot = self.search([
('employee_id', '=', slot.employee_id.id),
('end_datetime', '<=', slot.start_datetime),
('id', '!=', slot.id),
('state', '!=', 'cancelled'),
], order='end_datetime desc', limit=1)
if prev_slot:
rest = (slot.start_datetime - prev_slot.end_datetime)
min_rest = timedelta(hours=self.MIN_REST_HOURS)
if rest < min_rest:
raise ValidationError(
f"Insufficient rest period for "
f"{{slot.employee_id.name}}. "
f"Only {{rest.total_seconds() / 3600:.1f}}h "
f"between shifts (minimum: "
f"{{self.MIN_REST_HOURS}}h)."
)Many jurisdictions mandate minimum rest periods between shifts (11 hours in the EU under the Working Time Directive, varying by state in the US). Odoo 19 does not enforce rest periods out of the box. The custom constraint above is not optional for regulated industries — it is a legal requirement. Add it to a custom module and test it thoroughly before relying on it for compliance.
Reading and Using the Planning Gantt View in Odoo 19 for Visual Workforce Management
The Gantt view is the primary interface for planning managers. It displays employees on the Y-axis and time on the X-axis, with colored bars representing assigned shifts. Understanding its features is critical for effective scheduling.
Gantt View Controls
- Zoom Level — switch between Day, Week, Month, and Year views using toolbar buttons.
- Group By — group rows by Employee, Role, Department, or Project via the dropdown.
- Drag & Drop — move a shift to a different time slot or reassign to another employee row.
- Resize — extend or shorten a shift by dragging the left or right edge of the bar.
- Quick Create — click an empty area on an employee's row to open a new shift form.
- Copy Week — duplicate the entire current week's schedule to the next week with one click.
Color Coding and Visual Cues
Shift bars are color-coded by Role by default. This makes it instantly visible whether you have adequate coverage across all functions:
- Solid color bar — assigned and confirmed shift.
- Striped/hatched bar — draft or unconfirmed shift.
- Gray background area — employee time off or public holiday.
- Red border or warning icon — conflict detected (overlapping shift, over-allocation).
- Empty bar with dashed border — open shift (unassigned).
<!-- Extend the planning Gantt view to show allocated hours -->
<record id="planning_slot_gantt_custom" model="ir.ui.view">
<field name="name">planning.slot.gantt.custom</field>
<field name="model">planning.slot</field>
<field name="inherit_id"
ref="planning.planning_slot_view_gantt"/>
<field name="arch" type="xml">
<gantt position="attributes">
<attribute name="display_unavailability">1</attribute>
<attribute name="color">role_id</attribute>
<attribute name="decoration-warning">
allocated_percentage > 100
</attribute>
</gantt>
</field>
</record>Most managers view the Gantt grouped by Employee — "who is working when." Switch to Group by Role to answer a more important question: "do we have enough Cashiers on Saturday afternoon?" This view immediately surfaces coverage gaps that the employee view hides because the empty rows do not appear.
Integrating Odoo 19 Planning with Time Off: Approved Leave Blocks Scheduling Automatically
The most powerful aspect of Odoo 19 Planning is its native integration with the Time Off module (hr_holidays). When an employee's leave request is approved, the Planning module automatically reflects it — no manual synchronization, no spreadsheet cross-referencing.
How the Integration Works
- Approved leave appears as grayed-out/unavailable blocks in the Gantt view. You physically cannot drag a shift into that area.
- Existing shifts that overlap with newly approved leave are flagged with a conflict warning. The manager must reassign or cancel them.
- Auto-plan respects leave: when you use Auto Plan to generate the weekly schedule, employees on approved leave are automatically excluded from assignment.
- Open shifts filter out employees on leave: even if an employee has the right role, they won't see open shifts that fall during their approved time off on the portal.
# How to verify an employee is not on leave before assigning
def assign_employee_to_shift(self, employee, slot):
"""Safely assign an employee, checking leave first."""
leaves = self.env['hr.leave'].search([
('employee_id', '=', employee.id),
('state', '=', 'validate'),
('date_from', '<', slot.end_datetime),
('date_to', '>', slot.start_datetime),
])
if leaves:
return {
'warning': {
'title': 'Leave Conflict',
'message': (
f"{{employee.name}} has approved "
f"leave from {{leaves[0].date_from}} "
f"to {{leaves[0].date_to}}. "
f"Cannot assign this shift."
),
}
}
slot.write({'employee_id': employee.id})
return TrueHandling the Edge Case: Leave Approved After Shift Published
The tricky scenario: a manager publishes next week's schedule on Friday, then an employee's sick leave gets approved on Monday morning. Odoo 19 handles this with a notification to the planning manager when a leave approval creates conflicts with existing published shifts. The manager sees a warning and can:
- Reassign the conflicting shift to another employee.
- Convert the shift to an open shift for self-assignment.
- Cancel the shift if coverage is not needed.
# Extend leave approval to handle planning conflicts
# my_module/models/hr_leave.py
from odoo import models
class HrLeave(models.Model):
_inherit = 'hr.leave'
def action_validate(self):
"""Override to unassign conflicting planning slots."""
res = super().action_validate()
for leave in self:
conflicting_slots = self.env['planning.slot'].search([
('employee_id', '=', leave.employee_id.id),
('start_datetime', '<', leave.date_to),
('end_datetime', '>', leave.date_from),
('state', '!=', 'cancelled'),
])
if conflicting_slots:
# Convert to open shifts instead of deleting
conflicting_slots.write({
'employee_id': False,
})
# Notify the planning manager
conflicting_slots._notify_planning_manager(
reason=f"Leave approved for "
f"{{leave.employee_id.name}}"
)
return resPlanned leave (vacation) is typically approved days or weeks in advance — the schedule adjusts before publication. Sick leave is the real problem because it happens after the schedule is live. The auto-unassign pattern above converts sick employee shifts to open shifts, triggering notifications to eligible staff who can self-assign. This cuts the "frantic text message" replacement workflow down to a system notification.
4 Planning Module Mistakes That Create Scheduling Chaos in Production
Not Setting Up Resource Calendars Before Creating Shifts
The Planning module relies on resource.calendar to know when employees are available. If employees are still on the default "Standard 40h" calendar but actually work part-time or on rotating schedules, every conflict check and availability filter will produce wrong results. Auto-plan will assign part-time staff to full-time shifts. The Gantt view will show them as available when they are not contractually working.
Before touching Planning, audit every employee's Working Schedule on their contract or employee form. Create specific resource calendars for each shift pattern (e.g., "Part-Time Mon/Wed/Fri," "Rotating 4-on-3-off"). This is a prerequisite, not an optimization.
Ignoring Timezone Configuration for Multi-Location Teams
Planning slots store start_datetime and end_datetime in UTC. The Gantt view displays them in the user's timezone. If your company operates across timezones but employees have incorrect timezone settings on their user profiles, shifts appear at the wrong time. A 9 AM shift in New York shows as 9 AM for a manager viewing from London — who then wonders why the employee is 5 hours late.
Set the correct timezone on every user profile (Settings → Users) and on each company record. For multi-timezone operations, always verify shift times by switching the Gantt view to "Schedule by Resource" and spot-checking times against the employee's local clock.
Publishing Shifts Without Confirming Leave Calendar First
Managers often build next week's schedule on Thursday and publish immediately. But HR hasn't finished processing leave requests from Wednesday. The result: published shifts assigned to employees whose leave will be approved Friday morning. The manager gets a conflict notification after employees have already seen their schedule. Rescheduling after publication causes confusion and erodes trust in the system.
Establish a scheduling workflow: HR processes all pending leave requests by Wednesday noon. Planning managers build the schedule Wednesday afternoon. Review on Thursday. Publish Friday. This simple cadence eliminates 90% of post-publication conflicts.
Using Planning Without Roles Leads to Unqualified Assignments
If Planning Roles are not enabled, every employee is eligible for every shift. Auto-plan will assign your accountant to a warehouse picker shift because it only optimizes for availability, not qualification. Without roles, open shifts appear to everyone regardless of skill. You end up with a technically valid schedule that is operationally useless.
Enable Planning Roles as step one of any implementation. Even if you have a small team where "everyone does everything," create roles. They become essential the moment you grow past 10 employees or need to report on labor cost by function.
What Proper Workforce Planning Saves Your Operation
Shift scheduling isn't a back-office chore. It directly impacts labor cost, employee satisfaction, and customer experience. Here's what changes when you replace the spreadsheet:
Shift templates and Auto Plan replace manual slot-by-slot scheduling. A 30-person weekly schedule takes 15 minutes instead of 3 hours.
Automatic conflict detection catches double-bookings, leave overlaps, and rest period violations before the schedule is published.
Open shifts with portal self-assignment replace the phone tree. Sick calls at 6 AM result in a filled shift by 7 AM without manager intervention.
The hidden ROI is employee retention. Workers in shift-based roles consistently cite "unpredictable scheduling" and "unfair shift distribution" as top reasons for leaving. A transparent system where schedules are published on time, open shifts are available to everyone equally, and leave is automatically respected improves fairness perception measurably. In retail and hospitality, even a 5% reduction in turnover saves thousands per employee in hiring and training costs.
Optimization Metadata
Complete guide to Odoo 19 Planning module: shift templates, open shifts, conflict detection, Gantt view scheduling, and Time Off integration for workforce management.
1. "Installing and Configuring the Odoo 19 Planning Module for Workforce Scheduling"
2. "Building Reusable Shift Templates in Odoo 19 to Eliminate Repetitive Scheduling"
3. "Automatic Conflict Detection and Employee Availability Rules in Odoo 19 Planning"
4. "4 Planning Module Mistakes That Create Scheduling Chaos in Production"