Anatomy of an Odoo Module

Series: Odoo 18 Development for Python Developers Target Audience: Python developers who are new to web development and want to learn Odoo Prerequisites: Lesson 1 completed, Odoo 18 development environment running


Table of Contents

What is a Module? (Compared to a Python Package)

If you’ve worked with Python, you’re familiar with packages — a folder with an __init__.py file that groups related code together. An Odoo module is similar, but with extra structure and superpowers.

Python Package vs Odoo Module

Feature Python Package Odoo Module
Required files __init__.py __init__.py + __manifest__.py
Purpose Organize code Organize code + data + UI + security
Installation pip install Install via Odoo Apps menu or CLI
Can be enabled/disabled No (once imported, it’s there) Yes (install/uninstall at runtime)
Has metadata setup.py or pyproject.toml (optional) __manifest__.py (required)
Can modify other packages Not really Yes (through inheritance)

What Makes a Module a Module?

An Odoo module is a directory that contains:

  1. __init__.py — Makes it a Python package (just like regular Python)
  2. __manifest__.py — The module’s ID card. Without this file, Odoo won’t recognize the directory as a module

That’s the minimum. Just these two files, and Odoo will see it as a valid module. Of course, a module with only these two files doesn’t do anything useful — but it’s technically valid.

Real-World Analogy

Think of an Odoo module like a smartphone app:

  • It has a name, version, and description (like an app in the App Store)
  • It can be installed and uninstalled
  • It can depend on other apps (e.g., Instagram needs Camera access)
  • It can extend the system’s capabilities
  • Multiple apps can work together

In Odoo, instead of an App Store, you have the Apps menu. Instead of “Camera access,” you have module dependencies.


Module Directory Structure Deep Dive

Let’s look at the complete structure of a well-organized Odoo module. We’ll use our library_app module as the example:

library_app/                        # Root directory — the module's technical name
│
├── __init__.py                     # Root init — imports sub-packages
├── __manifest__.py                 # Module metadata (name, version, dependencies)
│
├── models/                         # Python files — your business logic
│   ├── __init__.py                 # Imports all model files
│   ├── book.py                     # The library.book model
│   └── member.py                   # The library.member model
│
├── views/                          # XML files — user interface definitions
│   ├── book_views.xml              # Form, list, search views for books
│   ├── member_views.xml            # Views for members
│   └── menu_views.xml              # Menu structure
│
├── security/                       # Access control
│   ├── ir.model.access.csv         # Model-level permissions (CRUD)
│   └── library_security.xml        # Groups and record rules
│
├── data/                           # Data loaded when module is installed
│   └── book_data.xml               # Default records (categories, sequences, etc.)
│
├── demo/                           # Demo data (only loaded if demo mode is on)
│   └── book_demo.xml               # Sample records for testing
│
├── wizard/                         # Transient models (popup forms)
│   ├── __init__.py
│   ├── book_borrow_wizard.py       # Python logic for the wizard
│   └── book_borrow_wizard_views.xml # XML view for the wizard
│
├── report/                         # PDF report templates
│   ├── book_report.xml             # QWeb template for the report
│   └── book_report_action.xml      # Report action definition
│
├── controllers/                    # Web controllers (HTTP routes)
│   ├── __init__.py
│   └── main.py                     # API endpoints or web pages
│
├── static/                         # Static files (no __init__.py needed)
│   ├── description/
│   │   ├── icon.png                # Module icon (shown in Apps menu)
│   │   └── index.html              # Module description page (optional)
│   └── src/
│       ├── js/                     # Custom JavaScript / OWL components
│       ├── css/                    # Custom stylesheets
│       └── xml/                    # OWL component templates
│
├── i18n/                           # Translation files
│   ├── vi.po                       # Vietnamese translation
│   └── fr.po                       # French translation
│
└── tests/                          # Automated tests
    ├── __init__.py
    ├── test_book.py                # Unit tests for the book model
    └── test_borrow.py              # Tests for borrowing logic

Don’t Panic!

This looks like a lot of files, but remember:

  • You don’t need all of them to start. A minimal module has just __init__.py and __manifest__.py
  • You add directories as you need them. Start simple, grow as your module gets more features
  • The structure is a convention, not a strict rule. But following it makes your code predictable and easy for other developers to read

