Skip to content
Back to blog
DevelopmentPythonArchitecture

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.

COConsultor Odoo26 May 20264 min read

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.

#Development#Python#Architecture
Share article

Comments (0)

Be the first to comment.

Sign in to leave a comment.

Sign in

Comments are reviewed before publishing.

Ready to get the most out of Odoo?

Tell us your challenge. In a first 30-minute call we'll tell you how Odoo can help, no strings attached.