Sari la conținut

Dezvoltare Odoo: de la estimare la producție

18 septembrie 2026 de

O ofertă pentru dezvoltare Odoo e de obicei un singur număr: ore. Nu spune nimic despre ce se întâmplă în interiorul acelor ore, iar acolo se decide dacă modulul Odoo mai funcționează după următoarea schimbare de versiune.

Deci iată ordinea în care lucrez pe proiectele mele. Mare parte din ea e țesut cicatrizat, adunat de la versiunea 15 până la 19.

Înainte să existe vreun modul Odoo, există două documente

Primul e planul: ce trebuie să facă sistemul altfel în ziua în care se termină lucrarea, scris în limbajul afacerii. „Un om din depozit scanează un cod și linia se închide" e un plan. „Adaugă un câmp în stock.move.line" nu e — asta e deja un răspuns, și de regulă unul greșit. De nouă ori din zece, scanarea are nevoie de o regulă de nomenclatură de coduri de bare și de o suprascriere pe livrarea existentă, nu de un câmp nou undeva. Planul trebuie să rămână în limbajul omului din depozit suficient de mult timp cât să se mai vadă asta.

Al doilea e arhitectura, iar aici dezvoltarea Odoo încetează să semene cu dezvoltarea în general. Un modul Odoo nu trăiește singur. Stă în interiorul codului altcuiva — o versiune majoră pe an, un build de corecții în aproape fiecare săptămână, de la o companie care nu știe că exiști. Așa că arhitectura răspunde la un set mai îngust de întrebări:

Care modele existente sunt implicate și dacă e suficient să le moștenești. Ce se adaugă: un câmp, o metodă, o vizualizare, o rută. Și ce rămâne neatins. Ce comportament din nucleu se suprascrie și dacă suprascrierea are măcar forma potrivită — _inherit extinde un model pe loc, în timp ce _inherits adaugă pe tăcute o cheie străină și o înregistrare delegată, și așa ajunge un sistem cu un rând în plus per produs și nimeni nu-și mai amintește de ce.

Which edition the system runs on, because a plan built around an Enterprise model is not a plan on Community. How many companies, websites and languages one instance has to serve: forty websites in a single Odoo instance is a different architecture from one.

Și întrebarea care se sare cel mai constant: de unde vin datele și ce face integrarea Odoo la trei dimineața, când partea cealaltă nu mai răspunde. „Reîncearcă de două ori, apoi parchează comanda și îmi trimite un e-mail" e o arhitectură. Tăcerea e și ea o arhitectură — doar că n-a ales-o nimeni.

Arhitectura se citește înainte să fie construită

Odată ce pagina aia există, o vezi. Deciziile, nu codul: aceste trei modele se ating, ăsta nu, integrarea Odoo trece printr-o coadă de joburi, ca un apel eșuat către furnizor să devină o înregistrare reîncercabilă la care se poate uita cineva, iar tot ce e programat rulează ca ir.cron cu lacăt, pentru că un cron care presupune că nu se suprapune niciodată cu el însuși va greși mai devreme sau mai târziu.

Revizuirea durează cam o oră și e ultimul moment ieftin în care te mai poți răzgândi. Te răzgândești după implementare și plătești o săptămână. Te răzgândești după lansare și plătești în plus și cele două săptămâni în care depozitul a introdus totul de două ori.

Tot aici spun și lucrul care mă costă bani: uneori răspunsul onest e Studio, sau un modul din app store întreținut de altcineva, sau nimic. Odoo face deja asta — prost, dar acceptabil — iar un modul custom ar adăuga un an de mentenanță ca să salveze zece minute pe lună. Un modul Odoo custom e cea mai scumpă dintre opțiunile astea și ar trebui să câștige pe merit.

Implementare: moștenește modelul Odoo, nu-l copia

Regula e plictisitoare: extinde, nu copia niciodată. Un fișier din nucleu copiat funcționează impecabil până la upgrade, iar de atunci e al tău pe veci.

Există o singură excepție onestă. Unele metode din nucleu n-au nicio cusătură — n-ai ce să înconjori cu super(), n-ai de ce te agăța. Când una trebuie înlocuită în întregime, copia poartă un comentariu cu fișierul-sursă, ramura și versiunea din care vine, ca următorul upgrade să aibă un diff de verificat în loc de un mister.

În practică: moștenire pentru modele, xpath pentru vizualizări, QWeb pentru rapoarte, suprascrieri care tot apelează originalul. Câmpurile adăugate pe un model din nucleu poartă prefixul modulului, ca un câmp din nucleu cu același nume, apărut două versiuni mai târziu, să nu aterizeze peste al meu, și ca numele câmpului să spună ce modul l-a pus acolo. Prefixul x_ nu-l ating — ăla e al lui Studio.

