How to Build Custom Odoo Modules: A Developer's Guide to OWL, ORM & Inheritance
When Odoo's 82 official modules and 40,000+ community modules do not cover your exact business requirements, custom module development fills the gap. This guide covers the fundamentals of building Odoo modules in 2026, including module structure, the OWL frontend framework, ORM patterns, inheritance mechanisms, and best practices aligned with OCA (Odoo Community Association) guidelines.
What Is a Custom Odoo Module?
A custom Odoo module is a self-contained package of Python backend logic, XML view definitions, JavaScript frontend components, and security rules that extends Odoo's functionality. Modules can add entirely new features, modify existing behavior, or integrate Odoo with external systems. Every piece of Odoo itself is a module, making the architecture inherently extensible.
Module Directory Structure
A well-organized module follows this standard structure:
my_custom_module/
├── __init__.py # Python package init
├── __manifest__.py # Module metadata and dependencies
├── models/
│ ├── __init__.py
│ └── my_model.py # Business logic and data models
├── views/
│ ├── my_model_views.xml # Form, tree, and kanban views
│ └── menu.xml # Menu items and actions
├── security/
│ ├── ir.model.access.csv # Access control list
│ └── security.xml # Record rules and groups
├── data/
│ └── data.xml # Default data records
├── static/
│ └── src/
│ ├── js/ # OWL components
│ ├── css/ # Stylesheets
│ └── xml/ # QWeb templates
├── wizard/ # Transient models for wizards
├── reports/ # QWeb report templates
└── tests/ # Unit tests
The Manifest File
The __manifest__.py file defines your module's identity:
{
'name': 'My Custom Module',
'version': '18.0.1.0.0',
'category': 'Custom',
'summary': 'Short description of what the module does',
'description': """
Long description with details about
features and configuration.
""",
'author': 'ECOSIRE',
'website': 'https://ecosire.com',
'license': 'LGPL-3',
'depends': ['base', 'sale', 'stock'],
'data': [
'security/ir.model.access.csv',
'views/my_model_views.xml',
'views/menu.xml',
'data/data.xml',
],
'assets': {
'web.assets_backend': [
'my_custom_module/static/src/js/**/*',
'my_custom_module/static/src/css/**/*',
'my_custom_module/static/src/xml/**/*',
],
},
'installable': True,
'application': False,
'auto_install': False,
}
Version convention: {odoo_version}.{major}.{minor}.{patch} (e.g., 18.0.1.0.0).
Working with the ORM
Odoo's Object-Relational Mapping (ORM) is the foundation of all backend development. Models map to database tables, and the ORM provides CRUD operations, computed fields, constraints, and workflow management.
Defining a Model
from odoo import models, fields, api
class ProjectTask(models.Model):
_name = 'my_module.task'
_description = 'Project Task'
_order = 'priority desc, create_date desc'
name = fields.Char(string='Task Name', required=True)
description = fields.Html(string='Description')
state = fields.Selection([
('draft', 'Draft'),
('in_progress', 'In Progress'),
('done', 'Done'),
('cancelled', 'Cancelled'),
], default='draft', tracking=True)
assigned_to = fields.Many2one('res.users', string='Assigned To')
deadline = fields.Date(string='Deadline')
priority = fields.Selection([
('0', 'Normal'),
('1', 'Important'),
('2', 'Urgent'),
], default='0')
tag_ids = fields.Many2many('my_module.tag', string='Tags')
progress = fields.Float(compute='_compute_progress', store=True)
Field Types Reference
| Field Type | Python Type | Use Case | |---|---|---| | Char | str | Short text (name, reference) | | Text | str | Long plain text | | Html | str | Rich text content | | Integer | int | Whole numbers | | Float | float | Decimal numbers | | Boolean | bool | True/False flags | | Date | date | Date without time | | Datetime | datetime | Date with time | | Selection | str | Dropdown choices | | Many2one | int | Link to one record | | One2many | list | Reverse of Many2one | | Many2many | list | Link to multiple records | | Binary | bytes | File attachments |
Computed Fields and Constraints
@api.depends('subtask_ids.state')
def _compute_progress(self):
for task in self:
total = len(task.subtask_ids)
done = len(task.subtask_ids.filtered(
lambda t: t.state == 'done'
))
task.progress = (done / total * 100) if total else 0
@api.constrains('deadline')
def _check_deadline(self):
for task in self:
if task.deadline and task.deadline < fields.Date.today():
raise ValidationError(
"Deadline cannot be in the past."
)
Inheritance Mechanisms
Odoo provides three types of inheritance, each serving a different purpose:
1. Class Inheritance (Extension)
Extend an existing model by adding fields or overriding methods. This is the most common pattern.
class SaleOrderExtend(models.Model):
_inherit = 'sale.order'
custom_reference = fields.Char(string='Custom Ref')
approved_by = fields.Many2one('res.users')
def action_confirm(self):
# Add custom logic before standard confirmation
for order in self:
if order.amount_total > 10000 and not order.approved_by:
raise UserError("Orders over $10,000 require approval.")
return super().action_confirm()
2. Prototype Inheritance
Create a new model that copies all fields and methods from an existing model.
class CustomPartner(models.Model):
_name = 'my_module.partner'
_inherit = 'res.partner' # Copies structure
_description = 'Custom Partner'
3. Delegation Inheritance
Create a new model that delegates to an existing model via a Many2one link. The parent model's fields appear on the child model transparently.
class LibraryMember(models.Model):
_name = 'library.member'
_inherits = {'res.partner': 'partner_id'}
partner_id = fields.Many2one('res.partner', required=True,
ondelete='cascade')
membership_date = fields.Date()
member_number = fields.Char()
The OWL Framework (Frontend)
Odoo 18 uses OWL (Odoo Web Library) as its frontend framework. OWL is a component-based framework similar to React or Vue but designed specifically for Odoo's needs.
Basic OWL Component
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
class TaskDashboard extends Component {
static template = "my_module.TaskDashboard";
setup() {
this.state = useState({
tasks: [],
filter: 'all',
});
this.loadTasks();
}
async loadTasks() {
this.state.tasks = await this.env.services.orm.searchRead(
"my_module.task",
[["state", "!=", "cancelled"]],
["name", "state", "assigned_to", "deadline"]
);
}
get filteredTasks() {
if (this.state.filter === 'all') return this.state.tasks;
return this.state.tasks.filter(
t => t.state === this.state.filter
);
}
}
QWeb Template (XML)
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_module.TaskDashboard">
<div class="o_task_dashboard">
<div class="task-filters">
<button t-on-click="() => state.filter = 'all'">All</button>
<button t-on-click="() => state.filter = 'in_progress'">
In Progress
</button>
</div>
<div class="task-list">
<t t-foreach="filteredTasks" t-as="task" t-key="task.id">
<div class="task-card">
<span t-esc="task.name"/>
</div>
</t>
</div>
</div>
</t>
</templates>
Security Configuration
Every model needs explicit access rules. Without them, no user can access the model's data.
Access Control List (ir.model.access.csv)
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_task_user,task.user,model_my_module_task,base.group_user,1,1,1,0
access_task_manager,task.manager,model_my_module_task,my_module.group_manager,1,1,1,1
Record Rules (security.xml)
<record id="task_own_rule" model="ir.rule">
<field name="name">Own Tasks Only</field>
<field name="model_id" ref="model_my_module_task"/>
<field name="domain_force">
[('assigned_to', '=', user.id)]
</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
Testing Your Module
Odoo supports Python unit tests using the unittest framework with Odoo-specific test classes:
from odoo.tests.common import TransactionCase
class TestTask(TransactionCase):
def setUp(self):
super().setUp()
self.task = self.env['my_module.task'].create({
'name': 'Test Task',
'state': 'draft',
})
def test_task_creation(self):
self.assertEqual(self.task.state, 'draft')
self.assertEqual(self.task.progress, 0)
def test_deadline_constraint(self):
with self.assertRaises(ValidationError):
self.task.write({
'deadline': fields.Date.subtract(
fields.Date.today(), days=1
),
})
Run tests with: odoo-bin -d test_db --test-enable -i my_custom_module --stop-after-init
Frequently Asked Questions
Q: How long does it take to build a custom Odoo module? Simple modules (new fields, basic views) take 1-3 days. Moderate modules (new models, workflows, reports) take 1-3 weeks. Complex modules (multi-model systems, external integrations, custom OWL components) take 4-12 weeks. For businesses that need custom modules but lack in-house Odoo developers, hire an experienced Odoo developer from our team.
Q: Should I modify core Odoo code or create a separate module? Always create a separate module. Modifying core code breaks upgradability and creates merge conflicts during version updates. Use inheritance to extend existing models and views from your custom module.
Q: What are OCA guidelines? The Odoo Community Association (OCA) publishes coding standards for module quality, including naming conventions, documentation requirements, test coverage expectations, and code style rules. Following OCA guidelines ensures your module is maintainable and compatible with the broader community ecosystem.
Getting Professional Help
Building custom Odoo modules requires expertise in Python, JavaScript, PostgreSQL, and Odoo's framework conventions. Whether you need a simple field extension or a complex multi-module system, ECOSIRE's Odoo customization services deliver production-ready modules built to OCA standards.
Contact us to discuss your custom module requirements and get a development estimate.
Written by
ECOSIRE Research and Development Team
Building enterprise-grade digital products at ECOSIRE. Sharing insights on Odoo integrations, e-commerce automation, and AI-powered business solutions.
Related Articles
AI-Powered Order Processing: How Automation Is Transforming eCommerce Fulfillment
How AI and machine learning automate order routing, fraud detection, demand forecasting, and customer service in modern eCommerce operations.
Amazon-Odoo Integration: The Complete 2026 Guide to Automating Your Amazon Business
Learn how to connect Amazon Seller Central with Odoo ERP for automated order sync, real-time inventory management, and unified financial reporting across all channels.
eBay-Odoo Integration: How to Automate Your eBay Selling Operations in 2026
Connect eBay with Odoo ERP to automate order management, inventory sync, and listing updates. Complete guide for eBay sellers scaling with Odoo.