Zum Inhalt springen

Odoo Integration: was nach dem Connector bricht

18. September 2026 durch

Die meisten Anfragen zu einer Odoo Integration kommen als Satz über Rohrleitungen: Odoo an den Marktplatz anbinden, ans Lager, an die Bank. Die Rohrleitung ist die einfache Hälfte. Zwei Systeme sind sich längst uneinig darüber, was ein Produkt ist, was in einem Preis steckt und ab wann ein Auftrag echt ist — und der Connector entscheidet diese Streitfragen entweder, oder er trägt sie Nacht für Nacht in Ihre Datenbank.

Hier die Reihenfolge, in der ich vorgehe, und die Dinge, die ich hinterher trotzdem reparieren musste.

Die erste Frage ist keine technische: Wem gehört welches Feld

Bevor irgendein Transport gewählt wird, muss eine Tabelle existieren. Links untereinander jedes Datum, das die Grenze überquert: Produkt, Bestand, Preis, Auftrag, Kunde, Rechnung. Oben zwei Spalten — wer es ändern darf, und wer es nur entgegennimmt.

Der Bestand ist das ehrliche Beispiel. Wenn der Marktplatz Mengen ändern kann und Odoo auch, haben Sie keine Integration, Sie haben ein Wettrennen, und der Verlierer ist das System, das als Zweites geschrieben hat. Also sagt die Tabelle: Mengen gehören Odoo. Der Marktplatz bekommt sie mitgeteilt, und was auch immer er zu wissen glaubt, wird beim nächsten Durchlauf überschrieben. Diese eine Zeile ist mehr wert als jede Menge Merge-Logik, und man einigt sich in zehn Minuten darauf.

Jedes Feld, bei dem die Antwort „beide" lautet, kommt später zurück. Das erste, das ich prüfe, ist die E-Mail-Adresse des Kunden, weil sie am ehesten auf beiden Seiten bearbeitet wird — einmal im Webshop vom Kunden, der einen Tippfehler korrigiert, einmal in Odoo von der Person, die ans Telefon geht. Der nächste Durchlauf gewinnt still und leise, und niemand erfährt davon, bis die Rechnungen an die alte Adresse gehen.

Der Transport ist eine Entscheidung, und in Odoo 19 hat sie sich geändert

Die Odoo API gibt es in mehreren Ausführungen, und die falsche zu wählen ist heute billig und im vierten Monat teuer.

XML-RPC und JSON-RPC sind eingebaut und brauchen auf der Odoo-Seite keinen Code. Gut für ein Skript, das anderswo lebt und eine überschaubare Zahl von Datensätzen bewegt. Schlecht als Rückgrat einer belebten Synchronisation: Jeder execute_kw trägt Zugangsdaten mit und meldet sich neu an, eine Schleife über zehntausend Datensätze sind also zehntausend Logins.

Zwei Dinge sollte man wissen, bevor man darauf baut. Als Zugangsdatum gehört ein API-Key aus den eigenen Einstellungen des Benutzers hin, kein Passwort — mit aktivierter Zwei-Faktor-Anmeldung funktioniert ein Passwort ohnehin nicht. Und in Odoo 19 sind die alten Endpunkte abgekündigt: Der Quellcode sagt es unverblümt, /xmlrpc, /xmlrpc/2 und /jsonrpc sind auf dem Weg nach draußen, POST /json/2/<model>/<method> mit Bearer-Token übernimmt. Heute antworten sie noch. Wenn Sie jetzt einen Connector gegen 19 schreiben, schreiben Sie ihn gegen den neuen Weg; wenn Sie einen erben, der gegen /xmlrpc/2 geschrieben wurde, ist das eine Zeile in der Schätzung für das nächste Upgrade statt einer Überraschung mittendrin.

Ein Controller im eigenen Odoo Modul ist das, was Sie wollen, wenn die Gegenseite Daten schickt und Sie sie prüfen, umformen und mit etwas Sinnvollem beantworten müssen. Er verlangt drei Entscheidungen, die sich nicht aufschieben lassen: auth='user' bedeutet einen echten Benutzer und eine echte Session, auth='public' läuft mit den sehr kleinen Rechten des Website-Benutzers, und auth='none' bedeutet, dass Sie die Authentifizierung in den ersten fünf Zeilen der Methode selbst erledigen. Ein Endpunkt, an den ein fremder Server postet, braucht außerdem csrf=False — und genau in diesem Moment haben Sie die Verantwortung dafür übernommen zu prüfen, wer da anklopft. In 19 heißt der Route-Typ für JSON jetzt jsonrpc; type='json' funktioniert weiterhin und schreibt eine Deprecation-Warnung ins Log.

