October 15, 2025

Custom Odoo Modules for Rental Operations

By CleverBusiness Engineering Team

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.