Skip to Content

Zece capcane mai puțin evidente la migrarea de la Odoo 18 la Odoo 19 CE și incidentul systemd-logind

Un post-mortem ingineresc de producție: analiza criminalistică a eliminării memoriei partajate POSIX în Debian 12 și modificările API majore din Odoo 19 Community Edition.

Fiecare migrare de versiune majoră în sistemele ERP promite îmbunătățiri de performanță, interfețe modernizate și curățarea datoriilor tehnice. Trecerea de la Odoo 18.0 la Odoo 19.0 Community Edition aduce indiscutabil avantaje arhitecturale majore: eliminarea vederilor arhaice din clientul web, modernizarea integrării cu Chart.js, unificarea verificărilor de securitate la nivel de rând și optimizarea masivă a interogărilor de agregare.

Cu toate acestea, dincolo de notele oficiale de lansare se ascunde un câmp minat de modificări incompatibile în ORM, erori tăcute de compilare și interacțiuni periculoase la nivelul nucleului Linux. Pe parcursul unei campanii de migrare desfășurate pe o perioadă de șase luni în infrastructuri cloud de producție, echipa noastră a identificat și remediat o serie de defecte structurale subtile. Multe dintre aceste probleme nu generează erori vizibile în jurnalul de aplicație; dimpotrivă, ele provoacă coruperea tăcută a datelor, eludarea barierelor comerciale de preț în timpul ciclurilor de reevaluare a memoriei cache ORM, alterarea fonturilor chirilice și a diacriticelor românești în facturile PDF sau prăbușirea catastrofală a motorului de baze de date.

Acest raport de laborator prezintă investigația noastră criminalistică asupra cauzelor fundamentale ale acestor disfuncționalități, oferă scripturi executabile de remediere și inventariază cele mai periculoase zece capcane ascunse din noul ORM Odoo 19.

Cronologia criminalistică a prăbușirii memoriei partajate POSIX în PostgreSQL

T+00:00:00 Declanșator
Deconectare de rutină din sesiunea SSH

Inginerul DevOps încheie o sesiune de mentenanță prin SSH; serviciul systemd-logind declanșează apelul user_stop() pentru session-142.

T+00:00:02 Eveniment kernel
Eliminarea memoriei partajate POSIX

systemd-logind execută clean_ipc_by_uid() pentru UID 1001 (postgres), ștergând /dev/shm/PostgreSQL.* și semafoarele IPC.

T+00:00:05 Pană de sistem
Panica procesului master PostgreSQL

Postmaster înregistrează 'FATAL: could not open shared memory segment' și forțează oprirea de urgență; toți workerii Odoo cad.

T+00:00:15 Diagnostic
Analiză journalctl și identificarea cauzei

Inspectarea jurnalului de sistem confirmă corelația directă între închiderea sesiunii logind și dezalocarea segmentelor IPC.

T+00:00:30 Rezolvat
Remediere permanentă aplicată

Configurat RemoveIPC=no în /etc/systemd/logind.conf; repornit serviciul; validată stabilitatea memoriei partajate.

Analiza la nivel de kernel a ștergerii memoriei partajate de către systemd-logind (UID 1001)


Cel mai distructiv incident întâlnit în timpul migrației la Odoo 19 pe sisteme Linux moderne (Debian 12 / Ubuntu 24.04) nu a provenit din vreo eroare de cod Python. Acesta s-a produs la joncțiunea dintre daemonul de sistem systemd-logind și arhitectura memoriei partajate din PostgreSQL.

Manifestarea incidentului

Fără niciun avertisment prealabil sau sarcină anormală, instanța PostgreSQL de producție se oprea brusc cu următoarea eroare fatală:

2026-03-12 14:22:01.402 UTC [8412] postgres@bitnami_odoo FATAL: could not open shared memory segment "/PostgreSQL.184729104": No such file or directory
2026-03-12 14:22:01.404 UTC [8410] LOG: server process (PID 8412) exited with exit code 1
2026-03-12 14:22:01.405 UTC [8410] LOG: terminating any other active server processes
2026-03-12 14:22:01.410 UTC [8410] LOG: all server processes were terminated; reinitializing

Simultan, toate procesele worker HTTP din Odoo, conexiunile deschise prin websocket și thread-urile cron de fundal eșuau instantaneu cu psycopg2.OperationalError: server closed the connection unexpectedly.

Cauza fundamentală a incidentului

