feat(import): mapare operatie->cod RAR inline in preview + camp denumire_op
Inchide deadlock-ul din canalul de import web: operatiile nemapate dintr-un
batch in staging nu aveau unde sa fie mapate din UI. Editorul "Mapari de
rezolvat" citea doar din submissions comise, iar commit-ul arunca randurile
needs_mapping -> utilizatorul ramanea blocat fara a putea trimite.
- camp canonic nou `denumire_op`: coloana descriptiva (ex. "Reparatie Motor")
alimenteaza denumirea operatiei, deci sugestia fuzzy devine utila (inainte
denumire = codul opac). Aplicat in cele 3 locuri de resolve (preview, commit
web, commit API).
- panou inline "Operatii de mapat la cod RAR" in preview: fiecare operatie
nemapata cu sugestie preselectata + dropdown + auto-send + salveaza.
- ruta POST /_import/{id}/mapare-operatie: salveaza maparea (persistenta,
operations_mapping) si re-randeaza preview-ul; randurile trec din
needs_mapping in ok fara re-upload, maparea se retine pentru fisiere viitoare.
- fix bug pre-existent de semnatura coloane: semnatura se calcula din campurile
mapate (json_mapare.keys), nu din antetul complet -> ignorarea unei coloane
schimba semnatura si maparea retinuta nu mai era gasita la preview/re-upload.
Acum mereu din antetul complet (web + API), consecvent cu preview/commit.
Teste noi: tests/test_import_mapare_operatie.py (6). Suita: 400 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,7 @@ from ..mapping import (
|
||||
reresolve_account,
|
||||
resolve_prestatii,
|
||||
save_mapping,
|
||||
suggest_codes,
|
||||
)
|
||||
|
||||
# Campuri canonice cu eticheta umana pentru dropdown mapare coloane (U5)
|
||||
@@ -253,6 +254,35 @@ def post_mapare(
|
||||
# Toate rutele /_import/* returneaza fragmente HTML (target #import-section). #
|
||||
# =========================================================================== #
|
||||
|
||||
def _collect_unmapped_ops(preview_rows: list[dict], nomenclator: list[dict]) -> list[dict]:
|
||||
"""Operatii distincte nemapate dintr-un preview de import (staging), cu sugestii fuzzy.
|
||||
|
||||
Echivalentul lui pending_unmapped() dar pe randuri de PREVIEW (import in staging,
|
||||
inca neexistente ca submissions). Aduna doar prestatiile fara cod_prestatie
|
||||
(cele cu auto_send=0 au deja cod -> nu apar aici). Sortare: cele mai blocate intai.
|
||||
"""
|
||||
agg: dict[str, dict[str, Any]] = {}
|
||||
for row in preview_rows:
|
||||
if row.get("resolved_status") != "needs_mapping":
|
||||
continue
|
||||
for item in (row.get("resolved", {}).get("prestatii") or []):
|
||||
if not isinstance(item, dict) or item.get("cod_prestatie"):
|
||||
continue
|
||||
op = (item.get("cod_op_service") or "").strip()
|
||||
if not op:
|
||||
continue
|
||||
entry = agg.setdefault(op, {"cod_op_service": op, "denumire": item.get("denumire"), "blocked": 0})
|
||||
if not entry["denumire"] and item.get("denumire"):
|
||||
entry["denumire"] = item.get("denumire")
|
||||
entry["blocked"] += 1
|
||||
out: list[dict] = []
|
||||
for entry in agg.values():
|
||||
entry["suggestions"] = suggest_codes(entry["denumire"], nomenclator, limit=5)
|
||||
out.append(entry)
|
||||
out.sort(key=lambda e: (-e["blocked"], e["cod_op_service"]))
|
||||
return out
|
||||
|
||||
|
||||
def _web_compute_preview(
|
||||
conn,
|
||||
import_id: int,
|
||||
@@ -416,11 +446,14 @@ def _web_compute_preview(
|
||||
except Exception:
|
||||
conn.execute("ROLLBACK")
|
||||
|
||||
nomenclator = load_nomenclator(conn)
|
||||
return {
|
||||
"rows": preview_rows,
|
||||
"summary": summary,
|
||||
"total": len(preview_rows),
|
||||
"filename": batch["filename"],
|
||||
"unmapped_ops": _collect_unmapped_ops(preview_rows, nomenclator),
|
||||
"nomenclator": nomenclator,
|
||||
}
|
||||
|
||||
|
||||
@@ -602,7 +635,11 @@ async def web_save_mapare_coloane(
|
||||
request, error="Batch de import inexistent sau expirat."
|
||||
))
|
||||
|
||||
sig = _signature(list(json_mapare.keys()))
|
||||
# Semnatura = antetul COMPLET al fisierului (toate coloanele, inclusiv cele
|
||||
# ignorate), nu doar campurile mapate. Altfel ignorarea unei coloane schimba
|
||||
# semnatura si maparea nu mai e gasita la preview/re-upload (col_names = antet
|
||||
# complet peste tot). `colnames` vine din form = toate coloanele randate.
|
||||
sig = _signature(colnames or list(json_mapare.keys()))
|
||||
|
||||
# Salveaza maparea (upsert)
|
||||
conn.execute(
|
||||
@@ -650,6 +687,49 @@ def web_preview_import(
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/_import/{import_id}/mapare-operatie", response_class=HTMLResponse)
|
||||
async def web_mapare_operatie(
|
||||
request: Request,
|
||||
import_id: int,
|
||||
) -> HTMLResponse:
|
||||
"""Mapeaza o operatie nemapata din preview-ul de import la un cod RAR, in flux.
|
||||
|
||||
Salveaza maparea (persistenta, operations_mapping) si re-randeaza preview-ul:
|
||||
_web_compute_preview recalculeaza cu noua mapare si re-scrie resolved_status in
|
||||
import_rows, deci randurile afectate trec din needs_mapping in ok fara re-upload.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
try:
|
||||
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 = bool(form.get("auto_send"))
|
||||
|
||||
def _render(message: str | None = None, error: bool = False) -> HTMLResponse:
|
||||
result = _web_compute_preview(conn, import_id, account_id)
|
||||
if isinstance(result, str):
|
||||
return templates.TemplateResponse("_upload.html", _ctx(request, error=result))
|
||||
return templates.TemplateResponse("_preview_import.html", _ctx(
|
||||
request, import_id=import_id, message=message, error=error, **result
|
||||
))
|
||||
|
||||
if not cod_op_service or not cod_prestatie:
|
||||
return _render("Alege un cod RAR pentru operatie.", error=True)
|
||||
|
||||
exists = conn.execute(
|
||||
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod_prestatie,)
|
||||
).fetchone()
|
||||
if not exists:
|
||||
return _render(f"Cod RAR necunoscut: {cod_prestatie}", error=True)
|
||||
|
||||
save_mapping(conn, account_id, cod_op_service, cod_prestatie, auto_send)
|
||||
return _render(f"Mapat {cod_op_service} -> {cod_prestatie}.")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/_import/reset", response_class=HTMLResponse)
|
||||
def web_import_reset(request: Request) -> HTMLResponse:
|
||||
"""Reseteaza sectiunea de import la starea initiala (drop zone gol)."""
|
||||
@@ -834,10 +914,12 @@ async def web_confirma_import(
|
||||
mapped["data_prestatie"] = iso_date
|
||||
break
|
||||
|
||||
# Operatia → prestatii
|
||||
# Operatia → prestatii (denumire_op alimenteaza denumirea reala)
|
||||
operatie_val = mapped.pop("operatie", None)
|
||||
denumire_val = mapped.pop("denumire_op", None)
|
||||
if operatie_val and "prestatii" not in mapped:
|
||||
mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": str(operatie_val)}]
|
||||
denumire = str(denumire_val).strip() if denumire_val not in (None, "") else str(operatie_val)
|
||||
mapped["prestatii"] = [{"cod_op_service": str(operatie_val), "denumire": denumire}]
|
||||
|
||||
# Rezolva prestatii
|
||||
prestatii = mapped.get("prestatii") or []
|
||||
|
||||
Reference in New Issue
Block a user