Lane contract: config/conturi_parteneri.csv, divergenta Oracle, poarta de regresie
Setul de conturi cu parteneri iese din literali si trece in config/conturi_parteneri.csv, cu categorie si motiv. Comportamentul e neschimbat; lipsa fisierului opreste cu eroare, nu cade pe set gol. CONFIG_CONT_IREG ramane consultativ: tools/divergenta_parteneri.py raporteaza divergenta, nu o aplica. Constatarea (27 conturi in Oracle si nu la noi, 3 la noi si nu in Oracle - 5121, 5124, 5311) e scrisa in docs/decizii_import.md. tests/test_regresie.py compara iesirea codului de azi cu tests/golden/ pe valori de celula, prin anonimizatorul refolosit ca functie. Trece pe FUNDATIA si MASTER; verificat ca pica la o schimbare reala de set de conturi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYeAtVxeS8m4oXekjX8Am2
This commit is contained in:
@@ -46,6 +46,69 @@ COL_TIP = 5
|
||||
IBAN = re.compile(r"RO\s?\d{2}(?:\s?[A-Z0-9]{4}){5}", re.IGNORECASE)
|
||||
|
||||
|
||||
def citeste_corespondenta(cale=CORESP):
|
||||
"""Tabela real -> fals folosita la construirea etalonului. NU se regenereaza."""
|
||||
return json.loads(pathlib.Path(cale).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def anonimizeaza_foaie(ws, coresp):
|
||||
"""Anonimizeaza foaia `ws` in loc, completand tabela `coresp`. NU salveaza.
|
||||
|
||||
Intoarce (randuri FACTURA atinse, celule cu IBAN inlocuite).
|
||||
"""
|
||||
schimbate = 0
|
||||
for rand in range(2, ws.max_row + 1):
|
||||
if (ws.cell(rand, COL_TIP).value or "").strip().upper() != "FACTURA":
|
||||
continue
|
||||
schimbate += 1
|
||||
|
||||
nume = ws.cell(rand, COL_NUME).value
|
||||
if nume:
|
||||
if nume not in coresp["nume"]:
|
||||
coresp["nume"][nume] = "PARTENER %03d" % (len(coresp["nume"]) + 1)
|
||||
ws.cell(rand, COL_NUME).value = coresp["nume"][nume]
|
||||
|
||||
cf = ws.cell(rand, COL_COD_FISCAL).value
|
||||
if cf:
|
||||
cf = str(cf)
|
||||
if cf not in coresp["cod_fiscal"]:
|
||||
coresp["cod_fiscal"][cf] = "CF%06d" % (len(coresp["cod_fiscal"]) + 1)
|
||||
ws.cell(rand, COL_COD_FISCAL).value = coresp["cod_fiscal"][cf]
|
||||
|
||||
for col in (COL_REG_COM, COL_ADRESA, COL_EXPLICATIA):
|
||||
if ws.cell(rand, col).value:
|
||||
ws.cell(rand, col).value = None
|
||||
|
||||
# IBAN-urile apar si in afara randurilor FACTURA (denumirea analiticului de banca)
|
||||
ibanuri = 0
|
||||
for rand in ws.iter_rows():
|
||||
for celula in rand:
|
||||
if not isinstance(celula.value, str) or not IBAN.search(celula.value):
|
||||
continue
|
||||
def fals(m, _c=coresp):
|
||||
real = m.group(0)
|
||||
if real not in _c["iban"]:
|
||||
_c["iban"][real] = "RO00 IBAN %04d" % (len(_c["iban"]) + 1)
|
||||
return _c["iban"][real]
|
||||
|
||||
celula.value = IBAN.sub(fals, celula.value)
|
||||
ibanuri += 1
|
||||
return schimbate, ibanuri
|
||||
|
||||
|
||||
def anonimizeaza_fisier(sursa, tinta, coresp=None):
|
||||
"""Scrie in `tinta` o copie anonimizata a lui `sursa`, cu aceleasi reguli.
|
||||
|
||||
Tabela implicita e cea comisa (CORESP); nu o regenereaza, doar o citeste.
|
||||
"""
|
||||
if coresp is None:
|
||||
coresp = citeste_corespondenta()
|
||||
wb = openpyxl.load_workbook(sursa)
|
||||
schimbate, ibanuri = anonimizeaza_foaie(wb.active, coresp)
|
||||
wb.save(tinta)
|
||||
return schimbate, ibanuri
|
||||
|
||||
|
||||
def anonimizeaza():
|
||||
GOLDEN.mkdir(parents=True, exist_ok=True)
|
||||
coresp = {"nume": {}, "cod_fiscal": {}, "iban": {}}
|
||||
@@ -61,44 +124,7 @@ def anonimizeaza():
|
||||
shutil.copy(cale, GOLDEN / tinta)
|
||||
wb = openpyxl.load_workbook(GOLDEN / tinta)
|
||||
ws = wb.active
|
||||
schimbate = 0
|
||||
|
||||
for rand in range(2, ws.max_row + 1):
|
||||
if (ws.cell(rand, COL_TIP).value or "").strip().upper() != "FACTURA":
|
||||
continue
|
||||
schimbate += 1
|
||||
|
||||
nume = ws.cell(rand, COL_NUME).value
|
||||
if nume:
|
||||
if nume not in coresp["nume"]:
|
||||
coresp["nume"][nume] = "PARTENER %03d" % (len(coresp["nume"]) + 1)
|
||||
ws.cell(rand, COL_NUME).value = coresp["nume"][nume]
|
||||
|
||||
cf = ws.cell(rand, COL_COD_FISCAL).value
|
||||
if cf:
|
||||
cf = str(cf)
|
||||
if cf not in coresp["cod_fiscal"]:
|
||||
coresp["cod_fiscal"][cf] = "CF%06d" % (len(coresp["cod_fiscal"]) + 1)
|
||||
ws.cell(rand, COL_COD_FISCAL).value = coresp["cod_fiscal"][cf]
|
||||
|
||||
for col in (COL_REG_COM, COL_ADRESA, COL_EXPLICATIA):
|
||||
if ws.cell(rand, col).value:
|
||||
ws.cell(rand, col).value = None
|
||||
|
||||
# IBAN-urile apar si in afara randurilor FACTURA (denumirea analiticului de banca)
|
||||
ibanuri = 0
|
||||
for rand in ws.iter_rows():
|
||||
for celula in rand:
|
||||
if not isinstance(celula.value, str) or not IBAN.search(celula.value):
|
||||
continue
|
||||
def fals(m, _c=coresp):
|
||||
real = m.group(0)
|
||||
if real not in _c["iban"]:
|
||||
_c["iban"][real] = "RO00 IBAN %04d" % (len(_c["iban"]) + 1)
|
||||
return _c["iban"][real]
|
||||
|
||||
celula.value = IBAN.sub(fals, celula.value)
|
||||
ibanuri += 1
|
||||
schimbate, ibanuri = anonimizeaza_foaie(ws, coresp)
|
||||
|
||||
wb.save(GOLDEN / tinta)
|
||||
raport.append((tinta, ws.max_row - 1, schimbate, ibanuri))
|
||||
|
||||
123
tests/test_regresie.py
Normal file
123
tests/test_regresie.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Poarta de regresie: codul de conversie de azi trebuie sa reproduca etalonul.
|
||||
|
||||
Ruleaza convertorul pe balantele CSV de pe disc (FUNDATIA, MASTER), scrie xlsx-urile
|
||||
de initializare intr-un director temporar (nu peste cele din radacina), le
|
||||
anonimizeaza cu ACELEASI reguli si ACEIASI tabela ca tests/anonimizeaza_etalon.py si
|
||||
le compara cu tests/golden/ pe VALORI DE CELULA.
|
||||
|
||||
Comparatia nu se face niciodata pe hash de fisier: un xlsx rescris de alta versiune
|
||||
de openpyxl are alti octeti cu acelasi continut.
|
||||
|
||||
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
|
||||
TESTS = pathlib.Path(__file__).resolve().parent
|
||||
for cale in (str(RADACINA), str(TESTS)):
|
||||
if cale not in sys.path:
|
||||
sys.path.insert(0, cale)
|
||||
|
||||
import genereaza_xlsx as generator
|
||||
import anonimizeaza_etalon as anonim
|
||||
|
||||
CORESP = TESTS / "_corespondenta_anonimizare.json"
|
||||
GOLDEN = TESTS / "golden"
|
||||
|
||||
FIRME = [
|
||||
("FUNDATIA", "balanta_FUNDATIA.csv", "golden_FUNDATIA_2025_12.xlsx"),
|
||||
("MASTER", "balanta_MASTERJOB.csv", "golden_MASTER_2025_12.xlsx"),
|
||||
]
|
||||
|
||||
MAX_DIFERENTE_RAPORTATE = 20
|
||||
|
||||
|
||||
class TestRegresieEtalon(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not CORESP.exists():
|
||||
raise unittest.SkipTest(
|
||||
"Lipseste %s (e ignorata de git). Etalonul nu se poate reproduce "
|
||||
"fara tabela de corespondenta - vezi docs/handoff_etapa2_convertor.md, "
|
||||
"sectiunea despre etalon." % CORESP
|
||||
)
|
||||
cls.coresp = anonim.citeste_corespondenta(CORESP)
|
||||
|
||||
cls._cwd = os.getcwd()
|
||||
os.chdir(str(RADACINA)) # TEMPLATE si CSV-urile sunt relative la radacina
|
||||
cls._tmp = tempfile.TemporaryDirectory(prefix="regresie_")
|
||||
tmpdir = pathlib.Path(cls._tmp.name)
|
||||
cls.iesiri = {}
|
||||
try:
|
||||
header = generator.citeste_header()
|
||||
for firma, csv_nume, golden_nume in FIRME:
|
||||
rows = generator.citeste_balanta(str(RADACINA / csv_nume))
|
||||
out, _, _, _, _ = generator.genereaza(rows, header)
|
||||
brut = tmpdir / ("init_%s_2025_12.xlsx" % firma)
|
||||
generator.scrie_xlsx(header, out, str(brut))
|
||||
anonimizat = tmpdir / ("anon_%s.xlsx" % firma)
|
||||
anonim.anonimizeaza_fisier(str(brut), str(anonimizat), cls.coresp)
|
||||
cls.iesiri[firma] = (anonimizat, GOLDEN / golden_nume)
|
||||
except Exception:
|
||||
os.chdir(cls._cwd)
|
||||
cls._tmp.cleanup()
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
os.chdir(cls._cwd)
|
||||
cls._tmp.cleanup()
|
||||
|
||||
def _verifica(self, firma):
|
||||
anonimizat, golden = self.iesiri[firma]
|
||||
self.assertTrue(golden.exists(), "lipseste etalonul %s" % golden)
|
||||
wb_gasit = openpyxl.load_workbook(anonimizat)
|
||||
wb_asteptat = openpyxl.load_workbook(golden)
|
||||
ws_gasit, ws_asteptat = wb_gasit.active, wb_asteptat.active
|
||||
|
||||
nr_g = ws_gasit.max_row
|
||||
nc_g = ws_gasit.max_column
|
||||
nr_a = ws_asteptat.max_row
|
||||
nc_a = ws_asteptat.max_column
|
||||
self.assertEqual(
|
||||
(nr_g, nc_g), (nr_a, nc_a),
|
||||
"Dimensiuni diferite pentru %s: asteptat %d randuri x %d coloane, "
|
||||
"gasit %d randuri x %d coloane" % (golden.name, nr_a, nc_a, nr_g, nc_g),
|
||||
)
|
||||
|
||||
diferente = []
|
||||
for rand in range(1, max(nr_g, nr_a) + 1):
|
||||
for col in range(1, max(nc_g, nc_a) + 1):
|
||||
gasit = ws_gasit.cell(rand, col).value
|
||||
asteptat = ws_asteptat.cell(rand, col).value
|
||||
if gasit != asteptat:
|
||||
diferente.append((rand, col, asteptat, gasit))
|
||||
|
||||
if diferente:
|
||||
linii = ["%d celule difera fata de %s:" % (len(diferente), golden.name)]
|
||||
for rand, col, asteptat, gasit in diferente[:MAX_DIFERENTE_RAPORTATE]:
|
||||
linii.append(
|
||||
" %s: rand %d, coloana %d: asteptat %r, gasit %r"
|
||||
% (golden.name, rand, col, asteptat, gasit)
|
||||
)
|
||||
if len(diferente) > MAX_DIFERENTE_RAPORTATE:
|
||||
linii.append(" ... si alte %d celule"
|
||||
% (len(diferente) - MAX_DIFERENTE_RAPORTATE))
|
||||
self.fail("\n".join(linii))
|
||||
|
||||
def test_fundatia(self):
|
||||
self._verifica("FUNDATIA")
|
||||
|
||||
def test_master(self):
|
||||
self._verifica("MASTER")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user