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:
2026-09-25 16:14:14 +03:00
parent 3499a0946c
commit 99d9cf0aff
8 changed files with 453 additions and 44 deletions

View File

@@ -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,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),