5.15 (propagare design + dashboard editare) si 5.14 (mapare LLM distilata) inchise dupa /code-review high. 8 buguri reparate TDD: - HIGH modal nu se deschidea pe randul slim (base.html: trimitere-slim) - HIGH /repune trunchia prestatii (declaratie incompleta la RAR) -> iterare peste existing, codes pozitional - HIGH embeddings incarca model ~230MB degeaba pe corpus gol -> poarta has_corpus() - HIGH picker chips gol pe re-render eroare -> conn/account_id pe toate ramurile - MED obs re-derivat dupa stergere explicita -> _merge_override pastreaza obs='' - MED mapare salvata fara denumire poluă GOLD -> _record_gold_validation guard - MED typo nome_prestatie -> nume_prestatie in select /repune - MED bucketare timp +3h gresita iarna -> SQLite localtime + TZ=Europe/Bucharest Embeddings WIRE-uit functional (PRD #15, decizie user): ensure_embeddings_corpus construieste corpus din nomenclator, gated pe AUTOPASS_EMBEDDINGS_ENABLED (default off). Marime model corectata ~50MB->~230MB (estimare PRD gresita). Cleanup: hoist load_* din bucla bulk-fix; import re la top. Regresie: 1256 passed, 1 deselected (live), 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
547 lines
20 KiB
Python
547 lines
20 KiB
Python
"""Teste US-006 (PRD 5.15): prestatii multi-cod (lista) la editare/corectie.
|
|
|
|
AC-uri verificate:
|
|
- Handler-ele accepta LISTA de cod_prestatie (form.getlist) -> prestatii cu mai multe coduri.
|
|
- cod_op_service/denumire RAMAN pe item (invariant D7, E1 IRON RULE).
|
|
- Cod invalid -> respins cu mesaj; cod necunoscut NU ajunge la RAR (ORA-12899).
|
|
- Lista goala -> ramane needs_mapping.
|
|
- Dedup per-item: (op_service, cod) unic, NU cod unic (doua ops diferite cu acelasi cod ok).
|
|
- Recalcul idempotenta dupa editare.
|
|
- odometruInitial obligatoriu cand cod_prestatie contine R-ODO/I-ODO.
|
|
- REGRESIE E1 (IRON RULE): op_service supravietuieste /repune cu cod.
|
|
|
|
TDD: toate testele sunt scrise INAINTE de implementare (RED -> GREEN).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import tempfile
|
|
|
|
import pytest
|
|
from starlette.testclient import TestClient
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Fixtures #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
@pytest.fixture()
|
|
def client(monkeypatch):
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "prestatii.db"))
|
|
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true")
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.web import ratelimit
|
|
ratelimit._hits.clear()
|
|
from app.main import app
|
|
with TestClient(app, follow_redirects=False) as c:
|
|
yield c
|
|
ratelimit._hits.clear()
|
|
get_settings.cache_clear()
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Helpere #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def _create_account_user(email: str, password: str = "parolasecreta10"):
|
|
from app.accounts import create_account
|
|
from app.users import create_user
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
acct_id = create_account(conn, "Service", active=True)
|
|
create_user(conn, acct_id, email, password)
|
|
return acct_id
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _login(client, email: str, password: str = "parolasecreta10") -> None:
|
|
resp = client.get("/login")
|
|
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \
|
|
re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
|
|
assert m, "csrf_token nu gasit in login"
|
|
resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)})
|
|
assert resp.status_code == 303
|
|
|
|
|
|
def _csrf(client) -> str:
|
|
resp = client.get("/?tab=coada")
|
|
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text)
|
|
assert m, "csrf_token nu gasit in dashboard"
|
|
return m.group(1)
|
|
|
|
|
|
def _insert(acct: int, *, status: str, payload: dict, key: str | None = None) -> int:
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
k = key or f"k-{os.urandom(6).hex()}"
|
|
cur = conn.execute(
|
|
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
|
|
"VALUES (?, ?, ?, ?)",
|
|
(k, acct, status, json.dumps(payload)),
|
|
)
|
|
conn.commit()
|
|
return int(cur.lastrowid)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _row(sid: int):
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
return conn.execute("SELECT * FROM submissions WHERE id=?", (sid,)).fetchone()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _payload_json(sid: int) -> dict:
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
r = conn.execute("SELECT payload_json FROM submissions WHERE id=?", (sid,)).fetchone()
|
|
return json.loads(r["payload_json"])
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _seed_cod(cod: str, denumire: str = "Prestatie test") -> None:
|
|
"""Insereaza un cod in nomenclator_rar (fara operatii_mapping)."""
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
|
|
(cod, denumire),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _payload_cu_ops(vin: str, ops: list[tuple[str, str]]) -> dict:
|
|
"""Payload cu prestatii avand cod_op_service/denumire (needs_mapping state)."""
|
|
return {
|
|
"vin": vin,
|
|
"nr_inmatriculare": "B100AAA",
|
|
"data_prestatie": "2026-06-10",
|
|
"odometru_final": "50000",
|
|
"prestatii": [
|
|
{"cod_op_service": op, "denumire": den}
|
|
for op, den in ops
|
|
],
|
|
}
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Teste #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_mai_multe_coduri_acceptate(client):
|
|
"""US-006 AC1: LISTA de cod_prestatie -> prestatii cu N itemi, fiecare cu cod setat.
|
|
|
|
RED: form.get("cod_prestatie") intoarce doar primul cod; form.getlist necesar.
|
|
"""
|
|
acct = _create_account_user("multi.cod@test.com")
|
|
_login(client, "multi.cod@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
_seed_cod("IG-1", "Inlocuire garnitura")
|
|
|
|
sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops(
|
|
"WVWZZZ1JZXW0MC001",
|
|
[("Op-A", "Schimb ulei motor"), ("Op-B", "Inlocuire garnitura chiulasa")],
|
|
))
|
|
csrf = _csrf(client)
|
|
|
|
resp = client.post(
|
|
f"/trimitere/{sid}/corecteaza",
|
|
data={
|
|
"csrf_token": csrf,
|
|
"cod_prestatie": ["OE-1", "IG-1"], # 2 coduri pentru 2 operatii
|
|
},
|
|
)
|
|
assert resp.status_code == 200, resp.text[:500]
|
|
|
|
r = _row(sid)
|
|
assert r["status"] == "queued", f"status asteptat queued, got {r['status']}"
|
|
prestatii = _payload_json(sid)["prestatii"]
|
|
assert len(prestatii) == 2, f"asteptat 2 prestatii, got {len(prestatii)}: {prestatii}"
|
|
coduri = [p.get("cod_prestatie") for p in prestatii]
|
|
assert "OE-1" in coduri, f"OE-1 lipsa din prestatii: {prestatii}"
|
|
assert "IG-1" in coduri, f"IG-1 lipsa din prestatii: {prestatii}"
|
|
|
|
|
|
def test_cod_op_service_pastrat_dupa_corecteaza(client):
|
|
"""E1/D7: cod_op_service si denumire RAMAN pe item dupa /corecteaza cu cod direct.
|
|
|
|
RED: implementarea veche injecta in prestatii[0] fara sa afecteze op_service
|
|
(intr-adevar in /corecteaza nu se facea pop), dar testul confirma explicit invariantul.
|
|
"""
|
|
acct = _create_account_user("op.pastrat@test.com")
|
|
_login(client, "op.pastrat@test.com")
|
|
_seed_cod("OE-1")
|
|
|
|
sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops(
|
|
"WVWZZZ1JZXW0OP001",
|
|
[("Schimb ulei", "Schimb ulei motor 5W30")],
|
|
))
|
|
csrf = _csrf(client)
|
|
|
|
resp = client.post(
|
|
f"/trimitere/{sid}/corecteaza",
|
|
data={"csrf_token": csrf, "cod_prestatie": "OE-1"},
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
prestatii = _payload_json(sid)["prestatii"]
|
|
assert len(prestatii) == 1
|
|
item = prestatii[0]
|
|
assert item.get("cod_prestatie") == "OE-1", f"cod_prestatie lipsa: {item}"
|
|
assert item.get("cod_op_service") == "Schimb ulei", f"cod_op_service pierdut: {item}"
|
|
assert item.get("denumire") == "Schimb ulei motor 5W30", f"denumire pierduta: {item}"
|
|
|
|
|
|
def test_cod_invalid_respins(client):
|
|
"""US-006 AC3: cod necunoscut in nomenclator -> respins cu mesaj, status neschimbat.
|
|
|
|
RED: validarea fata de nomenclator nu e aplicata per-cod la multi-select.
|
|
"""
|
|
acct = _create_account_user("cod.invalid@test.com")
|
|
_login(client, "cod.invalid@test.com")
|
|
# NU seed-uim "XX-99" -> cod necunoscut
|
|
|
|
sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops(
|
|
"WVWZZZ1JZXW0CI001",
|
|
[("Op-Test", "Operatie test")],
|
|
))
|
|
old_status = _row(sid)["status"]
|
|
csrf = _csrf(client)
|
|
|
|
resp = client.post(
|
|
f"/trimitere/{sid}/corecteaza",
|
|
data={"csrf_token": csrf, "cod_prestatie": "XX-99"},
|
|
)
|
|
assert resp.status_code == 200
|
|
# Cod invalid -> mesaj de eroare vizibil
|
|
assert "XX-99" in resp.text or "necunoscut" in resp.text.lower(), (
|
|
f"Mesaj de eroare lipsa pentru cod invalid; text={resp.text[:500]}"
|
|
)
|
|
# Status neschimbat
|
|
assert _row(sid)["status"] == old_status, (
|
|
f"Status s-a schimbat desi codul e invalid: {_row(sid)['status']}"
|
|
)
|
|
|
|
|
|
def test_lista_goala_needs_mapping(client):
|
|
"""US-006 AC4: nicio cod_prestatie trimis -> submission ramane needs_mapping.
|
|
|
|
RED: cu multi-select, lista goala nu injecteaza nimic; resolve_prestatii
|
|
gaseste inca operatii nemapate -> trebuie sa ramana needs_mapping.
|
|
"""
|
|
acct = _create_account_user("goala.nemap@test.com")
|
|
_login(client, "goala.nemap@test.com")
|
|
# NU seed-uim nicio mapare -> operatia ramane nemapata
|
|
|
|
sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops(
|
|
"WVWZZZ1JZXW0GN001",
|
|
[("Op-Nemap", "Operatie nemapata")],
|
|
))
|
|
csrf = _csrf(client)
|
|
|
|
# Trimit form FARA cod_prestatie (lista goala)
|
|
resp = client.post(
|
|
f"/trimitere/{sid}/corecteaza",
|
|
data={"csrf_token": csrf},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert _row(sid)["status"] == "needs_mapping", (
|
|
f"Status trebuia sa ramana needs_mapping, got {_row(sid)['status']}"
|
|
)
|
|
|
|
|
|
def test_idempotency_recalculat(client):
|
|
"""US-006 AC6: dupa setarea de coduri noi, cheia de idempotenta e recalculata.
|
|
|
|
RED: single-cod injecta in prestatii[0] si recalcula cheia; cu multi-cod
|
|
acelasi mecanism se aplica tuturor itemilor.
|
|
"""
|
|
acct = _create_account_user("ido.recalc@test.com")
|
|
_login(client, "ido.recalc@test.com")
|
|
_seed_cod("OE-1")
|
|
|
|
sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops(
|
|
"WVWZZZ1JZXW0ND001",
|
|
[("Op-Ido", "Operatie ido")],
|
|
))
|
|
old_key = _row(sid)["idempotency_key"]
|
|
csrf = _csrf(client)
|
|
|
|
resp = client.post(
|
|
f"/trimitere/{sid}/corecteaza",
|
|
data={"csrf_token": csrf, "cod_prestatie": "OE-1"},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert _row(sid)["status"] == "queued"
|
|
new_key = _row(sid)["idempotency_key"]
|
|
assert new_key != old_key, (
|
|
f"Cheia de idempotenta NU s-a schimbat dupa setarea codului: {new_key}"
|
|
)
|
|
|
|
|
|
def test_odometru_initial_conditionat_R_ODO(client):
|
|
"""US-006 AC7: cod_prestatie=R-ODO fara odometruInitial -> validate_prezentare
|
|
intoarce eroare -> submission ramane needs_data (NU queued).
|
|
|
|
RED: validarea R-ODO e deja in validate_prezentare; testul confirma ca
|
|
multi-cod nu bypass-eaza aceasta regula.
|
|
"""
|
|
acct = _create_account_user("odo.rodo@test.com")
|
|
_login(client, "odo.rodo@test.com")
|
|
_seed_cod("R-ODO", "Revizie odometru")
|
|
|
|
# Payload: needs_mapping (op fara cod), FARA odometru_initial
|
|
sid = _insert(acct, status="needs_mapping", payload={
|
|
"vin": "WVWZZZ1JZXW0RO001",
|
|
"nr_inmatriculare": "B100AAA",
|
|
"data_prestatie": "2026-06-10",
|
|
"odometru_final": "50000",
|
|
# odometru_initial ABSENT
|
|
"prestatii": [{"cod_op_service": "Revizie", "denumire": "Revizie odometru"}],
|
|
})
|
|
csrf = _csrf(client)
|
|
|
|
# Trimit R-ODO ca cod (valid in nomenclator), dar fara odometru_initial
|
|
resp = client.post(
|
|
f"/trimitere/{sid}/corecteaza",
|
|
data={"csrf_token": csrf, "cod_prestatie": "R-ODO"},
|
|
)
|
|
assert resp.status_code == 200
|
|
status = _row(sid)["status"]
|
|
# R-ODO fara odometruInitial -> validare esuata -> needs_data (nu queued)
|
|
assert status in ("needs_data", "needs_mapping"), (
|
|
f"Status neasteptat: {status}; trebuia needs_data/needs_mapping (R-ODO fara odo initial)"
|
|
)
|
|
assert status != "queued", (
|
|
"R-ODO fara odometruInitial NU trebuie sa treaca in queued!"
|
|
)
|
|
|
|
|
|
def test_dedup_per_item_nu_dupa_cod(client):
|
|
"""US-006 AC5 (E4): doua operatii DIFERITE cu ACELASI cod RAR ambele supravietuiesc.
|
|
|
|
Dedup = (op_service, cod) identice, NU cod singur. Doua ops distincte pot
|
|
mapa legitim la acelasi cod RAR fara sa fie sterse de dedup.
|
|
|
|
RED: dedupare naiva dupa cod ar sterge a doua operatie (op-B cu acelasi OE-1).
|
|
"""
|
|
acct = _create_account_user("dedup.ops@test.com")
|
|
_login(client, "dedup.ops@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
|
|
# Doua operatii distincte, ambele vor primi OE-1
|
|
sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops(
|
|
"WVWZZZ1JZXW0DD001",
|
|
[("Op-A", "Prima operatie"), ("Op-B", "A doua operatie")],
|
|
))
|
|
csrf = _csrf(client)
|
|
|
|
resp = client.post(
|
|
f"/trimitere/{sid}/corecteaza",
|
|
data={
|
|
"csrf_token": csrf,
|
|
"cod_prestatie": ["OE-1", "OE-1"], # acelasi cod pentru ambele ops
|
|
},
|
|
)
|
|
assert resp.status_code == 200
|
|
prestatii = _payload_json(sid)["prestatii"]
|
|
# Ambele TREBUIE sa supravietuiasca: (Op-A, OE-1) != (Op-B, OE-1)
|
|
assert len(prestatii) == 2, (
|
|
f"Dedup a sters o operatie distincta! prestatii={prestatii} "
|
|
"(doua ops cu acelasi cod trebuie pastrate)"
|
|
)
|
|
ops = [p.get("cod_op_service") for p in prestatii]
|
|
assert "Op-A" in ops and "Op-B" in ops, f"ops_service pierdute: {ops}"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Test de regresie E1 (IRON RULE): op_service supravietuieste /repune cu cod #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_op_service_supravietuieste_repune_cu_cod(client):
|
|
"""E1 IRON RULE: dupa /repune cu cod_prestatie, cod_op_service/denumire RAMAN pe item.
|
|
|
|
RED: routes.py:1371 face `p0.pop("cod_op_service", None)` — sterge operatia
|
|
cand se seteaza un cod direct prin /repune. US-006 ELIMINA acel pop.
|
|
|
|
Aceasta regresie e CRITICA: sterge contextul op->cod necesar pentru US-009
|
|
(salvare mapare din chip) si rupe invariantul D7.
|
|
"""
|
|
acct = _create_account_user("e1.repune@test.com")
|
|
_login(client, "e1.repune@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei motor")
|
|
|
|
# Starea error: payload cu op_service (operatia venita de la import/API)
|
|
sid = _insert(acct, status="error", payload={
|
|
"vin": "WVWZZZ1JZXW0E1001",
|
|
"nr_inmatriculare": "B100AAA",
|
|
"data_prestatie": "2026-06-10",
|
|
"odometru_final": "50000",
|
|
"prestatii": [{
|
|
"cod_op_service": "Schimb ulei",
|
|
"denumire": "Schimb ulei motor 5W30",
|
|
# fara cod_prestatie initial
|
|
}],
|
|
})
|
|
csrf = _csrf(client)
|
|
|
|
# /repune cu cod direct
|
|
resp = client.post(
|
|
f"/trimitere/{sid}/repune",
|
|
data={"csrf_token": csrf, "cod_prestatie": "OE-1"},
|
|
)
|
|
assert resp.status_code == 200, resp.text[:500]
|
|
|
|
r = _row(sid)
|
|
assert r["status"] == "queued", f"status neasteptat: {r['status']}"
|
|
|
|
prestatii = _payload_json(sid)["prestatii"]
|
|
assert len(prestatii) == 1
|
|
item = prestatii[0]
|
|
|
|
# IRON RULE E1: op_service si denumire TREBUIE sa fie prezente
|
|
assert item.get("cod_op_service") == "Schimb ulei", (
|
|
f"E1 VIOLATED: cod_op_service a fost sters de /repune! item={item}"
|
|
)
|
|
assert item.get("denumire") == "Schimb ulei motor 5W30", (
|
|
f"E1 VIOLATED: denumire a fost stearsa de /repune! item={item}"
|
|
)
|
|
# Codul trebuie setat
|
|
assert item.get("cod_prestatie") == "OE-1", (
|
|
f"cod_prestatie nu a fost setat corect: item={item}"
|
|
)
|
|
|
|
|
|
def test_repune_nu_trunchiaza_prestatii_multiple(client):
|
|
"""Bug fix (code-review 5.15): /repune NU pierde prestatii[1:].
|
|
|
|
Formularul /repune trimite UN SINGUR select cod_prestatie. Implementarea veche
|
|
itera `enumerate(codes)` -> pastra doar len(codes) itemi, deci un rand error cu
|
|
2+ prestatii pierdea toate prestatiile dupa prima -> declaratie INCOMPLETA la RAR
|
|
(FINALIZATA ireversibil). Fix: iteram peste `existing`, aplicam codes pozitional,
|
|
pastram toate prestatiile.
|
|
|
|
RED inainte de fix: len(prestatii) == 1 (a doua prestatie pierduta).
|
|
"""
|
|
acct = _create_account_user("repune.multi@test.com")
|
|
_login(client, "repune.multi@test.com")
|
|
_seed_cod("AAA", "Prestatie A")
|
|
_seed_cod("BBB", "Prestatie B")
|
|
_seed_cod("CCC", "Prestatie C")
|
|
|
|
# Rand error cu DOUA prestatii (ambele cu cod valid).
|
|
sid = _insert(acct, status="error", payload={
|
|
"vin": "WVWZZZ1JZXW0RM001",
|
|
"nr_inmatriculare": "B100AAA",
|
|
"data_prestatie": "2026-06-10",
|
|
"odometru_final": "50000",
|
|
"prestatii": [
|
|
{"cod_prestatie": "AAA"},
|
|
{"cod_prestatie": "BBB"},
|
|
],
|
|
})
|
|
csrf = _csrf(client)
|
|
|
|
# /repune cu UN SINGUR cod nou (schimba prima prestatie).
|
|
resp = client.post(
|
|
f"/trimitere/{sid}/repune",
|
|
data={"csrf_token": csrf, "cod_prestatie": "CCC"},
|
|
)
|
|
assert resp.status_code == 200, resp.text[:500]
|
|
|
|
r = _row(sid)
|
|
assert r["status"] == "queued", f"status neasteptat: {r['status']}"
|
|
|
|
prestatii = _payload_json(sid)["prestatii"]
|
|
assert len(prestatii) == 2, (
|
|
f"AMBELE prestatii trebuie pastrate de /repune, nu doar prima! got={prestatii}"
|
|
)
|
|
coduri = [p.get("cod_prestatie") for p in prestatii]
|
|
assert coduri == ["CCC", "BBB"], (
|
|
f"Codul nou se aplica POZITIONAL primei prestatii, a doua ramane intacta: {coduri}"
|
|
)
|
|
|
|
|
|
def test_corectie_eroare_validare_pastreaza_picker(client):
|
|
"""Bug fix (code-review 5.15): re-render-ul de eroare validare pastreaza optiunile pickerului.
|
|
|
|
post_corectie_trimitere re-randa _trimitere_detaliu pe ramura erori-validare FARA
|
|
`conn`/`account_id` -> `nomenclator_rar=[]` -> picker-ul chips randa ZERO optiuni ->
|
|
userul nu mai poate alege cod RAR fara sa inchida+redeschida modalul. Fix: pasam
|
|
`conn`+`account_id` la _detaliu_ctx pe TOATE ramurile de re-render.
|
|
|
|
RED inainte de fix: codul de picker "PK-1" lipseste din re-render.
|
|
"""
|
|
acct = _create_account_user("corectie.picker@test.com")
|
|
_login(client, "corectie.picker@test.com")
|
|
_seed_cod("ZZ-9", "Operatie existenta") # codul curent al randului (valid -> fara unmapped)
|
|
_seed_cod("PK-1", "Optiune picker") # cod doar in nomenclator (detector de picker)
|
|
|
|
# needs_data editabil, prestatie cu cod direct valid (resolve OK, fara unmapped).
|
|
sid = _insert(acct, status="needs_data", payload={
|
|
"vin": "WVWZZZ1JZXW0PK001",
|
|
"nr_inmatriculare": "B100AAA",
|
|
"data_prestatie": "2026-06-10",
|
|
"odometru_final": "50000",
|
|
"prestatii": [{"cod_prestatie": "ZZ-9"}],
|
|
})
|
|
csrf = _csrf(client)
|
|
|
|
# Corectie cu VIN invalid -> validare esueaza -> ramura de re-render 1432.
|
|
resp = client.post(
|
|
f"/trimitere/{sid}/corecteaza",
|
|
data={"csrf_token": csrf, "vin": "BAD"},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert _row(sid)["status"] == "needs_data"
|
|
# Picker-ul trebuie sa contina optiunile din nomenclator (conn/account_id pasate).
|
|
assert "PK-1" in resp.text, (
|
|
"Picker-ul chips e GOL dupa eroare de validare — _detaliu_ctx fara conn/account_id"
|
|
)
|
|
|
|
|
|
def test_repune_select_afiseaza_denumirea(client):
|
|
"""Bug fix (code-review 5.15): selectul /repune afiseaza denumirea operatiei.
|
|
|
|
Template-ul folosea cheia gresita `item.nome_prestatie` (typo) -> optiunile
|
|
apareau ca "AAA — " fara denumire. Cheia corecta e `nume_prestatie`.
|
|
"""
|
|
acct = _create_account_user("repune.denumire@test.com")
|
|
_login(client, "repune.denumire@test.com")
|
|
_seed_cod("AAA", "Schimb ulei motor")
|
|
|
|
sid = _insert(acct, status="error", payload={
|
|
"vin": "WVWZZZ1JZXW0RD001",
|
|
"nr_inmatriculare": "B100AAA",
|
|
"data_prestatie": "2026-06-10",
|
|
"odometru_final": "50000",
|
|
"prestatii": [{"cod_prestatie": "AAA"}],
|
|
})
|
|
|
|
resp = client.get(f"/_fragments/trimitere/{sid}")
|
|
assert resp.status_code == 200
|
|
html = resp.text
|
|
# Optiunea trebuie sa afiseze denumirea, nu doar codul gol.
|
|
assert "Schimb ulei motor" in html, (
|
|
"Selectul /repune nu afiseaza denumirea operatiei (typo nome_prestatie)"
|
|
)
|
|
assert "AAA — Schimb ulei motor" in html, (
|
|
f"Optiunea select nu randeaza 'cod — denumire': {html[html.find('AAA'):html.find('AAA')+60]}"
|
|
)
|