Odoo 19 modül geliştirme: yapı, kod örnekleri ve sessiz hatalar.
Bir Odoo modülü birkaç Python dosyası, birkaç XML ekranı ve bir erişim tablosundan oluşur. Bu sayfada sıfırdan çalışan bir "Servis Talepleri" modülünü adım adım kuruyoruz; Odoo 18'den 19'a değişen sözdizimini ve kurulumda hata vermeden yanlış çalışan durumları da gösteriyoruz.
# models/servis_talep.py from odoo import api, fields, models class ServisTalep(models.Model): _name = 'servis.talep' _description = 'Servis Talebi' name = fields.Char(string='Konu', required=True) partner_id = fields.Many2one('res.partner', string='Müşteri') sure_saat = fields.Float(string='Süre (saat)') saat_ucreti = fields.Float(string='Saat Ücreti') tutar = fields.Float(compute='_compute_tutar', store=True) @api.depends('sure_saat', 'saat_ucreti') def _compute_tutar(self): for talep in self: talep.tutar = talep.sure_saat * talep.saat_ucreti
Süre veya saat ücreti değişince tutar kendiliğinden yeniden hesaplanır; store=True sayesinde veritabanına da yazılır, listede sıralanabilir.
Odoo modülünün dizin yapısı
Her modül, Odoo'nun addons yolundaki tek bir klasördür. Odoo klasörü __manifest__.py dosyasından tanır; Python dosyalarını __init__.py zinciriyle, XML ve CSV dosyalarını manifest'teki data listesiyle yükler.
Klasör ağacı
Python tarafı models/, ekranlar views/, erişim hakları security/ altında durur. İç içe ikinci bir modül klasörü açılmaz; manifest doğrudan modül kökündedir.
servis_talep/
├── __init__.py from . import models
├── __manifest__.py modülün kimliği ve dosya listesi
├── models/
│ ├── __init__.py from . import servis_talep, res_partner
│ ├── servis_talep.py yeni model
│ └── res_partner.py mevcut modeli genişletme (_inherit)
├── views/
│ ├── servis_talep_views.xml list, form, arama, action
│ └── menus.xml menüler
└── security/
└── ir.model.access.csv kim okur, kim yazar
__manifest__.py
Modülün adı, sürümü, hangi modüllere bağlı olduğu ve hangi veri dosyalarını yükleyeceği burada yazar. data listesinde olmayan bir XML dosyasını Odoo hiç okumaz ve hata da vermez.
# __manifest__.py { 'name': 'Servis Talepleri', 'author': 'NaviCore', 'version': '19.0.1.0.0', 'category': 'Services', 'summary': 'Müşteri servis taleplerinin takibi', 'depends': ['base', 'mail'], # Sıra önemli: önce güvenlik, sonra view'lar, en son menüler 'data': [ 'security/ir.model.access.csv', 'views/servis_talep_views.xml', 'views/menus.xml', ], 'application': True, 'license': 'Other proprietary', }
| Katman | Dil | Görevi |
|---|---|---|
| Model | Python | Veritabanı tablosu, alanlar, hesaplamalar, iş kuralları ve düğme metotları |
| View | XML | Liste, form, arama ve kanban ekranlarının görünümü |
| Action ve menü | XML | Menüye tıklanınca hangi modelin hangi ekranlarla açılacağı |
| Erişim hakları | CSV | Hangi kullanıcı grubunun modeli okuyup yazabileceği |
| Kayıt kuralları | XML | Kullanıcının hangi kayıtları görebileceği (ör. yalnız kendi şirketinin) |
| Arayüz davranışı | JavaScript (OWL) | Standart ekranların yetmediği özel bileşenler |
Adım adım çalışan bir Odoo 19 modülü
Aşağıdaki dosyalar birlikte kurulabilir bir modül oluşturur. Sözdizimi Odoo 19 Community çekirdeğindeki kalıplardan alınmıştır.
1. Model: alanlar, hesaplama ve kurallar
Model, veritabanında bir tablo demektir: servis.talep adı servis_talep tablosuna dönüşür. @api.depends hesaplamanın neye bağlı olduğunu, @api.constrains kayıt yazılırken denetlenecek kuralı söyler.
# models/servis_talep.py from odoo import api, fields, models from odoo.exceptions import ValidationError class ServisTalep(models.Model): _name = 'servis.talep' _description = 'Servis Talebi' _inherit = ['mail.thread'] # değişiklik geçmişi ve mesajlaşma _order = 'tarih desc, id desc' name = fields.Char(string='Konu', required=True, tracking=True) partner_id = fields.Many2one('res.partner', string='Müşteri', required=True) tarih = fields.Date(string='Tarih', default=fields.Date.context_today) state = fields.Selection([ ('yeni', 'Yeni'), ('islemde', 'İşlemde'), ('bitti', 'Tamamlandı'), ], string='Durum', default='yeni', required=True, tracking=True) sure_saat = fields.Float(string='Süre (saat)') currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.company.currency_id) saat_ucreti = fields.Monetary(string='Saat Ücreti', currency_field='currency_id') tutar = fields.Monetary( string='Tutar', currency_field='currency_id', compute='_compute_tutar', store=True) # Odoo 19: _sql_constraints yerine models.Constraint _sure_pozitif = models.Constraint( 'CHECK(sure_saat >= 0)', 'Süre negatif olamaz.', ) @api.depends('sure_saat', 'saat_ucreti') def _compute_tutar(self): for talep in self: talep.tutar = talep.sure_saat * talep.saat_ucreti @api.constrains('state', 'sure_saat') def _check_bitti_sure(self): for talep in self: if talep.state == 'bitti' and not talep.sure_saat: raise ValidationError('Tamamlanan talepte süre girilmelidir.') def action_baslat(self): self.write({'state': 'islemde'}) def action_bitir(self): self.write({'state': 'bitti'})
2. Mevcut modeli genişletmek: _inherit
Odoo'nun kendi kodunu değiştirmek yerine, ayrı bir modülde _inherit ile mevcut modele alan ve davranış eklenir. Odoo güncellendiğinde özelleştirme kaybolmaz.
# models/res_partner.py from odoo import fields, models class ResPartner(models.Model): _inherit = 'res.partner' # yeni model değil: mevcut modele ekleme servis_talep_ids = fields.One2many( 'servis.talep', 'partner_id', string='Servis Talepleri') servis_talep_count = fields.Integer(compute='_compute_servis_talep_count') def _compute_servis_talep_count(self): # Tek sorguda tüm müşterilerin sayısı (döngüde search_count yerine) data = self.env['servis.talep']._read_group( [('partner_id', 'in', self.ids)], ['partner_id'], ['__count']) sayilar = {partner.id: count for partner, count in data} for partner in self: partner.servis_talep_count = sayilar.get(partner.id, 0)
3. Erişim hakları
Yeni her model için en az bir erişim satırı gerekir. Satır yoksa model kurulur ama menüye tıklayan kullanıcı erişim hatası alır; Odoo kurulumda yalnız uyarı basar.
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_servis_talep_user,servis.talep.user,model_servis_talep,base.group_user,1,1,1,0
4. Menü
Menü, action'a ref ile bağlanır. Bu yüzden menü dosyası manifest'te view dosyasından sonra gelir.
<!-- views/menus.xml --> <odoo> <menuitem id="servis_talep_menu_root" name="Servis"/> <menuitem id="servis_talep_menu" name="Talepler" parent="servis_talep_menu_root" action="servis_talep_action"/> </odoo>
5. Ekranlar: list, form, arama ve action
Odoo 19'da liste görünümü <list> etiketiyle yazılır; görünürlük ve salt okunurluk attrs sözlüğü yerine doğrudan Python ifadesiyle verilir (invisible="state != 'yeni'"). Mesaj ve değişiklik geçmişi alanı tek bir <chatter/> etiketidir.
<!-- views/servis_talep_views.xml --> <odoo> <record id="servis_talep_view_list" model="ir.ui.view"> <field name="name">servis.talep.list</field> <field name="model">servis.talep</field> <field name="arch" type="xml"> <list decoration-info="state == 'yeni'" decoration-success="state == 'bitti'"> <field name="tarih"/> <field name="name"/> <field name="partner_id"/> <field name="state" widget="badge"/> <field name="tutar" sum="Toplam"/> <field name="currency_id" column_invisible="True"/> </list> </field> </record> <record id="servis_talep_view_form" model="ir.ui.view"> <field name="name">servis.talep.form</field> <field name="model">servis.talep</field> <field name="arch" type="xml"> <form> <header> <button name="action_baslat" type="object" string="Başlat" class="btn-primary" invisible="state != 'yeni'"/> <button name="action_bitir" type="object" string="Tamamla" class="btn-primary" invisible="state != 'islemde'"/> <field name="state" widget="statusbar" statusbar_visible="yeni,islemde,bitti"/> </header> <sheet> <group> <group> <field name="name"/> <field name="partner_id"/> <field name="tarih"/> </group> <group> <field name="sure_saat" readonly="state == 'bitti'"/> <field name="saat_ucreti" readonly="state == 'bitti'"/> <field name="tutar"/> <field name="currency_id" invisible="1"/> </group> </group> </sheet> <chatter/> </form> </field> </record> <record id="servis_talep_view_search" model="ir.ui.view"> <field name="name">servis.talep.search</field> <field name="model">servis.talep</field> <field name="arch" type="xml"> <search> <field name="name"/> <field name="partner_id"/> <filter name="acik" string="Açık Talepler" domain="[('state', '!=', 'bitti')]"/> <group> <filter name="grp_state" string="Durum" context="{'group_by': 'state'}"/> </group> </search> </field> </record> <!-- action, kullandığı view'lardan SONRA tanımlanır --> <record id="servis_talep_action" model="ir.actions.act_window"> <field name="name">Servis Talepleri</field> <field name="res_model">servis.talep</field> <field name="view_mode">list,form</field> <field name="search_view_id" ref="servis_talep_view_search"/> <field name="context">{'search_default_acik': 1}</field> </record> </odoo>
6. Kurulum ve güncelleme komutları
Python dosyası değiştiyse modül güncellemesinden sonra Odoo servisi yeniden başlatılmalıdır; çalışan süreç yeni kodu kendiliğinden yüklemez. Yalnız XML değiştiyse -u yeterlidir.
# Boş modül iskeleti oluşturur ./odoo-bin scaffold servis_talep /yol/custom_addons # İlk kurulum ./odoo-bin -c odoo.conf -d test_db -i servis_talep --stop-after-init # Kod değiştikten sonra güncelleme ./odoo-bin -c odoo.conf -d test_db -u servis_talep --stop-after-init # Kayıtları elle denemek için Python kabuğu ./odoo-bin shell -c odoo.conf -d test_db >>> env['servis.talep'].search([('state', '=', 'yeni')])
Odoo 19'da değişen sözdizimi
Odoo 18'de çalışan bir modül 19'da kurulumda durabilir. Gerçek projelerde karşılaştığımız farklar:
| Eski yazım (Odoo 18 ve öncesi) | Odoo 19 | Not |
|---|---|---|
<tree> | <list> | Tüm liste görünümlerinde |
attrs="{'invisible': [...]}" | invisible="state != 'draft'" | Doğrudan Python ifadesi; readonly ve required için de aynı |
_sql_constraints = [...] | models.Constraint('CHECK (...)', 'mesaj') | Sınıf niteliği olarak tanımlanır |
<group expand="0" string="Grupla"> (arama) | <group> | expand ve string artık geçersiz; modül ParseError ile kurulmaz |
<t t-name="kanban-box"> | <t t-name="card"> | Kanban kart şablonu |
<div class="oe_chatter">... | <chatter/> | Tek etiket |
res.users.groups_id | res.users.group_ids | Kullanıcıya grup atarken |
product.packaging modeli | Kaldırıldı | Koli ve paket birimleri ayrı ölçü birimi olarak tanımlanır |
stock.move üzerindeki name | Kaldırıldı | create() içinde verilirse Invalid field 'name' hatası |
Odoo geliştirmede sessiz hatalar
Kurulum başarılı görünür ama ekran ya da veri yanlış çalışır. Modüllerimizi bu durumlara karşı test ederek teslim ediyoruz.
XML'de ileriye referans
Odoo veri dosyalarını yukarıdan aşağıya tek geçişte okur. Bir action, kendisinden sonra tanımlanan arama görünümüne ref verirse External ID not found hatası alınır. Sıra: view, arama, action, menü.
Salt okunur zorunlu alan
Ekranda readonly olan alanı arayüz kaydederken göndermez. Alan modelde required=True ise Missing required value hatası çıkar. Çözüm: force_save="1".
View ifadesinde abs()
Tarayıcıdaki ifade değerlendiricisinde abs, round, len yoktur. Kurulum hata vermez, ekran açılınca çöker. x > 0.01 or x < -0.01 yazılır.
Yanlış view'a miras
Bir modelin birden çok ana görünümü olabilir. Action görünüm belirtmiyorsa Odoo önceliği en düşük, eşitse adı alfabetik ilk olanı açar. Miras verdiğiniz görünüm ekranda hiç kullanılmıyor olabilir.
Hesaplamaya bağımlılık eklemek
Çekirdeğin hesaplanan alanını genişletirken @api.depends'e yeni alan eklemek, kayıt oluşturulurken verilen değeri ezebilir. Veri kaybı log'a düşmez; ancak sonradan fark edilir.
Güncelleme sonrası eski kod
-u veritabanını günceller ama çalışan Odoo süreci eski Python kodunu kullanmaya devam eder. Python değiştiyse servis yeniden başlatılır; sürüm numarası veritabanından doğrulanır.
Odoo modül geliştirme hakkında
Odoo modülü hangi dille yazılır?
İş mantığı ve veri modeli Python ile, ekranlar (view), menüler ve erişim kuralları XML ile, erişim hakları CSV ile yazılır. Arayüze özel davranış gerekiyorsa JavaScript (OWL) kullanılır.
Odoo 18 modülü Odoo 19'da çalışır mı?
Çoğu zaman küçük değişiklikler gerekir. Örneğin _sql_constraints yerine models.Constraint, res.users üzerindeki groups_id yerine group_ids kullanılır; product.packaging modeli kaldırılmıştır. Arama görünümündeki group etiketine expand veya string verilirse modül kurulmaz.
Odoo'nun kendi kodunu değiştirmek gerekir mi?
Hayır, değiştirilmemelidir. Değişiklikler ayrı bir modülde _inherit ile yapılır. Böylece Odoo güncellendiğinde özelleştirmeler kaybolmaz ve çekirdek temiz kalır.
Modülde yaptığım değişiklik neden ekranda görünmüyor?
En sık nedenler: XML dosyası __manifest__.py içindeki data listesine eklenmemiştir, modül -u ile güncellenmemiştir, Python değişikliğinden sonra servis yeniden başlatılmamıştır ya da miras verilen view ekranda açılan view değildir.
Odoo Community'de modül geliştirmek için Enterprise lisansı gerekir mi?
Gerekmez. Odoo 19 Community açık kaynaktır; kendi modüllerinizi Community üzerinde geliştirip kurabilirsiniz. Yalnız Enterprise modüllerine bağımlı bir modül Community'de kurulamaz.
Bunlar da ilginizi çekebilir
Odoo'da ihtiyacınız olan modülü birlikte yazalım
Standart Odoo'da olmayan iş akışınızı anlatın; çekirdeğe dokunmadan, güncellemeye dayanıklı bir Odoo 19 modülü olarak geliştirelim. İstanbul merkezli olarak İstanbul'da randevuyla yerinde, Türkiye geneline uzaktan Odoo geliştirme ve destek veriyoruz.
Hemen Arayın: 0 541 156 26 57 veya [email protected] adresine yazın

