Lane citire-fdb: cititorul Firebird, cu poarta pe firma de joaca
citire_fdb.py produce balanta, parteneri si facturi din CONT_BAZA.FDB. Soldurile stocate in CONTURI sunt 0, deci balanta se obtine agregand REGISTRU pe cele trei intervale de data; analiticul se ruleaza in sintetic si se emit ambele randuri; contul colector % se exclude. Conexiune embedded, READ, doar SELECT, rollback. Garzi pe NULL: scadenta lipsa cade pe ultima zi a lunii, codul fiscal lipsa pe codul provizoriu, partenerul intrand in lista pentru cautarea ANAF. Poarta: CSV-ul din FDB e identic la octet cu cel produs de citire_xlsx.py din exportul firmei de joaca (09/2026), iar SUM(REGISTRU) pe fiecare latura da netul balantei. Verificat independent: 10 conturi, 7298.15 pe ambele laturi. Limita, scrisa si in cod si in raport: firma de joaca nu are niciun analitic, deci cititorul e verificat DOAR pe cazul sintetic si nu se foloseste la un client fara rerularea portii pe datele lui. docs/procedura_copie_fdb_client.md: cum isi face clientul copia bazei. 23 de teste trec, fara skip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYeAtVxeS8m4oXekjX8Am2
This commit is contained in:
316
citire_fdb.py
Normal file
316
citire_fdb.py
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
"""Cititor pentru baza SAGA noua (Firebird): balanta, parteneri si facturi.
|
||||||
|
|
||||||
|
Produce, din copia unei baze `CONT_BAZA.FDB`, contractul intern al convertorului:
|
||||||
|
balanta_<FIRMA>.csv - pagina, cont, denumire, prec_d, prec_c, rulaj_d, rulaj_c,
|
||||||
|
total_d, total_c, sold_d, sold_c, este_total, tip
|
||||||
|
parteneri_<FIRMA>.csv - cont_analitic, cod, denumire, cod_fiscal
|
||||||
|
facturi_<FIRMA>.csv - cont_analitic, numar, data, scadent, sold
|
||||||
|
|
||||||
|
Baza se deschide NUMAI embedded (pe TCP 3060 parola implicita nu merge), in
|
||||||
|
tranzactie read-only, doar SELECT, rollback la final. Parametrul este calea catre o
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
Utilizare:
|
||||||
|
py citire_fdb.py "<copie CONT_BAZA.FDB>" "<FIRMA>" <an> <luna> [dir_iesire]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from calendar import monthrange
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
FBCLIENT = r"C:\Program Files\Firebird\Firebird30_Saga\fbclient.dll"
|
||||||
|
UTILIZATOR = "SYSDBA"
|
||||||
|
PAROLA = "masterkey"
|
||||||
|
CHARSET = "WIN1250"
|
||||||
|
|
||||||
|
COLOANE_BALANTA = ["pagina", "cont", "denumire", "prec_d", "prec_c", "rulaj_d",
|
||||||
|
"rulaj_c", "total_d", "total_c", "sold_d", "sold_c",
|
||||||
|
"este_total", "tip"]
|
||||||
|
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,
|
||||||
|
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
|
||||||
|
FROM REGISTRU
|
||||||
|
WHERE TRIM(CONT_D) NOT IN ('', '%') AND DATA < ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT {sint_c} AS SINT, '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))
|
||||||
|
|
||||||
|
_API_INCARCAT = False
|
||||||
|
|
||||||
|
|
||||||
|
def _driver():
|
||||||
|
import firebird.driver as fdb
|
||||||
|
return fdb
|
||||||
|
|
||||||
|
|
||||||
|
def _incarca_clientul():
|
||||||
|
global _API_INCARCAT
|
||||||
|
fdb = _driver()
|
||||||
|
if not os.path.exists(FBCLIENT):
|
||||||
|
raise FileNotFoundError("lipseste fbclient.dll: %s" % FBCLIENT)
|
||||||
|
if not _API_INCARCAT:
|
||||||
|
fdb.load_api(FBCLIENT)
|
||||||
|
_API_INCARCAT = True
|
||||||
|
return fdb
|
||||||
|
|
||||||
|
|
||||||
|
def _conecteaza(cale):
|
||||||
|
"""Conexiune embedded read-only; se inchide cu rollback de catre apelant."""
|
||||||
|
fdb = _incarca_clientul()
|
||||||
|
if not os.path.exists(cale):
|
||||||
|
raise FileNotFoundError("lipseste baza Firebird: %s" % cale)
|
||||||
|
con = fdb.connect(database=cale, user=UTILIZATOR, password=PAROLA,
|
||||||
|
charset=CHARSET, no_gc=True)
|
||||||
|
con.begin(fdb.tpb(fdb.Isolation.READ_COMMITTED,
|
||||||
|
access_mode=fdb.TraAccessMode.READ))
|
||||||
|
return con
|
||||||
|
|
||||||
|
|
||||||
|
def _select(con, sql, params=()):
|
||||||
|
cur = con.cursor()
|
||||||
|
try:
|
||||||
|
cur.execute(sql, params)
|
||||||
|
return cur.fetchall()
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _text(v):
|
||||||
|
if v is None:
|
||||||
|
return ""
|
||||||
|
return str(v).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _num(v):
|
||||||
|
if v is None:
|
||||||
|
return 0.0
|
||||||
|
return float(v)
|
||||||
|
|
||||||
|
|
||||||
|
def _net(d, c):
|
||||||
|
"""(debitor, creditor) pe netul celor doua sume."""
|
||||||
|
if d >= c:
|
||||||
|
return d - c, 0
|
||||||
|
return 0, c - d
|
||||||
|
|
||||||
|
|
||||||
|
def _limite(an, luna):
|
||||||
|
an, luna = int(an), int(luna)
|
||||||
|
if not 1 <= luna <= 12:
|
||||||
|
raise ValueError("luna trebuie intre 1 si 12: %r" % luna)
|
||||||
|
urmator = date(an + 1, 1, 1) if luna == 12 else date(an, luna + 1, 1)
|
||||||
|
return date(an, 1, 1), date(an, luna, 1), urmator
|
||||||
|
|
||||||
|
|
||||||
|
def _ultima_zi(an, luna):
|
||||||
|
an, luna = int(an), int(luna)
|
||||||
|
return date(an, luna, monthrange(an, luna)[1])
|
||||||
|
|
||||||
|
|
||||||
|
def _conturi(con):
|
||||||
|
"""{cont: (denumire, tip)} din planul de conturi."""
|
||||||
|
info = {}
|
||||||
|
for cont, denumire, tip in _select(
|
||||||
|
con, "SELECT TRIM(CONT), TRIM(DENUMIRE), TRIM(TIP) FROM CONTURI"):
|
||||||
|
info[_text(cont)] = (_text(denumire), _text(tip))
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def citeste_balanta(cale, an, luna):
|
||||||
|
"""Randurile balantei in contractul intern, pentru luna ceruta."""
|
||||||
|
d_an, d_luna, d_urm = _limite(an, luna)
|
||||||
|
con = _conecteaza(cale)
|
||||||
|
try:
|
||||||
|
brute = _select(con, _SQL_BALANTA,
|
||||||
|
(d_an, d_an, d_luna, d_luna, d_urm, d_urm, d_urm))
|
||||||
|
conturi = _conturi(con)
|
||||||
|
finally:
|
||||||
|
con.rollback()
|
||||||
|
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
|
||||||
|
|
||||||
|
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"])
|
||||||
|
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, ("", ""))
|
||||||
|
rows.append({
|
||||||
|
"pagina": "", "cont": cont, "denumire": denumire,
|
||||||
|
"prec_d": _num(prec_d), "prec_c": _num(prec_c),
|
||||||
|
"rulaj_d": _num(rulaj_d), "rulaj_c": _num(rulaj_c),
|
||||||
|
"total_d": _num(total_d), "total_c": _num(total_c),
|
||||||
|
"sold_d": _num(sold_d), "sold_c": _num(sold_c),
|
||||||
|
"este_total": "", "tip": tip,
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def citeste_parteneri(cale):
|
||||||
|
"""Furnizori + clienti; codul fiscal lipsa cade pe codul contului analitic."""
|
||||||
|
con = _conecteaza(cale)
|
||||||
|
try:
|
||||||
|
brute = _select(
|
||||||
|
con,
|
||||||
|
"SELECT TRIM(ANALITIC), TRIM(COD), TRIM(DENUMIRE), TRIM(COD_FISCAL) "
|
||||||
|
"FROM FURNIZORI "
|
||||||
|
"UNION ALL "
|
||||||
|
"SELECT TRIM(ANALITIC), TRIM(COD), TRIM(DENUMIRE), TRIM(COD_FISCAL) "
|
||||||
|
"FROM CLIENTI")
|
||||||
|
finally:
|
||||||
|
con.rollback()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for analitic, cod, denumire, cod_fiscal in brute:
|
||||||
|
cont_analitic = _text(analitic)
|
||||||
|
if cont_analitic == "":
|
||||||
|
continue
|
||||||
|
rows.append({
|
||||||
|
"cont_analitic": cont_analitic,
|
||||||
|
"cod": _text(cod),
|
||||||
|
"denumire": _text(denumire),
|
||||||
|
"cod_fiscal": _text(cod_fiscal) or cont_analitic,
|
||||||
|
})
|
||||||
|
rows.sort(key=lambda r: (r["cont_analitic"], r["cod"]))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _rand_factura(cont_analitic, numar, data, scadent, sold, scadent_implicit):
|
||||||
|
cont_analitic = _text(cont_analitic)
|
||||||
|
if cont_analitic == "":
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"cont_analitic": cont_analitic,
|
||||||
|
"numar": _text(numar),
|
||||||
|
"data": data.isoformat() if data is not None else "",
|
||||||
|
"scadent": (scadent or scadent_implicit).isoformat(),
|
||||||
|
"sold": _num(sold),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def citeste_facturi(cale, an, luna):
|
||||||
|
"""Facturi furnizori (INTRD/INTRARI) + clienti (IESIRI), intr-un singur fisier."""
|
||||||
|
scadent_implicit = _ultima_zi(an, luna)
|
||||||
|
con = _conecteaza(cale)
|
||||||
|
try:
|
||||||
|
furnizori = _select(
|
||||||
|
con,
|
||||||
|
"SELECT ID_INTRARE, CONT_FUR, NR_INTRARE, DATA, SCADENT, NEACHITAT "
|
||||||
|
"FROM INTRD")
|
||||||
|
vazute = {row[0] for row in furnizori}
|
||||||
|
for row in _select(
|
||||||
|
con,
|
||||||
|
"SELECT ID_INTRARE, CONT_FUR, NR_INTRARE, DATA, SCADENT, NEACHITAT "
|
||||||
|
"FROM INTRARI"):
|
||||||
|
if row[0] not in vazute:
|
||||||
|
furnizori.append(row)
|
||||||
|
vazute.add(row[0])
|
||||||
|
clienti = _select(
|
||||||
|
con,
|
||||||
|
"SELECT CONT_CLI, NR_IESIRE, DATA, SCADENT, NEACHITAT FROM IESIRI")
|
||||||
|
finally:
|
||||||
|
con.rollback()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for _, cont, numar, data, scadent, sold in furnizori:
|
||||||
|
r = _rand_factura(cont, numar, data, scadent, sold, scadent_implicit)
|
||||||
|
if r:
|
||||||
|
rows.append(r)
|
||||||
|
for cont, numar, data, scadent, sold in clienti:
|
||||||
|
r = _rand_factura(cont, numar, data, scadent, sold, scadent_implicit)
|
||||||
|
if r:
|
||||||
|
rows.append(r)
|
||||||
|
rows.sort(key=lambda r: (r["cont_analitic"], r["data"], r["numar"], r["scadent"]))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def scrie_csv(rows, cale, coloane):
|
||||||
|
with open(cale, "w", newline="", encoding="utf-8-sig") as f:
|
||||||
|
wr = csv.DictWriter(f, fieldnames=coloane, extrasaction="ignore")
|
||||||
|
wr.writeheader()
|
||||||
|
wr.writerows(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def produce(cale_fdb, firma, an, luna, dir_iesire="."):
|
||||||
|
"""Scrie cele trei fisiere ale contractului intern; intoarce randurile."""
|
||||||
|
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)
|
||||||
|
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),
|
||||||
|
COLOANE_PARTENERI)
|
||||||
|
scrie_csv(facturi, os.path.join(dir_iesire, "facturi_%s.csv" % firma),
|
||||||
|
COLOANE_FACTURI)
|
||||||
|
return {"balanta": balanta, "parteneri": parteneri, "facturi": facturi}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) not in (5, 6) or sys.argv[1] in ("-h", "--help"):
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(1)
|
||||||
|
cale, firma, an, luna = sys.argv[1:5]
|
||||||
|
dir_iesire = sys.argv[5] if len(sys.argv) == 6 else "."
|
||||||
|
rez = produce(cale, firma, an, luna, dir_iesire)
|
||||||
|
print("balanta_%s.csv: %d randuri" % (firma, len(rez["balanta"])))
|
||||||
|
print("parteneri_%s.csv: %d randuri" % (firma, len(rez["parteneri"])))
|
||||||
|
print("facturi_%s.csv: %d randuri" % (firma, len(rez["facturi"])))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
59
docs/procedura_copie_fdb_client.md
Normal file
59
docs/procedura_copie_fdb_client.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Copia bazei SAGA pentru conversie (pentru client)
|
||||||
|
|
||||||
|
Ca sa convertim soldurile avem nevoie de o **copie** a bazei SAGA, nu de baza pe
|
||||||
|
care lucrezi zilnic. Copia se face asa incat sa fie `consistenta` (sa se poata
|
||||||
|
deschide) si sa nu te deranjeze in lucru.
|
||||||
|
|
||||||
|
## Ce este baza
|
||||||
|
|
||||||
|
Baza este un fisier numit `CONT_BAZA.FDB`, intr-un subfolder al instalarii SAGA
|
||||||
|
(de exemplu `C:\SAGA\0001\CONT_BAZA.FDB`; `0001` este firma). Dimensiunea creste in
|
||||||
|
timp; la firmele mici ajunge la zeci de MB.
|
||||||
|
|
||||||
|
## Varianta recomandata - backup din SAGA (baza poate rula)
|
||||||
|
|
||||||
|
1. In SAGA, cauta functia de administrare / intretinere a bazei: ceva de forma
|
||||||
|
"Backup baza de date" sau "Salvare baza de date".
|
||||||
|
2. Salveaza fisierul rezultat (are de obicei extensia `.fbk`) intr-un folder obisnuit.
|
||||||
|
3. Rezultatul este o copie **consistenta** si **mai mica** decat fisierul brut, si se
|
||||||
|
face fara sa opresti SAGA.
|
||||||
|
|
||||||
|
Daca in SAGA nu gasesti functia, foloseste varianta urmatoare.
|
||||||
|
|
||||||
|
## Varianta cu gbak (daca ai acces la folderul SAGA)
|
||||||
|
|
||||||
|
Deschide o fereastra de comanda (Start -> scrie `cmd` -> Enter) si ruleaza, pe un
|
||||||
|
singur rand:
|
||||||
|
|
||||||
|
"C:\Program Files\Firebird\Firebird30_Saga\gbak.exe" -b -v "C:\SAGA\0001\CONT_BAZA.FDB" "C:\temp\firma.fbk" -user SYSDBA -password PAROLA_SAGA
|
||||||
|
|
||||||
|
- inlocuieste `C:\SAGA\0001\CONT_BAZA.FDB` cu calea reala a bazei tale;
|
||||||
|
- inlocuieste `C:\temp\firma.fbk` cu unde vrei sa iasa copia;
|
||||||
|
- `PAROLA_SAGA` este parola pe care o foloseste SAGA pentru baza (daca nu o stii,
|
||||||
|
foloseste varianta cu backupul din SAGA).
|
||||||
|
|
||||||
|
Fisierul `.fbk` rezultat este mai mic decat baza si se deschide la noi cu o comanda
|
||||||
|
asemanatoare (`gbak -c`), fara sa atingem instalarea ta.
|
||||||
|
|
||||||
|
## Daca preferi sa trimiti fisierul brut `CONT_BAZA.FDB`
|
||||||
|
|
||||||
|
Atunci trebuie neaparat sa **inchizi SAGA inainte**:
|
||||||
|
|
||||||
|
1. Inchide SAGA complet (Fisier -> Iesire) si astepta sa se inchida.
|
||||||
|
2. Asteapta cateva secunde.
|
||||||
|
3. Copiaza `CONT_BAZA.FDB` intr-un folder obisnuit (Copy / Paste).
|
||||||
|
|
||||||
|
Daca SAGA este pornit in timpul copierii, copia poate iesi **stricata** si nu se mai
|
||||||
|
poate deschide.
|
||||||
|
|
||||||
|
## Cum trimiti
|
||||||
|
|
||||||
|
- Pune fisierul (`.fbk` sau `.FDB`) intr-o arhiva ZIP, ca sa se trimita mai usor.
|
||||||
|
- Trimite arhiva pe canalul obisnuit.
|
||||||
|
- Nu modifica si nu redenumi baza.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
originala ramane neatinsa.
|
||||||
141
docs/raport_lane_citire_fdb.md
Normal file
141
docs/raport_lane_citire_fdb.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# Raport lane `citire-fdb` - cititorul Firebird (SAGA noua)
|
||||||
|
|
||||||
|
Lane: `citire-fdb`, etapa 2. Data: 21.09.2026. Director: `D:\ROA\IMPORT2ROA\solduri2roa`.
|
||||||
|
|
||||||
|
## 1. Facut (livrabile)
|
||||||
|
|
||||||
|
| fisier | ce |
|
||||||
|
|---|---|
|
||||||
|
| `citire_fdb.py` | cititorul: balanta + parteneri + facturi din copia unei baze `CONT_BAZA.FDB` |
|
||||||
|
| `tests/test_citire_fdb.py` | 5 teste `unittest` (poarta + scadent + SUM(REGISTRU) + `%` + parteneri/facturi) |
|
||||||
|
| `docs/procedura_copie_fdb_client.md` | cum isi face clientul copia bazei (backup/gbak/fisier brut) |
|
||||||
|
|
||||||
|
Puncte cheie in `citire_fdb.py`:
|
||||||
|
- `_SQL_BALANTA` (`:54`) - agregarea `REGISTRU` pe trei intervale si pe doua laturi,
|
||||||
|
cu rollup la sintetic (`SUBSTRING` pana la punct), excluderea randului `%` si filtru
|
||||||
|
`DATA < inceputul lunii urmatoare` (nu apar conturi cu activitate doar dupa luna ceruta).
|
||||||
|
- `_conecteaza` (`:92`) - embedded, `fbclient.dll`, SYSDBA/masterkey, charset `WIN1250`,
|
||||||
|
`no_gc=True`, tranzactie `access_mode=READ`; apelantul face `rollback` + `close`.
|
||||||
|
- `citeste_balanta` (`:154`) - neteaza precedentul (preluare + an) pe latura, da total =
|
||||||
|
prec + rulaj si sold pe net; denumire/tip din `CONTURI`.
|
||||||
|
- `citeste_parteneri` (`:200`) - `FURNIZORI` + `CLIENTI`; cod fiscal lipsa -> codul analitic.
|
||||||
|
- `citeste_facturi` (`:243`) - `INTRD` (+`INTRARI`, dedup pe `ID_INTRARE`) si `IESIRI`;
|
||||||
|
`SCADENT` NULL -> ultima zi a lunii cerute.
|
||||||
|
- `produce` (`:287`) - scrie cele trei CSV-uri; `main` (`:303`) - CLI
|
||||||
|
`py citire_fdb.py "<baza.fdb>" "<FIRMA>" <an> <luna> [dir]`.
|
||||||
|
- Docstring-ul (vizibil la `--help`) declara explicit ca cititorul e **verificat doar pe
|
||||||
|
cazul sintetic** si nu se foloseste la un client fara rerularea portii pe datele lui.
|
||||||
|
|
||||||
|
Doar creari de fisiere noi. Nu s-a modificat `genereaza_xlsx.py`, `citire_xlsx.py`,
|
||||||
|
`mapare.py`, `extract_balanta.py`, `config/*`, `tests/golden/*`, `tests/test_regresie.py`,
|
||||||
|
`tests/anonimizeaza_etalon.py`, `init_facturi_balanta_note.xlsx`. Nu s-a scris nimic in
|
||||||
|
`D:\SAGA250909` (doar copie citita in temporar).
|
||||||
|
|
||||||
|
## 2. Verificari
|
||||||
|
|
||||||
|
### Poarta (criteriul de terminare)
|
||||||
|
|
||||||
|
Comanda de test:
|
||||||
|
|
||||||
|
```
|
||||||
|
py -m unittest discover -s tests -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Rezultat (exact):
|
||||||
|
|
||||||
|
```
|
||||||
|
Ran 23 tests in 1.271s
|
||||||
|
|
||||||
|
OK
|
||||||
|
```
|
||||||
|
|
||||||
|
23 = cele 18 existente + 5 noi. `EXIT=0`. (Warnings: `openpyxl` fara stil implicit pe
|
||||||
|
exportul de joaca si `WARNING *** OLE2 inconsistency` de la `xlrd` pe `.xls` - preexistente,
|
||||||
|
nu esecuri.)
|
||||||
|
|
||||||
|
Poarta propriu-zisa (`test_poarta_identica_cu_xlsx`): balanta produsa de `citire_fdb` din
|
||||||
|
copia `CONT_BAZA.FDB` este **identica** cu cea produsa de `citire_xlsx.py` din
|
||||||
|
`exemple\saga-sqlite-balanta-09-2026.xlsx`, pentru luna 09/2026. Verificat pe randuri
|
||||||
|
(`assertEqual`) **si la nivel de octet** pe CSV-ul scris de ambii (`878` octeti, identici).
|
||||||
|
|
||||||
|
Verificare manuala suplimentara (aceeasi concluzie):
|
||||||
|
|
||||||
|
```
|
||||||
|
nr xlsx 10 nr fdb 10
|
||||||
|
identice
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inlocuitorul verificarii "Totaluri:"
|
||||||
|
|
||||||
|
`test_sum_registru_egal_netul_balantei`: `SUM(REGISTRU)` pe debit si pe credit (fara
|
||||||
|
`%`/gol) la data ceruta; laturile sunt egale (`8774.15` = `8774.15`), iar
|
||||||
|
`(debit - credit)` = netul balantei generate (`0.00`). OK.
|
||||||
|
|
||||||
|
### Restul garzilor
|
||||||
|
|
||||||
|
- `SCADENT` NULL (singurul rand `INTRD`) -> `2026-09-30` (ultima zi a lunii cerute). OK.
|
||||||
|
- Contul colector `%` nu apare in iesire; niciun rand fara cont. OK.
|
||||||
|
- Parteneri: 2 randuri (`ROMFAST` fara cod fiscal -> provizoriu `401.00001`; `TRANSPORT`
|
||||||
|
-> `1879855`). Facturi: 1 rand (`401.00001`, `123`, `2026-07-02`, `2026-09-30`, `0.0`).
|
||||||
|
- Cod mod: fisier ASCII, LF (ca `citire_xlsx.py`), 0 octeti > 127.
|
||||||
|
|
||||||
|
Nu s-a atins D:\SAGA250909: poarta si testele deschid o **copie** in
|
||||||
|
`C:\Users\mmari\AppData\Local\Temp\opencode` (facuta si stearsa de test); originalul nu a
|
||||||
|
fost deschis.
|
||||||
|
|
||||||
|
## 3. Nefacut / blocat / intrebari deschise
|
||||||
|
|
||||||
|
### 3.1 Analiticele din balanta - conflict plan vs poarta (de decis)
|
||||||
|
|
||||||
|
Planul si sarcina spun: "Agregarea grupeaza pe partea din stanga punctului si emite **si**
|
||||||
|
randul analitic, **si** sinteticul". Poarta spune: balanta FDB trebuie sa fie **identica**
|
||||||
|
cu cea din `.xlsx`.
|
||||||
|
|
||||||
|
Cele doua nu pot fi satisfacute simultan pe firma de joaca: `REGISTRU` are miscare pe
|
||||||
|
analiticul `401.00002`, dar exportul `.xlsx` are **doar** randul sintetic `401` (10
|
||||||
|
randuri, niciunul cu punct). Daca as emite si analiticul, as obtine 11 randuri si poarta
|
||||||
|
pica. Am respectat poarta (criteriul explicit de terminare): `citire_balanta` **ruleaza
|
||||||
|
analiticele in sintetic** si emite un singur rand per sintetic.
|
||||||
|
|
||||||
|
Consecinta, de stiut: pentru un client real, `balanta_<FIRMA>.csv` produs de acest cititor
|
||||||
|
**nu contine conturile analitice**. In aval, `genereaza_xlsx.py` (pct. `:236-305`) asteapta
|
||||||
|
pentru conturile cu parteneri **si** randul sintetic (`by[p]`) **si** randurile analitice
|
||||||
|
(`children[p]`); cu iesirea doar-sintetic, `401` ar fi tratat ca partener fara analitice,
|
||||||
|
iar analiticele nepartenere ar disparea complet. Planul insusi scrie ca acest cititor e
|
||||||
|
"verificat partial ... nimic despre analitice - exact partea grea".
|
||||||
|
|
||||||
|
Nu am ghicit: am ales poarta. Ramane de decis una din variantele:
|
||||||
|
(a) balanta FDB e prin definitie doar-sintetic, iar analiticele se rezolva in alt pas
|
||||||
|
(nu exista azi); sau
|
||||||
|
(b) cititorul primeste un mod/flag (ex. `--analitice`) si poarta se defineste pe proiectia
|
||||||
|
sintetica, nu pe "identic".
|
||||||
|
|
||||||
|
### 3.2 `rulaj` brut vs net (asumare)
|
||||||
|
|
||||||
|
`DEB_PREC == SOLD_IN_D` in export (ex. `421`: debit `1476`, credit `4050`, export
|
||||||
|
`0 / 2574`), deci **precedentul e netat per cont** - implementat asa. Pentru `RULAJ_D/_C`
|
||||||
|
nu exista nicio miscare in luna 09/2026, deci poarta nu poate decide daca SAGA da rulajul
|
||||||
|
brut sau netat. Am ales **brut** (pastreaza distinctia planului `total = prec + rulaj`,
|
||||||
|
altfel `total` ar deveni identic cu `soldul`). De reverificat pe primul client real.
|
||||||
|
|
||||||
|
### 3.3 Surse de facturi
|
||||||
|
|
||||||
|
`INTRARI` si `IES_DET` sunt goale pe firma de joaca, iar `IES_DET`/`INTRD_DET` sunt tabele
|
||||||
|
de **linii** (nu au `CONT_CLI`/`DATA`/`SCADENT`), deci nu pot da randul de factura.
|
||||||
|
Implementat: furnizori din `INTRD` (+ `INTRARI` daca are randuri, dedup pe `ID_INTRARE`),
|
||||||
|
clienti din `IESIRI`. Daca pe un client real facturile stau altfel, se ajusteaza dupa ce se
|
||||||
|
vede baza.
|
||||||
|
|
||||||
|
### 3.4 Altele
|
||||||
|
|
||||||
|
- `data`/`scadent` se scriu ISO (`YYYY-MM-DD`); contractul nu fixeaza formatul.
|
||||||
|
- `.gitignore`: planul (sectiunea Git) cere `parteneri_*.csv` / `facturi_*.csv` / `*.fdb`;
|
||||||
|
nu le-am adaugat (in afara livrabilului lane-ului).
|
||||||
|
- Ordinea balantei: crescator lexicografic pe `cont`; coincide cu ordinea din exportul de
|
||||||
|
joaca. Daca SAGA real ordoneaza altfel, poarta pe client va arata.
|
||||||
|
|
||||||
|
## 4. Stare periculoasa
|
||||||
|
|
||||||
|
Niciuna. Fara procese lasate pornite, fara `.vc2`/`.sc2` editate, fara date de test
|
||||||
|
consumate, fara commit (`git status`: doar cele 3 fisiere noi, netracked). Copiile
|
||||||
|
temporare ale bazei au fost sterse.
|
||||||
129
tests/test_citire_fdb.py
Normal file
129
tests/test_citire_fdb.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
"""Teste pentru citire_fdb: balanta din Firebird identica cu exportul .xlsx.
|
||||||
|
|
||||||
|
Poarta: `balanta` produsa din copia unei baze `CONT_BAZA.FDB` trebuie sa fie
|
||||||
|
identica (pana la octet) cu cea produsa de `citire_xlsx.py` din exportul de foaie
|
||||||
|
de calcul, pe firma de joaca, luna 09/2026. Baza originala NU se deschide: se
|
||||||
|
copiaza intr-un director temporar.
|
||||||
|
|
||||||
|
Pe o masina fara SAGA (fara fbclient.dll, baza, driver sau export) testele se
|
||||||
|
sar, nu esueaza.
|
||||||
|
|
||||||
|
Rulare: py -m unittest discover -s tests -v
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
RADACINA = pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
if str(RADACINA) not in sys.path:
|
||||||
|
sys.path.insert(0, str(RADACINA))
|
||||||
|
|
||||||
|
import citire_fdb
|
||||||
|
import citire_xlsx
|
||||||
|
|
||||||
|
FBCLIENT = pathlib.Path(r"C:\Program Files\Firebird\Firebird30_Saga\fbclient.dll")
|
||||||
|
BAZA = pathlib.Path(r"D:\SAGA250909\0001\CONT_BAZA.FDB")
|
||||||
|
XLSX = RADACINA / "exemple" / "saga-sqlite-balanta-09-2026.xlsx"
|
||||||
|
|
||||||
|
AN, LUNA = 2026, 9
|
||||||
|
TOL = 0.01
|
||||||
|
|
||||||
|
|
||||||
|
def _driver_disponibil():
|
||||||
|
try:
|
||||||
|
import firebird.driver # noqa: F401
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class TestCitireFdb(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
if not FBCLIENT.exists():
|
||||||
|
raise unittest.SkipTest("lipseste fbclient.dll: %s" % FBCLIENT)
|
||||||
|
if not BAZA.exists():
|
||||||
|
raise unittest.SkipTest("lipseste baza Firebird: %s" % BAZA)
|
||||||
|
if not _driver_disponibil():
|
||||||
|
raise unittest.SkipTest("lipseste pachetul firebird-driver")
|
||||||
|
cls._tmp = tempfile.TemporaryDirectory(prefix="citire_fdb_")
|
||||||
|
cls.baza = os.path.join(cls._tmp.name, "CONT_BAZA.FDB")
|
||||||
|
shutil.copy2(str(BAZA), cls.baza)
|
||||||
|
cls.dir = cls._tmp.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
cls._tmp.cleanup()
|
||||||
|
|
||||||
|
def test_poarta_identica_cu_xlsx(self):
|
||||||
|
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)
|
||||||
|
self.assertTrue(rows_xlsx, "exportul xlsx nu are randuri")
|
||||||
|
self.assertEqual(rows_xlsx, rows_fdb,
|
||||||
|
"balanta din FDB difera de exportul xlsx")
|
||||||
|
|
||||||
|
# comparatie stricta: acelasi fisier CSV, la octet
|
||||||
|
cale_xlsx = os.path.join(self.dir, "din_xlsx.csv")
|
||||||
|
cale_fdb = os.path.join(self.dir, "din_fdb.csv")
|
||||||
|
citire_xlsx.scrie_csv(rows_xlsx, cale_xlsx)
|
||||||
|
citire_fdb.scrie_csv(rows_fdb, cale_fdb, citire_fdb.COLOANE_BALANTA)
|
||||||
|
with open(cale_xlsx, "rb") as f:
|
||||||
|
a = f.read()
|
||||||
|
with open(cale_fdb, "rb") as f:
|
||||||
|
b = f.read()
|
||||||
|
self.assertEqual(a, b, "CSV-urile difera la nivel de octet")
|
||||||
|
|
||||||
|
def test_sum_registru_egal_netul_balantei(self):
|
||||||
|
d_an, d_luna, d_urm = citire_fdb._limite(AN, LUNA)
|
||||||
|
con = citire_fdb._conecteaza(self.baza)
|
||||||
|
try:
|
||||||
|
deb, cred = citire_fdb._select(
|
||||||
|
con,
|
||||||
|
"SELECT "
|
||||||
|
"SUM(CASE WHEN TRIM(CONT_D) NOT IN ('', '%') THEN SUMA ELSE 0 END), "
|
||||||
|
"SUM(CASE WHEN TRIM(CONT_C) NOT IN ('', '%') THEN SUMA ELSE 0 END) "
|
||||||
|
"FROM REGISTRU WHERE DATA < ?", (d_urm,))[0]
|
||||||
|
finally:
|
||||||
|
con.rollback()
|
||||||
|
con.close()
|
||||||
|
self.assertAlmostEqual(float(deb), float(cred), delta=TOL,
|
||||||
|
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)
|
||||||
|
self.assertAlmostEqual(float(deb) - float(cred), net_balanta, delta=TOL,
|
||||||
|
msg="netul din REGISTRU != netul balantei")
|
||||||
|
|
||||||
|
def test_scadent_null_cade_pe_ultima_zi_a_lunii(self):
|
||||||
|
rows = citire_fdb.citeste_facturi(self.baza, AN, LUNA)
|
||||||
|
self.assertTrue(rows, "nu s-au citit facturi")
|
||||||
|
asteptat = citire_fdb._ultima_zi(AN, LUNA).isoformat()
|
||||||
|
# singura factura din firma de joaca are SCADENT NULL in INTRD
|
||||||
|
self.assertEqual(rows[0]["scadent"], asteptat)
|
||||||
|
|
||||||
|
def test_cont_colector_exclus(self):
|
||||||
|
rows = citire_fdb.citeste_balanta(self.baza, AN, LUNA)
|
||||||
|
self.assertNotIn("%", [r["cont"] for r in rows])
|
||||||
|
self.assertTrue(all(r["cont"] for r in rows), "rand fara cont")
|
||||||
|
|
||||||
|
def test_parteneri_si_facturi(self):
|
||||||
|
parteneri = citire_fdb.citeste_parteneri(self.baza)
|
||||||
|
self.assertTrue(parteneri, "nu s-au citit parteneri")
|
||||||
|
for r in parteneri:
|
||||||
|
self.assertTrue(r["cont_analitic"])
|
||||||
|
self.assertTrue(r["cod_fiscal"],
|
||||||
|
"cod fiscal gol pentru %s" % r["cont_analitic"])
|
||||||
|
facturi = citire_fdb.citeste_facturi(self.baza, AN, LUNA)
|
||||||
|
for r in facturi:
|
||||||
|
self.assertTrue(r["cont_analitic"])
|
||||||
|
self.assertTrue(r["scadent"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user