"""Propunere automata de mapare SAGA -> ROA, cu corectii manuale, per firma. La fiecare rulare: 1. se citeste balanta (rows); 2. se propune automat `cont`/`acont` (regula din docs/plan_etapa2_convertor.md); 3. se suprapun corectiile din `corectii_.xlsx`, daca exista; 4. se scrie `mapare_.xlsx` complet. Doua fisiere, proprietari diferiti: - `mapare_.xlsx` - scris DOAR de unealta, regenerat complet; - `corectii_.xlsx` - scris DOAR de utilizator; se creeaza o singura data, gol, cu antet si un exemplu comentat. Unealta nu scrie niciodata in el, deci regenerarea nu poate distruge corectiile. Propunerea automata: `cont` = sinteticul SAGA, `acont` = restul analiticului (`1171.01.12` -> `cont=1171`, `acont=0112`). La conturile cu parteneri `acont` NU se pre-completa: forma lui o decide `genereaza_xlsx.py` (pct. 16 din docs/decizii_import.md). Conventii in `corectii_`, pe coloana `cont_saga`: - `` exact; - `.TOATE` - toate conturile sinteticului merg pe acelasi `cont`/`acont`; - `.RESTUL` - la fel, doar pentru cele fara potrivire exacta. Ordinea de cautare: potrivire exacta -> `.RESTUL` -> `.TOATE`. """ import csv import os import re from openpyxl import Workbook, load_workbook COLOANE_MAPARE = ["cont_saga", "denumire", "sold", "cont", "acont", "sursa"] COLOANE_CORECTII = ["cont_saga", "cont", "acont", "exclus", "motiv"] SURSA_AUTO = "AUTO" SURSA_CORECTAT = "CORECTAT" MAX = 4 PATTERN_GRUP = re.compile(r"^(.+)\.(TOATE|RESTUL)$") CONFIG_PARTENERI = os.path.join( os.path.dirname(os.path.abspath(__file__)), "config", "conturi_parteneri.csv") # Randul-exemplu din corectii: incepe cu '#' pe cont_saga, deci e ignorat la citire. EXEMPLU = ["# exemplu (sterge-l): 409.TOATE", "# 4091", "# (gol)", "", "# randurile cu '#' in cont_saga sunt comentarii; gol = pastreaza propunerea automata"] class Stop(Exception): """Conditie neprevazuta: nu ghicim, raportam.""" def citeste_parteneri(path=CONFIG_PARTENERI): """Sinteticele cu parteneri din config/conturi_parteneri.csv (doar citire).""" if not os.path.exists(path): raise Stop("lipseste fisierul de configurare a partenerilor: %s" % path) conturi = set() with open(path, encoding="utf-8-sig", newline="") as f: for r in csv.DictReader(f): cont = (r.get("cont") or "").strip() if cont: conturi.add(cont) return conturi def separa(cont_saga): """(sintetic, analitic): partea dinaintea punctului si restul, fara punct.""" seg = cont_saga.split(".") return seg[0], "".join(seg[1:]) def propune(cont_saga, partener=False): """Propunerea automata; la parteneri acont ramane gol (decide generatorul).""" cont, acont = separa(cont_saga) if partener: return cont, None return cont, (acont or None) def _text(v): """Celula ca text curatat; gol -> None. Numerele intregi devin text fara .0.""" if v is None or isinstance(v, bool): return None if isinstance(v, float) and v.is_integer(): v = int(v) s = str(v).strip() return s or None def _adevar(v): if isinstance(v, bool): return v text = _text(v) return text.lower() in ("1", "da", "true", "x", "yes") if text else False def creeaza_corectii(path): """Creeaza corectii_.xlsx gol, cu antet si un exemplu comentat.""" wb = Workbook() ws = wb.active ws.title = "corectii" ws.append(COLOANE_CORECTII) ws.append(EXEMPLU) wb.save(path) def citeste_corectii(path): """Corectiile din xlsx, cheie = cont_saga. Randurile '#' sunt comentarii.""" if not os.path.exists(path): return {} wb = load_workbook(path, read_only=True, data_only=True) try: ws = wb.active header = [_text(c.value) for c in ws[1]] while header and header[-1] is None: header.pop() if header != COLOANE_CORECTII: raise Stop("antet neasteptat in %s: %r" % (os.path.basename(path), header)) out = {} for row in ws.iter_rows(min_row=2, values_only=True): valori = dict(zip(COLOANE_CORECTII, list(row) + [None] * (len(COLOANE_CORECTII) - len(row)))) cheie = _text(valori["cont_saga"]) if cheie is None or cheie.startswith("#"): continue if cheie in out: raise Stop("rand duplicat in corectii pentru %s" % cheie) out[cheie] = { "cont": _text(valori["cont"]), "acont": _text(valori["acont"]), "exclus": _adevar(valori["exclus"]), "motiv": valori["motiv"], } return out finally: wb.close() def _regula(cont_saga, corectii): """(corectie, cheia_regulii) dupa exact -> .RESTUL -> .TOATE; (None, None) daca nu.""" if cont_saga in corectii: return corectii[cont_saga], cont_saga sintetic = cont_saga.split(".")[0] for sufix in ("RESTUL", "TOATE"): cheie = "%s.%s" % (sintetic, sufix) if cheie in corectii: return corectii[cheie], cheie return None, None def _sold(r): if r.get("sold") is not None: return round(float(r["sold"]), 2) return round(float(r.get("total_d", 0.0)) - float(r.get("total_c", 0.0)), 2) def _verifica_lungime(cont, acont, cont_saga): if cont and len(cont) > MAX: raise Stop("cont peste 4 caractere pentru %s: %r" % (cont_saga, cont)) if acont and len(acont) > MAX: raise Stop("acont peste 4 caractere pentru %s: %r" % (cont_saga, acont)) def _verifica_corectii(rows, corectii): existente = {r["cont"] for r in rows} sintetice = {c.split(".")[0] for c in existente} for cheie in corectii: m = PATTERN_GRUP.match(cheie) if m: if m.group(1) not in sintetice: raise Stop("corectie pentru un sintetic inexistent in balanta: %s" % cheie) elif cheie not in existente: raise Stop("corectie pentru un cont inexistent in balanta: %s" % cheie) def _verifica_coliziuni(rezultat, parteneri): """Doua conturi SAGA pe acelasi (cont, acont) sunt legale doar sub acelasi .TOATE/.RESTUL.""" grupe = {} for r in rezultat: if r["cont_saga"].split(".")[0] in parteneri or r["exclus"]: continue grupe.setdefault((r["cont"], r["acont"]), []).append(r) for (cont, acont), randuri in grupe.items(): if len(randuri) < 2: continue reguli = {r["_regula"] for r in randuri} singura = next(iter(reguli)) if len(reguli) == 1 else None if not (singura and PATTERN_GRUP.match(singura)): conturi = sorted(r["cont_saga"] for r in randuri) raise Stop("coliziune pe (%r, %r) intre %s; foloseste .TOATE/.RESTUL " "daca agregarea e intentionata" % (cont, acont, conturi)) def aplica(rows, corectii, parteneri=None): """Rezultatul final per cont: cont_saga, denumire, sold, cont, acont, sursa, exclus.""" if parteneri is None: parteneri = citeste_parteneri() _verifica_corectii(rows, corectii) rezultat = [] for r in rows: cont_saga = r["cont"] partener = cont_saga.split(".")[0] in parteneri cont_auto, acont_auto = propune(cont_saga, partener) cor, cheie = _regula(cont_saga, corectii) if cor is None: cont, acont = cont_auto, acont_auto sursa, exclus = SURSA_AUTO, False else: if cor["exclus"]: cont, acont = cont_auto, acont_auto else: cont = cor["cont"] or cont_auto acont = cor["acont"] sursa, exclus = SURSA_CORECTAT, cor["exclus"] _verifica_lungime(cont, acont, cont_saga) rezultat.append({ "cont_saga": cont_saga, "denumire": r.get("denumire") or "", "sold": _sold(r), "cont": cont, "acont": acont, "sursa": sursa, "exclus": exclus, "_regula": cheie, }) _verifica_coliziuni(rezultat, parteneri) for r in rezultat: r.pop("_regula") return rezultat def scrie_mapare(rezultat, path): """Scrie mapare_.xlsx complet, doar din rezultatul final.""" wb = Workbook() ws = wb.active ws.title = "mapare" ws.append(COLOANE_MAPARE) for r in rezultat: ws.append([r[c] for c in COLOANE_MAPARE]) wb.save(path) def genereaza_mapare(rows, firma, dir_iesire="."): """Scrie mapare_.xlsx, creeaza corectii_.xlsx doar daca lipseste.""" if not os.path.isdir(dir_iesire): os.makedirs(dir_iesire) cale_corectii = os.path.join(dir_iesire, "corectii_%s.xlsx" % firma) cale_mapare = os.path.join(dir_iesire, "mapare_%s.xlsx" % firma) if not os.path.exists(cale_corectii): creeaza_corectii(cale_corectii) rezultat = aplica(rows, citeste_corectii(cale_corectii)) scrie_mapare(rezultat, cale_mapare) return rezultat