Özel Odoo Modülleri Oluşturma: Geliştirici Eğitimi
Odoo'nun modül sistemi, ERP dünyasındaki en güçlü genişletme çerçevelerinden biridir. Odoo'daki muhasebeden envantere ve CRM'ye kadar her özellik bir modüldür. Bu, özel işlevsellik oluşturmanın Odoo'nun kendi geliştiricileri tarafından kullanılan kalıpların tamamen aynısını takip ettiği ve size çekirdeği çatallamadan tam çerçeveye erişim sağladığı anlamına gelir.
Bu eğitim, özel bir Odoo 19 modülünün tüm yaşam döngüsünü kapsar: dizin yapısını oluşturmak ve modelleri tanımlamaktan görünümler oluşturmaya, erişimi güvence altına almaya ve üretime dağıtmaya kadar. Sonunda Odoo 19 Enterprise kurallarını takip eden ve pazara hazır bir çalışma modülüne sahip olacaksınız.
Önemli Çıkarımlar
- Her Odoo modülü,
__manifest__.pytanımlayıcısına sahip bir Python paketidir- Modeller
models.Model'dan miras alır ve doğrudan PostgreSQL tablolarına eşlenir- Görünümler XML'de ve referans modeli alanlarında ada göre tanımlanır
- Güvenlik,
ir.model.accessCSV ve kayıt kuralları aracılığıyla uygulanır- Sihirbazlar (
TransientModel) çok adımlı kullanıcı etkileşimlerini yönetir- Hesaplanan alanlar ve değişiklik yöntemleri ilgili alanları dinamik olarak günceller
- Otomatik eylemler ve zamanlanmış işler, tetikleyiciler üzerinde sunucu tarafı mantığını çalıştırır
- Modül bağımlılıkları doğru yükleme sırasını ve özellik kullanılabilirliğini sağlar
Modül Yapısı ve İskele
Her Odoo modülü belirli bir yapıya sahip bir dizindir. Genel metni oluşturmak için Odoo'nun yerleşik iskele komutunu kullanın:
# From your Odoo addons directory
python odoo-bin scaffold my_module /path/to/addons
Bu şunu üretir:
my_module/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ └── my_model.py
├── views/
│ └── my_model_views.xml
├── security/
│ ├── ir.model.access.csv
│ └── my_module_security.xml
├── data/
│ └── my_module_data.xml
├── wizard/
│ └── my_wizard.py
├── report/
│ └── my_report.xml
└── static/
└── src/
└── js/
Bildirim dosyası (__manifest__.py)
{
'name': 'My Custom Module',
'version': '19.0.1.0.0',
'summary': 'Short description for module list',
'description': """
Extended description of what this module does.
Can be multi-line RST text.
""",
'author': 'ECOSIRE Private Limited',
'website': 'https://ecosire.com',
'category': 'Sales/CRM',
'depends': ['sale', 'account', 'stock'],
'data': [
'security/ir.model.access.csv',
'security/my_module_security.xml',
'data/my_module_data.xml',
'views/my_model_views.xml',
'views/menu_views.xml',
'report/my_report.xml',
'wizard/my_wizard_views.xml',
],
'assets': {
'web.assets_backend': [
'my_module/static/src/js/my_widget.js',
'my_module/static/src/css/my_styles.css',
],
},
'license': 'OPL-1',
'installable': True,
'application': False,
'auto_install': False,
'price': 249.0,
'currency': 'USD',
}
Sürüm numaralandırma kuralı: {odoo_version}.{major}.{minor}.{patch}. Yeni modüller için her zaman 19.0.1.0.0 ile başlayın.
Modelleri Tanımlama
Modeller herhangi bir Odoo modülünün kalbidir. Veri yapısını ve iş mantığını tanımlarlar.
# models/service_request.py
from odoo import api, fields, models
from odoo.exceptions import ValidationError, UserError
class ServiceRequest(models.Model):
_name = 'my.service.request'
_description = 'Service Request'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'date_request desc, name'
_rec_name = 'name'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self.env['ir.sequence'].next_by_code('my.service.request')
)
state = fields.Selection([
('draft', 'Draft'),
('submitted', 'Submitted'),
('in_progress', 'In Progress'),
('done', 'Completed'),
('cancelled', 'Cancelled'),
], string='Status', default='draft', tracking=True)
partner_id = fields.Many2one(
'res.partner', string='Customer',
required=True, tracking=True,
domain=[('customer_rank', '>', 0)]
)
user_id = fields.Many2one(
'res.users', string='Assigned To',
default=lambda self: self.env.user
)
date_request = fields.Datetime(
string='Request Date',
default=fields.Datetime.now,
required=True
)
date_deadline = fields.Date(string='Deadline')
description = fields.Html(string='Description')
priority = fields.Selection([
('0', 'Normal'),
('1', 'Low'),
('2', 'High'),
('3', 'Urgent'),
], string='Priority', default='0')
tag_ids = fields.Many2many(
'my.service.tag', string='Tags'
)
line_ids = fields.One2many(
'my.service.request.line', 'request_id',
string='Service Lines'
)
amount_total = fields.Float(
string='Total Amount',
compute='_compute_amount_total',
store=True
)
company_id = fields.Many2one(
'res.company', string='Company',
required=True,
default=lambda self: self.env.company
)
@api.depends('line_ids.subtotal')
def _compute_amount_total(self):
for request in self:
request.amount_total = sum(request.line_ids.mapped('subtotal'))
@api.constrains('date_deadline', 'date_request')
def _check_deadline(self):
for record in self:
if record.date_deadline and record.date_request:
if record.date_deadline < record.date_request.date():
raise ValidationError("Deadline cannot be before the request date.")
@api.onchange('partner_id')
def _onchange_partner_id(self):
if self.partner_id:
self.user_id = self.partner_id.user_id or self.env.user
def action_submit(self):
for record in self:
if not record.line_ids:
raise UserError("Cannot submit a request without service lines.")
record.state = 'submitted'
record.message_post(body="Service request submitted for processing.")
def action_start_progress(self):
self.write({'state': 'in_progress'})
def action_mark_done(self):
self.write({'state': 'done'})
def action_cancel(self):
for record in self:
if record.state == 'done':
raise UserError("Cannot cancel a completed request.")
record.state = 'cancelled'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', 'New') == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code(
'my.service.request'
) or 'New'
return super().create(vals_list)
Hizmet Talep Hattı modeli:
class ServiceRequestLine(models.Model):
_name = 'my.service.request.line'
_description = 'Service Request Line'
request_id = fields.Many2one(
'my.service.request', string='Request',
required=True, ondelete='cascade'
)
product_id = fields.Many2one(
'product.product', string='Service',
required=True,
domain=[('type', '=', 'service')]
)
description = fields.Text(string='Description')
quantity = fields.Float(string='Quantity', default=1.0)
price_unit = fields.Float(string='Unit Price')
subtotal = fields.Float(
string='Subtotal',
compute='_compute_subtotal',
store=True
)
@api.depends('quantity', 'price_unit')
def _compute_subtotal(self):
for line in self:
line.subtotal = line.quantity * line.price_unit
@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id:
self.price_unit = self.product_id.lst_price
self.description = self.product_id.description_sale
Görünüm Oluşturma
Görünümler, kayıtların kullanıcı arayüzünde nasıl görüntüleneceğini tanımlar. Odoo formları, listeleri, kanban panolarını ve daha fazlasını tanımlamak için XML kullanır.
<!-- views/service_request_views.xml -->
<odoo>
<!-- Form View -->
<record id="view_service_request_form" model="ir.ui.view">
<field name="name">my.service.request.form</field>
<field name="model">my.service.request</field>
<field name="arch" type="xml">
<form string="Service Request">
<header>
<button name="action_submit" string="Submit"
type="object" class="oe_highlight"
invisible="state != 'draft'"/>
<button name="action_start_progress" string="Start"
type="object" class="oe_highlight"
invisible="state != 'submitted'"/>
<button name="action_mark_done" string="Mark Done"
type="object" class="oe_highlight"
invisible="state != 'in_progress'"/>
<button name="action_cancel" string="Cancel"
type="object"
invisible="state in ['done', 'cancelled']"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,submitted,in_progress,done"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<group>
<group>
<field name="partner_id"
options="{'no_create': True}"/>
<field name="user_id"/>
<field name="priority" widget="priority"/>
</group>
<group>
<field name="date_request"/>
<field name="date_deadline"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<field name="tag_ids" widget="many2many_tags"/>
<notebook>
<page string="Service Lines">
<field name="line_ids">
<tree editable="bottom">
<field name="product_id"/>
<field name="description"/>
<field name="quantity"/>
<field name="price_unit"/>
<field name="subtotal" readonly="1"/>
</tree>
</field>
<group class="oe_subtotal_footer">
<field name="amount_total"
widget="monetary"
class="oe_subtotal_footer_separator"/>
</group>
</page>
<page string="Description">
<field name="description" widget="html"
placeholder="Detailed description..."/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- List View -->
<record id="view_service_request_tree" model="ir.ui.view">
<field name="name">my.service.request.list</field>
<field name="model">my.service.request</field>
<field name="arch" type="xml">
<tree string="Service Requests" decoration-danger="state=='cancelled'"
decoration-success="state=='done'">
<field name="name"/>
<field name="partner_id"/>
<field name="user_id" optional="show"/>
<field name="priority" widget="priority"/>
<field name="date_request"/>
<field name="date_deadline" optional="show"/>
<field name="amount_total" sum="Total"/>
<field name="state" widget="badge"
decoration-info="state=='draft'"
decoration-warning="state=='submitted'"
decoration-primary="state=='in_progress'"
decoration-success="state=='done'"
decoration-danger="state=='cancelled'"/>
</tree>
</field>
</record>
<!-- Search View -->
<record id="view_service_request_search" model="ir.ui.view">
<field name="name">my.service.request.search</field>
<field name="model">my.service.request</field>
<field name="arch" type="xml">
<search>
<field name="name" string="Reference"/>
<field name="partner_id"/>
<field name="user_id"/>
<filter string="My Requests" name="my_requests"
domain="[('user_id', '=', uid)]"/>
<filter string="In Progress" name="in_progress"
domain="[('state', '=', 'in_progress')]"/>
<filter string="Urgent" name="urgent"
domain="[('priority', '=', '3')]"/>
<separator/>
<filter string="This Month" name="this_month"
domain="[('date_request', '>=',
(context_today() - relativedelta(day=1)).strftime('%Y-%m-%d'))]"/>
<group expand="0" string="Group By">
<filter string="Customer" name="group_partner"
context="{'group_by': 'partner_id'}"/>
<filter string="Status" name="group_state"
context="{'group_by': 'state'}"/>
<filter string="Assigned To" name="group_user"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_service_request" model="ir.actions.act_window">
<field name="name">Service Requests</field>
<field name="res_model">my.service.request</field>
<field name="view_mode">list,form,kanban</field>
<field name="search_view_id" ref="view_service_request_search"/>
<field name="context">{'search_default_in_progress': 1}</field>
</record>
</odoo>
Güvenlik Yapılandırması
Güvenlik her üretim modülü için zorunludur.
Erişim kontrol listesi (security/ir.model.access.csv):
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_service_request_user,service.request.user,model_my_service_request,base.group_user,1,1,1,0
access_service_request_manager,service.request.manager,model_my_service_request,base.group_system,1,1,1,1
access_service_request_line_user,service.request.line.user,model_my_service_request_line,base.group_user,1,1,1,1
Kayıt kuralları (security/my_module_security.xml):
<odoo>
<!-- Users can only see their own requests unless they're managers -->
<record id="rule_service_request_own" model="ir.rule">
<field name="name">Service Request: Own Records</field>
<field name="model_id" ref="model_my_service_request"/>
<field name="domain_force">
[('user_id', '=', user.id)]
</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="False"/>
</record>
</odoo>
Sihirbazlar (GeçiciModel)
Sihirbazlar, kılavuzlu çok adımlı eylemler için geçici formlardır.
# wizard/service_request_wizard.py
from odoo import api, fields, models
class ServiceRequestBulkAssign(models.TransientModel):
_name = 'my.service.request.bulk.assign'
_description = 'Bulk Assign Service Requests'
user_id = fields.Many2one(
'res.users', string='Assign To', required=True
)
request_ids = fields.Many2many(
'my.service.request', string='Requests',
default=lambda self: self.env.context.get('active_ids', [])
)
note = fields.Text(string='Note')
def action_assign(self):
self.request_ids.write({'user_id': self.user_id.id})
if self.note:
for request in self.request_ids:
request.message_post(body=self.note)
return {'type': 'ir.actions.act_window_close'}
Otomatik Eylemler ve Sıralar
Otomatik numaralandırma sırası:
<record id="seq_service_request" model="ir.sequence">
<field name="name">Service Request</field>
<field name="code">my.service.request</field>
<field name="prefix">SRQ/%(year)s/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
Planlanmış eylem (cron işi):
# In the model
def _cron_remind_overdue_requests(self):
overdue = self.search([
('state', 'in', ['submitted', 'in_progress']),
('date_deadline', '<', fields.Date.today()),
])
for request in overdue:
request.activity_schedule(
'mail.mail_activity_data_warning',
summary='Overdue Service Request',
user_id=request.user_id.id
)
<record id="ir_cron_remind_overdue" model="ir.cron">
<field name="name">Remind Overdue Service Requests</field>
<field name="model_id" ref="model_my_service_request"/>
<field name="state">code</field>
<field name="code">model._cron_remind_overdue_requests()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active">True</field>
</record>
Sıkça Sorulan Sorular
models.Model, models.TransientModel ve models.AbstractModel arasındaki fark nedir?
models.Model veritabanında kalıcı bir tablo oluşturur. models.TransientModel periyodik olarak temizlenen geçici bir tablo oluşturur (sihirbazlar için kullanılır). models.AbstractModel hiçbir tablo oluşturmaz — bu, diğer modellerin ayrı bir tablo oluşturmadan yöntemler ve alanlar kazanmak için miras alabileceği bir karışımdır.
Mevcut bir Odoo modelini çekirdek kodu değiştirmeden nasıl genişletebilirim?
Mevcut model adıyla _inherit kullanın ve _name'yi atlayın. Bu, alanlarınızı ve yöntemlerinizi mevcut modele ekler: class SaleOrder(models.Model): _inherit = 'sale.order'. Başlangıç noktası olarak başka birinin davranışını kopyalayan yeni bir model oluşturmak için hem _name (yeni ad) hem de _inherit (kaynak model) kullanın.
Modülümün veri modeli değiştiğinde geçiş işlemlerini nasıl yapmalıyım?
my_module/migrations/{version}/pre-migrate.py veya post-migrate.py'de bir geçiş komut dosyası oluşturun. Bu komut dosyaları modül güncellemesi sırasında otomatik olarak çalışır. Sütun yeniden adlandırmaları için openupgradelib yardımcılarını kullanın. Üretime başvurmadan önce her zaman geçişleri üretim veritabanının bir kopyasında test edin.
Çekirdek XML dosyalarını değiştirmeden mevcut Odoo görünümlerini geçersiz kılabilir miyim?
Evet. Genişletmek istediğiniz görünüme başvurmak için inherit_id kullanın, ardından öğeyi bulmak için xpath ifadelerini ve değişikliği belirtmek için position niteliğini (önce, sonra, iç, değiştirme, nitelikler) kullanın. Bu, değişikliklerinizi yalıtılmış ve yükseltme açısından güvenli tutar.
Çok şirketli bir ortamda bir sahayı şirkete özgü hale nasıl getirebilirim?
Alan tanımında company_dependent=True kullanın: my_field = fields.Char(company_dependent=True). Bu, şirket başına ayrı bir değer depolar; dolayısıyla A Şirketi ve B Şirketi aynı kayıt için farklı değerlere sahip olabilir. Bu, fiyat listeleri, vergi hesapları ve diğer şirkete özel yapılandırmalar için kullanılır.
Geliştirme sırasında iletileri günlüğe kaydetmenin ve hata ayıklamanın doğru yolu nedir?
Python'un logging modülünü kullanın: import logging; _logger = logging.getLogger(__name__). Farklı önem düzeyleri için _logger.info(), _logger.warning(), _logger.error() kullanın. Üretim kodunda asla print() ifadelerini kullanmayın. Geliştirme aşamasında, tüm hata ayıklama çıktılarını görmek için Odoo'yu --log-level=debug ile çalıştırın.
Sonraki Adımlar
Üretime hazır bir Odoo modülü oluşturmak, çerçeve hakkında derin bilgi sahibi olmayı, PostgreSQL performans hususlarını, yükseltme güvenli kalıpları ve kapsamlı testleri gerektirir. Odoo pazarına yönelik modüller güvenlik, performans ve kod kalitesi açısından ek doğrulamaya tabi tutulur.
ECOSIRE, özel sektör iş akışlarından pazar konnektör modüllerine kadar belirli iş gereksinimlerine yönelik özel Odoo 19 Enterprise modülleri geliştirir. Geliştirme ekibimiz Odoo'nun resmi kodlama yönergelerini takip eder, kapsamlı birim testleri içerir ve tam belgelerle birlikte modüller sunar.
ECOSIRE'dan Özel Odoo Modülü Devreye Alın →
Gereksinimlerinizi paylaşın, biz de geliştirme çabasının kapsamını belirleyelim, bir zaman çizelgesi sunalım ve Odoo 19 Enterprise kurulumunuzla temiz bir şekilde entegre olan bir modül sunalım.
Yazan
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
İlgili Makaleler
Odoo Muhasebe ve QuickBooks: Ayrıntılı Karşılaştırma 2026
Özellikleri, fiyatlandırmayı, entegrasyonları, ölçeklenebilirliği ve iş gereksinimlerinize hangi platformun uyduğunu kapsayan Odoo Muhasebe ile QuickBooks'un ayrıntılı 2026 karşılaştırması.
Örnek Olay: Odoo Backend ile Shopify'a e-Ticaret Geçişi
Bir moda perakendecisinin WooCommerce'den Shopify'a geçip onu Odoo ERP'ye bağlayarak sipariş karşılama süresini nasıl %71 oranında kısalttığını ve gelirini %43 nasıl artırdığını.
Örnek Olay İncelemesi: Odoo 19 ile Üretim ERP Uygulaması
Pakistanlı bir otomobil parçası üreticisi, ECOSIRE'ın Odoo 19 uygulamasıyla sipariş işleme süresini nasıl %68 oranında azalttı ve envanter sapmasını %2'nin altına düşürdü.