"""Genereaza fisierele xlsx de initializare ROACONT din balantele SAGA (CSV). Produce (pentru fiecare firma): init__2025_12.xlsx - un sheet, randul 1 = capul de tabel, datele de la randul 2 in jos docs/verificare_.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 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"} # 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 FIRME = [ ("FUNDATIA", "balanta_FUNDATIA.csv"), ("MASTER", "balanta_MASTERJOB.csv"), ] 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 este_latura_formular(net, side): return (net > 0) == (side == "D") 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) 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) # 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] 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, TIP="BALANTA", cont=p, 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)) 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 = 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) 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 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) 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) add(rand(header, TIP="BALANTA", cont=p, acont=None, 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)) 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): 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("") 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, "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()