Directory Purpose Summary

Directory Contains When to Add
models/ Python classes (database models, business logic) Almost always — this is where your core code lives
views/ XML files (forms, lists, menus, actions) Almost always — users need a UI
security/ CSV and XML files (access rights, record rules) Almost always — you need to control who sees what
data/ XML/CSV files (default data loaded on install) When your module needs pre-configured data
demo/ XML/CSV files (sample data for testing) When you want sample data for development
wizard/ Python + XML (popup dialogs, multi-step forms) When you need interactive user dialogs
report/ XML (PDF report templates) When users need printable documents
controllers/ Python (HTTP endpoints) When you need web APIs or public pages
static/ JS, CSS, images, OWL templates When you customize the web client UI
i18n/ .po files (translations) When your module needs to support multiple languages
tests/ Python test files Always (you should always write tests!)

The __manifest__.py File — Every Field Explained

The __manifest__.py file is the single most important file in any Odoo module. It’s a Python dictionary that tells Odoo everything it needs to know about your module.

Here’s a complete example with every commonly used field:

# library_app/__manifest__.py

{
    # =============================================
    # BASIC INFORMATION
    # =============================================

    'name': 'Library Management',
    # The human-readable name of your module.
    # This is what appears in the Apps menu.
    # Keep it short and descriptive.

    'version': '18.0.1.0.0',
    # Module version number.
    # Convention: ODOO_VERSION.MAJOR.MINOR.PATCH
    # '18.0' = Odoo version
    # '1.0.0' = Your module version (major.minor.patch)

    'summary': 'Manage a library of books and members',
    # A one-line summary shown below the module name in the Apps menu.
    # Keep it under 80 characters.

    'description': """
        Library Management System
        =========================

        This module allows you to:
        * Manage a catalog of books
        * Track book authors and categories
        * Register library members
        * Handle book borrowing and returns

        **Key Features:**
        * Book inventory management
        * Member registration
        * Borrowing workflow with due dates
        * Overdue book notifications
    """,
    # A detailed description of what your module does.
    # Supports reStructuredText (RST) formatting.
    # Shown when the user clicks on the module in the Apps menu.
    # Tip: You can also put this in static/description/index.html for richer formatting.

    'author': 'Your Name',
    # Who wrote this module. Can be a person or company name.

    'website': 'https://your-website.com',
    # Optional: link to the author's website or module documentation.

    'category': 'Services/Library',
    # Category for organizing modules in the Apps menu.
    # Also used to create security groups automatically.
    # Common categories: Sales, Accounting, Inventory, HR, Website, Tools, Services

    'license': 'LGPL-3',
    # License type. Common values:
    # 'LGPL-3'  — for community modules (most common)
    # 'OEEL-1'  — for Odoo Enterprise modules
    # 'GPL-3'   — for strictly open-source modules

    # =============================================
    # DEPENDENCIES
    # =============================================

    'depends': ['base', 'mail'],
    # List of modules that MUST be installed before this one.
    # 'base' — always include this (it provides core models like res.partner)
    # 'mail' — adds chatter (message/activity tracking) to your models
    #
    # When your module is installed, Odoo automatically installs all dependencies first.
    # IMPORTANT: Only list DIRECT dependencies, not transitive ones.
    # If 'mail' depends on 'base', you don't need to list 'base' explicitly
    # (but it's common practice to include it for clarity).

    'external_dependencies': {
        'python': ['requests'],       # Python packages (installed via pip)
        'bin': ['wkhtmltopdf'],       # System binaries
    },
    # Optional: External libraries your module needs.
    # 'python' — pip packages that must be installed
    # 'bin' — system binaries that must exist in PATH
    # Odoo checks these at install time and shows an error if they're missing.

    # =============================================
    # DATA FILES
    # =============================================

    'data': [
        'security/library_security.xml',    # Load security groups first
        'security/ir.model.access.csv',     # Then access rights
        'views/book_views.xml',             # Then views
        'views/menu_views.xml',             # Then menus
        'data/book_data.xml',               # Then default data
    ],
    # List of data files to load when the module is INSTALLED or UPGRADED.
    # ORDER MATTERS! Files are loaded in the order listed here.
    #
    # Typical loading order:
    # 1. Security groups (other files may reference them)
    # 2. Access rights (CSV)
    # 3. Views (XML)
    # 4. Menus and actions
    # 5. Default data
    #
    # Supported file types: .xml and .csv

    'demo': [
        'demo/book_demo.xml',
    ],
    # Demo data files — only loaded when the database is created with
    # the "Load demonstration data" checkbox enabled.
    # Useful for testing and showcasing the module.

    # =============================================
    # TECHNICAL FLAGS
    # =============================================

    'installable': True,
    # If True, the module appears in the Apps menu and can be installed.
    # Set to False to hide a module (e.g., if it's deprecated or broken).
    # Default: True

    'application': True,
    # If True, the module appears as a main application in the Apps menu
    # (with a larger icon and prominent placement).
    # If False, it appears only when the "Extra" filter is applied.
    # Set True for top-level apps, False for small utility modules.

    'auto_install': False,
    # If True, the module is automatically installed when ALL of its
    # dependencies are installed. Used for "bridge" modules.
    # Example: A module with depends=['sale', 'stock'] and auto_install=True
    # will be installed automatically when both Sale and Inventory are installed.
    # Default: False (you almost always want this to be False)

    # =============================================
    # ASSETS (JavaScript, CSS for the web client)
    # =============================================

    'assets': {
        'web.assets_backend': [
            'library_app/static/src/js/**/*',     # All JS files
            'library_app/static/src/css/**/*',     # All CSS files
            'library_app/static/src/xml/**/*',     # All OWL templates
        ],
    },
    # Optional: Register static assets (JS, CSS, XML) with the web client.
    # 'web.assets_backend' — loaded in the main Odoo interface
    # 'web.assets_frontend' — loaded on the public website
    # We'll cover this in detail in Lesson 14 (OWL components).
}

