feat(5.7): raspuns API onest la blocaje + mapare inline din detaliu

Raportat din client VFP: POST /v1/prezentari raspundea submission_id+status
fara motiv pe randuri blocate (erori se popula doar pe on_unmapped_error=True),
deci un needs_data/needs_mapping parea succes.

API (aditiv): SubmissionResult += nemapate + motiv. create_prezentari
populeaza erori (validare continut, 3 niveluri) / nemapate (coduri fara
mapare, COD_NEMAPAT) / motiv (rezumat uman) pe TOATE caile non-queued —
enqueue, respins (on_unmapped_error=True) si reactivare dedup peste error,
prin helperele _rezultat_enqueue / _rezultat_respins / _motiv_clasificare.
on_unmapped_error=True pastreaza erori=COD_NEMAPAT (compat clienti vechi).

Web: mapare inline in panoul de detaliu trimitere — ruta
POST /trimitere/{id}/mapeaza (reuse save_mapping + reresolve_account, scoped
sesiune + CSRF, re-rezolva pe batch_id-ul randului), helper
_nemapate_pentru_submission + context in _detaliu_ctx, sectiune in
_trimitere_detaliu.html (selector cod RAR cu sugestie fuzzy preselectata).
Apare doar pe operatii nemapate reale (nu pe auto_send=0).

/code-review high: reparat raspuns neonest la reactivare + dublu
load_nomenclator in _detaliu_ctx.

Teste: pytest -q 765 passed. Backend trimitere (worker/masina stari/
idempotenta) si schema NEATINSE. PRD: docs/prd/prd-5.7-*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-23 20:51:16 +00:00
parent 6bad6bc01e
commit ac57b9250a
10 changed files with 688 additions and 21 deletions

View File

