Timp de câteva luni, un client mi-a spus că cifrele lui nu se
potrivesc cu ale mele.
Situația era clasică. Un ERP local, folosit zilnic de o firmă din
România. Un depozit de date corporate, în cloud, care aduna datele din
mai multe țări pentru raportarea grupului. Între ele, un API prin care
se transferau facturile și liniile de factură.
Rapoartele arătau bine. Totalurile păreau plauzibile. Doar că nu se
potriveau cu ce vedeam eu în sursă. Și de fiecare dată când comparam,
comparam altceva: alte produse, alte zile, alte sume.
Așa se pierd lunile. Capturi de ecran de la ei, capturi de ecran de
la mine, și nimeni care să poată demonstra nimic.
Articolul ăsta e despre cum am ieșit din bucla aia, ce am găsit, și
de ce cred că problema e mult mai răspândită decât pare.

Prima greșeală:
comparația pe agregate
Discuția a început de la o zi anume. Ei vedeau alte valori decât mine
pentru câteva produse dintr-o zi de ianuarie.
Când compari totaluri pe produs, poți doar constata că diferă. Nu
poți afla de ce. Un total mai mare poate veni dintr-un rând în plus,
dintr-o cantitate greșită, dintr-un preț greșit sau dintr-o dublare la
încărcare. Toate arată la fel din exterior.
Prima decizie corectă a fost să renunț complet la comparația pe
agregate și să cer datele brute, așa cum stau ele în depozit. Nu o
extragere nouă din API — aia ar fi returnat starea curentă a sursei și
n-ar fi arătat nimic. Ci exact ce aveau ei încărcat.
Pasul zero: fișierele erau
stricate
Aici am pierdut prima zi, și merită povestit pentru că e o capcană
banală.
Exportul a venit ca CSV separat prin virgulă, fără ghilimele în jurul
câmpurilor text. Iar denumirile de produse conțineau virgule:
SET PLACUTE FRANA 19,5" SN6. Adresele la fel:
ȘOS. ALEXANDRIEI, NR.327. Iar o coloană conținea liste de
forma Client,Furnizor.
Rezultatul: 1.853 de rânduri rupte din 21.421 în fișierul de linii.
În fișierul de parteneri, doar 4 rânduri din 4.222 erau întregi.
Am reconstruit rândurile programatic, cu ancore pe coloanele cu tipar
fix, și am verificat că după reparare toate valorile cad pe pozițiile
corecte. A funcționat. Dar am aruncat rezultatul.
Motivul e simplu: modificasem peste 2.000 de rânduri. Orice concluzie
ar fi purtat corectura mea ca posibilă sursă de eroare. Am folosit
fișierul reparat doar ca să văd în ce direcție să sap, și am cerut un
export nou, cu delimitator tab.
Lecția: dacă ai reparat datele, nu ai voie să
raportezi cifre din ele. Repari ca să înțelegi, ceri din nou ca să
demonstrezi.
Ca notă tehnică: delimitatorul nu e niciodată soluția. Al doilea
export, cu tab, avea și el rânduri rupte, pentru că unele câmpuri text
conțineau caractere de linie nouă. Singura soluție corectă e
ghilimelele, sau un format care nu are delimitatori deloc.
Metoda: chei stabile, nu
etichete
Comparația s-a făcut pe cheile ERP: identificatorul documentului
pentru facturi, identificatorul intern al liniei pentru linii.
Nu pe codul de produs, nu pe numele clientului. Astea sunt etichete.
Se pot schimba, pot avea spații la coadă, pot pierde zerourile din față
la import. Am văzut toate cele trei situații în același set de date.
Toată perioada, fără eșantionare. Și o dată de tăiere fixă, ca
cifrele să nu se miște la fiecare reîmprospătare a sursei.
Un detaliu care m-a costat o oră: dacă filtrezi ambele părți la
aceeași dată și apoi le compari, fabrici singur diferențe la marginea
intervalului. Două facturi apăreau ca lipsă doar pentru că fuseseră
redatate în ERP după export. Corect e să filtrezi populația, dar să
cauți perechea în setul întreg al celeilalte părți.
Prima descoperire:
valorile erau perfecte
Peste 20.000 de linii existau în ambele sisteme. Pe toate, cantitatea
și prețul erau identice. Diferența totală: zero.
Asta a schimbat complet natura discuției.
Nu era o transformare greșită. Nu era o mapare greșită. Nu era o
problemă de rotunjire sau de conversie. Logica lor de calcul funcționa
perfect pe fiecare înregistrare pe care o aveau corect.
Când eroarea e strict de tip „rânduri în plus” și niciodată de tip
„valori diferite”, ai eliminat dintr-o mișcare jumătate din ipoteze.
Rămâne o singură categorie de cauze: ceva se adaugă și nu se scoate
niciodată.
A doua
descoperire: rânduri care nu mai există
657 de linii de factură existau în depozit și nu existau în ERP.
Peste 3,6 milioane de lei.
În sens invers, nimic. Nicio linie din sursă nu lipsea.
Le-am împărțit în patru situații distincte, și abia atunci s-a văzut
mecanismul:
- 268 de linii orfane — factura lor nu mai există
nici în ERP, nici măcar în propriul lor tabel de facturi. Documentul a
fost șters complet; au rămas doar liniile, fără antet. - 74 de linii pe facturi care există la ei, dar nu
mai există în ERP. Documentul a fost șters din sursă, ei au păstrat și
antetul, și liniile. - 184 de linii pe facturi care există în ambele
sisteme, dar unde ERP-ul nu are nicio linie cu acel produs, cantitate și
preț. - 131 de linii pe facturi care există în ambele, unde
ERP-ul are exact aceeași linie — același produs, aceeași cantitate,
același preț — dar sub alt identificator.
Ultima categorie e cea care explică totul.
Exemplul care a lămurit
mecanismul
O factură din martie, un produs, 150 de bucăți la 335,94 lei.
În depozitul lor există două linii: una creată pe 10 martie la 12:14,
alta pe 11 martie la 11:57. Valori identice, identificatori
diferiți.
În ERP există una singură: cea din 11 martie.
Ce s-a întâmplat e banal. A doua zi, cineva a corectat factura.
ERP-ul a șters linia veche și a scris una nouă, cu identificator nou.
Sursa a rămas corectă. Depozitul a păstrat ambele.
Factura aia raportează 300 de bucăți în loc de 150.
Și nu e un duplicat. Cei doi identificatori sunt diferiți, fiecare a
existat cu adevărat. Dacă cineva caută rânduri duplicate, nu găsește
niciunul.
Cauza: API-urile nu
raportează ștergeri
Aici e miezul, și e valabil pentru orice integrare, nu doar pentru
cazul ăsta.
API-ul sursei se poate filtra după data modificării. Ceri tot ce s-a
schimbat de ieri, primești tot ce s-a schimbat de ieri.
Dar când ștergi un document, el nu apare în lista de modificări. Pur
și simplu nu mai apare deloc. Nu există niciun mesaj care să spună „am
dispărut”, pentru că nu mai există nimic care să-l trimită.
O încărcare care doar adaugă și actualizează nu are cum să elimine
ceva ce nu i se mai menționează niciodată. Rândul rămâne acolo pentru
totdeauna.
Iar corecțiile de facturi produc același efect fără să fie ștergeri
propriu-zise: linia veche dispare, una nouă apare cu alt identificator,
și în depozit ajung amândouă.
Detaliul contraintuitiv
Perioada veche era perfectă.
Trei ani de istoric, sute de mii de rânduri, potrivire exactă cu
sursa. Zero diferențe.
Toate cele 657 de linii fantomă erau în ultimele zece luni. Adică
exact perioada pe care o actualizau zilnic.
Istoricul intrase printr-o încărcare completă unică — o poză a sursei
la un moment dat, deja curățată de tot ce fusese șters înainte. Perioada
„întreținută” era singura stricată, și se strica puțin în fiecare
zi.
Ce nu atingi rămâne corect. Ce actualizezi greșit se degradează
continuu.
De ce nu se vedea în
rapoarte
Modelul lor făcea un inner join între linii și facturi.
Cele 268 de linii orfane, ale căror facturi nu mai existau, erau
tăiate de join și nu apăreau niciodată în raport. Depozitul era murdar,
raportul arăta curat.
În schimb, liniile fantomă atașate la facturi care încă există
treceau prin join și umflau valorile. Alea se vedeau, dar arătau ca o
discrepanță inexplicabilă între două surse, nu ca o eroare de
încărcare.
Jumătate din problemă era ascunsă, cealaltă jumătate arăta ca o
dispută între două sisteme. De aceea a durat luni.
Controlul care rezolvă totul
Există o verificare care ar fi semnalat problema din prima zi, nu are
nevoie de a doua sursă, și durează două secunde:
Suma liniilor unei facturi trebuie să dea valoarea din
antetul facturii.
Fără filtru pe conturi, fără excepții. Toleranță de un leu,
indiferent de mărimea facturii.
În ERP, controlul trece pe 100% din facturi, cu abatere maximă de 3
bani din rotunjire. În depozit, 117 facturi picau. Una avea 71.156 lei
în antet și 332.590 lei pe linii.
Al doilea control, la fel de simplu: fiecare linie trebuie să
aibă o factură. Orice identificator de document care nu există
în tabelul de facturi e o linie orfană.
Amândouă se pot implementa ca un view care rulează după fiecare
încărcare. Nu depind de sursă, nu depind de nimeni din exterior, și
funcționează identic pentru orice altă țară sau sistem care alimentează
același model.
Cum se repară încărcarea
Ștergerile nu pot fi detectate. Deci trebuie presupuse.
Concret, pentru facturi și linii de factură: fereastra de încărcare
se definește după data documentului, nu după data
modificării. Se șterge tot intervalul din destinație, se citește același
interval din API filtrat pe data documentului, se inserează.
Trei ferestre acoperă tot:
- 3 luni, zilnic — majoritatea corecțiilor se fac în
primele zile după emitere - 6 luni, lunar — stornările și facturile refăcute
ajung mai departe în urmă - anul financiar anterior, trimestrial, până la depunerea
bilanțului — după termenul legal anul e închis și poate ieși
definitiv din ciclu
Pentru tabelele de nomenclator, produse și parteneri, situația e
alta. Sunt liste mici, sub 10.000 de rânduri, iar înregistrările se
șterg rar. Acolo încărcarea incrementală e acceptabilă, cu o verificare
de duplicate pe cheie după fiecare rulare, plus o reîncărcare completă
lunar sau trimestrial. La volumul ăsta costă aproape nimic.
Ce aș reține din tot cazul
Compară pe chei, nu pe etichete. Un cod de produs se
poate schimba. Un identificator intern, nu.
Verifică integritatea fișierului înainte să tragi concluzii
din el. Am pierdut o zi, dar era mai bine decât să trimit cifre
calculate pe rânduri rupte.
Dacă ai reparat datele, cere-le din nou. Repari ca
să înțelegi, ceri ca să demonstrezi.
Când eroarea e strict unidirecțională, ai aflat jumătate din
răspuns. Valorile corecte plus rânduri în plus înseamnă
întotdeauna că ceva nu se șterge.
Controlul intern bate comparația între surse. Un
raport care se verifică singur nu are nevoie ca cineva din altă țară să
observe că nu se potrivesc cifrele.
Și, cel mai important: absența unui control de integritate nu se vede
niciodată. Rapoartele se generează, cifrele arată plauzibil, ședințele
decurg normal. Diferența apare abia când cineva compară cu sursa — și de
obicei nu are cine.
Dacă ai un ERP care alimentează un raport de grup și nimeni nu
verifică dacă facturile se potrivesc cu ele însele, verificarea din
articol durează două minute. Dacă vrei o părere pe cazul tău,
scrie-mi.