The Most Important Fields (Minimum Viable Manifest)

If you’re just starting, you only need these fields:

# Minimum __manifest__.py
{
    'name': 'Library Management',
    'version': '18.0.1.0.0',
    'depends': ['base'],
    'data': [],
    'installable': True,
    'application': True,
}

Everything else can be added later as your module grows.

Common Mistakes

Mistake Problem Fix
Forgetting a file in data Views/security not loaded, module seems broken Add the file path to the data list
Wrong file order in data Errors like “External ID not found” Load security → views → menus → data
Missing dependency in depends KeyError or model not found Add the module that provides the model you need
Typo in file path in data FileNotFoundError on install/upgrade Double-check the path matches the actual file location

The __init__.py Chain — How Odoo Discovers Your Code

In regular Python, __init__.py makes a directory a package. In Odoo, it serves the same purpose — but there’s a specific pattern for how __init__.py files chain together.

The Chain Explained

Odoo only reads the root __init__.py of your module. From there, your code must import everything else. Here’s how it works:

Odoo starts here
      │
      ▼
library_app/__init__.py          # Root init — imports sub-packages
      │
      ├──► models/__init__.py    # Models init — imports model files
      │       │
      │       ├──► book.py       # Defines library.book model
      │       └──► member.py     # Defines library.member model
      │
      ├──► wizard/__init__.py    # Wizard init — imports wizard files
      │       │
      │       └──► book_borrow_wizard.py
      │
      └──► controllers/__init__.py
              │
              └──► main.py

The Root __init__.py

# library_app/__init__.py

from . import models          # Import the models sub-package
from . import wizard          # Import the wizard sub-package (if it exists)
from . import controllers     # Import the controllers sub-package (if it exists)

What’s happening:

  • from . import models — This tells Python to run models/__init__.py
  • The dot (.) means “from the current package” (relative import)
  • You only import directories that contain Python files with Odoo models or controllers
  • You do NOT import views/, security/, data/, static/, or i18n/ — those are data directories, not Python packages

Sub-package __init__.py Files

# library_app/models/__init__.py

from . import book            # Import book.py (which defines the library.book model)
from . import member          # Import member.py (which defines the library.member model)

What’s happening:

  • from . import book — This runs the code in book.py, which registers the model with Odoo’s ORM
  • Each Python file that defines a model must be imported here
  • If you create a new model file (e.g., category.py), you must add from . import category here

Why Is This Necessary?

In Django, models are auto-discovered by the framework. In Odoo, you must explicitly import every file through the __init__.py chain. If you forget to import a file, Odoo won’t know it exists, and your model/controller won’t be registered.

