Files
solduri2roa/docs/plan_etapa2_convertor.md
Marius Mutu 15bb26ac15 Etapa 2: plan v3, rapoarte de cercetare si etalonul de regresie
Cercetare: ambele formate SAGA (VFP xls/xlsx si Firebird .FDB), cu dovezi
fisier:linie in docs/raport_sursa_saga_xlsx.md si raport_exporturi_saga_noua.md.

Plan v3 dupa review de strategie si arhitectura: contract intern + trei
cititoare, mapare in doua fisiere cu proprietari diferiti, lane-uri.

Etalon de regresie anonimizat in tests/golden/ (sume si structura neatinse,
zero IBAN si zero cod fiscal real). Tabela de corespondenta ramane ignorata.

Iesirile de productie ies din git (raman pe disc); .gitignore acopera si
copiile de baze de client si iesirile intermediare ale convertorului.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYeAtVxeS8m4oXekjX8Am2
2026-09-21 15:44:57 +03:00

249 lines
14 KiB
Markdown

# 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_<FIRMA>_<an>_<luna>.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_<FIRMA>.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_<FIRMA>.csv` | `cont_analitic, cod, denumire, cod_fiscal` | xlsx (fara `cod_fiscal`), FDB (complet) |
| `facturi_<FIRMA>.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_<FIRMA>.csv` din `FURNIZORI` + `CLIENTI` (**codul fiscal real vine de
aici**, deci pe acest drum etapa 5b - cautarea pe ANAF - dispare) si `facturi_<FIRMA>.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 `<cont>.<analitic>` 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_<FIRMA>.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_<FIRMA>.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_<FIRMA>.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_<FIRMA>.xlsx`:
- `<sintetic>.TOATE` - toate analiticele sinteticului merg pe acelasi `cont`/`acont` ROA;
- `<sintetic>.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_<FIRMA>.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.