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:
@@ -63,7 +63,8 @@ _CANONICAL_SYNONYMS: dict[str, list[str]] = {
|
||||
"data_prestatie": ["Data prestatie", "Data", "Date", "Data service", "Data lucrare"],
|
||||
"odometru_final": ["Odometru final", "Odometru", "KM", "Kilometri", "Km final", "Citire contor"],
|
||||
"odometru_initial": ["Odometru initial", "KM initial", "Km start"],
|
||||
"operatie": ["Operatie", "Denumire prestatie", "Prestatie", "Lucrare", "Tip lucrare", "Cod prestatie", "Cod op"],
|
||||
"operatie": ["Operatie", "Cod prestatie", "Prestatie", "Lucrare", "Tip lucrare", "Cod op"],
|
||||
"denumire_op": ["Denumire operatie", "Denumire", "Descriere", "Denumire prestatie", "Nume operatie"],
|
||||
"obs": ["Observatii", "Obs", "Mentiuni", "Note"],
|
||||
}
|
||||
|
||||
@@ -163,11 +164,15 @@ def _resolve_row_for_preview(
|
||||
if is_amb:
|
||||
is_ambiguous_date = True
|
||||
|
||||
# Operatia: daca camp canonic e "operatie", construieste prestatii
|
||||
# Operatia: daca camp canonic e "operatie", construieste prestatii.
|
||||
# denumire_op (coloana descriptiva, ex. "Reparatie Motor") alimenteaza
|
||||
# `denumire` -> sugestia fuzzy din editorul de mapari devine utila; fara ea,
|
||||
# denumire = codul opac (ex. "OP-MOTOR") si fuzzy nu are pe ce sa lucreze.
|
||||
operatie_val = mapped.pop("operatie", None)
|
||||
denumire_val = mapped.pop("denumire_op", None)
|
||||
if operatie_val and "prestatii" not in mapped:
|
||||
# Construieste un item de prestatie din operatie
|
||||
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}]
|
||||
|
||||
# Canonicalizare (T9): normalizeaza VIN/nr/odometru
|
||||
canon = canonicalize_row(mapped)
|
||||
@@ -528,8 +533,22 @@ def save_column_mapping(
|
||||
if not batch:
|
||||
raise HTTPException(status_code=404, detail="batch de import inexistent")
|
||||
|
||||
# Recalculeaza semnatura din coloanele fisierului (cheile maparii)
|
||||
# Semnatura = antetul COMPLET al fisierului (toate coloanele din batch), nu
|
||||
# doar campurile mapate. Altfel, daca clientul ignora o coloana, semnatura
|
||||
# difera de cea calculata la preview (col_names = antet complet) si maparea
|
||||
# retinuta nu mai e gasita. Citim antetul din primul rand al batch-ului.
|
||||
first_row = conn.execute(
|
||||
"SELECT raw_json FROM import_rows WHERE batch_id=? ORDER BY row_index LIMIT 1",
|
||||
(import_id,),
|
||||
).fetchone()
|
||||
columns = list(req.json_mapare.keys())
|
||||
if first_row:
|
||||
try:
|
||||
rd = decrypt_creds(first_row["raw_json"]) or {}
|
||||
if rd:
|
||||
columns = list(rd.keys())
|
||||
except Exception:
|
||||
pass
|
||||
sig = _signature(columns)
|
||||
|
||||
conn.execute(
|
||||
@@ -925,10 +944,12 @@ def commit_import(
|
||||
if iso_date:
|
||||
mapped["data_prestatie"] = iso_date
|
||||
|
||||
# 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 INAINTE de canonicalizare (altfel cheia difera de cea din preview)
|
||||
prestatii = mapped.get("prestatii") or []
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -52,6 +52,59 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Panou inline: operatii fara cod RAR, mapabile in flux (fara re-upload) -->
|
||||
{% if unmapped_ops %}
|
||||
<div class="card" style="border-color:var(--err); background:#241a1a; margin-bottom:14px;">
|
||||
<h3 style="font-size:14px; margin:0 0 6px;">Operatii de mapat la cod RAR</h3>
|
||||
<p class="muted" style="margin:0 0 12px; font-size:13px;">
|
||||
Aceste operatii din fisier nu au inca un cod RAR. Alege codul (sugestia e
|
||||
preselectata) si salveaza — randurile blocate trec automat in
|
||||
<span class="s-ok">ok</span> si maparea se retine pentru fisierele viitoare.
|
||||
</p>
|
||||
{% for e in unmapped_ops %}
|
||||
{%- set top = e.suggestions[0] if e.suggestions else None -%}
|
||||
{%- set preselect = top.cod_prestatie if (top and top.score >= 60) else '' -%}
|
||||
<form class="maprow" hx-post="/_import/{{ import_id }}/mapare-operatie"
|
||||
hx-target="#import-section" hx-swap="outerHTML"
|
||||
style="align-items:flex-end;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
<input type="hidden" name="cod_op_service" value="{{ e.cod_op_service }}">
|
||||
<div class="mapcol grow">
|
||||
<div><strong>{{ e.cod_op_service }}</strong>
|
||||
<span class="pill" title="randuri blocate">{{ e.blocked }} randuri</span></div>
|
||||
{% if e.denumire and e.denumire != e.cod_op_service %}
|
||||
<div class="muted">{{ e.denumire }}</div>
|
||||
{% endif %}
|
||||
{% if e.suggestions %}
|
||||
<div class="muted" style="font-size:12px; margin-top:4px;">
|
||||
sugestii:
|
||||
{% for s in e.suggestions[:3] %}
|
||||
<span class="sugg">{{ s.cod_prestatie }} ({{ s.score|round|int }}%)</span>{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mapcol">
|
||||
<select name="cod_prestatie" required aria-label="Cod RAR pentru {{ e.cod_op_service }}">
|
||||
<option value="">— alege cod RAR —</option>
|
||||
{% for n in nomenclator %}
|
||||
<option value="{{ n.cod_prestatie }}" {% if n.cod_prestatie == preselect %}selected{% endif %}>
|
||||
{{ n.cod_prestatie }} — {{ n.nume_prestatie }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mapcol">
|
||||
<label class="chk"><input type="checkbox" name="auto_send" value="true" checked> auto-send</label>
|
||||
</div>
|
||||
<div class="mapcol">
|
||||
<button type="submit" style="min-height:44px;">Salveaza</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tabel preview + bara confirmare (un singur form) -->
|
||||
<form id="confirm-form"
|
||||
hx-post="/_import/{{ import_id }}/confirma"
|
||||
|
||||
248
tests/test_import_mapare_operatie.py
Normal file
248
tests/test_import_mapare_operatie.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""Teste flux mapare operatie inline in preview (import web in staging).
|
||||
|
||||
Acopera gap-ul: operatiile nemapate dintr-un import 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). Acum:
|
||||
- camp canonic nou `denumire_op`: coloana descriptiva alimenteaza denumirea
|
||||
operatiei -> sugestia fuzzy devine utila (nu codul opac);
|
||||
- preview-ul expune `unmapped_ops` + panou inline de mapare;
|
||||
- POST /_import/{id}/mapare-operatie salveaza maparea (persistenta) si
|
||||
re-rezolva preview-ul -> randurile trec din needs_mapping in ok, fara re-upload.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv as csv_mod
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
|
||||
# Mod dev: fallback cont 1, fara login/CSRF (ca in test_import_ui).
|
||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
||||
from app.config import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
from app.main import app
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
_HEADER = ["VIN", "Nr inmatriculare", "Data prestatie", "Odometru final", "Cod operatie", "Denumire"]
|
||||
# Cod intern opac + descriere lizibila care se potriveste cu nomenclatorul RAR
|
||||
# (OE-3 = "REVIZIE PERIODICA"; fuzzy "Revizie periodica" -> OE-3 la 100%).
|
||||
_ROWS = [
|
||||
["WVWZZZ1KZAW001111", "B100TST", "2026-06-15", "123456", "OP-REV", "Revizie periodica"],
|
||||
["WVWZZZ1KZAW002222", "CJ200AB", "2026-05-20", "98765", "OP-REV", "Revizie periodica"],
|
||||
]
|
||||
_CANON = ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie", "denumire_op"]
|
||||
|
||||
|
||||
def _csv_bytes(header, rows, sep=";") -> bytes:
|
||||
buf = io.StringIO()
|
||||
w = csv_mod.writer(buf, delimiter=sep)
|
||||
w.writerow(header)
|
||||
for r in rows:
|
||||
w.writerow(r)
|
||||
return buf.getvalue().encode("utf-8")
|
||||
|
||||
|
||||
def _upload(client, header=_HEADER, rows=None) -> int:
|
||||
rows = _ROWS if rows is None else rows
|
||||
r = client.post(
|
||||
"/_import/upload",
|
||||
files={"file": ("t.csv", _csv_bytes(header, rows), "text/csv")},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
m = re.search(r"/_import/(\d+)/mapare-coloane", r.text)
|
||||
assert m, f"form mapare-coloane lipsa: {r.text[:300]}"
|
||||
return int(m.group(1))
|
||||
|
||||
|
||||
def _map_columns(client, import_id, canon=None):
|
||||
return client.post(
|
||||
f"/_import/{import_id}/mapare-coloane",
|
||||
data={
|
||||
"colname": _HEADER,
|
||||
"canon": canon or _CANON,
|
||||
"format_data": "YYYY-MM-DD",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 1. denumire_op alimenteaza denumirea -> sugestie fuzzy utila #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_denumire_op_alimenteaza_denumirea_si_sugestia(client):
|
||||
import_id = _upload(client)
|
||||
_map_columns(client, import_id)
|
||||
|
||||
from app.db import get_connection
|
||||
from app.web.routes import _web_compute_preview
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
result = _web_compute_preview(conn, import_id, account_id=1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
assert not isinstance(result, str), result
|
||||
ops = result["unmapped_ops"]
|
||||
assert len(ops) == 1, ops
|
||||
op = ops[0]
|
||||
assert op["cod_op_service"] == "OP-REV"
|
||||
# Cheia: denumirea e descrierea reala, NU codul opac.
|
||||
assert op["denumire"] == "Revizie periodica"
|
||||
assert op["blocked"] == 2
|
||||
# Sugestia fuzzy gaseste OE-3 (REVIZIE PERIODICA) sus, cu scor real.
|
||||
assert op["suggestions"], "fara sugestii"
|
||||
top = op["suggestions"][0]
|
||||
assert top["cod_prestatie"] == "OE-3"
|
||||
assert top["score"] >= 60
|
||||
|
||||
|
||||
def test_fara_denumire_op_denumirea_e_codul(client):
|
||||
"""Control: daca NU mapezi coloana descriptiva, denumirea ramane codul opac."""
|
||||
import_id = _upload(client)
|
||||
# operatie mapat, descrierea ignorata
|
||||
_map_columns(client, import_id, canon=[
|
||||
"vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie", "",
|
||||
])
|
||||
|
||||
from app.db import get_connection
|
||||
from app.web.routes import _web_compute_preview
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
result = _web_compute_preview(conn, import_id, account_id=1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
op = result["unmapped_ops"][0]
|
||||
assert op["denumire"] == "OP-REV" # fara denumire_op -> codul
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 2. Preview expune panoul inline #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_preview_arata_panoul_de_mapare(client):
|
||||
import_id = _upload(client)
|
||||
r = _map_columns(client, import_id)
|
||||
assert r.status_code == 200
|
||||
assert "Operatii de mapat la cod RAR" in r.text
|
||||
assert "OP-REV" in r.text
|
||||
assert "/_import/%d/mapare-operatie" % import_id in r.text
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 3. POST mapare-operatie deblocheaza randurile (needs_mapping -> ok) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_mapare_operatie_deblocheaza_randurile(client):
|
||||
import_id = _upload(client)
|
||||
r = _map_columns(client, import_id)
|
||||
assert "needs_mapping" in r.text
|
||||
|
||||
# Inainte: 2 needs_mapping, 0 ok
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
b = conn.execute(
|
||||
"SELECT ok, needs_mapping FROM import_batches WHERE id=?", (import_id,)
|
||||
).fetchone()
|
||||
assert (b["ok"], b["needs_mapping"]) == (0, 2)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# Mapeaza OP-REV -> OE-3 (auto_send)
|
||||
rm = client.post(f"/_import/{import_id}/mapare-operatie", data={
|
||||
"cod_op_service": "OP-REV",
|
||||
"cod_prestatie": "OE-3",
|
||||
"auto_send": "true",
|
||||
})
|
||||
assert rm.status_code == 200, rm.text
|
||||
# Preview re-randat: randurile sunt acum ok, panoul a disparut
|
||||
assert "2 gata de trimis" in rm.text or "s-ok" in rm.text
|
||||
assert f"/_import/{import_id}/mapare-operatie" not in rm.text
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
b = conn.execute(
|
||||
"SELECT ok, needs_mapping FROM import_batches WHERE id=?", (import_id,)
|
||||
).fetchone()
|
||||
assert (b["ok"], b["needs_mapping"]) == (2, 0), dict(b)
|
||||
# Maparea s-a persistat (operations_mapping)
|
||||
m = conn.execute(
|
||||
"SELECT cod_prestatie, auto_send FROM operations_mapping "
|
||||
"WHERE account_id=1 AND cod_op_service='OP-REV'"
|
||||
).fetchone()
|
||||
assert m is not None and m["cod_prestatie"] == "OE-3" and m["auto_send"] == 1
|
||||
# import_rows reflecta noua stare (commit-ul citeste de aici)
|
||||
statuses = {
|
||||
row["resolved_status"]
|
||||
for row in conn.execute(
|
||||
"SELECT resolved_status FROM import_rows WHERE batch_id=?", (import_id,)
|
||||
)
|
||||
}
|
||||
assert statuses == {"ok"}, statuses
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_mapare_operatie_cod_necunoscut_nu_salveaza(client):
|
||||
import_id = _upload(client)
|
||||
_map_columns(client, import_id)
|
||||
|
||||
rm = client.post(f"/_import/{import_id}/mapare-operatie", data={
|
||||
"cod_op_service": "OP-REV",
|
||||
"cod_prestatie": "NUEXISTA",
|
||||
"auto_send": "true",
|
||||
})
|
||||
assert rm.status_code == 200
|
||||
assert "necunoscut" in rm.text.lower()
|
||||
# Inca needs_mapping, nimic salvat
|
||||
assert "Operatii de mapat la cod RAR" in rm.text
|
||||
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
m = conn.execute(
|
||||
"SELECT 1 FROM operations_mapping WHERE cod_op_service='OP-REV'"
|
||||
).fetchone()
|
||||
assert m is None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 4. A doua incarcare: maparea retinuta -> direct ok (zero config) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_a_doua_incarcare_foloseste_maparea_retinuta(client):
|
||||
# Prima incarcare + mapare coloane + mapare operatie
|
||||
import_id = _upload(client)
|
||||
_map_columns(client, import_id)
|
||||
client.post(f"/_import/{import_id}/mapare-operatie", data={
|
||||
"cod_op_service": "OP-REV", "cod_prestatie": "OE-3", "auto_send": "true",
|
||||
})
|
||||
|
||||
# A doua incarcare acelasi antet -> preview direct, fara operatii de mapat
|
||||
r = client.post(
|
||||
"/_import/upload",
|
||||
files={"file": ("t2.csv", _csv_bytes(_HEADER, _ROWS), "text/csv")},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
assert "/mapare-operatie" not in r.text
|
||||
assert "gata de trimis" in r.text
|
||||
Reference in New Issue
Block a user