Files
solduri2roa/genereaza_xlsx.py
Marius Mutu 92dc8c7b32 solduri2roa: balante SAGA PDF -> xlsx initializare ROACONT
Extragere pe coordonate (pdfplumber), generarea celor doua xlsx pentru
FUNDATIA si MASTER 12/2025, regulile de mapare si rapoartele de verificare.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdA5a3ECKFD6Citf1ptPZu
2026-09-21 00:02:30 +03:00

491 lines
20 KiB
Python

"""Genereaza fisierele xlsx 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
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') nu primeste
FACTURA; ramane doar in BALANTA si apare ca NEREPARTIZAT.
"""
import csv
import os
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"
# 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"}
# 5121/5124: analiticele raman analitice SI primesc partener (BALANTA + FACTURA
# pe acelasi acont).
PARTENER_BANCA = {"5121", "5124"}
# Conturi cu parteneri, un singur BALANTA agregat pe sintetic + FACTURA per analitic.
PARTENER_SINTETIC = {"401", "4092", "4111", "461", "462", "4551"}
# Casa: fara analitice, BALANTA + FACTURA pe sintetic.
PARTENER_FARA_ANALITICE = {"5311"}
PARTENER = PARTENER_BANCA | PARTENER_SINTETIC | PARTENER_FARA_ANALITICE
FIRME = [
("FUNDATIA", "balanta_FUNDATIA.csv"),
("MASTER", "balanta_MASTERJOB.csv"),
]
class Stop(Exception):
"""Conditie neprevazuta: nu ghicim, raportam."""
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 rand(header, **kw):
r = {h: None for h in header}
r.update(an=AN, luna=LUNA, data=DATA, datascad=DATASCAD,
serie=SERIE, valuta=VALUTA)
r.update(kw)
return r
def _semn_sold(net):
return "creditor" if net < 0 else "debitor"
def genereaza(rows, header):
by, children = arbore(rows)
diffs = diferente(by, children)
diff_by_parent = dict(diffs)
# verificarile de structura care nu se ghicesc
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:
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)
sarite = [] # (cont, analitic, sold_semnat, motiv)
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)
# 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,
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)
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,
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:
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]
kac = split_cont(k)[1]
add(rand(header, TIP="BALANTA", cont=p, acont=kac, numar=1,
nume=kd["denumire"], totdeb=round(kd["total_d"], 2),
totcred=round(kd["total_c"], 2)), leaf=k)
net = round(kd["total_d"] - kd["total_c"], 2)
if net == 0:
continue
if (net > 0) == (side == "D"):
add(rand(header, TIP="FACTURA", cont=p, acont=kac,
numar=k.replace(".", ""),
nume=kd["denumire"] or f"FURNIZOR {k}",
cod_fiscal=k, sold=abs(net), explicatia=side))
else:
sarite.append((p, k, net,
f"sold {_semn_sold(net)} vs latura {side} "
f"a formularului"))
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,
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 = sum(by[k]["total_d"] for k in direct)
base_c = sum(by[k]["total_c"] for k in direct)
d = diff_by_parent.get(p, 0.0)
add(rand(header, TIP="BALANTA", cont=p, acont=None, 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
if (net > 0) == (side == "D"):
add(rand(header, TIP="FACTURA", cont=p, acont=None,
numar=k.replace(".", ""),
nume=by[k]["denumire"] or f"FURNIZOR {k}",
cod_fiscal=k, sold=abs(net), explicatia=side),
leaf=k)
else:
sarite.append((p, k, net,
f"sold {_semn_sold(net)} vs latura {side} "
f"a formularului"))
leaf_rep.add(k)
else:
net = round(by[p]["total_d"] - by[p]["total_c"], 2)
add(rand(header, TIP="BALANTA", cont=p, acont=None, numar=1,
nume=by[p]["denumire"], totdeb=round(by[p]["total_d"], 2),
totcred=round(by[p]["total_c"], 2)), leaf=p)
if net != 0:
if (net > 0) == (side == "D"):
nume = "CENTRALA" if p == "5311" else by[p]["denumire"]
add(rand(header, TIP="FACTURA", cont=p, acont=None,
numar=p, nume=nume, cod_fiscal=p, sold=abs(net),
explicatia=side))
else:
sarite.append((p, p, net,
f"sold {_semn_sold(net)} vs latura {side} "
f"a formularului"))
return out, diffs, leaf_map, leaf_rep, sarite
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,
sarite, header):
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)
sarite_by_cont = {}
for cont, _k, s, _motiv in sarite:
sarite_by_cont[cont] = round(sarite_by_cont.get(cont, 0.0) + s, 2)
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)
sarit = round(sarite_by_cont.get(p, 0.0), 2)
direct = round(diff_by_parent.get(p, 0.0), 2)
asteptat = round(sarit + direct, 2)
diferenta = round(net_sint - suma, 2)
ok = abs(diferenta - asteptat) <= 0.005
part_verif.append((p, suma, net_sint, sarit, direct, asteptat, diferenta, ok))
ok_bal = abs(net) <= 0.005
ok_leaves = not pierdute and not duplicate_leaf
ok_part = all(x[7] 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) = "
"analitice sarite + postare directa pe sintetic`.")
L.append("")
L.append("| cont | SUM(sold FACTURA) | net sintetic | analitice sarite | postare directa | NEREPARTIZAT asteptat | diferenta | rezultat |")
L.append("|---|---|---|---|---|---|---|---|")
for p, suma, net_sint, sarit, direct, asteptat, diferenta, ok in part_verif:
L.append(f"| {p} | {suma} | {net_sint} | {sarit} | {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 sarite (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 nu "
"primeste rand FACTURA; ramane in BALANTA si formularul ii pune "
"`NEREPARTIZAT`.")
L.append("")
if not sarite:
L.append("Niciunul.")
else:
L.append("| cont | analitic | sold (semnat) | motiv |")
L.append("|---|---|---|---|")
for cont, k, s, motiv in sarite:
L.append(f"| {cont} | {k} | {s} | {motiv} |")
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("")
with open(os.path.join("docs", 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, "sarite": sarite}
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, sarite = 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, sarite, 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["sarite"]:
print(f" analitice sarite: {rez['sarite']}")
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()