Replace Flask admin with FastAPI app (api/app/) featuring: - Dashboard with stat cards, sync control, and history - Mappings CRUD for ARTICOLE_TERTI with CSV import/export - Article autocomplete from NOM_ARTICOLE - SKU pre-validation before import - Sync orchestration: read JSONs -> validate -> import -> log to SQLite - APScheduler for periodic sync from UI - File logging to logs/sync_comenzi_YYYYMMDD_HHMMSS.log - Oracle pool None guard (503 vs 500 on unavailable) Test suite: - test_app_basic.py: 30 tests (imports + routes) without Oracle - test_integration.py: 9 integration tests with Oracle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
from fastapi import APIRouter, Query, Request, UploadFile, File
|
|
from fastapi.responses import StreamingResponse, HTMLResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from pydantic import BaseModel
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
import io
|
|
|
|
from ..services import mapping_service, sqlite_service
|
|
|
|
router = APIRouter(tags=["mappings"])
|
|
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
|
|
|
class MappingCreate(BaseModel):
|
|
sku: str
|
|
codmat: str
|
|
cantitate_roa: float = 1
|
|
procent_pret: float = 100
|
|
|
|
class MappingUpdate(BaseModel):
|
|
cantitate_roa: Optional[float] = None
|
|
procent_pret: Optional[float] = None
|
|
activ: Optional[int] = None
|
|
|
|
# HTML page
|
|
@router.get("/mappings", response_class=HTMLResponse)
|
|
async def mappings_page(request: Request):
|
|
return templates.TemplateResponse("mappings.html", {"request": request})
|
|
|
|
# API endpoints
|
|
@router.get("/api/mappings")
|
|
def list_mappings(search: str = "", page: int = 1, per_page: int = 50):
|
|
return mapping_service.get_mappings(search=search, page=page, per_page=per_page)
|
|
|
|
@router.post("/api/mappings")
|
|
async def create_mapping(data: MappingCreate):
|
|
try:
|
|
result = mapping_service.create_mapping(data.sku, data.codmat, data.cantitate_roa, data.procent_pret)
|
|
# Mark SKU as resolved in missing_skus tracking
|
|
await sqlite_service.resolve_missing_sku(data.sku)
|
|
return {"success": True, **result}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
@router.put("/api/mappings/{sku}/{codmat}")
|
|
def update_mapping(sku: str, codmat: str, data: MappingUpdate):
|
|
try:
|
|
updated = mapping_service.update_mapping(sku, codmat, data.cantitate_roa, data.procent_pret, data.activ)
|
|
return {"success": updated}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
@router.delete("/api/mappings/{sku}/{codmat}")
|
|
def delete_mapping(sku: str, codmat: str):
|
|
try:
|
|
deleted = mapping_service.delete_mapping(sku, codmat)
|
|
return {"success": deleted}
|
|
except Exception as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
@router.post("/api/mappings/import-csv")
|
|
async def import_csv(file: UploadFile = File(...)):
|
|
content = await file.read()
|
|
text = content.decode("utf-8-sig")
|
|
result = mapping_service.import_csv(text)
|
|
return result
|
|
|
|
@router.get("/api/mappings/export-csv")
|
|
def export_csv():
|
|
csv_content = mapping_service.export_csv()
|
|
return StreamingResponse(
|
|
io.BytesIO(csv_content.encode("utf-8-sig")),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": "attachment; filename=mappings.csv"}
|
|
)
|
|
|
|
@router.get("/api/mappings/csv-template")
|
|
def csv_template():
|
|
content = mapping_service.get_csv_template()
|
|
return StreamingResponse(
|
|
io.BytesIO(content.encode("utf-8-sig")),
|
|
media_type="text/csv",
|
|
headers={"Content-Disposition": "attachment; filename=mappings_template.csv"}
|
|
)
|