Lane integrare: extrage.py si genereaza.py cu parametri, maparea legata

Firmele si lunile nu mai sunt hardcodate. extrage.py dispecerizeaza catre cele
trei cititoare (pdf / xlsx / fdb, dedus din extensie, extensie necunoscuta =
eroare) si scrie contractul intern; genereaza.py aplica maparea si scrie
init_<FIRMA>_<an>_<luna>.xlsx plus mapare_<FIRMA>.xlsx.

genereaza_xlsx.py devine biblioteca: an/luna parametri, data si scadenta
calculate din ele, sablonul cautat langa script. Cand exista parteneri_ si
facturi_ (drumul FDB) se emite cate un FACTURA per document, cu cod fiscal real,
si numai daca suma documentelor da netul contului; altfel ramane comportamentul
de azi, un FACTURA agregat.

Corectat in orchestrare: la redenumirea unui cont cu parteneri, generatorul lua
primul analitic ca sa afle contul sintetic. Daca analiticele erau mapate pe
conturi diferite, BALANTA agregat si randurile FACTURA cadeau pe conturi
diferite, tacut. Acum se opreste cu eroare; colapsarea pe acelasi cont ramane
legala. Doua teste acopera ambele cazuri.

29 de teste trec, inclusiv regresia pe etalon cu generatorul modificat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYeAtVxeS8m4oXekjX8Am2
This commit is contained in:
2026-09-21 16:23:45 +03:00
parent 6b37d370d7
commit 82511c8adb
5 changed files with 630 additions and 70 deletions

View File

