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
173 lines
5.1 KiB
Python
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()
|