Webhooks. Odoo hat seit 17 einen eingebauten Empfänger: Eine Automatisierungsregel mit dem Auslöser „bei Webhook" gibt Ihnen eine URL mit einem Secret darin und legt den Body in eine Variable. Für einen Hook mit kleinem Volumen wirklich nützlich. Nur was er nicht tut: Es gibt nirgendwo darin eine Signaturprüfung, das Sicherheitsmodell lautet also „diese URL hat nie jemand in ein Support-Ticket kopiert" — und es gibt einen Rotieren-Button für den Tag, an dem es doch jemand getan hat. Alles, was Geld oder Bestand bewegt, führe ich weiterhin über meinen eigenen Controller, wo die Signatur geprüft wird, bevor der Body geparst wird. Ein öffentlicher Endpunkt, der schreibt, was er bekommt, ist der Schreibzugriff eines Fremden auf Ihre Datenbank.

Dateien über SFTP sind immer noch die Arbeitsweise vieler Lieferanten, und daran ist nichts falsch. Eine CSV, die jeden Morgen um sechs ankommt, ist leichter zu durchschauen als eine API ohne Changelog und ohne Statusseite. Was Dateien brauchen, ist ein Eingangsverzeichnis, ein Archiv des Empfangenen und eine aufgeschriebene Regel für die Zeile, die sich nicht parsen lässt.

Das meiste, was ich baue, nutzt zwei davon: einen Push für Aufträge, weil die eilig sind, und einen nächtlichen Pull, der Summen vergleicht und sich meldet, wenn sie auseinandergehen.

Gehen Sie davon aus, dass jede Nachricht zweimal ankommt

Netze wiederholen, Queues wiederholen, und Menschen drücken den Knopf noch einmal, weil der erste Klick „nichts gemacht" hat. Die Frage war nie, ob eine Nachricht zweimal ankommt.

Die Antwort ist ein externer Schlüssel auf der Odoo-Seite — die Auftragsnummer des Marktplatzes am Auftrag, die Positionsnummer des Lieferanten an der Position — mit einem Unique-Constraint dahinter. Dann aktualisiert die zweite Zustellung einen Datensatz, statt seinen Zwilling anzulegen.

Zwei Details, die andere schon eine Produktionswoche gekostet haben. Erstens: Machen Sie den Schlüssel pro Unternehmen, wenn die Datenbank mehr als eines hat — zwei Unternehmen, die beim selben Lieferanten kaufen, sehen völlig zu Recht dieselbe externe ID. Zweitens: Lesen Sie das Log, wenn der Constraint zum ersten Mal installiert wird. Enthält die Tabelle bereits Duplikate, weigert sich Postgres, ihn anzulegen, Odoo schreibt eine Warnung und macht weiter — das Modul kommt grün hoch, und die Garantie, die Sie gekauft zu haben glaubten, ist nicht da.

Ohne diesen Schlüssel fliegt nichts. Das Lager druckt einfach zwei Kommissionierscheine für einen Auftrag, und der Erste, der davon erfährt, ist die Person, die beide in der Hand hält.

Ausgehend gibt es dasselbe Problem spiegelverkehrt, und es hat keine saubere Lösung: Ihre Datenbanktransaktion kann den HTTP-Aufruf eines anderen nicht umschließen. Committen Sie das Flag vor dem Aufruf, hinterlässt ein Fehlschlag einen Datensatz, der als gesendet markiert ist und es nie war. Committen Sie danach, hinterlässt ein Timeout etwas, das Sie erneut senden. Also wird der Datensatz vor dem Aufruf als „unterwegs" markiert und danach als „gesendet", die Gegenseite wird auf meine Referenz idempotent gemacht, damit zweimal senden langweilig ist, und ein geplanter Durchlauf sammelt alles ein, was seit einer Stunde „unterwegs" ist — denn von außen sieht ein Absturz genau so aus.

Die Mapping-Tabelle ist das eigentliche Projekt

Der Transport kostet einen Tag. Das Mapping kostet das Projekt.

Mengeneinheiten, die es auf einer Seite gibt und auf der anderen nicht. Steuern, die in einem System enthalten sind und im anderen aufgeschlagen werden. Ein Marktplatz, der eine Variante als Produkt behandelt und ein Bundle als Variante. Rundung, die in Odoo keine Eigenschaft des Feldes ist: Die Preisgenauigkeit kommt aus decimal.precision-Datensätzen, die Währungsrundung aus der Währung selbst, zwei gleich aussehende Summen unterscheiden sich also um einen Cent, und die Position matcht stillschweigend nicht. Vergleichen Sie Geld mit float_compare, nie mit ==.

Barcodes treffe ich am häufigsten. Eine Seite speichert eine 13-stellige EAN, die andere verliert die führende Null, weil unterwegs eine Tabellenkalkulation die Datei angefasst hat, und der halbe Katalog findet keine Entsprechung, während beide Systeme Erfolg melden.

