Odoo 19 Community · Python · XML

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.

Modül Yapısı

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',
}
Odoo modülündeki dosya türleri ve görevleri
KatmanDilGörevi
ModelPythonVeritabanı tablosu, alanlar, hesaplamalar, iş kuralları ve düğme metotları
ViewXMLListe, form, arama ve kanban ekranlarının görünümü
Action ve menüXMLMenüye tıklanınca hangi modelin hangi ekranlarla açılacağı
Erişim haklarıCSVHangi kullanıcı grubunun modeli okuyup yazabileceği
Kayıt kurallarıXMLKullanı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
Kod Örnekleri

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 18 → 19

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:

Odoo 18 ile Odoo 19 arasındaki sözdizimi farkları
Eski yazım (Odoo 18 ve öncesi)Odoo 19Not
<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_idres.users.group_idsKullanıcıya grup atarken
product.packaging modeliKaldırıldıKoli ve paket birimleri ayrı ölçü birimi olarak tanımlanır
stock.move üzerindeki nameKaldırıldıcreate() içinde verilirse Invalid field 'name' hatası
Sık Yapılan Hatalar

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.

Sık Sorulanlar

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.

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
WhatsApp