Files
roa2web-service-auto/backend/modules/service_auto/services/comanda_service.py
Claude Agent 0a880baef9 feat(service-auto): phase 2 — comenzi browse, id_sucursala, cache, migrare SQL
Backend:
- GET /api/service-auto/comenzi cu paginare server-side, filtre dată/status
- ComandaRequest.id_sucursala (Optional) + FirmaItem.id_mama
- get_firme() expune id_mama din V_NOM_FIRME
- callproc SP_CREEAZA_COMANDA_PROTOTIP cu 7 argumente (+ p_id_sucursala)
- Cache TTL in-process: tip_deviz 24h, masini 5min

Frontend:
- ComenziBrowseView.vue — DataTable lazy + filtre + status badges
- ComandaNoua.vue — company store integration, idSucursala computed
- service-auto/stores/sharedStores.js (createCompaniesStore factory)
- HamburgerMenu: secțiune Service Auto (Comenzi + Comandă Nouă)
- router: /service-auto/comenzi

SQL:
- migrations/ff_2026_04_12_01_AUTO.sql — idempotent (COLUMNEXIST guard + CREATE OR REPLACE SP)
- onboarding_roa_web.sql — versioned, parametrizat cu :SCHEMA_NAME
- .claude/rules/oracle-migrations.md — convenție ff_YYYY_MM_DD_NN_MODULE.sql

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-05 09:37:09 +00:00

221 lines
7.8 KiB
Python

