Create Gantt View with vis-timeline in Odoo 18
By Braincuber Team
Published on January 28, 2026
Gantt charts turn abstract task lists into visual timelines. At a glance, you see which tasks overlap, what's behind schedule, and where resources are overallocated. Odoo Enterprise includes a native Gantt view, but Community Edition doesn't. This tutorial shows how to build a custom Gantt view for Odoo Community using the vis-timeline JavaScript library—giving you timeline visualization without the Enterprise license.
We'll build a complete Gantt view for a Manufacturing Orders module. You'll create a custom view type, integrate vis-timeline via OWL components, handle drag-and-drop rescheduling, and display tasks grouped by work center. The same approach works for project tasks, HR leaves, sales orders, or any model with start and end dates.
What You'll Build: A fully functional Gantt view showing manufacturing orders on a timeline, grouped by work center, with drag-drop rescheduling, zoom controls, and color-coded status indicators.
Why vis-timeline?
Lightweight & Fast
Renders thousands of items smoothly. No dependencies on heavy frameworks. CDN-ready or npm install.
Drag & Drop
Built-in item dragging and resizing. Reschedule tasks by dragging them along the timeline.
Grouping Support
Display items in swim lanes by category, resource, or project. Perfect for work center views.
Zoom & Pan
Mouse wheel zoom, range selection, fit-all button. Navigate from hour view to year view seamlessly.
Module Structure
Implementation Steps
Create Module Manifest
Define dependencies and asset bundles for the custom Gantt view.
{
'name': 'Manufacturing Gantt View',
'version': '18.0.1.0.0',
'category': 'Manufacturing',
'summary': 'Gantt chart for manufacturing orders using vis-timeline',
'depends': ['mrp', 'web'],
'data': [
'views/mrp_views.xml',
],
'assets': {
'web.assets_backend': [
# vis-timeline library (CDN or local)
'https://unpkg.com/vis-timeline@7.7.3/dist/vis-timeline-graph2d.min.js',
'https://unpkg.com/vis-timeline@7.7.3/dist/vis-timeline-graph2d.min.css',
# Custom Gantt components
'manufacturing_gantt/static/src/js/gantt_view.js',
'manufacturing_gantt/static/src/js/gantt_controller.js',
'manufacturing_gantt/static/src/js/gantt_renderer.js',
'manufacturing_gantt/static/src/css/gantt_view.css',
'manufacturing_gantt/static/src/xml/gantt_view.xml',
],
},
'installable': True,
'license': 'LGPL-3',
}
Register the Gantt View Type
Create a new view type that Odoo recognizes, following the MVC pattern.
/** @odoo-module */
import { registry } from "@web/core/registry";
import { GanttController } from "./gantt_controller";
import { GanttRenderer } from "./gantt_renderer";
export const ganttView = {
type: "gantt",
display_name: "Gantt",
icon: "fa fa-tasks",
multiRecord: true,
Controller: GanttController,
Renderer: GanttRenderer,
props(genericProps, view) {
const { arch } = genericProps;
const parser = new DOMParser();
const archDoc = parser.parseFromString(arch, "text/xml");
const ganttEl = archDoc.querySelector("gantt");
return {
...genericProps,
dateStart: ganttEl.getAttribute("date_start"),
dateEnd: ganttEl.getAttribute("date_end"),
groupBy: ganttEl.getAttribute("group_by"),
};
},
};
registry.category("views").add("gantt", ganttView);
Build the Controller
Handle data loading, user interactions, and updates.
/** @odoo-module */
import { Component, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class GanttController extends Component {
setup() {
this.orm = useService("orm");
this.action = useService("action");
this.state = useState({
records: [],
groups: [],
loading: true,
});
onWillStart(async () => {
await this.loadData();
});
}
async loadData() {
const { resModel, dateStart, dateEnd, groupBy, domain } = this.props;
// Fetch records with date fields
const fields = ["id", "name", dateStart, dateEnd, groupBy];
const records = await this.orm.searchRead(resModel, domain || [], fields);
// Transform for vis-timeline
const items = records.map(record => ({
id: record.id,
content: record.name,
start: record[dateStart],
end: record[dateEnd],
group: record[groupBy]?.[0] || 0,
}));
// Get groups (e.g., work centers)
const groupIds = [...new Set(items.map(i => i.group))];
const groupModel = this.getGroupModel(groupBy);
const groupRecords = await this.orm.read(groupModel, groupIds, ["name"]);
const groups = groupRecords.map(g => ({
id: g.id,
content: g.name,
}));
this.state.records = items;
this.state.groups = groups;
this.state.loading = false;
}
getGroupModel(fieldName) {
// Map field name to model (customize based on your fields)
const mapping = {
workcenter_id: "mrp.workcenter",
project_id: "project.project",
user_id: "res.users",
};
return mapping[fieldName] || "res.partner";
}
async onItemMoved(itemId, newStart, newEnd) {
const { resModel, dateStart, dateEnd } = this.props;
await this.orm.write(resModel, [itemId], {
[dateStart]: newStart.toISOString(),
[dateEnd]: newEnd.toISOString(),
});
await this.loadData();
}
onItemClick(itemId) {
this.action.doAction({
type: "ir.actions.act_window",
res_model: this.props.resModel,
res_id: itemId,
views: [[false, "form"]],
target: "current",
});
}
}
GanttController.template = "manufacturing_gantt.GanttView";
GanttController.components = { GanttRenderer: () => import("./gantt_renderer") };
Create the Renderer with vis-timeline
Initialize and configure the vis-timeline component.
/** @odoo-module */
import { Component, useRef, onMounted, onWillUnmount } from "@odoo/owl";
export class GanttRenderer extends Component {
setup() {
this.containerRef = useRef("container");
this.timeline = null;
onMounted(() => this.initTimeline());
onWillUnmount(() => this.destroyTimeline());
}
initTimeline() {
const container = this.containerRef.el;
const { records, groups } = this.props;
// Create DataSets
const items = new vis.DataSet(records);
const groupData = new vis.DataSet(groups);
// Timeline options
const options = {
editable: {
add: false,
updateTime: true,
updateGroup: true,
remove: false,
},
stack: true,
zoomMin: 1000 * 60 * 60, // 1 hour
zoomMax: 1000 * 60 * 60 * 24 * 365, // 1 year
orientation: "top",
margin: { item: 10 },
tooltip: {
followMouse: true,
},
};
// Initialize timeline
this.timeline = new vis.Timeline(container, items, groupData, options);
// Event handlers
this.timeline.on("select", (props) => {
if (props.items.length > 0) {
this.props.onItemClick(props.items[0]);
}
});
this.timeline.on("changed", () => {
// Handle item moved
items.forEach((item) => {
const original = records.find(r => r.id === item.id);
if (original && (
item.start !== original.start ||
item.end !== original.end
)) {
this.props.onItemMoved(item.id, item.start, item.end);
}
});
});
}
destroyTimeline() {
if (this.timeline) {
this.timeline.destroy();
this.timeline = null;
}
}
zoomIn() {
this.timeline.zoomIn(0.5);
}
zoomOut() {
this.timeline.zoomOut(0.5);
}
fitAll() {
this.timeline.fit();
}
}
GanttRenderer.template = "manufacturing_gantt.GanttRenderer";
Create QWeb Templates
Define the HTML structure for the Gantt view UI.
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="manufacturing_gantt.GanttView" owl="1">
<div class="o_gantt_view">
<!-- Toolbar -->
<div class="o_gantt_toolbar d-flex gap-2 p-2 bg-light border-bottom">
<button class="btn btn-outline-secondary btn-sm"
t-on-click="() => renderer.zoomIn()">
<i class="fa fa-search-plus"/> Zoom In
</button>
<button class="btn btn-outline-secondary btn-sm"
t-on-click="() => renderer.zoomOut()">
<i class="fa fa-search-minus"/> Zoom Out
</button>
<button class="btn btn-outline-secondary btn-sm"
t-on-click="() => renderer.fitAll()">
<i class="fa fa-expand"/> Fit All
</button>
</div>
<!-- Timeline Container -->
<t t-if="state.loading">
<div class="o_gantt_loading text-center p-5">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p class="mt-3">Loading timeline...</p>
</div>
</t>
<t t-else="">
<GanttRenderer
records="state.records"
groups="state.groups"
onItemClick="onItemClick"
onItemMoved="onItemMoved"/>
</t>
</div>
</t>
<t t-name="manufacturing_gantt.GanttRenderer" owl="1">
<div class="o_gantt_timeline" t-ref="container"
style="height: calc(100vh - 200px);"/>
</t>
</templates>
Define the View in XML
Add the Gantt view to your model's action.
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Gantt View Definition -->
<record id="mrp_production_gantt_view" model="ir.ui.view">
<field name="name">mrp.production.gantt</field>
<field name="model">mrp.production</field>
<field name="arch" type="xml">
<gantt string="Manufacturing Orders"
date_start="date_start"
date_end="date_finished"
group_by="workcenter_id">
<field name="id"/>
<field name="name"/>
<field name="state"/>
</gantt>
</field>
</record>
<!-- Add Gantt to Manufacturing Orders Action -->
<record id="mrp.mrp_production_action" model="ir.actions.act_window">
<field name="view_mode">tree,kanban,form,calendar,gantt</field>
</record>
<!-- View Sequence -->
<record id="mrp_production_gantt_action_view" model="ir.actions.act_window.view">
<field name="view_mode">gantt</field>
<field name="view_id" ref="mrp_production_gantt_view"/>
<field name="act_window_id" ref="mrp.mrp_production_action"/>
<field name="sequence">50</field>
</record>
</odoo>
Required Fields: Your model must have date fields for date_start and date_end. For mrp.production, these are date_start and date_finished. Adjust field names based on your model.
Styling the Gantt Chart
Add Custom CSS
Style the timeline to match Odoo's design language.
.o_gantt_view {
height: 100%;
display: flex;
flex-direction: column;
}
.o_gantt_timeline {
flex: 1;
min-height: 400px;
}
/* Item styling */
.vis-item {
border-radius: 4px;
border: none;
font-size: 12px;
padding: 4px 8px;
}
.vis-item.vis-selected {
box-shadow: 0 0 0 2px #714B67;
}
/* Status-based colors */
.vis-item.state-draft { background: #6c757d; }
.vis-item.state-confirmed { background: #0d6efd; }
.vis-item.state-progress { background: #ffc107; color: #000; }
.vis-item.state-done { background: #198754; }
.vis-item.state-cancel { background: #dc3545; }
/* Group labels */
.vis-label {
font-weight: 600;
background: #f8f9fa;
border-right: 2px solid #dee2e6;
}
/* Time axis */
.vis-time-axis .vis-text {
font-size: 11px;
color: #495057;
}
/* Today marker */
.vis-current-time {
background-color: #dc3545;
width: 2px;
}
Conclusion
The vis-timeline library transforms Odoo Community into a visual project management powerhouse. You get drag-and-drop scheduling, zoom controls, grouped swim lanes, and real-time database updates—all without Enterprise licensing costs. Apply this pattern to project tasks, HR leaves, fleet bookings, or any model with date ranges.
Key Takeaways: Register custom view type with registry.category("views"). Use vis.Timeline for rendering. Handle item movement via ORM writes. Map group_by fields to their models. Style with CSS classes matching record states.
