Reguli text per cont (operation_text_rules), resolve_prestatii cu param aditiv text_rules + precedenta stricta, threadat pe toate cele 6 callsite-uri + valid_codes + seam classify_prezentare. UI Mapari: sectiune reguli + preview pre-salvare + overlap + telemetrie text_rule_hit. UX tabel: cod_rar sub operatie, pill eticheta scurta, fara scroll orizontal (scopat .tabel-trimiteri + carduri <768px), detaliu inline expandabil (a11y + pauza poll). code-review: reparat regula auto_send=0 care trimitea automat la RAR in loc sa tina randul pentru review. 814 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
334 lines
12 KiB
Python
334 lines
12 KiB
Python
"""
|
|
labels.py — traducere stari tehnice in text uman + clasa CSS (US-001, PRD 3.4).
|
|
|
|
Functii pure: fara DB, fara request. Usor de testat unitar si de importat in template-uri.
|
|
|
|
Sursa de adevar pentru texte: tabelul din PRD 3.4 §3 US-001.
|
|
"""
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from typing import Tuple
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tipul returnat: (text_principal, subtext_tooltip, css_class)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
Eticheta = Tuple[str, str, str]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Etichete stari submissions
|
|
# Clasele CSS corespund celor definite in base.html:
|
|
# s-queued (accent/albastru), s-sending (warn/galben), s-sent (ok/verde),
|
|
# s-error, s-needs_data, s-needs_mapping (err/rosu).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
STARI_SUBMISSION: dict[str, Eticheta] = {
|
|
"queued": (
|
|
"In asteptare sa fie trimise",
|
|
"",
|
|
"s-queued",
|
|
),
|
|
"sending": (
|
|
"Se trimite acum",
|
|
"",
|
|
"s-sending",
|
|
),
|
|
"sent": (
|
|
"Declarate la RAR (finalizate)",
|
|
"Confirmate cu numar de prezentare; nu se mai pot modifica.",
|
|
"s-sent",
|
|
),
|
|
"needs_mapping": (
|
|
"Lipseste codul prestatiei",
|
|
"Alege codul RAR in tab-ul Mapari.",
|
|
"s-needs_mapping",
|
|
),
|
|
"needs_data": (
|
|
"Date incomplete (respinse de RAR)",
|
|
"Corecteaza randul si reimporta.",
|
|
"s-needs_data",
|
|
),
|
|
"error": (
|
|
"Eroare la trimitere",
|
|
"Trimiterea a esuat si nu se mai reincearca automat. Vezi detaliul randului; "
|
|
"daca tine de credentialele RAR, corecteaza-le in Cont.",
|
|
"s-error",
|
|
),
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Etichete scurte (pill) pentru coloana Stare din tabelul de trimiteri (US-006)
|
|
# Dict propriu — NU element in tuple Eticheta (ar rupe template-urile care
|
|
# despacheteaza 3 elemente). eticheta_stare ramane neatinsa.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ETICHETE_SCURTE: dict[str, str] = {
|
|
"queued": "In coada",
|
|
"sending": "Se trimite",
|
|
"sent": "Finalizat",
|
|
"needs_mapping": "De mapat",
|
|
"needs_data": "Date lipsa",
|
|
"error": "Eroare",
|
|
}
|
|
|
|
|
|
def eticheta_scurta(status: str) -> str:
|
|
"""
|
|
Returneaza eticheta compacta (pill) pentru o stare de submission.
|
|
|
|
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
|
|
stari noi adaugate in schema fara mapare corespunzatoare.
|
|
"""
|
|
try:
|
|
return ETICHETE_SCURTE[status]
|
|
except KeyError:
|
|
raise KeyError(
|
|
f"Starea de submission {status!r} nu are eticheta scurta in labels.py. "
|
|
"Adauga-o in ETICHETE_SCURTE."
|
|
)
|
|
|
|
|
|
def eticheta_stare(status: str) -> Eticheta:
|
|
"""
|
|
Returneaza (text, subtext, css_class) pentru o stare de submission.
|
|
|
|
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
|
|
stari noi adaugate in schema fara mapare corespunzatoare.
|
|
"""
|
|
try:
|
|
return STARI_SUBMISSION[status]
|
|
except KeyError:
|
|
raise KeyError(
|
|
f"Starea de submission {status!r} nu are eticheta umana in labels.py. "
|
|
"Adauga-o in STARI_SUBMISSION."
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Etichete worker (viu / mort)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def eticheta_worker(viu: bool) -> Eticheta:
|
|
"""
|
|
Returneaza (text, subtext, css_class) pentru starea worker-ului.
|
|
|
|
viu=True => "Trimitere automata: activa" (clasa s-sent / verde)
|
|
viu=False => "Trimitere automata: oprita" (clasa s-error / rosu)
|
|
"""
|
|
if viu:
|
|
return (
|
|
"Trimitere automata: activa",
|
|
"Sistemul verifica coada si trimite la RAR la fiecare cateva secunde.",
|
|
"s-sent",
|
|
)
|
|
return (
|
|
"Trimitere automata: oprita",
|
|
"Nimic nu pleaca spre RAR pana reporneste. Anunta administratorul.",
|
|
"s-error",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Etichete conexiune RAR (ok / indisponibil)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def eticheta_rar(stare: str) -> Eticheta:
|
|
"""
|
|
Returneaza (text, subtext, css_class) pentru starea conexiunii cu RAR.
|
|
|
|
stare="ok" => "Legatura cu RAR: functionala" (s-sent / verde)
|
|
stare="indisponibil" => "Legatura cu RAR: indisponibila" (s-error / rosu)
|
|
"""
|
|
if stare == "ok":
|
|
return (
|
|
"Legatura cu RAR: functionala",
|
|
"Portalul AUTOPASS raspunde.",
|
|
"s-sent",
|
|
)
|
|
return (
|
|
"Legatura cu RAR: indisponibila",
|
|
"Portalul RAR nu raspunde acum; coada se reia automat cand revine.",
|
|
"s-error",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Format data RAR (US-001, PRD 3.5)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def format_data_rar(raw: object) -> str:
|
|
"""Formateaza un timestamp ISO ca `dd.mm.yyyy hh24:mi:ss` (ora romaneasca).
|
|
|
|
- Valoare lipsa (None / "") -> "—".
|
|
- ISO valid (cu sau fara timezone / 'Z' / microsecunde) -> data formatata,
|
|
fara fractiuni de secunda.
|
|
- Format invalid -> fallback grijuliu: intoarce stringul brut (nu arunca),
|
|
ca operatorul sa vada totusi ceva, nu o pagina rupta.
|
|
"""
|
|
if raw is None:
|
|
return "—"
|
|
s = str(raw).strip()
|
|
if not s:
|
|
return "—"
|
|
iso = s.replace("Z", "+00:00") if s.endswith("Z") else s
|
|
try:
|
|
dt = datetime.fromisoformat(iso)
|
|
except (ValueError, TypeError):
|
|
return s
|
|
return dt.strftime("%d.%m.%Y %H:%M:%S")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Motiv uman din rar_error (US-004, PRD 3.5)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def motiv_uman(status: str, rar_error: object) -> str:
|
|
"""Transforma `rar_error` (JSON tehnic) intr-un motiv lizibil pentru coloana Motiv.
|
|
|
|
Formele intalnite (vezi router.py / mapping.py):
|
|
- validare continut: list[{field, message}] -> mesajele concatenate.
|
|
- operatie nemapata: {"unmapped": [{cod_op_service, denumire}]}.
|
|
- auto-send oprit: {"auto_send": "..."}.
|
|
- eroare RAR: text simplu sau dict generic.
|
|
Fara rar_error -> "". Nu arunca niciodata (degradeaza la text brut trunchiat).
|
|
"""
|
|
if not rar_error:
|
|
return ""
|
|
raw = rar_error if isinstance(rar_error, str) else str(rar_error)
|
|
try:
|
|
data = json.loads(raw)
|
|
except (ValueError, TypeError):
|
|
return raw[:160]
|
|
|
|
if isinstance(data, dict):
|
|
if "unmapped" in data:
|
|
ops = data.get("unmapped") or []
|
|
nume = ", ".join(
|
|
(o.get("cod_op_service") or "") for o in ops if isinstance(o, dict)
|
|
).strip(", ")
|
|
return f"Cod RAR lipsa pentru: {nume}" if nume else "Cod RAR lipsa"
|
|
if "auto_send" in data:
|
|
return "Necesita confirmare manuala (auto-send oprit pentru cod)"
|
|
if "problema" in data:
|
|
return str(data.get("problema") or "")[:200]
|
|
parti = [f"{k}: {v}" for k, v in data.items()]
|
|
return "; ".join(parti)[:200]
|
|
|
|
if isinstance(data, list):
|
|
msgs: list[str] = []
|
|
for e in data:
|
|
if isinstance(e, dict):
|
|
msgs.append(
|
|
str(e.get("message") or e.get("msg") or "; ".join(str(v) for v in e.values()))
|
|
)
|
|
else:
|
|
msgs.append(str(e))
|
|
return "; ".join(m for m in msgs if m)[:200]
|
|
|
|
return str(data)[:160]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parse_erori — transforma rar_error in lista 3-niveluri (US-006, PRD 5.4)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def parse_erori(rar_error: object) -> list[dict]:
|
|
"""Transforma `rar_error` (JSON stocat) intr-o lista de erori 3-niveluri.
|
|
|
|
Fiecare element al listei are cheile: problema, cauza, fix, field (sau None).
|
|
Functie PURA — nu arunca niciodata exceptii; degradeaza gratios pe orice forma.
|
|
|
|
Forme recunoscute:
|
|
- None / "" / falsy -> lista goala []
|
|
- array imbogatit (au cod sau problema) -> un element per eroare
|
|
- dict cu cod specific -> 1 element cu cele 3 niveluri din dict
|
|
- dict fara cod (forma veche: unmapped / auto_send) -> 1 element cu problema din context
|
|
- lista cu {field, message} fara cod -> degradare: problema=message, cauza/fix=""
|
|
- string plain -> 1 element cu problema=text, cauza/fix=""
|
|
- JSON corupt -> 1 element cu problema=text brut, cauza/fix=""
|
|
"""
|
|
if not rar_error:
|
|
return []
|
|
|
|
raw = rar_error if isinstance(rar_error, str) else str(rar_error)
|
|
|
|
# Incercare parsare JSON
|
|
try:
|
|
data = json.loads(raw)
|
|
except (ValueError, TypeError):
|
|
# String plain sau JSON corupt: degradare gratuoasa
|
|
return [{"problema": raw[:200], "cauza": "", "fix": "", "field": None}]
|
|
|
|
# --- Forma: array de erori ---
|
|
if isinstance(data, list):
|
|
rezultat = []
|
|
for e in data:
|
|
if not isinstance(e, dict):
|
|
rezultat.append({"problema": str(e)[:200], "cauza": "", "fix": "", "field": None})
|
|
continue
|
|
# Eroare imbogatita (are cod sau problema)
|
|
if e.get("cod") or e.get("problema"):
|
|
rezultat.append({
|
|
"problema": e.get("problema") or e.get("cod") or "",
|
|
"cauza": e.get("cauza") or e.get("message") or "",
|
|
"fix": e.get("fix") or "",
|
|
"field": e.get("field"),
|
|
})
|
|
else:
|
|
# Forma veche: {field, message} fara cod
|
|
msg = str(e.get("message") or e.get("msg") or "; ".join(str(v) for v in e.values()))
|
|
elem = {
|
|
"problema": msg[:200],
|
|
"cauza": "",
|
|
"fix": "",
|
|
"field": e.get("field"),
|
|
}
|
|
# Filtreaza elementele complet goale (problema/cauza/fix toate vide)
|
|
if not (
|
|
elem["problema"].strip() == ""
|
|
and elem["cauza"].strip() == ""
|
|
and elem["fix"].strip() == ""
|
|
):
|
|
rezultat.append(elem)
|
|
return rezultat
|
|
|
|
# --- Forma: dict ---
|
|
if isinstance(data, dict):
|
|
# Dict imbogatit cu cod explicit
|
|
if data.get("cod") or data.get("problema"):
|
|
return [{
|
|
"problema": data.get("problema") or data.get("cod") or "",
|
|
"cauza": data.get("cauza") or "",
|
|
"fix": data.get("fix") or "",
|
|
"field": data.get("field"),
|
|
}]
|
|
# Dict vechi: unmapped
|
|
if "unmapped" in data:
|
|
ops = data.get("unmapped") or []
|
|
coduri = ", ".join(
|
|
(o.get("cod_op_service") or "") for o in ops if isinstance(o, dict)
|
|
).strip(", ")
|
|
problema = f"Cod RAR lipsa pentru: {coduri}" if coduri else "Cod RAR lipsa"
|
|
return [{"problema": problema, "cauza": "", "fix": "", "field": None}]
|
|
# Dict vechi: auto_send
|
|
if "auto_send" in data:
|
|
return [{"problema": "Necesita confirmare manuala (auto-send oprit pentru cod)",
|
|
"cauza": "", "fix": "", "field": None}]
|
|
# Dict generic necunoscut
|
|
parti = "; ".join(f"{k}: {v}" for k, v in data.items())
|
|
if not parti.strip():
|
|
return []
|
|
return [{"problema": parti[:200], "cauza": "", "fix": "", "field": None}]
|
|
|
|
# Scalar (nr, bool, etc.)
|
|
return [{"problema": str(data)[:200], "cauza": "", "fix": "", "field": None}]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constante auxiliare (microcopy fix, fara logica)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ETICHETA_ULTIMA_AUTENTIFICARE_RAR = "Ultima autentificare la RAR"
|