Odoo Python Development: Vollständiger Leitfaden für Anfänger und Profis
Laut der Python-Entwicklerumfrage 2025 arbeiten über 47 % der Python-Entwickler in Unternehmen mit mindestens einem ERP-Framework, und Odoo dominiert dieses Segment mit einem Marktanteil von 68 % unter den Open-Source-ERPs. Unabhängig davon, ob Sie Ihr erstes Modul erstellen oder eine unternehmensübergreifende Bereitstellung planen, ist das Verständnis der Python-Ebene von Odoo die wirkungsvollste Fähigkeit, die Sie entwickeln können.
Dieser Leitfaden geht weit über oberflächliche Übersichten hinaus. Wir werden jede Ebene des Python-Backends von Odoo durchgehen – von der Metaklassenmagie des ORM bis zu erweiterten Controller-Mustern, vom Debuggen von Produktionsproblemen bis zum Schreiben von Tests, die tatsächlich Regressionen abfangen.
Wichtige Erkenntnisse
- Odoos ORM ist ein vollständiger objektrelationaler Mapper, der auf Metaklassen basiert, die automatisch SQL-Schemata generieren, Caching verwalten und unternehmensübergreifende Sicherheit handhaben – es genau zu verstehen macht den Unterschied zwischen der Arbeit mit Odoo und der Bekämpfung dagegen aus.
- Fünf Feldkategorien decken jeden Datenmodellierungsbedarf ab: Basisfelder (Char, Integer, Float, Boolean, Text, Html, Date, Datetime, Binary, Selection), relationale Felder (Many2one, One2many, Many2many), berechnete, verwandte und monetäre Felder.
- Drei Vererbungsmechanismen ermöglichen es Ihnen, jeden Teil von Odoo ohne Forking zu erweitern: Klassenvererbung (_inherit), Prototypenvererbung (_inherit + _name) und Delegationsvererbung (_inherits).
- Controller verarbeiten HTTP mithilfe von Dekoratoren (@http.route) mit JSON-RPC- und HTTP-Endpunktmustern, Authentifizierungsmodi und CORS-Konfiguration.
- Automatisierte Tests mit TransactionCase und HttpCase erkennen Regressionen, bevor sie in die Produktion gelangen – streben Sie eine Abdeckung der Geschäftslogik von über 80 % an.
1. Einrichten Ihrer Entwicklungsumgebung
Bevor Sie Code schreiben, benötigen Sie eine geeignete Entwicklungsumgebung. Hier ist das empfohlene Setup für 2026:
# Install system dependencies (Ubuntu/Debian)
# sudo apt install python3.12 python3.12-venv python3.12-dev
# sudo apt install postgresql-17 libpq-dev libxml2-dev libxslt1-dev
# Clone Odoo source (enterprise requires license)
# git clone https://github.com/odoo/odoo.git --branch 18.0 --depth 1
# git clone https://github.com/odoo/enterprise.git --branch 18.0 --depth 1
# Create virtual environment
# python3.12 -m venv odoo-venv
# source odoo-venv/bin/activate
# pip install -r odoo/requirements.txt
# Create database and start Odoo
# createdb mydb
# python odoo/odoo-bin -d mydb --addons-path=odoo/addons,enterprise,custom_addons
Erstellen Sie ein custom_addons-Verzeichnis für Ihre Module. Jedes Modul befindet sich in einem eigenen Unterverzeichnis mit einer spezifischen Struktur, die Odoo erwartet.
2. Tiefer Einblick in die Modularchitektur
Jedes Odoo-Modul folgt einer Konvention-über-Konfiguration-Struktur. Es ist wichtig, die Rolle jeder Komponente zu verstehen:
my_module/
├── __init__.py # Python package initializer
├── __manifest__.py # Module metadata and dependencies
├── models/
│ ├── __init__.py
│ ├── sale_order.py # Business logic models
│ └── res_partner.py # Partner extensions
├── views/
│ ├── sale_order_views.xml # Form, tree, search views
│ └── menu.xml # Menu items and actions
├── controllers/
│ ├── __init__.py
│ └── main.py # HTTP controllers
├── security/
│ ├── ir.model.access.csv # ACL rules
│ └── security.xml # Record rules and groups
├── data/
│ ├── data.xml # Default data
│ └── demo.xml # Demo data (dev only)
├── wizards/
│ ├── __init__.py
│ └── mass_update.py # TransientModel wizards
├── report/
│ ├── report_invoice.xml # QWeb report templates
│ └── report.py # Report parsers
├── static/
│ ├── description/
│ │ └── icon.png # Module icon (128x128)
│ └── src/
│ ├── js/ # OWL components
│ └── xml/ # OWL templates
└── tests/
├── __init__.py
└── test_sale_order.py # Automated tests
Die Manifestdatei
Der __manifest__.py ist das Herzstück Ihres Moduls. Jeder Bereich ist wichtig:
# __manifest__.py
{
'name': 'Sales Commission Engine',
'version': '18.0.1.0.0', # Odoo version.major.minor.patch
'category': 'Sales/Commission',
'summary': 'Automated commission calculations for sales teams',
'description': """
Long description with RST formatting.
Supports multi-tier commission plans.
""",
'author': 'ECOSIRE Private Limited',
'website': 'https://ecosire.com',
'license': 'LGPL-3',
'depends': ['sale', 'account', 'hr'], # Required modules
'data': [
'security/ir.model.access.csv',
'views/commission_plan_views.xml',
'views/menu.xml',
'data/commission_data.xml',
],
'demo': ['data/demo.xml'],
'installable': True,
'application': True, # Shows in Apps menu
'auto_install': False,
'external_dependencies': {
'python': ['pandas'], # pip packages
},
}
Versionsnummerierungskonvention: {odoo_version}.{major}.{minor}.{patch} – die erste Nummer stimmt immer mit der Odoo-Reihe überein.
3. Die ORM-API: Vollständige Referenz
Odoos ORM ist weitaus leistungsfähiger, als den meisten Entwicklern bewusst ist. Es übernimmt automatisch die Schemagenerierung, das Caching, die Zugriffskontrolle und die unternehmensübergreifende Filterung.
Modelltypen
from odoo import models, fields, api
# Persistent model — stored in database
class CommissionPlan(models.Model):
_name = 'commission.plan'
_description = 'Commission Plan'
_order = 'sequence, name'
_rec_name = 'name'
name = fields.Char(string='Plan Name', required=True, index=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
# Transient model — auto-cleaned after ~1 hour
class CommissionWizard(models.TransientModel):
_name = 'commission.calculate.wizard'
_description = 'Calculate Commissions'
date_from = fields.Date(required=True)
date_to = fields.Date(required=True)
# Abstract model — no database table, only for inheritance
class CommissionMixin(models.AbstractModel):
_name = 'commission.mixin'
_description = 'Commission Mixin'
commission_rate = fields.Float(string='Commission %')
Feldtypen Deep Dive
class SaleCommission(models.Model):
_name = 'sale.commission'
_description = 'Sale Commission Record'
# Basic fields
name = fields.Char(string='Reference', required=True, copy=False,
default=lambda self: self.env['ir.sequence'].next_by_code('sale.commission'))
amount = fields.Float(string='Amount', digits=(16, 2))
percentage = fields.Float(string='Rate %', digits=(5, 2))
is_paid = fields.Boolean(string='Paid', default=False)
notes = fields.Text(string='Internal Notes')
description = fields.Html(string='Description', sanitize=True)
date = fields.Date(string='Commission Date', default=fields.Date.today)
create_datetime = fields.Datetime(string='Created', default=fields.Datetime.now)
document = fields.Binary(string='Attachment')
document_name = fields.Char(string='File Name')
# Selection field
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('paid', 'Paid'),
('cancelled', 'Cancelled'),
], string='Status', default='draft', tracking=True)
# Relational fields
salesperson_id = fields.Many2one('res.users', string='Salesperson',
required=True, ondelete='restrict')
order_id = fields.Many2one('sale.order', string='Sale Order')
line_ids = fields.One2many('sale.commission.line', 'commission_id', string='Lines')
tag_ids = fields.Many2many('sale.commission.tag', string='Tags')
# Computed field with store and search
total_amount = fields.Monetary(string='Total', compute='_compute_total',
store=True, currency_field='currency_id')
currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
@api.depends('line_ids.amount')
def _compute_total(self):
for record in self:
record.total_amount = sum(record.line_ids.mapped('amount'))
# Related field (shortcut for computed)
salesperson_email = fields.Char(related='salesperson_id.email', store=True)
CRUD-Operationen
# CREATE — returns new recordset
commission = self.env['sale.commission'].create({
'salesperson_id': self.env.user.id,
'order_id': order.id,
'state': 'draft',
'line_ids': [
(0, 0, {'product_id': product.id, 'amount': 150.00}), # Create
],
})
# Batch create (much faster than loop)
vals_list = [{'name': f'COM-{i}', 'amount': i * 10} for i in range(100)]
records = self.env['sale.commission'].create(vals_list)
# READ — browse by IDs
commission = self.env['sale.commission'].browse(42)
commissions = self.env['sale.commission'].browse([42, 43, 44])
# SEARCH — returns recordset matching domain
pending = self.env['sale.commission'].search([
('state', '=', 'confirmed'),
('date', '>=', '2026-01-01'),
('salesperson_id.department_id.name', '=', 'Sales'),
], order='date desc', limit=50)
# search_count — just the count
count = self.env['sale.commission'].search_count([('state', '=', 'draft')])
# search_read — search + read in one query (optimal for API calls)
data = self.env['sale.commission'].search_read(
[('state', '=', 'confirmed')],
fields=['name', 'amount', 'salesperson_id'],
limit=20, offset=0, order='amount desc'
)
# UPDATE
commission.write({'state': 'paid', 'notes': 'Paid on March 2026'})
# Batch update (all records get same values)
pending.write({'state': 'confirmed'})
# DELETE
old_drafts.unlink()
# SPECIAL: (0, 0, vals) create, (1, id, vals) update, (2, id, 0) delete
# (3, id, 0) unlink M2M, (4, id, 0) link M2M, (5, 0, 0) clear M2M
# (6, 0, [ids]) replace M2M
Domänenausdrücke
Domänen sind die Abfragesprache von Odoo. Meistere sie:
# Basic operators: =, !=, >, >=, <, <=, like, ilike, in, not in
# Special operators: =like (SQL LIKE), =ilike (case-insensitive LIKE)
# child_of, parent_of — for hierarchical models
domain = [
'|', # OR (next two leaves)
('state', '=', 'confirmed'),
'&', # AND (next two leaves)
('state', '=', 'draft'),
('amount', '>', 1000),
('salesperson_id', '!=', False), # Implicit AND with above
('date', '>=', '2026-01-01'),
('tag_ids.name', 'in', ['priority', 'vip']), # Dot notation for relations
]
# Negation
domain = [('state', 'not in', ['cancelled', 'paid'])]
# NULL checks
domain = [('notes', '!=', False)] # Has notes
domain = [('notes', '=', False)] # No notes
4. Vererbungsmuster
Odoo bietet drei Vererbungsmechanismen, mit denen Sie jedes Modul erweitern können, ohne seinen Quellcode zu ändern. Durch die Klassenvererbung werden Felder und Methoden zu vorhandenen Modellen hinzugefügt. Durch die Prototypenvererbung werden neue Modelle basierend auf vorhandenen Modellen mit separaten Datenbanktabellen erstellt. Die Delegationsvererbung verknüpft Modelle durch Komposition und erstellt automatisch Datensätze in beiden Tabellen.
Klassenvererbung (am häufigsten)
class SaleOrderInherit(models.Model):
_inherit = 'sale.order' # Extend existing model
# Add new fields
commission_plan_id = fields.Many2one('commission.plan', string='Commission Plan')
commission_amount = fields.Monetary(compute='_compute_commission')
@api.depends('amount_total', 'commission_plan_id.rate')
def _compute_commission(self):
for order in self:
rate = order.commission_plan_id.rate or 0
order.commission_amount = order.amount_total * rate / 100
# Override existing method
def action_confirm(self):
res = super().action_confirm() # Always call super()
for order in self:
if order.commission_plan_id:
order._create_commission_record()
return res
def _create_commission_record(self):
self.ensure_one()
self.env['sale.commission'].create({
'order_id': self.id,
'salesperson_id': self.user_id.id,
'amount': self.commission_amount,
})
Prototypenvererbung
class ProjectCommission(models.Model):
_name = 'project.commission'
_inherit = 'sale.commission' # Copy structure to new model
_description = 'Project-based Commission'
# Has all fields from sale.commission PLUS its own table
project_id = fields.Many2one('project.project', required=True)
milestone_id = fields.Many2one('project.milestone')
Delegationsvererbung
class CommissionEmployee(models.Model):
_name = 'commission.employee'
_inherits = {'hr.employee': 'employee_id'} # Delegation
_description = 'Commission-eligible Employee'
employee_id = fields.Many2one('hr.employee', required=True, ondelete='cascade')
commission_plan_id = fields.Many2one('commission.plan')
lifetime_earnings = fields.Monetary()
# Can access employee fields directly: record.name, record.department_id
5. Geschäftslogikmuster
Einschränkungen
from odoo.exceptions import ValidationError
class CommissionPlan(models.Model):
_name = 'commission.plan'
rate = fields.Float(required=True)
name = fields.Char(required=True)
# Python constraint
@api.constrains('rate')
def _check_rate(self):
for plan in self:
if not 0 <= plan.rate <= 100:
raise ValidationError(
f"Commission rate must be between 0 and 100. Got: {plan.rate}"
)
# SQL constraint (faster, database-level)
_sql_constraints = [
('name_unique', 'UNIQUE(name, company_id)',
'Commission plan name must be unique per company.'),
('rate_positive', 'CHECK(rate >= 0)',
'Commission rate must be positive.'),
]
Onchange-Methoden
@api.onchange('commission_plan_id')
def _onchange_plan(self):
"""Triggered in UI when field changes. Sets defaults and warnings."""
if self.commission_plan_id:
self.percentage = self.commission_plan_id.rate
if self.commission_plan_id.rate > 30:
return {
'warning': {
'title': 'High Commission Rate',
'message': 'This plan has a rate above 30%. Please confirm with management.',
}
}
Workflow mit State Machine
class SaleCommission(models.Model):
_name = 'sale.commission'
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('approved', 'Approved'),
('paid', 'Paid'),
('cancelled', 'Cancelled'),
], default='draft', tracking=True)
def action_confirm(self):
for rec in self.filtered(lambda r: r.state == 'draft'):
rec.state = 'confirmed'
rec.message_post(body="Commission confirmed.")
def action_approve(self):
self.filtered(lambda r: r.state == 'confirmed').write({
'state': 'approved',
'approved_by': self.env.user.id,
'approved_date': fields.Datetime.now(),
})
def action_pay(self):
for rec in self.filtered(lambda r: r.state == 'approved'):
rec._process_payment()
rec.state = 'paid'
def action_cancel(self):
allowed = self.filtered(lambda r: r.state in ('draft', 'confirmed'))
allowed.write({'state': 'cancelled'})
6. Controller und HTTP-Endpunkte
from odoo import http
from odoo.http import request, Response
import json
class CommissionController(http.Controller):
# JSON-RPC endpoint (used by Odoo's internal JS framework)
@http.route('/commission/calculate', type='json', auth='user', methods=['POST'])
def calculate_commission(self, order_id, **kwargs):
order = request.env['sale.order'].browse(order_id)
if not order.exists():
return {'error': 'Order not found'}
commission = order.commission_amount
return {'order_id': order_id, 'commission': commission}
# HTTP endpoint (REST-style, for external integrations)
@http.route('/api/v1/commissions', type='http', auth='api_key',
methods=['GET'], csrf=False)
def list_commissions(self, **kwargs):
domain = [('state', '!=', 'cancelled')]
if kwargs.get('salesperson_id'):
domain.append(('salesperson_id', '=', int(kwargs['salesperson_id'])))
commissions = request.env['sale.commission'].search_read(
domain,
fields=['name', 'amount', 'state', 'date'],
limit=int(kwargs.get('limit', 50)),
offset=int(kwargs.get('offset', 0)),
)
return Response(
json.dumps({'data': commissions, 'count': len(commissions)}),
content_type='application/json',
status=200,
)
# File download endpoint
@http.route('/commission/report/<int:commission_id>', type='http', auth='user')
def download_report(self, commission_id, **kwargs):
commission = request.env['sale.commission'].browse(commission_id)
pdf = request.env['ir.actions.report']._render_qweb_pdf(
'my_module.commission_report', commission.ids
)
return request.make_response(
pdf[0],
headers=[
('Content-Type', 'application/pdf'),
('Content-Disposition', f'attachment; filename=commission_{commission_id}.pdf'),
]
)
Authentifizierungsmodi
| Modus | Beschreibung | Anwendungsfall |
|---|---|---|
auth='user' | Erfordert eine Anmeldesitzung | Interne Seiten, Benutzeraktionen |
auth='public' | Sitzung optional, kann anonym sein | Website-Seiten |
auth='none' | Keine Sitzung, keine Umgebung | Gesundheitschecks, statische Endpunkte |
auth='api_key' | API-Schlüssel im Header | Externe REST-Integrationen |
7. Sicherheit: ACL- und Aufzeichnungsregeln
Zugriffskontrolllisten (ir.model.access.csv)
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_commission_user,commission.user,model_sale_commission,sales_team.group_sale_salesman,1,0,0,0
access_commission_manager,commission.manager,model_sale_commission,sales_team.group_sale_manager,1,1,1,0
access_commission_admin,commission.admin,model_sale_commission,base.group_system,1,1,1,1
Regeln aufzeichnen (security.xml)
<record id="commission_own_rule" model="ir.rule">
<field name="name">Salesperson sees own commissions</field>
<field name="model_id" ref="model_sale_commission"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<field name="domain_force">[('salesperson_id', '=', user.id)]</field>
</record>
<record id="commission_company_rule" model="ir.rule">
<field name="name">Company-level commission isolation</field>
<field name="model_id" ref="model_sale_commission"/>
<field name="global" eval="True"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
8. Debugging-Techniken
Verwendung der Odoo-Shell
# Start interactive shell with full ORM access
python odoo-bin shell -d mydb --addons-path=addons,custom_addons
# In shell:
>>> orders = env['sale.order'].search([('state', '=', 'sale')], limit=5)
>>> for o in orders:
... print(f"{o.name}: {o.amount_total} - {o.partner_id.name}")
>>> env.cr.commit() # Commit changes made in shell
Protokollierung
import logging
_logger = logging.getLogger(__name__)
class SaleCommission(models.Model):
_name = 'sale.commission'
def action_confirm(self):
_logger.info("Confirming commission %s for user %s", self.name, self.env.user.name)
try:
self._validate_commission()
except Exception as e:
_logger.exception("Failed to confirm commission %s: %s", self.name, e)
raise
Leistungsprofilierung
# Enable SQL logging in odoo.conf:
# log_level = debug_sql
# Or use the profiler decorator
from odoo.tools.profiler import profile
@profile('/tmp/commission_profile')
def action_bulk_calculate(self):
# This will generate a profile dump you can analyze
for order in self.env['sale.order'].search([]):
order._calculate_commission()
Häufige Leistungsprobleme:
– Zugriff auf relationale Felder in Schleifen (N+1 Abfragen) – verwenden Sie stattdessen read() oder mapped()
- Aufrufen von
search()innerhalb von Schleifen – Batch-Suchen –store=Truewird nicht für häufig gefilterte berechnete Felder verwendet - Das Vergessen von
sudo()führt bei jedem Vorgang zu zusätzlichen Zugriffsrechtsprüfungen
9. Automatisiertes Testen
from odoo.tests.common import TransactionCase, HttpCase, tagged
from odoo.exceptions import ValidationError
@tagged('post_install', '-at_install')
class TestCommission(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.env['res.partner'].create({'name': 'Test Customer'})
cls.user = cls.env['res.users'].create({
'name': 'Test Salesperson',
'login': 'test_sales',
'groups_id': [(4, cls.env.ref('sales_team.group_sale_salesman').id)],
})
cls.plan = cls.env['commission.plan'].create({
'name': 'Standard 10%',
'rate': 10.0,
})
def test_commission_calculation(self):
"""Commission amount should be 10% of order total."""
order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'user_id': self.user.id,
'commission_plan_id': self.plan.id,
})
# Add order line...
self.assertAlmostEqual(order.commission_amount, 100.0, places=2)
def test_rate_constraint(self):
"""Rates above 100 should raise ValidationError."""
with self.assertRaises(ValidationError):
self.env['commission.plan'].create({
'name': 'Invalid Plan',
'rate': 150.0,
})
def test_state_machine(self):
"""Commission should follow draft -> confirmed -> paid flow."""
commission = self.env['sale.commission'].create({
'salesperson_id': self.user.id,
'amount': 500,
})
self.assertEqual(commission.state, 'draft')
commission.action_confirm()
self.assertEqual(commission.state, 'confirmed')
# HTTP/Tour tests
@tagged('post_install', '-at_install')
class TestCommissionTour(HttpCase):
def test_commission_dashboard(self):
"""Test that the commission dashboard loads correctly."""
self.start_tour('/web#action=commission.action_dashboard',
'commission_dashboard_tour', login='admin')
Führen Sie Tests durch mit:
# Run all tests for a module
python odoo-bin -d testdb -i my_module --test-enable --stop-after-init
# Run specific test class
python odoo-bin -d testdb --test-tags /my_module:TestCommission --stop-after-init
# Run with coverage
coverage run odoo-bin -d testdb -i my_module --test-enable --stop-after-init
coverage report --include="custom_addons/my_module/*"
10. Erweiterte Muster
Geplante Aktionen (Cron-Jobs)
<record id="ir_cron_monthly_commission" model="ir.cron">
<field name="name">Calculate Monthly Commissions</field>
<field name="model_id" ref="model_sale_commission"/>
<field name="state">code</field>
<field name="code">model._cron_calculate_monthly()</field>
<field name="interval_number">1</field>
<field name="interval_type">months</field>
<field name="numbercall">-1</field>
</record>
Multi-Company-Muster
class CommissionPlan(models.Model):
_name = 'commission.plan'
company_id = fields.Many2one('res.company', required=True,
default=lambda self: self.env.company)
# Use sudo() carefully — only when crossing company boundaries is intended
def get_global_plans(self):
return self.sudo().search([('global_plan', '=', True)])
Mixin-Muster für wiederverwendbare Logik
class CommissionMixin(models.AbstractModel):
_name = 'commission.mixin'
_description = 'Adds commission tracking to any model'
commission_ids = fields.One2many('sale.commission', 'res_id')
commission_total = fields.Monetary(compute='_compute_commission_total', store=True)
@api.depends('commission_ids.amount')
def _compute_commission_total(self):
for record in self:
record.commission_total = sum(record.commission_ids.mapped('amount'))
# Apply to any model via class inheritance
class SaleOrder(models.Model):
_inherit = ['sale.order', 'commission.mixin']
Beispiel aus der Praxis: Komplettes Provisionsmodul
Ein vollständiges, produktionsbereites Beispiel für ein Provisionsmodul finden Sie unter ECOSIREs Open-Source-Odoo-Module, wo wir Konnektoren, Dienstprogramme und Geschäftsmodule veröffentlichen und dabei alle in diesem Leitfaden behandelten Best Practices befolgen.
Unser Odoo-Entwicklungsteam hat über 60 Odoo-Module mit genau diesen Mustern erstellt. Wenn Sie eine individuelle Odoo-Python-Entwicklung benötigen – von einer einzelnen Feldergänzung bis hin zu einer vollständigen ERP-Implementierung – kontaktieren Sie unser Team für eine kostenlose Beratung.
Häufig gestellte Fragen
Welche Python-Version benötigt Odoo 18?
Odoo 18 erfordert Python 3.10 oder höher, wobei Python 3.12 die empfohlene Version ist. Odoo 17 unterstützt Python 3.10+. Überprüfen Sie immer die offizielle Datei „requirements.txt“ auf die genauen Abhängigkeiten, die für Ihre Odoo-Version erforderlich sind.
Wie debugge ich Odoo ORM-Abfragen?
Aktivieren Sie die SQL-Protokollierung, indem Sie log_level in Ihrer odoo.conf-Datei auf debug_sql setzen oder --log-sql in der Befehlszeile übergeben. Sie können self.env.cr.sql_log auch verwenden, um Abfragen im Code zu verfolgen. Verwenden Sie für die Leistungsprofilerstellung den integrierten @profile-Dekorator von odoo.tools.profiler.
Soll ich _inherit oder _inherits zum Erweitern von Modellen verwenden?
Verwenden Sie _inherit (Klassenvererbung) in 95 % der Fälle – es fügt Felder und Methoden zur Tabelle eines vorhandenen Modells hinzu. Verwenden Sie _inherits (Delegierung) nur, wenn Sie ein separates Modell benötigen, das transparent auf die Felder eines anderen Modells zugreift, z. B. beim Erstellen eines speziellen Mitarbeitertyps, der alle hr.employee-Felder erbt.
Wie gehe ich mit der unternehmensübergreifenden Datenisolierung um?
Fügen Sie Ihrem Modell ein Feld „company_id Many2one“ mit einem Standard-Lambda hinzu, das „self.env.company“ zurückgibt. Erstellen Sie dann eine globale ir.rule mit der Domäne [('company_id', 'in', company_ids)]. Dadurch wird sichergestellt, dass Benutzer nur Datensätze von Unternehmen sehen, auf die sie Zugriff haben, was dem standardmäßigen Odoo-Muster für mehrere Unternehmen entspricht.
Was ist der Unterschied zwischen @api.depends und @api.onchange?
@api.depends wird ausgelöst, wenn sich bestimmte Felder in der Datenbank ändern, und wird für gespeicherte berechnete Felder verwendet – es funktioniert sowohl für UI- als auch für programmgesteuerte Änderungen. @api.onchange wird nur in der Benutzeroberfläche ausgelöst, wenn ein Benutzer ein Feld ändert, und wird zum Festlegen von Standardwerten oder zum Anzeigen von Warnungen verwendet. Verwenden Sie @api.depends für Datenintegrität und @api.onchange für UX-Verbesserungen.
Wie teste ich mein Odoo-Modul effektiv?
Verwenden Sie TransactionCase für Komponententests, die die Geschäftslogik testen – jeder Test wird in einer Transaktion ausgeführt, die zurückgesetzt wird. Verwenden Sie HttpCase zum Testen von Controllern und Webtouren. Tag-Tests mit @tagged('post_install', '-at_install') zur Ausführung nach der Modulinstallation. Streben Sie eine Codeabdeckung von mindestens 80 % für Geschäftslogikmethoden an.
Nächste Schritte
Für die Erstellung von Odoo-Modulen in Produktionsqualität sind sowohl technische Fähigkeiten als auch Domänenkenntnisse erforderlich. Beginnen Sie mit der einfachen Klassenvererbung und fahren Sie dann mit vollständig benutzerdefinierten Modulen fort. Zur weiteren Lektüre:
- Odoo Custom Module Development Guide – Allgemeiner Überblick über die Modulentwicklung
- Odoo REST API Tutorial – Externe Integrationen mit Odoo
- Beste ERP-Software 2026 – Odoos Position auf dem Markt verstehen
Benötigen Sie erfahrene Odoo-Entwickler? Das Odoo-Entwicklungsteam von ECOSIRE hat über 200 benutzerdefinierte Module für Unternehmen in 40 Ländern bereitgestellt. Von Provisions-Engines bis hin zu vollständigen Marktplatzintegrationen schreibt unser Team produktionsbereiten Code nach jedem Muster in diesem Leitfaden. Holen Sie sich eine kostenlose Beratung.
Geschrieben von
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.
Verwandte Artikel
KI-gestützte Kundensegmentierung: Von RFM zum Predictive Clustering
Erfahren Sie, wie KI die Kundensegmentierung von statischer RFM-Analyse in dynamisches prädiktives Clustering umwandelt. Implementierungsleitfaden mit Python, Odoo und echten ROI-Daten.
KI zur Supply-Chain-Optimierung: Sichtbarkeit, Vorhersage und Automatisierung
Transformieren Sie Lieferkettenabläufe mit KI: Bedarfserkennung, Lieferantenrisikobewertung, Routenoptimierung, Lagerautomatisierung und Störungsvorhersage. Leitfaden 2026.
B2B-E-Commerce-Strategie: Bauen Sie im Jahr 2026 ein Online-Großhandelsgeschäft auf
Meistern Sie den B2B-E-Commerce mit Strategien für Großhandelspreise, Kontoverwaltung, Kreditbedingungen, Punchout-Kataloge und die Konfiguration des Odoo B2B-Portals.