Cele mai multe cereri de integrare Odoo vin ca o propoziție despre țevărie: conectează Odoo la marketplace, la depozit, la bancă. Țevăria e jumătatea ușoară. Cele două sisteme sunt deja în dezacord despre ce înseamnă un produs, ce include un preț și când devine reală o comandă, iar conectorul ori tranșează disputele astea, ori ți le trimite în baza de date în fiecare noapte.
Mai jos e ordinea în care merg eu, și lucrurile pe care tot a trebuit să le repar după aceea.
Prima întrebare nu e tehnică: cine deține fiecare câmp
Înainte să fie ales vreun transport, trebuie să existe un tabel. Pe verticală, fiecare bucată de date care trece granița: produs, stoc, preț, comandă, client, factură. Pe orizontală, două coloane — cine are voie s-o schimbe și cine doar o primește.
Stocul e exemplul cinstit. Dacă marketplace-ul poate schimba cantitățile și poate și Odoo, n-ai o integrare, ai o cursă, iar cel care pierde e sistemul care a scris al doilea. Deci tabelul spune: cantitățile sunt ale Odoo. Marketplace-ul e anunțat, iar tot ce crede el că știe e suprascris la rularea următoare. Rândul ăla valorează mai mult decât orice cantitate de logică de îmbinare și durează zece minute să cazi de acord pe el.
Fiecare câmp la care răspunsul e „ambele" se întoarce mai târziu. Primul pe care îl verific e e-mailul clientului, pentru că e câmpul cel mai probabil să fie editat de ambele părți — o dată în magazinul online, de clientul care își corectează o greșeală de tastare, o dată în Odoo, de cine răspunde la telefon. Rularea care se întâmplă următoarea câștigă pe tăcute, și nimeni nu află până când facturile nu pleacă la adresa veche.
Transportul e o alegere, iar în Odoo 19 s-a schimbat
API-ul Odoo poate vorbi în mai multe feluri, iar alegerea greșită e ieftină azi și scumpă în luna a patra.
XML-RPC și JSON-RPC vin din cutie și nu cer niciun rând de cod pe partea Odoo. Bune pentru un script care trăiește în altă parte și mută un număr modest de înregistrări. Proaste ca șiră a spinării pentru o sincronizare aglomerată: fiecare execute_kw cară credențialele și se reautentifică, așa că o buclă peste zece mii de înregistrări înseamnă zece mii de autentificări.
Două lucruri de știut înainte să construiești pe ele. Credențialul ar trebui să fie o cheie API din preferințele utilizatorului, nu o parolă — cu autentificare în doi pași activată, o parolă nici nu funcționează. Iar în Odoo 19 vechile endpoint-uri sunt depreciate: sursa o spune pe șleau, /xmlrpc, /xmlrpc/2 și /jsonrpc sunt pe ducă, iar locul lor îl iau POST /json/2/<model>/<method> și un token bearer. Azi încă răspund. Dacă scrii acum un conector pentru 19, scrie-l pe cel nou; dacă moștenești unul scris pe /xmlrpc/2, ăla e un rând în estimarea următorului upgrade, nu o surpriză în timpul lui.
Un controller într-un modul Odoo propriu e ce vrei atunci când sistemul celălalt împinge date și tu trebuie să le validezi, să le transformi și să răspunzi cu ceva cu sens. Cere trei decizii pe care nu le poți amâna: auth='user' înseamnă un utilizator real și o sesiune reală, auth='public' rulează cu drepturile foarte mici ale utilizatorului de site, iar auth='none' înseamnă că faci tu autentificarea, în primele cinci rânduri ale metodei. Un endpoint către care postează alt server are nevoie și de csrf=False — exact momentul în care ți-ai asumat responsabilitatea de a verifica cine sună. În 19, tipul de rută pentru JSON e acum jsonrpc; type='json' încă merge și scrie în log un avertisment de depreciere.
Webhook-uri. Odoo are un receptor din cutie încă de la 17: o regulă de automatizare cu declanșator „on webhook" îți dă un URL cu un secret în el și aruncă corpul cererii într-o variabilă. Chiar util pentru un hook cu volum mic. Uite ce nu face — nu există nicăieri în el vreo verificare de semnătură, așa că modelul de securitate e „nimeni n-a lipit vreodată URL-ul ăsta într-un tichet de suport", iar pentru ziua în care cineva chiar a făcut-o există un buton de rotire. Orice mișcă bani sau stoc trece tot printr-un controller de-al meu, unde semnătura se verifică înainte să fie parsat corpul. Un endpoint public care scrie orice primește e accesul de scriere al altcuiva în baza ta de date.
Fișierele prin SFTP sunt în continuare modul în care lucrează o grămadă de furnizori, și n-are nimic în neregulă. Un CSV care aterizează în fiecare dimineață la șase e mai ușor de urmărit decât un API fără changelog și fără pagină de stare. Ce cer fișierele e un director de aterizare, o arhivă cu ce s-a primit și o regulă scrisă pentru rândul care refuză să se parseze.
Mare parte din ce construiesc folosește două dintre ele: un push pentru comenzi, pentru că alea sunt urgente, și o extragere de noapte care compară totalurile și se plânge dacă diferă.
Presupune că fiecare mesaj ajunge de două ori
Rețelele reîncearcă, cozile reîncearcă, iar oamenii apasă butonul din nou pentru că primul clic „n-a făcut nimic". Întrebarea n-a fost niciodată dacă un mesaj ajunge de două ori.
Răspunsul e o cheie externă ținută pe partea Odoo — id-ul comenzii din marketplace pe comandă, id-ul liniei furnizorului pe linie — cu o constrângere de unicitate în spate. Atunci a doua livrare actualizează o înregistrare în loc să-i creeze geamănul.
Două detalii care au costat oameni o săptămână de producție. Întâi, fă cheia pe companie dacă baza de date are mai mult de una: două companii care cumpără de la același furnizor vor vedea, pe bună dreptate, același id extern. Apoi, uită-te în log prima dată când se instalează constrângerea. Dacă tabelul conține deja duplicate, Postgres refuză s-o creeze, Odoo scrie un avertisment și merge mai departe — modulul pornește verde, iar garanția pe care credeai că ai cumpărat-o nu e acolo.
Fără cheia aia nu crapă nimic. Depozitul doar tipărește două liste de ridicare pentru o singură comandă, iar primul care află e omul care stă acolo cu amândouă în mână.
Pe direcția de ieșire e aceeași problemă în oglindă, și n-are un răspuns curat: tranzacția ta din baza de date nu poate conține apelul HTTP al altcuiva. Marchezi steagul înainte de apel și o eroare îți lasă o înregistrare marcată ca trimisă, care n-a fost niciodată. Îl marchezi după și un timeout te pune s-o trimiți din nou. Așa că înregistrarea e marcată „în zbor" înainte de apel și „trimisă" după, partea care primește e făcută idempotentă pe referința mea, ca trimiterea de două ori să fie plictisitoare, iar o măturare programată ridică tot ce stă „în zbor" de o oră — pentru că, din afară, așa arată o cădere.
Tabelul de mapare e locul în care stă de fapt proiectul
Transportul ia o zi. Maparea ia proiectul.
Unități de măsură care există pe o parte și pe cealaltă nu. Taxe incluse într-un sistem și adăugate în celălalt. Un marketplace care tratează o variantă ca produs și un pachet ca variantă. Rotunjirea, care în Odoo nu e o proprietate a câmpului: precizia prețului vine din înregistrările decimal.precision, iar rotunjirea monetară din monedă, așa că două totaluri care arată egale diferă cu un cent și linia nu se potrivește, în tăcere. Compară banii cu float_compare, niciodată cu ==.
Codurile de bare sunt cele peste care dau cel mai des. O parte ține un EAN de 13 cifre, cealaltă pierde zeroul din față pentru că un fișier de calcul tabelar a atins fișierul pe drum, și jumătate din catalog nu se potrivește, în timp ce ambele sisteme raportează succes.
Nimic din toate astea nu e greu. E doar lung, și e locul în care o integrare ori spune cu voce tare ce face cu cazurile la limită, ori pierde pe tăcute două procente din rânduri. Procentele alea două sunt ce găsește cineva în martie, în contabilitate.
Mapările trăiesc ca date, nu ca cod: un model cu o regulă de acces, editat dintr-o vizualizare de tip listă de oamenii clientului. Nu vreau să fiu eu gâtul de sticlă pentru „furnizorul ăsta zice paletului PAL și celălalt îi zice PLT".
Ce se întâmplă când partea cealaltă e căzută
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.
Rulările trec printr-o coadă de joburi — în practică queue_job de la OCA, pentru că nucleul Odoo n-are așa ceva. Un apel devine o înregistrare de job cu propriul canal, un tipar de reîncercare cu întârzieri care cresc și un plafon după care se oprește și așteaptă un om, în loc să bată într-un serviciu care e vizibil bolnav. Are nevoie de un rând în configurația serverului și de un worker pe care să ruleze, ceea ce merită știut înainte ca cineva să-l promită pe găzduire partajată ieftină.
Lucrul programat rulează ca ir.cron, și aici trebuie să corectez ceva ce am spus în trecut cu neglijență: cronul n-are nevoie de un lacăt de-al tău. Odoo blochează rândul singur, așa că un job nu se poate suprapune cu propria lui rulare anterioară. Capcana adevărată e ceasul. Un worker de cron e omorât la o limită de timp, așa că un import lung nu eșuează — e tăiat în două, și afli pentru că rularea de ieri s-a oprit la furnizorul F. Lucrul lung stă în coadă, cu cronul doar ca declanșator. Iar pe un server cu workeri, joburile programate au nevoie ca un fir de cron să fie configurat pur și simplu; fără el nu rulează nimic și nu se plânge nimic, ceea ce e un mod foarte tăcut în care o integrare poate fi moartă din start.
O regulă pe care n-o îndoi: o integrare nu are voie să eșueze în tăcere. Un job oprit pe care nu-l vede nimeni e mai rău decât o cădere, pentru că afacerea continuă să ia decizii pe niște cifre care au încetat să se miște acum trei zile.
Volumul schimbă designul, nu calendarul
Două sute de înregistrări iartă orice. La o sută de mii, același cod dă peste timeout-ul cererii, peste limita de memorie și peste limitatorul de rată al celeilalte părți, de obicei în ordinea asta, și niciunul nu se vedea în fișierul de probă trimis de client.
Scrie în loturi: create() primește o listă, iar un apel cu o mie de dicționare e cu totul alt animal decât o mie de apeluri. La un import în masă opresc și mecanica făcută pentru oameni — urmărirea, chatter-ul, notificările — pentru că altfel mare parte din rulare se duce pe scris mesaje pe care nu le va citi nimeni.
Sincronizarea pe diferențe înseamnă să ceri ce s-a schimbat de la ultima rulare, iar write_date e filtrul evident și unul ușor perfid: se mișcă de fiecare dată când un câmp calculat și stocat e recalculat, așa că o singură recalculare în masă face tot catalogul „schimbat". Și nu spune nimic despre ștergeri. Îmi țin propria marcă de ultimă sincronizare pe conector și tratez ștergerile explicit, pentru că niciunul dintre sisteme nu-ți va spune din proprie inițiativă că un rând a încetat să existe.
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.
Prima zi e un proiect în sine
Nimic din ce e mai sus nu descrie lansarea. Înainte să conteze vreo sincronizare de noapte, cineva mută istoria: comenzile deschise, stocul curent, clienții care există în ambele sisteme sub nume ușor diferite.
Ăla e un import separat, de unică folosință, cu propriul lui raport de potrivire — câte rânduri s-au potrivit, câte au fost create, câte au rămas pentru un om — și rulează de două ori pe o copie înainte să se apropie de producție. Apoi, de obicei, cele două sisteme merg în paralel o săptămână, cu o comparație zilnică a totalurilor. Săptămâna aia e cea mai ieftină ocazie pe care o vei avea vreodată să afli că maparea era greșită.
Cum se testează când partea cealaltă e un furnizor viu
Puțini parteneri au un sandbox, iar o bază de staging e o copie a producției cu credențiale reale în ea. Prima repetiție neatinsă confirmă comenzi reale către un furnizor real și trimite e-mailuri unor clienți reali.
Așa că stagingul primește neutralizarea obișnuită — e-mailurile de ieșire oprite, joburile programate oprite — iar peste asta, credențialele conectorului sunt schimbate cu unele de test sau îndreptate către un fișier pe disc. Acolo unde nu există niciun endpoint de test, înregistrez o săptămână de răspunsuri reale și le redau. Se pregătește mai greu și e singura cale de a testa exact eșecurile pe care nu poți cere unui partener să le reproducă la comandă.
Credențialele, și tentația lui sudo
Cheile API stau în înregistrarea de configurare a modulului sau în parametrii de sistem, niciodată în sursă. Amândouă bat un repozitoriu și niciuna nu e un seif: parametrii de sistem se citesc prin sudo, așa că orice cod din baza de date poate lua valoarea, iar valoarea călătorește în fiecare backup — inclusiv în copia de pe staging și în cea de pe laptopul cuiva. Ce ține o cheie departe de ecrane e restricționarea câmpului la grupul de setări, care îl scoate din vizualizare, nu doar îl ascunde. Acolo unde găzduirea permite, cel mai sigur loc e fișierul de configurare al serverului, pentru că ăla nu călătorește cu dump-ul.
Și acum lucrul pe care îl caut cel mai insistent în codul oricui, inclusiv în al meu: sudo(). Renunță dintr-o dată și la regulile de acces, și la cele de înregistrare, e cea mai rapidă cale de a face o integrare „să meargă", iar într-un controller public e felul în care un endpoint construit pentru un singur furnizor ajunge să răspundă oricui ghicește URL-ul. Când un endpoint public chiar trebuie să atingă un model, forma cinstită e un utilizator dedicat cu exact drepturile alea și with_user().
Cine plătește când li se schimbă API-ul
O integrare nu se cumpără o singură dată, pentru că partea cealaltă are și ea un vot. Marketplace-urile redenumesc câmpuri, băncile schimbă autentificarea, iar Odoo însuși scoate transporturi din uz — endpoint-urile RPC depreciate de mai sus sunt exact povestea asta, anunțate cu ani înainte pentru oricine citește notele de versiune.
Așa că se pune pe hârtie: ce stricăciuni sunt ale mele, de reparat în garanție, care se facturează pentru că s-a mișcat partea cealaltă și, aproximativ, cât costă o săritură de versiune pe oricare dintre părți. Atunci prima schimbare care rupe ceva e o după-amiază programată, nu o ceartă.
Trei întrebări înainte să comanzi una
Care sistem deține fiecare câmp și ce se întâmplă când îl schimbă amândouă?
Ce face integrarea când partea cealaltă întoarce erori șase ore la rând?
Cum verific mâine dimineață că rularea de azi-noapte a fost în regulă?
Nu notezi răspunsurile, asculți dacă există. Primul ar trebui să producă un tabel, nu o propoziție. Al doilea ar trebui să conțină pe undeva o coadă. Al treilea n-ar trebui să mă conțină pe mine — dacă singurul mod de a ști dacă importul de azi-noapte a mers e să-i scrii dezvoltatorului, lucrarea nu e terminată; ar trebui să fie un element de meniu pe care șefa de birou îl deschide cu cafeaua.
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.