Stackvora.tech

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=all to 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.py for schema changes.
  • Test with the built-in TransactionCase before touching production data.

Back to the knowledge base · Ask the AI assistant