5.12 (livrat): editare in modal a randurilor de preview, cont obligatoriu inainte de import, formular editare extras (_form_editare, _editare_preview_modal), plus suita de teste aferenta (preview edit/compact, mapare op, form editare, signup, admin panel). Design + planificare: - docs/design.md: sistem de design (tokeni, breakpoints, scara control, componente, a11y). - docs/prd/prd-5.12-* si prd-5.13-* (5.13 cu raport /autoplan: CEO+Design+Eng, audit trail). Curatare: sterse PNG-urile de test/mockup temporare din radacina. Nota: implementarea CSS 5.13 (responsive compact + sistem butoane) NU e inca facuta — planul revizuit cere refactorul testelor fragile din test_web_responsive.py INAINTE de CSS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
250 lines
8.7 KiB
Python
250 lines
8.7 KiB
Python
"""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
|
|
# US-004: panoul foloseste ruta plurala (un singur form pentru toate operatiile)
|
|
assert "/_import/%d/mapare-operatii" % 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
|