Nichts davon ist schwer. Es ist nur lang, und es ist die Stelle, an der eine Integration entweder laut ausspricht, was sie mit den Randfällen tut, oder still zwei Prozent der Zeilen verliert. Diese zwei Prozent findet im März jemand in der Buchhaltung.

Mappings leben als Daten, nicht als Code: ein Modell mit Zugriffsregel, das die Leute des Kunden selbst in einer Listenansicht pflegen. Ich möchte nicht der Flaschenhals für „dieser Lieferant nennt eine Palette PAL und jener PLT" sein.

Was passiert, wenn die Gegenseite weg ist

I wrote in the piece on how modules get built that "retries twice, then parks the order and emails me" is an architecture. This is what it costs to actually have one.

Durchläufe gehen über eine Job-Queue — in der Praxis queue_job von der OCA, weil der Odoo-Kern nichts dergleichen hat. Ein Aufruf wird zu einem Job-Datensatz mit eigenem Kanal, einem Wiederholungsmuster mit wachsenden Abständen und einer Obergrenze, nach der er stehen bleibt und auf einen Menschen wartet, statt weiter auf einen Dienst einzuschlagen, dem es sichtlich nicht gut geht. Das braucht eine Zeile in der Serverkonfiguration und einen Worker, auf dem es läuft — gut zu wissen, bevor jemand es auf billigem Shared Hosting verspricht.

Geplante Arbeit läuft als ir.cron, und hier muss ich etwas korrigieren, das ich früher zu beiläufig gesagt habe: Der Cron braucht keinen eigenen Lock. Odoo sperrt die Zeile selbst, ein Job kann sich also nicht mit seinem eigenen Vorgänger überlappen. Die eigentliche Falle ist die Uhr. Ein Cron-Worker wird nach einem Zeitlimit abgeräumt, ein langer Import scheitert also nicht — er wird in der Mitte durchgeschnitten, und Sie erfahren davon, weil der gestrige Durchlauf bei Lieferant F stehen geblieben ist. Lange Arbeit gehört in die Queue, der Cron ist nur der Startknopf. Und auf einem Server mit Workern brauchen geplante Jobs überhaupt erst einen konfigurierten Cron-Thread; ohne ihn läuft nichts und beschwert sich nichts, was eine sehr leise Art ist, auf die eine Integration tot zur Welt kommt.

Eine Regel, von der ich nicht abrücke: Eine Integration darf niemals stillschweigend scheitern. Ein stehengebliebener Job, den niemand sieht, ist schlimmer als ein Absturz, weil das Unternehmen weiter Entscheidungen auf Zahlen trifft, die sich seit drei Tagen nicht mehr bewegt haben.

Das Volumen ändert die Konstruktion, nicht den Zeitplan

Zweihundert Datensätze verzeihen alles. Bei hunderttausend trifft derselbe Code auf das Request-Timeout, das Speicherlimit und den Rate-Limiter der Gegenseite, meistens in dieser Reihenfolge, und nichts davon war in der Beispieldatei zu sehen, die der Kunde geschickt hat.

Schreiben Sie in Stapeln: create() nimmt eine Liste, und ein Aufruf mit tausend Dictionaries ist ein anderes Tier als tausend Aufrufe. Bei einem Massenimport schalte ich außerdem die Maschinerie ab, die für Menschen gebaut ist — Tracking, Chatter, Benachrichtigungen — weil sonst der größte Teil des Durchlaufs damit vergeht, Nachrichten zu schreiben, die niemand lesen wird.

Delta-Sync heißt, nach dem zu fragen, was sich seit dem letzten Lauf geändert hat, und write_date ist der naheliegende und zugleich leicht tückische Filter: Es bewegt sich, sobald ein gespeichertes berechnetes Feld neu berechnet wird, eine einzige Massenneuberechnung macht also den ganzen Katalog „geändert". Über Löschungen sagt es ebenfalls nichts. Ich führe meinen eigenen Zeitstempel des letzten Laufs am Connector und behandle Löschungen ausdrücklich, denn keines der beiden Systeme wird von sich aus erzählen, dass eine Zeile aufgehört hat zu existieren.

Scale multiplies in ways the design has to know about in advance. A catalogue of a hundred and ten thousand products will not fit a full nightly pull into any sensible window. An instance serving forty storefronts is forty price lists and forty sets of stock in one run.

Der erste Tag ist ein eigenes Projekt

Nichts von alledem beschreibt den Go-live. Bevor eine nächtliche Synchronisation überhaupt eine Rolle spielt, bewegt jemand die Historie: offene Aufträge, aktuelle Bestände, die Kunden, die es in beiden Systemen unter leicht verschiedenen Namen gibt.