This is a very common mistake for beginners:

# You created models/category.py with a new model...
# But forgot to add this line in models/__init__.py:
from . import category   # ← Without this, Odoo doesn't see your model!

Quick Rule of Thumb

Directory Needs __init__.py? Needs import in parent __init__.py?
models/ Yes Yes
wizard/ Yes Yes
controllers/ Yes Yes
tests/ Yes No (Odoo auto-discovers tests)
views/ No No (loaded via __manifest__.pydata)
security/ No No (loaded via __manifest__.pydata)
data/ No No (loaded via __manifest__.pydata)
demo/ No No (loaded via __manifest__.pydemo)
static/ No No (loaded via __manifest__.pyassets)
i18n/ No No (auto-discovered by Odoo)
report/ No* No*

*Reports are usually XML-only (loaded via data in manifest). If a report has Python logic, it goes in models/ or its own package with __init__.py.


Module Dependencies and Load Order

Why Dependencies Matter

When you write 'depends': ['base', 'mail'] in your manifest, you’re telling Odoo:

“My module needs base and mail to work. Please install them first.”

This is important because:

  1. Models from dependencies are available to your module. If you depend on mail, you can inherit from mail.thread in your model.
  2. Views from dependencies exist before yours. If you want to extend a view from the sale module, you must depend on sale.
  3. Security groups from dependencies are available. If you reference a group defined in another module, you need that module as a dependency.

How Load Order Works

When you install your module, Odoo:

  1. Checks all dependencies recursively
  2. Installs any missing dependencies first (in dependency order)
  3. Loads your module last

When Odoo loads a module, it follows this order:

1. Run __init__.py chain (registers Python models)
2. Load 'data' files in the order listed in __manifest__.py
3. Load 'demo' files (if demo mode is enabled)

Dependency Examples

# Scenario 1: Basic module (most modules start here)
'depends': ['base'],
# 'base' provides: res.partner (contacts), res.users (users),
# res.company (companies), ir.* (infrastructure models)

# Scenario 2: Module with chatter (message tracking)
'depends': ['base', 'mail'],
# 'mail' provides: mail.thread mixin, activity tracking, followers

# Scenario 3: Module extending Sales
'depends': ['sale'],
# 'sale' already depends on 'base', so you don't need to list 'base' explicitly
# But many developers still list it for clarity

# Scenario 4: Module connecting Sales and Inventory
'depends': ['sale', 'stock'],
# Both Sale and Inventory must be installed

Common Dependency Pitfall

# ❌ WRONG: Depending on a module just to use its features in the UI
'depends': ['sale', 'purchase', 'stock', 'account'],
# This forces ALL these modules to be installed!

# ✅ RIGHT: Only depend on what your Python code actually needs
'depends': ['base', 'mail'],
# If you only add fields to res.partner and use mail.thread,
# you only need 'base' and 'mail'

Rule of thumb: Only add a dependency if your Python code directly imports or inherits from a model in that module, or if your XML files reference records (views, groups, actions) defined in that module.


Installing, Upgrading, and Uninstalling Modules

Understanding the module lifecycle is crucial for development.

Installing a Module

There are two ways to install a module:

Method 1: Via the Apps Menu (UI)

  1. Go to Apps in the main menu
  2. Remove the “Apps” filter in the search bar (to see all modules)
  3. Search for your module’s name
  4. Click the “Install” button

If you don’t see your module:

  • Make sure your module directory is in the addons_path (check odoo.conf or docker-compose.yml)
  • Click “Update Apps List” in the Apps menu (only visible in Developer Mode)
  • Check that your __manifest__.py has no syntax errors

Method 2: Via Command Line (Faster for development)

# Docker method:
docker compose exec odoo odoo -d odoo18dev -i library_app --stop-after-init

# Source method:
python odoo/odoo-bin -c odoo.conf -d odoo18dev -i library_app --stop-after-init

What the flags mean:

  • -d odoo18dev — Target database name
  • -i library_app — Install this module (-i = install)
  • --stop-after-init — Stop the server after installation (useful for scripts)

Upgrading a Module

When you change Python code, XML views, or data files, you need to upgrade the module for changes to take effect.

When Do You Need to Upgrade?

