Files
solduri2roa/citire_xlsx.py
Marius Mutu ba4cbbf62c Lane-urile citire-xlsx si mapare: cititorul de foaie SAGA si pasul de mapare
citire_xlsx.py citeste .xlsx (openpyxl) si .xls (xlrd) in acelasi contract intern
ca extract_balanta.py. Prima foaie, nu dupa nume; potrivire exacta a coloanelor,
niciodata pe substring (TOTAL_DEB vs TOTAL_DEB_1); CONT ramane text.

mapare.py: propunere automata, corectii_<FIRMA>.xlsx detinut de utilizator si
mapare_<FIRMA>.xlsx regenerat de unealta, .TOATE/.RESTUL in ordinea exact ->
RESTUL -> TOATE, redenumirea sinteticului. Se opreste cu eroare la cont/acont
peste 4 caractere, coliziune accidentala si cont_saga inexistent in corectii.

Corectat in orchestrare: .xls-ul SAGA nu are inregistrare CODEPAGE, deci xlrd
cadea pe iso-8859-1 si strica denumirile ("FURNIZORI \x97 DEBITORI"). Citirea se
face cu encoding_override="cp1250", cu o verificare in test care pica daca revine.

18 teste trec, inclusiv poarta de regresie pe etalon.

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

173 lines
5.1 KiB
Python

"""Cititor pentru balantele SAGA exportate ca foaie de calcul (.xls / .xlsx).
Produce balanta_<FIRMA>.csv cu contractul intern, aceleasi coloane ca extract_balanta.py:
pagina, cont, denumire, prec_d, prec_c, rulaj_d, rulaj_c, total_d, total_c,
sold_d, sold_c, este_total, tip.
.xlsx cu openpyxl, .xls cu xlrd (openpyxl nu citeste BIFF). Se citeste prima foaie a
registrului, fara cautare dupa nume (numele difera: balanta / xl / Sheet1). Numele de
coloana se potrivesc exact si case-insensitive, niciodata pe substring (TOTAL_DEB si
TOTAL_DEB_1 sunt coloane diferite). CONT se citeste ca text: 401.00002 citit ca numar
se strica. pagina si este_total raman goale (exportul nu are rand de totaluri).
Utilizare:
py citire_xlsx.py "<fisier.xls sau .xlsx>" "<iesire.csv>"
"""
import csv
import os
import sys
import openpyxl
import xlrd
COLOANE = ["pagina", "cont", "denumire", "prec_d", "prec_c", "rulaj_d", "rulaj_c",
"total_d", "total_c", "sold_d", "sold_c", "este_total", "tip"]
MAPARE_SUME = {
"DEB_PREC": "prec_d",
"CRED_PREC": "prec_c",
"RULAJ_D": "rulaj_d",
"RULAJ_C": "rulaj_c",
"TOTAL_DEB": "total_d",
"TOTAL_CRED": "total_c",
"FIN_D": "sold_d",
"FIN_C": "sold_c",
}
COLOANE_SURSA = ["CONT", "DENUMIRE", "TIP"] + list(MAPARE_SUME)
def _index_antet(antet):
"""Indexul fiecarei coloane cerute, pe potrivire exacta case-insensitive."""
normal = [("" if h is None else str(h)).strip().upper() for h in antet]
idx = {}
for i, nume in enumerate(normal):
if nume == "":
continue
if nume in idx:
raise ValueError("coloana %s apare de doua ori in antet" % nume)
idx[nume] = i
lipsa = [c for c in COLOANE_SURSA if c not in idx]
if lipsa:
raise ValueError("coloane lipsa in antetul SAGA: %s" % ", ".join(lipsa))
return idx
def _randuri_xlsx(cale):
wb = openpyxl.load_workbook(cale, data_only=True, read_only=True)
try:
ws = wb[wb.sheetnames[0]]
it = ws.iter_rows(values_only=True)
try:
antet = list(next(it))
except StopIteration:
raise ValueError("fisier fara antet: %s" % cale)
randuri = [list(r) for r in it]
finally:
wb.close()
return antet, randuri
def _randuri_xls(cale):
# Exportul SAGA (VFP) nu are inregistrare CODEPAGE; fara override xlrd cade pe
# iso-8859-1 si strica diacriticele ("FURNIZORI \x97 DEBITORI"). SAGA scrie WIN1250.
book = xlrd.open_workbook(cale, encoding_override="cp1250")
sh = book.sheet_by_index(0)
if sh.nrows == 0:
raise ValueError("fisier fara antet: %s" % cale)
antet = [sh.cell_value(0, c) for c in range(sh.ncols)]
randuri = [[sh.cell_value(r, c) for c in range(sh.ncols)]
for r in range(1, sh.nrows)]
return antet, randuri
def _randuri(cale):
ext = os.path.splitext(cale)[1].lower()
if ext == ".xlsx":
return _randuri_xlsx(cale)
if ext == ".xls":
return _randuri_xls(cale)
raise ValueError("extensie nesuportata pentru %s (astept .xls sau .xlsx)" % cale)
def _camp(raw, i):
return raw[i] if i < len(raw) else None
def _text(v):
if v is None:
return ""
if isinstance(v, str):
return v.strip()
return str(v).strip()
def _cont(v):
"""CONT ca text: un float intreg nu primeste zecimale, unul cu zecimale ramane exact."""
if v is None:
return ""
if isinstance(v, str):
return v.strip()
if isinstance(v, bool):
return str(v)
if isinstance(v, float):
return str(int(v)) if v.is_integer() else repr(v)
return str(v)
def _num(v):
if v is None:
return 0.0
if isinstance(v, bool):
return float(int(v))
if isinstance(v, (int, float)):
return float(v)
if isinstance(v, str):
s = v.strip()
if s in ("", "-"):
return 0.0
return float(s)
raise ValueError("valoare numerica neasteptata: %r" % (v,))
def citeste_balanta(cale):
"""Randurile balantei din prima foaie, in contractul intern."""
antet, brute = _randuri(cale)
idx = _index_antet(antet)
rows = []
for raw in brute:
cont = _cont(_camp(raw, idx["CONT"]))
denumire = _text(_camp(raw, idx["DENUMIRE"]))
if cont == "" and denumire == "" and \
all(_camp(raw, idx[n]) in (None, "") for n in MAPARE_SUME):
continue
rand = {"pagina": "", "cont": cont, "denumire": denumire}
for nume, col in MAPARE_SUME.items():
rand[col] = _num(_camp(raw, idx[nume]))
rand["este_total"] = ""
rand["tip"] = _text(_camp(raw, idx["TIP"]))
rows.append(rand)
return rows
def scrie_csv(rows, cale):
with open(cale, "w", newline="", encoding="utf-8-sig") as f:
wr = csv.DictWriter(f, fieldnames=COLOANE, extrasaction="ignore")
wr.writeheader()
wr.writerows(rows)
def main():
if len(sys.argv) != 3:
print(__doc__)
sys.exit(1)
cale, iesire = sys.argv[1], sys.argv[2]
rows = citeste_balanta(cale)
scrie_csv(rows, iesire)
print("scris %s: %d randuri" % (iesire, len(rows)))
if __name__ == "__main__":
main()