Lane regresie: poarta finala trece, etapa 2 e gata

tests/test_regresie_capat_la_capat.py ruleaza extrage.py si genereaza.py ca
subprocese, pornind de la PDF-urile originale, si compara rezultatul anonimizat cu
tests/golden/ pe valori de celula: 0 diferente pe ambele firme. Fluxul nou
reproduce exact xlsx-urile deja importate in ROACONT.

33 de teste trec, fara niciun skip.

CLAUDE.md: dependintele noi (xlrd, firebird-driver) si punctele de intrare.
Handoff-ul rescris pe starea de final, cu ce NU e dovedit: cititorul FDB e
verificat doar pe cazul sintetic al firmei de joaca si nu se duce la un client
fara rerularea portii pe datele lui.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYeAtVxeS8m4oXekjX8Am2
This commit is contained in:
2026-09-21 16:29:29 +03:00
parent 82511c8adb
commit 0d0b3e26fe
5 changed files with 404 additions and 139 deletions

View File

@@ -3,14 +3,21 @@
Convertor balante SAGA (PDF) -> xlsx de initializare solduri ROACONT.
## Mediu
- `py` (Python 3.13). Instalate: pdfplumber, openpyxl. Fara alte dependinte.
- `py` (Python 3.13). Instalate: pdfplumber, openpyxl, xlrd (.xls BIFF), firebird-driver
(Firebird embedded). Fara alte dependinte; testele folosesc `unittest` din stdlib.
- Nu folosi `sed -i` (corupe fisierele). Scrie cu heredoc sau Python.
- `D:\ROA\ROACONT` = read-only. Nu comite (git/svn) fara aprobare.
## Fisiere
| | |
|---|---|
| `extrage.py` | **punctul de intrare**: sursa (pdf/xlsx/fdb) -> contractul intern |
| `genereaza.py` | **punctul de intrare**: contractul intern -> xlsx de import |
| `extract_balanta.py` | PDF -> `balanta_<FIRMA>.csv` (pdfplumber pe coordonate) |
| `citire_xlsx.py` | foaie SAGA .xls/.xlsx -> acelasi contract |
| `citire_fdb.py` | CONT_BAZA.FDB -> balanta + parteneri + facturi (verificat doar pe sintetic) |
| `mapare.py` | propunere automata + `corectii_<FIRMA>.xlsx`, .TOATE/.RESTUL |
| `config/conturi_parteneri.csv` | **conturile cu parteneri** - autoritatea, nu CONFIG_CONT_IREG |
| `genereaza_xlsx.py` | CSV -> `init_<FIRMA>_<an>_<luna>.xlsx` |
| `init_facturi_balanta_note.xlsx` | sablonul ROA (rand 1 = coloane, 2-13 instructiuni, 20+ exemple) |
| `docs/decizii_import.md` | **regulile de mapare - citeste intai** |

9
bash.exe.stackdump Normal file
View File

@@ -0,0 +1,9 @@
Stack trace:
Frame Function Args
000FFFFAFF0 0018006286E (0018028EC28, 0018026DE3E, 00000000000, 000FFFF9EF0)
000FFFFAFF0 0018004846A (00000000000, 00000000000, 00000000000, 001004E2F5F)
000FFFFAFF0 001800484A2 (0018028ECD9, 000FFFFAEA8, 00000000000, 00000000000)
000FFFFAFF0 001800CC6BE (00000000000, 00000000000, 00000000000, 00000000000)
000FFFFAFF0 001800CC7E5 (000FFFFB000, 00000000000, 00000000000, 00000000000)
00000000000 001800CDDA5 (000FFFFB000, 00000000000, 00000000000, 00000000000)
End of stack trace

View File

