Zum Hauptinhalt springen

Odoo Python-Entwicklung: Vollständiger Leitfaden für Anfänger und Profis

Beherrschen Sie die Entwicklung von Odoo Python mit diesem vollständigen Leitfaden, der Modulstruktur, ORM-API, Ansichten, Controller, Vererbungsmuster, Debugging und Tests abdeckt.

E
ECOSIRE Research and Development Team
|23. März 202615 Min. Lesezeit2.9k Wörter|

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.

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

ModusBeschreibungAnwendungsfall
auth='user'Erfordert eine AnmeldesitzungInterne Seiten, Benutzeraktionen
auth='public'Sitzung optional, kann anonym seinWebsite-Seiten
auth='none'Keine Sitzung, keine UmgebungGesundheitschecks, statische Endpunkte
auth='api_key'API-Schlüssel im HeaderExterne 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=True wird 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:

Benötigen Sie erfahrene Odoo-Entwickler? Das Odoo-Entwicklungsteam von ECOSIRE entwickelt individuelle Odoo-Module. 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.

E

Geschrieben von

ECOSIRE Research and Development Team

Technische Redaktion

Das Team für technische Redaktion bei ECOSIRE schreibt über Odoo ERP, E-Commerce mit Shopify, KI-Agenten, Analysen mit Power BI, Automatisierung mit GoHighLevel und Best Practices für Unternehmenssoftware. Unsere Leitfäden helfen Unternehmen, fundierte Technologieentscheidungen zu treffen.

ECOSIRE

Transformieren Sie Ihr Unternehmen mit Odoo ERP

Kompetente Odoo-Implementierung, Anpassung und Support zur Optimierung Ihrer Abläufe.