🗄️ ​How to Build Your First Odoo Module: Step-by-Step Guide
🗄️

  ​​​​

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.

💡

📚 Want to dive deeper into Odoo development?

For more detailed explanations and advanced concepts — like:

  • Available field types ⚙️;

  • Data files and how they work 📄;

  • Relations between models and dependencies 🔗;

  • Actions & Menus setup 🧭;

  • Search views and wizards 🔍;

  • Computed fields & default values & onchange methods 🧠;

  • ...and much more!

👉 Visit the official Odoo Developer Documentation

(Replace 18.0 with your current Odoo version if needed)

📦 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?


💡

scaffold is an Odoo command that generates a module template including:

  • manifest file (__manifest__.py);

  • folders for models, views, data, etc.;

  • basic boilerplate code.

It saves you time on routine setup so you can start building faster! 🎨

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
⚠️

Make sure to choose the right name and path where you want your module to be created!

📁 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! 💡

⚠️

If you're using a regular terminal, make sure your Python virtual environment is activated. Otherwise, the required packages won’t be available, and the module creation might fail ❌.

🛠️ To activate it, navigate to the directory where your virtual environment is located and run:

source odooXXenv/bin/activate

! Change odooXXenv to your correct name !

🧩 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/
🔍 What Each Folder/File Does
  • __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.

💡

Tip: You don’t always need every folder. Start small — use only what your module actually needs, and expand as you go! 🚀

📁 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
  • lailio_crm

  • lailio_account_reports

  • lailio_inventory_tags

Bad Examples
  • lailio-CrmModule

  • LailioCustomModule

  • my_module_1

📄 __manifest__.py

A manifest template to be used in all of our modules:
	​# -*- 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,
}
⚠️

Common manifest mistakes:

  • Incorrect or missing version (use format 17.0.1.0.0 or others);

  • Missing or wrong dependencies in depends;

  • Typos or wrong paths in data files;

  • installable set to False (module won’t appear);

  • Syntax errors or missing commas

📄 models/<your_model>.py

Use this file to define your business logic (models, fields, methods, etc.).

⚠️

File name must match the model name, whether it’s an inherited model or a custom one:

  – For sale.order → sale_order.py

  – For lailio.project.task.report → project_task_report.py

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)
💡
  • Class name uses CamelCase: class LailioCustomModel(models.Model);

  • _name is all lowercase with dots: lailio.custom.model;

  • Don't forget to inherit with _inherit when extending existing models;

  • Use clear, human-readable labels in string= (e.g., fields.Char(string="Title"));

  • Keep field and method names in snake_case;

  • Always call super() when overriding built-in methods (e.g., create, write, unlink);

  • Field naming convention:

    • Many2one → name ends with _id (e.g., user_id);
    • Many2many/One2many → name ends with _ids (e.g., tag_ids).

📄 views/<your_model_view>.xml

Define how your model appears in the UI.

⚠️

Name your view file after the model, followed by _views.xml

  – For sale.order → sale_order_views.xml

  – For lailio.project.task.report → project_task_report_views.xml

<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
  • Use clear and consistent id and name attributes in custom modules:

    • id="lailio_model_form";
    • name="lailio.model.form".
  • Use clear and consistent id and name attributes when inherit:
    • id="lailio_model_form_inherit";

    • name="model.form.inherit".

  • Always set a human-readable string= in form, tree, field, etc.

  • Use position="inside", before, or after properly in xpath when inheriting views.

🚨 Common Mistakes
  • Missing id or duplicate id across different modules;
  • Incorrect inheritance target (inherit_id);
  • Forgetting to include the XML file in the __manifest__.py.

📄 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;
⚠️

🚨 Common beginner mistakes:

  • Missing the ir.model.access.csv file — your model won’t show up in the UI;

  • Incorrect model_id:id or group_id:id references;

  • Forgetting to assign appropriate permissions (e.g., no read access).

📄 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:

  1. Open Odoo (with debug mode) and go to:
    Settings → Translations → Export Translation;

  2. 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;

  3. Click the Export button, and the translation file will be downloaded to your computer.

Steps to update translations:

  1. Update your .po file;

  2. Load updated translations into Odoo: 

    1. Using Odoo UI:

      1. Go to SettingsTranslationsLanguages;
      2. Find the language you want to update (e.g., Latvian) and click Update;
    2. Using command line:
      1. ./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:

  1. Make sure it's placed in a folder listed in addons_path (check odoo.conf);

  2. Restart your Odoo server;

  3. Go to the Apps menu in the Odoo interface;

  4. Remove the Apps filter to see all modules;

  5. Search for your module by name (e.g. lailio_custom_module);

  6. 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.