What You Changed Restart Server? Upgrade Module?
Python code (logic only) Yes No (if dev_mode=reload is on, auto-restarts)
Python code (new field added) Yes Yes (ORM needs to add the column)
XML view file No* Yes
CSV security file No* Yes
__manifest__.py Yes Yes
JavaScript / CSS No Yes (to re-bundle assets)

*With dev_mode=xml, XML changes are reloaded automatically without upgrade. But new files still need an upgrade.

How to Upgrade

Via UI:

  1. Go to Apps
  2. Find your module
  3. Click the three dots menu (⋮)“Upgrade”

Via Command Line:

# Docker method:
docker compose exec odoo odoo -d odoo18dev -u library_app --stop-after-init

# Source method:
python odoo/odoo-bin -c odoo.conf -d odoo18dev -u library_app --stop-after-init

Note: -u means “upgrade” (vs -i for “install”).

Uninstalling a Module

Via UI:

  1. Go to Apps → find your module
  2. Click the three dots menu (⋮)“Uninstall”

Warning: Uninstalling a module deletes all its data from the database (tables, records, views, everything). This action is irreversible. In production, be very careful with uninstallation.

The Development Cycle

During development, your typical workflow looks like this:

1. Edit Python/XML files
        │
        ▼
2. Upgrade the module
   (CLI: -u library_app, or UI: Apps → Upgrade)
        │
        ▼
3. Refresh the browser
   (Ctrl+Shift+R for hard refresh)
        │
        ▼
4. Test your changes
        │
        ▼
5. If something's wrong → check server logs → fix → go to step 1

Pro tip: Use dev_mode = reload,xml in your odoo.conf. This way:

  • Python changes auto-restart the server
  • XML changes are reloaded without a full upgrade
  • You still need to upgrade when adding new fields or new files

Scaffolding a Module with odoo-bin scaffold

Odoo provides a built-in command to generate a module skeleton. It’s like django-admin startapp but for Odoo.

Using the Scaffold Command

# Docker method:
docker compose exec odoo odoo scaffold library_app /mnt/extra-addons

# Source method:
python odoo/odoo-bin scaffold library_app ./custom-addons

What this does:

  • Creates a library_app/ directory in the specified path
  • Generates basic files: __init__.py, __manifest__.py, models/, views/, security/, etc.
  • Fills them with boilerplate code and helpful comments

What the Scaffolded Module Looks Like

After running the command, you’ll get something like:

library_app/
├── __init__.py
├── __manifest__.py
├── controllers/
│   ├── __init__.py
│   └── controllers.py
├── demo/
│   └── demo.xml
├── models/
│   ├── __init__.py
│   └── models.py
├── security/
│   └── ir.model.access.csv
└── views/
    ├── templates.xml
    └── views.xml

Should You Use Scaffold?

Pros:

  • Quick way to get started
  • Correct directory structure out of the box
  • Includes helpful comments in generated files

Cons:

  • Generated code is generic (you’ll rename/rewrite most of it)
  • File names are generic (models.py instead of book.py)
  • Some developers prefer building from scratch to understand every file

Our recommendation: Use scaffold once to see what it generates, then build manually for learning purposes. Once you’re experienced, use scaffold (or your own template) for speed.

In the hands-on section below, we’ll build manually so you understand every file.


Hands-on: Create a Skeleton Module “library_app”

Let’s build our module from scratch. This is the module we’ll extend throughout the entire series.

Step 1: Create the Module Directory

# Docker method — create files on your host machine
cd odoo18-dev/custom-addons
mkdir -p library_app/models
mkdir -p library_app/views
mkdir -p library_app/security
mkdir -p library_app/data
mkdir -p library_app/demo
mkdir -p library_app/static/description

# Source method
cd ~/odoo18-workspace/custom-addons
mkdir -p library_app/models
mkdir -p library_app/views
mkdir -p library_app/security
mkdir -p library_app/data
mkdir -p library_app/demo
mkdir -p library_app/static/description

What -p does: Creates parent directories as needed. If library_app/ doesn’t exist, it creates it too.

Step 2: Create __manifest__.py

Create the file library_app/__manifest__.py:

