diff --git a/.gitignore b/.gitignore index 8140584..eef042a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd8907f --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# solduri2roa + +Transforma soldurile unei firme din SAGA in xlsx-ul de initializare pentru ROACONT. + +## Pe scurt + +``` +py extrage.py --fisier --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__.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_.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). diff --git a/citire_fdb.py b/citire_fdb.py index 6b29534..15eb343 100644 --- a/citire_fdb.py +++ b/citire_fdb.py @@ -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 "" "" [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,28 +165,33 @@ 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, - "a_c": 0, "l_d": 0, "l_c": 0}) - if latura == "D": - s["i_d"] += init or 0 - s["a_d"] += an_prec or 0 - s["l_d"] += luna_v or 0 - else: - s["i_c"] += init or 0 - s["a_c"] += an_prec or 0 - s["l_c"] += luna_v or 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 + s["a_d"] += an_prec or 0 + s["l_d"] += luna_v or 0 + else: + s["i_c"] += init or 0 + s["a_c"] += an_prec or 0 + s["l_c"] += luna_v or 0 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), diff --git a/docs/analiza_lacerta_fdb.md b/docs/analiza_lacerta_fdb.md new file mode 100644 index 0000000..14486b8 --- /dev/null +++ b/docs/analiza_lacerta_fdb.md @@ -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 .FDB -user SYSDBA -password masterkey # vezi constatarea 1 +py extrage.py --fisier .FDB --firma LACERTA --an 2026 --luna 6 --dir +py genereaza.py --firma LACERTA --an 2026 --luna 6 --dir --dir-iesire \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. diff --git a/docs/handoff_etapa2_convertor.md b/docs/handoff_etapa2_convertor.md index d771891..22ea366 100644 --- a/docs/handoff_etapa2_convertor.md +++ b/docs/handoff_etapa2_convertor.md @@ -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 diff --git a/docs/procedura_copie_fdb_client.md b/docs/procedura_copie_fdb_client.md index 7c1a29c..05dc106 100644 --- a/docs/procedura_copie_fdb_client.md +++ b/docs/procedura_copie_fdb_client.md @@ -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. diff --git a/tests/test_citire_fdb.py b/tests/test_citire_fdb.py index b11562c..54b0da0 100644 --- a/tests/test_citire_fdb.py +++ b/tests/test_citire_fdb.py @@ -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") diff --git a/tests/test_lacerta.py b/tests/test_lacerta.py new file mode 100644 index 0000000..168286f --- /dev/null +++ b/tests/test_lacerta.py @@ -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()