Skip to content

Odoo AbstractModel vs TransientModel: Complete Guide

DeployMonkey Team · March 24, 2026 11 min read

Three Model Types in Odoo

Odoo has three model base classes, each serving a different purpose. Choosing the wrong one causes either unnecessary database tables, missing functionality, or data that never gets cleaned up.

TypeDatabase TablePersistenceUse Case
models.ModelYesPermanentBusiness data (orders, contacts, products)
models.AbstractModelNoNoneMixins and shared behavior
models.TransientModelYes (temporary)Auto-cleanedWizards and temporary dialogs

models.Model — Persistent Models

Standard Odoo models. Every record is stored permanently in the database until explicitly deleted:

class SaleOrder(models.Model):
    _name = 'sale.order'
    _description = 'Sales Order'

    name = fields.Char(required=True)
    partner_id = fields.Many2one('res.partner')
    amount_total = fields.Float()

Characteristics

  • Creates a PostgreSQL table named after _name (dots replaced with underscores)
  • Records persist until deleted via unlink()
  • Supports all ORM features: access rules, record rules, workflows
  • Supports mail.thread for chatter integration
  • This is what you use for 95% of business models

models.AbstractModel — Mixins

Abstract models define reusable behavior without creating database tables:

class ApprovableMixin(models.AbstractModel):
    _name = 'approvable.mixin'
    _description = 'Approvable Mixin'

    approved = fields.Boolean(default=False)
    approved_by = fields.Many2one('res.users')
    approved_date = fields.Datetime()

    def action_approve(self):
        self.write({
            'approved': True,
            'approved_by': self.env.uid,
            'approved_date': fields.Datetime.now(),
        })

    def action_reject(self):
        self.write({
            'approved': False,
            'approved_by': False,
            'approved_date': False,
        })

Using the Mixin

class PurchaseRequest(models.Model):
    _name = 'purchase.request'
    _inherit = ['approvable.mixin', 'mail.thread']

    name = fields.Char(required=True)
    amount = fields.Float()

Characteristics

  • No database table created
  • Fields are added to the inheriting model's table
  • Methods are available on the inheriting model
  • Cannot be instantiated directly
  • Cannot have record rules or access rights
  • Used for: mail.thread, mail.activity.mixin, portal.mixin, image.mixin

When to Create an AbstractModel

  • You have behavior shared across 2+ unrelated models
  • You want to standardize field names and methods
  • You are building a framework-level feature (like tracking, approval, or soft-delete)

models.TransientModel — Wizards

Transient models create temporary records that are automatically garbage collected:

class MassMailWizard(models.TransientModel):
    _name = 'mass.mail.wizard'
    _description = 'Mass Mail Wizard'

    subject = fields.Char(required=True)
    body = fields.Html(required=True)
    partner_ids = fields.Many2many('res.partner')

    def action_send(self):
        for partner in self.partner_ids:
            # Send email logic
            partner.message_post(
                body=self.body,
                subject=self.subject,
            )
        return {'type': 'ir.actions.act_window_close'}

Characteristics

  • Creates a database table (records are stored temporarily)
  • Records are automatically deleted by a cron job (default: after ~1 hour)
  • Designed for user dialogs and multi-step wizards
  • Access rules are simpler (users can always access their own wizard records)
  • Should NOT be used for permanent data storage

Wizard View and Action

<!-- Form view -->
<record id="mass_mail_wizard_form" model="ir.ui.view">
    <field name="name">mass.mail.wizard.form</field>
    <field name="model">mass.mail.wizard</field>
    <field name="arch" type="xml">
        <form string="Send Mass Mail">
            <group>
                <field name="subject"/>
                <field name="partner_ids" widget="many2many_tags"/>
            </group>
            <field name="body"/>
            <footer>
                <button string="Send" type="object" name="action_send" class="btn-primary"/>
                <button string="Cancel" class="btn-secondary" special="cancel"/>
            </footer>
        </form>
    </field>
</record>

<!-- Action -->
<record id="mass_mail_wizard_action" model="ir.actions.act_window">
    <field name="name">Send Mass Mail</field>
    <field name="res_model">mass.mail.wizard</field>
    <field name="view_mode">form</field>
    <field name="target">new</field>
</record>

Pre-Populating Wizards

def open_wizard(self):
    return {
        'type': 'ir.actions.act_window',
        'res_model': 'mass.mail.wizard',
        'view_mode': 'form',
        'target': 'new',
        'context': {
            'default_partner_ids': [(6, 0, self.partner_ids.ids)],
            'active_ids': self.ids,
        },
    }

Garbage Collection

TransientModel records are cleaned by _transient_vacuum():

  • Default retention: records older than 1 hour are deleted
  • Configurable via transient_age_limit config parameter (hours)
  • The cron runs periodically to clean up
  • Do not rely on wizard records existing after the user closes the dialog

Common Pitfalls

  • Using TransientModel for persistent data — Records will be garbage collected. Use Model for anything that needs to persist.
  • Creating AbstractModel for one model — If only one model uses the behavior, skip the mixin. Abstract models add complexity.
  • Forgetting target='new' on wizards — Without this, the wizard opens as a full-page form instead of a dialog.
  • Wizard data after close — Never reference wizard records from permanent models. The wizard record will be deleted by garbage collection.