@@ -1,9 +1,9 @@
"""Genereaza fisierele xlsx de initializare ROACONT din balantele SAGA (CSV).
"""Generatorul xlsx-urilor de initializare ROACONT din balantele SAGA (CSV).
Produce (pentru fiecare firma):
init_<FIRMA>_2025_12.xlsx - un sheet, randul 1 = capul de tabel,
datele de la randul 2 in jos
docs/verificare_<FIRMA>.md - verificarile obligatorii
Biblioteca folosita de `genereaza.py` (parametri de linie de comanda); produce:
init_<FIRMA>_<an>_<luna>.xlsx - un sheet, randul 1 = capul de tabel,
datele de la randul 2 in jos
verificare_<FIRMA>.md - verificarile obligatorii
Reguli: docs/decizii_import.md (pct. 11-15) si docs/conturi_cu_analitice.md.
@@ -26,17 +26,18 @@ Forma randurilor (revizuit 20.09.2026, vezi pct. 14-15):
import csv
import os
from calendar import monthrange
from openpyxl import load_workbook
AN = 2025
LUNA = 12
DATA = "01.12.2025"
DATASCAD = "31.12.2025"
SERIE = None
VALUTA = None
TEMPLATE = "init_facturi_balanta_note.xlsx"
# Sablonul sta langa script, nu langa directorul de lucru: iesirile pot merge in temp.
TEMPLATE = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"init_facturi_balanta_note.xlsx")
# Latura pe care formularul pune randurile FACTURA (frm...sc2:379/'383'):
# conturile din lista merg pe CREDIT, tot restul pe DEBIT.
@@ -78,11 +79,6 @@ def citeste_conturi_parteneri(path=CONFIG_PARTENERI):
PARTENER = PARTENER_BANCA | PARTENER_SINTETIC | PARTENER_FARA_ANALITICE
FIRME = [
("FUNDATIA", "balanta_FUNDATIA.csv"),
("MASTER", "balanta_MASTERJOB.csv"),
]
def citeste_header():
wb = load_workbook(TEMPLATE, read_only=True)
@@ -170,30 +166,81 @@ def acont_diferenta(parent, kids):
return ac
def rand(header, **kw):
def _data(an, luna):
"""Prima zi a lunii, in formatul din sablon (DD.MM.YYYY)."""
return "01.%02d.%04d" % (int(luna), int(an))
def _datascad(an, luna):
"""Ultima zi a lunii, in formatul din sablon (DD.MM.YYYY)."""
return "%02d.%02d.%04d" % (monthrange(int(an), int(luna))[1],
int(luna), int(an))
def rand(header, an, luna, **kw):
r = {h: None for h in header}
r.update(an=AN, luna=LUNA, data=DATA, datascad=DATASCAD,
r.update(an=an, luna=luna, data=_data(an, luna), datascad=_datascad(an, luna),
serie=SERIE, valuta=VALUTA)
r.update(kw)
return r
def _mapare_cont(cont_saga, cont_def, acont_def, mapare, partener):
"""Cont/acont final: maparea suprascrie doar ce i s-a dat explicit.
La parteneri `acont` ramane al generatorului (o pre-completare generica ar
pune acont pe 401 -> chei diferite -> dublare tacuta, decizii_import.md pct. 16);
o corectie explicita de `acont` se aplica totusi, pe ambele randuri.
"""
if not mapare:
return cont_def, acont_def
m = mapare.get(cont_saga)
if m is None:
return cont_def, acont_def
cont = m.get("cont") or cont_def
if partener and m.get("acont") is None:
return cont, acont_def
return cont, m.get("acont")
def este_latura_formular(net, side):
return (net > 0) == (side == "D")
def genereaza(rows, header):
def _text(v):
return "" if v is None else str(v).strip()
def _iso(s):
"""'YYYY-MM-DD' -> 'DD.MM.YYYY'; altceva -> None."""
p = _text(s).split("-")
if len(p) == 3 and len(p[0]) == 4:
return "%s.%s.%s" % (p[2], p[1], p[0])
return None
def genereaza(rows, header, an=AN, luna=LUNA, mapare=None, parteneri=None,
facturi=None):
by, children = arbore(rows)
diffs = diferente(by, children)
diff_by_parent = dict(diffs)
# verificarile de structura care nu se ghicesc
# parteneri/facturi reale (doar pe drumul FDB); lipsa = comportamentul de azi
part_map = {}
for r in (parteneri or []):
part_map[_text(r.get("cont_analitic"))] = r
fact_map = {}
for r in (facturi or []):
fact_map.setdefault(_text(r.get("cont_analitic")), []).append(r)
# verificarile de structura care nu se ghicesc (pe valorile finale, dupa mapare)
for cont in by:
if este_partener(cont):
continue
if cont not in children:
cont4, ac = split_cont(cont)
if len(cont4) > 4 or len(ac) > 4:
cont4, ac = _mapare_cont(cont, cont4, ac, mapare, partener=False)
if len(cont4) > 4 or len(ac or "") > 4:
raise Stop(f"cont/acont prea lung: {cont} -> ({cont4},{ac})")
for parent in diff_by_parent:
if not este_partener(parent) or parent.split(".")[0] in PARTENER_BANCA:
@@ -214,27 +261,64 @@ def genereaza(rows, header):
leaf_map.setdefault(leaf, []).append(len(out) - 1)
leaf_rep.add(leaf)
def factura(cod_saga, cont_out, acont, nume_def, side, net, sold, leaf):
"""Un FACTURA agregat (azi) sau, cand exista documente reale, unul per factura."""
info = part_map.get(cod_saga) or {}
cod_fisc = _text(info.get("cod_fiscal")) or cod_saga
nume = nume_def or _text(info.get("denumire")) or ("FURNIZOR %s" % cod_saga)
docs = fact_map.get(cod_saga)
if docs and sold > 0 and este_latura_formular(net, side):
total = round(sum(float(d.get("sold") or 0) for d in docs), 2)
if abs(total - abs(net)) <= 0.005:
primul = True
for d in docs:
v = round(float(d.get("sold") or 0), 2)
if v == 0:
continue
r = rand(header, an, luna, TIP="FACTURA", cont=cont_out,
acont=acont, numar=_text(d.get("numar")), nume=nume,
cod_fiscal=cod_fisc, sold=v, explicatia=side)
data, scad = _iso(d.get("data")), _iso(d.get("scadent"))
if data:
r["data"] = data
if scad:
r["datascad"] = scad
if primul:
add(r, leaf=leaf)
primul = False
else:
add(r)
if leaf is not None:
leaf_rep.add(leaf)
if not primul:
return
add(rand(header, an, luna, TIP="FACTURA", cont=cont_out, acont=acont,
numar=cod_saga.replace(".", ""), nume=nume, cod_fiscal=cod_fisc,
sold=sold, explicatia=side), leaf=leaf)
# A. conturi nepartener: cate un BALANTA per frunza
for r in rows:
if este_partener(r["cont"]) or r["cont"] in children:
continue
cont4, ac = split_cont(r["cont"])
add(rand(header, TIP="BALANTA", cont=cont4, acont=ac, numar=1,
cont4, ac = _mapare_cont(r["cont"], cont4, ac, mapare, partener=False)
add(rand(header, an, luna, TIP="BALANTA", cont=cont4, acont=ac, numar=1,
nume=r["denumire"], totdeb=round(r["total_d"], 2),
totcred=round(r["total_c"], 2)), leaf=r["cont"])
# A. analitic de diferenta pe parinte nepartener (postare directa pe sintetic)
# A. analitic de diferenta pe parinte nepartener (postare directa pe sintetic).
# Randurile de diferenta raman calculate aici, nu trec prin mapare.
for cont, d in diffs:
if este_partener(cont):
continue
ac = acont_diferenta(cont, children[cont])
add(rand(header, TIP="BALANTA", cont=cont.split(".")[0], acont=ac,
add(rand(header, an, luna, TIP="BALANTA", cont=cont.split(".")[0], acont=ac,
numar=1, nume="DIFERENTA SINTETIC SAGA",
totdeb=d if d > 0 else None,
totcred=-d if d < 0 else None))
parteneri = sorted({c.split(".")[0] for c in by if este_partener(c)})
for p in parteneri:
parteneri_activi = sorted({c.split(".")[0] for c in by if este_partener(c)})
for p in parteneri_activi:
direct = children.get(p, [])
side = latura_factura(p)
@@ -242,7 +326,7 @@ def genereaza(rows, header):
# 5121/5124: BALANTA cu acont + FACTURA cu ACELASI acont.
for k in direct:
kd = by[k]
kac = split_cont(k)[1]
cont_k, kac = _mapare_cont(k, p, split_cont(k)[1], mapare, True)
net = round(kd["total_d"] - kd["total_c"], 2)
if net != 0 and not este_latura_formular(net, side):
neg = -abs(net)
@@ -253,19 +337,17 @@ def genereaza(rows, header):
td, tc = None, neg
else:
td, tc = round(kd["total_d"], 2), round(kd["total_c"], 2)
add(rand(header, TIP="BALANTA", cont=p, acont=kac, numar=1,
nume=kd["denumire"], totdeb=td, totcred=tc), leaf=k)
add(rand(header, an, luna, TIP="BALANTA", cont=cont_k, acont=kac,
numar=1, nume=kd["denumire"], totdeb=td, totcred=tc),
leaf=k)
if net == 0:
continue
sold = abs(net) if este_latura_formular(net, side) else -abs(net)
add(rand(header, TIP="FACTURA", cont=p, acont=kac,
numar=k.replace(".", ""),
nume=kd["denumire"] or f"FURNIZOR {k}",
cod_fiscal=k, sold=sold, explicatia=side))
factura(k, cont_k, kac, kd["denumire"], side, net, sold, None)
if p in diff_by_parent:
d = diff_by_parent[p]
ac = acont_diferenta(p, direct)
add(rand(header, TIP="BALANTA", cont=p, acont=ac, numar=1,
add(rand(header, an, luna, TIP="BALANTA", cont=p, acont=ac, numar=1,
nume="DIFERENTA SINTETIC SAGA",
totdeb=d if d > 0 else None,
totcred=-d if d < 0 else None))
@@ -288,8 +370,20 @@ def genereaza(rows, header):
base_d += by[k]["total_d"]
base_c += by[k]["total_c"]
d = diff_by_parent.get(p, 0.0)
add(rand(header, TIP="BALANTA", cont=p, acont=None, numar=1,
nume=by[p]["denumire"],
cont_p, ac_p = _mapare_cont(p, p, None, mapare, True)
if cont_p == p: # .TOATE/.RESTUL poate redenumi fara rand pe sintetic
# BALANTA agregat si randurile FACTURA trebuie sa cada pe acelasi
# cont. Daca analiticele sunt mapate pe conturi diferite, agregatul
# nu mai corespunde detaliului: oprim, nu luam primul analitic.
variante = {_mapare_cont(k, p, None, mapare, True)[0] for k in direct}
if len(variante) > 1:
raise Stop(f"analiticele lui {p} sunt mapate pe conturi diferite "
f"({sorted(variante)}); BALANTA agregat nu mai poate "
f"corespunde randurilor FACTURA")
if variante:
cont_p = variante.pop()
add(rand(header, an, luna, TIP="BALANTA", cont=cont_p, acont=ac_p,
numar=1, nume=by[p]["denumire"],
totdeb=round(base_d + (d if d > 0 else 0.0), 2),
totcred=round(base_c + (-d if d < 0 else 0.0), 2)))
for k in direct:
@@ -298,11 +392,8 @@ def genereaza(rows, header):
leaf_rep.add(k)
continue
sold = abs(net) if este_latura_formular(net, side) else -abs(net)
add(rand(header, TIP="FACTURA", cont=p, acont=None,
numar=k.replace(".", ""),
nume=by[k]["denumire"] or f"FURNIZOR {k}",
cod_fiscal=k, sold=sold, explicatia=side),
leaf=k)
cont_k, kac = _mapare_cont(k, cont_p, ac_p, mapare, True)
factura(k, cont_k, kac, by[k]["denumire"], side, net, sold, k)
else:
net = round(by[p]["total_d"] - by[p]["total_c"], 2)
if net != 0 and not este_latura_formular(net, side):
@@ -315,14 +406,14 @@ def genereaza(rows, header):
else:
td = round(by[p]["total_d"], 2)
tc = round(by[p]["total_c"], 2)
add(rand(header, TIP="BALANTA", cont=p, acont=None, numar=1,
nume=by[p]["denumire"], totdeb=td, totcred=tc), leaf=p)
cont_p, ac_p = _mapare_cont(p, p, None, mapare, True)
add(rand(header, an, luna, TIP="BALANTA", cont=cont_p, acont=ac_p,
numar=1, nume=by[p]["denumire"], totdeb=td, totcred=tc),
leaf=p)
if net != 0:
nume = "CENTRALA" if p == "5311" else by[p]["denumire"]
sold = abs(net) if este_latura_formular(net, side) else -abs(net)
add(rand(header, TIP="FACTURA", cont=p, acont=None,
numar=p, nume=nume, cod_fiscal=p, sold=sold,
explicatia=side))
factura(p, cont_p, ac_p, nume, side, net, sold, None)
return out, diffs, leaf_map, leaf_rep, negative
@@ -341,7 +432,7 @@ def scrie_xlsx(header, rows, path):
def verifica(firma, csv_path, xlsx_path, rows, out, diffs, leaf_map, leaf_rep,
negative, header):
negative, header, dir_verificare="docs"):
by, children = arbore(rows)
leaves = [c for c in by if c not in children]
diff_by_parent = dict(diffs)
@@ -488,7 +579,9 @@ def verifica(firma, csv_path, xlsx_path, rows, out, diffs, leaf_map, leaf_rep,
"control intern si este goala in xlsx-ul final (capul de tabel o pastreaza).")
L.append("")
with open(os.path.join("docs", f"verificare_{firma}.md"), "w",
if dir_verificare:
os.makedirs(dir_verificare, exist_ok=True)
with open(os.path.join(dir_verificare, f"verificare_{firma}.md"), "w",
encoding="utf-8") as f:
f.write("\n".join(L))
@@ -499,29 +592,4 @@ def verifica(firma, csv_path, xlsx_path, rows, out, diffs, leaf_map, leaf_rep,
"part_verif": part_verif, "negative": negative}
def main():
header = citeste_header()
assert len(header) == 39, f"cap de tabel neasteptat: {len(header)} coloane"
for firma, csv_path in FIRME:
rows = citeste_balanta(csv_path)
out, diffs, leaf_map, leaf_rep, negative = genereaza(rows, header)
xlsx_path = f"init_{firma}_2025_12.xlsx"
scrie_xlsx(header, out, xlsx_path)
rez = verifica(firma, csv_path, xlsx_path, rows, out, diffs, leaf_map,
leaf_rep, negative, header)
print(f"=== {firma} ===")
print(f" rows: {len(out)} totdeb={rez['totdeb']} totcred={rez['totcred']} net={rez['net']}")
print(f" balanta={'OK' if rez['bal'] else 'ESEC'} "
f"frunze={'OK' if rez['leaves'] else 'ESEC'} "
f"parteneri={'OK' if rez['part'] else 'ESEC'} "
f"lungimi={'OK' if rez['lung'] else 'ESEC'}")
print(f" scris {xlsx_path} si docs/verificare_{firma}.md")
if rez["negative"]:
print(f" analitice cu semn negativ: {rez['negative']}")
if rez["pierdute"] or rez["dubluri_leaf"] or rez["dubluri_bal"] or rez["lungimi"]:
print(" ATENTIE:", rez["pierdute"], rez["dubluri_leaf"],
rez["dubluri_bal"], rez["lungimi"])
if __name__ == "__main__":
main()