Mejoras genéricas traídas: - core/crud_router.py: el PATCH genérico reenvía cualquier header B1S-* recibido del caller en vez de decidirlo el backend. - routers/catalogs.py: nuevo endpoint /api/Catalogs/UdfValidValues (valores válidos de cualquier UDF vía Service Layer). - routers/batch.py: /api/batch/stock (stock de un lote por almacén) y /api/batch/lot-source-warehouse (resuelve contra HANA de qué almacén salió un lote, repartiendo por origen). - routers/inventory_transfer_requests.py + core/inventory_transfer_requests.py: CRUD genérico de Solicitudes de Transferencia de Inventario. - routers/series.py + core/series.py: series activas de un tipo de documento. - routers/user_tables.py: catálogo Code/Name genérico de tablas de usuario (mapeo USER_TABLES vacío, cada cliente completa el suyo). - routers/reports.py / templates/reports/stickers.html: fix de márgenes/alto de página del reporte de etiquetas. - mini-backend/test/ -> mini-backend/tools/ (rename organizativo). Limpieza de datos de Tagumedica que ya estaban en el commit base de main (anteriores a la branch cliente/tagumedica, no relacionados con el backport): - tools/sap-tagumedica/sl.http: eliminado (tenía CompanyDB y password reales). - tools/ApiGateway/Login.yml: CompanyDB/host/password reemplazados por placeholders. - templates/reports/stickers.html: título hardcodeado "TAGUMEDICA" reemplazado por "EMPRESA". - menu_permissions.json: reemplazado por un ejemplo demo (usuarios/módulos ficticios) en vez de emails reales. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
115 lines
4.1 KiB
Python
115 lines
4.1 KiB
Python
import os
|
|
import uuid
|
|
import uvicorn
|
|
import urllib3
|
|
import logging
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import JSONResponse
|
|
from contextlib import asynccontextmanager
|
|
from dotenv import load_dotenv
|
|
from logger_config import setup_logging
|
|
from context import set_request_id, get_request_id, set_current_user
|
|
from core.session import validate_and_refresh_session, get_employee_name, get_emp_id
|
|
|
|
from routers.auth import router as auth_router
|
|
from routers.business_partners import router as bp_router
|
|
from routers.items import router as items_router
|
|
from routers.sales import router as sales_router
|
|
from routers.purchases import router as purchases_router
|
|
from routers.catalogs import router as catalogs_router
|
|
from routers.batch import router as batch_router
|
|
from routers.permissions import router as permissions_router
|
|
from routers.apigateway import router as apigateway_router
|
|
from routers.reports import router as reports_router
|
|
from routers.series import router as series_router
|
|
from routers.user_tables import router as user_tables_router
|
|
from routers.inventory_transfer_requests import router as inventory_transfer_requests_router
|
|
|
|
# ============================================================================
|
|
load_dotenv(dotenv_path=".env")
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
LOG_ENVIRONMENT = os.getenv("LOG_ENVIRONMENT", "production")
|
|
|
|
# ============================================================================
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
setup_logging(environment=LOG_ENVIRONMENT)
|
|
yield
|
|
|
|
|
|
app = FastAPI(
|
|
title="SAP Business One Integration API",
|
|
description="API para integrar Socios de negocios con SAP Business One Service Layer.",
|
|
version="1.1.0",
|
|
lifespan=lifespan
|
|
)
|
|
|
|
app.include_router(auth_router)
|
|
app.include_router(bp_router)
|
|
app.include_router(items_router)
|
|
app.include_router(sales_router)
|
|
app.include_router(purchases_router)
|
|
app.include_router(catalogs_router)
|
|
app.include_router(batch_router)
|
|
app.include_router(permissions_router)
|
|
app.include_router(apigateway_router)
|
|
app.include_router(reports_router)
|
|
app.include_router(series_router)
|
|
app.include_router(user_tables_router)
|
|
app.include_router(inventory_transfer_requests_router)
|
|
|
|
# ============================================================================
|
|
@app.middleware("http")
|
|
async def correlation_id_middleware(request: Request, call_next):
|
|
req_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
|
|
set_request_id(req_id)
|
|
response = await call_next(request)
|
|
response.headers["X-Request-ID"] = get_request_id()
|
|
return response
|
|
|
|
# ============================================================================
|
|
@app.middleware("http")
|
|
async def session_middleware(request: Request, call_next):
|
|
if request.url.path == "/api/Login":
|
|
return await call_next(request)
|
|
|
|
if request.url.path.startswith("/api/"):
|
|
token = request.headers.get("X-Session-Token")
|
|
if not token:
|
|
logger.warning(f"Request sin token: {request.method} {request.url.path}")
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={"detail": "Se requiere autenticación. Inicie sesión primero."}
|
|
)
|
|
|
|
webUser = validate_and_refresh_session(token)
|
|
if not webUser:
|
|
logger.warning(f"Token inválido o expirado en: {request.method} {request.url.path}")
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={"detail": "Sesión expirada. Debe iniciar sesión nuevamente."}
|
|
)
|
|
|
|
set_current_user(webUser, get_employee_name(webUser), get_emp_id(webUser))
|
|
request.state.webUser = webUser
|
|
|
|
return await call_next(request)
|
|
|
|
# ============================================================================
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
urllib3.disable_warnings()
|
|
|
|
# ============================================================================
|
|
if __name__ == "__main__":
|
|
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="debug", access_log=True)
|