Writing your first Odoo module: models, views and security
Scaffold an Odoo addon, define a model with the ORM, build list/form views in XML, wire menus and set access rights and record rules correctly.
An Odoo module is a directory with a manifest. Everything else — models, views, security, data — is convention. Here is the smallest complete example that is still production-shaped.
Structure
my_asset/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ └── asset.py
├── security/
│ ├── ir.model.access.csv
│ └── asset_security.xml
└── views/
└── asset_views.xml
Manifest
{
'name': 'IT Asset Register',
'version': '18.0.1.0.0',
'depends': ['base', 'mail'],
'category': 'Inventory',
'license': 'LGPL-3',
'data': [
'security/asset_security.xml',
'security/ir.model.access.csv',
'views/asset_views.xml',
],
'installable': True,
'application': True,
}
Model
from odoo import api, fields, models
class ItAsset(models.Model):
_name = 'it.asset'
_description = 'IT Asset'
_inherit = ['mail.thread']
_order = 'name'
name = fields.Char(required=True, tracking=True)
serial = fields.Char(string='Serial Number', copy=False)
user_id = fields.Many2one('res.users', string='Assigned To')
state = fields.Selection(
[('stock', 'In Stock'), ('use', 'In Use'), ('retired', 'Retired')],
default='stock', tracking=True)
warranty_end = fields.Date()
days_left = fields.Integer(compute='_compute_days_left', store=False)
_sql_constraints = [('serial_uniq', 'unique(serial)', 'Serial must be unique.')]
@api.depends('warranty_end')
def _compute_days_left(self):
today = fields.Date.today()
for rec in self:
rec.days_left = (rec.warranty_end - today).days if rec.warranty_end else 0
Views and menu
<odoo>
<record id="view_it_asset_list" model="ir.ui.view">
<field name="model">it.asset</field>
<field name="arch" type="xml">
<list>
<field name="name"/><field name="serial"/>
<field name="user_id"/><field name="state"/>
</list>
</field>
</record>
<record id="action_it_asset" model="ir.actions.act_window">
<field name="name">IT Assets</field>
<field name="res_model">it.asset</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_it_asset_root" name="Assets" sequence="10"/>
<menuitem id="menu_it_asset" parent="menu_it_asset_root"
action="action_it_asset"/>
</odoo>
Security is not optional
A model with no ir.model.access.csv entry is invisible to everyone except the superuser — and shipping one with blanket write access to base.group_user is the most common review failure.
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_it_asset_user,it.asset.user,model_it_asset,base.group_user,1,0,0,0
access_it_asset_manager,it.asset.manager,model_it_asset,base.group_system,1,1,1,1
Use record rules (ir.rule) for row-level restrictions such as "users see only their own assets", and multi-company rules if you run more than one company.
Development workflow
- Run with
--dev=allto auto-reload Python and XML. - Upgrade the module after data-file changes:
odoo-bin -u my_asset -d mydb. - Keep migrations in
migrations/<version>/pre-|post-migrate.pyfor schema changes. - Test with the built-in
TransactionCasebefore touching production data.