Skip to Content

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.

Written by Tayyab RasheedOdoo 19 Certified, Technical Consultant

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 <tree> to <list> 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.

In this guide

Go deeper on one part of it.

The pillar above is the overview. These take one question each and answer it properly.

Odoo Module Structure: Every File and What It DoesAn Odoo module is a directory, and only a few things in it are enforced by the framework. Knowing which parts Odoo genuinely requires and which are convention is what stops you cargo-culting a layout you cannot explain.Read itWriting an Odoo Manifest File CorrectlyThe manifest is the first file Odoo reads and the one people copy without reading. Three keys have no default, several have defaults that will quietly surprise you, and one mistake in version can make your module vanish from the Apps list with only a log line to explain it.Read itCreating Your First Odoo ModelA model is a Python class that becomes a PostgreSQL table. Writing one takes ten minutes. Making it visible to a user who is not the administrator takes a CSV line that nobody tells you about, and its absence is the most common reason a new feature appears to do nothing at all.Read itOdoo Model Inheritance: Extension, Prototype and DelegationOdoo has three inheritance mechanisms that look similar and behave nothing alike. One reopens a model, one copies it, one links to it. Choosing wrong is not a style mistake, it is a decision about where your data physically lives, and it is expensive to reverse once records exist.Read itOdoo Computed Fields: Store, Depends and InverseA computed field is a method that assigns a value to every record it is given. Almost all the difficulty is in two decisions: whether to store it, and whether the depends list is complete. Get the second wrong and the field is right on save and wrong forever after.Read itOdoo Related Fields and When They Hurt PerformanceA related field is a computed field wearing a shortcut. That single fact explains its performance behaviour, its storage behaviour, and why a stored related field is a copy of data rather than a view of it.Read itOdoo Constraints: Python vs SQL, and What Changed in v19Odoo gives you two places to enforce a rule: Python, which is flexible and only runs when the ORM runs, and the database, which is absolute and cannot be bypassed. Odoo 19 changed how you declare the second one, and the old syntax now does nothing at all while logging a warning nobody reads.Read itOdoo Wizards and Transient ModelsA wizard is a dialog that collects input before an action runs, and underneath it is an ordinary model with one difference: its records are temporary and get cleaned up automatically. That single difference drives everything about how you write one.Read itOdoo Scheduled Actions and Cron JobsA scheduled action is a server action Odoo runs on a timer. The framework part is simple. The hard part is writing a job that behaves correctly when it runs late, runs twice, or runs against ten thousand records instead of the ten you tested with.Read itOdoo Server Actions and Automated Actions in CodeServer actions are Odoo's way of expressing behaviour as data rather than code. Combined with automated rules they cover a lot of ground with no module at all, which is genuinely useful and quietly becomes a maintenance problem the moment nobody remembers they exist.Read itOdoo Data Files, XML IDs and noupdateEverything in Odoo that is not code is a record, and records shipped by a module are identified by XML IDs. Understanding what that identifier is, and what noupdate does to it on the next upgrade, is the difference between a module that updates cleanly and one that overwrites a customer's configuration every time.Read itOdoo Module Upgrade Scripts and Data MigrationsWhen a module version changes the meaning of stored data rather than just its shape, existing rows need converting. Odoo's mechanism is a folder of Python scripts named by version and phase, and the most common mistake is not writing a bad one, it is writing none and never noticing.Read itOdoo Coding Standards and a Module Review ChecklistMost Odoo review comments are about the same dozen things. Collecting them into a checklist turns review from an opinion exchange into a pass or fail, which is the only version of review that survives a deadline.Read itPublishing a Module on the Odoo Apps StorePublishing on the Odoo Apps Store is less about the code than people expect. The technical bar is a module that installs cleanly on a fresh database. The real commitment is the one that starts afterwards, because every module you list is one you now maintain for every Odoo version you claimed to support.Read itCould Not Serialize Access Due to Concurrent UpdateThis error is PostgreSQL refusing to let two transactions commit incompatible changes to the same row. Odoo expects it, catches it and retries five times before you ever see it, which is why the message reaching a user means something more specific than a collision.Read it

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 call