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:
@@ -3,14 +3,21 @@
|
|||||||
Convertor balante SAGA (PDF) -> xlsx de initializare solduri ROACONT.
|
Convertor balante SAGA (PDF) -> xlsx de initializare solduri ROACONT.
|
||||||
|
|
||||||
## Mediu
|
## 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.
|
- Nu folosi `sed -i` (corupe fisierele). Scrie cu heredoc sau Python.
|
||||||
- `D:\ROA\ROACONT` = read-only. Nu comite (git/svn) fara aprobare.
|
- `D:\ROA\ROACONT` = read-only. Nu comite (git/svn) fara aprobare.
|
||||||
|
|
||||||
## Fisiere
|
## 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) |
|
| `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` |
|
| `genereaza_xlsx.py` | CSV -> `init_<FIRMA>_<an>_<luna>.xlsx` |
|
||||||
| `init_facturi_balanta_note.xlsx` | sablonul ROA (rand 1 = coloane, 2-13 instructiuni, 20+ exemple) |
|
| `init_facturi_balanta_note.xlsx` | sablonul ROA (rand 1 = coloane, 2-13 instructiuni, 20+ exemple) |
|
||||||
| `docs/decizii_import.md` | **regulile de mapare - citeste intai** |
|
| `docs/decizii_import.md` | **regulile de mapare - citeste intai** |
|
||||||
|
|||||||
9
bash.exe.stackdump
Normal file
9
bash.exe.stackdump
Normal 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
|
||||||
@@ -1,167 +1,98 @@
|
|||||||
# Handoff - etapa 2, convertorul reutilizabil SAGA -> ROA
|
# Handoff - etapa 2, convertorul reutilizabil SAGA -> ROA
|
||||||
|
|
||||||
Scris 21.09.2026. Sesiunea urmatoare este **orchestrator**, nu executant: imparte pe lane-uri,
|
Actualizat 21.09.2026, la finalul executiei. Versiunea anterioara a acestui fisier descria starea
|
||||||
trimite sarcinile la agenti opencode, verifica portile. Nu scrie ea codul.
|
**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.**
|
**Stare: etapa 2 este terminata.** Toate cele sase lane-uri din plan au trecut portile lor,
|
||||||
Marius nu a dat inca "porneste" pe lane-uri. Intreaba-l inainte sa lansezi primul lane.
|
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 |
|
| `extrage.py` | punctul de intrare: `--sursa pdf/xlsx/fdb` (dedus din extensie) -> contractul intern |
|
||||||
| `docs/raport_sursa_saga_xlsx.md` | SAGA veche (VFP); conventiile `conturi_roa.dbf` (`.TOATE`, `.RESTUL`) |
|
| `genereaza.py` | punctul de intrare: contractul intern + mapare -> `init_<FIRMA>_<an>_<luna>.xlsx` |
|
||||||
| `docs/raport_exporturi_saga_noua.md` | SAGA noua (Firebird); structura exporturilor si a bazei |
|
| `extract_balanta.py` | cititorul PDF, **nemodificat** (era validat in productie) |
|
||||||
| `docs/decizii_import.md` | regulile de mapare, **validate in productie - nu se redeschid** |
|
| `citire_xlsx.py` | cititorul de foaie SAGA `.xls` (xlrd) si `.xlsx` (openpyxl) |
|
||||||
| `docs/review_ceo_etapa2.md`, `docs/review_eng_etapa2.md` | de unde vin corectiile din v3; reciteste doar daca vrei sa redeschizi o decizie |
|
| `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
|
1. **Regresia pe etalon** (`tests/test_regresie.py`): biblioteca reproduce celula cu celula cele
|
||||||
trecut**; scripturile `sql/update_cod_fiscal_*.sql` au fost **executate** de Marius.
|
doua xlsx-uri importate in ROACONT. Verificat ca poarta e reala: scoaterea lui `5311` din
|
||||||
- Cercetarea pe ambele formate SAGA + conectarea la Firebird, verificata live.
|
config o face sa pice.
|
||||||
- Planul v3, dupa review de strategie si de arhitectura.
|
2. **Poarta cititoarelor** (`tests/test_citire_fdb.py`): CSV-ul din `CONT_BAZA.FDB` e identic **la
|
||||||
- `.gitignore` + `git rm --cached` pe iesirile de productie (fisierele raman pe disc).
|
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
|
## 3. Ce NU e dovedit - citeste inainte sa duci asta la un client
|
||||||
`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 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
|
## 4. Doua defecte prinse la verificare, nu de lane-uri
|
||||||
conversie, niciun test, niciun `config/conturi_parteneri.csv`.
|
|
||||||
|
|
||||||
## 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
|
1. **`.xls`-ul SAGA nu are inregistrare CODEPAGE**, deci `xlrd` cadea pe iso-8859-1 si strica
|
||||||
in `config/conturi_parteneri.csv`. `CONFIG_CONT_IREG` e consultativ; unealta **raporteaza**
|
denumirile (`FURNIZORI \x97 DEBITORI`). Reparat cu `encoding_override="cp1250"`, cu asertie in
|
||||||
divergentele, nu le aplica. Motivul, cu dovada, e in plan - Oracle nu listeaza 5121/5124/5311.
|
`tests/test_citire_xlsx.py` care pica daca revine.
|
||||||
2. **`acont` pentru conturi cu parteneri** ramane decis de `genereaza_xlsx.py`; maparea doar
|
2. **Redenumirea unui cont cu parteneri lua primul analitic** ca sa afle contul sintetic. Cu
|
||||||
suprascrie explicit. Randurile de diferenta raman calculate in cod.
|
analitice mapate pe conturi diferite, BALANTA agregat si randurile FACTURA cadeau pe conturi
|
||||||
3. **Maparea = doua fisiere**: `mapare_<FIRMA>.xlsx` scris doar de unealta (regenerat complet),
|
diferite. Acum ridica `Stop`; colapsarea prin `.TOATE` ramane legala.
|
||||||
`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.
|
|
||||||
|
|
||||||
## 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` ->
|
## 5. Divergenta fata de Oracle, constatata si scrisa
|
||||||
`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.
|
|
||||||
|
|
||||||
## 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) -
|
- o baza `.FDB` **reala** + exportul ei de balanta, ca sa se reruleze poarta pe date cu analitice
|
||||||
oprirea serverului dintr-un proiect omoara lane-urile celuilalt. Portul 4296 **nu e inca trecut in
|
si cu volum;
|
||||||
tabelul din ghid** - adauga-l acolo (COMUN are nevoie de aprobare inainte de commit).
|
- portul **4296** al acestui proiect nu e inca trecut in tabelul din
|
||||||
- Pornire, din directorul proiectului: `opencode serve --port 4296`
|
`D:\ROA\ROACONT\COMUN\docs\opencode_agenti_orchestrare.md` (COMUN cere aprobare inainte de commit);
|
||||||
- Model: `{"providerID":"opencode-go","modelID":"deepseek-v4.1-flash"}`
|
- `docs/plan_solduri2roa.md` descrie inca etapele 1-6; nu a fost rescris dupa etapa 2.
|
||||||
- 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`
|
|
||||||
|
|
||||||
Sablon PowerShell pentru trimitere (JSON pe UTF-8, altfel se strica diacriticele si ghilimelele):
|
## 7. Starea pe disc
|
||||||
|
|
||||||
```powershell
|
Totul comis si impins pe `origin/main`. Nimic periculos: niciun proces ramas viu (serverul opencode
|
||||||
$u='http://127.0.0.1:4296'
|
de pe 4296 a fost oprit), `D:\ROA\ROACONT`, `D:\SAGA250909` si `saga2roa*` neatinse, baza firmei de
|
||||||
$sys = [IO.File]::ReadAllText('D:\ROA\ROACONT\COMUN\docs\opencode_prompt_initial.md')
|
joaca deschisa numai prin copie in temporar, `tests/_corespondenta_anonimizare.json` ramasa ignorata
|
||||||
$m = @{providerID='opencode-go'; modelID='deepseek-v4.1-flash'}
|
de git.
|
||||||
$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))
|
|
||||||
```
|
|
||||||
|
|
||||||
Asteptarea: nu face `Start-Sleep` lung in foreground (harness-ul il blocheaza). Foloseste Bash in
|
Fisierele `init_FUNDATIA_2025_12.xlsx` si `init_MASTER_2025_12.xlsx` sunt pe disc dar nu in git;
|
||||||
fundal: `until curl -s http://127.0.0.1:4296/session/status | grep -qv busy; do sleep 20; done`.
|
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,
|
## 8. Cum rulezi acum unealta
|
||||||
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`):
|
|
||||||
|
|
||||||
```
|
```
|
||||||
M .gitignore
|
py extrage.py --fisier "IBB 31.12.2025.pdf" --firma FUNDATIA --an 2025 --luna 12
|
||||||
D balanta_FUNDATIA.csv
|
py genereaza.py --firma FUNDATIA --an 2025 --luna 12
|
||||||
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/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Verificari inainte de orice altceva:
|
Sursa se deduce din extensie (`.pdf`, `.xls`/`.xlsx`, `.fdb`); o extensie necunoscuta da eroare.
|
||||||
- `init_FUNDATIA_2025_12.xlsx` si `init_MASTER_2025_12.xlsx` sunt pe disc dar nu mai sunt in git.
|
Pentru Firebird, da calea catre o **copie** a bazei, niciodata baza vie a clientului.
|
||||||
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.
|
|
||||||
|
|||||||
108
docs/raport_lane_regresie.md
Normal file
108
docs/raport_lane_regresie.md
Normal 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.
|
||||||
210
tests/test_regresie_capat_la_capat.py
Normal file
210
tests/test_regresie_capat_la_capat.py
Normal 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()
|
||||||
Reference in New Issue
Block a user