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
This commit is contained in:
243
extract_balanta.py
Normal file
243
extract_balanta.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""Extractor fidel pentru balantele de verificare SAGA (PDF cu text).
|
||||
|
||||
Foloseste pdfplumber cu coordonate (NU pdftotext -layout, care decaleaza coloanele).
|
||||
Fiecare pagina are un antet cu 8 coloane numerice:
|
||||
Sume precedente (D,C) | Rulaje perioada (D,C) | Sume totale (D,C) | Solduri finale (D,C)
|
||||
|
||||
Utilizare:
|
||||
py extract_balanta.py "<fisier.pdf>" "<iesire.csv>"
|
||||
"""
|
||||
|
||||
import csv
|
||||
import re
|
||||
import sys
|
||||
|
||||
import pdfplumber
|
||||
|
||||
CONT_RE = re.compile(r"^\d+(?:\.\d+)*$")
|
||||
NUM_RE = re.compile(r"^-?\d[\d.]*$")
|
||||
|
||||
CONT_MAX_X = 80.0
|
||||
DENUM_MAX_X = 240.0
|
||||
ROW_TOL = 3.0
|
||||
TOL = 0.01
|
||||
|
||||
HEADER_WORDS = {"Cont", "Denumirea", "contului", "Debitoare", "Creditoare",
|
||||
"Sume", "precedente", "Rulaje", "perioada", "totale",
|
||||
"Solduri", "finale"}
|
||||
TOTAL_WORDS = ("total", "totaluri")
|
||||
|
||||
|
||||
def coloane_numerice(page0):
|
||||
"""Centrele celor 8 coloane, din etichetele Debitoare/Creditoare de pe pagina 1."""
|
||||
labels = [w for w in page0.extract_words()
|
||||
if w["text"] in ("Debitoare", "Creditoare") and 75 <= w["top"] <= 100]
|
||||
labels.sort(key=lambda w: w["x0"])
|
||||
assert len(labels) == 8, f"astept 8 etichete numerice, gasit {len(labels)}"
|
||||
centre = [(w["x0"] + w["x1"]) / 2.0 for w in labels]
|
||||
granite = [(centre[i] + centre[i + 1]) / 2.0 for i in range(len(centre) - 1)]
|
||||
return centre, granite, max(w["top"] for w in labels)
|
||||
|
||||
|
||||
def col_index(center, granite):
|
||||
idx = 0
|
||||
for g in granite:
|
||||
if center >= g:
|
||||
idx += 1
|
||||
else:
|
||||
break
|
||||
return idx
|
||||
|
||||
|
||||
def randuri(pagina):
|
||||
"""Grupeaza cuvintele in randuri dupa coordonata top (toleranta ROW_TOL)."""
|
||||
words = sorted(pagina.extract_words(), key=lambda w: (w["top"], w["x0"]))
|
||||
rows = []
|
||||
for w in words:
|
||||
if rows and abs(w["top"] - rows[-1][0]) <= ROW_TOL:
|
||||
rows[-1][1].append(w)
|
||||
else:
|
||||
rows.append((w["top"], [w]))
|
||||
return rows
|
||||
|
||||
|
||||
def numar(txt):
|
||||
txt = txt.strip()
|
||||
if txt in ("", "-", "."):
|
||||
return 0.0
|
||||
return float(txt)
|
||||
|
||||
|
||||
def parse_pdf(path):
|
||||
conturi = []
|
||||
with pdfplumber.open(path) as pdf:
|
||||
_, granite, header_top = coloane_numerice(pdf.pages[0])
|
||||
for pno, page in enumerate(pdf.pages, start=1):
|
||||
for top, ws in randuri(page):
|
||||
if top < header_top:
|
||||
continue
|
||||
cont = None
|
||||
denum = []
|
||||
valori = [None] * 8
|
||||
for w in ws:
|
||||
t = w["text"]
|
||||
if t in HEADER_WORDS:
|
||||
continue
|
||||
if w["x0"] < CONT_MAX_X:
|
||||
if CONT_RE.match(t):
|
||||
cont = t
|
||||
continue
|
||||
if w["x0"] < DENUM_MAX_X:
|
||||
denum.append(w)
|
||||
continue
|
||||
if NUM_RE.match(t):
|
||||
ci = col_index((w["x0"] + w["x1"]) / 2.0, granite)
|
||||
if valori[ci] is None:
|
||||
valori[ci] = []
|
||||
valori[ci].append(w)
|
||||
|
||||
denum = " ".join(w["text"] for w in sorted(denum, key=lambda x: x["x0"]))
|
||||
vals = []
|
||||
for i in range(8):
|
||||
if valori[i] is None:
|
||||
vals.append(0.0)
|
||||
else:
|
||||
txt = "".join(x["text"] for x in
|
||||
sorted(valori[i], key=lambda x: x["x0"]))
|
||||
vals.append(numar(txt))
|
||||
|
||||
este_total = any(k in denum.lower() for k in TOTAL_WORDS)
|
||||
if cont:
|
||||
conturi.append({
|
||||
"pagina": pno, "cont": cont, "denumire": denum,
|
||||
**dict(zip(("prec_d", "prec_c", "rulaj_d", "rulaj_c",
|
||||
"total_d", "total_c", "sold_d", "sold_c"), vals)),
|
||||
"este_total": 0,
|
||||
})
|
||||
elif este_total:
|
||||
conturi.append({
|
||||
"pagina": pno, "cont": "", "denumire": denum,
|
||||
**dict(zip(("prec_d", "prec_c", "rulaj_d", "rulaj_c",
|
||||
"total_d", "total_c", "sold_d", "sold_c"), vals)),
|
||||
"este_total": 1,
|
||||
})
|
||||
elif denum and conturi and conturi[-1]["este_total"] == 0:
|
||||
# rand de continuare a denumirii contului precedent
|
||||
conturi[-1]["denumire"] = (conturi[-1]["denumire"] + " " + denum).strip()
|
||||
return conturi
|
||||
|
||||
|
||||
NUMS = ["prec_d", "prec_c", "rulaj_d", "rulaj_c",
|
||||
"total_d", "total_c", "sold_d", "sold_c"]
|
||||
|
||||
|
||||
def verifica(rows):
|
||||
reale = [r for r in rows if not r["este_total"]]
|
||||
totaluri = [r for r in rows if r["este_total"]]
|
||||
|
||||
a = b = 0
|
||||
for r in reale:
|
||||
if abs(r["total_d"] - (r["prec_d"] + r["rulaj_d"])) > TOL or \
|
||||
abs(r["total_c"] - (r["prec_c"] + r["rulaj_c"])) > TOL:
|
||||
a += 1
|
||||
if abs((r["sold_d"] - r["sold_c"]) - (r["total_d"] - r["total_c"])) > TOL:
|
||||
b += 1
|
||||
|
||||
coduri = {r["cont"] for r in reale if r["cont"]}
|
||||
copii = {}
|
||||
for c in coduri:
|
||||
parts = c.split(".")
|
||||
if len(parts) > 1:
|
||||
copii.setdefault(".".join(parts[:-1]), []).append(c)
|
||||
|
||||
# frunza = nu apare ca parinte; radacina = parintele ei nu exista in balanta
|
||||
frunze = [r for r in reale if r["cont"] not in copii]
|
||||
radacini = [r for r in reale
|
||||
if ".".join(r["cont"].split(".")[:-1]) not in coduri]
|
||||
|
||||
fd = round(sum(r["sold_d"] for r in frunze), 2)
|
||||
fc = round(sum(r["sold_c"] for r in frunze), 2)
|
||||
rd = round(sum(r["sold_d"] for r in radacini), 2)
|
||||
rc = round(sum(r["sold_c"] for r in radacini), 2)
|
||||
|
||||
# (d) parinte vs suma analiticelor: pe fiecare latura si pe net (SAGA neteaza D/C)
|
||||
byk = {r["cont"]: r for r in reale}
|
||||
d_laturi = []
|
||||
d_net = []
|
||||
for r in reale:
|
||||
kids = copii.get(r["cont"])
|
||||
if not kids:
|
||||
continue
|
||||
for col in NUMS:
|
||||
s = round(sum(byk[k][col] for k in kids if k in byk), 2)
|
||||
if abs(s - r[col]) > TOL:
|
||||
d_laturi.append((r["cont"], col, r[col], s, round(r[col] - s, 2)))
|
||||
pnet = round(r["sold_d"] - r["sold_c"], 2)
|
||||
cnet = round(sum(byk[k]["sold_d"] - byk[k]["sold_c"]
|
||||
for k in kids if k in byk), 2)
|
||||
if abs(pnet - cnet) > TOL:
|
||||
d_net.append((r["cont"], pnet, cnet, round(pnet - cnet, 2)))
|
||||
|
||||
# cross-check parser: fiecare "Total sume clasa N" = suma radacinilor din clasa
|
||||
clase_ok = clase_rau = 0
|
||||
clase_det = []
|
||||
for t in totaluri:
|
||||
if "clasa" not in t["denumire"].lower():
|
||||
continue
|
||||
cl = t["denumire"].split()[-1]
|
||||
top = [r for r in radacini if r["cont"].startswith(cl)]
|
||||
sd = round(sum(r["sold_d"] for r in top), 2)
|
||||
sc = round(sum(r["sold_c"] for r in top), 2)
|
||||
ok = abs(sd - t["sold_d"]) <= TOL and abs(sc - t["sold_c"]) <= TOL
|
||||
if ok:
|
||||
clase_ok += 1
|
||||
else:
|
||||
clase_rau += 1
|
||||
clase_det.append((cl, t["sold_d"], sd, t["sold_c"], sc))
|
||||
tot_row = [t for t in totaluri if "totaluri" in t["denumire"].lower()]
|
||||
|
||||
print("=== VERIFICARE ===")
|
||||
print(f"randuri conturi reale : {len(reale)}")
|
||||
print(f"conturi frunza : {len(frunze)} conturi radacina: {len(radacini)}")
|
||||
print(f"(a) total=prec+rulaj : {len(reale) - a}/{len(reale)} OK, {a} esec")
|
||||
print(f"(b) sold=total : {len(reale) - b}/{len(reale)} OK, {b} esec")
|
||||
print(f"(c) balanta frunze : sold_d={fd} sold_c={fc} "
|
||||
f"diferenta={round(fd - fc, 2)} {'OK' if abs(fd - fc) <= TOL else 'ESEC'}")
|
||||
print(f"(c2) balanta radacini : sold_d={rd} sold_c={rc} "
|
||||
f"diferenta={round(rd - rc, 2)} {'OK' if abs(rd - rc) <= TOL else 'ESEC'}")
|
||||
if tot_row:
|
||||
tr = tot_row[0]
|
||||
print(f" Totaluri (PDF) : sold_d={tr['sold_d']} sold_c={tr['sold_c']}")
|
||||
print(f" cross-check clase: {clase_ok} OK, {clase_rau} esec")
|
||||
print(f"(d) parinti vs analitice: {len(d_laturi)} diferente pe laturi, "
|
||||
f"{len(d_net)} diferente pe net")
|
||||
for p in d_laturi:
|
||||
print(" d-latura", p)
|
||||
for p in d_net:
|
||||
print(" d-NET ", p)
|
||||
for p in clase_det:
|
||||
print(" clasa ", p)
|
||||
return {"n": len(reale), "frunze": len(frunze), "radacini": len(radacini),
|
||||
"a": a, "b": b, "fd": fd, "fc": fc, "rd": rd, "rc": rc,
|
||||
"d_laturi": d_laturi, "d_net": d_net, "clase_det": clase_det,
|
||||
"totaluri": [(t["sold_d"], t["sold_c"]) for t in tot_row]}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
pdf_path, csv_path = sys.argv[1], sys.argv[2]
|
||||
rows = parse_pdf(pdf_path)
|
||||
cols = ["pagina", "cont", "denumire"] + NUMS + ["este_total"]
|
||||
with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
|
||||
wr = csv.DictWriter(f, fieldnames=cols)
|
||||
wr.writeheader()
|
||||
for r in rows:
|
||||
wr.writerow(r)
|
||||
print(f"scris {csv_path}: {len(rows)} randuri")
|
||||
verifica(rows)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user