Your Maintenance Crew Is Firefighting. Your Equipment Knows When It Will Fail — You're Just Not Listening.
Every manufacturing plant has the same story: a critical CNC machine goes down at 2 PM on a Thursday, the production line stops, and a maintenance technician scrambles to find the right spare part while a supervisor calls the vendor. The repair takes 6 hours. The lost production costs $40,000. A week later, the post-mortem reveals the machine's vibration readings had been deteriorating for three weeks — but nobody was tracking them.
Odoo 19's Maintenance module closes this gap. It gives you a centralized equipment registry, automated preventive maintenance schedules, a structured corrective request workflow, and built-in MTBF/MTTR tracking that turns your reactive maintenance operation into a predictive one. And unlike standalone CMMS tools, it's natively integrated with Manufacturing, Inventory, and Purchase — so when a machine needs a spare part, the procurement chain activates automatically.
This guide walks through the full setup: registering equipment, configuring preventive schedules, handling corrective requests, reading MTBF/MTTR dashboards, connecting equipment to manufacturing work centers, and managing spare parts inventory. Every configuration shown is from production Odoo 19 instances running real manufacturing operations.
How to Build a Complete Equipment Registry in Odoo 19 Maintenance
The equipment registry is the foundation of everything. Every maintenance request, every schedule, and every KPI ties back to an equipment record. Getting this right means getting the hierarchy, categories, and metadata right from day one.
Enable the Maintenance Module
Navigate to Apps, search for "Maintenance," and install the module. If you're running Manufacturing, also install MRP Maintenance (mrp_maintenance) for work center integration.
# Install maintenance + MRP integration
./odoo-bin -c /etc/odoo/odoo.conf \
-d production_db \
-i maintenance,mrp_maintenance \
--stop-after-initEquipment Categories
Before adding individual machines, define your category taxonomy. Categories in Odoo 19 serve as templates — each category carries default maintenance team assignments, default technicians, and a color for Kanban views. A well-designed category tree reduces per-equipment setup time by 70%.
<odoo>
<data noupdate="1">
<record id="equip_cat_cnc" model="maintenance.equipment.category">
<field name="name">CNC Machines</field>
<field name="technician_user_id" ref="base.user_admin"/>
<field name="color">4</field>
<field name="alias_name">maint-cnc</field>
</record>
<record id="equip_cat_conveyor" model="maintenance.equipment.category">
<field name="name">Conveyors</field>
<field name="color">2</field>
<field name="alias_name">maint-conveyor</field>
</record>
</data>
</odoo>Registering Equipment
Each equipment record captures identity, location, ownership, and vendor information. The critical fields that most teams skip on initial setup — and regret later — are serial number, effective date, warranty expiration, and assigned work center.
Equipment = self.env['maintenance.equipment']
Equipment.create({
'name': 'CNC Haas VF-2SS #001',
'category_id': self.env.ref(
'my_module.equip_cat_cnc'
).id,
'serial_no': 'HAAS-VF2SS-2024-001',
'model': 'VF-2SS',
'partner_id': self.env.ref(
'my_module.vendor_haas'
).id,
'effective_date': '2024-06-15',
'warranty_date': '2027-06-15',
'location': 'Building A — Bay 3',
'note': 'Spindle: 12,000 RPM. Coolant: flood + TSC.',
'period': 30, # Preventive every 30 days
'color': 4,
})
Equipment.create({
'name': 'Conveyor Belt — Assembly Line 1',
'category_id': self.env.ref(
'my_module.equip_cat_conveyor'
).id,
'serial_no': 'CONV-AL1-2023-007',
'effective_date': '2023-01-10',
'location': 'Building B — Line 1',
'period': 90, # Preventive every 90 days
}) The alias_name field on equipment categories creates an automatic email-to-maintenance-request pipeline. When a floor operator emails maint-cnc@yourcompany.com, Odoo creates a corrective request, assigns it to the category's default technician, and notifies the maintenance team. This is the fastest path from "machine is broken" to "someone is working on it."
Configuring Preventive Maintenance Schedules That Actually Get Followed
Preventive maintenance is the backbone of equipment longevity. Odoo 19 generates maintenance requests automatically based on time intervals you define per equipment. The key is setting intervals that match manufacturer recommendations and your actual operating conditions — not arbitrary round numbers.
Setting Up Recurring Preventive Schedules
Navigate to Maintenance → Equipments → Machines, open an equipment record, and configure the Maintenance tab. The period field (in days) controls how often Odoo auto-generates the next preventive request after the current one is closed.
class MaintenancePreventiveSchedule(models.Model):
_name = 'maintenance.preventive.schedule'
_description = 'Preventive Maintenance Schedule'
equipment_id = fields.Many2one(
'maintenance.equipment', required=True,
ondelete='cascade',
)
name = fields.Char(required=True)
interval_days = fields.Integer(
string='Interval (Days)', required=True,
)
last_done = fields.Date(string='Last Completed')
next_due = fields.Date(
string='Next Due', compute='_compute_next_due',
store=True,
)
task_description = fields.Html(string='Task Checklist')
maintenance_team_id = fields.Many2one('maintenance.team')
duration_expected = fields.Float(default=1.0)
@api.depends('last_done', 'interval_days')
def _compute_next_due(self):
for rec in self:
if rec.last_done and rec.interval_days > 0:
rec.next_due = rec.last_done + timedelta(
days=rec.interval_days
)
else:
rec.next_due = fields.Date.today()
def action_generate_request(self):
"""Generate requests for all overdue schedules."""
Request = self.env['maintenance.request']
today = fields.Date.today()
overdue = self.search([('next_due', '<=', today)])
for schedule in overdue:
Request.create({
'name': f'[Preventive] {{schedule.name}}',
'equipment_id': schedule.equipment_id.id,
'request_date': today,
'maintenance_type': 'preventive',
'maintenance_team_id': schedule.maintenance_team_id.id,
'duration': schedule.duration_expected,
'description': schedule.task_description,
})
schedule.last_done = todayCron Job for Auto-Generation
The built-in cron maintenance.cron_generate_requests runs daily and creates preventive requests for equipment whose period field has elapsed. For custom schedules, register your own cron:
<odoo>
<data noupdate="1">
<record id="cron_preventive_schedule"
model="ir.cron">
<field name="name">
Generate Preventive Maintenance Requests
</field>
<field name="model_id"
ref="model_maintenance_preventive_schedule"/>
<field name="state">code</field>
<field name="code">
model.action_generate_request()
</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
</record>
</data>
</odoo>| Equipment Type | Typical Interval | Key Tasks | Expected Duration |
|---|---|---|---|
| CNC Machine | 30 days | Spindle lubrication, coolant check, axis calibration | 2–4 hrs |
| Conveyor Belt | 90 days | Belt tension, roller alignment, motor inspection | 1–2 hrs |
| HVAC Unit | 180 days | Filter replacement, refrigerant level, duct cleaning | 3–5 hrs |
| Packaging Line | 60 days | Seal bar inspection, sensor calibration, safety guards | 1–3 hrs |
Manufacturer-recommended intervals assume average operating conditions. A CNC machine running 20 hours/day in a dusty environment needs preventive maintenance far more frequently than the spec sheet's 30-day interval. Use MTBF data (covered in section 04) to calibrate your intervals after 6 months of operation. We've seen clients reduce unplanned downtime by 40% just by shortening two intervals based on actual failure data.
Corrective Maintenance Requests: From Breakdown to Resolution in a Structured Workflow
When equipment fails unexpectedly, speed matters. Odoo 19's corrective maintenance workflow gives you a Kanban pipeline that tracks every request from creation through diagnosis, repair, and closure — with full time tracking at each stage.
Creating Corrective Requests
Any user with Maintenance / User access can create a corrective request. There are three input channels:
- Manual — Maintenance → Maintenance Requests → Create, set type to "Corrective."
- Email — Send to the category's email alias. Odoo parses the subject and body into a request.
- Programmatic — Triggered from manufacturing work orders when an operator flags a machine issue mid-production.
class MrpWorkorder(models.Model):
_inherit = 'mrp.workorder'
def action_report_equipment_issue(self):
"""Operator clicks 'Report Issue' during production."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Report Equipment Issue',
'res_model': 'maintenance.request',
'view_mode': 'form',
'target': 'new',
'context': {
'default_equipment_id': (
self.workcenter_id.equipment_ids[:1].id
),
'default_maintenance_type': 'corrective',
'default_name': (
f'[URGENT] {{self.workcenter_id.name}}'
f' — reported during '
f'{{self.production_id.name}}'
),
'default_description': (
f'Work order: {{self.name}}\n'
f'Product: '
f'{{self.product_id.display_name}}\n'
f'Reported by operator at '
f'{{fields.Datetime.now()}}'
),
},
}The Kanban Pipeline
The default Kanban stages in Odoo 19 Maintenance are: New Request → In Progress → Repaired → Scrap. For most manufacturing operations, we recommend adding two stages:
| Stage | Purpose | Auto-Actions |
|---|---|---|
| New Request | Intake — request logged, awaiting triage | Notify maintenance team lead |
| Diagnosed | Root cause identified, parts/labor estimated | Trigger spare parts check |
| Waiting for Parts | Repair blocked on spare part availability | Auto-create purchase order if stock is zero |
| In Progress | Technician actively repairing | Start time tracking |
| Repaired | Equipment back in service | Update MTTR, notify production |
| Scrap | Equipment beyond repair | Archive record, trigger replacement procurement |
Priority and SLA Tracking
Odoo 19 supports a star-based priority system (0–3 stars). Extend it with custom SLA deadlines: map priority 0 to 72 hours, priority 1 to 24 hours, priority 2 to 8 hours, and priority 3 (critical) to 2 hours. Add a computed sla_deadline field on maintenance.request and a boolean sla_breached that flips when the deadline passes while the request is still open.
SLA_HOURS = {'0': 72, '1': 24, '2': 8, '3': 2}
@api.depends('priority', 'request_date')
def _compute_sla_deadline(self):
for req in self:
if req.request_date and req.priority:
hours = self.SLA_HOURS.get(req.priority, 72)
req.sla_deadline = (
req.request_date + timedelta(hours=hours)
)
else:
req.sla_deadline = FalseThe default maintenance request form has a description field but no structured root cause classification. Add a selection field with categories like Electrical, Mechanical, Software, Operator Error, and Wear & Tear. After 6 months, this data reveals which failure categories dominate your operation — and whether your preventive schedules are actually preventing the right things.
MTBF and MTTR Tracking: Turn Maintenance Data into Predictive Intelligence
Odoo 19 computes two critical reliability metrics automatically for every equipment record:
- MTBF (Mean Time Between Failures) — average days between corrective maintenance requests. Higher is better.
- MTTR (Mean Time To Repair) — average days from request creation to close. Lower is better.
These metrics live on the equipment form under the Maintenance tab and are also available as Kanban card badges and dashboard KPIs.
How Odoo 19 Computes MTBF and MTTR
# In maintenance.equipment (Odoo 19 source)
@api.depends('maintenance_ids.close_date',
'maintenance_ids.request_date')
def _compute_maintenance_stats(self):
for equipment in self:
corrective = equipment.maintenance_ids.filtered(
lambda r: r.maintenance_type == 'corrective'
and r.close_date
).sorted('request_date')
if len(corrective) >= 2:
# MTBF: average gap between failures
deltas = []
for i in range(1, len(corrective)):
gap = (
corrective[i].request_date
- corrective[i - 1].request_date
).days
deltas.append(gap)
equipment.mtbf = (
sum(deltas) / len(deltas)
if deltas else 0
)
# MTTR: average repair duration
repairs = [
(r.close_date - r.request_date).days
for r in corrective
]
equipment.mttr = (
sum(repairs) / len(repairs)
if repairs else 0
)
else:
equipment.mtbf = 0
equipment.mttr = 0Building a Maintenance KPI Dashboard
The raw MTBF/MTTR numbers are useful, but the real power comes from trending them over time and comparing across equipment categories. Create a pivot view with category_id as rows and mtbf, mttr, maintenance_count as measures. Wrap it in an ir.actions.act_window with view_mode=pivot,graph,list and add it to the Maintenance menu.
| Metric | Good Target | Warning | Action Required |
|---|---|---|---|
| MTBF (CNC) | > 90 days | 60–90 days | < 60 days |
| MTBF (Conveyor) | > 180 days | 120–180 days | < 120 days |
| MTTR (all) | < 1 day | 1–3 days | > 3 days |
| Preventive Compliance | > 95% | 80–95% | < 80% |
True predictive maintenance means adjusting preventive intervals based on actual MTBF trends. If a CNC machine's MTBF is declining from 90 → 75 → 60 days over three quarters, don't wait for it to fail again. Shorten the preventive interval to 70% of the current MTBF (in this case, ~42 days). This simple rule converts reactive MTBF data into proactive scheduling. Some clients integrate IoT sensor data (vibration, temperature) for real-time predictions, but even without sensors, the MTBF trend alone is powerful.
Connecting Equipment to Manufacturing Work Centers for Production-Aware Maintenance
The mrp_maintenance bridge module links equipment records to MRP work centers. This connection enables production-aware scheduling — Odoo won't schedule preventive maintenance during active production runs, and it can automatically block work orders when a machine is under repair.
Linking Equipment to Work Centers
# In mrp.workcenter (via mrp_maintenance module)
# The field already exists after installing mrp_maintenance:
# equipment_ids = fields.Many2many(
# 'maintenance.equipment'
# )
# To assign via code:
workcenter = self.env.ref('mrp.workcenter_cnc_bay3')
equipment = self.env.ref(
'my_module.equip_cnc_haas_001'
)
workcenter.equipment_ids = [
(4, equipment.id)
]
# Now maintenance requests for this equipment
# will show the linked work center, and the
# work center's capacity planning considers
# equipment availability.Blocking Work Orders During Repair
When a work center's equipment has an open corrective request, production planning needs to know. Add a computed has_active_repair boolean on mrp.workcenter that searches for open corrective requests linked to the work center's equipment. Then inherit the work center form view to display a warning banner:
<xpath expr="//sheet" position="before">
<div class="alert alert-warning"
role="alert"
invisible="not has_active_repair">
<strong>Equipment under repair.</strong>
Production capacity may be reduced.
<button name="action_view_repairs"
type="object"
class="btn btn-link">
View Repairs
</button>
</div>
</xpath> With equipment linked to work centers, you can compute OEE (Overall Equipment Effectiveness) as Availability x Performance x Quality. Availability comes directly from maintenance data: (Planned Production Time - Downtime) / Planned Production Time. Downtime is the sum of all corrective request durations for that work center's equipment. This closes the loop between maintenance operations and production KPIs.
Spare Parts Management: Never Wait 3 Days for a $12 Bearing Again
The #1 cause of extended MTTR isn't diagnosis time — it's waiting for spare parts. Odoo 19 lets you link products (spare parts) directly to equipment records and maintenance requests, with automatic reorder rules that keep critical spares in stock.
Linking Spare Parts to Equipment
Create a custom model to track which spare parts each equipment record requires, with minimum stock levels and typical consumption rates:
class MaintenanceSparePart(models.Model):
_name = 'maintenance.spare.part'
_description = 'Equipment Spare Part'
equipment_id = fields.Many2one(
'maintenance.equipment', required=True,
ondelete='cascade',
)
product_id = fields.Many2one(
'product.product', required=True,
domain=[('type', '=', 'product')],
string='Spare Part',
)
qty_needed = fields.Float(
string='Qty Per Repair', default=1.0,
)
min_stock = fields.Float(string='Minimum Stock')
current_stock = fields.Float(
related='product_id.qty_available',
)
stock_status = fields.Selection(
[('ok', 'In Stock'),
('low', 'Low Stock'),
('out', 'Out of Stock')],
compute='_compute_stock_status', store=True,
)
@api.depends('current_stock', 'min_stock')
def _compute_stock_status(self):
for part in self:
if part.current_stock <= 0:
part.stock_status = 'out'
elif part.current_stock <= part.min_stock:
part.stock_status = 'low'
else:
part.stock_status = 'ok'Consuming Parts on Maintenance Requests
When a technician completes a repair, they should record which spare parts were used. Add a consumed_part_ids One2many on maintenance.request pointing to a child model with product_id, quantity, and a computed subtotal. A total_parts_cost computed field on the request sums all consumed part lines — giving you per-request and per-equipment cost tracking over time.
For every spare part linked to critical equipment, create an Inventory reorder rule (stock.warehouse.orderpoint) with product_min_qty set to at least one repair's worth. The most expensive spare part is always the one you don't have when you need it. We've seen clients where a $12 bearing caused $40,000 in lost production because nobody set a reorder rule and the warehouse ran out.
4 Maintenance Module Pitfalls That Sabotage Your Equipment Reliability Program
Preventive Requests Pile Up Because Nobody Closes Them
Odoo generates the next preventive request only after the current one is marked as done. If technicians complete the work but forget to close the request in Odoo, the next one never generates. After 6 months, you have 200 open preventive requests and zero actual scheduling.
Add a weekly cron that emails the maintenance manager a list of preventive requests open longer than their expected duration. Also add a "quick close" button on the Kanban card so technicians can close requests without opening the form.
MTBF Is Zero Because You Only Track Corrective Requests — Not Failures
MTBF measures time between failures, not time between maintenance requests. If your team logs a corrective request for "oil top-up" and another for "bearing seized," Odoo treats both as failures. Your MTBF drops to 15 days even though only one was an actual breakdown. The metric becomes meaningless.
Add a boolean field is_breakdown on maintenance requests. Only compute MTBF from requests where is_breakdown = True. Train technicians: a breakdown means production stopped. Everything else is a corrective task, not a failure event.
Spare Parts Exist in Inventory but Nobody Links Them to Equipment
You bought 50 units of bearing SKU-6205-2RS. They're sitting in the warehouse. But when the CNC machine needs one, the technician doesn't know the SKU, searches the warehouse manually, and takes 45 minutes to find it. The parts-to-equipment link doesn't exist out of the box in Odoo's standard maintenance module.
The maintenance.spare.part model from section 06 solves this. Each equipment record gets a "Spare Parts" tab listing every part it may need, with current stock levels visible. Technicians see exactly what's available before walking to the warehouse.
Maintenance Teams Don't Match Your Actual Crew Structure
Odoo 19 lets you define maintenance teams, but many implementations create one team called "Maintenance" and dump everyone in it. This means every technician sees every request. Electricians see mechanical tasks, the HVAC specialist sees conveyor belt issues, and nobody can filter for their actual work.
Create teams that mirror your crew specializations: Electrical, Mechanical, HVAC, Automation. Assign equipment categories to teams. When a corrective request comes in for a CNC machine (Mechanical category), it lands in the Mechanical team's Kanban. Electricians never see it.
What Structured Maintenance Saves Your Manufacturing Operation
The ROI of a properly configured maintenance module isn't theoretical — it's measured in production hours recovered and emergency repair costs avoided.
Preventive schedules catch wear before it causes failure. MTBF trending surfaces declining equipment health months before breakdown.
Structured corrective workflows with spare parts pre-linked to equipment eliminate diagnosis delays and warehouse scavenger hunts.
Data-driven reorder points replace gut-feel bulk purchasing. You stock what you need, not what someone panic-ordered last time.
Consistent preventive maintenance extends equipment useful life. A CNC machine that lasts 12 years instead of 6 saves $150K+ in capital expenditure.
The hidden ROI is operational predictability. When your production manager can trust that equipment will be available for next week's production schedule — because the maintenance module shows all preventive tasks are current and no corrective requests are blocking work centers — planning becomes reliable. That predictability reduces overtime, eliminates rush orders, and improves on-time delivery rates.
Optimization Metadata
Complete guide to Odoo 19 Maintenance module: equipment registry, preventive schedules, corrective workflows, MTBF/MTTR tracking, work center integration, and spare parts management.
1. "How to Build a Complete Equipment Registry in Odoo 19 Maintenance"
2. "Configuring Preventive Maintenance Schedules That Actually Get Followed"
3. "MTBF and MTTR Tracking: Turn Maintenance Data into Predictive Intelligence"
4. "Spare Parts Management: Never Wait 3 Days for a $12 Bearing Again"