Das ist ein eigener Wegwerf-Import mit eigenem Abgleichsbericht — wie viele Zeilen zugeordnet wurden, wie viele neu angelegt, wie viele für einen Menschen zum Nachsehen übrig blieben — und er läuft zweimal auf einer Kopie, bevor er auch nur in die Nähe der Produktion kommt. Danach laufen die beiden Systeme meistens eine Woche parallel, mit täglichem Summenvergleich. Diese Woche ist die billigste Gelegenheit, die Sie je bekommen werden, um herauszufinden, dass das Mapping falsch war.

Wie getestet wird, wenn die Gegenseite ein echter Lieferant ist

Wenige Partner haben eine Sandbox, und eine Staging-Datenbank ist eine Kopie der Produktion mit echten Zugangsdaten darin. Die erste unveränderte Generalprobe bestätigt echte Aufträge bei einem echten Lieferanten und schreibt echten Kunden eine Mail.

Also bekommt Staging die übliche Neutralisierung — ausgehende Mails aus, geplante Jobs aus — und obendrein werden die Zugangsdaten des Connectors gegen Testzugänge getauscht oder auf eine Datei auf der Platte gerichtet. Wo es überhaupt keinen Testendpunkt gibt, zeichne ich eine Woche echter Antworten auf und spiele sie wieder ein. Das ist in der Einrichtung langsamer, und es ist der einzige Weg, die Fehlerfälle zu testen, deren Wiederholung man von einem Partner nicht auf Zuruf verlangen kann.

Zugangsdaten, und die Versuchung sudo

API-Keys leben im eigenen Konfigurationsdatensatz des Moduls oder in Systemparametern, nie im Quellcode. Beides ist besser als ein Repository, und keines von beiden ist ein Tresor: Systemparameter werden über sudo gelesen, jeder Code in der Datenbank kann den Wert also holen, und der Wert reist in jedem Backup mit — auch in der Kopie auf Staging und der auf irgendeinem Laptop. Was einen Key von Bildschirmen fernhält, ist, das Feld auf die Einstellungsgruppe zu beschränken, denn das entfernt es aus der Ansicht, statt es nur zu verbergen. Wo das Hosting es erlaubt, ist der sicherste Ort die Serverkonfigurationsdatei, weil die nicht mit dem Dump mitreist.

Und jetzt das, wonach ich in fremdem Code am gründlichsten suche, in meinem eigenen auch: sudo(). Es setzt Zugriffsregeln und Datensatzregeln in einem Zug außer Kraft, es ist der schnellste Weg, eine Integration zum „Funktionieren" zu bringen, und in einem öffentlichen Controller ist es der Weg, auf dem ein für einen einzigen Lieferanten gebauter Endpunkt jedem antwortet, der die URL errät. Wenn ein öffentlicher Endpunkt tatsächlich ein Modell anfassen muss, ist die ehrliche Form ein eigener Benutzer mit genau diesen Rechten und with_user().

Wer zahlt, wenn sich deren API ändert

Eine Integration wird nicht einmal gekauft, denn die Gegenseite hat eine Stimme. Marktplätze benennen Felder um, Banken ändern die Authentifizierung, und Odoo selbst mustert Transporte aus — die abgekündigten RPC-Endpunkte weiter oben sind genau diese Geschichte, angekündigt mit Jahren Vorlauf für jeden, der die Release Notes liest.

Also steht es schriftlich da: welche Brüche unter Gewährleistung meine sind, welche kostenpflichtig sind, weil die Gegenseite sich bewegt hat, und was ein Versionssprung auf einer der beiden Seiten ungefähr kostet. Dann ist die erste Breaking Change ein eingeplanter Nachmittag statt einer Diskussion.

Drei Fragen, bevor Sie eine beauftragen

Welchem System gehört welches Feld, und was passiert, wenn beide es ändern?

Was tut die Integration, wenn die Gegenseite sechs Stunden lang Fehler zurückgibt?

Wie prüfe ich morgen früh, dass der Lauf der letzten Nacht sauber war?

Sie benoten die Antworten nicht, Sie hören hin, ob es sie überhaupt gibt. Die erste sollte eine Tabelle hervorbringen, keinen Satz. In der zweiten sollte irgendwo eine Queue vorkommen. In der dritten sollte ich nicht vorkommen — wenn der einzige Weg zu erfahren, ob der nächtliche Import funktioniert hat, eine Mail an den Entwickler ist, ist die Arbeit nicht fertig; es sollte ein Menüeintrag sein, den die Büroleiterin bei ihrem Kaffee öffnet.

If you have two systems that should be talking and are not, write to me. Bring the field list, even a rough one, and we will spend the first half hour on who owns what.

Odoo Entwicklung: vom Angebot bis zur Produktion