import re
from datetime import date
from typing import List, NoReturn, Optional
import oracledb
from fastapi import HTTPException
from shared.database.oracle_pool import oracle_pool
from ..schemas.comanda import (
ComandaListItem, ComandaListResponse, ComandaRequest, ComandaResponse,
)
from .. import logger
_MAX_PER_PAGE = 100
def _handle_oracle_error(e: Exception) -> NoReturn:
"""
Map Oracle error codes to FastAPI HTTPExceptions. Always raises.
Code ranges:
20001-20999 → 422 Unprocessable (business rule errors from RAISE_APPLICATION_ERROR)
12541/12170/12154/12560 → 503 Service Unavailable (Oracle unreachable / network)
1017 → 500 + CRITICAL log (bad credentials — config error)
942 → 500 + CRITICAL log (missing object/grant — deployment error)
* → 500 + ERROR log (unexpected)
"""
err = e.args[0]
code = getattr(err, "code", 0)
raw_message = getattr(err, "message", str(e))
if 20001 <= code <= 20999:
# Strip "ORA-2xxxx: " prefix injected by Oracle; expose the business message only
clean = re.sub(r"^ORA-\d+:\s*", "", raw_message).strip()
raise HTTPException(status_code=422, detail=clean)
if code in (12541, 12170, 12154, 12560):
raise HTTPException(
status_code=503,
detail="Serviciul bazei de date e temporar indisponibil",
)
if code == 1017:
logger.critical("Oracle credentials rejected (ORA-01017)", exc_info=True)
raise HTTPException(
status_code=500,
detail="Eroare de configurare. Contactați administratorul.",
)
if code == 942:
logger.critical("Oracle object not found or grant missing (ORA-00942)", exc_info=True)
raise HTTPException(
status_code=500,
detail="Eroare internă. Contactați administratorul.",
)
logger.error("Unexpected Oracle error ORA-%05d", code, exc_info=True)
raise HTTPException(status_code=500, detail="Eroare internă neașteptată")
class ComandaService:
@staticmethod
async def creeaza_comanda(
data: ComandaRequest,
username: str,
) -> ComandaResponse:
logger.info(
"service_auto.create_comanda START",
extra={
"user": username,
"tip": data.tip_id,
"client_id": data.id_masiniclient,
"id_firma": data.id_firma,
},
)
async with oracle_pool.get_connection("mariusm_test") as connection:
try:
with connection.cursor() as cursor:
out_id_ordl = cursor.var(oracledb.NUMBER)
out_nrord = cursor.var(oracledb.STRING)
cursor.callproc(
"MARIUSM_AUTO.SP_CREEAZA_COMANDA_PROTOTIP",
[
data.tip_id, # p_tip IN NUMBER
data.id_masiniclient, # p_id_masiniclient IN NUMBER
data.solicitari, # p_solicitari IN VARCHAR2
data.id_firma, # p_id_firma IN NUMBER
data.id_sucursala, # p_id_sucursala IN NUMBER (None for parent firm)
out_id_ordl, # p_id_ordl OUT NUMBER
out_nrord, # p_nrord OUT VARCHAR2
],
)
connection.commit()
id_ordl = int(out_id_ordl.getvalue())
nrord = out_nrord.getvalue() or ""
except oracledb.DatabaseError as e:
try:
connection.rollback()
except Exception:
pass # connection may be dead on network errors; ignore
_handle_oracle_error(e)
logger.info(
"service_auto.create_comanda OK",
extra={"user": username, "id_ordl": id_ordl, "nrord": nrord},
)
return ComandaResponse(
id_ordl=id_ordl,
nrord=nrord,
mesaj=f"Comanda {nrord} creata cu succes.",
)
@staticmethod
async def get_comenzi(
page: int,
per_page: int,
validat: Optional[int],
data_de_la: Optional[date],
data_pana_la: Optional[date],
) -> ComandaListResponse:
per_page = min(per_page, _MAX_PER_PAGE)
offset = (page - 1) * per_page
where_parts = ["d.sters = 0"]
filter_params: dict = {}
if validat is not None:
where_parts.append("d.validat = :validat")
filter_params["validat"] = validat
if data_de_la is not None:
where_parts.append("d.datai >= :data_de_la")
filter_params["data_de_la"] = data_de_la
if data_pana_la is not None:
# +1 day range avoids TRUNC (keeps index use on datai)
where_parts.append("d.datai < :data_pana_la + 1")
filter_params["data_pana_la"] = data_pana_la
where_clause = " AND ".join(where_parts)
base_from = f"""
FROM MARIUSM_AUTO.DEV_ORDL d
LEFT JOIN MARIUSM_AUTO.NOM_LUCRARI l ON d.id_lucrare = l.id_lucrare
LEFT JOIN MARIUSM_AUTO.AUTO_VMASINICLIENTI mc
ON d.id_masiniclient = mc.id_masiniclient
LEFT JOIN MARIUSM_AUTO.DEV_TIP_DEVIZ t ON d.id_tip = t.id_tip
WHERE {where_clause}
"""
count_query = f"SELECT COUNT(*) {base_from}"
data_query = f"""
SELECT d.id_ordl, l.nrord, d.datai, d.validat, d.inchis_fortat,
d.id_tip, t.denumire,
d.id_masiniclient, mc.nrinmat, mc.marca, mc.masina,
mc.anfabricatie, mc.partener
{base_from}
ORDER BY d.datai DESC, d.id_ordl DESC
OFFSET :offset ROWS FETCH NEXT :per_page ROWS ONLY
"""
try:
async with oracle_pool.get_connection("mariusm_test") as conn:
with conn.cursor() as cur:
cur.execute(count_query, filter_params)
total = cur.fetchone()[0]
cur.execute(
data_query,
{**filter_params, "offset": offset, "per_page": per_page},
)
rows = cur.fetchall()
except oracledb.DatabaseError as e:
_handle_oracle_error(e)
comenzi: List[ComandaListItem] = []
for r in rows:
(id_ordl, nrord, datai, validat_val, inchis_fortat,
id_tip, tip_denumire,
id_mc, nrinmat, marca, masina, an, partener) = r
if id_mc:
parts = []
if marca:
parts.append(marca)
if masina:
parts.append(masina)
vehicul_str = " ".join(parts) if parts else "?"
an_str = f" ({int(an)})" if an else ""
vehicul = f"{partener or '?'}{vehicul_str}, {nrinmat or '?'}{an_str}"
else:
vehicul = ""
comenzi.append(ComandaListItem(
id_ordl=int(id_ordl),
nrord=nrord or "",
datai=datai.strftime("%Y-%m-%d") if datai else None,
validat=int(validat_val),
inchis_fortat=int(inchis_fortat or 0),
id_tip=int(id_tip),
tip_denumire=tip_denumire or "",
vehicul=vehicul,
id_masiniclient=int(id_mc) if id_mc else None,
))
logger.debug(
"service_auto.get_comenzi page=%d per_page=%d total=%d",
page, per_page, total,
)
return ComandaListResponse(
comenzi=comenzi,
total=total,
page=page,
per_page=per_page,
)