feat(mapari): regula 'exclude de la declarare' per operatie + stare preview Nedeclarat

Operatiile care nu se declara la RAR (ex. ITP facturat in service) primesc o
regula exclus=1 in operations_mapping, setabila din panoul de mapare al
preview-ului de import si din tab-ul Mapari (optiunea 'Nu se declara la RAR').

- resolve_prestatii(excluded_ops): item nemapat cu op exclusa -> adnotat exclus,
  nu mai e needs_mapping; precedenta: cod explicit > exclus > mapare > reguli text
- split_prestatii_excluse: itemii exclusi nu intra niciodata in payload/cheie
- preview import: rand cu toate operatiile excluse -> stare 'excluded'
  (eticheta Nedeclarat), necomis; operatia dispare din panoul de mapat
- reresolve/corectie/API: submission cu toate operatiile excluse -> needs_data
  cu motiv explicit; ingestia API trateaza excluderea la clasificare
- migrare: coloana operations_mapping.exclus + rebuild import_rows pentru
  CHECK-ul resolved_status cu 'excluded' (o singura data, gardat pe sqlite_master)
- fix flake: clamp similaritate embeddings la [-1,1] (float32 dadea 1.0000001)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-07 17:49:47 +00:00
parent c6e29aa65d
commit 0bc618763e
14 changed files with 616 additions and 37 deletions

View File