Două capcane care merită numite, pentru că amândouă costă un deploy ca să le găsești. Moștenirea de vizualizări nu acceptă un atribut arbitrar drept selector: un xpath pe aria-label e refuzat din start. Iar hasclass(...) nu găsește nimic atunci când șablonul părinte construiește clasa aia cu t-attf-class, pentru că în sursă nu există niciun atribut class. Ambele erori apar abia la instalarea modulului — adică după ce verificările statice erau verzi. Eu citesc XML-ul brut al șablonului pe care îl extind, niciodată HTML-ul randat, unde clasele sunt deja lipite între ele și un selector stricat arată corect.

Încă ceva ce clienții presupun, rezonabil, că funcționează invers: dezinstalarea unui modul șterge coloanele pe care le-a adăugat, și datele din ele. Așa că dezinstalarea se testează pe o copie, iar dacă un câmp ține ceva ce afacerea va mai vrea după aceea, asta se decide înainte să fie scris câmpul, nu în ziua în care cineva bifează căsuța.

Cea mai mare parte din ce face un modul Odoo să supraviețuiască unei migrări Odoo la versiunea următoare se decide aici, în alegeri pe care nimeni nu le vede din afară și pe care toată lumea le simte doi ani mai târziu.

Testele, și ce face de fapt AI-ul aici

Orice modificare netrivială primește teste: un TransactionCase pentru logica din ORM, un tur HttpCase atunci când se schimbă un ecran, ambele marcate post_install, ca să ruleze lângă modulele care chiar vor fi instalate alături de al meu.

Le scriu cu AI, și expresia asta a fost întinsă destul de mult încât merită definită. E rapid la jumătatea mecanică: asamblat fixture-uri, acoperit a treia și a patra ramură a unei condiții, transformat „verifică dacă o comandă anulată eliberează rezervarea" în cod care rulează. Testele erau primul lucru tăiat când estimarea se strâmta. Nu mai sunt, și asta e toată diferența practică — nu că testele au devenit mai bune, ci că acum chiar se scriu.

Ce nu poate face e să decidă ce anume să testeze. Nu știe că exact integrarea asta cade când un furnizor trimite virgulă ca separator zecimal, pentru că faptul ăsta nu trăiește în cod — trăiește în ultimii doi ani de exporturi ale furnizorului respectiv. Alegerea cazurilor care merită un test rămâne treaba mea și n-am găsit nicio cale s-o predau.

Două reguli în jurul acestui lucru. Un test generat trebuie să pice înainte să treacă: dacă nu pot strica codul și să-l văd roșu, testul nu testează nimic, iar testele generate sunt foarte bune la a nu testa nimic în mod convingător. Și fixture-urile sunt înregistrări inventate, nu un export cu clienții tăi.

Staging pe date reale, apoi producție

A change goes to staging first, on a copy of the real database. Demo data is polite. It has no customers with two spaces in the name, no products imported years ago under rules nobody wrote down, no invoices in a currency that was discontinued. It also has no volume: a search that answers instantly on demo data took eight seconds on a hundred and ten thousand products.

O copie a bazei tale e tot baza ta, cu clienții tăi în ea, așa că se neutralizează înainte să lucreze cineva pe ea: e-mailurile de ieșire oprite, joburile programate oprite, procesatorii de plăți pe credențiale de test. Altfel, prima rerulare a unui flux pe staging trimite e-mailuri către trei sute de oameni reali, de pe un server care nici n-ar trebui să existe.

Pe staging, lucrarea e verificată de omul care a cerut-o, pe propriile lui înregistrări. Apoi producția, într-o ordine scrisă: ce modul se actualizează, ce script de migrare rulează înainte de actualizare și care după, și ce se întâmplă cu datele deja aflate în baza de date. Conținutul publicat e surpriza obișnuită — o înregistrare încărcată cu noupdate nu se reimportă orice ai face, așa că un text deja publicat se schimbă scriind în înregistrare dintr-o migrare, sau nu se schimbă deloc. Și un rollback pe care chiar l-am rulat cel puțin o dată. Un rollback presupus nu e un rollback.

Defectele care apar doar în producție

Here is the part of Odoo development that estimates never mention. Some defects need a real cache and a real visitor before they exist at all, and a test database has neither. I have written separately about what holds and what breaks in a live Odoo system; these three are from one week on this very site.

Bannerul de consimțământ scria răspunsul vizitatorului într-un format greșit. „Doar esențiale" salva JSON corect; „Accept" salva textul true, pentru că un consimțământ complet repornește interacțiunile paginii, iar handlerul de închidere al ferestrei rula pe o instanță proaspătă care încă ținea valoarea implicită. Pe server, verificarea care citește cookie-ul ăla așteaptă un dicționar și șterge orice altceva — o ramură de compatibilitate care face exact ce a fost scrisă să facă. Așa că toți cei care apăsau „accept" erau întrebați din nou pe pagina următoare, iar analytics-ul din spate rămânea la zero, arătând perfect instalat.

