data_prestatie traieste in payload_json, nu e coloana SQL: filtrarea se aplica in Python dupa derivarea prezentarii (fetch scope+status fara LIMIT, filtreaza, apoi LIMIT dupa filtrare), acelasi pattern deja folosit de dashboard-ul web (/_fragments/submissions). Extrage helper-ul iso_date_prefix in payload_view.py ca sa fie partajat intre canalul web si cel API (elimina duplicarea). Format invalid pe data_de/data_pana -> 422. Fara parametri, comportamentul raspunsului ramane identic cu inainte.
189 lines
6.9 KiB
Python
189 lines
6.9 KiB
Python
"""Extragere payload submission -> campuri afisabile.
|
|
|
|
Helper PUR partajat intre canalul web (dashboard Trimiteri) si canalul API
|
|
(`GET /v1/prezentari`), ca extragerea sa NU diverge intre cele doua.
|
|
Fara DB, fara request — primeste `payload_json` (text JSON
|
|
plaintext, vezi `submissions.payload_json`) sau un dict deja parsat.
|
|
|
|
Defensiv prin constructie: nu arunca niciodata pe payload malformat — degradeaza
|
|
la em-dash. Citeste cheile tolerant (canalele API si import pot diferi usor:
|
|
`nr_inmatriculare` vs `numar`/`numarInmatriculare`; coercion Excel pe odometru/VIN).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
EMPTY = "—"
|
|
|
|
|
|
def iso_date_prefix(value: Any) -> str | None:
|
|
"""Intoarce primele 10 caractere (YYYY-MM-DD) daca incep cu o data ISO valida, altfel None.
|
|
|
|
Permite filtrarea dupa data_prestatie chiar daca valoarea contine ora/minut/secunda
|
|
(ex. '2026-06-20 14:35:07' sau '2026-06-20T14:35:07') — extrage portiunea de data
|
|
fara a exclude timestamp-urile. Valori care nu incep cu o data ISO valida
|
|
(ex. '05.12.2024') intorc None si sunt excluse din filtru.
|
|
|
|
Partajat intre canalul web (dashboard Trimiteri, filtrul data_de/data_pana) si
|
|
canalul API (`GET /v1/prezentari`), ca extragerea sa NU diverge intre cele doua.
|
|
"""
|
|
s = str(value or "").strip()
|
|
if len(s) < 10:
|
|
return None
|
|
prefix = s[:10]
|
|
try:
|
|
datetime.strptime(prefix, "%Y-%m-%d")
|
|
return prefix
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
|
|
def _clean_str(value: Any) -> str:
|
|
"""str() defensiv: None/'' -> '', altfel string strip-uit (coercion Excel safe)."""
|
|
if value is None:
|
|
return ""
|
|
return str(value).strip()
|
|
|
|
|
|
def _clean_odometru(value: Any) -> str:
|
|
"""Odometru afisat curat: strip '.0' din coercion Excel (123456.0 -> 123456)."""
|
|
s = _clean_str(value)
|
|
if "." in s:
|
|
before, after = s.split(".", 1)
|
|
if after == "0" and before.lstrip("-").isdigit():
|
|
return before
|
|
return s
|
|
|
|
|
|
def _clean_cod_rar(value: Any) -> str:
|
|
"""Cod RAR afisat curat: uppercase + strip '.0' defensiv (coercion Excel 'OE-2.0' -> 'OE-2').
|
|
|
|
Codurile RAR nu au zecimale, dar fii defensiv ca la odometru.
|
|
"""
|
|
s = _clean_str(value)
|
|
if s.endswith(".0"):
|
|
s = s[:-2]
|
|
return s.upper() if s else ""
|
|
|
|
|
|
def _vin_scurt(vin: str) -> str:
|
|
"""Forma trunchiata a VIN-ului pentru tabel (integral ramane in detaliu).
|
|
|
|
VIN are 17 caractere; in tabel aratam ultimele 6 cu prefix elipsa ca sa
|
|
incapa fara sa ascundem complet identitatea vehiculului.
|
|
"""
|
|
if not vin:
|
|
return ""
|
|
if len(vin) <= 8:
|
|
return vin
|
|
return "…" + vin[-6:]
|
|
|
|
|
|
def _coduri_rar(prestatii: Any) -> list[str]:
|
|
"""Codurile RAR (cod_prestatie) din toate prestatiile: ordonate, dedup."""
|
|
coduri: list[str] = []
|
|
if isinstance(prestatii, list):
|
|
for item in prestatii:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
cod = _clean_cod_rar(item.get("cod_prestatie"))
|
|
if cod and cod not in coduri:
|
|
coduri.append(cod)
|
|
return coduri
|
|
|
|
|
|
def _prima_prestatie(prestatii: Any) -> dict[str, Any]:
|
|
"""Primul item de prestatie ca dict, defensiv (lista goala/non-dict -> {})."""
|
|
if isinstance(prestatii, list):
|
|
for item in prestatii:
|
|
if isinstance(item, dict):
|
|
return item
|
|
return {}
|
|
|
|
|
|
def _payload_dict(payload: str | dict | None) -> dict[str, Any]:
|
|
"""Normalizeaza intrarea la dict. JSON invalid / tip neasteptat -> {}."""
|
|
if payload is None:
|
|
return {}
|
|
if isinstance(payload, dict):
|
|
return payload
|
|
if isinstance(payload, str):
|
|
try:
|
|
data = json.loads(payload)
|
|
except (ValueError, TypeError):
|
|
return {}
|
|
return data if isinstance(data, dict) else {}
|
|
return {}
|
|
|
|
|
|
def prezentare_din_payload(payload: str | dict | None) -> dict[str, Any]:
|
|
"""Campuri afisabile dintr-un payload de submission.
|
|
|
|
Intoarce un dict cu chei stabile (string-uri, EMPTY cand lipsesc; exceptie
|
|
`coduri_rar` = list[str]):
|
|
vehicul_nr, vin, vin_scurt, operatie, data_prestatie, odometru, cod.
|
|
|
|
`operatie` = denumire prestatiei daca exista, altfel codul; `cod` = codul RAR
|
|
(`cod_prestatie`) sau, in lipsa, codul intern (`cod_op_service`).
|
|
"""
|
|
data = _payload_dict(payload)
|
|
|
|
vin = _clean_str(data.get("vin"))
|
|
nr = _clean_str(
|
|
data.get("nr_inmatriculare")
|
|
or data.get("numar")
|
|
or data.get("numarInmatriculare")
|
|
)
|
|
odo = _clean_odometru(
|
|
data.get("odometru_final")
|
|
if data.get("odometru_final") is not None
|
|
else data.get("odometru")
|
|
)
|
|
data_prest = _clean_str(data.get("data_prestatie") or data.get("dataPrestatie"))
|
|
|
|
item = _prima_prestatie(data.get("prestatii"))
|
|
cod = _clean_str(item.get("cod_prestatie") or item.get("cod_op_service"))
|
|
denumire = _clean_str(item.get("denumire"))
|
|
operatie = denumire or cod
|
|
|
|
# cod_rar: primul cod_prestatie din TOATE prestatiile (NU doar primul item si NU
|
|
# fallback la cod_op_service). Primul item poate fi operatia originala exclusa
|
|
# (cod_prestatie null), cu codurile declarate pe itemii urmatori.
|
|
coduri_rar = _coduri_rar(data.get("prestatii"))
|
|
cod_rar = coduri_rar[0] if coduri_rar else ""
|
|
|
|
# Operatia de service originala (codul intern + denumire venita prin API/import),
|
|
# distincta de operatia RAR mapata (cod_rar).
|
|
# Conventie goala: aceste campuri intorc "" (string gol) cand lipsesc — NU EMPTY="—".
|
|
# Motivul: apelantul decide sa nu afiseze randul deloc (vs afisaj gol), testând `!= ""`.
|
|
# Campurile vechi (vehicul_nr, vin, operatie etc.) pastreaza conventia EMPTY="—".
|
|
op_service_cod = _clean_str(item.get("cod_op_service"))
|
|
# op_service_denumire e relevant doar cand exista un cod de operatie de service;
|
|
# altfel ar expune denumirea RAR drept op. de service, ceea ce e semantic incorect.
|
|
op_service_denumire = _clean_str(item.get("denumire")) if op_service_cod else ""
|
|
|
|
# obs: text liber observatii (camp RAR, optional). Conventie goala "" (nu EMPTY).
|
|
# Traieste in payload_json, nu in coloana separata.
|
|
obs = _clean_str(data.get("obs"))
|
|
|
|
return {
|
|
"vehicul_nr": nr or EMPTY,
|
|
"vin": vin or EMPTY,
|
|
"vin_scurt": _vin_scurt(vin) or EMPTY,
|
|
"operatie": operatie or EMPTY,
|
|
"data_prestatie": data_prest or EMPTY,
|
|
"odometru": odo or EMPTY,
|
|
"cod": cod or EMPTY,
|
|
"cod_rar": cod_rar or EMPTY,
|
|
# Lista ordonata, fara duplicate, a codurilor RAR din TOATE prestatiile
|
|
# (goala cand nimic nu e mapat). Randul compact afiseaza primul + "+N".
|
|
"coduri_rar": coduri_rar,
|
|
# Chei cu conventie goala "" (nu EMPTY) — vezi comentariu de mai sus
|
|
"op_service_cod": op_service_cod,
|
|
"op_service_denumire": op_service_denumire,
|
|
"obs": obs,
|
|
}
|