În imaginile cloud preconfigurate (cum sunt mașinile virtuale Bitnami Odoo), conturile de sistem non-root sunt create frecvent începând de la identificatorul UID 1000. Pe aceste sisteme:

  • Utilizatorul bitnami primește UID 1000.
  • Utilizatorul de sistem postgres primește UID 1001.

În filozofia de securitate a systemd, orice utilizator cu un UID ≥1000 (prag stabilit prin parametrul SYS_UID_MAX din fișierul /etc/login.defs) este tratat ca un utilizator uman interactiv, și nu ca un serviciu de sistem izolat (care ocupă de regulă plaja de UID-uri 100…999).

În Debian 12, fișierul de configurare /etc/systemd/logind.conf vine preinstalat cu directiva implicită:

RemoveIPC=yes

Conform documentației tehnice a systemd, când parametrul RemoveIPC=yes este activ, serviciul systemd-logind elimină forțat toate semafoarele System V și segmentele de memorie partajată POSIX (/dev/shm/*) aparținând utilizatorului respectiv în momentul în care se încheie ultima sa sesiune interactivă (user_stop()).

De fiecare dată când un script automat, un cron job sau un inginer deschidea o sesiune SSH ori executa comenzi prin sudo -u postgres sau su - postgres, systemd-logind aloca o sesiune tranzitorie. În momentul deconectării sesiunii, logind apela funcția internă clean_ipc_by_uid(1001). Procedând astfel, ștergea fișierul de legătură /dev/shm/PostgreSQL.* și distrugea semafoarele interne folosite de procesul principal postmaster pentru coordonarea memoriei între procesele copil. Neavând acces la memoria partajată, serverul PostgreSQL intra în panică și se prăbușea.

# Verificare Criminalistică în Terminal:
loginctl show-user postgres | grep RemoveIPC

# Protocol de Remediere Permanentă:
sudo mkdir -p /etc/systemd/logind.conf.d/
sudo tee /etc/systemd/logind.conf.d/override-removeipc.conf << 'EOF'
[Login]
RemoveIPC=no
EOF

sudo systemctl restart systemd-logind

Setarea RemoveIPC=no interzice procesului systemd-logind să mai șteargă segmentele de memorie partajată la deconectarea utilizatorilor, eliminând complet riscul de corupere a bazei de date.

Latența Interogărilor și Blocajele Tranzacționale la Eliminarea Memoriei Partajate

Cele zece capcane ascunse din ORM-ul Odoo 19


Migrarea modulelor custom de la Odoo 18 la Odoo 19 nu se rezumă la schimbarea versiunii în __manifest__.py. Nucleul Odoo a introdus modificări structurale majore care eșuează fără a genera erori explicite.

Ignorarea completă a declarațiilor _sql_constraints

În Odoo 18, restricțiile unice de bază de date erau declarate ca o listă de tupluri:

_sql_constraints = [
('code_uniq', 'UNIQUE(code)', 'Codul introdus trebuie să fie unic!'),
]

În Odoo 19, declararea _sql_constraints generează doar un avertisment discret în log. Motorul ORM nu mai execută instrucțiunea SQL ALTER TABLE ADD CONSTRAINT. Dacă afacerea se bazează pe această sintaxă pentru a asigura unicitatea codurilor fiscale, a numerelor de serie sau a token-urilor, baza de date va accepta duplicate fără nicio eroare.

În Odoo 19, restricțiile trebuie definite ca atribute de clasă folosind noua clasă models.Constraint:

_uniq_code = models.Constraint('UNIQUE(code)', 'Codul introdus trebuie să fie unic!')

Moștenirea sub formă de listă _inherit = ['model'] fără _name transformă clasa în AbstractModel

În Odoo 18, declararea _inherit = ['sale.order'] sau _inherit = 'sale.order' fără precizarea atributului _name funcționa identic pentru extinderea modelului existent. În Odoo 19, dacă _inherit este transmis ca o listă Python fără atributul _name, compilatorul de registry tratează clasa în mod automat ca pe un AbstractModel. Modelul nu va modifica tabela din PostgreSQL, câmpurile noi nu vor fi adăugate, iar vederile XML care accesează aceste câmpuri vor cădea cu Field does not exist.

Utilizați întotdeauna un șir simplu de caractere (_inherit = 'sale.order') atunci când extindeți un model existent.

Scoaterea din uz a metodei read_group() în favoarea _read_group()

În Odoo 18, rapoartele analitice foloseau frecvent apelul self.env['account.move.line'].read_group(domain, fields, groupby). În Odoo 19, read_group() este considerată perimată și înlocuită de _read_group(), care returnează tupluri SQL pure în loc de liste greoaie de dicționare, economisind cantități masive de memorie RAM:

Migrarea agregărilor ORM: Odoo 18 vs Odoo 19

# Odoo 19: Returnează tupluri curate (partner_id, count, sum_total)
groups = self.env['account.move']._read_group(
domain=[('state', '=', 'posted')],
groupby=['partner_id'],
aggregates=['__count', 'amount_total:sum']
)
for partner, count, total in groups:
_logger.info("Partener %s: %d facturi, total: %f", partner.name, count, total)
# Odoo 18: Alocă liste masive de dicționare în memorie
groups = self.env['account.move'].read_group(
domain=[('state', '=', 'posted')],
fields=['partner_id', 'amount_total'],
groupby=['partner_id']
)
for g in groups:
_logger.info("Partener %s: %s", g.get('partner_id'), g.get('amount_total'))

Declanșarea @api.constrains sub contextul self.env.su == True

În Odoo 19, când o înregistrare este creată sau actualizată în fluxuri complexe (cum ar fi validarea unei comenzi POS sau confirmarea unei vânzări), câmpurile calculate stocate declanșează recalcularea la finalul tranzacției (flush phase). În această etapă, mediul Odoo este ridicat temporar la privilegii de superadministrator (self.env.su == True).

Dacă o validare comercială a fost scrisă cu scurtătura uzuală:

@api.constrains('price_unit')
def _check_price_floor(self):
if self.env.su:
return # PERICULOS! Ocolit în timpul recalculărilor de flush
if self.price_unit < 10.0:
raise ValidationError("Prețul este sub marja minimă admisă!")

Restricția va fi complet ignorată în operațiunile de masă și în fluxurile automate, permițând salvarea în contabilitate a unor reduceri neautorizate.

Validările bazate pe rolurile utilizatorilor trebuie efectuate explicit în metodele create() și write() unde self.env.user reflectă identitatea operatorului uman, în timp ce @api.constrains trebuie să asigure invariabilitatea matematică indiferent de starea su.

Redenumirea câmpului sale.order.line.product_uom în product_uom_id

În Odoo 19, câmpul moștenit product_uom din linia de comandă de vânzare a fost redenumit conform nomenclaturii standard relaționale în product_uom_id. Toate șabloanele QWeb de facturi și oferte PDF sau codurile Python care accesează line.product_uom.name vor genera erori fatale de tipul AttributeError.

Eliminarea completă a decoratorului @api.returns

Decoratorul perimat @api.returns('model.name') a fost șters complet din Odoo 19. Orice modul care menține acest decorator va eșua la încărcare cu AttributeError: module 'odoo.api' has no attribute 'returns'.

Unificarea check_access_rights() și check_access_rule() în check_access()

În Odoo 19, verificarea securității a fost simplificată. Apelul clasic record.check_access_rights('read') este depășit. Auditul de securitate se realizează prin:

record.check_access('read')

care validează simultan drepturile pe model (ir.model.access) și regulile pe înregistrare (ir.rule).

Modificarea utilitarului CLI de traduceri (opțiunea --i18n-export eliminată)

Comanda clasică odoo-bin --i18n-export=file.po nu mai funcționează în Odoo 19. Gestionarea fișierelor lingvistice se efectuează prin subcomanda dedicată:

odoo-bin i18n export -c /etc/odoo.conf -d bitnami_odoo --modules=my_module -o my_module.pot

În plus, în Odoo 19 traducerile marcate ca incerte (#, fuzzy în fișierele .po) sunt acum încărcate ca traduceri active în baza de date, spre deosebire de Odoo 18 care le ignora.

Substituirea fonturilor în wkhtmltopdf (lipsa diacriticelor ș/ț/ă și a caracterelor chirilice în fontul Lato)

Implicit, Odoo configurează fontul companiei ca fiind res.company.font = 'Lato'. Fișierul inclus /opt/bitnami/odoo/lib/odoo/addons/web/static/fonts/lato/Lato-Reg-webfont.ttf (53 KB) conține exclusiv caracterele din blocul Basic Latin (U+0020–00FF). Acest font nu include diacriticele românești cu virgulă dedesubt (Ș, ș, Ț, ț U+0218–021B, Ă, ă U+0102–0103) și nici caracterele chirilice (U+0400–04FF).

La generarea facturilor PDF, utilitarul wkhtmltopdf desenează literele ASCII cu Lato, dar când întâlnește literele ș, ț sau chirilice, comută brusc pe fontul de rezervă al sistemului de operare (DejaVu Sans). Rezultatul este inacceptabil estetic: litere cu înălțimi diferite în interiorul aceluiași cuvânt și tabele de facturare decalate.

# Instalarea Fonturilor Google în Sistemul de Operare:
sudo mkdir -p /usr/local/share/fonts/truetype/google/
sudo cp -r /opt/bitnami/odoo/lib/odoo/addons/web/static/fonts/google/* /usr/local/share/fonts/truetype/google/
sudo fc-cache -f -v

După actualizarea fontconfig, modificați res.company.font în 'Roboto' sau 'Montserrat' și ștergeți atașamentele cache din tabela ir.attachment.

Prăbușirea indicelui UNACCENT din hr_recruitment în PostgreSQL

La instalarea modulului hr_recruitment pe Odoo 19, scriptul de inițializare execută crearea unui index trigram:

CREATE INDEX IF NOT EXISTS ir_attachment_index_content_applicant_trgm_idx
ON ir_attachment USING gin (UNACCENT(index_content) gin_trgm_ops)
WHERE res_model = 'hr.applicant';

În instalările standard PostgreSQL, funcția unaccent(text) este marcată ca STABLE (provolatile = 's'). PostgreSQL refuză categoric să construiască un index pe baza unei funcții stabile, solicitând ca orice funcție din expresia indexului să fie strict IMMUTABLE. Migrarea se întrerupe cu: psycopg2.errors.InvalidObjectDefinition: functions in index expression must be marked IMMUTABLE.

-- Remediere SQL Imediată:
ALTER FUNCTION public.unaccent(text) IMMUTABLE;
ALTER FUNCTION public.unaccent(regdictionary, text) IMMUTABLE;

Când nu trebuie efectuată migrarea imediat


Trecerea la Odoo 19 nu constituie o urgență absolută pentru orice organizație. Conducerea tehnică trebuie să evalueze cu atenție următoarele trei situații înainte de a planifica fereastra de migrare:

  1. Companiile care depind de zeci de module externe dezvoltate de terți și neauditate pentru modificările din Odoo 19 (_sql_constraints, _read_group, product_uom_id) riscă să lase restricțiile de bază de date inactive și să corupă datele financiare printr-o migrare grăbită.
  2. Organizațiile care rulează pe servere partajate administrate unde echipa tehnică nu poate edita fișierul /etc/systemd/logind.conf pentru a impune RemoveIPC=no sau nu poate instala fonturi în /usr/local/share/fonts își vor expune baza de date la opriri neașteptate ale memoriei partajate pe Debian 12. Rămânerea pe o versiune stabilă LTS anterioară este mult mai prudentă.
  3. Mediile simple cu predominanță de citiri și vederi cache obțin beneficii modeste din noul mecanism models.Constraint și din verificările unificate de securitate, care avantajează în special bazele cu volum masiv de scrieri tranzacționale concurente, astfel încât diferența de performanță nu justifică întotdeauna efortul de rescriere a codului personalizat.

Pentru echipele care pregătesc serverele Debian 12 și Ubuntu 24.04, merită reținut că directiva RemoveIPC=yes a fost concepută inițial ca o politică de securitate systemd pentru stațiile de lucru partajate între mai mulți utilizatori, prevenind scurgerile de date din memorie după deconectare. Dezactivarea ei prin RemoveIPC=no pe serverele de baze de date dedicate nu provoacă scurgeri de memorie RAM, deoarece PostgreSQL își eliberează singur segmentele de memorie partajată la oprirea normală a serviciului. Alături de migrarea restricțiilor _sql_constraints și verificarea obiectelor de font din fișierele PDF cu re.findall(rb'/BaseFont\s*/([^\s/>]+)', pdf_bytes) pentru a depista combinarea nedorită între /Lato-Regular și /DejaVuSans, blocarea curățării automate din systemd-logind este obligatorie înainte de comutarea traficului de producție.

Consolidare SaaS și Migrare ERP dlab.md Engineering Advisory

Pierdeți marjă din cauza abonamentelor SaaS fragmentate sau a unui ERP legacy?

Consolidăm aplicațiile cloud deconectate (CRM, facturare, gestiune stocuri, punți iPaaS) și sistemele legacy într-un nucleu unificat Odoo 19 și MCP în 6 săptămâni, reducând TCO-ul infrastructurii cu până la 68%.