How to Build Your First Odoo Module: Step-by-Step Guide
Welcome to the exciting world of Odoo development! Today, we’re going to build your very first Odoo module — from scratch, with clear explanations of every file and its purpose. If you want to understand module structure better, this guide has got you covered.
📦 What Is an Odoo Module?
An Odoo module is basically a package that adds new features or customizes existing ones in your Odoo system. It contains Python code 🐍, XML files for views 🖼️, data files 📄, and more — all organized in a certain way so Odoo knows how to load and use them.
🏗️ How to Create a Module using Scaffold?
1. Open your terminal (you can also use the terminal built into PyCharm) and navigate to your Odoo source folder, for example:
cd ~/work/odooXX/odoo
2. Run the scaffold command with your desired module name:
./odoo-bin scaffold lailio_custom_module ../../odooXX-custom-addons/extra
📁 This will do the following:
Create a new folder called lailio_custom_module;
Place it inside extra, which is located at: ../../odooXX-custom-addons/extra;
Automatically generate the module structure and basic files for you to start coding! 💡
🧩 Odoo Module Structure — Quick Overview
Here's what a typical custom module looks like:
lailio_custom_module/
├── __init__.py
├── __manifest__.py
├── i18n/
├── controllers/
│ └── __init__.py
├── models/
│ └── __init__.py
├── views/
├── security/
├── data/
├── static/
│ ├── description/
│ └── src/
__init__.py
Tells Python that this directory is a module. Usually, you'll import your models/controllers here.__manifest__.py
The module's metadata file. Defines the module name, version, dependencies, author, description, and which files to load.models/
Contains Python files that define your business logic, data models, and database structure.controllers/
If your module has a website or backend interface, controllers handle routes and views.views/
Contains XML files for UI layout: forms, tree views, kanban, etc.security/
Includes access control files like ir.model.access.csv and record rules to manage user permissions.data/
Optional. Used to load default or demo data (like configuration records or scheduled actions).static/
For static web assets (CSS, JS, images). static/description/ usually holds images for the module description in the Odoo Apps interface.
📁 Examples & Naming Conventions for Key Module Files
🧩 Module Name
Format: lailio_<functional_name> |
Always prefix the module name with "lailio_" to match company naming conventions;
If you're inheriting an existing Odoo module, use the exact name of the original module as <functional_name>:
👉 Example: lailio_hr (inherits and extends the hr module);If it's a custom module, choose a name that clearly and logically reflects its purpose.
👉 Example: lailio_project_task_timer or lailio_employee_rewards;Use only lowercase letters, numbers, and underscores. Avoid spaces or special characters.
✅ Good Examples
|
❌ Bad Examples
|
📄 __manifest__.py
# -*- coding: utf-8 -*-
{
'name': "....",
'description': """
Detailed description of the module.
Explain the purpose, features, and any special notes.
Created by Lailio SIA.
""",
'summary': "..."
'author': "Lailio SIA",
'website': "https://www.lailio.com",
'license': 'LGPL-3',
'category': 'Productivity',
'version': '',
# any module necessary for this one to work correctly
'depends': ['base'],
# always loaded
'data': [
],
'installable': True,
'application': False,
}
📄 models/<your_model>.py
Use this file to define your business logic (models, fields, methods, etc.).
from odoo import models, fields
class HrEmployeeRating(models.Model):
_name = 'hr.employee.rating'
_description = 'Employee Rating'
employee_id = fields.Many2one('hr.employee', string='Employee', required=True)
score = fields.Integer(string='Score')
feedback = fields.Text(string='Feedback')
date = fields.Date(string='Rating Date', default=fields.Date.today)
📄 views/<your_model_view>.xml
Define how your model appears in the UI.
<odoo>
<record id="view_employee_rating_form" model="ir.ui.view">
<field name="name">employee.rating.form</field>
<field name="model">hr.employee.rating</field>
<field name="arch" type="xml">
<form string="Employee Rating">
<group>
<field name="employee_id"/>
<field name="score"/>
<field name="feedback"/>
<field name="date"/>
</group>
</form>
</field>
</record>
<record id="action_employee_rating" model="ir.actions.act_window">
<field name="name">Employee Ratings</field>
<field name="res_model">hr.employee.rating</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_hr_employee_rating_root" name="Employee Ratings" parent="hr.menu_hr_root" action="action_employee_rating"/>
</odoo>
✅ Best Practices
|
🚨 Common Mistakes
|
📄 security/ir.model.access.csv
Make sure your model is visible and editable by the right users. Each line defines access rights for one model and one user group.
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_lailio_task_manager,lailio.task.manager,model_lailio_task_manager,base.group_user,1,1,1,0
Means:
- Model lailio.task.manager;
- Access given to regular users (base.group_user);
- Permissions to read, write, create, but no delete;
📄 i18n/<language_code>.po
This file contains translations of all the user-facing text strings in your module for a specific language.
msgid "Original English Text"
msgstr "Translated Text"
Steps to export translations:
Open Odoo (with debug mode) and go to:
Settings → Translations → Export Translation;In the export form:
Choose the language you want to export (e.g., Latvian);
Select the module for which you want the translations (or leave blank for all modules);
Select the file format — usually .po;
Click the Export button, and the translation file will be downloaded to your computer.
Steps to update translations:
Update your .po file;
Load updated translations into Odoo:
Using Odoo UI:
- Go to Settings → Translations → Languages;
- Find the language you want to update (e.g., Latvian) and click Update;
- Using command line:
./odoo-bin -d your_database_name --load-language=lv --i18n-update=your_module -u your_module
🚀 Installing & Updating Your First Odoo Module
When your custom module is ready, here’s how to install it and keep it up-to-date in Odoo ⬇️
After creating your module:
Make sure it's placed in a folder listed in addons_path (check odoo.conf);
Restart your Odoo server;
Go to the Apps menu in the Odoo interface;
Remove the Apps filter to see all modules;
Search for your module by name (e.g. lailio_custom_module);
Click Install or Update.
Update via terminal:
./odoo-bin -u your_module_name -d your_database_name
❓ Common Beginner Questions & Answers
🧩 Q: Why is my module not showing up in the Apps menu?
A: Check if it’s inside a folder listed in your addons_path (configured in odoo.conf). Also, click the Update Apps List button in Developer Mode to refresh the module list.
📝 Q: Why aren’t my views or menus loading?
A: Make sure your XML files are included in the data list in __manifest__.py.
🔐 Q: Why can’t I see my model in the UI or access it?
A: Check if you’ve added the correct rules in security/ir.model.access.csv.
⚠️ Q: My module folder doesn’t work — why?
A: Ensure the folder name uses only lowercase letters, underscores, and no special characters or spaces.
🔄 Q: I made changes, but nothing updates — what should I do?
A: Restart the Odoo server and update the module.
👀 Q: Some models or menus are missing in the interface?
A: Activate Developer Mode from the Settings menu.
🐍 Q: scaffold or pip commands fail — why?
A: Your Python virtual environment might not be activated. Run:
source path_to_your_env/bin/activate
🐛 Q: My model isn’t loading — even with access rights added?
A: Make sure your model file is imported in __init__.py.
🔤 Q: My translations don’t work — what’s wrong?
A: Check if your .po file is placed in the i18n/ folder and the language is installed in Odoo.
🧠 Q: How do I reload translations manually?
A: Use the UI:
Settings → Translations → Load a Translation
Or terminal:
./odoo-bin -d db_name -i module_name --load-language=lv_LV
⚠️ Q: Why am I getting an error when creating a menu in my XML file?
A: This usually happens when the action is declared after the menu that references it.
❓ Q: Why is my view not loading correctly or giving parsing errors?
A: This often happens when the <field name="arch"> is missing the type="xml" attribute.
👀 Why do I see errors or no data with a One2many field?
A: Because a One2many field depends on a matching Many2one field in the other model. If that Many2one field is missing, the connection won’t work.