A doua se ascunde într-un ecran de setări. Dacă un tracker are voie să se încarce se decide într-o metodă care începe cu „dacă bannerul de cookie-uri e oprit, consimțământul e dat" — raționamentul fiind că un proprietar care a oprit bannerul și-a construit consimțământul altfel. Debifezi căsuța aia și fiecare vizitator e numărat tacit drept consimțit, câte o oră odată, pentru că pagina randată e în cache. Nimic din ecranul ăla de setări nu arată periculos.

A treia: un eveniment de conversie era legat de un moment din ciclul de viață al paginii care trecuse deja când se încărca scriptul, pentru că scriptul vine într-un bundle amânat care pornește după ce pagina s-a terminat de încărcat. Formularul funcționa, lead-ul ajungea, cifra rămânea zero.

Toate trei au fost găsite la fel — deschizi site-ul live, apeși ambele butoane, citești cookie-ul și tabul de rețea în loc de cod. Sunt cincisprezece minute și e singurul loc în care bug-urile astea există. Le facturez ca parte din lucrare.

Cât costă versiunea următoare

Articolul ar fi necinstit dacă ți-ar spune să întrebi despre upgrade-uri și apoi ar ocoli întrebarea. Upgrade-ul bazei de date e treaba celor de la Odoo și se pricep. Upgrade-ul codului tău custom e treaba mea, iar prețul depinde de un singur lucru: cât din modul stă peste comportament din nucleu care s-a mutat.

Un modul care își adaugă propriul model, câteva câmpuri și câteva vizualizări supraviețuiește de obicei unei versiuni majore cu o după-amiază de corecții. Un modul care suprascrie create, modifică un raport și moștenește patru șabloane din nucleu înseamnă o săptămână. Merită știut înainte să-l ai pe cap: create e @api.model_create_multi și primește o listă de înregistrări, nu una singură, așa că o suprascriere scrisă pentru un singur set de valori nu eșuează zgomotos — procesează tăcut primul rând dintr-un import în lot și le ignoră pe restul.

Și upgrade-urile se fac versiune cu versiune. Odoo nu sare peste nimic, așa că orice migrare Odoo cară codul custom prin fiecare pas, nu o singură dată la final.

Drepturile fac parte din funcționalitate

Fiecare model pe care îl adaug vine cu regulile lui de acces și cu o regulă de înregistrare atunci când datele nu sunt pentru toată lumea — scrise în același timp cu modelul. Drepturile lipite ulterior pe un modul terminat sunt modul în care un om de vânzări ajunge să citească marjele altei regiuni.

Lucrul pe care îl caut cel mai insistent în codul oricui, inclusiv în al meu, e sudo(). Oprește toate regulile dintr-o dată, e cea mai rapidă cale de a face să dispară un bug de permisiuni, iar într-un controller public e o scurgere de date cu URL prietenos.

Șlefuirea de după lansare

Modulul e gata cam la două săptămâni după lansare, când se întoarce lista de mici frecări: câmpul ăsta ar trebui să aibă o valoare implicită, ecranul ăsta are nevoie de un clic mai puțin, raportul ăsta e citit în fiecare dimineață la opt, deci ar trebui să fie un element de meniu și nu un filtru pe care cineva îl reaplică manual și îl greșește lunea.

Trecerea asta e scurtă. E și diferența dintre un modul care își trece testele și unul pe care oamenii nu-l mai ocolesc. Sari peste ea și ai o funcționalitate corectă tehnic, cu un fișier de calcul tabelar lângă ea care face treaba adevărată.

Singura întrebare care merită pusă

N-o să-ți amintești nimic din toate astea când stai față în față cu un dezvoltator. Așa că iată întrebarea pe care aș pune-o în locul tău:

Ce se întâmplă cu modulul ăsta când Odoo scoate versiunea următoare?

Dacă răspunsul numește un risc concret — suprascrierea asta stă pe o metodă pe care Odoo o atinge des, deci se reverifică la fiecare upgrade — omul a mai făcut o migrare Odoo. „O să fie bine" poate fi și el răspunsul adevărat, pentru un modul care adaugă două câmpuri și o vizualizare de tip listă. Ce asculți e dacă îți poate spune în care dintre cele două cazuri intră al tău.

That question works on anyone you are considering, and the answer tells you more than a badge does — which is most of the difference between an Odoo partner and an independent developer.

If you have a system that runs and a change you are not sure how to make, write to me. The first conversation is about what should be different when the work is done, not about hours.

Partener Odoo sau dezvoltator independent: care e diferența cu adevărat
Din ce se calculează nivelurile Ready, Silver și Gold și cine e plătit când cumpărați mai multe locuri.