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
596 lines
25 KiB
Python
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}
|
|
|
|
|
|
|