@@ -1,167 +1,98 @@
# Handoff - etapa 2, convertorul reutilizabil SAGA -> ROA
Scris 21.09.2026. Sesiunea urmatoare este **orchestrator**, nu executant: imparte pe lane-uri,
trimite sarcinile la agenti opencode, verifica portile. Nu scrie ea codul.
Actualizat 21.09.2026, la finalul executiei. Versiunea anterioara a acestui fisier descria starea
**inainte** de lansarea lane-urilor; toate lane-urile au rulat intre timp.
**Stare: planul e gata si aprobat pe decizii; nu s-a scris nicio linie de cod pentru etapa 2.**
Marius nu a dat inca "porneste" pe lane-uri. Intreaba-l inainte sa lansezi primul lane.
**Stare: etapa 2 este terminata.** Toate cele sase lane-uri din plan au trecut portile lor,
33 de teste trec fara niciun skip, totul e comis si impins pe `origin/main`.
---
## 1. Ce sa citesti, in ordine, si de ce
## 1. Ce exista acum
| fisier | de ce |
| fisier | ce face |
|---|---|
| `docs/plan_etapa2_convertor.md` | **contractul de lucru, v3.** Tot ce urmeaza se masoara fata de el |
| `docs/raport_sursa_saga_xlsx.md` | SAGA veche (VFP); conventiile `conturi_roa.dbf` (`.TOATE`, `.RESTUL`) |
| `docs/raport_exporturi_saga_noua.md` | SAGA noua (Firebird); structura exporturilor si a bazei |
| `docs/decizii_import.md` | regulile de mapare, **validate in productie - nu se redeschid** |
| `docs/review_ceo_etapa2.md`, `docs/review_eng_etapa2.md` | de unde vin corectiile din v3; reciteste doar daca vrei sa redeschizi o decizie |
| `extrage.py` | punctul de intrare: `--sursa pdf/xlsx/fdb` (dedus din extensie) -> contractul intern |
| `genereaza.py` | punctul de intrare: contractul intern + mapare -> `init_<FIRMA>_<an>_<luna>.xlsx` |
| `extract_balanta.py` | cititorul PDF, **nemodificat** (era validat in productie) |
| `citire_xlsx.py` | cititorul de foaie SAGA `.xls` (xlrd) si `.xlsx` (openpyxl) |
| `citire_fdb.py` | cititorul Firebird: balanta, parteneri, facturi |
| `mapare.py` | propunere automata, `corectii_<FIRMA>.xlsx`, `.TOATE`/`.RESTUL`, redenumire sintetic |
| `genereaza_xlsx.py` | biblioteca de generare, acum parametrizata pe an/luna |
| `config/conturi_parteneri.csv` | conturile cu parteneri - **autoritatea** |
| `tools/divergenta_parteneri.py` | raporteaza divergenta fata de `CONFIG_CONT_IREG`, nu o aplica |
| `tests/` | 33 de teste, `unittest` din stdlib |
| `docs/procedura_copie_fdb_client.md` | cum isi face clientul copia bazei |
| `docs/raport_lane_*.md` | raportul fiecarui lane |
Nu reface cercetarea. Cele doua rapoarte au costat doua lane-uri si contin dovezi cu fisier si linie.
Commit-uri: `15bb26a` (plan + etalon), `f23b336` (contract), `ba4cbbf` (citire-xlsx + mapare),
`6b37d37` (citire-fdb), `82511c8` (integrare), plus commit-ul final de regresie.
## 2. Ce e gata
## 2. Portile care au trecut, si ce dovedesc
- Etapele 1-6 din `docs/plan_solduri2roa.md`: cele doua xlsx au fost **importate in ROACONT si au
trecut**; scripturile `sql/update_cod_fiscal_*.sql` au fost **executate** de Marius.
- Cercetarea pe ambele formate SAGA + conectarea la Firebird, verificata live.
- Planul v3, dupa review de strategie si de arhitectura.
- `.gitignore` + `git rm --cached` pe iesirile de productie (fisierele raman pe disc).
1. **Regresia pe etalon** (`tests/test_regresie.py`): biblioteca reproduce celula cu celula cele
doua xlsx-uri importate in ROACONT. Verificat ca poarta e reala: scoaterea lui `5311` din
config o face sa pice.
2. **Poarta cititoarelor** (`tests/test_citire_fdb.py`): CSV-ul din `CONT_BAZA.FDB` e identic **la
octet** cu cel din exportul xlsx, pe firma de joaca, 09/2026. Plus `SUM(REGISTRU)` pe fiecare
latura = netul balantei.
3. **Poarta finala** (`tests/test_regresie_capat_la_capat.py`): `extrage.py` + `genereaza.py`
rulate ca **subprocese**, pornind de la PDF-urile originale, reproduc etalonul cu **0 diferente**.
- **Etalonul de regresie este facut si pus in git** (21.09.2026): `tests/golden/` cu
`golden_FUNDATIA_2025_12.xlsx` si `golden_MASTER_2025_12.xlsx`, produse de
`tests/anonimizeaza_etalon.py`. Verificat: structura, numarul de randuri si sumele sunt
identice cu originalele (`SUM(totdeb) = SUM(totcred)` neatins), au fost schimbate doar
coloanele 10-14 pe randurile FACTURA plus IBAN-urile; zero IBAN si zero cod fiscal real
ramase. Tabela de corespondenta (`tests/_corespondenta_anonimizare.json`) e ignorata de git -
nu o comite, cu ea anonimizarea se desface.
## 3. Ce NU e dovedit - citeste inainte sa duci asta la un client
## 3. Ce NU e facut
- **Cititorul FDB e verificat doar pe cazul sintetic.** Firma de joaca are 10 conturi, toate
sintetice, niciun analitic, 2 furnizori, 0 clienti, 1 factura. Rollup-ul analitic -> sintetic si
periodizarea sunt dovedite doar acolo. Scris si in docstring-ul lui `citire_fdb.py`.
**Nu se foloseste la un client fara rerularea portii pe datele lui.**
- Conectarea la Firebird **prin retea** nu merge cu parola implicita: cere credentiale clientului.
- Drumul cu parteneri si facturi reale (un `FACTURA` per document, cod fiscal din `FURNIZORI`) a
fost exersat doar pe o singura factura a firmei de joaca.
Tot ce e in tabelul de lane-uri din plan, mai putin etalonul. Niciun fisier de cod nou pentru
conversie, niciun test, niciun `config/conturi_parteneri.csv`.
## 4. Doua defecte prinse la verificare, nu de lane-uri
## 4. Deciziile deja luate - nu le redeschide
Amandoua erau tacute - ar fi trecut testele si ar fi stricat date:
1. **Conturile cu parteneri**: setul validat din `genereaza_xlsx.py:46-50` este autoritatea, tinut
in `config/conturi_parteneri.csv`. `CONFIG_CONT_IREG` e consultativ; unealta **raporteaza**
divergentele, nu le aplica. Motivul, cu dovada, e in plan - Oracle nu listeaza 5121/5124/5311.
2. **`acont` pentru conturi cu parteneri** ramane decis de `genereaza_xlsx.py`; maparea doar
suprascrie explicit. Randurile de diferenta raman calculate in cod.
3. **Maparea = doua fisiere**: `mapare_<FIRMA>.xlsx` scris doar de unealta (regenerat complet),
`corectii_<FIRMA>.xlsx` scris doar de Marius.
4. **Cititorul FDB se face acum**, marcat explicit "verificat doar pe cazul sintetic".
5. **Etalonul de regresie**: xlsx-uri intregi, **anonimizate** (sume si structura neatinse), in
`tests/golden/`. Tabela de corespondenta nume real -> nume fals **nu se comite**.
6. **Teste**: `unittest` din stdlib. Fara dependinte noi de test.
7. Sursa: PDF, foaie de calcul SAGA si Firebird, toate trei, egale.
1. **`.xls`-ul SAGA nu are inregistrare CODEPAGE**, deci `xlrd` cadea pe iso-8859-1 si strica
denumirile (`FURNIZORI \x97 DEBITORI`). Reparat cu `encoding_override="cp1250"`, cu asertie in
`tests/test_citire_xlsx.py` care pica daca revine.
2. **Redenumirea unui cont cu parteneri lua primul analitic** ca sa afle contul sintetic. Cu
analitice mapate pe conturi diferite, BALANTA agregat si randurile FACTURA cadeau pe conturi
diferite. Acum ridica `Stop`; colapsarea prin `.TOATE` ramane legala.
## 5. Ordinea lane-urilor
Morala pentru sesiunea urmatoare: **raportul unui lane nu tine loc de verificare.** Ambele lane-uri
raportasera "GATA" corect dupa criteriile lor.
`contract` singur, primul -> apoi `citire-xlsx` si `mapare` in paralel -> `citire-fdb` ->
`integrare` -> `regresie`. Criteriile de terminare sunt in plan, in tabelul de lane-uri si in cel de
testare. Etalonul din `tests/golden/` a fost fixat **inainte** sa se schimbe vreo linie de cod -
asta e tot rostul ordinii. Daca vreun lane modifica `genereaza_xlsx.py` si regresia incepe sa pice,
etalonul are dreptate, nu codul nou.
## 5. Divergenta fata de Oracle, constatata si scrisa
## 6. Cum lansezi un lane (verificat azi, functioneaza)
`CONFIG_CONT_IREG` listeaza 27 de conturi pe care codul validat nu le trateaza ca parteneri, si
**nu** listeaza `5121`, `5124`, `5311`, pe care le trateaza. Confirma decizia din plan: Oracle e
consultativ. Detaliile in `docs/decizii_import.md`.
Ghidul complet: `D:\ROA\ROACONT\COMUN\docs\opencode_agenti_orchestrare.md`.
## 6. Ce ramane pentru mai tarziu
- **Port pentru acest proiect: 4296.** Nu-l folosi pe 4196 (ROACONT) sau 4096 (ROAFACTURARE) -
oprirea serverului dintr-un proiect omoara lane-urile celuilalt. Portul 4296 **nu e inca trecut in
tabelul din ghid** - adauga-l acolo (COMUN are nevoie de aprobare inainte de commit).
- Pornire, din directorul proiectului: `opencode serve --port 4296`
- Model: `{"providerID":"opencode-go","modelID":"deepseek-v4.1-flash"}`
- Promptul de sistem, obligatoriu la fiecare sesiune, in campul `system`:
`D:\ROA\ROACONT\COMUN\docs\opencode_prompt_initial.md`
- Sesiune noua: `POST /session {title}` -> `id`;
sarcina: `POST /session/:id/prompt_async?directory=D:\ROA\IMPORT2ROA\solduri2roa`
- Stare: `GET /session/status` - sesiunea lipseste cand e libera, `{}` = toate libere.
- Oprire la final:
`Stop-Process -Id (Get-NetTCPConnection -LocalPort 4296 -State Listen).OwningProcess`
- o baza `.FDB` **reala** + exportul ei de balanta, ca sa se reruleze poarta pe date cu analitice
si cu volum;
- portul **4296** al acestui proiect nu e inca trecut in tabelul din
`D:\ROA\ROACONT\COMUN\docs\opencode_agenti_orchestrare.md` (COMUN cere aprobare inainte de commit);
- `docs/plan_solduri2roa.md` descrie inca etapele 1-6; nu a fost rescris dupa etapa 2.
Sablon PowerShell pentru trimitere (JSON pe UTF-8, altfel se strica diacriticele si ghilimelele):
## 7. Starea pe disc
```powershell
$u='http://127.0.0.1:4296'
$sys = [IO.File]::ReadAllText('D:\ROA\ROACONT\COMUN\docs\opencode_prompt_initial.md')
$m = @{providerID='opencode-go'; modelID='deepseek-v4.1-flash'}
$sid = (Invoke-RestMethod -Method Post "$u/session" -ContentType 'application/json' -Body '{"title":"lane-X"}').id
$b = @{model=$m; system=$sys; parts=@(@{type='text';text=$task})} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Method Post "$u/session/$sid/prompt_async?directory=D:\ROA\IMPORT2ROA\solduri2roa" `
-ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($b))
```
Totul comis si impins pe `origin/main`. Nimic periculos: niciun proces ramas viu (serverul opencode
de pe 4296 a fost oprit), `D:\ROA\ROACONT`, `D:\SAGA250909` si `saga2roa*` neatinse, baza firmei de
joaca deschisa numai prin copie in temporar, `tests/_corespondenta_anonimizare.json` ramasa ignorata
de git.
Asteptarea: nu face `Start-Sleep` lung in foreground (harness-ul il blocheaza). Foloseste Bash in
fundal: `until curl -s http://127.0.0.1:4296/session/status | grep -qv busy; do sleep 20; done`.
Fisierele `init_FUNDATIA_2025_12.xlsx` si `init_MASTER_2025_12.xlsx` sunt pe disc dar nu in git;
sunt necesare doar daca vrei sa **regenerezi** etalonul (`py tests\anonimizeaza_etalon.py`).
**Ce trebuie sa contina fiecare sarcina de lane**: numele lane-ului, fisierele exacte de atins,
criteriul de terminare luat din plan, interdictiile (fara commit, fara `sed -i`, fara modificari in
`D:\ROA\ROACONT`, `D:\SAGA250909`, `saga2roa*`), unde scrie raportul, si ultimul mesaj
`GATA <lane>` / `BLOCAT <lane>: motiv`. Agentii respecta asta; doi lane-uri au mers azi asa.
## 7. Capcane deja platite - nu le redescoperi
- `openpyxl` **nu** citeste `.xls` (BIFF). Pentru `.xls`: `xlrd` (instalat, 2.0.2) sau conversie
`soffice --headless --convert-to xlsx`. Ambele verificate.
- Firebird: **numai embedded**. `firebird-driver` (instalat, 2.0.3) + `fdb.load_api` catre
`C:\Program Files\Firebird\Firebird30_Saga\fbclient.dll`, SYSDBA/masterkey, charset `WIN1250`,
`access_mode=READ`, `no_gc=True`. Pe TCP 3060 parola implicita **nu** merge.
- In `CONTURI` **soldurile stocate sunt 0**; balanta se obtine agregand `REGISTRU`. Cine citeste
`CONTURI.DEB_PREC` scoate o balanta goala fara nicio eroare.
- Coloanele `_1` din export (S..AF) seamana cu cele principale. Potrivirea numelor de coloana se
face **exact**, niciodata pe substring: `TOTAL_DEB` vs `TOTAL_DEB_1`.
- Numele foii difera intre surse (`balanta` / `xl` / `Sheet1`) - ia prima foaie, nu cauta dupa nume.
- `CONT` se citeste ca text. `401.00002` citit ca numar se strica.
- Heredoc-urile lungi in Bash au esuat pe fisierele mari de documentatie; scrie-le cu tool-ul de
scriere de fisiere.
- `python` direct din Bash e stub Windows Store si nu scrie nimic (iese 0 si fisierul ramane
neatins). Foloseste `py`, sau PowerShell.
## 8. Starea pe disc - nimic periculos, dar necomis
`git status` la predare (HEAD = `ceab089`, branch `main`):
## 8. Cum rulezi acum unealta
```
M .gitignore
D balanta_FUNDATIA.csv
D balanta_MASTERJOB.csv
M docs/parteneri_cui_gasite.md
M docs/plan_solduri2roa.md
D export_balanta_fundatia_12_2025.xlsx
D init_FUNDATIA_2025_12.xlsx
D init_MASTER_2025_12.xlsx
M sql/update_cod_fiscal_FUNDATIA.sql
M sql/update_cod_fiscal_MASTER.sql
A tests/anonimizeaza_etalon.py
A tests/golden/golden_FUNDATIA_2025_12.xlsx
A tests/golden/golden_MASTER_2025_12.xlsx
?? docs/handoff_etapa2_convertor.md
?? docs/plan_etapa2_convertor.md
?? docs/raport_exporturi_saga_noua.md
?? docs/raport_sursa_saga_xlsx.md
?? docs/review_ceo_etapa2.md
?? docs/review_eng_etapa2.md
?? exemple/
py extrage.py --fisier "IBB 31.12.2025.pdf" --firma FUNDATIA --an 2025 --luna 12
py genereaza.py --firma FUNDATIA --an 2025 --luna 12
```
Verificari inainte de orice altceva:
- `init_FUNDATIA_2025_12.xlsx` si `init_MASTER_2025_12.xlsx` sunt pe disc dar nu mai sunt in git.
Etalonul anonimizat e deja generat din ele si comis, deci poarta de regresie nu mai depinde de
ele. Sunt necesare doar daca vrei sa **regenerezi** etalonul (`py testsnonimizeaza_etalon.py`).
- Nu se da commit fara ca Marius sa vada diff-ul (regula proiectului).
- Niciun proces ramas viu: serverul opencode de pe 4296 a fost oprit la predare. Il pornesti tu,
cu comanda din sectiunea 6.
- `D:\ROA\ROACONT`, `D:\SAGA250909` si `saga2roa*` nu au fost modificate si nu se modifica.
## 9. Primul lucru de facut in sesiunea noua
1. Citeste planul v3.
2. Intreaba-l pe Marius daca pornesti (nu a dat inca "da" pe lansarea lane-urilor).
3. La "da": lanseaza **doar** lane-ul `contract`. Etalonul exista deja, deci lane-ul are de facut
`config/conturi_parteneri.csv` + scriptul de divergenta Oracle, si de scris testul de regresie
care compara iesirea codului de azi cu `tests/golden/`, pe valori de celula (nu pe hash: un
xlsx rescris de alta versiune de openpyxl are alti octeti cu acelasi continut). Testul trebuie
sa treaca **cu codul de conversie neschimbat**. Daca nu trece, e bug in test sau in etalon, nu
in cod, si restul lane-urilor nu au voie sa porneasca.
Sursa se deduce din extensie (`.pdf`, `.xls`/`.xlsx`, `.fdb`); o extensie necunoscuta da eroare.
Pentru Firebird, da calea catre o **copie** a bazei, niciodata baza vie a clientului.

