Why custom Odoo development?
Odoo is a powerful ERP out of the box, but every business has processes that don’t fit standard modules. Custom development lets you make Odoo work the way your business works — not the other way around.
In this guide, I’ll walk you through building your first custom Odoo module from scratch.
Setting up your development environment
Prerequisites
- Python 3.10+
- PostgreSQL
- Git
- Docker (recommended for isolation)
The quick start with Docker
I recommend using Docker for Odoo development. It keeps your system clean and makes it easy to test different Odoo versions.
# docker-compose.yml
version: '3'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: odoo
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
volumes:
- odoo-db:/var/lib/postgresql/data
odoo:
image: odoo:17
depends_on:
- db
ports:
- "8069:8069"
volumes:
- odoo-data:/var/lib/odoo
- ./addons:/mnt/extra-addons
environment:
- HOST=db
- USER=odoo
- PASSWORD=odoo
volumes:
odoo-db:
odoo-data:
Run docker-compose up -d and navigate to http://localhost:8069 to access your Odoo instance.
Anatomy of an Odoo module
An Odoo module is a directory with a specific structure:
my_module/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ └── my_model.py
├── views/
│ └── my_model_views.xml
├── security/
│ └── ir.model.access.csv
└── data/
└── demo_data.xml
The manifest file
The __manifest__.py file is the entry point of your module. It declares the module’s metadata, dependencies, and data files:
{
'name': 'Custom Manufacturing',
'version': '17.0.1.0.0',
'summary': 'Custom manufacturing workflow with quality control',
'author': 'Mahmoud Hashem',
'category': 'Manufacturing',
'depends': ['mrp', 'quality_control'],
'data': [
'security/ir.model.access.csv',
'views/manufacturing_order_views.xml',
'views/quality_check_views.xml',
],
'installable': True,
'application': False,
'auto_install': False,
}
Creating your first model
Models in Odoo are Python classes that extend models.Model:
from odoo import models, fields, api
class ManufacturingOrder(models.Model):
_name = 'custom.manufacturing.order'
_description = 'Custom Manufacturing Order'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, default='New')
partner_id = fields.Many2one('res.partner', string='Customer', required=True)
product_id = fields.Many2one('product.product', string='Product', required=True)
quantity = fields.Float(string='Quantity', default=1.0, required=True)
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('in_progress', 'In Progress'),
('quality_check', 'Quality Check'),
('done', 'Done'),
('cancel', 'Cancelled'),
], string='Status', default='draft', tracking=True)
quality_check_ids = fields.One2many('custom.quality.check', 'order_id', string='Quality Checks')
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', 'New') == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code('custom.mfg.order') or 'New'
return super().create(vals_list)
def action_confirm(self):
self.state = 'confirmed'
def action_start_production(self):
self.state = 'in_progress'
Creating views
Views define the user interface. Here’s a form view for our manufacturing order:
<record id="view_manufacturing_order_form" model="ir.ui.view">
<field name="name">custom.manufacturing.order.form</field>
<field name="model">custom.manufacturing.order</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_confirm" type="object" string="Confirm"
class="btn-primary" invisible="state != 'draft'"/>
<button name="action_start_production" type="object" string="Start Production"
class="btn-primary" invisible="state != 'confirmed'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,confirmed,in_progress,quality_check,done"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" readonly="1"/></h1>
</div>
<group>
<field name="partner_id"/>
<field name="product_id"/>
<field name="quantity"/>
</group>
<notebook>
<page string="Quality Checks">
<field name="quality_check_ids">
<tree editable="bottom">
<field name="check_type"/>
<field name="result"/>
<field name="checked_by"/>
<field name="check_date"/>
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
Security and access rights
Every model needs access rights defined in security/ir.model.access.csv:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_custom_mfg_order_user,custom.mfg.order.user,model_custom_manufacturing_order,base.group_user,1,1,1,0
access_custom_mfg_order_manager,custom.mfg.order.manager,model_custom_manufacturing_order,base.group_system,1,1,1,1
Best practices I’ve learned
1. Follow the Odoo naming conventions
- Model names:
module.model_name(e.g.,custom.manufacturing.order) - Field names:
snake_case - View IDs:
view_model_type(e.g.,view_manufacturing_order_form)
2. Use inheritance wisely
Odoo has three types of inheritance:
- Classical inheritance (
_inherit): Extends an existing model - Prototype inheritance (
_inherits): Delegates to another model - Delegation inheritance: Uses
_inheritsfor composition
Use classical inheritance to add fields and methods to existing models. Use prototype inheritance when you need a new model that includes all features of an existing one.
3. Keep modules small and focused
Don’t build a monolithic module. Split functionality into small, focused modules that can be installed independently.
4. Write tests
Odoo has a built-in testing framework. Always write tests for your custom modules:
from odoo.tests import common
class TestManufacturingOrder(common.TransactionCase):
def setUp(self):
super().setUp()
self.Order = self.env['custom.manufacturing.order']
def test_create_order(self):
order = self.Order.create({
'partner_id': self.env.ref('base.res_partner_1').id,
'product_id': self.env.ref('product.product_product_1').id,
'quantity': 10.0,
})
self.assertEqual(order.state, 'draft')
order.action_confirm()
self.assertEqual(order.state, 'confirmed')
5. Document your code
Custom Odoo modules can be complex. Document your models, methods, and workflows. Future developers (including yourself) will thank you.
Deploying your module
To install your custom module:
- Copy the module directory to the
addonspath - Restart the Odoo service
- Update the apps list (Settings → Apps → Update Apps List)
- Search for your module and click Install
For production deployment, I use Docker with the module baked into the image. This ensures consistency across environments.
Conclusion
Custom Odoo development is powerful but requires understanding Odoo’s conventions and architecture. Start small, follow best practices, and test thoroughly. The effort you put into writing clean, maintainable modules pays off enormously when you need to upgrade or extend them later.