# library_app/__manifest__.py
{
    'name': 'Library Management',
    'version': '18.0.1.0.0',
    'summary': 'Manage a library of books and members',
    'description': """
        Library Management System
        =========================
        A complete library management solution for tracking
        books, authors, members, and borrowing activities.
    """,
    'author': 'Your Name',
    'website': 'https://your-website.com',
    'category': 'Services/Library',
    'license': 'LGPL-3',

    # Dependencies — we only need 'base' for now
    # We'll add 'mail' in a later lesson when we add chatter
    'depends': ['base'],

    # Data files to load (empty for now — we'll add files in the next lessons)
    'data': [
        # 'security/ir.model.access.csv',
        # 'views/book_views.xml',
        # 'views/menu_views.xml',
    ],

    # Demo data
    'demo': [
        # 'demo/book_demo.xml',
    ],

    'installable': True,
    'application': True,
    'auto_install': False,
}

Why are some lines commented out? Because we haven’t created those files yet. If you list a file in data that doesn’t exist, Odoo will throw an error on installation. We’ll uncomment them as we create the files in upcoming lessons.

Step 3: Create the Root __init__.py

Create the file library_app/__init__.py:

# library_app/__init__.py

# Import the models sub-package so Odoo can register our models.
# When Odoo loads this module, it runs this file first.
# This file then triggers models/__init__.py, which imports our model files.
from . import models

Step 4: Create the Models __init__.py

Create the file library_app/models/__init__.py:

# library_app/models/__init__.py

# Import each model file here.
# Every time you create a new .py file in this directory,
# you must add a corresponding import line.
from . import book

Step 5: Create the Book Model (Placeholder)

Create the file library_app/models/book.py:

# library_app/models/book.py

from odoo import models, fields

class LibraryBook(models.Model):
    """A model to represent a book in the library."""

    _name = 'library.book'
    # _name is the technical name of the model.
    # Convention: 'module_name.model_name' using dots as separators.
    # Odoo creates a database table called 'library_book' (dots → underscores).

    _description = 'Library Book'
    # _description is a human-readable name shown in logs and error messages.
    # If not set, Odoo uses _name, which is less readable.

    name = fields.Char(
        string='Title',       # Label shown in the UI
        required=True,        # Cannot be empty (NOT NULL in the database)
        help='The title of the book',  # Tooltip shown when hovering in the UI
    )
    # fields.Char creates a VARCHAR column.
    # 'string' is optional — if omitted, Odoo uses the Python field name
    # with underscores replaced by spaces and title-cased ("name" → "Name").

Why is this model so simple? Because we’re building incrementally. This is just the skeleton. In Lesson 3, we’ll add many more fields. In Lesson 4, we’ll add relational fields. In Lesson 5, we’ll add business logic.

Odoo shows module icons in the Apps menu. Without an icon, your module gets a generic placeholder.

You can use any PNG image (ideally 128×128 pixels). Save it as:

library_app/static/description/icon.png

If you don’t have a custom icon, that’s fine — the module will still work.

Step 7: Verify the Structure

Your module should now look like this:

# Verify the structure
find library_app -type f | sort

Expected output:

library_app/__init__.py
library_app/__manifest__.py
library_app/models/__init__.py
library_app/models/book.py

Plus the empty directories we created (views/, security/, data/, demo/, static/description/).

Step 8: Install the Module

Now let’s install it and verify everything works.

First, Update the Apps List

If this is a new module that Odoo hasn’t seen before, you need to tell Odoo to scan for new modules:

Via UI (Developer Mode must be on):

  1. Go to Apps
  2. Click “Update Apps List” (top menu)
  3. Click “Update” in the dialog

Via CLI:

# Docker method:
docker compose restart odoo

# Source method: just restart the Odoo server
# (Ctrl+C to stop, then start again)

Then, Install the Module

Via UI:

  1. Go to Apps
  2. Remove the default “Apps” filter in the search bar
  3. Search for “Library”
  4. Click “Install” on “Library Management”

Via CLI:

# Docker method:
docker compose exec odoo odoo -d odoo18dev -i library_app --stop-after-init

# Source method:
python odoo/odoo-bin -c odoo.conf -d odoo18dev -i library_app --stop-after-init

Step 9: Verify the Installation

If installation succeeds:

  1. Check the Apps menu — “Library Management” should appear as installed
  2. Check the database — Go to Settings → Technical → Database Structure → Models, search for “library.book”. You should see your model listed.
  3. Check the table — Using the Odoo Shell:
