Odoo Development
Creating Your First Odoo Model
A 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.
Part of our guide to Odoo Development
The smallest useful model
from odoo import fields, models
class EquipmentCheck(models.Model):
_name = "equipment.check"
_description = "Equipment Check"
_order = "check_date desc, id desc"
name = fields.Char(string="Reference", required=True, copy=False, default="New")
check_date = fields.Date(string="Checked on", default=fields.Date.context_today)
equipment_id = fields.Many2one("maintenance.equipment", required=True, ondelete="cascade")
notes = fields.Text()
state = fields.Selection([
("draft", "Draft"),
("done", "Done"),
], default="draft", required=True)Install that and Odoo creates an equipment_check table with the columns implied, plus create_uid, create_date, write_uid and write_date, which every model gets for free.
The underscore attributes that matter
_name is the model's identity. Dotted, singular, lowercase. It becomes the table name with underscores, so equipment.check becomes equipment_check.
_description is required in practice: Odoo logs a warning without it, and it is what appears wherever the model is listed for a human.
_order sets the default sort. It is SQL, so it must reference stored columns. Sorting on a non-stored computed field is a common and confusing failure.
_rec_name names the display field. It defaults to name, which is why a model without a name field shows records as bare identifiers in every dropdown until you set it.
_inherit is how you extend an existing model instead of creating one, which is covered separately in model inheritance.
Field types you will actually use
Char for short text, Text for long, Html for rich content. Integer, Float and Monetary, the last requiring a currency_id on the model. Boolean. Date and Datetime, where datetimes are stored in UTC and displayed in the user's timezone, which is a source of off-by-one-day bugs whenever someone forgets it. Selection for a fixed list. Binary for files.
The relational three are where the design happens:
Many2oneis a foreign key. Setondeletedeliberately:set nullis the default,cascadedeletes your record with the parent,restrictblocks the parent's deletion. Leaving it unset means accepting the default silently.One2manyis the reverse of a Many2one and stores nothing itself. It needs the target model and the name of the Many2one field pointing back.Many2manycreates a relation table, which Odoo names automatically unless you specify.
Access rights are not optional
This is the step that catches everybody. A new model has no access rights, so no user other than the superuser can read it.
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_equipment_check_user,equipment.check.user,model_equipment_check,base.group_user,1,1,1,0
The model_id:id value is model_ plus the model name with dots replaced by underscores. Odoo generates that external identifier for every model automatically.
Because administrators test as superuser, the missing line does not surface until an ordinary user tries the feature. Add the CSV line in the same commit as the model, and list it first in the manifest data.
Making it reachable
A model with no action and no menu exists but cannot be opened. Both are XML records:
Equipment Checks equipment.check list,form
Note view_mode uses list, not tree. The view type was renamed in Odoo 17 and the old name no longer works in current versions.
The action must load before the menu that references it, which is a manifest ordering constraint rather than a file placement one.
Four reasons a new model appears to do nothing
In the order they actually occur:
- No access rights. Works as admin, invisible to everyone else.
- The Python is never imported. A model in a file missing from
__init__.pyproduces no error at all, it simply does not exist. - The XML is not in the manifest
datalist. Same silence. - The module was installed, not upgraded. New models arrive on install, but changes to an already-installed module need an upgrade to take effect.
Work down that list before debugging the model itself. It is nearly always one of the four, and none of the four produce a traceback.
What comes next
Once the model exists, the interesting work is behaviour: computed fields for derived values, constraints for the rules that must always hold, and wizards for actions that need input before they run.
FAQ
Questions, answered.
Why can't users see my new Odoo model?
Almost always a missing line in ir.model.access.csv. A new model has no access rights by default, so every user except the superuser sees nothing. Because the administrator usually tests as superuser, it looks like it works right up until somebody else logs in.
Do I need to write SQL to create the table?
No. Odoo creates and alters the table from the model definition when the module is installed or upgraded. You define fields as class attributes and the ORM handles columns, types and indexes. Writing raw SQL to create structure fights the framework.
What does _rec_name do?
It names the field used as the record's display label in dropdowns, links and breadcrumbs. It defaults to a field called name, so if your model has no name field you must set _rec_name or every record shows as a bare identifier.
Keep reading
More on Odoo Development.
The full guide, plus the other articles in this cluster.
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 callReady to make Odoo work the way your business does?
Book a free callCODEerts is a team of certified Odoo partners and full-stack engineers. We implement, customise and support Odoo ERP, then build the software around it.