Cititorul FDB: sume totale ca SAGA, analitice pastrate, baza oprita acceptata
Proba pe LACERTA (06/2026, firma reala) a gasit doua defecte in citire_fdb: - precedentul netea si rulajele din an, deci conturile de cheltuieli/venituri pierdeau rulajul (CPP gresit). Acum: preluare netata + rulaj an brut; potriveste 44/44 sintetice din importul facut din exportul SAGA (inainte 19/44). - analiticele se rulau in sintetic, deci facturile per document nu se foloseau si analiticele nepartener disparea. Acum balanta are toate nivelurile. Baza primita se copiaza in temporar si copia se repune online (clientul o trimite si in full shutdown). tests/test_lacerta.py: 48/53 frunze identice cu REF, restul explicate; se sare fara fisierele clientului, care sunt ignorate de git. README nou; analiza in docs/analiza_lacerta_fdb.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYeAtVxeS8m4oXekjX8Am2
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,5 +21,8 @@ parteneri_*.csv
|
||||
facturi_*.csv
|
||||
corectii_*.xlsx
|
||||
|
||||
# fisierele primite de la clienti (nume, CNP-uri) - nu in git
|
||||
exemple/lacerta_*
|
||||
|
||||
# gunoi de la shell-ul Windows
|
||||
*.stackdump
|
||||
|
||||
80
README.md
Normal file
80
README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# solduri2roa
|
||||
|
||||
Transforma soldurile unei firme din SAGA in xlsx-ul de initializare pentru ROACONT.
|
||||
|
||||
## Pe scurt
|
||||
|
||||
```
|
||||
py extrage.py --fisier <sursa> --firma FIRMA --an 2026 --luna 6
|
||||
py genereaza.py --firma FIRMA --an 2026 --luna 6
|
||||
```
|
||||
Rezultatul este `iesiri/init_FIRMA_2026_6.xlsx`, care se importa in ROACONT.
|
||||
|
||||
`--an`/`--luna` = luna ale carei solduri finale se preiau.
|
||||
|
||||
## Surse acceptate
|
||||
|
||||
| sursa | fisier | ce se obtine |
|
||||
|---|---|---|
|
||||
| SAGA noua (Firebird) | `CONT_BAZA.FDB` (o **copie**, nu baza vie) | balanta + parteneri cu CUI + facturi per document |
|
||||
| export SAGA | `.xls` / `.xlsx` | doar balanta |
|
||||
| balanta tiparita | `.pdf` | doar balanta |
|
||||
|
||||
Tipul se deduce din extensie. **FDB-ul e sursa recomandata.** Clientul face copia dupa
|
||||
`docs/procedura_copie_fdb_client.md`. Poate fi si oprita (`shutdown`): unealta
|
||||
lucreaza pe o copie a ei si nu atinge fisierul primit. Backupul `.fbk` nu e acceptat
|
||||
inca.
|
||||
|
||||
## Pasii
|
||||
|
||||
1. **Extrage** - `extrage.py` scrie in `date/`: `balanta_FIRMA.csv` (+ `parteneri_`,
|
||||
`facturi_` din FDB).
|
||||
2. **Genereaza** - `genereaza.py` scrie in `iesiri/`:
|
||||
- `init_FIRMA_<an>_<luna>.xlsx` - fisierul de import;
|
||||
- `verificare_FIRMA.md` - verificarile; toate trebuie sa fie `OK`.
|
||||
|
||||
Si in `date/`:
|
||||
- `mapare_FIRMA.xlsx` - ce cont ROA primeste fiecare cont SAGA (doar de citit);
|
||||
- `corectii_FIRMA.xlsx` - creat gol la prima rulare; **aici corectezi tu**.
|
||||
3. **Corecteaza** daca genereaza se opreste (`Stop: ...`) sau maparea nu e buna.
|
||||
Completezi `corectii_FIRMA.xlsx` si rulezi din nou pasul 2.
|
||||
4. **Importa** xlsx-ul in ROACONT (formularul de initializare solduri).
|
||||
5. **Coduri fiscale** - partenerii fara CUI intra cu un cod provizoriu (`401.00027`).
|
||||
Le cauti pe ANAF si le corectezi dupa import (`sql/update_cod_fiscal_<SCHEMA>.sql`).
|
||||
Din FDB raman doar partenerii fara CUI in SAGA; din PDF/xls, toti.
|
||||
|
||||
## Corectii (`corectii_FIRMA.xlsx`)
|
||||
|
||||
Coloane: `cont_saga`, `cont`, `acont`, `exclus`, `motiv`. `cont` are maxim 4 caractere,
|
||||
`acont` la fel.
|
||||
|
||||
| `cont_saga` | se aplica la |
|
||||
|---|---|
|
||||
| `419.NETOPIA` | exact acel cont |
|
||||
| `4428.TOATE` | toate analiticele lui 4428 |
|
||||
| `4428.RESTUL` | analiticele lui 4428 fara corectie exacta |
|
||||
|
||||
Exemple: `419.NETOPIA -> 419 / 2`, `4428.TOATE -> 4428 / (gol)` = strange tot pe sintetic.
|
||||
Cand un analitic nu incape in 4 caractere, genereaza se opreste si iti cere corectia.
|
||||
|
||||
## Reguli de stiut
|
||||
|
||||
- Conturile cu parteneri sunt in `config/conturi_parteneri.csv` (autoritatea).
|
||||
Primesc un rand `BALANTA` pe sintetic si randuri `FACTURA` per partener sau document.
|
||||
- In xlsx intra doar frunzele (ultimul nivel de analitic).
|
||||
- Diferenta dintre sintetic si suma analiticelor (note puse direct pe sintetic) iese
|
||||
pe un analitic de diferenta (`9999`, `99`) sau, la parteneri, pe `NEREPARTIZAT` in ROA.
|
||||
- Nu ajusta cifre ca sa treaca o verificare. Raporteaza.
|
||||
|
||||
## Teste
|
||||
|
||||
```
|
||||
py -m unittest discover -s tests
|
||||
```
|
||||
Testele pe date de client (LACERTA) se sar daca fisierele din `exemple/` lipsesc.
|
||||
Aceste fisiere nu sunt in git.
|
||||
|
||||
## Mai mult
|
||||
|
||||
`docs/decizii_import.md` (regulile de mapare), `docs/analiza_lacerta_fdb.md` (proba pe
|
||||
client real), `CLAUDE.md` (verificarile obligatorii).
|
||||
@@ -12,17 +12,21 @@ COPIE a bazei (gbak sau fisier oprit), niciodata baza vie a clientului.
|
||||
|
||||
Sumele nu vin din soldurile stocate in CONTURI (sunt 0); se agrega REGISTRU pe trei
|
||||
intervale: inainte de inceputul anului (sold de preluare), de la inceputul anului pana
|
||||
la inceputul lunii cerute si luna ceruta. Precedentul (preluare + an) se neteaza pe
|
||||
latura, ca in exportul SAGA; rulajul lunii se ia brut, iar total = prec + rulaj.
|
||||
Analiticul din REGISTRU (401.00002) se ruleaza in sinteticul lui (401); contul
|
||||
la inceputul lunii cerute si luna ceruta. Precedentul = soldul de preluare netat pe
|
||||
latura + rulajele din an, BRUTE (ca SAGA: netarea lor ar anula rulajul conturilor de
|
||||
cheltuieli si venituri); rulajul lunii se ia brut, iar total = prec + rulaj.
|
||||
Fiecare cont din REGISTRU intra in toate nivelurile lui: 401.00002 -> 401 si 401.00002,
|
||||
deci balanta are sinteticele si analiticele, ca exportul SAGA cu analitice. Contul
|
||||
colector `%` se exclude.
|
||||
|
||||
LIMITA VERIFICARII (scrisa si in raport): cititorul este verificat DOAR pe cazul
|
||||
sintetic al firmei de joaca din `D:\\SAGA250909\\0001` (10 conturi, toate sintetice,
|
||||
fara analitice in iesire, 2 furnizori, 0 clienti, 1 factura, luna 09/2026). Poarta
|
||||
(balanta FDB identica cu exportul .xlsx) dovedeste rollup-ul la sintetic si
|
||||
periodizarea pe acest caz; NU dovedeste nimic despre conturile analitice. Nu se
|
||||
foloseste la un client fara rerularea portii pe datele lui.
|
||||
Baza primita se copiaza in temporar si copia se repune online (clientul o poate opri
|
||||
cu `full shutdown` inainte de copiere); fisierul primit nu se atinge.
|
||||
|
||||
VERIFICARI: firma de joaca din `D:\SAGA250909\0001` (sinteticele = exportul .xlsx,
|
||||
la octet) si LACERTA 06/2026 (tests/test_lacerta.py: 48 din 53 de frunze identice cu
|
||||
importul facut din exportul SAGA, restul explicate in docs/analiza_lacerta_fdb.md;
|
||||
facturile per document insumeaza netul fiecarui partener). La un client nou se
|
||||
reruleaza o poarta pe datele lui.
|
||||
|
||||
Utilizare:
|
||||
py citire_fdb.py "<copie CONT_BAZA.FDB>" "<FIRMA>" <an> <luna> [dir_iesire]
|
||||
@@ -30,11 +34,15 @@ Utilizare:
|
||||
|
||||
import csv
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from calendar import monthrange
|
||||
from datetime import date
|
||||
|
||||
FBCLIENT = r"C:\Program Files\Firebird\Firebird30_Saga\fbclient.dll"
|
||||
GFIX = os.path.join(os.path.dirname(FBCLIENT), "gfix.exe")
|
||||
UTILIZATOR = "SYSDBA"
|
||||
PAROLA = "masterkey"
|
||||
CHARSET = "WIN1250"
|
||||
@@ -45,30 +53,23 @@ COLOANE_BALANTA = ["pagina", "cont", "denumire", "prec_d", "prec_c", "rulaj_d",
|
||||
COLOANE_PARTENERI = ["cont_analitic", "cod", "denumire", "cod_fiscal"]
|
||||
COLOANE_FACTURI = ["cont_analitic", "numar", "data", "scadent", "sold"]
|
||||
|
||||
# Prefix pe partea din stanga punctului (401.00002 -> 401). TRIM inainte, coloanele
|
||||
# SAGA sunt VARCHAR(20) cu spatii la dreapta.
|
||||
_SINT = ("CASE WHEN POSITION('.' IN TRIM(%s)) > 0 "
|
||||
"THEN SUBSTRING(TRIM(%s) FROM 1 FOR POSITION('.' IN TRIM(%s)) - 1) "
|
||||
"ELSE TRIM(%s) END")
|
||||
|
||||
_SQL_BALANTA = """
|
||||
SELECT SINT, LATURA,
|
||||
SELECT CONT, LATURA,
|
||||
SUM(CASE WHEN DATA < ? THEN SUMA ELSE 0 END) AS INIT,
|
||||
SUM(CASE WHEN DATA >= ? AND DATA < ? THEN SUMA ELSE 0 END) AS AN_PREC,
|
||||
SUM(CASE WHEN DATA >= ? AND DATA < ? THEN SUMA ELSE 0 END) AS LUNA
|
||||
FROM (
|
||||
SELECT {sint_d} AS SINT, 'D' AS LATURA, SUMA, DATA
|
||||
SELECT TRIM(CONT_D) AS CONT, 'D' AS LATURA, SUMA, DATA
|
||||
FROM REGISTRU
|
||||
WHERE TRIM(CONT_D) NOT IN ('', '%') AND DATA < ?
|
||||
UNION ALL
|
||||
SELECT {sint_c} AS SINT, 'C' AS LATURA, SUMA, DATA
|
||||
SELECT TRIM(CONT_C) AS CONT, 'C' AS LATURA, SUMA, DATA
|
||||
FROM REGISTRU
|
||||
WHERE TRIM(CONT_C) NOT IN ('', '%') AND DATA < ?
|
||||
) A
|
||||
GROUP BY SINT, LATURA
|
||||
ORDER BY SINT, LATURA
|
||||
""".format(sint_d=_SINT % (("CONT_D",) * 4),
|
||||
sint_c=_SINT % (("CONT_C",) * 4))
|
||||
GROUP BY CONT, LATURA
|
||||
ORDER BY CONT, LATURA
|
||||
"""
|
||||
|
||||
_API_INCARCAT = False
|
||||
|
||||
@@ -164,8 +165,11 @@ def citeste_balanta(cale, an, luna):
|
||||
con.close()
|
||||
|
||||
sume = {}
|
||||
for sint, latura, init, an_prec, luna_v in brute:
|
||||
s = sume.setdefault(_text(sint), {"i_d": 0, "i_c": 0, "a_d": 0,
|
||||
for cont, latura, init, an_prec, luna_v in brute:
|
||||
seg = _text(cont).split(".")
|
||||
# contul intra in fiecare nivel al lui: 215.1 -> 215 si 215.1
|
||||
for k in range(1, len(seg) + 1):
|
||||
s = sume.setdefault(".".join(seg[:k]), {"i_d": 0, "i_c": 0, "a_d": 0,
|
||||
"a_c": 0, "l_d": 0, "l_c": 0})
|
||||
if latura == "D":
|
||||
s["i_d"] += init or 0
|
||||
@@ -179,13 +183,15 @@ def citeste_balanta(cale, an, luna):
|
||||
rows = []
|
||||
for cont in sorted(sume):
|
||||
s = sume[cont]
|
||||
# precedent = soldul la inceputul lunii (preluare + an), pe net
|
||||
prec_d, prec_c = _net(s["i_d"] + s["a_d"], s["i_c"] + s["a_c"])
|
||||
# precedent = soldul de preluare pe net + rulajele din an, brute (ca SAGA:
|
||||
# netarea rulajelor ar sterge rulajul conturilor de cheltuieli/venituri)
|
||||
prec_d, prec_c = _net(s["i_d"], s["i_c"])
|
||||
prec_d, prec_c = prec_d + s["a_d"], prec_c + s["a_c"]
|
||||
rulaj_d, rulaj_c = s["l_d"], s["l_c"]
|
||||
total_d = prec_d + rulaj_d
|
||||
total_c = prec_c + rulaj_c
|
||||
sold_d, sold_c = _net(total_d, total_c)
|
||||
denumire, tip = conturi.get(cont, ("", ""))
|
||||
denumire, tip = conturi.get(cont) or ("", conturi.get(cont.split(".")[0], ("", ""))[1])
|
||||
rows.append({
|
||||
"pagina": "", "cont": cont, "denumire": denumire,
|
||||
"prec_d": _num(prec_d), "prec_c": _num(prec_c),
|
||||
@@ -284,13 +290,31 @@ def scrie_csv(rows, cale, coloane):
|
||||
wr.writerows(rows)
|
||||
|
||||
|
||||
def copie_online(cale_fdb, dir_tmp):
|
||||
"""Copiaza baza in dir_tmp si o repune online, daca clientul a oprit-o inainte de
|
||||
copiere (`full shutdown`). Fisierul primit nu se atinge. Pe o baza deja online
|
||||
gfix raspunde cu eroare, inofensiva: daca repunerea chiar esueaza, conectarea
|
||||
urmatoare pica cu mesajul real (`database ... shutdown`)."""
|
||||
if not os.path.exists(cale_fdb):
|
||||
raise FileNotFoundError("lipseste baza Firebird: %s" % cale_fdb)
|
||||
copie = os.path.join(dir_tmp, "CONT_BAZA.FDB")
|
||||
shutil.copyfile(cale_fdb, copie)
|
||||
if os.path.exists(GFIX):
|
||||
subprocess.run([GFIX, "-online", "normal", copie, "-user", UTILIZATOR,
|
||||
"-password", PAROLA], capture_output=True)
|
||||
return copie
|
||||
|
||||
|
||||
def produce(cale_fdb, firma, an, luna, dir_iesire="."):
|
||||
"""Scrie cele trei fisiere ale contractului intern; intoarce randurile."""
|
||||
"""Scrie cele trei fisiere ale contractului intern; intoarce randurile.
|
||||
Citeste dintr-o copie temporara, repusa online (vezi copie_online)."""
|
||||
if not os.path.isdir(dir_iesire):
|
||||
os.makedirs(dir_iesire)
|
||||
balanta = citeste_balanta(cale_fdb, an, luna)
|
||||
parteneri = citeste_parteneri(cale_fdb)
|
||||
facturi = citeste_facturi(cale_fdb, an, luna)
|
||||
with tempfile.TemporaryDirectory(prefix="citire_fdb_") as tmp:
|
||||
copie = copie_online(cale_fdb, tmp)
|
||||
balanta = citeste_balanta(copie, an, luna)
|
||||
parteneri = citeste_parteneri(copie)
|
||||
facturi = citeste_facturi(copie, an, luna)
|
||||
scrie_csv(balanta, os.path.join(dir_iesire, "balanta_%s.csv" % firma),
|
||||
COLOANE_BALANTA)
|
||||
scrie_csv(parteneri, os.path.join(dir_iesire, "parteneri_%s.csv" % firma),
|
||||
|
||||
135
docs/analiza_lacerta_fdb.md
Normal file
135
docs/analiza_lacerta_fdb.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Analiza LACERTA - drumul FDB comparat cu importul facut din exportul SAGA
|
||||
|
||||
25.09.2026. Prima proba a cititorului FDB pe o firma reala, cu analitice si volum.
|
||||
|
||||
Intrari (in `exemple/`, **nu in git** - contin nume de persoane si CNP-uri):
|
||||
- `lacerta_CONT_BAZA-20-08_2026.FDB` - copia bazei, facuta la 20.08.2026;
|
||||
- `lacerta_init_facturi_balanta_note-20-08_2026.xlsx` - xlsx-ul de initializare facut din
|
||||
exportul de balanta SAGA, perioada **06/2026** (56 BALANTA + 46 FACTURA). Mai jos: **REF**.
|
||||
|
||||
Rulare (copie in scratchpad, `date/` si `iesiri/` neatinse):
|
||||
```
|
||||
gfix -online normal <copie>.FDB -user SYSDBA -password masterkey # vezi constatarea 1
|
||||
py extrage.py --fisier <copie>.FDB --firma LACERTA --an 2026 --luna 6 --dir <tmp>
|
||||
py genereaza.py --firma LACERTA --an 2026 --luna 6 --dir <tmp> --dir-iesire <tmp>\out
|
||||
```
|
||||
Rezultat: 54 de randuri, 4.323.688,60 pe ambele laturi, verificarile interne trec.
|
||||
Verificarile trec, dar **iesirea e gresita** in doua moduri (constatarile 2 si 3).
|
||||
|
||||
---
|
||||
|
||||
## Constatari despre unealta noastra
|
||||
|
||||
### 1. Copia clientului vine in stare `full shutdown` - cititorul nu o deschide
|
||||
`gstat -h` arata `Attributes: force write, full shutdown`; conectarea da `database ... shutdown`.
|
||||
Clientul (sau SAGA) a oprit baza inainte de copiere - corect din partea lui. Pe **copia
|
||||
noastra** `gfix -online normal` o repune; originalul ramane neatins.
|
||||
|
||||
### 2. Sumele totale sunt calculate gresit - afecteaza CPP-ul (defect)
|
||||
`citire_fdb.py:84` neteaza **impreuna** soldul de preluare si rulajele lunilor anterioare din an:
|
||||
`prec = net(init + an)`. SAGA (si REF) calculeaza `prec = net(init) + rulaj an`, **brut**.
|
||||
|
||||
| regula | conturi sintetice din REF potrivite |
|
||||
|---|---|
|
||||
| actuala: `net(init + an) + luna` | 19 / 44 |
|
||||
| propusa: `net(init) + an + luna` | **44 / 44** |
|
||||
|
||||
Netul iese la fel in ambele cazuri, de aceea verificarile interne trec. Dar pe conturile de
|
||||
cheltuieli si venituri rulajele Ian-Mai se anuleaza: `6022`, `6123`, `6231`, `6232`, `635`,
|
||||
`667` ies cu `0 / 0` in loc de rulaj, iar `704` iese cu 684,68 in loc de 14.139,64.
|
||||
Contul de profit si pierdere din ROA ar iesi gresit.
|
||||
|
||||
Pe firma de joaca regulile dau acelasi rezultat pe toate conturile. De aceea poarta a trecut,
|
||||
si tot de aceea schimbarea nu o strica.
|
||||
|
||||
### 3. Cititorul arunca analiticele - pierde facturile si analiticele nepartener (defect)
|
||||
`citire_fdb` ruleaza orice analitic in sintetic (`401.00002` -> `401`, `215.1` -> `215`). Baza
|
||||
are planul analitic complet in `CONTURI`: 12 sintetice cu analitice in `REGISTRU`, inclusiv
|
||||
`215.1-3` / `2815.1-3`, `4551.1-3`, `5121.1-2`, `542.01-02`, 15 furnizori, 26 clienti.
|
||||
|
||||
Consecinte:
|
||||
- **Facturile nu se folosesc deloc.** `facturi_LACERTA.csv` are 74 de documente, dar generatorul
|
||||
le cauta dupa analiticul partenerului (`genereaza_xlsx.py:265`), care nu mai exista. Rezultatul
|
||||
e un singur `FACTURA` agregat pe cont, fara nume si fara cod fiscal real.
|
||||
- **Analiticele nepartener dispar.** REF are `215` pe trei analitice; noi avem un singur rand.
|
||||
- Facturile se potrivesc cu balanta: la **29 / 29** parteneri 401/4111, suma facturilor din baza
|
||||
este egala cu netul analiticului. Deci, daca pastram analiticele, drumul "un FACTURA per
|
||||
document" functioneaza fara nicio ajustare.
|
||||
|
||||
Aceasta este exact limita scrisa in docstring-ul lui `citire_fdb.py` ("NU dovedeste nimic despre
|
||||
conturile analitice"). Acum e dovedita, si nu tine.
|
||||
|
||||
### 4. Analitice care nu intra in `acont` de 4 caractere
|
||||
`419.NETOPIA`, `4428.M`, `4428.TI`, `4428.TP`, `461.SGR`. Dupa constatarea 3 vor ajunge la
|
||||
mapare. `mapare.py` trebuie sa se opreasca pe ele si sa ceara o corectie (REF a pus `419.2`).
|
||||
|
||||
---
|
||||
|
||||
## Constatari despre REF (xlsx-ul deja facut din exportul SAGA)
|
||||
|
||||
Le scriu pentru ca REF a fost folosit la initializarea din ROA:
|
||||
|
||||
1. **REF nu e echilibrat**: are 6.315.365,48 pe debit si 5.043.406,14 pe credit. Cauza:
|
||||
`215`, `2815` si `4091` apar si cu randul sintetic, si cu analiticele, adica de doua ori
|
||||
(+1.530.528,54 D, +265.716,75 C, +7.810,92 D). Incalca regula "in xlsx intra doar frunzele".
|
||||
**De verificat in ROA daca soldurile acestor conturi au intrat dublu.**
|
||||
2. Pe **`4092` lipsesc 1.563,37, iar pe `419` lipsesc 900,00.** Sunt note inregistrate direct pe
|
||||
sintetic, nu pe analitic (5 x 230 Starlink + 183,37 Netopia + 230 in iunie; 900 in februarie).
|
||||
REF a luat doar analiticele. Regula noastra de "analitic de diferenta" le prinde.
|
||||
3. Randurile `FACTURA` pe 401 insumeaza 9.220,75, fata de un net de 48.261,67. Multe au `sold`
|
||||
gol, iar una are data `1910-05-13`.
|
||||
|
||||
Pe restul, REF si FDB coincid: 44 / 44 de sintetice pe sume totale, iar netul pe fiecare cont.
|
||||
|
||||
---
|
||||
|
||||
## Propuneri, in ordinea in care le-as face
|
||||
|
||||
| # | ce | marime | dovada ca a mers |
|
||||
|---|---|---|---|
|
||||
| P1 | `citire_fdb.py:84`: `prec = net(init) + an`, brut | ~2 linii | 44/44 pe LACERTA; testele pe firma de joaca raman verzi |
|
||||
| P2 | `citire_fdb` emite si randurile analitice (sintetic + analitic, ca exportul SAGA "cu analitice"); poarta pe firma de joaca compara doar sinteticele | mediu | FACTURA per document pe 401/4111; `215.1-3` ca frunze |
|
||||
| P3 | `extrage.py` pe drumul fdb copiaza baza in temporar si ruleaza `gfix -online` pe copie; procedura clientului mentioneaza ca oprirea bazei e acceptata | mic | LACERTA se deschide fara pas manual |
|
||||
| P4 | test de regresie LACERTA: BALANTA pe frunze = REF, cu exceptiile de mai sus scrise explicit; se sare daca fisierele lipsesc (nu sunt in git) | mic | pica daca revine P1 sau P2 |
|
||||
| P5 | `exemple/lacerta_*` in `.gitignore`: xlsx-ul are CNP-uri, iar acum nu e ignorat (FDB-ul este) | 1 linie | `git check-ignore` |
|
||||
|
||||
**Decizii care sunt ale tale, nu ale mele**, si nu le-am atins:
|
||||
- `542` (decontari din avansuri): REF il trateaza ca cont cu parteneri (2 persoane); config-ul nostru nu.
|
||||
- `461` si `4092`: config-ul le trateaza ca parteneri, REF nu.
|
||||
- `config/conturi_parteneri.csv` descrie `4092` ca "Furnizori de imobilizari", dar 4092 e
|
||||
"furnizori-debitori pt. prestari servicii" (furnizorii de imobilizari sunt 404). Verifica daca
|
||||
e doar descrierea gresita sau si contul.
|
||||
- Coloana `data` la BALANTA: noi scriem `01.06.2026`, REF are `30.06.2026`. Etalonul importat
|
||||
foloseste forma noastra, deci probabil ambele sunt acceptate, dar confirma.
|
||||
|
||||
## Ce s-a facut (25.09.2026)
|
||||
|
||||
P1-P5 sunt facute. Rulare pe LACERTA dupa ele (`tests/test_lacerta.py`):
|
||||
|
||||
- `extrage.py` citeste direct fisierul oprit din `exemple/`; originalul ramane `full shutdown`;
|
||||
- 97 de randuri de balanta (sintetice + analitice), 79 de randuri in xlsx, echilibrat
|
||||
(4.775.134,59 pe ambele laturi), verificarile interne OK;
|
||||
- **48 din 53** de frunze BALANTA sunt identice cu REF; restul diferentelor sunt listate
|
||||
in test (`DIFERENTE_ASTEPTATE`), fiecare cu cauza;
|
||||
- 401: 12 randuri FACTURA, cate unul per document nesoldat, cu numar, data, scadenta si
|
||||
CUI real; suma e 9.220,75, cat in REF. Restul de 39.040,92 (preluarea postata direct pe
|
||||
401, fara partener) ramane pe `NEREPARTIZAT`, ca in regula stabilita;
|
||||
- poarta e reala: fara P1 testul pica pe sume, fara P2 pica la generare.
|
||||
|
||||
Corectiile folosite (in test, ca in REF): `419.NETOPIA -> 419 / 2`, `4428.TOATE -> 4428`.
|
||||
|
||||
## Intrebari deschise
|
||||
|
||||
1. **4428 iese pe doua randuri**: `4428` fara acont (373,14, analiticele M/TI/TP stranse
|
||||
prin `.TOATE`) si `4428 / 99` (1.376,54, postarile direct pe sintetic). Nu stiu cum
|
||||
trateaza importul un cont cu rand si fara acont, si cu acont. Varianta sigura:
|
||||
corectia `4428.TOATE -> 4428 / 01`, ca sa iasa doua frunze curate. Decizia e a ta.
|
||||
2. `.fbk`: procedura clientului recomanda backupul `.fbk` ca varianta preferata, dar
|
||||
`extrage.py` nu il accepta (nu il restaureaza cu `gbak -c`). Deocamdata clientul
|
||||
trebuie sa trimita `.FDB`.
|
||||
3. Deciziile de config de mai sus (`542`, `461`, `4092`) si coloana `data`.
|
||||
|
||||
## Starea pe disc
|
||||
Cod: `citire_fdb.py` (P1-P3), `tests/test_citire_fdb.py` (poarta restransa la sintetice),
|
||||
`tests/test_lacerta.py` (P4), `.gitignore` (P5). Suita: `py -m unittest discover -s tests`,
|
||||
37 de teste, toate trec. Fisierele LACERTA din `exemple/` sunt ignorate de git.
|
||||
@@ -73,6 +73,12 @@ consultativ. Detaliile in `docs/decizii_import.md`.
|
||||
|
||||
## 6. Ce urmeaza - de aici incepe sesiunea urmatoare
|
||||
|
||||
**Actualizare 25.09.2026 - proba pe client real facuta (LACERTA, 06/2026).** A gasit doua
|
||||
defecte in `citire_fdb` (sumele totale netate gresit si analiticele aruncate), acum
|
||||
reparate, cu poarta in `tests/test_lacerta.py`. Detalii si intrebari deschise:
|
||||
`docs/analiza_lacerta_fdb.md`. Punctul 1 de mai jos e facut pe o firma; punctul 2 ramane
|
||||
(importul in ROACONT al iesirii LACERTA).
|
||||
|
||||
In ordinea in care le-as face:
|
||||
|
||||
1. **Proba pe un client real** - singurul lucru care mai conteaza acum. Cere unui client cu SAGA
|
||||
|
||||
@@ -46,6 +46,9 @@ Atunci trebuie neaparat sa **inchizi SAGA inainte**:
|
||||
Daca SAGA este pornit in timpul copierii, copia poate iesi **stricata** si nu se mai
|
||||
poate deschide.
|
||||
|
||||
Daca SAGA (sau tu) opreste baza inainte de copiere, copia vine marcata ca oprita
|
||||
(`shutdown`). E in regula: o repornim noi, pe o copie a copiei tale.
|
||||
|
||||
## Cum trimiti
|
||||
|
||||
- Pune fisierul (`.fbk` sau `.FDB`) intr-o arhiva ZIP, ca sa se trimita mai usor.
|
||||
@@ -54,6 +57,6 @@ poate deschide.
|
||||
|
||||
## Ce facem noi cu ea
|
||||
|
||||
Deschidem doar copia, in mod **citire** (`read-only`), nu modificam si nu scriem
|
||||
nimic in ea, si extragem balanta, partenerii si facturile pentru conversie. Baza ta
|
||||
Lucram pe o copie a fisierului primit (pe care, daca e oprita, o repornim), o
|
||||
citim in mod **citire** (`read-only`), fara sa scriem date in ea, si extragem balanta, partenerii si facturile pentru conversie. Baza ta
|
||||
originala ramane neatinsa.
|
||||
|
||||
@@ -63,7 +63,9 @@ class TestCitireFdb(unittest.TestCase):
|
||||
if not XLSX.exists():
|
||||
self.skipTest("lipseste exportul %s" % XLSX)
|
||||
rows_xlsx = citire_xlsx.citeste_balanta(str(XLSX))
|
||||
rows_fdb = citire_fdb.citeste_balanta(self.baza, AN, LUNA)
|
||||
# exportul firmei de joaca e fara analitice; cititorul le emite si pe ele
|
||||
rows_fdb = [r for r in citire_fdb.citeste_balanta(self.baza, AN, LUNA)
|
||||
if "." not in r["cont"]]
|
||||
self.assertTrue(rows_xlsx, "exportul xlsx nu are randuri")
|
||||
self.assertEqual(rows_xlsx, rows_fdb,
|
||||
"balanta din FDB difera de exportul xlsx")
|
||||
@@ -96,7 +98,8 @@ class TestCitireFdb(unittest.TestCase):
|
||||
msg="SUM(REGISTRU) nu e echilibrat pe laturi")
|
||||
|
||||
rows = citire_fdb.citeste_balanta(self.baza, AN, LUNA)
|
||||
net_balanta = sum(r["total_d"] - r["total_c"] for r in rows)
|
||||
net_balanta = sum(r["total_d"] - r["total_c"] for r in rows
|
||||
if "." not in r["cont"])
|
||||
self.assertAlmostEqual(float(deb) - float(cred), net_balanta, delta=TOL,
|
||||
msg="netul din REGISTRU != netul balantei")
|
||||
|
||||
|
||||
155
tests/test_lacerta.py
Normal file
155
tests/test_lacerta.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""Poarta pe o firma reala cu analitice: LACERTA, drumul FDB, 06/2026.
|
||||
|
||||
Ruleaza ca subprocese, pornind de la copia bazei primita de la client (in stare
|
||||
`full shutdown`, deci exerseaza si repunerea online pe copia temporara):
|
||||
|
||||
py extrage.py --fisier exemple/lacerta_CONT_BAZA-20-08_2026.FDB --firma LACERTA --an 2026 --luna 6
|
||||
py genereaza.py --firma LACERTA --an 2026 --luna 6
|
||||
|
||||
si compara frunzele BALANTA cu importul facut din exportul SAGA (REF, in exemple/).
|
||||
Analiza completa: docs/analiza_lacerta_fdb.md.
|
||||
|
||||
REF nu e un etalon curat. Diferentele de mai jos sunt explicate acolo si scrise aici
|
||||
explicit; orice alta diferenta pica testul. Nu se adauga exceptii ca sa treaca.
|
||||
|
||||
Fisierele clientului nu sunt in git (nume, CNP-uri): lipsa lor = test sarit.
|
||||
"""
|
||||
import collections
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import openpyxl
|
||||
|
||||
RADACINA = pathlib.Path(__file__).resolve().parent.parent
|
||||
if str(RADACINA) not in sys.path:
|
||||
sys.path.insert(0, str(RADACINA))
|
||||
|
||||
import mapare
|
||||
|
||||
EXEMPLE = RADACINA / "exemple"
|
||||
BAZA = EXEMPLE / "lacerta_CONT_BAZA-20-08_2026.FDB"
|
||||
REF = EXEMPLE / "lacerta_init_facturi_balanta_note-20-08_2026.xlsx"
|
||||
AN, LUNA = 2026, 6
|
||||
|
||||
# deciziile de mapare din REF, pentru analiticele care nu intra in acont
|
||||
CORECTII = [
|
||||
["419.NETOPIA", "419", "2", None, "ca in REF"],
|
||||
["4428.TOATE", "4428", None, None, "ca in REF"],
|
||||
]
|
||||
|
||||
# frunza -> (REF, NOI), cu (totdeb, totcred); None = frunza lipseste
|
||||
DIFERENTE_ASTEPTATE = {
|
||||
# postarile direct pe sinteticul 401 intra pe net (regula stabilita), deci rulajul scade
|
||||
("401", ""): ((8991.55, 57253.22), (5536.75, 53798.42)),
|
||||
# 4092 e cont cu parteneri in config; REF l-a pus pe analitic si a pierdut 1563.37
|
||||
("4092", ""): (None, (4170.23, 0.0)),
|
||||
("4092", "1"): ((2606.86, 0.0), None),
|
||||
# nota de 900 direct pe 419: analitic de diferenta; REF a pierdut-o
|
||||
("419", "9999"): (None, (0.0, 900.0)),
|
||||
# 4428: postarile direct pe sintetic -> analitic de diferenta; totalul coincide
|
||||
("4428", ""): ((1749.68, 0.0), (373.14, 0.0)),
|
||||
("4428", "99"): (None, (1376.54, 0.0)),
|
||||
# 5121: regula de banca (BALANTA cu acont per cont bancar), validata la MASTER
|
||||
("5121", ""): ((40801.41, 35876.25), None),
|
||||
("5121", "1"): (None, (40601.41, 35876.25)),
|
||||
("5121", "2"): (None, (200.0, 0.0)),
|
||||
# 542 nu e cont cu parteneri in config: analitice + diferenta
|
||||
("542", ""): ((2243.57, 1824.07), None),
|
||||
("542", "01"): (None, (1828.77, 1824.07)),
|
||||
("542", "02"): (None, (124.74, 0.0)),
|
||||
("542", "99"): (None, (290.06, 0.0)),
|
||||
# 891 are rulaj zero; REF nu-l scrie
|
||||
("891", ""): (None, (0.0, 0.0)),
|
||||
}
|
||||
|
||||
|
||||
def _randuri(cale):
|
||||
ws = openpyxl.load_workbook(cale, data_only=True).worksheets[0]
|
||||
h = [c.value for c in ws[1]]
|
||||
return [d for d in (dict(zip(h, r)) for r in ws.iter_rows(min_row=2, values_only=True))
|
||||
if d.get("TIP") in ("BALANTA", "FACTURA")]
|
||||
|
||||
|
||||
def _v(x):
|
||||
return round(float(x or 0), 2)
|
||||
|
||||
|
||||
def _frunze(randuri):
|
||||
"""{(cont, acont): (totdeb, totcred)}. REF are pe 215/2815/4091 si sinteticul
|
||||
langa analitice, cu suma lor (dublura): acela se scoate, oricare altul ramane."""
|
||||
d = {}
|
||||
for r in randuri:
|
||||
if r["TIP"] == "BALANTA":
|
||||
d[(str(r["cont"]), str(r["acont"] or ""))] = (_v(r["totdeb"]), _v(r["totcred"]))
|
||||
suma = collections.defaultdict(lambda: [0.0, 0.0])
|
||||
for (c, a), (td, tc) in d.items():
|
||||
if a:
|
||||
suma[c][0] += td
|
||||
suma[c][1] += tc
|
||||
dubluri = {(c, "") for c, (td, tc) in suma.items()
|
||||
if d.get((c, "")) == (round(td, 2), round(tc, 2))}
|
||||
return {k: v for k, v in d.items() if k not in dubluri}
|
||||
|
||||
|
||||
@unittest.skipUnless(BAZA.exists() and REF.exists(), "lipsesc fisierele LACERTA din exemple/")
|
||||
class TestLacerta(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls._tmp = tempfile.TemporaryDirectory(prefix="lacerta_")
|
||||
d = pathlib.Path(cls._tmp.name)
|
||||
wb = openpyxl.Workbook()
|
||||
wb.active.append(mapare.COLOANE_CORECTII)
|
||||
for r in CORECTII:
|
||||
wb.active.append(r)
|
||||
wb.save(d / "corectii_LACERTA.xlsx")
|
||||
for cmd in (["extrage.py", "--fisier", str(BAZA), "--dir", str(d)],
|
||||
["genereaza.py", "--dir", str(d), "--dir-iesire", str(d)]):
|
||||
subprocess.run([sys.executable, str(RADACINA / cmd[0])] + cmd[1:] +
|
||||
["--firma", "LACERTA", "--an", str(AN), "--luna", str(LUNA)],
|
||||
check=True, capture_output=True, cwd=str(RADACINA))
|
||||
cls.noi = _randuri(d / ("init_LACERTA_%d_%d.xlsx" % (AN, LUNA)))
|
||||
cls.ref = _randuri(REF)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls._tmp.cleanup()
|
||||
|
||||
def test_frunze_balanta_ca_in_ref(self):
|
||||
ref, noi = _frunze(self.ref), _frunze(self.noi)
|
||||
gasite = {}
|
||||
for k in sorted(set(ref) | set(noi)):
|
||||
if ref.get(k) != noi.get(k):
|
||||
gasite[k] = (ref.get(k), noi.get(k))
|
||||
self.assertEqual(gasite, DIFERENTE_ASTEPTATE)
|
||||
|
||||
def test_echilibru(self):
|
||||
bal = [r for r in self.noi if r["TIP"] == "BALANTA"]
|
||||
self.assertAlmostEqual(sum(_v(r["totdeb"]) for r in bal),
|
||||
sum(_v(r["totcred"]) for r in bal), delta=0.005)
|
||||
|
||||
def test_facturi_per_document_cu_cod_fiscal_real(self):
|
||||
fac = [r for r in self.noi if r["TIP"] == "FACTURA" and str(r["cont"]) == "401"]
|
||||
# un rand per document nesoldat; suma = cea din REF
|
||||
self.assertEqual(len(fac), 12)
|
||||
self.assertAlmostEqual(sum(_v(r["sold"]) for r in fac), 9220.75, delta=0.005)
|
||||
provizorii = [r["cod_fiscal"] for r in fac if str(r["cod_fiscal"]).startswith("401.")]
|
||||
self.assertEqual(provizorii, [], "furnizor fara cod fiscal real")
|
||||
|
||||
def test_facturi_per_cont(self):
|
||||
c = collections.Counter()
|
||||
s = collections.defaultdict(float)
|
||||
for r in self.noi:
|
||||
if r["TIP"] == "FACTURA":
|
||||
c[str(r["cont"])] += 1
|
||||
s[str(r["cont"])] += _v(r["sold"])
|
||||
self.assertEqual({k: (c[k], round(s[k], 2)) for k in c}, {
|
||||
"401": (12, 9220.75), "4092": (1, 2606.86), "4111": (1, 90.0),
|
||||
"4551": (3, 3023583.0), "461": (1, 2250.0), "5121": (2, 4925.16)})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user