@@ -72,12 +72,14 @@ from ..submissions_admin import (
from ..mapping import (
DEFAULT_ACCOUNT_ID,
_emite_text_rule_hits,
EXCLUDE_SENTINEL,
account_or_default,
account_scope_clause,
delete_text_rule,
enrich_suggestions,
ensure_embeddings_corpus,
has_no_auto_send,
load_excluded_ops,
load_mapping_meta,
load_nomenclator,
load_nomenclator_codes,
@@ -86,8 +88,10 @@ from ..mapping import (
pending_unmapped,
reresolve_account,
resolve_prestatii,
save_exclusion,
save_mapping,
save_text_rule,
split_prestatii_excluse,
suggest_codes,
text_rules_overlap,
)
@@ -1727,12 +1731,19 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
mapping = {op: m["cod_prestatie"] for op, m in mapping_meta.items()}
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, account_id)
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping, valid_codes, text_rules)
excluded_ops = load_excluded_ops(conn, account_id)
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping, valid_codes, text_rules, excluded_ops)
content["prestatii"] = resolved
# telemetrie pentru itemii rezolvati prin regula text (calea corectie web).
_emite_text_rule_hits(conn, account_id, row["id"], resolved)
# Prestatiile excluse de la declarare ies din payload-ul trimis (ca in
# reresolve_account); toate excluse -> needs_data cu motiv explicit mai jos.
declarabile, excluse = split_prestatii_excluse(resolved)
if not unmapped and excluse and declarabile:
content["prestatii"] = declarabile
# Canonicalizare (strip ".0" odometru, VIN/nr upper) INAINTE de validare si cheie.
canon = canonicalize_row(content)
content.update({
@@ -1755,6 +1766,24 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
message="Lipseste inca un cod RAR — alege-l mai jos sau in tab-ul Mapari."),
)
if excluse and not declarabile:
ops_excluse = ", ".join((p.get("cod_op_service") or "") for p in excluse)
motiv = [{
"field": "prestatii",
"message": f"Toate operatiile sunt excluse de la declarare ({ops_excluse}) — randul nu se trimite la RAR.",
}]
conn.execute(
"UPDATE submissions SET status='needs_data', payload_json=?, rar_error=?, "
"updated_at=datetime('now') WHERE id=?",
(payload_json, json.dumps(motiv, ensure_ascii=False), row["id"]),
)
row2 = _fetch_submission_scoped(conn, account_id, submission_id)
return templates.TemplateResponse(
"_trimitere_detaliu.html",
_detaliu_ctx(request, row2, conn=conn, account_id=account_id, error=True,
message="Toate operatiile randului sunt excluse de la declarare — nu se trimite la RAR."),
)
errors = validate_prezentare(content)
if errors:
# Inca invalid: persista valorile introduse, ramane needs_data, arata motivul pe camp.
@@ -2571,7 +2600,7 @@ def _load_saved_op_mappings(conn, account_id: int) -> list[dict]:
prestatiei jonctionat din nomenclator. Scoped pe cont (NOT NULL → simplu)."""
acct = account_or_default(account_id)
rows = conn.execute(
"SELECT o.id, o.cod_op_service, o.cod_prestatie, o.auto_send, n.nume_prestatie "
"SELECT o.id, o.cod_op_service, o.cod_prestatie, o.auto_send, o.exclus, n.nume_prestatie "
"FROM operations_mapping o "
"LEFT JOIN nomenclator_rar n ON n.cod_prestatie = o.cod_prestatie "
"WHERE o.account_id=? ORDER BY o.cod_op_service",
@@ -2583,6 +2612,7 @@ def _load_saved_op_mappings(conn, account_id: int) -> list[dict]:
"cod_op_service": r["cod_op_service"],
"cod_prestatie": r["cod_prestatie"],
"auto_send": bool(r["auto_send"]),
"exclus": bool(r["exclus"]),
"nume_prestatie": r["nume_prestatie"],
}
for r in rows
@@ -2669,6 +2699,17 @@ def post_mapare(
conn = get_connection()
try:
cod = cod_prestatie.strip().upper()
if cod == EXCLUDE_SENTINEL:
# Regula "nu se declara la RAR": exclude + re-rezolva blocatele
# (randurile cu toate operatiile excluse trec pe needs_data cu motiv).
save_exclusion(conn, account_id, cod_op_service)
stats = reresolve_account(conn, account_id)
msg = (
f"{cod_op_service.strip()} exclus de la declarare. "
f"Deblocate: {stats['requeued']} in coada, {stats['needs_data']} oprite/date lipsa, "
f"{stats['still_blocked']} inca nemapate."
)
return _render_mapari(request, conn, account_id, message=msg)
exists = conn.execute("SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod,)).fetchone()
if not exists:
return _render_mapari(request, conn, account_id, message=f"Cod necunoscut: {cod}")
@@ -2715,6 +2756,16 @@ def post_editeaza_mapare_salvata(
conn = get_connection()
try:
cod = cod_prestatie.strip().upper()
if cod == EXCLUDE_SENTINEL:
# Trecerea unei mapari salvate pe "nu se declara" — exclude + re-rezolva.
save_exclusion(conn, account_id, cod_op_service)
stats = reresolve_account(conn, account_id)
msg = (
f"{cod_op_service.strip()} exclus de la declarare. "
f"Deblocate: {stats['requeued']} in coada, {stats['needs_data']} oprite/date lipsa, "
f"{stats['still_blocked']} inca nemapate."
)
return _render_mapari(request, conn, account_id, message=msg)
exists = conn.execute(
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod,)
).fetchone()
@@ -3131,6 +3182,7 @@ def _web_compute_preview(
# needs_mapping si exclus din commit. Incarcate o data.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
excluded_ops = load_excluded_ops(conn, acct)
# Detectie coercion flags din valorile stocate (VIN numeric)
coercion_flags_map: dict[int, list[str]] = {}
@@ -3177,6 +3229,7 @@ def _web_compute_preview(
valid_codes=valid_codes,
text_rules=text_rules,
reviewed=reviewed_flags[i],
excluded_ops=excluded_ops,
)
key: str | None = None
@@ -4162,6 +4215,7 @@ async def web_mapare_operatii(
codes_list = form.getlist("cod_prestatie")
salvate: list[str] = []
excluse_ops: list[str] = []
sarite_invalide: list[str] = []
for cod_op_service, cod_prestatie in zip(ops_list, codes_list):
@@ -4172,6 +4226,12 @@ async def web_mapare_operatii(
if not cod_op_service or not cod_prestatie:
continue
# Regula "nu se declara la RAR": exclude operatia de la declarare.
if cod_prestatie == EXCLUDE_SENTINEL:
save_exclusion(conn, account_id, cod_op_service)
excluse_ops.append(cod_op_service)
continue
# Validare per-item (D#12): cod invalid -> skip + sumar, nu all-or-nothing
exists = conn.execute(
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod_prestatie,)
@@ -4187,10 +4247,12 @@ async def web_mapare_operatii(
parts: list[str] = []
if salvate:
parts.append(f"Salvate: {', '.join(salvate)}.")
if excluse_ops:
parts.append(f"Excluse de la declarare: {', '.join(excluse_ops)}.")
if sarite_invalide:
parts.append(f"Coduri necunoscute ignorate: {', '.join(sarite_invalide)}.")
message = " ".join(parts) if parts else None
error = bool(sarite_invalide) and not salvate
error = bool(sarite_invalide) and not salvate and not excluse_ops
result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str):
@@ -4404,9 +4466,10 @@ async def web_confirma_import(
# Mapare operatii
mapping_meta = load_mapping_meta(conn, acct)
mapping_ops = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
# validare nomenclator + reguli text + excluderi incarcate O DATA, inainte de bucla.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
excluded_ops = load_excluded_ops(conn, acct)
# Rezolva mediul RAR tinta al lotului: form > default cont > ancora globala.
try:
@@ -4468,7 +4531,12 @@ async def web_confirma_import(
# Rezolva prestatii
prestatii = mapped.get("prestatii") or []
resolved_p, _ = resolve_prestatii(prestatii, mapping_ops, valid_codes, text_rules)
resolved_p, _ = resolve_prestatii(prestatii, mapping_ops, valid_codes, text_rules, excluded_ops)
# Prestatiile excluse NU pleaca la RAR (ca in preview); rand fara nicio
# prestatie declarabila = 'excluded' in preview -> defensiv skip.
resolved_p, _excluse_p = split_prestatii_excluse(resolved_p)
if not resolved_p:
continue
mapped["prestatii"] = resolved_p
# Canonicalizare (cheia identica cu preview: canonicalize_row + build_key)