Belajar Membuat Module di Odoo

ODOO · MODULE DEVELOPMENT

    Odoo adalah kumpulan module (addon) yang independen tapi juga bisa saling depends. Satu module = satu folder dengan struktur standar begini:

diagram inti struktur addon Odoo

Ini adalah enam bagian inti yang wajib kita kenali di tiap module Odoo. Beberapa poin penting per bagian:

  1. __manifest__.py — file dict Python (bukan JSON, meskipun mirip) yang isinya metadata: name, version, depends (list module lain yang harus sudah ter-install duluan), data (daftar file XML/CSV yang di-load saat install), dan installable. Ini yang dibaca Odoo pertama kali saat scan addons_path untuk tahu module apa saja yang tersedia.
  2. models/ — isinya class Python yang extend models.Model, tempat kamu definisikan field dan business logic. Biasanya ada models/__init__.py yang meng-import semua file model di folder itu.
  3. views/ — file XML yang mendefinisikan form view, tree/list view, search view, dan menu. View ini yang "menempel" ke model lewat <field name="model">.
  4. security/ — dua hal utama: ir.model.access.csv (permission dasar per-model: siapa boleh read/write/create/unlink) dan security/*.xml untuk record rules yang lebih detail (misalnya multi-company).
  5. data/ — record XML/CSV yang otomatis dimasukkan saat install, misalnya sequence number, default config, atau data demo (yang kemarin kita matikan pakai --without-demo=all).
  6. controllers/ — kalau module butuh expose HTTP endpoint kustom (di luar web client standar Odoo), route-nya didefinisikan di sini pakai decorator @http.route.
  7. Satu hal yang penting dipahami di awal: urutan load itu ditentukan oleh dependency graph dari depends di manifest — bukan urutan folder. Kalau my_module depends ke base dan sale, Odoo akan install/upgrade base dan sale dulu sebelum masuk ke my_module. Ini konsepnya mirip Maven/Gradle dependency resolution, cuma levelnya module bukan library.

Di Odoo, cara mikirnya memang mirip modular monolith di Spring Boot: satu runtime aplikasi, tapi fitur dipisah jadi banyak module (addon) yang saling bergantung secara jelas. Setiap addon adalah "paket fitur" mandiri yang punya model, view, security, data, dan metadata sendiri.

Struktur addon umumnya seperti ini:

struktur folder
my_module/
├── __init__.py
├── __manifest__.py
├── models/
├── views/
├── controllers/
├── security/
├── data/
└── demo/

Fungsi inti per bagian:

  • __manifest__.py: identitas module + dependency + file yang harus diload.
  • models/: business object ORM (models.Model), field, method, constraint.
  • views/: XML UI (form/list/search/kanban/menu/action).
  • controllers/: endpoint HTTP/route (kalau butuh web controller/API style).
  • security/: akses model (ir.model.access.csv) + record rule (biasanya XML).
  • data/: data master/konfigurasi awal yang wajib ada saat install.
  • demo/: sample data untuk latihan/testing (tidak untuk production).

__manifest__.py adalah "contract" modul. Contoh minimal:

__manifest__.py (contoh)
{
    "name": "My Training Module",
    "depends": ["base"],
    "data": [
        "security/ir.model.access.csv",
        "views/my_model_views.xml",
        "data/my_seed_data.xml",
    ],
    "demo": [
        "demo/demo_data.xml",
    ],
    "installable": True,
    "application": True,
}
TENTANG DEPENDS (SANGAT PENTING)
  1. Odoo akan install dependency lebih dulu.
  2. Modul kamu boleh mewarisi (_inherit) model/view dari modul dependency.
  3. Kalau dependency dicopot, modul yang bergantung juga ikut terdampak/uninstall.
  4. Praktik bagus: dependency dibuat sekecil mungkin (hindari over-coupling).

Urutan mental model saat module diinstall:

  1. baca __manifest__.py,
  2. pastikan depends terpenuhi,
  3. load Python package (__init__.py, models, dll),
  4. load file di data sesuai urutan list,
  5. load demo bila mode demo aktif.
KERJA PRAKTEK
    bikin satu module mini di services/odoo/addons (contoh academy_core) berisi 1 model + 1 view + 1 access rule, supaya kita bisa lihat end-to-end arsitektur Odoo secara konkret.
Fondasi Odoo Development: Struktur Module hingga Dasar Security

    Kita mulai dari fondasi karena di Odoo, pemahaman struktur module adalah kunci untuk semua hal lanjutan: user management, multi-company/multi-tenancy, sampai security. Dengan memahami bagaimana __manifest__.py, models, views, security, dan data saling terhubung, kamu tidak hanya bisa "membuat fitur jalan", tapi juga bisa merancang sistem yang rapi, aman, dan mudah di-maintain. Untuk yang terbiasa dengan Spring Boot, pendekatan ini akan terasa familiar sebagai modular monolith: tiap module punya tanggung jawab jelas, tetapi tetap berjalan dalam satu platform terpadu.

Tahap-1 Setup AddOns

Oke, kita lanjut bertahap dan langsung praktik di project. Step pertama: siapkan skeleton module custom yang sudah langsung runnable — manifest, satu model dasar, satu view, ACL sederhana, dan menu — supaya begitu di-install, module ini langsung bisa dites dan muncul di Apps.

/ai-erp-platform/services/odoo
academy_core/__manifest__.py
academy_core/__init__.py
academy_core/models/__init__.py
Struktur file academy_core setelah dibuat

Kenapa ada __init__.py di bawah academy_core dan models?

  1. Karena Odoo (Python) butuh file itu untuk mengenali package dan menjalankan import chain. academy_core/__init__.py fungsinya mengimpor subpackage (models, nanti bisa controllers, wizard, dll). Jadi saat module academy_core diload, Odoo tahu harus masuk ke bagian mana.
  2. academy_core/models/__init__.py fungsinya mengimpor file model konkret, misalnya academy_course.py, dan tanpa ini, class AcademyCourse bisa tidak ikut ter-load ke registry Odoo.

Alurnya sederhana:

  1. Odoo baca __manifest__.py
  2. Odoo load package module
  3. Python jalankan academy_core/__init__.py
  4. dari situ masuk ke models/__init__.py
  5. baru class model di file .py didaftarkan

Jadi dua __init__.py itu bukan duplikasi, tapi dua level "pintu masuk" import. Tanpa salah satu, modul sering terlihat "installable" tapi modelnya tidak muncul/terdaftar dengan benar.

academy_core/models/academy_course.py
from odoo import fields, models


class AcademyCourse(models.Model):
    _name = "academy.course"
    _description = "Academy Course"
    _order = "name"

    name = fields.Char(required=True)
    code = fields.Char(help="Unique short code for easier identification.")
    description = fields.Text()
    active = fields.Boolean(default=True)
academy_core/security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_academy_course_user,academy.course.user,model_academy_course,base.group_user,1,1,1,1
academy_core/security/ir.model.access.csv
academy_core/views/academy_course_views.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="academy_course_view_list" model="ir.ui.view">
        <field name="name">academy.course.view.list</field>
        <field name="model">academy.course</field>
        <field name="arch" type="xml">
            <list string="Courses">
                <field name="name"/>
                <field name="code"/>
                <field name="active"/>
            </list>
        </field>
    </record>

    <record id="academy_course_view_form" model="ir.ui.view">
        <field name="name">academy.course.view.form</field>
        <field name="model">academy.course</field>
        <field name="arch" type="xml">
            <form string="Course">
                <sheet>
                    <group>
                        <field name="name"/>
                        <field name="code"/>
                        <field name="active"/>
                    </group>
                    <group>
                        <field name="description"/>
                    </group>
                </sheet>
            </form>
        </field>
    </record>

    <record id="academy_course_action" model="ir.actions.act_window">
        <field name="name">Courses</field>
        <field name="res_model">academy.course</field>
        <field name="view_mode">list,form</field>
    </record>

    <menuitem id="academy_menu_root" name="Academy" sequence="90"/>
    <menuitem
        id="academy_menu_courses"
        name="Courses"
        parent="academy_menu_root"
        action="academy_course_action"
        sequence="10"
    />
</odoo>

File yang sudah dibuat:

  • services/odoo/addons/academy_core/__init__.py
  • services/odoo/addons/academy_core/__manifest__.py
  • services/odoo/addons/academy_core/models/__init__.py
  • services/odoo/addons/academy_core/models/academy_course.py
  • services/odoo/addons/academy_core/security/ir.model.access.csv
  • services/odoo/addons/academy_core/views/academy_course_views.xml
Tahap-2 Security Dasar (ACL + Record Rule)

Lanjut, kita kerjakan bagian security: tambah pemilik course, group User/Manager, lalu record rule supaya akses data bisa dibedakan per role. Setelah itu module di-update di Odoo untuk verifikasi.

Perubahan yang ditambahkan: field owner_id di model course, grup Academy User/Academy Manager, dan record rule berbasis kepemilikan (ownership), supaya nanti bisa langsung diuji dengan 2 akun berbeda.

academy_core/models/academy_course.py
from odoo import fields, models


class AcademyCourse(models.Model):
    _name = "academy.course"
    _description = "Academy Course"
    _order = "name"

    name = fields.Char(required=True)
    code = fields.Char(help="Unique short code for easier identification.")
    owner_id = fields.Many2one(
        "res.users",
        string="Owner",
        required=True,
        default=lambda self: self.env.user,
    )
    description = fields.Text()
    active = fields.Boolean(default=True)
academy_core/security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_academy_course_user,academy.course.user,model_academy_course,academy_core.academy_group_user,1,1,1,0
access_academy_course_manager,academy.course.manager,model_academy_course,academy_core.academy_group_manager,1,1,1,1
ir.model.access.csv (versi update: group user vs manager)
academy_core/__manifest__.py
{
    "name": "Academy Core",
    "summary": "Learning module for Odoo architecture fundamentals",
    "version": "17.0.1.0.0",
    "category": "Tools",
    "author": "AI ERP Platform",
    "website": "https://example.local",
    "license": "LGPL-3",
    "depends": ["base"],
    "data": [
        "security/academy_security.xml",
        "security/ir.model.access.csv",
        "views/academy_course_views.xml",
    ],
    "application": True,
    "installable": True,
}
academy_core/security/academy_security.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="module_category_academy" model="ir.module.category">
        <field name="name">Academy</field>
        <field name="description">Academy training access groups</field>
        <field name="sequence">35</field>
    </record>

    <record id="academy_group_user" model="res.groups">
        <field name="name">Academy User</field>
        <field name="category_id" ref="academy_core.module_category_academy"/>
        <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
    </record>

    <record id="academy_group_manager" model="res.groups">
        <field name="name">Academy Manager</field>
        <field name="category_id" ref="academy_core.module_category_academy"/>
        <field name="implied_ids" eval="[(4, ref('academy_group_user'))]"/>
        <field name="users" eval="[(4, ref('base.user_admin'))]"/>
    </record>

    <record id="academy_course_rule_user_own" model="ir.rule">
        <field name="name">Academy Course: own records only</field>
        <field name="model_id" ref="model_academy_course"/>
        <field name="groups" eval="[(4, ref('academy_group_user'))]"/>
        <field name="domain_force">[('owner_id', '=', user.id)]</field>
    </record>

    <record id="academy_course_rule_manager_all" model="ir.rule">
        <field name="name">Academy Course: manager can access all</field>
        <field name="model_id" ref="model_academy_course"/>
        <field name="groups" eval="[(4, ref('academy_group_manager'))]"/>
        <field name="domain_force">[(1, '=', 1)]</field>
    </record>
</odoo>
academy_core/views/academy_course_views.xml (diperbarui)
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="academy_course_view_list" model="ir.ui.view">
        <field name="name">academy.course.view.list</field>
        <field name="model">academy.course</field>
        <field name="arch" type="xml">
            <list string="Courses">
                <field name="name"/>
                <field name="code"/>
                <field name="owner_id"/>
                <field name="active"/>
            </list>
        </field>
    </record>

    <record id="academy_course_view_form" model="ir.ui.view">
        <field name="name">academy.course.view.form</field>
        <field name="model">academy.course</field>
        <field name="arch" type="xml">
            <form string="Course">
                <sheet>
                    <group>
                        <field name="name"/>
                        <field name="code"/>
                        <field name="owner_id"/>
                        <field name="active"/>
                    </group>
                    <group>
                        <field name="description"/>
                    </group>
                </sheet>
            </form>
        </field>
    </record>

    <record id="academy_course_action" model="ir.actions.act_window">
        <field name="name">Courses</field>
        <field name="res_model">academy.course</field>
        <field name="view_mode">list,form</field>
    </record>

    <menuitem
        id="academy_menu_root"
        name="Academy"
        sequence="90"
        groups="academy_core.academy_group_user"
    />
    <menuitem
        id="academy_menu_courses"
        name="Courses"
        parent="academy_menu_root"
        action="academy_course_action"
        sequence="10"
        groups="academy_core.academy_group_user"
    />
</odoo>

Perubahan security sudah masuk. Sekarang jalankan update module untuk memastikan rule/group/ACL valid dan tidak ada error saat load XML/CSV:

terminal
docker compose run --rm odoo odoo -d odoo -u academy_core --stop-after-init
docker compose run --rm odoo odoo -d odoo -u academy_core --stop-after-init

Setelah dijalankan, module berhasil masuk ke Step 2 (Security Dasar) dan semuanya ter-load tanpa error. Ringkasan perubahan yang diterapkan di academy_core:

  • Tambah field pemilik data: owner_id pada model academy.course (default ke user login saat create).
  • Tambah grup akses: Academy User dan Academy Manager (Manager mewarisi/imply User, dan admin otomatis masuk ke Manager).
  • Tambah record rule: user hanya bisa akses course miliknya sendiri (owner_id = user.id), sedangkan manager bisa akses semua course.
  • Update ACL (ir.model.access.csv): User dapat read/write/create tanpa delete, Manager dapat full CRUD.
  • Update views: field owner_id ditampilkan di list dan form; menu Academy hanya muncul untuk user yang berada dalam group Academy.
  • Update manifest: load security/academy_security.xml sebelum ir.model.access.csv, karena file ACL mereferensikan group yang didefinisikan di file security tersebut — urutan ini penting, kalau terbalik module akan gagal load.

Validasi yang sudah dilakukan: proses upgrade module berhasil tanpa error, dan tidak ada linter error pada file yang diubah maupun file baru security/academy_security.xml.

Cek Apakah Add-on Kita Sudah Jalan
Module Academy Core terdaftar di Apps

Menu Academy muncul di sidebar setelah module ter-install

Bila Ada Error saat Memilih Menu Academy

Notifikasi error saat klik menu Courses

Saat mencoba membuka menu Courses, muncul error berikut. Error lengkapnya seperti ini:

error log
RPC_ERROR
Odoo Server Error
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/odoo/http.py", line 1985, in _serve_db
    return service_model.retrying(self._serve_ir_http, self.env)
  File "/usr/lib/python3/dist-packages/odoo/service/model.py", line 153, in retrying
    result = func()
  File "/usr/lib/python3/dist-packages/odoo/http.py", line 2013, in _serve_ir_http
    response = self.dispatcher.dispatch(rule.endpoint, args)
  File "/usr/lib/python3/dist-packages/odoo/http.py", line 2217, in dispatch
    result = self.request.registry['ir.http']._dispatch(endpoint)
  File "/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_http.py", line 221, in _dispatch
    result = endpoint(**request.params)
  File "/usr/lib/python3/dist-packages/odoo/http.py", line 799, in route_wrapper
    result = endpoint(self, *args, **params_ok)
  File "/usr/lib/python3/dist-packages/odoo/addons/web/controllers/dataset.py", line 25, in call_kw
    return self._call_kw(model, method, args, kwargs)
  File "/usr/lib/python3/dist-packages/odoo/addons/web/controllers/dataset.py", line 19, in _call_kw
    Model = request.env[model]
  File "/usr/lib/python3/dist-packages/odoo/api.py", line 550, in __getitem__
    return self.registry[model_name](self, (), ())
  File "/usr/lib/python3/dist-packages/odoo/modules/registry.py", line 195, in __getitem__
    return self.models[model_name]
KeyError: 'academy.course'

The above server error caused the following client error:
OwlError: An error occured in the owl lifecycle (see this Error's "cause" property)
    Error: An error occured in the owl lifecycle (see this Error's "cause" property)
        at handleError (http://localhost:8069/web/assets/f0c3177/web.assets_web.min.js:944:101)
        at App.handleError (http://localhost:8069/web/assets/f0c3177/web.assets_web.min.js:1606:29)
        at ComponentNode.initiateRender (http://localhost:8069/web/assets/f0c3177/web.assets_web.min.js:1037:19)

Caused by: RPC_ERROR: Odoo Server Error
    RPC_ERROR
        at makeErrorFromResponse (http://localhost:8069/web/assets/f0c3177/web.assets_web.min.js:2950:163)
        at XMLHttpRequest.<anonymous> (http://localhost:8069/web/assets/f0c3177/web.assets_web.min.js:2954:13)
PENYEBAB
Inti errornya ada di baris KeyError: 'academy.course' — artinya Odoo belum mengenali model academy.course di registry-nya. Penyebab paling umum untuk kasus ini adalah daftar Apps di database belum menyertakan versi terbaru dari module (belum di-refresh) setelah kita menambah/mengubah module tersebut.
Cara Mengatasi Error Ini

Klik Update Apps List di menu Apps (mode developer aktif) untuk merefresh daftar module

Lalu restart container-nya:

terminal
docker compose restart odoo && docker compose logs odoo --since=2m

Log setelah restart — module berhasil ter-load tanpa error

LANGKAH UJI DI UI (DISARANKAN SEKARANG)
  1. Buat user A (Academy User), user B (Academy User), dan user M (Academy Manager).
  2. Login user A, buat 1 course.
  3. Login user B, pastikan course milik A tidak terlihat.
  4. Login manager, pastikan semua course terlihat.

Comments

Popular posts from this blog

Numpang Kerja Remote dari Bandung Creative Hub

Debugging PHP Web dengan XDebug di Intellij IDEA (PHP STORM)

Membangun AI Development Assistant Lokal