Odoo Development
Odoo Development: How Custom Odoo Actually Gets Built
Almost everything people call an Odoo customisation is a module. Understanding what a module is, and what the framework will and will not let it do, explains most of the difference between a change that survives three upgrades and one that has to be rewritten every year.
What Odoo development actually is
Odoo is a framework before it is a product. The applications you install are written against the same object relational mapper, the same view engine and the same security model that your customisations will use. There is no separate extension API and no plugin sandbox: your code sits at the same level as Odoo's code and can reach anything it can reach.
That is the source of both its power and its risk. Adding a field to a sale order is a five-line change. So is accidentally making every sale order in the system recompute on save.
The unit of all of this is the module: a directory of Python, XML and CSV that Odoo can install, upgrade and uninstall as one thing. Every app Odoo ships is a module. Every app on the Odoo Apps Store is a module. Every customisation your team writes should be a module too, because that is the only form the system knows how to manage.
The module is the unit of everything
A module is a folder with a manifest and, usually, four kinds of file inside it.
- `__manifest__.py` declares the module: its name, version, dependencies, the data files to load and in what order, and its licence. Odoo reads this before anything else, and the `depends` list is what guarantees the models you are extending already exist when your code runs.
- Python models define or extend business objects. A model is a class, its fields are class attributes, and Odoo turns that into a PostgreSQL table with the columns, indexes and constraints it implies.
- XML data files define views, menus, actions, email templates, reports and any records that should exist on install. They are data, not code, which is why they can be edited by users afterwards and why that is sometimes a problem.
- `ir.model.access.csv` grants access. A new model with no line here is invisible to every user who is not a superuser, and that is the single most common reason a newly built feature appears to do nothing.
The manifest is worth more attention than it usually gets. `installable` and `license` have defaults you will inherit silently, `application` decides whether your module shows up as a tile, and `external_dependencies` is how you declare a Python package requirement rather than discovering it missing on the production server.
The four ways to change behaviour
Odoo customisation is mostly inheritance, and choosing the wrong kind is a decision you live with for years.
Classic extension is `_inherit` with no `_name`. You reopen an existing model, add fields, add methods, and override existing ones. The table stays the same and every other module extending that model still works. This is the correct answer roughly nine times out of ten.
Prototype inheritance is `_inherit` plus a different `_name`. You copy the structure into a brand new model with its own table. Useful when you genuinely want a separate object that behaves like an existing one, and wrong whenever you actually wanted the original record to change.
Delegation is `_inherits`, with the s. Your model holds a foreign key to another and transparently exposes its fields. This is how a product variant relates to its template. It is powerful and it is the one people misuse most, because the linked record has a lifecycle of its own.
View inheritance is the XML equivalent: you point at an existing view, locate an element with an XPath or a field name, and insert, replace or modify around it. The rule that matters is to inherit rather than replace. A view you copied wholesale is a view that will not receive any of the changes Odoo makes to it in the next version, and you will not be told.
Overriding a method follows the same principle. Call `super()`, do your work around it, and keep the override as small as the change requires. An override that reimplements the parent instead of calling it is a promise to re-read the parent's source at every upgrade.
Data files, XML IDs and the trap in noupdate
Every record created from XML has an external identifier, the XML ID, in the form `module_name.record_name`. That identifier is how Odoo knows on the next upgrade that this record already exists and should be updated rather than duplicated. Records created without one, or with one that changes, become orphans that accumulate quietly.
`noupdate="1"` tells Odoo to create the record once and never touch it again on upgrade. It is the right choice for anything a user is expected to edit, such as a default email template or a sequence, because otherwise your next module update would silently overwrite their changes. It is the wrong choice for anything you intend to keep improving, and the two categories are not always obvious at the time you write the file.
Security records deserve their own note. Access rights in `ir.model.access.csv` are per model and per group. Record rules, which filter which rows a user can see, are separate and are written in XML. Getting the first right and forgetting the second is how a multi-company setup ends up showing one company's records to another.
Where Studio ends and code begins
Odoo Studio can add fields, edit views, build automations and design reports without a developer, and for a large share of real requests it is the correct tool. The boundary is not about capability, it is about what happens next.
Write a module when the change needs tests, when the logic is complex enough that someone will need to read it in a year, when it touches an external system, when performance matters, or when the same change has to exist identically in more than one database. Use Studio when the change is small, local and unlikely to be revisited.
The failure mode to avoid is the database with two hundred undocumented Studio changes and no record of who made which. Studio exports its changes as a module, and using that export as the promotion path from test to production is the difference between a customisation you can audit and one you can only excavate.
What actually breaks at upgrade time
Odoo publishes a new major version every year, and version 19 ships tooling that makes the mechanical part of the work visible. `odoo-bin upgrade_code` runs a set of scripts against your source that rewrite known renames automatically: the `` view rename, the move of SQL constraints from the old `_sql_constraints` list to the `models.Constraint` class attribute, the JSON-RPC route type change, and more. Odoo's own documentation for it is blunt that these are best-effort and not silver bullets, which is the right expectation.
What the scripts cannot do is fix design. The changes that hurt at upgrade time are structural: views replaced instead of inherited, methods overridden instead of extended, fields renamed by a version that your code still refers to by the old name, and logic that depended on a core behaviour which is now implemented differently. None of that is detectable by search and replace.
Data migrations are the other half. A module can ship a `migrations/` directory with `pre-`, `post-` and `end-` scripts that run before initialisation, after it, and after every module has finished, respectively. If a field changes type or a model is split, that script is the only place the existing rows get fixed. Skipping it does not produce an error; it produces a database that installs cleanly and is quietly wrong.
What good looks like
A module you will be glad to own in three years tends to share the same characteristics. It depends on the smallest set of modules it actually needs. It extends rather than replaces. It has an `ir.model.access.csv` written deliberately rather than generated and forgotten. It has tests for the logic that would be expensive to get wrong. Its manifest version moves when it changes. And it is small enough that its purpose can be explained in a sentence.
The opposite is also recognisable: one large module that does everything, depends on half of Odoo, contains copied core views, and has a name nobody remembers the reason for. It works, right up until the version it was written for stops being supported.
FAQ
Odoo Development, answered.
What language is Odoo written in?
Python on the server, with PostgreSQL underneath, XML for views and data, and JavaScript for the web client. Since version 16 the client framework is OWL, Odoo's own component library, which replaced the older widget system. A backend-only customisation may never touch JavaScript at all.
Do I need to be a Python developer to build an Odoo module?
You need Python, but the harder requirement is the framework. Odoo has strong opinions about how models, fields, views and access rules fit together, and a competent Python developer who ignores those conventions will produce something that works once and breaks at the first upgrade. The framework is the skill, not the language.
Can I modify Odoo's own code instead of writing a module?
You can, and you should not. Editing core files means your changes are silently discarded the moment the source is updated, and there is no record of what was changed or why. Inheritance exists precisely so customisations live in your own module and core stays untouched.
How long does a custom Odoo module take to build?
A single field with a bit of logic is hours. A well-specified feature with views, access rights and reports is usually days. Anything involving an external system, document generation or a nontrivial calculation runs into weeks, mostly because the edge cases are discovered during testing rather than during specification.
Will my custom modules survive an Odoo upgrade?
That depends almost entirely on how they were written. Modules that extend cleanly through inheritance and avoid copying core views usually need small adjustments per version. Modules that override large methods wholesale, or replace core views instead of inheriting them, tend to need real rework each time.
What is the difference between a module and an app?
Technically nothing. An app is a module whose manifest sets application to True, which is what makes it appear as a tile in the Apps menu rather than only in the technical module list. Everything else about the two is identical.
Should custom development be done by our team or an external one?
The deciding factor is usually continuity rather than cost. Odoo code is cheap to write and expensive to inherit, so whoever builds it should still be reachable at the next upgrade. If neither option gives you that, insist on documentation and tests as part of the deliverable rather than as an extra.
Related
Keep exploring.
Other Odoo apps we have written up in the same depth.
With CODEerts
Need something built properly?
We are certified Odoo partners. We build custom Odoo modules that extend the framework rather than fight it, and we publish our own apps on the Odoo Store.
See how we can helpBook a callReady to make Odoo work the way your business does?
Book a free callCODEerts is a team of certified Odoo partners and full-stack engineers. We implement, customise and support Odoo ERP, then build the software around it.