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
This commit is contained in:
2026-09-21 15:59:56 +03:00
parent f23b3367d8
commit ba4cbbf62c
6 changed files with 898 additions and 0 deletions

143
tests/test_citire_xlsx.py Normal file
View File

@@ -0,0 +1,143 @@
"""Teste pentru citire_xlsx: contractul intern produs din foaia de calcul SAGA.
Rulare: py -m unittest discover -s tests -v
"""
import os
import pathlib
import sys
import tempfile
import unittest
import openpyxl
RADACINA = pathlib.Path(__file__).resolve().parent.parent
if str(RADACINA) not in sys.path:
sys.path.insert(0, str(RADACINA))
import citire_xlsx
EXEMPLE = RADACINA / "exemple"
XLSX = EXEMPLE / "saga-sqlite-balanta-09-2026.xlsx"
XLS = EXEMPLE / "saga-sqlite-balanta-09-2026.xls"
DANUBE = RADACINA.parent / "saga2roa_danube" / "balanta.xls"
TOL = 0.01
NUMERICE = ["prec_d", "prec_c", "rulaj_d", "rulaj_c",
"total_d", "total_c", "sold_d", "sold_c"]
def frunze(rows):
"""Randurile care nu sunt parintele niciunui alt cont din balanta."""
conts = [r["cont"] for r in rows if r["cont"]]
parinti = {c.rsplit(".", 1)[0] for c in conts if "." in c}
return [r for r in rows if r["cont"] and r["cont"] not in parinti]
def scrie_xlsx(cale, antet, randuri):
wb = openpyxl.Workbook()
ws = wb.active
ws.append(antet)
for rand in randuri:
ws.append(rand)
wb.save(cale)
wb.close()
class TestCitireXlsx(unittest.TestCase):
def _verifica_relatii(self, rows, eticheta):
self.assertTrue(rows, "%s: nicio linie citita" % eticheta)
for r in rows:
self.assertAlmostEqual(
r["total_d"], r["prec_d"] + r["rulaj_d"], delta=TOL,
msg="%s %s: total_d != prec_d + rulaj_d" % (eticheta, r["cont"]))
self.assertAlmostEqual(
r["total_c"], r["prec_c"] + r["rulaj_c"], delta=TOL,
msg="%s %s: total_c != prec_c + rulaj_c" % (eticheta, r["cont"]))
self.assertAlmostEqual(
r["sold_d"] - r["sold_c"], r["total_d"] - r["total_c"], delta=TOL,
msg="%s %s: sold_d - sold_c != total_d - total_c" % (eticheta, r["cont"]))
def _verifica_frunze(self, rows, eticheta):
f = frunze(rows)
sd = sum(r["sold_d"] for r in f)
sc = sum(r["sold_c"] for r in f)
self.assertAlmostEqual(sd, sc, delta=TOL,
msg="%s: SUM(sold_d)=%s != SUM(sold_c)=%s" % (eticheta, sd, sc))
def test_xlsx_si_xls_identice(self):
rows_xlsx = citire_xlsx.citeste_balanta(str(XLSX))
rows_xls = citire_xlsx.citeste_balanta(str(XLS))
self.assertTrue(rows_xlsx)
self.assertEqual(len(rows_xlsx), len(rows_xls))
for a, b in zip(rows_xlsx, rows_xls):
self.assertEqual(a["cont"], b["cont"])
self.assertEqual(a["denumire"], b["denumire"])
self.assertEqual(a["tip"], b["tip"])
for col in ("pagina", "este_total"):
self.assertEqual(a[col], b[col])
for col in NUMERICE:
self.assertAlmostEqual(a[col], b[col], delta=TOL,
msg="%s %s: %s xlsx=%s xls=%s"
% (a["cont"], col, a["cont"], a[col], b[col]))
def test_relatii_si_frunze(self):
for eticheta, cale in (("xlsx", XLSX), ("xls", XLS)):
rows = citire_xlsx.citeste_balanta(str(cale))
self._verifica_relatii(rows, eticheta)
self._verifica_frunze(rows, eticheta)
def test_cont_ramane_text(self):
for eticheta, cale in (("xlsx", XLSX), ("xls", XLS)):
rows = citire_xlsx.citeste_balanta(str(cale))
for r in rows:
self.assertIsInstance(r["cont"], str, "%s: cont nu e text" % eticheta)
def test_danube_analitice(self):
if not DANUBE.exists():
self.skipTest("lipseste %s" % DANUBE)
rows = citire_xlsx.citeste_balanta(str(DANUBE))
self._verifica_relatii(rows, "danube")
self._verifica_frunze(rows, "danube")
analitice = [r for r in rows if "." in r["cont"]]
self.assertTrue(analitice, "danube ar trebui sa aiba conturi analitice")
for r in analitice:
self.assertIsInstance(r["cont"], str, "cont analitic nu e text")
# .xls-ul SAGA nu are CODEPAGE: fara encoding_override, xlrd cade pe iso-8859-1
# si denumirile ies cu caractere de control in loc de diacritice.
stricate = [r["denumire"] for r in rows
if any(ord(c) < 32 or 127 <= ord(c) < 160 for c in str(r["denumire"]))]
self.assertEqual([], stricate, "denumiri decodate gresit (codepage)")
cautat = [r for r in rows if r["cont"] == "105.10"]
self.assertTrue(cautat, "lipseste contul analitic 105.10")
self.assertIsInstance(cautat[0]["cont"], str)
self.assertEqual(cautat[0]["cont"], "105.10")
def test_cont_numeric_devine_text(self):
with tempfile.TemporaryDirectory() as d:
cale = os.path.join(d, "numeric.xlsx")
scrie_xlsx(
cale,
["CONT", "DENUMIRE", "TIP", "DEB_PREC", "CRED_PREC", "RULAJ_D",
"RULAJ_C", "TOTAL_DEB", "TOTAL_CRED", "FIN_D", "FIN_C"],
[[401.00002, "PARTENER", "P", 0, 0, 0, 0, 0, 100, 0, 100]],
)
rows = citire_xlsx.citeste_balanta(cale)
self.assertEqual(len(rows), 1)
self.assertIsInstance(rows[0]["cont"], str)
self.assertEqual(rows[0]["cont"], "401.00002")
def test_coloana_lipsa_si_fara_substring(self):
antet = ["CONT", "DENUMIRE", "TIP", "DEB_PREC", "CRED_PREC", "RULAJ_D",
"RULAJ_C", "TOTAL_DEB_1", "TOTAL_CRED", "FIN_D", "FIN_C"]
with tempfile.TemporaryDirectory() as d:
cale = os.path.join(d, "lipsa.xlsx")
scrie_xlsx(cale, antet, [["401", "FURNIZORI", "P", 0, 0, 0, 0, 0, 0, 0, 0]])
with self.assertRaises(ValueError) as cm:
citire_xlsx.citeste_balanta(cale)
self.assertIn("TOTAL_DEB", str(cm.exception))
if __name__ == "__main__":
unittest.main()

