# Plan etapa 2 - convertorul reutilizabil SAGA -> xlsx initializare ROA Stare: **v3, deciziile luate, lansarea lane-urilor neaprobata inca**. Versiunea v2 a trecut prin review de strategie si de arhitectura (`docs/review_ceo_etapa2.md`, `docs/review_eng_etapa2.md`); ce urmeaza include corectiile. Premise: importurile FUNDATIA + MASTER au trecut in ROACONT, scripturile de coduri fiscale au fost executate (confirmat 21.09.2026). Regulile din `decizii_import.md` sunt validate in productie si **nu se redeschid**. Cercetarea pe care se sprijina planul: - `docs/raport_sursa_saga_xlsx.md` - SAGA veche (VFP), conventiile tabelei `conturi_roa.dbf`; - `docs/raport_exporturi_saga_noua.md` - SAGA noua (Firebird), exporturile xlsx si baza `.FDB`. ## Ce ramane in scop si ce nu IN scop: **initializarea soldurilor la o luna**, adica fisierul pe care il inghite `frm_initializare_facturi_balanta` din ROACONT. IN AFARA scopului (asta faceau programele `saga2roa*` vechi, noi nu): importul registrului jurnal, generarea balantelor de verificare lunare, generarea nomenclatorului de parteneri la fiecare luna. ## Corectia cea mai importanta fata de v2: cine decide conturile cu parteneri v2 spunea ca `CONFIG_CONT_IREG` din Oracle e sursa de adevar. **Este gresit si ar fi produs o regresie tacuta.** Dovada: - `genereaza_xlsx.py:46-50` trateaza ca parteneri `5121`, `5124` (banca, cu analitic) si `5311` (casa, fara analitic); - in `docs/config_cont_ireg.md:89` (lista `CU_INREGISTRARI = 1`) **niciunul dintre ele nu apare**; - invers, Oracle listeaza `404, 408, 409, 4091, 4093, 4094, 418, 419, 471, 472, 456, 457...` pe care codul validat **nu** le trateaza ca parteneri. Daca unealta ar fi decis din Oracle, `5121.01 PIRAEUS BANK` de la MASTER - cazul din `decizii_import.md` pct. 15, cu sold creditor pe cont de trezorerie - ar fi devenit cont obisnuit, fara rand `FACTURA`, fara ca nimic sa dea eroare. **Regula, de acum:** setul operational este cel validat in productie, tinut intr-un fisier in repo, `config/conturi_parteneri.csv`, cu o coloana de categorie (`BANCA` / `SINTETIC` / `FARA_ANALITICE`) si una de motiv. `CONFIG_CONT_IREG` ramane **consultativ**: un script mic il exporta alaturi, iar unealta **raporteaza divergentele** la fiecare rulare (cont in Oracle si nu la noi, sau invers) - le raporteaza, nu le aplica. Divergenta constatata azi se scrie in `docs/decizii_import.md`, ca sa nu fie redescoperita. ## Arhitectura: un contract intern, trei cititoare ``` PDF balanta --\ xlsx/xls SAGA --+--> [cititor] --> contractul intern --> mapare --> init___.xlsx CONT_BAZA.FDB --/ ``` ### Contractul intern - fixat aici, nu lasat la latitudinea cititoarelor Trei fisiere, nume si coloane exacte. Potrivirea numelor de coloana la citire este **exacta si case-insensitive**, niciodata pe substring (`TOTAL_DEB` vs `TOTAL_DEB_1` se confunda usor). | fisier | coloane | scris de | |---|---|---| | `balanta_.csv` | `pagina, cont, denumire, prec_d, prec_c, rulaj_d, rulaj_c, total_d, total_c, sold_d, sold_c, este_total, tip` | toate cele 3 cititoare | | `parteneri_.csv` | `cont_analitic, cod, denumire, cod_fiscal` | xlsx (fara `cod_fiscal`), FDB (complet) | | `facturi_.csv` | `cont_analitic, numar, data, scadent, sold` | xlsx (fara `scadent`), FDB (complet) | Coloanele pe care o sursa nu le poate da raman **goale**, niciodata inventate. `tip` (A/P) exista doar pe drumurile xlsx si FDB; consumatorul ei e stabilirea laturii soldului, in locul deducerii dupa clasa contului. Ultimele doua fisiere lipsesc cu totul cand sursa e doar balanta - atunci se cade pe comportamentul de azi (un `FACTURA` per partener, sold total). ### Cititorul 1 - PDF (exista, nemodificat) `extract_balanta.py`, pdfplumber pe coordonate. Singura sursa care merge pe orice versiune SAGA, fiind raportul tiparit. Nu se atinge: e validat pe FUNDATIA si MASTER. ### Cititorul 2 - foaie de calcul SAGA (`.xls` sau `.xlsx`), nou `.xlsx` cu openpyxl, `.xls` cu `xlrd` (`openpyxl` nu citeste BIFF). Acelasi cititor pentru SAGA veche si noua: au aceleasi 35 de coloane, in aceeasi ordine. Maparea: `DEB_PREC/CRED_PREC -> prec_d/prec_c`, `RULAJ_D/RULAJ_C -> rulaj_d/rulaj_c`, `TOTAL_DEB/TOTAL_CRED -> total_d/total_c`, `FIN_D/FIN_C -> sold_d/sold_c`, `TIP -> tip`. `pagina` si `este_total` raman goale (exportul nu are rand de totaluri). Foaia: prima foaie a registrului - numele difera (`balanta` la VFP, `xl` la `.xls` nou, `Sheet1` la `.xlsx`), deci nu se cauta dupa nume. `CONT` se citeste ca **text**: `401.00002` citit ca numar se strica. ### Cititorul 3 - Firebird, nou Parametru: calea catre o **copie** de `CONT_BAZA.FDB`. Niciodata baza vie a clientului. Conexiune embedded (pe TCP 3060 parola implicita nu merge), `access_mode=READ`, `no_gc=True`, doar SELECT, `rollback` la final. Charset `WIN1250`. **Algoritmul balantei, specificat aici pentru ca e inima corectitudinii.** Soldurile stocate in `CONTURI` sunt 0 (`raport_exporturi_saga_noua.md` C.4); cifrele vin din agregarea `REGISTRU`. Sunt necesare **trei intervale de data**, nu unul - exportul are patru marimi distincte: | marime | interval | |---|---| | `DEB_INIT/CRED_INIT` | tot ce e anterior inceputului anului fiscal | | `prec_d/prec_c` | de la inceputul anului pana la inceputul lunii cerute | | `rulaj_d/rulaj_c` | in luna ceruta | | `total_*` | `prec + rulaj` ; `sold_d - sold_c = total_d - total_c` | **Rollup la sintetic**: `REGISTRU.CONT_D`/`CONT_C` contin analiticul (`401.00002`), iar exportul SAGA are sinteticul (`401`) - `raport_exporturi_saga_noua.md` C.4. Agregarea grupeaza pe partea din stanga punctului si emite si randul analitic, si sinteticul. Randul `%` din `CONTURI` este cont colector si se exclude. Mai produce: `parteneri_.csv` din `FURNIZORI` + `CLIENTI` (**codul fiscal real vine de aici**, deci pe acest drum etapa 5b - cautarea pe ANAF - dispare) si `facturi_.csv` din `INTRD`/`INTRARI` si `IESIRI`/`IES_DET`, toate deodata, nu per partener. **Garzi pe NULL, nu presupuneri**: `INTRD.SCADENT` a fost NULL pe singurul rand existent, iar `FURNIZORI.COD_FISCAL` era gol la unul din doi. Deci "scadenta reala" si "cod fiscal real" inseamna *cand exista*: scadenta lipsa cade pe ultima zi a lunii (ca azi), codul fiscal lipsa cade pe codul provizoriu `.` si partenerul intra in lista pentru cautarea ANAF. ## Poarta de verificare a cititoarelor **Cititorul 3 (FDB) si cititorul 2 (xlsx) trebuie sa produca acelasi `balanta_.csv`** pentru firma de joaca `D:\SAGA250909\0001\CONT_BAZA.FDB`, luna 09/2026, al carei export este in `exemple\`. Daca difera, e bug de cititor, nu date gresite. Acesta inchide lane-ul `citire-fdb`, nu "ruleaza fara eroare". **Limitele acestei porti, scrise si in raport, nu ascunse:** firma de joaca are 10 conturi, **toate sintetice, niciun analitic**, 2 furnizori, 0 clienti, 1 factura. Deci poarta dovedeste rollup-ul si periodizarea pe cazul sintetic si **nimic despre analitice** - exact partea grea. Pana la o baza reala, cititorul FDB este **verificat partial** si se marcheaza ca atare in ajutorul uneltei; nu se foloseste la un client fara rerularea portii pe datele lui. **Inlocuitorul verificarii "Totaluri:"**. Pe drumul PDF, poarta era randul "Totaluri:" din raport. Exportul xlsx si baza nu au asa ceva. Nu se sterge verificarea, se inlocuieste cu una reala: `SUM(REGISTRU)` pe fiecare latura, la data ceruta, = netul balantei generate. Pe drumul xlsx, unde `REGISTRU` nu e disponibil, ramane `SUM(sold_d) = SUM(sold_c)` pe frunze plus relatiile `total = prec + rulaj` si `sold_d - sold_c = total_d - total_c` pe fiecare cont (au trecut 341/341 pe DANUBE si 10/10 pe firma de joaca). ## Pasul de mapare Fiecare firma are propriile analitice, deci fiecare firma are propriul fisier. Implicit merge pe propunerile automate; corectiile sunt posibile oricand, fara sa fie obligatorii. **Doua fisiere, cu proprietari diferiti** - asa regenerarea nu-ti poate distruge corectiile: | fisier | cine scrie | ce contine | |---|---|---| | `mapare_.xlsx` | **doar unealta**, regenerat complet la fiecare rulare | toate conturile din balanta, cu propunerea automata: `cont_saga`, `denumire`, `sold`, `cont`, `acont`, `sursa` (`AUTO` sau `CORECTAT`) | | `corectii_.xlsx` | **doar tu** | numai randurile pe care le schimbi: `cont_saga`, `cont`, `acont`, `exclus`, `motiv` | La rulare: se citeste balanta, se propune automat, se suprapun corectiile, se scrie maparea completa ca sa vezi rezultatul final. Fisierul de corectii se creeaza gol, cu antet si un exemplu comentat, doar la prima rulare a firmei - pentru o firma simpla nu-l deschizi niciodata. Propunerea automata, cu regula de azi din `decizii_import.md`: `cont` = primele max 4 caractere, `acont` = cifrele analiticului concatenate, max 4. **Conventii preluate din `conturi_roa.dbf`** (`raport_sursa_saga_xlsx.md` 2.3), ca sa nu ai sute de randuri de corectat - se scriu in `corectii_.xlsx`: - `.TOATE` - toate analiticele sinteticului merg pe acelasi `cont`/`acont` ROA; - `.RESTUL` - la fel, dar doar pentru analiticele fara rand propriu. Ordinea de cautare: potrivire exacta -> `.RESTUL` -> `.TOATE`. `PLANCONT` **nu** se preia: era fallback pentru planul de conturi, noi nu generam plan de conturi. Maparea acopera si **redenumirea sinteticului** (`409 -> 4091`, `431 -> 4311`), nu doar analiticul - era jumatate din ce facea `conturi_roa.dbf` si lipsea din v2. **Ce NU se configureaza aici**: care conturi merg pe parteneri (vezi sectiunea de mai sus - fisierul `config/conturi_parteneri.csv`). ## Cine detine forma lui `acont` (corectie fata de v2) v2 spunea ca `genereaza` ia `cont`/`acont` din mapare in loc sa le calculeze. **Prea larg**, si ar fi dublat o regula deja validata. Doua lucruri nu pot veni din mapare: - **randurile de diferenta** nu exista in balanta, sunt calculate (`genereaza_xlsx.py:204-212`, `:243-249`) - raman in cod; - **forma lui `acont` pentru conturile cu parteneri** depinde de categorie: `401` cere `acont` gol pe ambele randuri, `5121` cere `acont` completat identic pe BALANTA si FACTURA (`decizii_import.md` pct. 16). O regula generica de pre-completare ar pune `acont` pe `401` -> chei diferite -> **dublare tacuta a sumei**, exact capcana din pct. 16. **Regula:** `genereaza_xlsx.py` ramane singurul care decide forma pentru conturile cu parteneri; maparea doar suprascrie ce ii dai explicit. Restul fisierului - randuri BALANTA / FACTURA, sume negative pe latura opusa, verificarile din etapa 5 - ramane neatins. Opreste-te cu eroare, nu cu ghicit, daca: `cont` sau `acont` depaseste 4 caractere; doua conturi SAGA diferite produc acelasi `(cont, acont)` **fara** sa fie acoperite de un `.TOATE`/`.RESTUL` comun (colapsarea intentionata e legala, coliziunea accidentala nu); un rand din `corectii` nu corespunde niciunui cont din balanta (typo tacut). ## Testare `unittest` din stdlib, fara dependinte noi. Proiectul nu are azi niciun test; fiecare lane lasa in urma cel putin unul, altfel lane-ul nu e gata: | lane | testul pe care il lasa | |---|---| | `citire-xlsx` | citeste `exemple\saga-sqlite-balanta-09-2026.xlsx` si `saga2roa_danube\balanta.xls`; relatiile de sume trec pe toate randurile; `CONT` ramane text | | `mapare` | o corectie supravietuieste regenerarii; `.TOATE`/`.RESTUL` se aplica in ordinea ceruta; un `cont_saga` inexistent in corectii da eroare | | `citire-fdb` | CSV-ul din FDB identic cu cel din xlsx pe firma de joaca; `SCADENT` NULL cade pe ultima zi a lunii | | `integrare` | rulare capat-la-capat pe firma de joaca, din ambele surse | | `regresie` | FUNDATIA + MASTER reproduc etalonul | ## Poarta de regresie si etalonul `tests/golden/` primeste copii **anonimizate** ale celor doua xlsx importate in productie: sumele, conturile, structura randurilor si numarul lor raman **neatinse**; denumirile de parteneri devin `PARTENER 001...` si codurile fiscale `CF000001...`, stabil (acelasi nume real -> acelasi nume fals, ca relatiile dintre randuri sa se pastreze). Asa poarta compara orice celula si in git nu intra numele niciunui client. Scriptul de anonimizare se comite; **tabela de corespondenta nu**. Comparatia se face pe **valori de celula**, nu pe hash de fisier: un xlsx rescris de alta versiune de openpyxl are alti octeti cu acelasi continut. Regresia ruleaza **dupa fiecare lane care atinge generatorul**, nu doar la final - e cel mai ieftin test care protejeaza zona deja validata in productie. ## Git `.gitignore` ignora `init_*.xlsx`, `export_*.xlsx`, `mapare_*.xlsx`, `balanta_*.csv`. Se adauga: `*.fdb`, `*.FDB` (copii de baze de client, zeci de MB), `parteneri_*.csv`, `facturi_*.csv`, `corectii_*.xlsx`. Exceptii explicite: sablonul si `tests/golden/`. PDF-urile sursa raman in git: sunt datele pe care ruleaza regresia. `exemple\` ramane in git - firma de joaca, nu date de client. Dependintele noi (`xlrd`, `firebird-driver`) se trec in `CLAUDE.md`, la `Mediu`. ## Ordinea de lucru (lane-uri opencode) | lane | livrabil | criteriu de terminare | depinde de | |---|---|---|---| | `contract` | `config/conturi_parteneri.csv` + scriptul de divergenta Oracle; anonimizatorul + `tests/golden/` | regresia ruleaza si trece pe etalonul anonimizat, cu codul de azi neschimbat | - | | `citire-xlsx` | cititorul de foaie de calcul SAGA | vezi tabelul de testare | contract | | `mapare` | propunerea automata + `corectii_.xlsx` + `.TOATE`/`.RESTUL` + redenumirea sinteticului | vezi tabelul de testare | contract | | `citire-fdb` | balanta + parteneri + facturi din Firebird | CSV identic cu drumul xlsx pe firma de joaca | citire-xlsx | | `integrare` | `extrage.py` / `genereaza.py` cu parametri | rulare capat-la-capat din ambele surse | toate | | `regresie` | rerularea pe FUNDATIA + MASTER | identice cu etalonul, pe valori de celula | integrare | Lane-ul `contract` merge primul si singur: fixeaza etalonul **inainte** ca vreo linie de cod sa se schimbe, altfel regresia masoara fata de un rezultat deja mutat. Poarta finala: `regresie`. Daca noul flux nu reproduce randurile celor doua xlsx-uri validate in productie, nu e gata - indiferent cat de curat e codul. ## Ce ramane pentru mai tarziu - **o baza `.FDB` reala + exportul ei de balanta**, ca sa se reruleze poarta pe date cu analitice si cu volum. Pana atunci cititorul FDB e verificat partial (vezi mai sus); - procedura pentru client: cum isi face copia bazei (`gbak` da o copie consistenta si mai mica decat fisierul brut) si ca trebuie sa opreasca SAGA daca trimite fisierul direct. Se scrie odata cu lane-ul `citire-fdb`, altfel livram un cititor care nu se poate folosi in teren; - conectarea la un Firebird prin retea - azi nu merge cu parola implicita (`raport_exporturi_saga_noua.md` C.1), deci cere credentiale de la client.