Khichdi InfoTech
Skip to content

Odoo Customization · intermediate · 12 min read

How Custom Odoo Modules Work

A practical tour of custom Odoo modules: __manifest__.py, models, view XML, security CSV, data XML, dependencies, install vs upgrade, and _inherit.

Last reviewed 2026-07-30 · Estimated effort: 2–6 weeks typical implementation slice

Table of contents

A custom Odoo module is a Python package on your `addons_path` that declares metadata in `__manifest__.py`, loads models into the ORM registry, extends UI with XML, and protects data with access rights and record rules. Odoo applies that package when you install or upgrade the module.

Inheritance is the core extension pattern: `_inherit` extends existing models without copying them; view records with `inherit_id` splice XML into standard forms. Doing this correctly keeps core addons untouched and makes upgrades tractable.

Install creates the module’s tables and loads data; upgrade re-applies changed Python/XML and can run migration scripts. Confusing those modes—or editing files without `-u`—is a common source of “it works on my machine” bugs.

You do not need to memorize every framework hook to ship a solid module, but you do need the skeleton: manifest, models, views, security, and data. Missing any one of those pieces produces modules that install with Access Errors or invisible menus.

This article walks the skeleton in the order you should build it, then explains install versus upgrade and `_inherit` behaviour you will use constantly.

Every module needs `__manifest__.py` (or legacy `__openerp__.py`) with at least `name`, `version`, `depends`, `data`, and usually `license` and `installable`. The `depends` list must include every module whose models or XML IDs you reference—typically `base` plus apps like `sale`, `stock`, or `account`.

List XML/CSV files in `data` (and `demo` if needed) in load order: security before views that need groups, data before menus that reference records. Set `application` only for true top-level apps. Keep `version` meaningful so operators know what shipped to production.

  • `depends`: hard requirements loaded before your module
  • `data`: security CSV/XML, views, actions, menus, cron, templates
  • `assets`: backend/frontend JS/CSS bundles when you extend the web client
  • `external_dependencies`: Python packages that must exist on the server

Python files under `models/` define classes inheriting `models.Model`. Use `_name` for a new model and `_inherit` to extend an existing one (optionally with `_name` when creating a new model that also inherits behaviour). Register imports in `models/__init__.py` and the module root `__init__.py`.

Fields (`fields.Char`, `Many2one`, `One2many`, `Monetary`, etc.), `@api.depends` computes, `@api.constrains`, and overrides of `create`/`write`/`unlink` live here. Prefer extending standard methods with `super()` rather than copying entire methods. Keep business rules close to the model that owns the data.

View files declare `ir.ui.view` records. Extension views set `inherit_id` to the XML ID of a standard view and use XPath (`expr` + `position`) to insert fields, buttons, or pages. Primary views set a full `arch` for brand-new models.

Pair views with `ir.actions.act_window` and `menuitem` records so users can open them. Stable XML IDs matter: other modules, Studio, and tests will reference them. Target parent view IDs that Odoo documents as public extension points when possible.

`security/ir.model.access.csv` grants model-level CRUD to groups (`perm_read`, `perm_write`, `perm_create`, `perm_unlink`). Without rows for a new `_name`, only the superuser can use the model—classic “Access Error” after install.

Record rules (`ir.rule` in XML) filter rows by domain for multi-company, salespeople seeing own documents, or warehouse isolation. Add rules intentionally; an empty or wrong global rule can hide all records or expose too much. Also create `res.groups` XML when your module introduces new roles instead of overloading `base.group_user`.

Data XML creates noupdate-sensitive records: sequences, default stages, email templates, server actions. Use `noupdate="1"` for records operators may edit later so upgrades do not wipe their changes. Keep demo data in `demo` so production installs stay clean.

Installing a module (`-i`) creates schema and loads all data files. Upgrading (`-u`) reloads changed definitions: new fields, view updates, and updated CSV rights. After pulling Git changes that alter Python or XML, always upgrade the module on that database. Major version jumps may add `migrations/<version>/` scripts for data transforms.

  • Dev cycle: edit → restart/load → `-u your_module` on the DB → test
  • Never assume file save alone refreshes ORM fields in a running worker pool
  • Pin `depends` tightly; undeclared dependencies cause intermittent load failures

Classic extension: `_inherit = 'sale.order'` without a new `_name` patches the existing model in the registry. Classic delegation or prototype inheritance patterns exist for composition, but most business customizations use extension inheritance plus view inheritance.

When multiple modules inherit the same model, method Resolution Order (MRO) and `super()` chains matter. Keep overrides small, call `super()`, and avoid monkey-patching (`model.method = ...`) outside the ORM. If two modules fight over the same button, consolidate or coordinate depends order explicitly.

  • Scaffold modules with security CSV on day one—even for prototype models.
  • Name XML IDs with a module prefix (`my_module.view_order_form_inherit`).
  • Prefer small `_inherit` methods with `super()` over pasted core methods.
  • Keep `depends` honest; add a dependency rather than duplicating a model.
  • Use staging databases to practice `-u` before production maintenance windows.
  • Add at least one smoke test or automated UI path for critical overrides.
  • Document required server restart / upgrade steps in the module README.

  • !Forgetting `ir.model.access.csv` for new models.
  • !Putting view XML in `data` before security groups those views reference.
  • !Editing core addons instead of `_inherit` / `inherit_id`.
  • !Changing Python fields locally and testing without module upgrade.
  • !Using `noupdate="0"` on operator-owned data that upgrades keep resetting.
  • !Circular or missing `depends` that only fail on clean database installs.

Custom modules are predictable when you respect the skeleton: manifest dependencies, ORM models, XML views, security, and deliberate install/upgrade cycles. Inheritance keeps core code intact while letting you encode real process.

Once the skeleton is clear, invest in upgrade-friendly patterns and avoid the mistakes that make modules toxic at go-live—covered in the mistakes and best-practices articles.

Module folder tree: __manifest__.py, models/, views/, security/, data/, report/.
  • Client / Channel

    Users & devices

  • Odoo Application

    Business logic

  • PostgreSQL

    System of record

  • Workers / Cron

    Async work

  • clientapp(HTTP)
  • appdb(ORM)
  • appjobs(queue)
Flow from Git pull → workers → -u module → registry reload → test.
1

Trigger

Business event starts the flow.

2

Execute

Operate in the system of record.

3

Validate

Check outcomes and exceptions.

4

Close

Reconcile and hand off.

Frequently asked questions

What is the difference between install and upgrade?

Install (`-i`) loads a module for the first time: creates tables and imports all data files. Upgrade (`-u`) reapplies module definitions to an existing database—new columns, updated views, changed access—and can run migration scripts. Use upgrade whenever module code changes on a DB that already has it installed.

Do I need both _name and _inherit?

For extending `sale.order`, use only `_inherit = 'sale.order'`. Use `_name` alone for a brand-new model. Use both when creating a new model that should also inherit another model’s fields/behaviour (less common for simple customizations).

Where do access rights files go?

Conventionally `security/ir.model.access.csv`, listed early in the manifest `data` list. Record rules usually live in `security/*.xml`. Load groups before CSV rows that reference those groups.

Why is my inherited field missing on the form?

The Python field may be loaded while the view XML was not upgraded, XPath failed silently against a changed parent arch, or the user lacks field/model rights. Check upgrade logs, view inheritance architecture, and access rights before rewriting the model.

Need a module built or reviewed?

Dedicated Odoo developers can scaffold upgrade-minded addons, review inheritance, and set up staging upgrade habits with your team.

Business technology and software delivery