161
tests/test_mapare.py Normal file
View File

@@ -0,0 +1,161 @@
"""Teste pentru mapare.py - propunere automata + corectii manuale.
Ruleaza pe date sintetice in director temporar; nu scrie mapare_*.xlsx /
corectii_*.xlsx in radacina proiectului.
Rulare: py -m unittest discover -s tests -v
"""
import os
import pathlib
import sys
import tempfile
import unittest
import openpyxl
RADACINA = pathlib.Path(__file__).resolve().parent.parent
if str(RADACINA) not in sys.path:
sys.path.insert(0, str(RADACINA))
import mapare
def rand(cont, denumire="", sold=0.0):
return {"cont": cont, "denumire": denumire, "sold": sold}
class TestMapare(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory(prefix="mapare_test_")
self.dir = self._tmp.name
def tearDown(self):
self._tmp.cleanup()
def _cale(self, nume):
return os.path.join(self.dir, nume)
def _ruleaza(self, rows, firma="FIRMA"):
return mapare.genereaza_mapare(rows, firma, self.dir)
def _scrie_corectii(self, randuri, firma="FIRMA"):
cale = self._cale("corectii_%s.xlsx" % firma)
wb = openpyxl.load_workbook(cale)
ws = wb.active
for r in randuri:
ws.append(r)
wb.save(cale)
wb.close()
def _citeste_mapare(self, firma="FIRMA"):
wb = openpyxl.load_workbook(self._cale("mapare_%s.xlsx" % firma))
ws = wb.active
header = [c.value for c in ws[1]]
out = [dict(zip(header, [c.value for c in row]))
for row in ws.iter_rows(min_row=2)]
wb.close()
return out
def test_corectie_supravietuieste_regenerarii(self):
rows = [rand("2131.01", "CLADIRI", 100.0), rand("371.01", "MARFA", 50.0)]
self._ruleaza(rows)
cale = self._cale("corectii_FIRMA.xlsx")
self.assertTrue(os.path.exists(cale))
self._scrie_corectii([["371.01", "3711", "99", "", "corectie marfa"]])
inainte = pathlib.Path(cale).read_bytes()
rezultat = self._ruleaza(rows)
dupa = pathlib.Path(cale).read_bytes()
self.assertEqual(inainte, dupa, "regenerarea a modificat corectii_*.xlsx")
gasit = {r["cont_saga"]: r for r in rezultat}
self.assertEqual(
(gasit["371.01"]["cont"], gasit["371.01"]["acont"], gasit["371.01"]["sursa"]),
("3711", "99", "CORECTAT"))
self.assertEqual(gasit["2131.01"]["sursa"], "AUTO")
def test_ordine_exact_bate_restul_bate_toate(self):
rows = [rand("409.01"), rand("409.02"), rand("409.03")]
self._ruleaza(rows)
self._scrie_corectii([
["409.TOATE", "4091", "T", "", ""],
["409.RESTUL", "4091", "R", "", ""],
["409.01", "4091", "X", "", ""],
])
rezultat = self._ruleaza(rows)
gasit = {r["cont_saga"]: (r["cont"], r["acont"]) for r in rezultat}
self.assertEqual(gasit["409.01"], ("4091", "X"))
self.assertEqual(gasit["409.02"], ("4091", "R"))
self.assertEqual(gasit["409.03"], ("4091", "R"))
def test_toate_se_aplica_fara_exact(self):
rows = [rand("2131.01"), rand("2131.02")]
self._ruleaza(rows)
self._scrie_corectii([["2131.TOATE", "2131", "", "", ""]])
rezultat = self._ruleaza(rows)
for r in rezultat:
self.assertEqual((r["cont"], r["acont"]), ("2131", None))
self.assertEqual(r["sursa"], "CORECTAT")
def test_cont_saga_inexistent_da_eroare(self):
rows = [rand("2131.01")]
self._ruleaza(rows)
self._scrie_corectii([["9999.99", "9999", "99", "", "typo"]])
with self.assertRaises(mapare.Stop):
self._ruleaza(rows)
def test_coliziune_accidentala_da_eroare(self):
rows = [rand("409.01"), rand("410.01")]
self._ruleaza(rows)
self._scrie_corectii([
["409.01", "4091", "01", "", ""],
["410.01", "4091", "01", "", ""],
])
with self.assertRaises(mapare.Stop):
self._ruleaza(rows)
def test_colapsare_prin_toate_nu_da_eroare(self):
rows = [rand("409.01"), rand("409.02")]
self._ruleaza(rows)
self._scrie_corectii([["409.TOATE", "4091", "", "", "agregare intentionata"]])
rezultat = self._ruleaza(rows)
self.assertEqual([(r["cont"], r["acont"]) for r in rezultat],
[("4091", None), ("4091", None)])
def test_redenumire_sintetic(self):
rows = [rand("409"), rand("409.01")]
self._ruleaza(rows)
self._scrie_corectii([["409.TOATE", "4091", "", "", "redenumire sintetic"]])
rezultat = self._ruleaza(rows)
gasit = {r["cont_saga"]: r["cont"] for r in rezultat}
self.assertEqual(gasit["409"], "4091")
self.assertEqual(gasit["409.01"], "4091")
def test_cont_prea_lung_da_eroare(self):
rows = [rand("2131.01")]
self._ruleaza(rows)
self._scrie_corectii([["2131.01", "12345", "01", "", ""]])
with self.assertRaises(mapare.Stop):
self._ruleaza(rows)
def test_acont_prea_lung_da_eroare(self):
rows = [rand("2131.01")]
self._ruleaza(rows)
self._scrie_corectii([["2131.01", "2131", "12345", "", ""]])
with self.assertRaises(mapare.Stop):
self._ruleaza(rows)
def test_propunere_automata_si_partener(self):
rows = [rand("1171.01.12"), rand("401.00002")]
rezultat = mapare.aplica(rows, {}, parteneri={"401"})
gasit = {r["cont_saga"]: r for r in rezultat}
self.assertEqual((gasit["1171.01.12"]["cont"], gasit["1171.01.12"]["acont"]),
("1171", "0112"))
self.assertEqual((gasit["401.00002"]["cont"], gasit["401.00002"]["acont"]),
("401", None))
if __name__ == "__main__":
unittest.main()