Files
solduri2roa/citire_fdb.py
Marius Mutu 6b37d370d7 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
2026-09-21 16:13:30 +03:00

317 lines
11 KiB
Python

"""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()