Belajar Membuat Module di Odoo
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:
__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), daninstallable. Ini yang dibaca Odoo pertama kali saat scanaddons_pathuntuk tahu module apa saja yang tersedia.models/— isinya class Python yang extendmodels.Model, tempat kamu definisikan field dan business logic. Biasanya adamodels/__init__.pyyang meng-import semua file model di folder itu.views/— file XML yang mendefinisikan form view, tree/list view, search view, dan menu. View ini yang "menempel" ke model lewat<field name="model">.security/— dua hal utama:ir.model.access.csv(permission dasar per-model: siapa boleh read/write/create/unlink) dansecurity/*.xmluntuk record rules yang lebih detail (misalnya multi-company).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).controllers/— kalau module butuh expose HTTP endpoint kustom (di luar web client standar Odoo), route-nya didefinisikan di sini pakai decorator@http.route.- Satu hal yang penting dipahami di awal: urutan load itu ditentukan oleh dependency graph dari
dependsdi manifest — bukan urutan folder. Kalaumy_moduledepends kebasedansale, Odoo akan install/upgradebasedansaledulu sebelum masuk kemy_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:
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:
{
"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,
}
- Odoo akan install dependency lebih dulu.
- Modul kamu boleh mewarisi (_inherit) model/view dari modul dependency.
- Kalau dependency dicopot, modul yang bergantung juga ikut terdampak/uninstall.
- Praktik bagus: dependency dibuat sekecil mungkin (hindari over-coupling).
Urutan mental model saat module diinstall:
- baca __manifest__.py,
- pastikan depends terpenuhi,
- load Python package (__init__.py, models, dll),
- load file di data sesuai urutan list,
- load demo bila mode demo aktif.
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.
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?
- Karena Odoo (Python) butuh file itu untuk mengenali package dan menjalankan import chain.
academy_core/__init__.pyfungsinya mengimpor subpackage (models, nanti bisa controllers, wizard, dll). Jadi saat module academy_core diload, Odoo tahu harus masuk ke bagian mana. academy_core/models/__init__.pyfungsinya mengimpor file model konkret, misalnyaacademy_course.py, dan tanpa ini, class AcademyCourse bisa tidak ikut ter-load ke registry Odoo.
Alurnya sederhana:
- Odoo baca __manifest__.py
- Odoo load package module
- Python jalankan academy_core/__init__.py
- dari situ masuk ke models/__init__.py
- 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.
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)
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 |
<?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__.pyservices/odoo/addons/academy_core/__manifest__.pyservices/odoo/addons/academy_core/models/__init__.pyservices/odoo/addons/academy_core/models/academy_course.pyservices/odoo/addons/academy_core/security/ir.model.access.csvservices/odoo/addons/academy_core/views/academy_course_views.xml
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.
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)
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) |
{
"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,
}
<?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>
<?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:
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_idpada modelacademy.course(default ke user login saat create). - Tambah grup akses:
Academy UserdanAcademy 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_idditampilkan di list dan form; menu Academy hanya muncul untuk user yang berada dalam group Academy. - Update manifest: load
security/academy_security.xmlsebelumir.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.
| Module Academy Core terdaftar di Apps |
Menu Academy muncul di sidebar setelah module ter-install
Notifikasi error saat klik menu Courses
Saat mencoba membuka menu Courses, muncul error berikut. Error lengkapnya seperti ini:
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)
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.
Klik Update Apps List di menu Apps (mode developer aktif) untuk merefresh daftar module
Lalu restart container-nya:
docker compose restart odoo && docker compose logs odoo --since=2m
Log setelah restart — module berhasil ter-load tanpa error
- Buat user A (Academy User), user B (Academy User), dan user M (Academy Manager).
- Login user A, buat 1 course.
- Login user B, pastikan course milik A tidak terlihat.
- Login manager, pastikan semua course terlihat.

Comments
Post a Comment