# Open the shell
# Docker: docker compose exec odoo odoo shell -d odoo18dev
# Source: python odoo/odoo-bin shell -c odoo.conf -d odoo18dev

# Check if the model exists
model = env['library.book']
print(model._name)          # Output: library.book
print(model._description)   # Output: Library Book

# Check the fields
print(list(model._fields.keys()))
# Output includes: 'id', 'name', 'create_date', 'write_date', 'create_uid', 'write_uid', etc.
# Notice: Odoo automatically adds id, create_date, write_date, etc.!

What about the UI? You won’t see any menu items yet, because we haven’t created views or menus. The model exists in the database, but there’s no way to interact with it from the browser. We’ll fix that in Lesson 6 (Views).

If Something Goes Wrong

Symptom Likely Cause Fix
Module not in Apps list Not in addons_path Check your odoo.conf or docker-compose.yml volumes
Module not in Apps list Apps list not updated Click “Update Apps List” in Developer Mode
Error on install: SyntaxError Typo in __manifest__.py or Python files Check for missing commas, brackets, quotes
Error: ModuleNotFoundError Missing import in __init__.py Check the __init__.py chain
Error: FileNotFoundError File listed in data doesn’t exist Check file paths in __manifest__.py
Model not found in shell Model file not imported Check models/__init__.py has from . import book

Summary & What’s Next

Key Takeaways

  1. A module is a directory with __init__.py and __manifest__.py — that’s the minimum.
  2. __manifest__.py is the module’s ID card. It defines the name, version, dependencies, and which files to load. The data list order matters.
  3. The __init__.py chain is how Odoo discovers your Python code. Root __init__.py → sub-package __init__.py → model files. Forget one link, and Odoo won’t see your code.
  4. Dependencies tell Odoo which modules must be installed first. Only list direct dependencies your code actually uses.
  5. Three lifecycle operations: Install (first time), Upgrade (after changes), Uninstall (removes everything).
  6. The development cycle: Edit → Upgrade → Refresh → Test → Repeat.

File Checklist for Our library_app

File Status Purpose
__manifest__.py ✅ Created Module metadata
__init__.py (root) ✅ Created Imports models package
models/__init__.py ✅ Created Imports book.py
models/book.py ✅ Created Defines library.book (minimal)
views/book_views.xml ⬜ Lesson 6 Form, list, search views
views/menu_views.xml ⬜ Lesson 6 Navigation menu
security/ir.model.access.csv ⬜ Lesson 8 Access control
data/book_data.xml ⬜ Lesson 10 Default data
demo/book_demo.xml ⬜ Lesson 10 Demo data

What’s Next?

In Lesson 3: The ORM — Odoo’s Heart (Part 1: Models & Fields), we’ll expand our library.book model with many more field types. You’ll learn:

  • The difference between Model, TransientModel, and AbstractModel
  • All field types: Char, Text, Integer, Float, Boolean, Date, Selection, Html, Binary
  • Field attributes: default values, required, readonly, index
  • Automatic fields that Odoo creates for you

We’ll go from our one-field placeholder to a fully defined book model with 10+ fields.


Exercises: 1. If you used the scaffold command, compare the generated structure with what we built manually. What differences do you see? 2. Try creating a second model called library.member in a new file models/member.py. Remember to: – Create the Python file with a basic model (just _name, _description, and a name field) – Import it in models/__init__.py – Upgrade the module – Verify it exists in the Odoo Shell 3. Try deliberately breaking something: – Remove the from . import book line from models/__init__.py – Upgrade the module — what error do you get? – Add a non-existent file to the data list in __manifest__.py — what happens? – These experiments help you recognize common errors quickly 4. Explore an existing Odoo module. Look at the source code of the contacts module: – Docker: docker compose exec odoo find /usr/lib/python3/dist-packages/odoo/addons/contacts -type f – Source: ls -la odoo/addons/contacts/ – Open its __manifest__.py and __init__.py — do they follow the same patterns we learned?


Previous lesson: Lesson 1 — Introduction to Odoo Next lesson: Lesson 3 — The ORM: Models & Fields

Leave a Reply

Your email address will not be published. Required fields are marked *