Pengembangan Custom Module Odoo ERP: Panduan Developer untuk Alur Kerja Bisnis Indonesia

Foto oleh Unsplash

Foto oleh Unsplash
Arsitektur modular Odoo menjadikannya salah satu platform ERP paling dapat dikembangkan, namun menyesuaikannya dengan regulasi Indonesia — NPWP, NIK, dan hierarki persetujuan khusus perusahaan — membutuhkan pemahaman mendalam tentang ORM dan sistem inheritance framework tersebut. Panduan ini memandu pembangunan custom module lengkap untuk alur kerja persetujuan pembelian perusahaan PT, dari scaffold direktori hingga deployment produksi.
Setiap module Odoo adalah package Python dengan file __manifest__.py wajib yang mendeklarasikan metadata, dependensi, dan file data module. Module yang terorganisir dengan baik memisahkan concern ke dalam direktori models/, views/, security/, controllers/, dan static/. Menjaga struktur ini konsisten membuat module Anda dapat dirawat oleh developer lain dan kompatibel dengan mekanisme upgrade Odoo.
Manifest mendeklarasikan nama, versi, author, lisensi module, dan yang terpenting daftar depends — aplikasi Odoo lain yang harus diinstal agar module Anda berfungsi. Selalu cantumkan versi Odoo minimum dengan format '17.0.1.0.0'. Menyertakan 'auto_install': False memastikan module hanya aktif saat diinstal secara manual, mencegah perilaku tak terduga di produksi.
File security/ir.model.access.csv mendefinisikan grup user mana yang dapat membuat, membaca, menulis, atau menghapus record di model custom Anda. Record rules (ir.rule) menyediakan keamanan tingkat baris, misalnya membatasi permintaan pembelian hanya untuk kantor cabang pembuat. Selalu definisikan keamanan terlebih dahulu — men-deploy module tanpa ACL yang tepat mengekspos semua data ke semua user internal.
Gunakan 'odoo scaffold nama_module .' untuk membuat skeleton direktori lengkap secara otomatis. Perintah ini membuat __manifest__.py, __init__.py, models/__init__.py, views/, dan security/ dengan import yang benar, menghemat 10–15 menit setup boilerplate per module.
ORM Odoo memetakan kelas Python yang mewarisi models.Model langsung ke tabel PostgreSQL, menangani migrasi secara otomatis ketika Anda menaikkan versi module. Field dideklarasikan sebagai atribut kelas menggunakan tipe field seperti fields.Char, fields.Many2one, dan fields.One2many. Computed field menggunakan @api.depends menghitung ulang otomatis ketika dependensinya berubah, menjaga UI responsif tanpa trigger manual.
Hukum perpajakan Indonesia mengharuskan NPWP (Nomor Pokok Wajib Pajak) dalam format XX.XXX.XXX.X-XXX.XXX untuk transaksi bisnis di atas ambang tertentu. Menggunakan @api.constrains untuk memvalidasi format ini di level ORM memastikan data tidak valid tidak pernah mencapai database, terlepas dari apakah record dibuat melalui UI web, XML-RPC, atau impor data. Demikian pula, NIK harus tepat 16 digit — constraint yang layak diterapkan untuk kepatuhan audit.
# models/purchase_approval.py
from odoo import models, fields, api
from odoo.exceptions import ValidationError
import re
class PurchaseApproval(models.Model):
_name = 'custom.purchase.approval'
_description = 'Custom Purchase Approval for PT Companies'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='Reference', required=True, copy=False,
readonly=True, default='New')
vendor_id = fields.Many2one('res.partner', string='Vendor', required=True)
npwp = fields.Char(string='NPWP Vendor', size=20,
help='Nomor Pokok Wajib Pajak (15-digit tax ID)')
nik_pic = fields.Char(string='NIK PIC', size=16,
help='Nomor Induk Kependudukan of responsible person')
amount_total = fields.Float(string='Total Amount (IDR)', required=True)
approval_level = fields.Selection([
('manager', 'Manager (< 50jt)'),
('director', 'Director (50jt – 500jt)'),
('board', 'Board of Directors (> 500jt)'),
], string='Required Approval Level', compute='_compute_approval_level',
store=True)
state = fields.Selection([
('draft', 'Draft'),
('submitted', 'Submitted'),
('approved', 'Approved'),
('rejected', 'Rejected'),
], default='draft', tracking=True)
@api.depends('amount_total')
def _compute_approval_level(self):
for rec in self:
if rec.amount_total < 50_000_000:
rec.approval_level = 'manager'
elif rec.amount_total < 500_000_000:
rec.approval_level = 'director'
else:
rec.approval_level = 'board'
@api.constrains('npwp')
def _check_npwp(self):
for rec in self:
if rec.npwp and not re.match(r'^\d{2}\.\d{3}\.\d{3}\.\d-\d{3}\.\d{3}$',
rec.npwp):
raise ValidationError(
'Format NPWP tidak valid. Gunakan format: XX.XXX.XXX.X-XXX.XXX'
)
@api.model
def create(self, vals):
if vals.get('name', 'New') == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code(
'custom.purchase.approval') or 'New'
return super().create(vals)Perusahaan PT Indonesia biasanya memerlukan persetujuan pembelian bertingkat: manager untuk jumlah di bawah Rp 50 juta, direktur hingga Rp 500 juta, dan dewan direksi di atas itu. Mengkodekan logika ini dalam computed field yang disimpan (store=True) berarti level persetujuan dipertahankan ke database, memungkinkan filtering dan pelaporan cepat tanpa menghitung ulang di setiap load view.
View di Odoo adalah record XML yang disimpan di model ir.ui.view. Form view mendefinisikan tata letak record individual, tree view menampilkan list, dan search view menambahkan opsi filter ke list. Ketiganya biasanya diperlukan agar model baru dapat digunakan sepenuhnya. Widget field referensi seperti many2one, statusbar, dan stat_button secara signifikan meningkatkan UX dengan kode tambahan minimal.
Saat mewarisi dari model yang ada seperti purchase.order, Anda dapat menyisipkan field baru ke dalam form view yang ada menggunakan inherit_id yang menunjuk ke view asli dan ekspresi xpath untuk menargetkan titik penyisipan dengan tepat. Gunakan position='after', position='before', atau position='inside' untuk mengontrol penempatan. Hindari mengganti seluruh bagian view — targetkan node sekecil mungkin untuk meminimalkan konflik upgrade.
Mengedit file di dalam source tree Odoo (addons/account/, addons/purchase/, dll.) akan ditimpa di setiap upgrade, menghancurkan perubahan Anda dan berpotensi merusak seluruh instalasi. Selalu gunakan _inherit untuk memperluas model yang ada dan inherit_id di XML untuk memperluas view yang ada. Ini tidak bisa ditawar untuk deployment produksi yang akan menerima patch keamanan.
Odoo mengekspos endpoint JSON-RPC dan XML-RPC secara bawaan, tetapi terkadang Anda memerlukan HTTP route custom — untuk aplikasi mobile, penerima webhook, atau integrasi pihak ketiga seperti pengiriman e-Faktur ke DJP. Controller yang mewarisi http.Controller menggunakan dekorator @http.route() dengan auth='user' atau auth='public' tergantung pada persyaratan akses endpoint.
Dokumen bisnis Indonesia memerlukan nomor referensi terformat (misalnya PA/2026/05/0001 untuk persetujuan pembelian). Definisikan record ir.sequence di data XML Anda dengan pengaturan prefix dan padding, lalu panggil self.env['ir.sequence'].next_by_code() di override create() model. Menggunakan 'New' sebagai default dan menggantinya saat simpan adalah konvensi Odoo yang mencegah gap sequence dari record draft yang dibuang.
TransientModel (models.TransientModel) membuat form wizard yang tidak bertahan di database dalam jangka panjang — Odoo membersihkannya secara otomatis setelah periode yang dapat dikonfigurasi. Gunakan wizard untuk dialog konfirmasi persetujuan, operasi massal, atau aksi multi-langkah yang memerlukan input user sebelum commit. Mengikat wizard ke action melalui binding_model_id membuatnya muncul sebagai tombol Action kontekstual di list view yang relevan.
Jalankan 'odoo-bin -d your_db -u your_module --test-enable --stop-after-init' di pipeline CI Anda untuk mengeksekusi unit test di setiap push. Framework test Odoo mendukung TransactionCase untuk test terisolasi dan suite test bertag, memudahkan otomatisasi regression testing untuk logika custom module.
Module Odoo produksi harus menyertakan minimal smoke test yang menginstal module di database bersih dan membuat satu record melalui alur kerja lengkap. Framework test Odoo mendukung unit test (TransactionCase) dan tour test (HttpCase) untuk otomasi UI. Mengintegrasikan ini ke pipeline GitLab CI atau GitHub Actions menangkap regresi sebelum mencapai instance produksi.
Kemas module Anda sebagai file ZIP untuk klien yang tidak memiliki akses server langsung, atau deploy melalui Git dengan menambahkan direktori module Anda ke addons_path di odoo.conf. Untuk lingkungan produksi di provider hosting Indonesia seperti Biznet atau IDCloudHost, gunakan user odoo khusus, jalankan server di belakang Nginx dengan TLS, dan simpan addons custom di luar source tree Odoo agar bertahan dari upgrade.
Istilah kunci dalam artikel ini meliputi models.Model, NPWP, _inherit, and computed field.