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:
@@ -400,6 +400,7 @@ STARI_PREVIEW: dict[str, tuple[str, str]] = {
|
||||
"needs_data": ("Date incomplete", "s-needs_data"),
|
||||
"already_sent": ("Deja trimis", "s-already_sent"),
|
||||
"duplicate_in_file": ("Duplicat in fisier", "s-duplicate_in_file"),
|
||||
"excluded": ("Nedeclarat", "s-excluded"),
|
||||
}
|
||||
|
||||
|
||||
@@ -422,6 +423,8 @@ def nota_umana_preview(status: str, errors: list, flags: list) -> str:
|
||||
"""
|
||||
if status in ("already_sent", "duplicate_in_file"):
|
||||
return ""
|
||||
if status == "excluded":
|
||||
return "Operatie exclusa de la declarare — randul nu se trimite la RAR."
|
||||
# needs_mapping: codul RAR lipseste — prioritizeaza 'unmapped' inaintea flags,
|
||||
# altfel un rand cu si un flag (ex. VIN numeric) ar afisa textul flag-ului
|
||||
# si ascunde motivul real (cod lipsa).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
<select name="cod_prestatie" form="map-rez-{{ loop.index }}" required
|
||||
aria-label="Cod RAR pentru {{ e.cod_op_service }}">
|
||||
<option value="">— alege cod RAR —</option>
|
||||
<option value="__NEDECLARAT__">Nu se declara la RAR (exclude operatia)</option>
|
||||
{% for n in nomenclator %}
|
||||
<option value="{{ n.cod_prestatie }}" {% if n.cod_prestatie == preselect %}selected{% endif %}>
|
||||
{{ n.cod_prestatie }} — {{ n.nume_prestatie }}
|
||||
@@ -129,7 +130,7 @@
|
||||
<tbody>
|
||||
{% for m in saved_mappings %}
|
||||
{# data-dt-row = haystack de cautare (randul contine un <select> cu tot nomenclatorul). #}
|
||||
<tr data-dt-row="{{ m.cod_op_service }} {{ m.cod_prestatie }} {{ m.nume_prestatie or '' }}">
|
||||
<tr data-dt-row="{{ m.cod_op_service }} {{ m.cod_prestatie }} {{ m.nume_prestatie or '' }}{% if m.exclus %} nedeclarat exclus{% endif %}">
|
||||
<td data-eticheta="Operatie">
|
||||
<form id="map-salv-{{ loop.index }}" hx-post="/mapari/salvate" hx-target="#mapari-section" hx-swap="outerHTML">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
@@ -142,14 +143,16 @@
|
||||
</form>
|
||||
<div><strong>{{ m.cod_op_service }}</strong></div>
|
||||
<div class="muted map-acum" style="font-size:12px;">
|
||||
acum: {{ m.cod_prestatie }}{% if m.nume_prestatie %} — {{ m.nume_prestatie }}{% endif %}
|
||||
{% if m.exclus %}acum: <span class="pill s-excluded" style="font-size:11px;">Nu se declara la RAR</span>
|
||||
{% else %}acum: {{ m.cod_prestatie }}{% if m.nume_prestatie %} — {{ m.nume_prestatie }}{% endif %}{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td data-eticheta="Cod RAR">
|
||||
<select name="cod_prestatie" form="map-salv-{{ loop.index }}" required
|
||||
aria-label="Cod RAR pentru {{ m.cod_op_service }}">
|
||||
<option value="__NEDECLARAT__" {% if m.exclus %}selected{% endif %}>Nu se declara la RAR (exclude operatia)</option>
|
||||
{% for n in nomenclator %}
|
||||
<option value="{{ n.cod_prestatie }}" {% if n.cod_prestatie == m.cod_prestatie %}selected{% endif %}>
|
||||
<option value="{{ n.cod_prestatie }}" {% if not m.exclus and n.cod_prestatie == m.cod_prestatie %}selected{% endif %}>
|
||||
{{ n.cod_prestatie }} — {{ n.nume_prestatie }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
('needs_data', 'Date incomplete'),
|
||||
('already_sent', 'Deja trimis'),
|
||||
('duplicate_in_file','Duplicat in fisier'),
|
||||
('excluded', 'Nedeclarat'),
|
||||
] %}
|
||||
<div id="preview-rezumat" style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px;">
|
||||
{% for status_key, label in status_labels %}
|
||||
@@ -126,6 +127,7 @@
|
||||
<div class="mapcol">
|
||||
<select name="cod_prestatie" aria-label="Cod RAR pentru {{ e.cod_op_service }}">
|
||||
<option value="">— alege cod RAR —</option>
|
||||
<option value="__NEDECLARAT__">Nu se declara la RAR (exclude operatia)</option>
|
||||
{% for n in nomenclator %}
|
||||
<option value="{{ n.cod_prestatie }}" {% if n.cod_prestatie == preselect %}selected{% endif %}>
|
||||
{{ n.cod_prestatie }} — {{ n.nume_prestatie }}
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
.s-error,.s-needs_data,.s-needs_mapping{color:var(--err);}
|
||||
.s-ok{color:var(--ok);}
|
||||
.s-needs_review{color:var(--warn);}
|
||||
.s-already_sent,.s-duplicate_in_file{color:var(--muted);}
|
||||
.s-already_sent,.s-duplicate_in_file,.s-excluded{color:var(--muted);}
|
||||
/* Badge mediu RAR — semantica risc L.142.
|
||||
Productie: fill atentie (ton --err), text alb — declaratie reala, ireversibila.
|
||||
Testare: outline discret, ton --muted — mediu de proba, low-stakes.
|
||||
|
||||
Reference in New Issue
Block a user