View File

@@ -0,0 +1,108 @@
# Raport lane `regresie` - solduri2roa etapa 2, poarta finala
Data: 21.09.2026. Director: `D:\ROA\IMPORT2ROA\solduri2roa`. Singurul lane activ.
Intrebarea portii: fluxul nou, pornit de la PDF-urile originale, reproduce exact cele
doua xlsx-uri deja importate in ROACONT si trecute? **Da.**
## 1. Facut
- `tests/test_regresie_capat_la_capat.py` (nou, 210 linii, ASCII, LF):
- `:44-56` `_ruleaza` - punctele de intrare rulate ca **subprocese**
(`sys.executable extrage.py ...` / `genereaza.py ...`), deci parsarea argumentelor
e sub test; orice cod de iesire != 0 e esec.
- `:29-36` `FIRME` - PDF -> firma -> balanta de referinta -> etalon.
- `:70-82` `_diferente_referinta` - compara balanta extrasa din PDF cu balanta de
referinta din radacina, ca sa confirme asocierea PDF->firma.
- `:105-128` `setUpClass` - extrage fiecare PDF in temp, verifica, ruleaza
`genereaza.py`, anonimizeaza cu `tests/anonimizeaza_etalon.py` (functie importabila,
tabela comisa) in temp.
- `:148-194` `_verifica_etalon` - comparatie pe **valori de celula** cu
`tests/golden/`; mesajul de esec da fisierul, randul, coloana (numar si litera),
asteptat, gasit. `skipTest` la PDF/tabela lipsa.
- 4 teste: `test_pdf_fundatia_corespunde_referintei`, `test_pdf_master_corespunde_referintei`,
`test_etalon_fundatia`, `test_etalon_master`.
Niciun fisier de productie, niciun etalon, niciun config nu a fost modificat. Cod de
productie schimbat: **niciunul** (diferente zero, nu a fost nevoie).
## 2. Verificari
### 2.1 Asocierea PDF -> firma (dedusa, nu ghicita)
Ambele PDF-uri extrase in temp, comparate cu balantele de referinta din radacina pe
coloanele comune (`cont, denumire, prec_*, rulaj_*, total_*, sold_*, este_total`):
- `IBB 31.12.2025.pdf` vs `balanta_FUNDATIA.csv` -> **0 diferente**; vs `balanta_MASTERJOB.csv` -> 570.
- `MJC 31.12.2025.pdf` vs `balanta_MASTERJOB.csv` -> **0 diferente**; vs `balanta_FUNDATIA.csv` -> 570.
Concluzie: **IBB = FUNDATIA, MJC = MASTER**. Afirmatia e verificata si in test
(`test_pdf_*_corespunde_referintei`), nu doar in raport.
### 2.2 Comenzile rulate
```
py extrage.py --sursa pdf --fisier "IBB 31.12.2025.pdf" --firma FUNDATIA --an 2025 --luna 12 --dir <temp>
py extrage.py --sursa pdf --fisier "MJC 31.12.2025.pdf" --firma MASTER --an 2025 --luna 12 --dir <temp>
py genereaza.py --firma FUNDATIA --an 2025 --luna 12 --dir <temp>
py genereaza.py --firma MASTER --an 2025 --luna 12 --dir <temp>
py tests/anonimizeaza_etalon.py (functie importabila: anonimizeaza_fisier)
py -m unittest discover -s tests -v
```
`extrage.py`: 88 randuri pentru fiecare PDF. `genereaza.py`: FUNDATIA `rows: 75
totdeb=totcred=7068369.5`, toate 4 verificarile OK, exit 0; MASTER `rows: 80
totdeb=totcred=2318572.11`, toate 4 OK, exit 0 (un analitic negativ cunoscut:
`5121.01` -20.8 D, raportat, nu normalizat).
### 2.3 Iesirea testului
```
test_etalon_fundatia (test_regresie_capat_la_capat.TestRegresieCapatLaCapat.test_etalon_fundatia) ... ok
test_etalon_master (test_regresie_capat_la_capat.TestRegresieCapatLaCapat.test_etalon_master) ... ok
test_pdf_fundatia_corespunde_referintei (...) ... ok
test_pdf_master_corespunde_referintei (...) ... ok
...
Ran 33 tests in 6.676s
OK
EXIT=0
```
33 = 29 existente + 4 ale acestui lane. `tests/test_regresie.py` trece neschimbat.
### 2.4 Rezultatul portii
Anonimizat vs `tests/golden/`, pe valori de celula:
- `golden_FUNDATIA_2025_12.xlsx`: dimensiuni 76 x 39, **0 celule diferite**.
- `golden_MASTER_2025_12.xlsx`: dimensiuni 81 x 39, **0 celule diferite**.
Nu exista diferente de raportat (nici macar denumiri cu spatii in plus: zero celule).
### 2.5 Cens octeti / linii
`tests/test_regresie_capat_la_capat.py`: 0 octeti > 0x7F, CRLF=0 / LF=210 (identic cu
`tests/test_regresie.py`, `tests/test_integrare.py`). Toate iesirile testului au fost
scrise doar in directoare temporare.
## 3. Nefacut / blocat / intrebari deschise
- Niciun blocaj. Poarta a trecut din prima, fara ajustari de cifre.
- Observatie (nu blocant): `genereaza.py` nu are un flag care sa impuna absenta
corectiilor pre-existente; testul ruleaza in temp gol, deci `corectii_<FIRMA>.xlsx`
se creeaza gol si nu influenteaza rezultatul. Daca cineva pune corectii reale in
radacina, testul nu le foloseste (si nici nu trebuie).
## 4. Stare periculoasa
Niciuna. Fara binar VFP editat/write-back, fara procese lasate pornite, fara date de
test consumate. Fisierele din radacina (`balanta_*`, `init_*`) au timestamp neschimbat.
Fara commit, fara `git add`. `git status --short` arata un singur fisier nou:
`tests/test_regresie_capat_la_capat.py`.
## 5. Verdict
**Etapa 2 este gata.** Fluxul nou, pornit de la PDF-urile originale prin linia de
comanda, reproduce identic (0 diferente pe valori de celula) ambele xlsx-uri de
initializare deja importate in ROACONT.

