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:
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