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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user