date/ - intrari de la client (PDF-urile, exportul SAGA) si balanta intermediara iesiri/ - xlsx-urile generate si verificare_<FIRMA>.md (ignorat de git) sablon/ - sablonul ROA Punctele de intrare scriu acolo implicit: extrage.py --dir=date, genereaza.py citeste din date/ si scrie in iesiri/ (--dir-iesire), ca radacina sa nu se murdareasca la fiecare rulare. Sablonul e cautat langa script, in sablon/. Actualizate: TEMPLATE, cele trei teste care tin caile, .gitignore, CLAUDE.md si handoff-ul. Poarta capat-la-capat incepuse sa sara tacut dupa mutarea PDF-urilor (skipTest pe "lipsesc PDF-urile"); acum arata din nou catre date/ si ruleaza. 33 de teste trec, zero skip-uri. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYeAtVxeS8m4oXekjX8Am2
147 lines
5.2 KiB
Python
147 lines
5.2 KiB
Python
"""Produce etalonul de regresie: copii anonimizate ale xlsx-urilor importate in productie.
|
|
|
|
Ce se pastreaza NEATINS: conturile, sumele, structura si numarul randurilor, datele, tipul
|
|
randului. Asta este ce verifica poarta de regresie.
|
|
Ce se inlocuieste:
|
|
- pe randurile FACTURA: numele partenerului, codul fiscal, registrul comertului, adresa,
|
|
explicatia;
|
|
- ORIUNDE in fisier: IBAN-urile. Randurile BALANTA de pe 5121/5124 au in `nume` denumirea
|
|
analiticului, care la conturile de banca este chiar IBAN-ul clientului
|
|
(`RO73 BACX 0000 0008 7243 0001`). Restul denumirilor de cont (CAPITAL SOCIAL, REZULTAT
|
|
REPORTAT, DEBITORI DIVERSI) sunt din planul de conturi si raman.
|
|
|
|
Inlocuirea e stabila: acelasi nume real primeste mereu acelasi nume fals, ca relatiile dintre
|
|
randuri (mai multe facturi ale aceluiasi partener) sa se pastreze.
|
|
|
|
Tabela de corespondenta se scrie in tests/_corespondenta_anonimizare.json, care este IGNORAT de
|
|
git. Nu o comite: cu ea, anonimizarea se poate desface.
|
|
|
|
Rulare: py tests\anonimizeaza_etalon.py
|
|
"""
|
|
import json
|
|
import pathlib
|
|
import re
|
|
import shutil
|
|
|
|
import openpyxl
|
|
|
|
RADACINA = pathlib.Path(__file__).resolve().parent.parent
|
|
GOLDEN = RADACINA / "tests" / "golden"
|
|
CORESP = RADACINA / "tests" / "_corespondenta_anonimizare.json"
|
|
|
|
SURSE = [
|
|
("init_FUNDATIA_2025_12.xlsx", "golden_FUNDATIA_2025_12.xlsx"),
|
|
("init_MASTER_2025_12.xlsx", "golden_MASTER_2025_12.xlsx"),
|
|
]
|
|
|
|
# coloana (1-based) -> cum se falsifica
|
|
COL_NUME = 10
|
|
COL_COD_FISCAL = 11
|
|
COL_REG_COM = 12
|
|
COL_ADRESA = 13
|
|
COL_EXPLICATIA = 14
|
|
COL_TIP = 5
|
|
|
|
# IBAN romanesc, cu sau fara spatii: RO + 2 cifre + 4 litere + 16 caractere alfanumerice
|
|
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": {}}
|
|
raport = []
|
|
|
|
for sursa, tinta in SURSE:
|
|
cale = RADACINA / "iesiri" / sursa
|
|
if not cale.exists():
|
|
raise SystemExit(
|
|
"Lipseste iesiri/%s. Etalonul nu se poate construi - vezi docs/handoff_etapa2_convertor.md"
|
|
% sursa
|
|
)
|
|
shutil.copy(cale, GOLDEN / tinta)
|
|
wb = openpyxl.load_workbook(GOLDEN / tinta)
|
|
ws = wb.active
|
|
schimbate, ibanuri = anonimizeaza_foaie(ws, coresp)
|
|
|
|
wb.save(GOLDEN / tinta)
|
|
raport.append((tinta, ws.max_row - 1, schimbate, ibanuri))
|
|
|
|
CORESP.write_text(json.dumps(coresp, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
|
|
for tinta, randuri, schimbate, ibanuri in raport:
|
|
print(
|
|
"%-34s %4d randuri, %3d FACTURA anonimizate, %2d celule cu IBAN"
|
|
% (tinta, randuri, schimbate, ibanuri)
|
|
)
|
|
print(
|
|
"%d nume, %d coduri fiscale si %d IBAN-uri inlocuite. Corespondenta: %s (ignorata de git)"
|
|
% (len(coresp["nume"]), len(coresp["cod_fiscal"]), len(coresp["iban"]), CORESP.name)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
anonimizeaza()
|