The ORM — Odoo’s Heart (Part 1: Models & Fields)

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 & 2 completed, library_app module installed


Table of Contents

What is the Odoo ORM? (vs SQLAlchemy, Django ORM)

ORM in 30 Seconds

ORM stands for Object-Relational Mapping. It’s a technique that lets you interact with a database using Python objects instead of writing raw SQL.

Without ORM (raw SQL):

INSERT INTO library_book (name, pages, isbn) VALUES ('The Hobbit', 310, '978-0547928227');
SELECT * FROM library_book WHERE pages > 200;

With ORM (Python):

# Create a book
env['library.book'].create({'name': 'The Hobbit', 'pages': 310, 'isbn': '978-0547928227'})

# Search for books with more than 200 pages
books = env['library.book'].search([('pages', '>', 200)])

Both do the same thing. The ORM translates your Python calls into SQL behind the scenes.

How Odoo’s ORM Compares

If you’ve used other Python ORMs, here’s how Odoo’s ORM compares:

Feature Django ORM SQLAlchemy Odoo ORM
Model definition models.Model class Declarative base class models.Model class
Migration makemigrations / migrate Alembic (separate tool) Automatic (on module install/upgrade)
Query syntax Chaining methods Chaining methods Domains (Polish notation)
Admin UI Django Admin (optional) None built-in Built-in (it IS the UI)
Inheritance Python class inheritance Python class inheritance 3 types of Odoo-specific inheritance
Security Manual (decorators, middleware) Manual Built-in (ACL + record rules)
Caching Manual (Redis, Memcached) Manual Built-in (prefetching, caching)

The Biggest Difference: No Manual Migrations

In Django, when you add a field, you run makemigrations and migrate to update the database. In SQLAlchemy, you use Alembic.

In Odoo, there are no migration files. When you:

  • Add a new field → Upgrade the module → Odoo adds the column automatically
  • Remove a field → Upgrade the module → Odoo removes the column (after a safety warning)
  • Change a field type → Upgrade → Odoo tries to convert the column

This makes development faster, but it also means you need to be careful about field changes in production (data loss is possible if you change a field type carelessly).

How the ORM Creates Tables

When you write this Python class:

class LibraryBook(models.Model):
    _name = 'library.book'

    name = fields.Char(string='Title', required=True)
    pages = fields.Integer(string='Pages')

Odoo automatically creates this SQL table:

CREATE TABLE library_book (
    id          SERIAL PRIMARY KEY,       -- Auto-created by Odoo
    name        VARCHAR,                  -- From fields.Char
    pages       INTEGER,                  -- From fields.Integer
    create_date TIMESTAMP,                -- Auto-created: when the record was created
    write_date  TIMESTAMP,                -- Auto-created: when the record was last modified
    create_uid  INTEGER REFERENCES res_users,  -- Auto-created: who created it
    write_uid   INTEGER REFERENCES res_users   -- Auto-created: who last modified it
);

You never write this SQL. The ORM handles everything. Your job is to define the Python class correctly.


models.Model vs models.TransientModel vs models.AbstractModel

Odoo has three types of model base classes. Each has a specific purpose.

models.Model — The Standard Model

from odoo import models, fields

class LibraryBook(models.Model):
    _name = 'library.book'
    _description = 'Library Book'

    name = fields.Char(string='Title', required=True)

What it does:

  • Creates a permanent table in the database (library_book)
  • Records persist forever (until explicitly deleted)
  • This is what you use 90% of the time

Use for: Books, customers, orders, invoices — any data that should be stored permanently.

models.TransientModel — The Temporary Model

class BookBorrowWizard(models.TransientModel):
    _name = 'library.book.borrow.wizard'
    _description = 'Book Borrow Wizard'

    borrower_name = fields.Char(string='Borrower Name', required=True)
    borrow_date = fields.Date(string='Borrow Date', default=fields.Date.today)

What it does:

  • Creates a table, but records are automatically deleted after a period (usually a few hours)
  • Used for wizards — popup dialogs that collect user input and then perform an action
  • The data doesn’t need to persist because it’s only used during the user interaction

Use for: Import wizards, bulk action dialogs, confirmation prompts, multi-step forms.

Real-world analogy: Think of a TransientModel like a post-it note. You write something on it, use it, then throw it away. A regular Model is like a page in a notebook — you keep it.

models.AbstractModel — The Mixin

class LibraryMixin(models.AbstractModel):
    _name = 'library.mixin'
    _description = 'Library Common Fields'

    notes = fields.Text(string='Internal Notes')
    active = fields.Boolean(string='Active', default=True)

What it does:

  • Does NOT create a database table
  • Exists only to be inherited by other models
  • Provides reusable fields and methods

Use for: Sharing common fields/methods across multiple models, like Odoo’s built-in mail.thread (adds chatter to any model).

Real-world analogy: An AbstractModel is like a recipe ingredient list that you mix into multiple dishes. It doesn’t exist on its own — it becomes part of whatever dish uses it.

Comparison Table

Feature models.Model models.TransientModel models.AbstractModel
Creates database table ✅ Yes ✅ Yes (temporary) ❌ No
Data persists ✅ Forever ❌ Auto-cleaned N/A
Can be used standalone ✅ Yes ✅ Yes (as wizards) ❌ No (must be inherited)
Has views (UI) ✅ Yes ✅ Yes (popups) ❌ No
Usage frequency ~85% ~10% ~5%
Typical use Core business data User input dialogs Shared field/method mixins

