Files
solduri2roa/genereaza_xlsx.py
Marius Mutu 82511c8adb 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
2026-09-21 16:23:45 +03:00

596 lines
25 KiB
Python

"""Generatorul xlsx-urilor de initializare ROACONT din balantele SAGA (CSV).
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.
Forma randurilor (revizuit 20.09.2026, vezi pct. 14-15):
- cont obisnuit: doar BALANTA per frunza (acont completat).
- 401/4092/4111/461/462/4551: un BALANTA agregat pe sintetic (acont gol) + cate
un FACTURA per analitic (acont gol).
- 5121/5124: analiticele raman analitice; per analitic un BALANTA cu acont SI un
FACTURA cu ACELASI acont.
- 5311: BALANTA + FACTURA pe sintetic (acont gol), partener CENTRALA.
- 5125: cont obisnuit cu analitice (fara parteneri).
- diferenta neta parinte/copii (postare directa pe sintetic): la nepartener si la
5121/5124 devine rand BALANTA de diferenta (analitic nou, fara FACTURA); la
ceilalti parteneri se include in BALANTA agregat. NU se mai emite FACTURA de
diferenta (o rezolva formularul, prin NEREPARTIZAT).
- analiticul cu sold pe latura opusa celei fixate de formular (':379') primeste
FACTURA, cu suma scrisa cu semn negativ pe latura formularului (atat in BALANTA
cat si in sold-ul FACTURA); soldul ramane pe latura corecta si nu apare NEREPARTIZAT.
"""
import csv
import os
from calendar import monthrange
from openpyxl import load_workbook
AN = 2025
LUNA = 12
SERIE = None
VALUTA = None
# 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.
LATURA_CREDIT = {"401", "404", "462", "419", "408", "4551"}
# Setul operational de conturi cu parteneri: config/conturi_parteneri.csv (validat
# in productie; CONFIG_CONT_IREG e doar consultativ, vezi docs/decizii_import.md).
# Categoriile CSV corespund celor trei seturi: BANCA, SINTETIC, FARA_ANALITICE.
CONFIG_PARTENERI = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"config", "conturi_parteneri.csv")
class Stop(Exception):
"""Conditie neprevazuta: nu ghicim, raportam."""
def citeste_conturi_parteneri(path=CONFIG_PARTENERI):
"""(BANCA, SINTETIC, FARA_ANALITICE) din CSV; fisier lipsa = eroare, nu set gol."""
if not os.path.exists(path):
raise Stop(f"lipseste fisierul de configurare a partenerilor: {path}")
seturi = {"BANCA": set(), "SINTETIC": set(), "FARA_ANALITICE": set()}
with open(path, encoding="utf-8-sig", newline="") as f:
for r in csv.DictReader(f):
cont = (r.get("cont") or "").strip()
categorie = (r.get("categorie") or "").strip()
if not cont:
continue
if categorie not in seturi:
raise Stop(f"categorie necunoscuta in {path}: {categorie!r}")
seturi[categorie].add(cont)
return seturi["BANCA"], seturi["SINTETIC"], seturi["FARA_ANALITICE"]
# BANCA (5121/5124): analiticele raman analitice SI primesc partener.
# SINTETIC: un singur BALANTA agregat pe sintetic + FACTURA per analitic.
# FARA_ANALITICE (5311): fara analitice, BALANTA + FACTURA pe sintetic.
(PARTENER_BANCA, PARTENER_SINTETIC,
PARTENER_FARA_ANALITICE) = citeste_conturi_parteneri()
PARTENER = PARTENER_BANCA | PARTENER_SINTETIC | PARTENER_FARA_ANALITICE
def citeste_header():
wb = load_workbook(TEMPLATE, read_only=True)
ws = wb.active
hdr = [c.value for c in ws[1]]
wb.close()
return hdr
def citeste_balanta(path):
rows = []
with open(path, encoding="utf-8-sig", newline="") as f:
for r in csv.DictReader(f):
cont = (r["cont"] or "").strip()
if r["este_total"] == "1" or cont == "":
continue
rows.append({
"cont": cont,
"denumire": (r["denumire"] or "").strip(),
"total_d": float(r["total_d"]),
"total_c": float(r["total_c"]),
})
return rows
def citeste_totaluri(path):
with open(path, encoding="utf-8-sig", newline="") as f:
for r in csv.DictReader(f):
if "totaluri" in (r["denumire"] or "").lower():
return float(r["total_d"]), float(r["total_c"])
return None
def arbore(rows):
by = {r["cont"]: r for r in rows}
children = {}
for c in by:
p = ".".join(c.split(".")[:-1])
if p in by:
children.setdefault(p, []).append(c)
return by, children
def este_partener(cont):
return cont.split(".")[0] in PARTENER
def latura_factura(cont):
"""Latura fixa pe care formularul pune randurile FACTURA ale contului."""
return "C" if cont.split(".")[0] in LATURA_CREDIT else "D"
def split_cont(cont):
seg = cont.split(".")
return seg[0], "".join(seg[1:])
def diferente(by, children):
"""(cont_parinte, diferenta_net) pentru parintii cu postare directa."""
out = []
for cont, r in by.items():
kids = children.get(cont)
if not kids:
continue
pnet = round(r["total_d"] - r["total_c"], 2)
cnet = round(sum(by[k]["total_d"] - by[k]["total_c"] for k in kids), 2)
d = round(pnet - cnet, 2)
if abs(d) > 0.005:
out.append((cont, d))
return out
def acont_diferenta(parent, kids):
"""Primul cod liber de forma 99 (segmente scurte) / 9999 (segmente lungi)."""
seg = parent.split(".")
base = "".join(seg[1:])
style = 2 if max(len(k.split(".")[-1]) for k in kids) <= 2 else 4
cod = "99" if style == 2 else "9999"
ac = base + cod
if len(ac) > 4:
raise Stop(f"acont diferenta prea lung pentru {parent}: '{ac}'")
frati = {split_cont(k)[1] for k in kids}
if ac in frati:
raise Stop(f"acont diferenta '{ac}' deja folosit sub {parent}")
return ac
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(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 _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)
# 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)
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:
acont_diferenta(parent, children[parent])
for p in {c.split(".")[0] for c in by if este_partener(c)}:
for k in children.get(p, []):
if k in children:
raise Stop(f"analitic partener cu sub-analitice: {k}")
out = []
leaf_map = {} # frunza -> randurile dedicate (folosite la dubluri)
leaf_rep = set() # toate frunzele reprezentate (inclusiv in BALANTA agregat)
negative = [] # (cont, analitic, sold_semnat, latura_formular)
def add(r, leaf=None):
out.append(r)
if leaf is not None:
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"])
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).
# 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, 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_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)
if p in PARTENER_BANCA and direct:
# 5121/5124: BALANTA cu acont + FACTURA cu ACELASI acont.
for k in direct:
kd = by[k]
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)
negative.append((p, k, neg, side))
if side == "D":
td, tc = neg, None
else:
td, tc = None, neg
else:
td, tc = round(kd["total_d"], 2), round(kd["total_c"], 2)
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)
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, 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))
continue
# 401/4092/4111/461/462/4551 si 5311
if direct:
base_d = 0.0
base_c = 0.0
for k in direct:
net = round(by[k]["total_d"] - by[k]["total_c"], 2)
if net != 0 and not este_latura_formular(net, side):
neg = -abs(net)
negative.append((p, k, neg, side))
if side == "D":
base_d += neg
else:
base_c += neg
else:
base_d += by[k]["total_d"]
base_c += by[k]["total_c"]
d = diff_by_parent.get(p, 0.0)
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:
net = round(by[k]["total_d"] - by[k]["total_c"], 2)
if net == 0:
leaf_rep.add(k)
continue
sold = abs(net) if este_latura_formular(net, side) else -abs(net)
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):
neg = -abs(net)
negative.append((p, p, neg, side))
if side == "D":
td, tc = neg, None
else:
td, tc = None, neg
else:
td = round(by[p]["total_d"], 2)
tc = round(by[p]["total_c"], 2)
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)
factura(p, cont_p, ac_p, nume, side, net, sold, None)
return out, diffs, leaf_map, leaf_rep, negative
def scrie_xlsx(header, rows, path):
wb = load_workbook(TEMPLATE)
ws = wb.active
if ws.max_row > 1:
ws.delete_rows(2, ws.max_row - 1)
for i, r in enumerate(rows, start=2):
for j, h in enumerate(header, start=1):
v = r.get(h)
if h == "explicatia": # coloana temporara de control
v = None
ws.cell(i, j, v)
wb.save(path)
def verifica(firma, csv_path, xlsx_path, rows, out, diffs, leaf_map, leaf_rep,
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)
totdeb = round(sum(r["totdeb"] or 0 for r in out), 2)
totcred = round(sum(r["totcred"] or 0 for r in out), 2)
net = round(totdeb - totcred, 2)
toturi = citeste_totaluri(csv_path)
pierdute = sorted(set(leaves) - leaf_rep)
duplicate_leaf = sorted(c for c, v in leaf_map.items() if len(v) != 1)
# lungimi + dubluri (cont,acont) pe BALANTA
lungimi = []
bal_pairs = {}
for r in out:
if len(r["cont"]) > 4:
lungimi.append(("cont", r["cont"]))
if r["acont"] and len(r["acont"]) > 4:
lungimi.append(("acont", r["acont"]))
if r["TIP"] == "BALANTA":
bal_pairs.setdefault((r["cont"], r["acont"]), []).append(r)
dubluri_bal = {k: v for k, v in bal_pairs.items() if len(v) > 1}
# parteneri: SUM(sold FACTURA cu semn) + NEREPARTIZAT asteptat = net sintetic
part_rows = {}
for r in out:
if r["TIP"] == "FACTURA":
semn = 1 if latura_factura(r["cont"]) == "D" else -1
part_rows[r["cont"]] = part_rows.get(r["cont"], 0.0) + semn * (r["sold"] or 0)
part_verif = []
for p in sorted({c.split(".")[0] for c in by if este_partener(c)}):
net_sint = round(by[p]["total_d"] - by[p]["total_c"], 2)
suma = round(part_rows.get(p, 0.0), 2)
direct = round(diff_by_parent.get(p, 0.0), 2)
asteptat = direct
diferenta = round(net_sint - suma, 2)
ok = abs(diferenta - asteptat) <= 0.005
part_verif.append((p, suma, net_sint, direct, asteptat, diferenta, ok))
ok_bal = abs(net) <= 0.005
ok_leaves = not pierdute and not duplicate_leaf
ok_part = all(x[6] for x in part_verif)
ok_lung = not lungimi and not dubluri_bal
L = []
L.append(f"# Verificare `{os.path.basename(xlsx_path)}`")
L.append("")
L.append(f"Generat de `genereaza_xlsx.py` din `{os.path.basename(csv_path)}`. "
f"Randuri de date: {len(out)} ({sum(1 for r in out if r['TIP']=='BALANTA')} BALANTA, "
f"{sum(1 for r in out if r['TIP']=='FACTURA')} FACTURA).")
L.append("")
L.append("## 1. Balanta generala (SUM totdeb = SUM totcred)")
L.append("")
L.append(f"- SUM(totdeb) = {totdeb}")
L.append(f"- SUM(totcred) = {totcred}")
L.append(f"- diferenta (totdeb - totcred) = {net} -> {'OK' if ok_bal else '**ESEC**'}")
if toturi:
td, tc = toturi
L.append(f"- Totaluri balanta: total_d={td} total_c={tc} "
f"net={round(td - tc, 2)}")
L.append(f"- NET xlsx ({net}) vs NET Totaluri ({round(td - tc, 2)}) -> "
f"{'OK' if abs(net - (td - tc)) <= 0.005 else '**ESEC**'}")
L.append(f"- Nota: SUM(totdeb)={totdeb} difera de Totaluri.total_d={td} cu "
f"{round(totdeb - td, 2)}; suma pe laturi a PDF-ului nu se poate "
f"reconstrui din frunze acolo unde SAGA a netat sinteticul (gross-ul "
f"parintelui e netat). Netul se pastreaza exact.")
L.append("")
L.append("## 2. Conturi frunza din CSV, reprezentate o singura data")
L.append("")
L.append(f"- frunze in CSV: {len(leaves)}")
L.append(f"- reprezentate in xlsx: {len(leaf_rep)}")
L.append(f"- conturi pierdute: {pierdute if pierdute else '[] (gol)'} -> "
f"{'OK' if not pierdute else '**ESEC**'}")
L.append(f"- frunze cu mai putin/mai mult de un rand dedicat: "
f"{duplicate_leaf if duplicate_leaf else '[] (gol)'} -> "
f"{'OK' if not duplicate_leaf else '**ESEC**'}")
L.append("")
L.append("## 3. Conturi partener: FACTURA + NEREPARTIZAT = net sintetic")
L.append("")
L.append("Formularul scade din randul BALANTA suma randurilor FACTURA de pe "
"acelasi `(cont, acont, latura)` si lasa restul pe partenerul "
"`NEREPARTIZAT`. Deci: `net sintetic - SUM(sold FACTURA cu semn) = "
"postare directa pe sintetic`.")
L.append("")
L.append("| cont | SUM(sold FACTURA) | net sintetic | postare directa | NEREPARTIZAT asteptat | diferenta | rezultat |")
L.append("|---|---|---|---|---|---|---|")
for p, suma, net_sint, direct, asteptat, diferenta, ok in part_verif:
L.append(f"| {p} | {suma} | {net_sint} | {direct} | {asteptat} "
f"| {diferenta} | {'OK' if ok else '**ESEC**'} |")
L.append("")
L.append(f"Rezultat: {'OK' if ok_part else '**ESEC**'}")
L.append("")
L.append("## 4. Analitice cu semn negativ (sold pe latura opusa formularului)")
L.append("")
L.append("Latura FACTURA e fixa in cod (`:379`): `401,404,462,419,408,4551` -> "
"CREDIT, orice alt cont -> DEBIT. Un analitic cu sold pe latura opusa "
"primeste totusi rand FACTURA, iar suma se scrie cu **semn negativ pe "
"latura formularului**, atat in randul BALANTA cat si in `sold`-ul "
"randului FACTURA. Soldul ramane pe latura corecta, balanta ramane "
"inchisa, iar randul FACTURA consuma exact randul BALANTA (fara "
"`NEREPARTIZAT`).")
L.append("")
if not negative:
L.append("Niciunul.")
else:
L.append("| cont | analitic | sold (semnat) | latura formularului |")
L.append("|---|---|---|---|")
for cont, k, s, side in negative:
L.append(f"| {cont} | {k} | {s} | {side} |")
L.append("")
L.append("## 5. Lungimi si dubluri")
L.append("")
L.append(f"- cont > 4 sau acont > 4: {lungimi if lungimi else '[] (gol)'} -> "
f"{'OK' if not lungimi else '**ESEC**'}")
L.append(f"- perechi (cont, acont) duplicate la BALANTA: "
f"{list(dubluri_bal) if dubluri_bal else '[] (gol)'} -> "
f"{'OK' if not dubluri_bal else '**ESEC**'}")
L.append("")
L.append("## 6. Randuri de diferenta generate (pct. 13-15)")
L.append("")
if not diffs:
L.append("Niciunul (toate diferentele nete sunt zero).")
for cont, d in diffs:
base = cont.split(".")[0]
if base in PARTENER_BANCA:
kind = "BALANTA (analitic de diferenta, fara FACTURA)"
elif este_partener(cont):
kind = "inclus in BALANTA agregat al sinteticului (fara FACTURA)"
else:
kind = "BALANTA (analitic de diferenta)"
L.append(f"- {cont}: diferenta neta {d} -> {kind}")
L.append("")
L.append("Nota: randurile FACTURA de tip `DIFERENTA SINTETIC` au fost eliminate; "
"diferenta nu se mai inventa, o rezolva formularul prin `NEREPARTIZAT`.")
L.append("")
L.append("## Verdict")
L.append("")
toate = ok_bal and ok_leaves and ok_part and ok_lung
L.append(f"{'TOATE verificarile au trecut.' if toate else '**UNA SAU MAI MULTE VERIFICARI AU PICAT - datele NU au fost ajustate.**'}")
L.append("")
L.append("Nota: coloana `explicatia` (D/C la FACTURA) a fost folosita doar ca "
"control intern si este goala in xlsx-ul final (capul de tabel o pastreaza).")
L.append("")
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))
return {"bal": ok_bal, "leaves": ok_leaves, "part": ok_part, "lung": ok_lung,
"totdeb": totdeb, "totcred": totcred, "net": net,
"pierdute": pierdute, "dubluri_leaf": duplicate_leaf,
"dubluri_bal": list(dubluri_bal), "lungimi": lungimi,
"part_verif": part_verif, "negative": negative}