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>
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
import time
|
||||
from typing import List
|
||||
from datetime import date
|
||||
from typing import List, Optional
|
||||
|
||||
import oracledb
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from shared.auth.dependencies import get_current_user
|
||||
from shared.auth.models import CurrentUser
|
||||
from shared.database.oracle_pool import oracle_pool
|
||||
|
||||
from ..schemas.comanda import (
|
||||
ComandaRequest, ComandaResponse,
|
||||
ComandaListResponse, ComandaRequest, ComandaResponse,
|
||||
FirmaItem, TipDevizItem, MasinaClientItem,
|
||||
)
|
||||
from ..services.comanda_service import ComandaService
|
||||
@@ -51,6 +52,26 @@ async def get_masini(_: CurrentUser = Depends(get_current_user)):
|
||||
return await LookupService.get_masini()
|
||||
|
||||
|
||||
@router.get("/comenzi", response_model=ComandaListResponse)
|
||||
async def list_comenzi(
|
||||
page: int = Query(default=1, ge=1),
|
||||
per_page: int = Query(default=20, ge=1, le=100),
|
||||
validat: Optional[int] = Query(default=None, ge=0, le=1),
|
||||
data_de_la: Optional[date] = Query(default=None),
|
||||
data_pana_la: Optional[date] = Query(default=None),
|
||||
_: CurrentUser = Depends(get_current_user),
|
||||
):
|
||||
# NOTE: DEV_ORDL has no id_firma column — firmă filter not available at DB level.
|
||||
# All comenzi in MARIUSM_AUTO schema are visible (companies 110/167/169 share schema).
|
||||
return await ComandaService.get_comenzi(
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
validat=validat,
|
||||
data_de_la=data_de_la,
|
||||
data_pana_la=data_pana_la,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/comenzi", response_model=ComandaResponse)
|
||||
async def creeaza_comanda(
|
||||
data: ComandaRequest,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -6,6 +8,7 @@ class ComandaRequest(BaseModel):
|
||||
id_masiniclient: int
|
||||
solicitari: str
|
||||
id_firma: int
|
||||
id_sucursala: Optional[int] = None
|
||||
|
||||
|
||||
class ComandaResponse(BaseModel):
|
||||
@@ -18,6 +21,7 @@ class FirmaItem(BaseModel):
|
||||
id_firma: int
|
||||
firma: str
|
||||
schema_name: str
|
||||
id_mama: Optional[int] = None
|
||||
|
||||
|
||||
class TipDevizItem(BaseModel):
|
||||
@@ -29,3 +33,22 @@ class TipDevizItem(BaseModel):
|
||||
class MasinaClientItem(BaseModel):
|
||||
id_masiniclient: int
|
||||
label: str # "PARTENER — MARCA MASINA, NRINMAT (ANFABRICATIE)"
|
||||
|
||||
|
||||
class ComandaListItem(BaseModel):
|
||||
id_ordl: int
|
||||
nrord: str
|
||||
datai: Optional[str] # ISO date "YYYY-MM-DD"
|
||||
validat: int # 0=deschisă, 1=validată
|
||||
inchis_fortat: int # 1=arhivată fără validare
|
||||
id_tip: int
|
||||
tip_denumire: str
|
||||
vehicul: str # "PARTENER — MARCA MASINA, NRINMAT (AN)"
|
||||
id_masiniclient: Optional[int]
|
||||
|
||||
|
||||
class ComandaListResponse(BaseModel):
|
||||
comenzi: List[ComandaListItem]
|
||||
total: int
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import re
|
||||
from typing import NoReturn
|
||||
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 ComandaRequest, ComandaResponse
|
||||
from ..schemas.comanda import (
|
||||
ComandaListItem, ComandaListResponse, ComandaRequest, ComandaResponse,
|
||||
)
|
||||
from .. import logger
|
||||
|
||||
_MAX_PER_PAGE = 100
|
||||
|
||||
|
||||
def _handle_oracle_error(e: Exception) -> NoReturn:
|
||||
"""
|
||||
@@ -81,6 +86,7 @@ class ComandaService:
|
||||
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
|
||||
],
|
||||
@@ -107,3 +113,108 @@ class ComandaService:
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,8 @@ Lookup data for service_auto forms — tip deviz, masini, firme.
|
||||
|
||||
All three endpoints are read-only and infrequently changing.
|
||||
"""
|
||||
from typing import List
|
||||
import time
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import oracledb
|
||||
from fastapi import HTTPException
|
||||
@@ -12,6 +13,23 @@ from shared.database.oracle_pool import oracle_pool
|
||||
from ..schemas.comanda import FirmaItem, MasinaClientItem, TipDevizItem
|
||||
from .. import logger
|
||||
|
||||
# In-memory TTL cache: key → (monotonic_timestamp, value)
|
||||
_cache: dict = {}
|
||||
|
||||
_TTL_TIP_DEVIZ = 86400 # 24 h — tip deviz changes only via DB migration
|
||||
_TTL_MASINI = 300 # 5 min — vehicle inventory changes regularly
|
||||
|
||||
|
||||
def _cache_get(key: str, ttl: float):
|
||||
entry: Optional[Tuple] = _cache.get(key)
|
||||
if entry and (time.monotonic() - entry[0]) < ttl:
|
||||
return entry[1]
|
||||
return None
|
||||
|
||||
|
||||
def _cache_set(key: str, value) -> None:
|
||||
_cache[key] = (time.monotonic(), value)
|
||||
|
||||
|
||||
class LookupService:
|
||||
|
||||
@@ -26,7 +44,7 @@ class LookupService:
|
||||
|
||||
placeholders = ", ".join(f":id{i}" for i in range(len(company_ids)))
|
||||
query = f"""
|
||||
SELECT id_firma, firma, schema
|
||||
SELECT id_firma, firma, schema, id_mama
|
||||
FROM CONTAFIN_ORACLE.V_NOM_FIRME
|
||||
WHERE id_firma IN ({placeholders})
|
||||
ORDER BY id_firma
|
||||
@@ -43,7 +61,7 @@ class LookupService:
|
||||
raise HTTPException(status_code=503, detail="Eroare la încărcarea firmelor")
|
||||
|
||||
return [
|
||||
FirmaItem(id_firma=r[0], firma=r[1], schema_name=r[2] or "")
|
||||
FirmaItem(id_firma=r[0], firma=r[1], schema_name=r[2] or "", id_mama=r[3])
|
||||
for r in rows
|
||||
]
|
||||
|
||||
@@ -51,8 +69,13 @@ class LookupService:
|
||||
async def get_tip_deviz() -> List[TipDevizItem]:
|
||||
"""
|
||||
Returns all active tip deviz from MARIUSM_AUTO.DEV_TIP_DEVIZ.
|
||||
Cached in-process for 24 h (changes only via DB migration).
|
||||
ROA_WEB has SELECT grant on this view.
|
||||
"""
|
||||
cached = _cache_get("tip_deviz", _TTL_TIP_DEVIZ)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
query = """
|
||||
SELECT id_tip, denumire, inch_validare
|
||||
FROM MARIUSM_AUTO.DEV_TIP_DEVIZ
|
||||
@@ -67,18 +90,25 @@ class LookupService:
|
||||
logger.error("get_tip_deviz Oracle error", exc_info=True)
|
||||
raise HTTPException(status_code=503, detail="Eroare la încărcarea tipurilor de deviz")
|
||||
|
||||
return [
|
||||
result = [
|
||||
TipDevizItem(id_tip=r[0], denumire=r[1], inch_validare=r[2] or 0)
|
||||
for r in rows
|
||||
]
|
||||
_cache_set("tip_deviz", result)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def get_masini() -> List[MasinaClientItem]:
|
||||
"""
|
||||
Returns active masini from MARIUSM_AUTO.AUTO_VMASINICLIENTI.
|
||||
Cached in-process for 5 min (vehicle inventory changes regularly).
|
||||
ROA_WEB has SELECT grant on this view.
|
||||
Label format: "PARTENER — MARCA MASINA, NRINMAT (ANFABRICATIE)"
|
||||
"""
|
||||
cached = _cache_get("masini", _TTL_MASINI)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
query = """
|
||||
SELECT id_masiniclient, nrinmat, marca, masina, anfabricatie, partener
|
||||
FROM MARIUSM_AUTO.AUTO_VMASINICLIENTI
|
||||
@@ -107,4 +137,5 @@ class LookupService:
|
||||
label = f"{partener or '?'} — {vehicul}, {nrinmat or '?'}{an_str}"
|
||||
result.append(MasinaClientItem(id_masiniclient=int(id_mc), label=label))
|
||||
|
||||
_cache_set("masini", result)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user