Custom Odoo Modules for Rental Operations
Out-of-the-box Odoo doesn't handle rental-specific workflows well. Here's how we built custom modules to transform Odoo into a complete rental management platform.
The Rental Industry Gap
Standard Odoo is designed for traditional sales and inventory. Rental businesses need:
- Time-based availability: Same item can be rented to different customers at different times
- Reservation conflicts: Prevent double-booking across locations
- Pricing complexity: Daily, weekly, monthly rates with distance-based adjustments
- Maintenance scheduling: Track service intervals between rentals
- Damage tracking: Before/after inspections with photo uploads
Our Core Rental Module Architecture
1. Rental Product Extension
We extend Odoo's product.template model to add rental-specific fields:
class ProductTemplate(models.Model):
_inherit = 'product.template'
is_rental = fields.Boolean('Available for Rent')
rental_pricing = fields.One2many(
'rental.pricing', 'product_id',
string='Rental Rates'
)
min_rental_days = fields.Integer('Minimum Rental Period')
max_rental_days = fields.Integer('Maximum Rental Period')
requires_inspection = fields.Boolean('Pre/Post Inspection Required')
maintenance_interval = fields.Integer('Service Interval (hours)')
# Availability tracking
total_units = fields.Integer(compute='_compute_inventory')
available_units = fields.Integer(compute='_compute_availability')
reserved_units = fields.Integer(compute='_compute_availability')
2. Reservation Management
The core of our system is the rental.reservation model:
class RentalReservation(models.Model):
_name = 'rental.reservation'
_description = 'Equipment Rental Reservation'
name = fields.Char('Reservation #', required=True)
customer_id = fields.Many2one('res.partner', required=True)
product_id = fields.Many2one('product.product', required=True)
# Dates
start_date = fields.Datetime('Rental Start', required=True)
end_date = fields.Datetime('Rental End', required=True)
actual_return = fields.Datetime('Actual Return')
# Location
pickup_location = fields.Many2one('stock.location')
return_location = fields.Many2one('stock.location')
delivery_distance = fields.Float('Delivery Distance (km)')
# Pricing
base_rate = fields.Float('Base Rate')
distance_fee = fields.Float('Distance Fee')
insurance = fields.Float('Insurance')
total_amount = fields.Float(compute='_compute_total')
# State management
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('picked_up', 'In Use'),
('returned', 'Returned'),
('cancelled', 'Cancelled')
], default='draft')
@api.constrains('start_date', 'end_date', 'product_id')
def _check_availability(self):
"""Prevent double-booking"""
for record in self:
conflicts = self.search([
('product_id', '=', record.product_id.id),
('state', 'in', ['confirmed', 'picked_up']),
('id', '!=', record.id),
'|',
'&', ('start_date', '<=', record.start_date),
('end_date', '>', record.start_date),
'&', ('start_date', '<', record.end_date),
('end_date', '>=', record.end_date)
])
if conflicts:
raise ValidationError(
f'Equipment already reserved for {conflicts[0].name}'
)
3. Dynamic Pricing Engine
Pricing in rental is complex. We built a flexible pricing rules engine:
class RentalPricing(models.Model):
_name = 'rental.pricing'
product_id = fields.Many2one('product.product', required=True)
duration_type = fields.Selection([
('hourly', 'Hourly'),
('daily', 'Daily'),
('weekly', 'Weekly'),
('monthly', 'Monthly')
])
min_duration = fields.Integer('Minimum Duration')
max_duration = fields.Integer('Maximum Duration')
rate = fields.Float('Rate')
# Distance-based pricing
include_distance = fields.Boolean('Include Distance Fee')
free_distance = fields.Float('Free Distance (km)')
per_km_rate = fields.Float('Rate per KM')
# Seasonal adjustments
is_seasonal = fields.Boolean('Seasonal Pricing')
season_start = fields.Date('Season Start')
season_end = fields.Date('Season End')
seasonal_multiplier = fields.Float('Price Multiplier', default=1.0)
Integration with Core Odoo
Inventory Management
We hook into Odoo's stock management to track equipment movement:
def action_confirm_reservation(self):
"""Create stock moves when reservation is confirmed"""
for record in self:
# Reserve inventory
self.env['stock.quant'].sudo()._update_reserved_quantity(
record.product_id,
record.pickup_location,
quantity=1,
lot_id=None,
package_id=None,
strict=True
)
# Create delivery order
picking = self.env['stock.picking'].create({
'partner_id': record.customer_id.id,
'location_id': record.pickup_location.id,
'location_dest_id': record.customer_id.property_stock_customer.id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'origin': record.name
})
record.state = 'confirmed'
Invoicing & Accounting
Rental invoices are auto-generated based on actual rental duration:
def action_create_invoice(self):
"""Generate invoice on rental return"""
invoice_lines = []
# Base rental charge
rental_days = (self.actual_return - self.start_date).days
invoice_lines.append({
'product_id': self.product_id.id,
'quantity': rental_days,
'price_unit': self._get_daily_rate(),
'name': f'Rental: {self.product_id.name}'
})
# Distance fee
if self.delivery_distance:
invoice_lines.append({
'product_id': self.env.ref('rental.product_distance_fee').id,
'quantity': self.delivery_distance,
'price_unit': self._get_distance_rate(),
'name': 'Delivery Fee'
})
# Create invoice
invoice = self.env['account.move'].create({
'partner_id': self.customer_id.id,
'move_type': 'out_invoice',
'invoice_line_ids': [(0, 0, line) for line in invoice_lines]
})
return invoice
Real-World Performance
After deploying our custom modules to production rental businesses:
- 30% reduction in double-booking incidents (from constraint validation)
- 85% automation of invoicing (previously manual)
- Real-time availability across 12+ warehouse locations
- 42% faster quote generation (dynamic pricing engine)
Challenges & Solutions
Challenge 1: Performance with Large Inventories
Initial availability calculations were slow (2-3 seconds) with 10K+ items.
Solution: Implemented materialized views and Redis caching for availability checks. Now under 50ms.
Challenge 2: Timezone Handling
Multi-location rentals across timezones caused confusion.
Solution: All dates stored in UTC, converted to location timezone for display. Added explicit timezone indicators in UI.
Challenge 3: Mobile Access
Field teams needed mobile access for inspections and pickups.
Solution: Built progressive web app (PWA) using Odoo's web framework. Works offline with sync.
Open Source vs. Custom
There are open-source rental modules for Odoo (like rental_management by OCA), but we found them lacking:
| Feature | Open Source | Our Custom |
|---|---|---|
| Multi-location availability | ❌ Limited | ✅ Full support |
| Distance-based pricing | ❌ Not available | ✅ Built-in |
| Maintenance tracking | ⚠️ Basic | ✅ Advanced |
| Photo inspections | ❌ Manual only | ✅ Mobile app |
Next Features in Development
- AI-powered demand forecasting: Predict rental demand to optimize inventory
- Customer self-service portal: Let customers manage reservations online
- IoT integration: Track equipment location via GPS for theft prevention
- Automated maintenance scheduling: Based on usage hours and calendar intervals
Need custom Odoo modules for your rental business? Contact our team to discuss your requirements.