@@ -771,10 +771,57 @@ def _payload_form_values(payload_json) -> dict:
}
def _nemapate_pentru_submission(row, nomenclator: list[dict]) -> list[dict]:
"""Operatiile nemapate ale UNUI submission needs_mapping, cu sugestii fuzzy (PRD 5.7).
Echivalentul `pending_unmapped` restrans la un singur rand: parseaza payload_json,
aduna prestatiile fara cod_prestatie (cu cod_op_service) si ataseaza sugestii din
`nomenclator` (pasat de apelant — evita un SELECT redundant in _detaliu_ctx). Goala
daca randul nu e needs_mapping sau nu are operatii nemapate reale (ex. needs_mapping
din auto_send=0 — codul exista deja, doar trimiterea e oprita).
"""
if row["status"] != "needs_mapping":
return []
try:
content = json.loads(row["payload_json"]) if row["payload_json"] else {}
if not isinstance(content, dict):
content = {}
except (ValueError, TypeError):
content = {}
seen: set[str] = set()
out: list[dict] = []
for item in content.get("prestatii") or []:
if not isinstance(item, dict) or (item.get("cod_prestatie") or ""):
continue
op = (item.get("cod_op_service") or "").strip()
if not op or op in seen:
continue
seen.add(op)
out.append({
"cod_op_service": op,
"denumire": item.get("denumire"),
"suggestions": suggest_codes(item.get("denumire"), nomenclator, limit=5),
})
return out
def _detaliu_ctx(request: Request, row, *, message: str | None = None,
error: bool = False, corectie_errors: list | None = None) -> dict:
"""Context pentru _trimitere_detaliu.html dintr-un rand de submission."""
error: bool = False, corectie_errors: list | None = None,
conn=None, account_id: int | None = None) -> dict:
"""Context pentru _trimitere_detaliu.html dintr-un rand de submission.
`conn`+`account_id` (optional): cand sunt date si randul e needs_mapping, expune
`nemapate_inline` + `nomenclator` pentru maparea inline din panou (PRD 5.7).
"""
eticheta = eticheta_stare(row["status"])
nemapate_inline: list[dict] = []
nomenclator: list[dict] = []
if conn is not None and row["status"] == "needs_mapping":
# Un singur SELECT pe nomenclator: il refolosim si pentru sugestii si pentru dropdown.
nomenclator = load_nomenclator(conn)
nemapate_inline = _nemapate_pentru_submission(row, nomenclator)
if not nemapate_inline:
nomenclator = [] # nu expunem dropdown-ul cand nu exista operatii de mapat
ctx = {
"request": request,
"csrf_token": get_csrf_token(request),
@@ -797,6 +844,9 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
"editabil": row["status"] in _CORECTABILE,
# US-011: error/needs_data/needs_mapping pot fi sterse / re-puse in coada
"gestionabil": row["status"] in _GESTIONABILE_WEB,
# PRD 5.7: mapare inline (operatii nemapate ale acestui rand + nomenclator)
"nemapate_inline": nemapate_inline,
"nomenclator": nomenclator,
"corectie_msg": message,
"corectie_error": error,
"corectie_errors": corectie_errors or [],
@@ -829,7 +879,64 @@ def fragment_trimitere_detaliu(request: Request, submission_id: int) -> HTMLResp
row = _fetch_submission_scoped(conn, account_id, submission_id)
if not row:
raise HTTPException(status_code=404, detail="trimitere inexistenta")
return templates.TemplateResponse("_trimitere_detaliu.html", _detaliu_ctx(request, row))
return templates.TemplateResponse(
"_trimitere_detaliu.html",
_detaliu_ctx(request, row, conn=conn, account_id=account_id),
)
finally:
conn.close()
@router.post("/trimitere/{submission_id}/mapeaza", response_class=HTMLResponse)
async def post_mapeaza_inline(request: Request, submission_id: int) -> HTMLResponse:
"""Mapare inline din panoul de detaliu (PRD 5.7): alege cod RAR pentru o operatie nemapata.
Reutilizeaza EXACT save_mapping + reresolve_account (ca tab-ul Mapari) — fara logica
noua de clasificare. Re-rezolva scoped pe batch-ul randului (canal API batch_id IS NULL
SAU import batch), deblocand si randurile-frate cu aceeasi operatie. Scoped pe sesiune
(404 cross-account/inexistent), CSRF obligatoriu, gard pe status needs_mapping.
"""
account_id = require_login(request)
form = await request.form()
verify_csrf(request, str(form.get("csrf_token") or ""))
cod_op_service = str(form.get("cod_op_service") or "").strip()
cod_prestatie = str(form.get("cod_prestatie") or "").strip().upper()
auto_send = str(form.get("auto_send") or "") not in ("", "false", "0", "off")
conn = get_connection()
try:
row = _fetch_submission_scoped(conn, account_id, submission_id)
if not row:
raise HTTPException(status_code=404, detail="trimitere inexistenta")
if row["status"] != "needs_mapping":
raise HTTPException(status_code=403, detail="trimitere fara operatii de mapat")
if not cod_op_service or not cod_prestatie:
return templates.TemplateResponse(
"_trimitere_detaliu.html",
_detaliu_ctx(request, row, conn=conn, account_id=account_id, error=True,
message="Alege un cod RAR pentru operatie."),
)
exists = conn.execute(
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod_prestatie,)
).fetchone()
if not exists:
return templates.TemplateResponse(
"_trimitere_detaliu.html",
_detaliu_ctx(request, row, conn=conn, account_id=account_id, error=True,
message=f"Cod necunoscut in nomenclator: {cod_prestatie}."),
)
save_mapping(conn, account_id, cod_op_service, cod_prestatie, auto_send)
# Re-rezolva scoped pe canalul randului: batch_id None (API) sau batch import.
reresolve_account(conn, account_id, batch_id=row["batch_id"])
row2 = _fetch_submission_scoped(conn, account_id, submission_id)
eticheta = eticheta_stare(row2["status"])
resp = templates.TemplateResponse(
"_trimitere_detaliu.html",
_detaliu_ctx(request, row2, conn=conn, account_id=account_id,
message=f"Mapat {cod_op_service} -> {cod_prestatie}. "
f"Stare noua: {eticheta[0]}."),
)
resp.headers["HX-Trigger"] = "trimiteriChanged"
return resp
finally:
conn.close()
@@ -895,8 +1002,8 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
row2 = _fetch_submission_scoped(conn, account_id, submission_id)
return templates.TemplateResponse(
"_trimitere_detaliu.html",
_detaliu_ctx(request, row2, error=True,
message="Lipseste inca un cod RAR — rezolva operatia in tab-ul Mapari."),
_detaliu_ctx(request, row2, conn=conn, account_id=account_id, error=True,
message="Lipseste inca un cod RAR — alege-l mai jos sau in tab-ul Mapari."),
)
if has_no_auto_send(resolved, mapping_meta):
@@ -999,7 +1106,8 @@ async def post_repune_trimitere(request: Request, submission_id: int) -> HTMLRes
row = _fetch_submission_scoped(conn, account_id, submission_id)
resp = templates.TemplateResponse(
"_trimitere_detaliu.html",
_detaliu_ctx(request, row, message="Re-pus in coada — pleaca la urmatoarea trimitere."),
_detaliu_ctx(request, row, conn=conn, account_id=account_id,
message="Re-pus in coada — pleaca la urmatoarea trimitere."),
)
resp.headers["HX-Trigger"] = "trimiteriChanged"
return resp