View File

@@ -0,0 +1,210 @@
"""Poarta finala: de la PDF-urile originale la xlsx-urile de import ROA.
Complementul lui tests/test_regresie.py. Acela verifica biblioteca pornind de la
balantele CSV de pe disc; acesta ruleaza EXACT ce ruleaza un om in teren, ca
subprocese (deci si parsarea argumentelor e sub test):
py extrage.py --sursa pdf --fisier "<pdf>" --firma <FIRMA> --an 2025 --luna 12 --dir <temp>
py genereaza.py --firma <FIRMA> --an 2025 --luna 12 --dir <temp>
Iesirile se scriu in temporar (nimic in radacina), se anonimizeaza cu ACELEASI
reguli si ACEIASI tabela ca tests/anonimizeaza_etalon.py si se compara cu
tests/golden/ pe VALORI DE CELULA, nu pe hash de fisier.
Asocierea PDF -> firma nu se presupune: fiecare balanta extrasa se compara cu
balanta de referinta din radacina (balanta_FUNDATIA.csv / balanta_MASTERJOB.csv),
care e cea folosita la constructia etalonului.
Daca iesirea difera de etalon, etalonul are dreptate: se raporteaza, nu se ajusteaza.
Rulare: py -m unittest discover -s tests -v
"""
import csv
import pathlib
import subprocess
import sys
import tempfile
import unittest
import openpyxl
from openpyxl.utils import get_column_letter
RADACINA = pathlib.Path(__file__).resolve().parent.parent
TESTS = pathlib.Path(__file__).resolve().parent
for cale in (str(RADACINA), str(TESTS)):
if cale not in sys.path:
sys.path.insert(0, cale)
import anonimizeaza_etalon as anonim
CORESP = TESTS / "_corespondenta_anonimizare.json"
GOLDEN = TESTS / "golden"
EXTRAGE = RADACINA / "extrage.py"
GENEREAZA = RADACINA / "genereaza.py"
AN, LUNA = 2025, 12
MAX_DIFERENTE_RAPORTATE = 20
# PDF din radacina -> firma -> balanta de referinta din radacina -> etalon.
# Asocierea e verificata de test_pdf_corespunde_referintei, nu doar afirmata.
FIRME = [
("FUNDATIA", "IBB 31.12.2025.pdf", "balanta_FUNDATIA.csv",
"golden_FUNDATIA_2025_12.xlsx"),
("MASTER", "MJC 31.12.2025.pdf", "balanta_MASTERJOB.csv",
"golden_MASTER_2025_12.xlsx"),
]
COLOANE_REF = ["cont", "denumire", "prec_d", "prec_c", "rulaj_d", "rulaj_c",
"total_d", "total_c", "sold_d", "sold_c", "este_total"]
COLOANE_SUMA = {"prec_d", "prec_c", "rulaj_d", "rulaj_c",
"total_d", "total_c", "sold_d", "sold_c"}
TOL = 0.001
def _ruleaza(script, argumente, cwd):
"""Ruleaza un punct de intrare ca subproces; orice cod de iesire != 0 e esec."""
comanda = [sys.executable, str(script)] + [str(a) for a in argumente]
rez = subprocess.run(comanda, cwd=str(cwd), capture_output=True, text=True)
if rez.returncode != 0:
raise AssertionError(
"comanda a esuat (cod %d): %s\n--- stdout ---\n%s\n--- stderr ---\n%s"
% (rez.returncode, " ".join(comanda), rez.stdout, rez.stderr))
return rez
def _citeste_csv(cale):
with open(cale, encoding="utf-8-sig", newline="") as f:
return list(csv.DictReader(f))
def _diferente_referinta(gasit, referinta):
"""Diferente pe coloanele comune, ca lista (rand, coloana, asteptat, gasit)."""
if len(gasit) != len(referinta):
return [(0, "nr_randuri", len(referinta), len(gasit))]
dif = []
for i, (a, b) in enumerate(zip(gasit, referinta)):
for c in COLOANE_REF:
va, vb = a.get(c), b.get(c)
if c in COLOANE_SUMA:
if va in (None, "") or vb in (None, ""):
if (va or "").strip() != (vb or "").strip():
dif.append((i + 1, c, vb, va))
elif abs(float(va) - float(vb)) > TOL:
dif.append((i + 1, c, vb, va))
elif (va or "").strip() != (vb or "").strip():
dif.append((i + 1, c, vb, va))
return dif
class TestRegresieCapatLaCapat(unittest.TestCase):
@classmethod
def setUpClass(cls):
lipsa = [pdf for _, pdf, _, _ in FIRME if not (RADACINA / pdf).exists()]
if lipsa:
raise unittest.SkipTest("lipsesc PDF-urile din radacina: %s"
% ", ".join(lipsa))
if not CORESP.exists():
raise unittest.SkipTest(
"lipseste %s (e ignorata de git). Etalonul nu se poate reproduce "
"fara tabela de corespondenta." % CORESP)
cls.coresp = anonim.citeste_corespondenta(CORESP)
cls._tmp = tempfile.TemporaryDirectory(prefix="regresie_cap_")
tmpdir = pathlib.Path(cls._tmp.name)
cls.balante = {}
cls.iesiri = {}
try:
for firma, pdf, _, _ in FIRME:
_ruleaza(EXTRAGE, ["--sursa", "pdf", "--fisier", str(RADACINA / pdf),
"--firma", firma, "--an", AN, "--luna", LUNA,
"--dir", str(tmpdir)], tmpdir)
cls.balante[firma] = tmpdir / ("balanta_%s.csv" % firma)
for firma, _, _, _ in FIRME:
_ruleaza(GENEREAZA, ["--firma", firma, "--an", AN, "--luna", LUNA,
"--dir", str(tmpdir)], tmpdir)
for firma, _, _, golden_nume in FIRME:
brut = tmpdir / ("init_%s_%d_%d.xlsx" % (firma, AN, LUNA))
anonimizat = tmpdir / ("anon_%s.xlsx" % firma)
anonim.anonimizeaza_fisier(str(brut), str(anonimizat), cls.coresp)
cls.iesiri[firma] = (anonimizat, GOLDEN / golden_nume)
except Exception:
cls._tmp.cleanup()
raise
@classmethod
def tearDownClass(cls):
cls._tmp.cleanup()
def _verifica_referinta(self, firma):
"""Balanta extrasa din PDF = balanta de referinta (verifica asocierea PDF->firma)."""
referinta = RADACINA / dict(
(f, r) for f, _, r, _ in FIRME)[firma]
if not referinta.exists():
self.skipTest("lipseste referinta %s" % referinta)
gasit = _citeste_csv(self.balante[firma])
asteptat = _citeste_csv(referinta)
diferente = _diferente_referinta(gasit, asteptat)
if diferente:
linii = ["%s: %d diferente fata de %s (PDF-ul nu e al firmei %s?)"
% (self.balante[firma].name, len(diferente), referinta.name, firma)]
for rand, col, asteptat_v, gasit_v in diferente[:MAX_DIFERENTE_RAPORTATE]:
linii.append(" rand %s, coloana %s: asteptat %r, gasit %r"
% (rand, col, asteptat_v, gasit_v))
self.fail("\n".join(linii))
def _verifica_etalon(self, firma):
anonimizat, golden = self.iesiri[firma]
self.assertTrue(golden.exists(), "lipseste etalonul %s" % golden)
wb_gasit = openpyxl.load_workbook(anonimizat)
wb_asteptat = openpyxl.load_workbook(golden)
try:
ws_gasit, ws_asteptat = wb_gasit.active, wb_asteptat.active
nr_g, nc_g = ws_gasit.max_row, ws_gasit.max_column
nr_a, nc_a = ws_asteptat.max_row, ws_asteptat.max_column
self.assertEqual(
(nr_g, nc_g), (nr_a, nc_a),
"Dimensiuni diferite pentru %s: asteptat %d randuri x %d coloane, "
"gasit %d randuri x %d coloane" % (golden.name, nr_a, nc_a, nr_g, nc_g))
diferente = []
for rand in range(1, max(nr_g, nr_a) + 1):
for col in range(1, max(nc_g, nc_a) + 1):
gasit = ws_gasit.cell(rand, col).value
asteptat = ws_asteptat.cell(rand, col).value
if gasit != asteptat:
diferente.append((rand, col, asteptat, gasit))
if diferente:
linii = ["%s: %d celule difera (fluxul din PDF nu reproduce etalonul):"
% (golden.name, len(diferente))]
for rand, col, asteptat, gasit in diferente[:MAX_DIFERENTE_RAPORTATE]:
linii.append(
" %s: rand %d, coloana %d (%s): asteptat %r, gasit %r"
% (golden.name, rand, col, get_column_letter(col),
asteptat, gasit))
if len(diferente) > MAX_DIFERENTE_RAPORTATE:
linii.append(" ... si alte %d celule"
% (len(diferente) - MAX_DIFERENTE_RAPORTATE))
self.fail("\n".join(linii))
finally:
wb_gasit.close()
wb_asteptat.close()
def test_pdf_fundatia_corespunde_referintei(self):
self._verifica_referinta("FUNDATIA")
def test_pdf_master_corespunde_referintei(self):
self._verifica_referinta("MASTER")
def test_etalon_fundatia(self):
self._verifica_etalon("FUNDATIA")
def test_etalon_master(self):
self._verifica_etalon("MASTER")
if __name__ == "__main__":
unittest.main()