Which One to Choose?

Ask yourself:

  1. Does the data need to be stored permanently?models.Model
  2. Is this a popup/wizard that collects input and runs an action?models.TransientModel
  3. Do I just want to share fields/methods with other models?models.AbstractModel

For our library.book, the answer is clearly models.Model — books are permanent data.


The _name and _description Attributes

Every Odoo model has special class attributes that start with an underscore. The two most fundamental ones are _name and _description.

_name — The Model’s Identity

class LibraryBook(models.Model):
    _name = 'library.book'

What it does:

  • Uniquely identifies the model in the Odoo system
  • Determines the database table name (dots become underscores): library.booklibrary_book
  • Used to reference the model everywhere: in Python (env['library.book']), in XML (library.book), and in security files

Naming conventions:

# Convention: module_name.model_name (using dots)
_name = 'library.book'         # ✅ Good — clear prefix, descriptive name
_name = 'library.book.author'  # ✅ Good — three-part name for sub-models

_name = 'book'                 # ❌ Bad — no prefix, might conflict with other modules
_name = 'library_book'         # ❌ Bad — use dots, not underscores
_name = 'LibraryBook'          # ❌ Bad — use lowercase with dots

Why use dots? The dot convention creates a namespace for your module. Since many modules can be installed together, library.book won’t conflict with another module’s store.book or school.book.

_description — The Human-Readable Name

class LibraryBook(models.Model):
    _name = 'library.book'
    _description = 'Library Book'

What it does:

  • Provides a human-readable name for the model
  • Shown in error messages, logs, and the technical menu
  • If omitted, Odoo uses _name which is less readable

Always set _description. It costs one line and makes debugging much easier. Compare:

# Without _description (error message):
Error in model library.book: field 'isbn' is required

# With _description (error message):
Error in model Library Book: field 'isbn' is required

Python Class Name vs _name

Notice that the Python class name (LibraryBook) and the _name (library.book) are different:

class LibraryBook(models.Model):    # Python class name — CamelCase
    _name = 'library.book'          # Odoo model name — dotted.lowercase

The Python class name doesn’t matter to Odoo. Odoo only cares about _name. You could name the class Xyz and it would still work (but please don’t). The convention is to use CamelCase that matches the model name: library.bookLibraryBook.


Field Types Overview

Odoo provides a rich set of field types. Let’s start with the big picture before diving into each one.

The Field Type Family Tree

Fields
├── Simple (store a single value)
│   ├── Char          — Short text (VARCHAR)
│   ├── Text          — Long text (TEXT)
│   ├── Html          — Rich text with formatting (TEXT + HTML)
│   ├── Integer       — Whole numbers
│   ├── Float         — Decimal numbers
│   ├── Monetary      — Currency amounts (with currency field)
│   ├── Boolean       — True/False
│   ├── Date          — Date only (no time)
│   ├── Datetime      — Date + time
│   ├── Selection     — Dropdown choices
│   ├── Binary        — File storage (BYTEA)
│   └── Image         — Image storage (inherits from Binary)
│
└── Relational (link to other models) — covered in Lesson 4
    ├── Many2one       — FK: this record → one record in another model
    ├── One2many       — Reverse: one record ← many records
    └── Many2many      — Junction: many records ↔ many records

In this lesson, we’ll cover all the Simple field types. Relational fields get their own lesson (Lesson 4) because they’re complex and essential.


Basic Field Types in Detail

fields.Char — Short Text

name = fields.Char(
    string='Title',          # Label in the UI
    required=True,           # Cannot be empty
    size=256,                # Maximum length (optional, rarely used)
    trim=True,               # Strip whitespace (default: True)
    translate=True,          # Can be translated to other languages
)

Database: VARCHAR column Python type: str (or False if empty — Odoo uses False instead of None) Use for: Names, titles, short descriptions, codes, phone numbers, emails

Important: In Odoo, empty fields are False, not None or "":

book = env['library.book'].browse(1)
print(book.name)       # 'The Hobbit' (if set)
print(book.isbn)       # False (if empty — NOT None, NOT "")

# Always check with:
if book.isbn:          # ✅ Correct
    print(book.isbn)

# Not with:
if book.isbn is not None:   # ❌ Wrong — it's False, not None
    print(book.isbn)

fields.Text — Long Text

synopsis = fields.Text(
    string='Synopsis',
    help='A brief summary of the book',
    translate=True,
)

Database: TEXT column (unlimited length) Python type: str or False Use for: Descriptions, notes, comments, any long-form text without formatting

Char vs Text — When to use which:

Scenario Use
Book title, person name, email Char
Book synopsis, internal notes Text
Short reference codes (ISBN, SKU) Char
Address, multi-line content Text

Rule of thumb: If the content might be more than one line, use Text. Otherwise, use Char.

fields.Html — Rich Text

description = fields.Html(
    string='Full Description',
    sanitize=True,           # Remove potentially dangerous HTML tags (default: True)
    sanitize_tags=True,      # Strip disallowed tags
    sanitize_attributes=True, # Strip disallowed attributes
    strip_style=False,       # Keep inline styles (default: False)
)

Database: TEXT column (stores HTML as a string) Python type: str (containing HTML) or False Use for: Rich content with bold, italic, lists, images — like a mini word processor

In the UI: Odoo renders an Html field as a WYSIWYG editor (What You See Is What You Get) — the user sees formatting buttons like Bold, Italic, Insert Image, etc.

Security note: sanitize=True (the default) is important. It strips dangerous HTML like