Anatomy of an Odoo module: what's inside and why it matters
We open up an Odoo module —manifest, models, fields, security and views— to understand why almost everything is declared instead of coded.
Part of our complete guide: Odoo module development.
The first time I opened an Odoo module I expected to find code. Lots of it. Loops, controllers, HTML templates stitched together by hand. What I found was something else: a handful of small files where almost everything was a declaration. "This is a model. It has these fields. It's shown like this. This group can see it." The engine does the rest.
That's the idea that takes the longest to internalise and, at the same time, the one that explains why a good module ages well. Let's open the box piece by piece.
The manifest: the module's ID card
Every module starts with __manifest__.py. It has no logic; it's the identity card Odoo reads to know what to load, in what order and what it depends on.
{
"name": "Real Estate",
"version": "1.0",
"depends": ["base", "mail"],
"data": [
"security/ir.model.access.csv",
"views/property_views.xml",
"views/property_menus.xml",
],
"application": True,
"license": "LGPL-3",
}
There are already important decisions here. The order of the data list is the load order: if a view references a permission that doesn't exist yet, Odoo complains. And depends is not decorative: it declares which modules yours builds on, and that determines what you can inherit. A careless manifest is the first sign that the rest of the module is going to hurt.
Models: a Python class that becomes a table
The heart of the module is the model. A class that inherits from models.Model and that, almost like magic, turns into a PostgreSQL table without you writing a single line of SQL.
from odoo import models, fields
class Property(models.Model):
_name = "estate.property"
_description = "Real Estate Property"
name = fields.Char(required=True)
expected_price = fields.Float()
bedrooms = fields.Integer(default=2)
date_availability = fields.Date()
The _name is the model's global identity across all of Odoo: it's used to build the table (estate_property), to reference permissions and to link relations. The _description looks like a cosmetic detail, but Odoo uses it in error messages and warnings; leaving it out is one of those things that gives away a rushed job.
The field type says more than it seems
Choosing between Char, Text, Selection or Many2one isn't just choosing how the data is stored: it defines how it's validated, how it's indexed and how it looks in the interface. A Selection gives you a closed, consistent list; a Char leaves the door open for every user to type "Madrid", "madrid" and "MADRID". That decision, made in thirty seconds, shapes your data quality for years.
That's why I keep telling teams: modelling isn't the boring part before "the interesting stuff". Modelling is the interesting stuff. We dig into it in our module best-practices guide.
Security isn't an afterthought
This is where many people trip up. The moment you create a model, Odoo needs to know who can read, write or delete it. If you don't tell it, the model exists but nobody can see it. The minimum viable version lives in a CSV:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_property_user,property.user,model_estate_property,base.group_user,1,1,1,1
This is only half the story —model-level permissions—. The other half, the rules that filter which records each user sees, is where the most expensive mistakes concentrate. We gave it a whole article: access rights vs record rules.
Views are XML, not templates
Odoo's interface isn't "programmed". It's declared. You tell it which fields you want and in what order, and the framework draws the form, the list or the kanban.
<record id="estate_property_view_form" model="ir.ui.view">
<field name="name">estate.property.form</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="expected_price"/>
<field name="bedrooms"/>
</group>
</sheet>
</form>
</field>
</record>
It takes getting used to if you come from a world where you control every <div>. But this surrender is exactly what gives you the responsive layout, dark mode, filters and batch editing for free. Fighting the framework to nail a pixel usually gets expensive; learning to think in declarations doesn't.
What I learned opening other people's modules
I've audited plenty of inherited modules, and the problems are almost never in the business logic. They're in the foundations: manifests with badly declared dependencies, Char fields where there should have been relations, views that rewrite the entire form instead of inheriting it. When the migration to the next version arrives, those shortcuts charge interest.
Understanding a module's anatomy won't turn you into an Odoo developer overnight, but it gives you something more valuable: the judgement to tell a module that will last from one that will become a burden. And if you're going to invest in custom development, that judgement saves you money.
Got a custom module that already gives you trouble on every upgrade, or a new build on your hands? In a diagnosis session we review it and tell you straight whether the foundations hold. You can also see how we approach module development.
Comments (0)
Be the first to comment.
Sign in to leave a comment.
Sign inComments are reviewed before publishing.
Related articles
Automated actions in Odoo: let the ERP do the boring work
Server actions, automation rules and scheduled jobs: the three pieces that make Odoo react, notify and run on its own —without anyone having to remember.
PDF reports with QWeb in Odoo: from the form to the document that closes deals
QWeb is HTML that turns into PDF. We show you how to build a report, loop over records with t-foreach, and why t-field saves you a thousand headaches.
Inheritance in Odoo: the feature that decides whether your code survives upgrades
Classical extension, delegation and view inheritance. The three ways to modify Odoo without touching the core —and why inheriting instead of copying saves you every migration.