3433 lines
106 KiB
Python
3433 lines
106 KiB
Python
from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body, Request
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.openapi.utils import get_openapi
|
|
from fastapi.responses import StreamingResponse, HTMLResponse, Response
|
|
from fastapi.encoders import jsonable_encoder
|
|
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
|
|
|
|
from pydantic import BaseModel, Field, EmailStr
|
|
from typing import List, Optional
|
|
from datetime import datetime, date
|
|
import uvicorn
|
|
import asyncio
|
|
from contextlib import asynccontextmanager
|
|
import uuid
|
|
import csv
|
|
import io
|
|
import logging
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
import os
|
|
from pathlib import Path as FilePath
|
|
from data.data import TAGS_METADATA
|
|
from config.config import settings
|
|
from database import (
|
|
User,
|
|
init_db,
|
|
async_session_factory,
|
|
get_session,
|
|
EmailLog,
|
|
StatutEmail as StatutEmailDB,
|
|
WorkflowLog,
|
|
SignatureLog,
|
|
StatutSignature as StatutSignatureDB,
|
|
)
|
|
from email_queue import email_queue
|
|
from sage_client import sage_client, SageGatewayClient
|
|
|
|
from schemas import (
|
|
SocieteInfo,
|
|
TiersDetails,
|
|
BaremeRemiseResponse,
|
|
Users,
|
|
ClientCreate,
|
|
ClientDetails,
|
|
ClientUpdate,
|
|
FournisseurCreate,
|
|
FournisseurDetails,
|
|
FournisseurUpdate,
|
|
Contact,
|
|
AvoirCreate,
|
|
AvoirUpdate,
|
|
CommandeCreate,
|
|
CommandeUpdate,
|
|
DevisRequest,
|
|
Devis,
|
|
DevisUpdate,
|
|
TypeDocument,
|
|
TypeDocumentSQL,
|
|
StatutEmail,
|
|
EmailEnvoi,
|
|
FactureCreate,
|
|
FactureUpdate,
|
|
LivraisonCreate,
|
|
LivraisonUpdate,
|
|
ArticleCreate,
|
|
Article,
|
|
ArticleUpdate,
|
|
EntreeStock,
|
|
SortieStock,
|
|
MouvementStock,
|
|
RelanceDevis,
|
|
Familles,
|
|
FamilleCreate,
|
|
ContactCreate,
|
|
ContactUpdate,
|
|
)
|
|
from schemas.documents.reglements import ReglementFactureCreate, ReglementMultipleCreate
|
|
from schemas.tiers.commercial import (
|
|
CollaborateurCreate,
|
|
CollaborateurDetails,
|
|
CollaborateurUpdate,
|
|
)
|
|
from utils.normalization import normaliser_type_tiers
|
|
from routes.auth import router as auth_router
|
|
from routes.sage_gateway import router as sage_gateway_router
|
|
from routes.universign import router as universign_router
|
|
from routes.enterprise import router as entreprises_router
|
|
|
|
from services.universign_sync import UniversignSyncService, UniversignSyncScheduler
|
|
|
|
from core.sage_context import (
|
|
get_sage_client_for_user,
|
|
get_gateway_context_for_user,
|
|
GatewayContext,
|
|
)
|
|
from utils.generic_functions import (
|
|
_preparer_lignes_document,
|
|
universign_envoyer,
|
|
)
|
|
|
|
from middleware.security import SwaggerAuthMiddleware, ApiKeyMiddlewareHTTP
|
|
from core.dependencies import get_current_user
|
|
from config.cors_config import setup_cors
|
|
from routes.api_keys import router as api_keys_router
|
|
|
|
if os.path.exists("/app"):
|
|
LOGS_DIR = FilePath("/app/logs")
|
|
else:
|
|
LOGS_DIR = FilePath(__file__).resolve().parent / "logs"
|
|
|
|
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
handlers=[
|
|
logging.FileHandler(LOGS_DIR / "sage_api.log", encoding="utf-8"),
|
|
logging.StreamHandler(),
|
|
],
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Lifecycle de l'application"""
|
|
await init_db()
|
|
logger.info("Base de données initialisée")
|
|
|
|
email_queue.session_factory = async_session_factory
|
|
email_queue.sage_client = sage_client
|
|
logger.info("sage_client injecté dans email_queue")
|
|
|
|
email_queue.start(num_workers=settings.max_email_workers)
|
|
logger.info("Email queue démarrée")
|
|
|
|
sync_service = UniversignSyncService(
|
|
api_url=settings.universign_api_url, api_key=settings.universign_api_key
|
|
)
|
|
sync_service.configure(
|
|
sage_client=sage_client, email_queue=email_queue, settings=settings
|
|
)
|
|
|
|
scheduler = UniversignSyncScheduler(sync_service=sync_service, interval_minutes=5)
|
|
sync_task = asyncio.create_task(scheduler.start(async_session_factory))
|
|
logger.info("Synchronisation Universign démarrée (5min)")
|
|
|
|
yield
|
|
|
|
scheduler.stop()
|
|
sync_task.cancel()
|
|
email_queue.stop()
|
|
logger.info("Services arrêtés")
|
|
|
|
|
|
app = FastAPI(
|
|
title="Sage Gateways API",
|
|
version="3.0.0",
|
|
description="API multi-tenant pour Sage 100c avec authentification hybride",
|
|
lifespan=lifespan,
|
|
openapi_tags=TAGS_METADATA,
|
|
docs_url=None,
|
|
redoc_url=None,
|
|
openapi_url=None,
|
|
)
|
|
|
|
|
|
def get_swagger_user_from_state(request: Request) -> Optional[dict]:
|
|
return getattr(request.state, "swagger_user", None)
|
|
|
|
|
|
def generate_filtered_openapi_schema(
|
|
app: FastAPI, allowed_tags: Optional[List[str]] = None
|
|
) -> dict:
|
|
base_schema = get_openapi(
|
|
title=app.title,
|
|
version=app.version,
|
|
description=app.description,
|
|
routes=app.routes,
|
|
)
|
|
|
|
base_schema["components"]["securitySchemes"] = {
|
|
"HTTPBearer": {
|
|
"type": "http",
|
|
"scheme": "bearer",
|
|
"bearerFormat": "JWT",
|
|
"description": "Authentification JWT pour utilisateurs (POST /auth/login)",
|
|
},
|
|
"ApiKeyAuth": {
|
|
"type": "apiKey",
|
|
"in": "header",
|
|
"name": "X-API-Key",
|
|
"description": "Clé API pour intégrations externes (format: sdk_live_xxx)",
|
|
},
|
|
}
|
|
|
|
base_schema["security"] = [{"HTTPBearer": []}, {"ApiKeyAuth": []}]
|
|
|
|
if not allowed_tags:
|
|
logger.info("📚 Schéma OpenAPI complet (admin)")
|
|
return base_schema
|
|
|
|
filtered_paths = {}
|
|
|
|
for path, path_item in base_schema.get("paths", {}).items():
|
|
filtered_operations = {}
|
|
|
|
for method, operation in path_item.items():
|
|
if method not in [
|
|
"get",
|
|
"post",
|
|
"put",
|
|
"delete",
|
|
"patch",
|
|
"options",
|
|
"head",
|
|
]:
|
|
continue
|
|
|
|
operation_tags = operation.get("tags", [])
|
|
|
|
if any(tag in allowed_tags for tag in operation_tags):
|
|
filtered_operations[method] = operation
|
|
|
|
if filtered_operations:
|
|
filtered_paths[path] = filtered_operations
|
|
|
|
base_schema["paths"] = filtered_paths
|
|
|
|
if "tags" in base_schema:
|
|
base_schema["tags"] = [
|
|
tag_obj
|
|
for tag_obj in base_schema["tags"]
|
|
if tag_obj.get("name") in allowed_tags
|
|
]
|
|
|
|
logger.info(f"🔒 Schéma filtré: {len(filtered_paths)} paths, tags: {allowed_tags}")
|
|
|
|
return base_schema
|
|
|
|
|
|
@app.get("/openapi.json", include_in_schema=False)
|
|
async def custom_openapi_endpoint(request: Request):
|
|
swagger_user = get_swagger_user_from_state(request)
|
|
|
|
if not swagger_user:
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={"detail": "Authentification Swagger requise"},
|
|
headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'},
|
|
)
|
|
|
|
username = swagger_user.get("username", "unknown")
|
|
allowed_tags = swagger_user.get("allowed_tags")
|
|
|
|
logger.info(f"📖 OpenAPI demandé par: {username}, tags: {allowed_tags or 'ALL'}")
|
|
|
|
schema = generate_filtered_openapi_schema(app, allowed_tags)
|
|
|
|
return JSONResponse(content=schema)
|
|
|
|
|
|
@app.get("/docs", include_in_schema=False)
|
|
async def custom_swagger_ui(request: Request):
|
|
swagger_user = get_swagger_user_from_state(request)
|
|
|
|
if not swagger_user:
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={"detail": "Authentification Swagger requise"},
|
|
headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'},
|
|
)
|
|
|
|
return get_swagger_ui_html(
|
|
openapi_url="/openapi.json",
|
|
title=f"{app.title} - Documentation",
|
|
swagger_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
|
|
swagger_ui_parameters={
|
|
"persistAuthorization": True,
|
|
"displayRequestDuration": True,
|
|
"filter": True,
|
|
"tryItOutEnabled": True,
|
|
},
|
|
)
|
|
|
|
|
|
@app.get("/redoc", include_in_schema=False)
|
|
async def custom_redoc(request: Request):
|
|
swagger_user = get_swagger_user_from_state(request)
|
|
|
|
if not swagger_user:
|
|
return JSONResponse(
|
|
status_code=401,
|
|
content={"detail": "Authentification Swagger requise"},
|
|
headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'},
|
|
)
|
|
|
|
return get_redoc_html(
|
|
openapi_url="/openapi.json",
|
|
title=f"{app.title} - Documentation",
|
|
redoc_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
|
|
)
|
|
|
|
|
|
setup_cors(app, mode="open")
|
|
|
|
app.add_middleware(SwaggerAuthMiddleware)
|
|
|
|
app.add_middleware(ApiKeyMiddlewareHTTP)
|
|
|
|
|
|
app.include_router(api_keys_router)
|
|
app.include_router(auth_router)
|
|
app.include_router(sage_gateway_router)
|
|
app.include_router(universign_router)
|
|
app.include_router(entreprises_router)
|
|
|
|
|
|
@app.get("/clients", response_model=List[ClientDetails], tags=["Clients"])
|
|
async def obtenir_clients(
|
|
query: Optional[str] = Query(None),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
clients = sage.lister_clients(filtre=query or "")
|
|
return [ClientDetails(**c) for c in clients]
|
|
except Exception as e:
|
|
logger.error(f"Erreur recherche clients: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/clients/{code}", response_model=ClientDetails, tags=["Clients"])
|
|
async def lire_client_detail(
|
|
code: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
client = sage.lire_client(code)
|
|
|
|
if not client:
|
|
raise HTTPException(404, f"Client {code} introuvable")
|
|
|
|
return ClientDetails(**client)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture client {code}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.put("/clients/{code}", tags=["Clients"])
|
|
async def modifier_client(
|
|
code: str,
|
|
client_update: ClientUpdate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.modifier_client(code, client_update.dict(exclude_none=True))
|
|
|
|
logger.info(f"Client {code} modifié avec succès")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Client {code} modifié avec succès",
|
|
"client": resultat,
|
|
}
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Erreur métier modification client {code}: {e}")
|
|
raise HTTPException(404, str(e))
|
|
except Exception as e:
|
|
logger.error(f"Erreur technique modification client {code}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/clients", status_code=201, tags=["Clients"])
|
|
async def ajouter_client(
|
|
client: ClientCreate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
nouveau_client = sage.creer_client(client.model_dump(mode="json"))
|
|
|
|
logger.info(f"Client créé via API: {nouveau_client.get('numero')}")
|
|
|
|
return jsonable_encoder(
|
|
{
|
|
"success": True,
|
|
"message": "Client créé avec succès",
|
|
"data": nouveau_client,
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la création du client: {e}")
|
|
status = 400 if "existe déjà" in str(e) else 500
|
|
raise HTTPException(status, str(e))
|
|
|
|
|
|
@app.get("/articles", response_model=List[Article], tags=["Articles"])
|
|
async def rechercher_articles(
|
|
query: Optional[str] = Query(None),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
articles = sage.lister_articles(filtre=query or "")
|
|
return [Article(**a) for a in articles]
|
|
except Exception as e:
|
|
logger.error(f"Erreur recherche articles: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post(
|
|
"/articles",
|
|
response_model=Article,
|
|
status_code=status.HTTP_201_CREATED,
|
|
tags=["Articles"],
|
|
)
|
|
async def creer_article(
|
|
article: ArticleCreate,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
if not article.reference or not article.designation:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Les champs 'reference' et 'designation' sont obligatoires",
|
|
)
|
|
|
|
article_data = article.dict(exclude_unset=True)
|
|
|
|
logger.info(f"Création article: {article.reference} - {article.designation}")
|
|
|
|
resultat = sage.creer_article(article_data)
|
|
|
|
logger.info(
|
|
f"Article créé: {resultat.get('reference')} (stock: {resultat.get('stock_reel', 0)})"
|
|
)
|
|
|
|
return Article(**resultat)
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Erreur métier création article: {e}")
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
|
|
except HTTPException:
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur technique création article: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Erreur lors de la création de l'article: {str(e)}",
|
|
)
|
|
|
|
|
|
@app.put("/articles/{reference}", response_model=Article, tags=["Articles"])
|
|
async def modifier_article(
|
|
reference: str = Path(..., description="Référence de l'article à modifier"),
|
|
article: ArticleUpdate = Body(...),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
article_data = article.dict(exclude_unset=True)
|
|
|
|
if not article_data:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Aucun champ à modifier. Fournissez au moins un champ à mettre à jour.",
|
|
)
|
|
|
|
logger.info(f"Modification article {reference}: {list(article_data.keys())}")
|
|
|
|
resultat = sage.modifier_article(reference, article_data)
|
|
|
|
if "stock_reel" in article_data:
|
|
logger.info(
|
|
f"Stock {reference} modifié: {article_data['stock_reel']} "
|
|
f"(peut résoudre erreur 2881)"
|
|
)
|
|
|
|
logger.info(f"Article {reference} modifié ({len(article_data)} champs)")
|
|
|
|
return Article(**resultat)
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Erreur métier modification article: {e}")
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
|
|
except HTTPException:
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur technique modification article: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Erreur lors de la modification de l'article: {str(e)}",
|
|
)
|
|
|
|
|
|
@app.get("/articles/{reference}", response_model=Article, tags=["Articles"])
|
|
async def lire_article(
|
|
reference: str = Path(..., description="Référence de l'article"),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
article = sage.lire_article(reference)
|
|
|
|
if not article:
|
|
logger.warning(f"Article {reference} introuvable")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Article {reference} introuvable",
|
|
)
|
|
|
|
logger.info(f"Article {reference} lu: {article.get('designation', '')}")
|
|
|
|
return Article(**article)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture article {reference}: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Erreur lors de la lecture de l'article: {str(e)}",
|
|
)
|
|
|
|
|
|
@app.post("/devis", response_model=Devis, status_code=201, tags=["Devis"])
|
|
async def creer_devis(
|
|
devis: DevisRequest,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
devis_data = {
|
|
"client_id": devis.client_id,
|
|
"date_devis": devis.date_devis.isoformat() if devis.date_devis else None,
|
|
"date_livraison": (
|
|
devis.date_livraison.isoformat() if devis.date_livraison else None
|
|
),
|
|
"reference": devis.reference,
|
|
"lignes": _preparer_lignes_document(devis.lignes),
|
|
}
|
|
|
|
resultat = sage.creer_devis(devis_data)
|
|
|
|
logger.info(
|
|
f"Devis créé: {resultat.get('numero_devis')} "
|
|
f"({resultat.get('total_ttc')}€ TTC)"
|
|
)
|
|
|
|
return Devis(
|
|
id=resultat["numero_devis"],
|
|
client_id=devis.client_id,
|
|
date_devis=resultat["date_devis"],
|
|
montant_total_ht=resultat["total_ht"],
|
|
montant_total_ttc=resultat["total_ttc"],
|
|
nb_lignes=resultat["nb_lignes"],
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur création devis: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.put("/devis/{id}", tags=["Devis"])
|
|
async def modifier_devis(
|
|
id: str,
|
|
devis_update: DevisUpdate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
update_data = {}
|
|
|
|
if devis_update.date_devis:
|
|
update_data["date_devis"] = devis_update.date_devis.isoformat()
|
|
|
|
if devis_update.lignes is not None:
|
|
update_data["lignes"] = [
|
|
{
|
|
"article_code": ligne.article_code,
|
|
"quantite": ligne.quantite,
|
|
"remise_pourcentage": ligne.remise_pourcentage,
|
|
}
|
|
for ligne in devis_update.lignes
|
|
]
|
|
|
|
if devis_update.statut is not None:
|
|
update_data["statut"] = devis_update.statut
|
|
|
|
if devis_update.reference is not None:
|
|
update_data["reference"] = devis_update.reference
|
|
|
|
resultat = sage.modifier_devis(id, update_data)
|
|
|
|
logger.info(f"Devis {id} modifié avec succès")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Devis {id} modifié avec succès",
|
|
"devis": resultat,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur modification devis {id}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/commandes", status_code=201, tags=["Commandes"])
|
|
async def creer_commande(
|
|
commande: CommandeCreate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
commande_data = {
|
|
"client_id": commande.client_id,
|
|
"date_commande": (
|
|
commande.date_commande.isoformat() if commande.date_commande else None
|
|
),
|
|
"date_livraison": (
|
|
commande.date_livraison.isoformat() if commande.date_livraison else None
|
|
),
|
|
"reference": commande.reference,
|
|
"lignes": _preparer_lignes_document(commande.lignes),
|
|
}
|
|
|
|
resultat = sage.creer_commande(commande_data)
|
|
|
|
logger.info(
|
|
f"Commande créée: {resultat.get('numero_commande')} "
|
|
f"({resultat.get('total_ttc')}€ TTC)"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Commande créée avec succès",
|
|
"data": {
|
|
"numero_commande": resultat["numero_commande"],
|
|
"client_id": commande.client_id,
|
|
"date_commande": resultat["date_commande"],
|
|
"total_ht": resultat["total_ht"],
|
|
"total_ttc": resultat["total_ttc"],
|
|
"nb_lignes": resultat["nb_lignes"],
|
|
"reference": resultat.get("reference"),
|
|
"date_livraison": resultat.get("date_livraison"),
|
|
},
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur création commande: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.put("/commandes/{id}", tags=["Commandes"])
|
|
async def modifier_commande(
|
|
id: str,
|
|
commande_update: CommandeUpdate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
update_data = {}
|
|
|
|
if commande_update.date_commande:
|
|
update_data["date_commande"] = commande_update.date_commande.isoformat()
|
|
|
|
if commande_update.lignes is not None:
|
|
update_data["lignes"] = [
|
|
{
|
|
"article_code": ligne.article_code,
|
|
"quantite": ligne.quantite,
|
|
"remise_pourcentage": ligne.remise_pourcentage,
|
|
}
|
|
for ligne in commande_update.lignes
|
|
]
|
|
|
|
if commande_update.statut is not None:
|
|
update_data["statut"] = commande_update.statut
|
|
|
|
if commande_update.reference is not None:
|
|
update_data["reference"] = commande_update.reference
|
|
|
|
resultat = sage.modifier_commande(id, update_data)
|
|
|
|
logger.info(f"Commande {id} modifiée avec succès")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Commande {id} modifiée avec succès",
|
|
"commande": resultat,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur modification commande {id}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/devis", tags=["Devis"])
|
|
async def lister_devis(
|
|
limit: int = Query(100, le=1000),
|
|
statut: Optional[int] = Query(None),
|
|
inclure_lignes: bool = Query(
|
|
True, description="Inclure les lignes de chaque devis"
|
|
),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
devis_list = sage.lister_devis(
|
|
limit=limit, statut=statut, inclure_lignes=inclure_lignes
|
|
)
|
|
return devis_list
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur liste devis: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/devis/{id}", tags=["Devis"])
|
|
async def lire_devis(
|
|
id: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
devis = sage.lire_devis(id)
|
|
|
|
if not devis:
|
|
raise HTTPException(404, f"Devis {id} introuvable")
|
|
|
|
return {"success": True, "data": devis}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture devis: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/devis/{id}/pdf", tags=["Devis"])
|
|
async def telecharger_devis_pdf(
|
|
id: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
pdf_bytes = email_queue._generate_pdf(id, TypeDocument.DEVIS)
|
|
|
|
return StreamingResponse(
|
|
iter([pdf_bytes]),
|
|
media_type="application/pdf",
|
|
headers={"Content-Disposition": f"attachment; filename=Devis_{id}.pdf"},
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Erreur génération PDF: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/documents/{type_doc}/{numero}/pdf", tags=["Documents"])
|
|
async def telecharger_document_pdf(
|
|
type_doc: int = Path(
|
|
...,
|
|
description="Type de document (0=Devis, 10=Commande, 30=Livraison, 60=Facture, 50=Avoir)",
|
|
),
|
|
numero: str = Path(..., description="Numéro du document"),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
types_labels = {
|
|
0: "Devis",
|
|
10: "Commande",
|
|
20: "Preparation",
|
|
30: "BonLivraison",
|
|
40: "BonRetour",
|
|
50: "Avoir",
|
|
60: "Facture",
|
|
}
|
|
|
|
if type_doc not in types_labels:
|
|
raise HTTPException(
|
|
400,
|
|
f"Type de document invalide: {type_doc}. "
|
|
f"Types valides: {list(types_labels.keys())}",
|
|
)
|
|
|
|
label = types_labels[type_doc]
|
|
|
|
logger.info(f"Génération PDF: {label} {numero} (type={type_doc})")
|
|
|
|
pdf_bytes = sage.generer_pdf_document(numero, type_doc)
|
|
|
|
if not pdf_bytes:
|
|
raise HTTPException(500, f"Le PDF du document {numero} est vide")
|
|
|
|
logger.info(f"PDF généré: {len(pdf_bytes)} octets")
|
|
|
|
filename = f"{label}_{numero}.pdf"
|
|
|
|
return StreamingResponse(
|
|
iter([pdf_bytes]),
|
|
media_type="application/pdf",
|
|
headers={
|
|
"Content-Disposition": f"attachment; filename={filename}",
|
|
"Content-Length": str(len(pdf_bytes)),
|
|
},
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Erreur génération PDF {numero} (type {type_doc}): {e}", exc_info=True
|
|
)
|
|
raise HTTPException(500, f"Erreur génération PDF: {str(e)}")
|
|
|
|
|
|
@app.post("/devis/{id}/envoyer", tags=["Devis"])
|
|
async def envoyer_devis_email(
|
|
id: str,
|
|
request: EmailEnvoi,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
tous_destinataires = [request.destinataire] + request.cc + request.cci
|
|
email_logs = []
|
|
|
|
for dest in tous_destinataires:
|
|
email_log = EmailLog(
|
|
id=str(uuid.uuid4()),
|
|
destinataire=dest,
|
|
sujet=request.sujet,
|
|
corps_html=request.corps_html,
|
|
document_ids=id,
|
|
type_document=TypeDocument.DEVIS,
|
|
statut=StatutEmailDB.EN_ATTENTE,
|
|
date_creation=datetime.now(),
|
|
nb_tentatives=0,
|
|
)
|
|
|
|
session.add(email_log)
|
|
await session.flush()
|
|
|
|
email_queue.enqueue(email_log.id)
|
|
email_logs.append(email_log.id)
|
|
|
|
await session.commit()
|
|
|
|
logger.info(
|
|
f"Email devis {id} mis en file: {len(tous_destinataires)} destinataire(s)"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"email_log_ids": email_logs,
|
|
"devis_id": id,
|
|
"message": f"{len(tous_destinataires)} email(s) en file d'attente",
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur envoi email: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.put("/document/{type_doc}/{numero}/statut", tags=["Documents"])
|
|
async def changer_statut_document(
|
|
type_doc: int = Path(
|
|
...,
|
|
description="Type de document (0=Devis, 10=Commande, 30=Livraison, 60=Facture, 50=Avoir)",
|
|
),
|
|
numero: str = Path(..., description="Numéro du document"),
|
|
nouveau_statut: int = Query(
|
|
..., ge=0, le=6, description="0=Saisi, 1=Confirmé, 2=Accepté"
|
|
),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
document_type_sql = None
|
|
document_type_code = None
|
|
type_doc_normalized = None
|
|
|
|
try:
|
|
match type_doc:
|
|
case 0:
|
|
document_type_sql = TypeDocumentSQL.DEVIS
|
|
document_type_code = TypeDocument.DEVIS
|
|
type_doc_normalized = 0
|
|
case 10 | 1:
|
|
document_type_sql = TypeDocumentSQL.BON_COMMANDE
|
|
document_type_code = TypeDocument.BON_COMMANDE
|
|
type_doc_normalized = 10
|
|
case 20 | 2:
|
|
document_type_sql = TypeDocumentSQL.PREPARATION
|
|
document_type_code = TypeDocument.PREPARATION
|
|
type_doc_normalized = 20
|
|
case 30 | 3:
|
|
document_type_sql = TypeDocumentSQL.BON_LIVRAISON
|
|
document_type_code = TypeDocument.BON_LIVRAISON
|
|
type_doc_normalized = 30
|
|
case 40 | 4:
|
|
document_type_sql = TypeDocumentSQL.BON_RETOUR
|
|
document_type_code = TypeDocument.BON_RETOUR
|
|
type_doc_normalized = 40
|
|
case 50 | 5:
|
|
document_type_sql = TypeDocumentSQL.BON_AVOIR
|
|
document_type_code = TypeDocument.BON_AVOIR
|
|
type_doc_normalized = 50
|
|
case 60 | 6:
|
|
document_type_sql = TypeDocumentSQL.FACTURE
|
|
document_type_code = TypeDocument.FACTURE
|
|
type_doc_normalized = 60
|
|
case _:
|
|
raise HTTPException(
|
|
400,
|
|
f"Type de document invalide: {type_doc}",
|
|
)
|
|
|
|
document_existant = sage.lire_document(numero, document_type_sql)
|
|
if not document_existant:
|
|
raise HTTPException(404, f"Document {numero} introuvable")
|
|
|
|
statut_actuel = document_existant.get("statut", 0)
|
|
|
|
match type_doc:
|
|
case 0:
|
|
if statut_actuel >= 2:
|
|
statuts_devis = {2: "accepté", 3: "perdu", 4: "archivé"}
|
|
raise HTTPException(
|
|
400,
|
|
f"Le devis {numero} est {statuts_devis.get(statut_actuel, 'verrouillé')} "
|
|
f"et ne peut plus changer de statut",
|
|
)
|
|
|
|
case 10 | 1 | 20 | 2 | 30 | 3 | 40 | 4 | 50 | 5 | 60 | 6:
|
|
if statut_actuel >= 2:
|
|
type_names = {
|
|
10: "la commande",
|
|
1: "la commande",
|
|
20: "la préparation",
|
|
2: "la préparation",
|
|
30: "la livraison",
|
|
3: "la livraison",
|
|
40: "le retour",
|
|
4: "le retour",
|
|
50: "l'avoir",
|
|
5: "l'avoir",
|
|
60: "la facture",
|
|
6: "la facture",
|
|
}
|
|
raise HTTPException(
|
|
400,
|
|
f"Le document {numero} ({type_names.get(type_doc, 'document')}) "
|
|
f"ne peut plus changer de statut (statut actuel ≥ 2)",
|
|
)
|
|
|
|
document_type_int = (
|
|
document_type_code.value
|
|
if hasattr(document_type_code, "value")
|
|
else type_doc_normalized
|
|
)
|
|
|
|
resultat = sage.changer_statut_document(
|
|
document_type_code=document_type_int,
|
|
numero=numero,
|
|
nouveau_statut=nouveau_statut,
|
|
)
|
|
|
|
logger.info(
|
|
f"Statut document {numero} changé: {statut_actuel} → {nouveau_statut}"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"document_id": numero,
|
|
"type_document_code": document_type_int,
|
|
"type_document_sql": str(document_type_sql.value),
|
|
"statut_ancien": resultat.get("statut_ancien", statut_actuel),
|
|
"statut_nouveau": resultat.get("statut_nouveau", nouveau_statut),
|
|
"message": f"Statut mis à jour: {statut_actuel} → {nouveau_statut}",
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur changement statut document {numero}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/commandes/{id}", tags=["Commandes"])
|
|
async def lire_commande(
|
|
id: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
commande = sage.lire_document(id, TypeDocumentSQL.BON_COMMANDE)
|
|
if not commande:
|
|
raise HTTPException(404, f"Commande {id} introuvable")
|
|
return commande
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture commande: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/commandes", tags=["Commandes"])
|
|
async def lister_commandes(
|
|
limit: int = Query(100, le=1000),
|
|
statut: Optional[int] = Query(None),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
commandes = sage.lister_commandes(limit=limit, statut=statut)
|
|
return commandes
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur liste commandes: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/workflow/devis/{id}/to-commande", tags=["Workflows"])
|
|
async def devis_vers_commande(
|
|
id: str,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.transformer_document(
|
|
numero_source=id,
|
|
type_source=settings.SAGE_TYPE_DEVIS, # = 0
|
|
type_cible=settings.SAGE_TYPE_BON_COMMANDE, # = 10
|
|
)
|
|
|
|
workflow_log = WorkflowLog(
|
|
id=str(uuid.uuid4()),
|
|
document_source=id,
|
|
type_source=TypeDocument.DEVIS,
|
|
document_cible=resultat.get("document_cible", ""),
|
|
type_cible=TypeDocument.BON_COMMANDE,
|
|
nb_lignes=resultat.get("nb_lignes", 0),
|
|
date_transformation=datetime.now(),
|
|
succes=True,
|
|
)
|
|
|
|
session.add(workflow_log)
|
|
await session.commit()
|
|
|
|
logger.info(
|
|
f"Transformation: Devis {id} → Commande {resultat['document_cible']}"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"document_source": id,
|
|
"document_cible": resultat["document_cible"],
|
|
"nb_lignes": resultat["nb_lignes"],
|
|
"statut_devis_mis_a_jour": True,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur transformation: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/workflow/commande/{id}/to-facture", tags=["Workflows"])
|
|
async def commande_vers_facture(
|
|
id: str,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.transformer_document(
|
|
numero_source=id,
|
|
type_source=settings.SAGE_TYPE_BON_COMMANDE, # = 10
|
|
type_cible=settings.SAGE_TYPE_FACTURE, # = 60
|
|
)
|
|
|
|
workflow_log = WorkflowLog(
|
|
id=str(uuid.uuid4()),
|
|
document_source=id,
|
|
type_source=TypeDocument.BON_COMMANDE,
|
|
document_cible=resultat.get("document_cible", ""),
|
|
type_cible=TypeDocument.FACTURE,
|
|
nb_lignes=resultat.get("nb_lignes", 0),
|
|
date_transformation=datetime.now(),
|
|
succes=True,
|
|
)
|
|
|
|
session.add(workflow_log)
|
|
await session.commit()
|
|
|
|
logger.info(
|
|
f"Transformation: Commande {id} → Facture {resultat['document_cible']}"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"document_source": id,
|
|
"document_cible": resultat["document_cible"],
|
|
"nb_lignes": resultat["nb_lignes"],
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur transformation: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
class EmailBatch(BaseModel):
|
|
destinataires: List[EmailStr] = Field(..., min_length=1, max_length=100)
|
|
sujet: str = Field(..., min_length=1, max_length=500)
|
|
corps_html: str = Field(..., min_length=1)
|
|
document_ids: Optional[List[str]] = None
|
|
type_document: Optional[TypeDocument] = None
|
|
|
|
|
|
@app.post("/emails/send-batch", tags=["Emails"])
|
|
async def envoyer_emails_lot(
|
|
batch: EmailBatch, session: AsyncSession = Depends(get_session)
|
|
):
|
|
resultats = []
|
|
|
|
for destinataire in batch.destinataires:
|
|
email_log = EmailLog(
|
|
id=str(uuid.uuid4()),
|
|
destinataire=destinataire,
|
|
sujet=batch.sujet,
|
|
corps_html=batch.corps_html,
|
|
document_ids=",".join(batch.document_ids) if batch.document_ids else None,
|
|
type_document=batch.type_document,
|
|
statut=StatutEmailDB.EN_ATTENTE,
|
|
date_creation=datetime.now(),
|
|
nb_tentatives=0,
|
|
)
|
|
|
|
session.add(email_log)
|
|
await session.flush()
|
|
|
|
email_queue.enqueue(email_log.id)
|
|
|
|
resultats.append(
|
|
{
|
|
"destinataire": destinataire,
|
|
"log_id": email_log.id,
|
|
"statut": "EN_ATTENTE",
|
|
}
|
|
)
|
|
|
|
await session.commit()
|
|
|
|
nb_documents = len(batch.document_ids) if batch.document_ids else 0
|
|
|
|
logger.info(
|
|
f"{len(batch.destinataires)} emails mis en file avec {nb_documents} docs"
|
|
)
|
|
|
|
return {
|
|
"total": len(batch.destinataires),
|
|
"succes": len(batch.destinataires),
|
|
"documents_attaches": nb_documents,
|
|
"details": resultats,
|
|
}
|
|
|
|
|
|
@app.post(
|
|
"/devis/valider-remise", response_model=BaremeRemiseResponse, tags=["Validation"]
|
|
)
|
|
async def valider_remise(
|
|
client_id: str = Query(..., min_length=1),
|
|
remise_pourcentage: float = Query(0.0, ge=0, le=100),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
remise_max = sage.lire_remise_max_client(client_id)
|
|
|
|
autorisee = remise_pourcentage <= remise_max
|
|
|
|
if not autorisee:
|
|
message = f"Remise trop élevée (max autorisé: {remise_max}%)"
|
|
logger.warning(
|
|
f"Remise refusée pour {client_id}: {remise_pourcentage}% > {remise_max}%"
|
|
)
|
|
else:
|
|
message = "Remise autorisée"
|
|
|
|
return BaremeRemiseResponse(
|
|
client_id=client_id,
|
|
remise_max_autorisee=remise_max,
|
|
remise_demandee=remise_pourcentage,
|
|
autorisee=autorisee,
|
|
message=message,
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur validation remise: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/devis/{id}/relancer-signature", tags=["Devis"])
|
|
async def relancer_devis_signature(
|
|
id: str,
|
|
relance: RelanceDevis,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
devis = sage.lire_devis(id)
|
|
if not devis:
|
|
raise HTTPException(404, f"Devis {id} introuvable")
|
|
|
|
contact = sage.lire_contact_client(devis["client_code"])
|
|
if not contact or not contact.get("email"):
|
|
raise HTTPException(400, "Aucun email trouvé pour ce client")
|
|
|
|
pdf_bytes = email_queue._generate_pdf(id, TypeDocument.DEVIS)
|
|
|
|
resultat = await universign_envoyer(
|
|
id,
|
|
pdf_bytes,
|
|
contact["email"],
|
|
contact["nom"] or contact["client_intitule"],
|
|
)
|
|
|
|
if "error" in resultat:
|
|
raise HTTPException(500, resultat["error"])
|
|
|
|
signature_log = SignatureLog(
|
|
id=str(uuid.uuid4()),
|
|
document_id=id,
|
|
type_document=TypeDocument.DEVIS,
|
|
transaction_id=resultat["transaction_id"],
|
|
signer_url=resultat["signer_url"],
|
|
email_signataire=contact["email"],
|
|
nom_signataire=contact["nom"] or contact["client_intitule"],
|
|
statut=StatutSignatureDB.ENVOYE,
|
|
date_envoi=datetime.now(),
|
|
est_relance=True,
|
|
nb_relances=1,
|
|
)
|
|
|
|
session.add(signature_log)
|
|
await session.commit()
|
|
|
|
return {
|
|
"success": True,
|
|
"devis_id": id,
|
|
"transaction_id": resultat["transaction_id"],
|
|
"message": "Relance signature envoyée",
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur relance: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
class ContactClientResponse(BaseModel):
|
|
client_code: str
|
|
client_intitule: str
|
|
email: Optional[str]
|
|
nom: Optional[str]
|
|
telephone: Optional[str]
|
|
peut_etre_relance: bool
|
|
|
|
|
|
@app.get("/devis/{id}/contact", response_model=ContactClientResponse, tags=["Devis"])
|
|
async def recuperer_contact_devis(
|
|
id: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
devis = sage.lire_devis(id)
|
|
if not devis:
|
|
raise HTTPException(404, f"Devis {id} introuvable")
|
|
|
|
contact = sage.lire_contact_client(devis["client_code"])
|
|
if not contact:
|
|
raise HTTPException(
|
|
404, f"Contact introuvable pour client {devis['client_code']}"
|
|
)
|
|
|
|
peut_relancer = bool(contact.get("email"))
|
|
|
|
return ContactClientResponse(**contact, peut_etre_relance=peut_relancer)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur récupération contact: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/factures", tags=["Factures"])
|
|
async def lister_factures(
|
|
limit: int = Query(100, le=1000),
|
|
statut: Optional[int] = Query(None),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
factures = sage.lister_factures(limit=limit, statut=statut)
|
|
return factures
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur liste factures: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/factures/{numero}", tags=["Factures"])
|
|
async def lire_facture_detail(
|
|
numero: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
facture = sage.lire_document(numero, TypeDocumentSQL.FACTURE)
|
|
|
|
if not facture:
|
|
raise HTTPException(404, f"Facture {numero} introuvable")
|
|
|
|
return {"success": True, "data": facture}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture facture {numero}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
class RelanceFacture(BaseModel):
|
|
doc_id: str
|
|
message_personnalise: Optional[str] = None
|
|
|
|
|
|
@app.post("/factures", status_code=201, tags=["Factures"])
|
|
async def creer_facture(
|
|
facture: FactureCreate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
facture_data = {
|
|
"client_id": facture.client_id,
|
|
"date_facture": (
|
|
facture.date_facture.isoformat() if facture.date_facture else None
|
|
),
|
|
"date_livraison": (
|
|
facture.date_livraison.isoformat() if facture.date_livraison else None
|
|
),
|
|
"reference": facture.reference,
|
|
"lignes": _preparer_lignes_document(facture.lignes),
|
|
}
|
|
|
|
resultat = sage.creer_facture(facture_data)
|
|
|
|
logger.info(
|
|
f"Facture créée: {resultat.get('numero_facture')} "
|
|
f"({resultat.get('total_ttc')}€ TTC)"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Facture créée avec succès",
|
|
"data": {
|
|
"numero_facture": resultat["numero_facture"],
|
|
"client_id": facture.client_id,
|
|
"date_facture": resultat["date_facture"],
|
|
"total_ht": resultat["total_ht"],
|
|
"total_ttc": resultat["total_ttc"],
|
|
"nb_lignes": resultat["nb_lignes"],
|
|
"reference": resultat.get("reference"),
|
|
"date_livraison": resultat.get("date_livraison"),
|
|
},
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur création facture: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.put("/factures/{id}", tags=["Factures"])
|
|
async def modifier_facture(
|
|
id: str,
|
|
facture_update: FactureUpdate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
update_data = {}
|
|
|
|
if facture_update.date_facture:
|
|
update_data["date_facture"] = facture_update.date_facture.isoformat()
|
|
|
|
if facture_update.lignes is not None:
|
|
update_data["lignes"] = [
|
|
{
|
|
"article_code": ligne.article_code,
|
|
"quantite": ligne.quantite,
|
|
"remise_pourcentage": ligne.remise_pourcentage,
|
|
}
|
|
for ligne in facture_update.lignes
|
|
]
|
|
|
|
if facture_update.statut is not None:
|
|
update_data["statut"] = facture_update.statut
|
|
|
|
if facture_update.reference is not None:
|
|
update_data["reference"] = facture_update.reference
|
|
|
|
resultat = sage.modifier_facture(id, update_data)
|
|
|
|
logger.info(f"Facture {id} modifiée avec succès")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Facture {id} modifiée avec succès",
|
|
"facture": resultat,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur modification facture {id}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
templates_email_db = {
|
|
"relance_facture": {
|
|
"id": "relance_facture",
|
|
"nom": "Relance Facture",
|
|
"sujet": "Rappel - Facture {{DO_Piece}}",
|
|
"corps_html": """
|
|
<p>Bonjour {{CT_Intitule}},</p>
|
|
<p>La facture <strong>{{DO_Piece}}</strong> du {{DO_Date}}
|
|
d'un montant de <strong>{{DO_TotalTTC}}€ TTC</strong> reste impayée.</p>
|
|
<p>Merci de régulariser dans les meilleurs délais.</p>
|
|
<p>Cordialement,</p>
|
|
""",
|
|
"variables_disponibles": [
|
|
"DO_Piece",
|
|
"DO_Date",
|
|
"CT_Intitule",
|
|
"DO_TotalHT",
|
|
"DO_TotalTTC",
|
|
],
|
|
}
|
|
}
|
|
|
|
|
|
@app.post("/factures/{id}/relancer", tags=["Factures"])
|
|
async def relancer_facture(
|
|
id: str,
|
|
relance: RelanceFacture,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
facture = sage.lire_document(id, TypeDocumentSQL.FACTURE)
|
|
if not facture:
|
|
raise HTTPException(404, f"Facture {id} introuvable")
|
|
|
|
contact = sage.lire_contact_client(facture["client_code"])
|
|
if not contact or not contact.get("email"):
|
|
raise HTTPException(400, "Aucun email trouvé pour ce client")
|
|
|
|
template = templates_email_db["relance_facture"]
|
|
|
|
variables = {
|
|
"DO_Piece": facture.get("numero", id),
|
|
"DO_Date": str(facture.get("date", "")),
|
|
"CT_Intitule": facture.get("client_intitule", ""),
|
|
"DO_TotalHT": f"{facture.get('total_ht', 0):.2f}",
|
|
"DO_TotalTTC": f"{facture.get('total_ttc', 0):.2f}",
|
|
}
|
|
|
|
sujet = template["sujet"]
|
|
corps = relance.message_personnalise or template["corps_html"]
|
|
|
|
for var, valeur in variables.items():
|
|
sujet = sujet.replace(f"{{{{{var}}}}}", valeur)
|
|
corps = corps.replace(f"{{{{{var}}}}}", valeur)
|
|
|
|
email_log = EmailLog(
|
|
id=str(uuid.uuid4()),
|
|
destinataire=contact["email"],
|
|
sujet=sujet,
|
|
corps_html=corps,
|
|
document_ids=id,
|
|
type_document=TypeDocument.FACTURE,
|
|
statut=StatutEmailDB.EN_ATTENTE,
|
|
date_creation=datetime.now(),
|
|
nb_tentatives=0,
|
|
)
|
|
|
|
session.add(email_log)
|
|
await session.flush()
|
|
|
|
email_queue.enqueue(email_log.id)
|
|
|
|
sage.mettre_a_jour_derniere_relance(id, TypeDocument.FACTURE)
|
|
|
|
await session.commit()
|
|
|
|
logger.info(f"Relance facture: {id} → {contact['email']}")
|
|
|
|
return {
|
|
"success": True,
|
|
"facture_id": id,
|
|
"email_log_id": email_log.id,
|
|
"destinataire": contact["email"],
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur relance facture: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/emails/logs", tags=["Emails"])
|
|
async def journal_emails(
|
|
statut: Optional[StatutEmail] = Query(None),
|
|
destinataire: Optional[str] = Query(None),
|
|
limit: int = Query(100, le=1000),
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
query = select(EmailLog)
|
|
|
|
if statut:
|
|
query = query.where(EmailLog.statut == StatutEmailDB[statut.value])
|
|
|
|
if destinataire:
|
|
query = query.where(EmailLog.destinataire.contains(destinataire))
|
|
|
|
query = query.order_by(EmailLog.date_creation.desc()).limit(limit)
|
|
|
|
result = await session.execute(query)
|
|
logs = result.scalars().all()
|
|
|
|
return [
|
|
{
|
|
"id": log.id,
|
|
"destinataire": log.destinataire,
|
|
"sujet": log.sujet,
|
|
"statut": log.statut.value,
|
|
"date_creation": log.date_creation.isoformat(),
|
|
"date_envoi": log.date_envoi.isoformat() if log.date_envoi else None,
|
|
"nb_tentatives": log.nb_tentatives,
|
|
"derniere_erreur": log.derniere_erreur,
|
|
"document_ids": log.document_ids,
|
|
}
|
|
for log in logs
|
|
]
|
|
|
|
|
|
@app.get("/emails/logs/export", tags=["Emails"])
|
|
async def exporter_logs_csv(
|
|
statut: Optional[StatutEmail] = Query(None),
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
query = select(EmailLog)
|
|
if statut:
|
|
query = query.where(EmailLog.statut == StatutEmailDB[statut.value])
|
|
|
|
query = query.order_by(EmailLog.date_creation.desc())
|
|
|
|
result = await session.execute(query)
|
|
logs = result.scalars().all()
|
|
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
|
|
writer.writerow(
|
|
[
|
|
"ID",
|
|
"Destinataire",
|
|
"Sujet",
|
|
"Statut",
|
|
"Date Création",
|
|
"Date Envoi",
|
|
"Nb Tentatives",
|
|
"Erreur",
|
|
"Documents",
|
|
]
|
|
)
|
|
|
|
for log in logs:
|
|
writer.writerow(
|
|
[
|
|
log.id,
|
|
log.destinataire,
|
|
log.sujet,
|
|
log.statut.value,
|
|
log.date_creation.isoformat(),
|
|
log.date_envoi.isoformat() if log.date_envoi else "",
|
|
log.nb_tentatives,
|
|
log.derniere_erreur or "",
|
|
log.document_ids or "",
|
|
]
|
|
)
|
|
|
|
output.seek(0)
|
|
|
|
return StreamingResponse(
|
|
iter([output.getvalue()]),
|
|
media_type="text/csv",
|
|
headers={
|
|
"Content-Disposition": f"attachment; filename=emails_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
|
},
|
|
)
|
|
|
|
|
|
class TemplateEmail(BaseModel):
|
|
id: Optional[str] = None
|
|
nom: str
|
|
sujet: str
|
|
corps_html: str
|
|
variables_disponibles: List[str] = []
|
|
|
|
|
|
class TemplatePreview(BaseModel):
|
|
template_id: str
|
|
document_id: str
|
|
type_document: TypeDocument
|
|
|
|
|
|
@app.get("/templates/emails", response_model=List[TemplateEmail], tags=["Emails"])
|
|
async def lister_templates():
|
|
return [TemplateEmail(**template) for template in templates_email_db.values()]
|
|
|
|
|
|
@app.get(
|
|
"/templates/emails/{template_id}", response_model=TemplateEmail, tags=["Emails"]
|
|
)
|
|
async def lire_template(
|
|
template_id: str,
|
|
):
|
|
if template_id not in templates_email_db:
|
|
raise HTTPException(404, f"Template {template_id} introuvable")
|
|
|
|
return TemplateEmail(**templates_email_db[template_id])
|
|
|
|
|
|
@app.post("/templates/emails", response_model=TemplateEmail, tags=["Emails"])
|
|
async def creer_template(
|
|
template: TemplateEmail,
|
|
):
|
|
template_id = str(uuid.uuid4())
|
|
|
|
templates_email_db[template_id] = {
|
|
"id": template_id,
|
|
"nom": template.nom,
|
|
"sujet": template.sujet,
|
|
"corps_html": template.corps_html,
|
|
"variables_disponibles": template.variables_disponibles,
|
|
}
|
|
|
|
logger.info(f"Template créé: {template_id}")
|
|
|
|
return TemplateEmail(id=template_id, **template.dict())
|
|
|
|
|
|
@app.put(
|
|
"/templates/emails/{template_id}", response_model=TemplateEmail, tags=["Emails"]
|
|
)
|
|
async def modifier_template(
|
|
template_id: str,
|
|
template: TemplateEmail,
|
|
):
|
|
if template_id not in templates_email_db:
|
|
raise HTTPException(404, f"Template {template_id} introuvable")
|
|
|
|
if template_id in ["relance_devis", "relance_facture"]:
|
|
raise HTTPException(400, "Les templates système ne peuvent pas être modifiés")
|
|
|
|
templates_email_db[template_id] = {
|
|
"id": template_id,
|
|
"nom": template.nom,
|
|
"sujet": template.sujet,
|
|
"corps_html": template.corps_html,
|
|
"variables_disponibles": template.variables_disponibles,
|
|
}
|
|
|
|
logger.info(f"Template modifié: {template_id}")
|
|
|
|
return TemplateEmail(id=template_id, **template.dict())
|
|
|
|
|
|
@app.delete("/templates/emails/{template_id}", tags=["Emails"])
|
|
async def supprimer_template(
|
|
template_id: str,
|
|
):
|
|
if template_id not in templates_email_db:
|
|
raise HTTPException(404, f"Template {template_id} introuvable")
|
|
|
|
if template_id in ["relance_devis", "relance_facture"]:
|
|
raise HTTPException(400, "Les templates système ne peuvent pas être supprimés")
|
|
|
|
del templates_email_db[template_id]
|
|
|
|
logger.info(f"Template supprimé: {template_id}")
|
|
|
|
return {"success": True, "message": f"Template {template_id} supprimé"}
|
|
|
|
|
|
@app.post("/templates/emails/preview", tags=["Emails"])
|
|
async def previsualiser_email(
|
|
preview: TemplatePreview,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
if preview.template_id not in templates_email_db:
|
|
raise HTTPException(404, f"Template {preview.template_id} introuvable")
|
|
|
|
template = templates_email_db[preview.template_id]
|
|
|
|
doc = sage.lire_document(preview.document_id, preview.type_document)
|
|
if not doc:
|
|
raise HTTPException(404, f"Document {preview.document_id} introuvable")
|
|
|
|
variables = {
|
|
"DO_Piece": doc.get("numero", preview.document_id),
|
|
"DO_Date": str(doc.get("date", "")),
|
|
"CT_Intitule": doc.get("client_intitule", ""),
|
|
"DO_TotalHT": f"{doc.get('total_ht', 0):.2f}",
|
|
"DO_TotalTTC": f"{doc.get('total_ttc', 0):.2f}",
|
|
}
|
|
|
|
sujet_preview = template["sujet"]
|
|
corps_preview = template["corps_html"]
|
|
|
|
for var, valeur in variables.items():
|
|
sujet_preview = sujet_preview.replace(f"{{{{{var}}}}}", valeur)
|
|
corps_preview = corps_preview.replace(f"{{{{{var}}}}}", valeur)
|
|
|
|
return {
|
|
"template_id": preview.template_id,
|
|
"document_id": preview.document_id,
|
|
"sujet": sujet_preview,
|
|
"corps_html": corps_preview,
|
|
"variables_utilisees": variables,
|
|
}
|
|
|
|
|
|
@app.get("/prospects", tags=["Prospects"])
|
|
async def rechercher_prospects(
|
|
query: Optional[str] = Query(None),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
prospects = sage.lister_prospects(filtre=query or "")
|
|
return prospects
|
|
except Exception as e:
|
|
logger.error(f"Erreur recherche prospects: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/prospects/{code}", tags=["Prospects"])
|
|
async def lire_prospect(
|
|
code: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
prospect = sage.lire_prospect(code)
|
|
if not prospect:
|
|
raise HTTPException(404, f"Prospect {code} introuvable")
|
|
return prospect
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture prospect: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get(
|
|
"/fournisseurs", response_model=List[FournisseurDetails], tags=["Fournisseurs"]
|
|
)
|
|
async def rechercher_fournisseurs(
|
|
query: Optional[str] = Query(None),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
fournisseurs = sage.lister_fournisseurs(filtre=query or "")
|
|
|
|
logger.info(f"{len(fournisseurs)} fournisseurs")
|
|
|
|
if len(fournisseurs) == 0:
|
|
logger.warning("Aucun fournisseur retourné - vérifier la gateway Windows")
|
|
|
|
return [FournisseurDetails(**f) for f in fournisseurs]
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur recherche fournisseurs: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/fournisseurs", status_code=201, tags=["Fournisseurs"])
|
|
async def ajouter_fournisseur(
|
|
fournisseur: FournisseurCreate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
nouveau_fournisseur = sage.creer_fournisseur(fournisseur.dict())
|
|
|
|
logger.info(f"Fournisseur créé via API: {nouveau_fournisseur.get('numero')}")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Fournisseur créé avec succès",
|
|
"data": nouveau_fournisseur,
|
|
}
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Erreur métier création fournisseur: {e}")
|
|
raise HTTPException(400, str(e))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur technique création fournisseur: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.put(
|
|
"/fournisseurs/{code}", response_model=FournisseurDetails, tags=["Fournisseurs"]
|
|
)
|
|
async def modifier_fournisseur(
|
|
code: str,
|
|
fournisseur_update: FournisseurUpdate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.modifier_fournisseur(
|
|
code, fournisseur_update.dict(exclude_none=True)
|
|
)
|
|
|
|
logger.info(f"Fournisseur {code} modifié avec succès")
|
|
|
|
return FournisseurDetails(**resultat)
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Erreur métier modification fournisseur {code}: {e}")
|
|
raise HTTPException(404, str(e))
|
|
except Exception as e:
|
|
logger.error(f"Erreur technique modification fournisseur {code}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/fournisseurs/{code}", tags=["Fournisseurs"])
|
|
async def lire_fournisseur(
|
|
code: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
fournisseur = sage.lire_fournisseur(code)
|
|
if not fournisseur:
|
|
raise HTTPException(404, f"Fournisseur {code} introuvable")
|
|
return fournisseur
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture fournisseur: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/avoirs", tags=["Avoirs"])
|
|
async def lister_avoirs(
|
|
limit: int = Query(100, le=1000),
|
|
statut: Optional[int] = Query(None),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
avoirs = sage.lister_avoirs(limit=limit, statut=statut)
|
|
return avoirs
|
|
except Exception as e:
|
|
logger.error(f"Erreur liste avoirs: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/avoirs/{numero}", tags=["Avoirs"])
|
|
async def lire_avoir(
|
|
numero: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
avoir = sage.lire_document(numero, TypeDocumentSQL.BON_AVOIR)
|
|
if not avoir:
|
|
raise HTTPException(404, f"Avoir {numero} introuvable")
|
|
return avoir
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture avoir: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/avoirs", status_code=201, tags=["Avoirs"])
|
|
async def creer_avoir(
|
|
avoir: AvoirCreate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
avoir_data = {
|
|
"client_id": avoir.client_id,
|
|
"date_avoir": (avoir.date_avoir.isoformat() if avoir.date_avoir else None),
|
|
"date_livraison": (
|
|
avoir.date_livraison.isoformat() if avoir.date_livraison else None
|
|
),
|
|
"reference": avoir.reference,
|
|
"lignes": _preparer_lignes_document(avoir.lignes),
|
|
}
|
|
|
|
resultat = sage.creer_avoir(avoir_data)
|
|
|
|
logger.info(
|
|
f"Avoir créé: {resultat.get('numero_avoir')} "
|
|
f"({resultat.get('total_ttc')}€ TTC)"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Avoir créé avec succès",
|
|
"data": {
|
|
"numero_avoir": resultat["numero_avoir"],
|
|
"client_id": avoir.client_id,
|
|
"date_avoir": resultat["date_avoir"],
|
|
"total_ht": resultat["total_ht"],
|
|
"total_ttc": resultat["total_ttc"],
|
|
"nb_lignes": resultat["nb_lignes"],
|
|
"reference": resultat.get("reference"),
|
|
"date_livraison": resultat.get("date_livraison"),
|
|
},
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur création avoir: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.put("/avoirs/{id}", tags=["Avoirs"])
|
|
async def modifier_avoir(
|
|
id: str,
|
|
avoir_update: AvoirUpdate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
update_data = {}
|
|
|
|
if avoir_update.date_avoir:
|
|
update_data["date_avoir"] = avoir_update.date_avoir.isoformat()
|
|
|
|
if avoir_update.lignes is not None:
|
|
update_data["lignes"] = [
|
|
{
|
|
"article_code": ligne.article_code,
|
|
"quantite": ligne.quantite,
|
|
"remise_pourcentage": ligne.remise_pourcentage,
|
|
}
|
|
for ligne in avoir_update.lignes
|
|
]
|
|
|
|
if avoir_update.statut is not None:
|
|
update_data["statut"] = avoir_update.statut
|
|
|
|
if avoir_update.reference is not None:
|
|
update_data["reference"] = avoir_update.reference
|
|
|
|
resultat = sage.modifier_avoir(id, update_data)
|
|
|
|
logger.info(f"Avoir {id} modifié avec succès")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Avoir {id} modifié avec succès",
|
|
"avoir": resultat,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur modification avoir {id}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/livraisons", tags=["Livraisons"])
|
|
async def lister_livraisons(
|
|
limit: int = Query(100, le=1000),
|
|
statut: Optional[int] = Query(None),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
livraisons = sage.lister_livraisons(limit=limit, statut=statut)
|
|
return livraisons
|
|
except Exception as e:
|
|
logger.error(f"Erreur liste livraisons: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/livraisons/{numero}", tags=["Livraisons"])
|
|
async def lire_livraison(
|
|
numero: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
livraison = sage.lire_document(numero, TypeDocumentSQL.BON_LIVRAISON)
|
|
if not livraison:
|
|
raise HTTPException(404, f"Livraison {numero} introuvable")
|
|
return livraison
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture livraison: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/livraisons", status_code=201, tags=["Livraisons"])
|
|
async def creer_livraison(
|
|
livraison: LivraisonCreate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
livraison_data = {
|
|
"client_id": livraison.client_id,
|
|
"date_livraison": (
|
|
livraison.date_livraison.isoformat()
|
|
if livraison.date_livraison
|
|
else None
|
|
),
|
|
"date_livraison_prevue": (
|
|
livraison.date_livraison_prevue.isoformat()
|
|
if livraison.date_livraison_prevue
|
|
else None
|
|
),
|
|
"reference": livraison.reference,
|
|
"lignes": _preparer_lignes_document(livraison.lignes),
|
|
}
|
|
|
|
resultat = sage.creer_livraison(livraison_data)
|
|
|
|
logger.info(
|
|
f"Livraison créée: {resultat.get('numero_livraison')} "
|
|
f"({resultat.get('total_ttc')}€ TTC)"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Livraison créée avec succès",
|
|
"data": {
|
|
"numero_livraison": resultat["numero_livraison"],
|
|
"client_id": livraison.client_id,
|
|
"date_livraison": resultat["date_livraison"],
|
|
"date_livraison_prevue": resultat.get("date_livraison_prevue"),
|
|
"total_ht": resultat["total_ht"],
|
|
"total_ttc": resultat["total_ttc"],
|
|
"nb_lignes": resultat["nb_lignes"],
|
|
"reference": resultat.get("reference"),
|
|
},
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur création livraison: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.put("/livraisons/{id}", tags=["Livraisons"])
|
|
async def modifier_livraison(
|
|
id: str,
|
|
livraison_update: LivraisonUpdate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
update_data = {}
|
|
|
|
if livraison_update.date_livraison:
|
|
update_data["date_livraison"] = livraison_update.date_livraison.isoformat()
|
|
|
|
if livraison_update.lignes is not None:
|
|
update_data["lignes"] = [
|
|
{
|
|
"article_code": ligne.article_code,
|
|
"quantite": ligne.quantite,
|
|
"remise_pourcentage": ligne.remise_pourcentage,
|
|
}
|
|
for ligne in livraison_update.lignes
|
|
]
|
|
|
|
if livraison_update.statut is not None:
|
|
update_data["statut"] = livraison_update.statut
|
|
|
|
if livraison_update.reference is not None:
|
|
update_data["reference"] = livraison_update.reference
|
|
|
|
resultat = sage.modifier_livraison(id, update_data)
|
|
|
|
logger.info(f"Livraison {id} modifiée avec succès")
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Livraison {id} modifiée avec succès",
|
|
"livraison": resultat,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur modification livraison {id}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/workflow/livraison/{id}/to-facture", tags=["Workflows"])
|
|
async def livraison_vers_facture(
|
|
id: str,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.transformer_document(
|
|
numero_source=id,
|
|
type_source=settings.SAGE_TYPE_BON_LIVRAISON, # = 30
|
|
type_cible=settings.SAGE_TYPE_FACTURE, # = 60
|
|
)
|
|
|
|
workflow_log = WorkflowLog(
|
|
id=str(uuid.uuid4()),
|
|
document_source=id,
|
|
type_source=TypeDocument.BON_LIVRAISON,
|
|
document_cible=resultat.get("document_cible", ""),
|
|
type_cible=TypeDocument.FACTURE,
|
|
nb_lignes=resultat.get("nb_lignes", 0),
|
|
date_transformation=datetime.now(),
|
|
succes=True,
|
|
)
|
|
|
|
session.add(workflow_log)
|
|
await session.commit()
|
|
|
|
logger.info(
|
|
f"Transformation: Livraison {id} → Facture {resultat['document_cible']}"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"document_source": id,
|
|
"document_cible": resultat["document_cible"],
|
|
"nb_lignes": resultat["nb_lignes"],
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur transformation: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/workflow/devis/{id}/to-facture", tags=["Workflows"])
|
|
async def devis_vers_facture_direct(
|
|
id: str,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
devis_existant = sage.lire_devis(id)
|
|
if not devis_existant:
|
|
raise HTTPException(404, f"Devis {id} introuvable")
|
|
|
|
statut_devis = devis_existant.get("statut", 0)
|
|
if statut_devis == 5:
|
|
raise HTTPException(
|
|
400,
|
|
f"Le devis {id} a déjà été transformé (statut=5). "
|
|
f"Vérifiez les documents déjà créés depuis ce devis.",
|
|
)
|
|
|
|
resultat = sage.transformer_document(
|
|
numero_source=id,
|
|
type_source=settings.SAGE_TYPE_DEVIS, # = 0
|
|
type_cible=settings.SAGE_TYPE_FACTURE, # = 60
|
|
)
|
|
|
|
workflow_log = WorkflowLog(
|
|
id=str(uuid.uuid4()),
|
|
document_source=id,
|
|
type_source=TypeDocument.DEVIS,
|
|
document_cible=resultat.get("document_cible", ""),
|
|
type_cible=TypeDocument.FACTURE,
|
|
nb_lignes=resultat.get("nb_lignes", 0),
|
|
date_transformation=datetime.now(),
|
|
succes=True,
|
|
)
|
|
|
|
session.add(workflow_log)
|
|
await session.commit()
|
|
|
|
logger.info(
|
|
f"Transformation DIRECTE: Devis {id} → Facture {resultat['document_cible']}"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"workflow": "devis_to_facture_direct",
|
|
"document_source": id,
|
|
"document_cible": resultat["document_cible"],
|
|
"nb_lignes": resultat["nb_lignes"],
|
|
"statut_devis_mis_a_jour": True,
|
|
"message": f"Facture {resultat['document_cible']} créée directement depuis le devis {id}",
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur transformation devis→facture: {e}", exc_info=True)
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/workflow/commande/{id}/to-livraison", tags=["Workflows"])
|
|
async def commande_vers_livraison(
|
|
id: str,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
commande_existante = sage.lire_document(id, TypeDocumentSQL.BON_COMMANDE)
|
|
|
|
if not commande_existante:
|
|
raise HTTPException(404, f"Commande {id} introuvable")
|
|
|
|
statut_commande = commande_existante.get("statut", 0)
|
|
if statut_commande == 5:
|
|
raise HTTPException(
|
|
400,
|
|
f"La commande {id} a déjà été transformée (statut=5). "
|
|
f"Un bon de livraison existe probablement déjà.",
|
|
)
|
|
|
|
if statut_commande == 6:
|
|
raise HTTPException(
|
|
400,
|
|
f"La commande {id} est annulée (statut=6) et ne peut pas être transformée.",
|
|
)
|
|
|
|
resultat = sage.transformer_document(
|
|
numero_source=id,
|
|
type_source=settings.SAGE_TYPE_BON_COMMANDE, # = 10
|
|
type_cible=settings.SAGE_TYPE_BON_LIVRAISON, # = 30
|
|
)
|
|
|
|
workflow_log = WorkflowLog(
|
|
id=str(uuid.uuid4()),
|
|
document_source=id,
|
|
type_source=TypeDocument.BON_COMMANDE,
|
|
document_cible=resultat.get("document_cible", ""),
|
|
type_cible=TypeDocument.BON_LIVRAISON,
|
|
nb_lignes=resultat.get("nb_lignes", 0),
|
|
date_transformation=datetime.now(),
|
|
succes=True,
|
|
)
|
|
|
|
session.add(workflow_log)
|
|
await session.commit()
|
|
|
|
logger.info(
|
|
f"Transformation: Commande {id} → Livraison {resultat['document_cible']}"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"workflow": "commande_to_livraison",
|
|
"document_source": id,
|
|
"document_cible": resultat["document_cible"],
|
|
"nb_lignes": resultat["nb_lignes"],
|
|
"message": f"Bon de livraison {resultat['document_cible']} créé depuis la commande {id}",
|
|
"next_step": f"Utilisez /workflow/livraison/{resultat['document_cible']}/to-facture pour créer la facture",
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur transformation commande→livraison: {e}", exc_info=True)
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get(
|
|
"/familles",
|
|
response_model=List[Familles],
|
|
tags=["Familles"],
|
|
summary="Liste toutes les familles d'articles",
|
|
)
|
|
async def lister_familles(
|
|
filtre: Optional[str] = Query(None, description="Filtre sur code ou intitulé"),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
familles = sage.lister_familles(filtre or "")
|
|
|
|
logger.info(f"{len(familles)} famille(s) retournée(s)")
|
|
|
|
return [Familles(**f) for f in familles]
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur liste familles: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Erreur lors de la récupération des familles: {str(e)}",
|
|
)
|
|
|
|
|
|
@app.get(
|
|
"/familles/{code}",
|
|
response_model=Familles,
|
|
tags=["Familles"],
|
|
summary="Lecture d'une famille par son code",
|
|
)
|
|
async def lire_famille(
|
|
code: str = Path(..., description="Code de la famille (ex: ZDIVERS)"),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
famille = sage.lire_famille(code)
|
|
|
|
if not famille:
|
|
logger.warning(f"Famille {code} introuvable")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Famille {code} introuvable",
|
|
)
|
|
|
|
logger.info(f"Famille {code} lue: {famille.get('intitule', '')}")
|
|
|
|
return Familles(**famille)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture famille {code}: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Erreur lors de la lecture de la famille: {str(e)}",
|
|
)
|
|
|
|
|
|
@app.post(
|
|
"/familles",
|
|
response_model=Familles,
|
|
status_code=status.HTTP_201_CREATED,
|
|
tags=["Familles"],
|
|
summary="Création d'une famille d'articles",
|
|
)
|
|
async def creer_famille(
|
|
famille: FamilleCreate,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
if not famille.code or not famille.intitule:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Les champs 'code' et 'intitule' sont obligatoires",
|
|
)
|
|
|
|
famille_data = famille.dict()
|
|
|
|
logger.info(f"Création famille: {famille.code} - {famille.intitule}")
|
|
|
|
resultat = sage.creer_famille(famille_data)
|
|
|
|
logger.info(f"Famille créée: {resultat.get('code')}")
|
|
|
|
return Familles(**resultat)
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Erreur métier création famille: {e}")
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
|
|
except HTTPException:
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur technique création famille: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Erreur lors de la création de la famille: {str(e)}",
|
|
)
|
|
|
|
|
|
@app.post(
|
|
"/stock/entree",
|
|
response_model=MouvementStock,
|
|
status_code=status.HTTP_201_CREATED,
|
|
tags=["Stock"],
|
|
summary="ENTRÉE EN STOCK : Ajoute des articles dans le stock",
|
|
)
|
|
async def creer_entree_stock(
|
|
entree: EntreeStock,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
entree_data = entree.dict()
|
|
if entree_data.get("date_entree"):
|
|
entree_data["date_entree"] = entree_data["date_entree"].isoformat()
|
|
|
|
logger.info(f"Création entrée stock: {len(entree.lignes)} ligne(s)")
|
|
|
|
resultat = sage.creer_entree_stock(entree_data)
|
|
|
|
logger.info(f"Entrée stock créée: {resultat.get('numero')}")
|
|
|
|
return MouvementStock(**resultat)
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Erreur métier entrée stock: {e}")
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur technique entrée stock: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Erreur lors de la création de l'entrée: {str(e)}",
|
|
)
|
|
|
|
|
|
@app.post(
|
|
"/stock/sortie",
|
|
response_model=MouvementStock,
|
|
status_code=status.HTTP_201_CREATED,
|
|
tags=["Stock"],
|
|
summary="SORTIE DE STOCK : Retire des articles du stock",
|
|
)
|
|
async def creer_sortie_stock(
|
|
sortie: SortieStock,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
sortie_data = sortie.dict()
|
|
if sortie_data.get("date_sortie"):
|
|
sortie_data["date_sortie"] = sortie_data["date_sortie"].isoformat()
|
|
|
|
logger.info(f"Création sortie stock: {len(sortie.lignes)} ligne(s)")
|
|
|
|
resultat = sage.creer_sortie_stock(sortie_data)
|
|
|
|
logger.info(f"Sortie stock créée: {resultat.get('numero')}")
|
|
|
|
return MouvementStock(**resultat)
|
|
|
|
except ValueError as e:
|
|
logger.warning(f"Erreur métier sortie stock: {e}")
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur technique sortie stock: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Erreur lors de la création de la sortie: {str(e)}",
|
|
)
|
|
|
|
|
|
@app.get(
|
|
"/stock/mouvement/{numero}",
|
|
response_model=MouvementStock,
|
|
tags=["Stock"],
|
|
summary="Lecture d'un mouvement de stock",
|
|
)
|
|
async def lire_mouvement_stock(
|
|
numero: str = Path(..., description="Numéro du mouvement (ex: ME00123 ou MS00124)"),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
mouvement = sage.lire_mouvement_stock(numero)
|
|
|
|
if not mouvement:
|
|
logger.warning(f"Mouvement {numero} introuvable")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Mouvement de stock {numero} introuvable",
|
|
)
|
|
|
|
logger.info(f"Mouvement {numero} lu")
|
|
|
|
return MouvementStock(**mouvement)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture mouvement {numero}: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Erreur lors de la lecture du mouvement: {str(e)}",
|
|
)
|
|
|
|
|
|
@app.get(
|
|
"/familles/stats/global",
|
|
tags=["Familles"],
|
|
summary="Statistiques sur les familles",
|
|
)
|
|
async def statistiques_familles(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
stats = sage.get_stats_familles()
|
|
|
|
return {"success": True, "data": stats}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur stats familles: {e}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Erreur lors de la récupération des statistiques: {str(e)}",
|
|
)
|
|
|
|
|
|
@app.get("/debug/users", response_model=List[Users], tags=["Debug"])
|
|
async def lister_utilisateurs_debug(
|
|
session: AsyncSession = Depends(get_session),
|
|
limit: int = Query(100, le=1000),
|
|
role: Optional[str] = Query(None),
|
|
verified_only: bool = Query(False),
|
|
):
|
|
from database import User
|
|
from sqlalchemy import select
|
|
|
|
try:
|
|
query = select(User)
|
|
|
|
if role:
|
|
query = query.where(User.role == role)
|
|
|
|
if verified_only:
|
|
query = query.where(User.is_verified)
|
|
|
|
query = query.order_by(User.created_at.desc()).limit(limit)
|
|
|
|
result = await session.execute(query)
|
|
users = result.scalars().all()
|
|
|
|
users_response = []
|
|
for user in users:
|
|
users_response.append(
|
|
Users(
|
|
id=user.id,
|
|
email=user.email,
|
|
nom=user.nom,
|
|
prenom=user.prenom,
|
|
role=user.role,
|
|
is_verified=user.is_verified,
|
|
is_active=user.is_active,
|
|
created_at=user.created_at.isoformat() if user.created_at else "",
|
|
last_login=user.last_login.isoformat() if user.last_login else None,
|
|
failed_login_attempts=user.failed_login_attempts or 0,
|
|
)
|
|
)
|
|
|
|
logger.info(f"Liste utilisateurs retournée: {len(users_response)} résultat(s)")
|
|
|
|
return users_response
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur liste utilisateurs: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/debug/users/stats", tags=["Debug"])
|
|
async def statistiques_utilisateurs(session: AsyncSession = Depends(get_session)):
|
|
from database import User
|
|
from sqlalchemy import select, func
|
|
|
|
try:
|
|
total_query = select(func.count(User.id))
|
|
total_result = await session.execute(total_query)
|
|
total = total_result.scalar()
|
|
|
|
verified_query = select(func.count(User.id)).where(User.is_verified)
|
|
verified_result = await session.execute(verified_query)
|
|
verified = verified_result.scalar()
|
|
|
|
active_query = select(func.count(User.id)).where(User.is_active)
|
|
active_result = await session.execute(active_query)
|
|
active = active_result.scalar()
|
|
|
|
roles_query = select(User.role, func.count(User.id)).group_by(User.role)
|
|
roles_result = await session.execute(roles_query)
|
|
roles_stats = {role: count for role, count in roles_result.all()}
|
|
|
|
return {
|
|
"total_utilisateurs": total,
|
|
"utilisateurs_verifies": verified,
|
|
"utilisateurs_actifs": active,
|
|
"utilisateurs_non_verifies": total - verified,
|
|
"repartition_roles": roles_stats,
|
|
"taux_verification": f"{(verified / total * 100):.1f}%"
|
|
if total > 0
|
|
else "0%",
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur stats utilisateurs: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/tiers/{numero}/contacts", response_model=Contact, tags=["Contacts"])
|
|
async def creer_contact(
|
|
numero: str,
|
|
contact: ContactCreate,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
try:
|
|
sage.lire_tiers(numero)
|
|
except HTTPException:
|
|
raise
|
|
except Exception:
|
|
raise HTTPException(404, f"Tiers {numero} non trouvé")
|
|
|
|
if contact.numero != numero:
|
|
contact.numero = numero
|
|
|
|
resultat = sage.creer_contact(contact.dict())
|
|
|
|
if isinstance(resultat, dict) and "data" in resultat:
|
|
contact_data = resultat["data"]
|
|
else:
|
|
contact_data = resultat
|
|
|
|
return Contact(**contact_data)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur création contact: {e}", exc_info=True)
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/tiers/{numero}/contacts", response_model=List[Contact], tags=["Contacts"])
|
|
async def lister_contacts(
|
|
numero: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
contacts = sage.lister_contacts(numero)
|
|
return [Contact(**c) for c in contacts]
|
|
except Exception as e:
|
|
logger.error(f"Erreur liste contacts: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get(
|
|
"/tiers/{numero}/contacts/{contact_numero}",
|
|
response_model=Contact,
|
|
tags=["Contacts"],
|
|
)
|
|
async def obtenir_contact(
|
|
numero: str,
|
|
contact_numero: int,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
contact = sage.obtenir_contact(numero, contact_numero)
|
|
if not contact:
|
|
raise HTTPException(
|
|
404, f"Contact {contact_numero} non trouvé pour client {numero}"
|
|
)
|
|
return Contact(**contact)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur récupération contact: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.put(
|
|
"/tiers/{numero}/contacts/{contact_numero}",
|
|
response_model=Contact,
|
|
tags=["Contacts"],
|
|
)
|
|
async def modifier_contact(
|
|
numero: str,
|
|
contact_numero: int,
|
|
contact: ContactUpdate,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
contact_existant = sage.obtenir_contact(numero, contact_numero)
|
|
if not contact_existant:
|
|
raise HTTPException(404, f"Contact {contact_numero} non trouvé")
|
|
|
|
updates = {k: v for k, v in contact.dict().items() if v is not None}
|
|
|
|
if not updates:
|
|
raise HTTPException(400, "Aucune modification fournie")
|
|
|
|
resultat = sage.modifier_contact(numero, contact_numero, updates)
|
|
if isinstance(resultat, dict) and "data" in resultat:
|
|
contact_data = resultat["data"]
|
|
else:
|
|
contact_data = resultat
|
|
|
|
return Contact(**contact_data)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur modification contact: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.delete("/tiers/{numero}/contacts/{contact_numero}", tags=["Contacts"])
|
|
async def supprimer_contact(
|
|
numero: str,
|
|
contact_numero: int,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
sage.supprimer_contact(numero, contact_numero)
|
|
return {"success": True, "message": f"Contact {contact_numero} supprimé"}
|
|
except Exception as e:
|
|
logger.error(f"Erreur suppression contact: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/tiers/{numero}/contacts/{contact_numero}/definir-defaut", tags=["Contacts"])
|
|
async def definir_contact_defaut(
|
|
numero: str,
|
|
contact_numero: int,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.definir_contact_defaut(numero, contact_numero)
|
|
return {
|
|
"success": True,
|
|
"message": f"Contact {contact_numero} défini comme contact par défaut",
|
|
"data": resultat,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Erreur définition contact par défaut: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/tiers", response_model=List[TiersDetails], tags=["Tiers"])
|
|
async def obtenir_tiers(
|
|
type_tiers: Optional[str] = Query(
|
|
None,
|
|
description="Filtre par type: 0/client, 1/fournisseur, 2/prospect, 3/all ou strings",
|
|
),
|
|
query: Optional[str] = Query(None, description="Recherche sur code ou intitulé"),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
type_normalise = normaliser_type_tiers(type_tiers)
|
|
tiers = sage.lister_tiers(type_tiers=type_normalise, filtre=query or "")
|
|
return [TiersDetails(**t) for t in tiers]
|
|
except Exception as e:
|
|
logger.error(f"Erreur recherche tiers: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/tiers/{code}", response_model=TiersDetails, tags=["Tiers"])
|
|
async def lire_tiers_detail(
|
|
code: str,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
tiers = sage.lire_tiers(code)
|
|
if not tiers:
|
|
raise HTTPException(404, f"Tiers {code} introuvable")
|
|
return TiersDetails(**tiers)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture tiers {code}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/sage/current-config", tags=["System"])
|
|
async def get_current_sage_config(
|
|
ctx: GatewayContext = Depends(get_gateway_context_for_user),
|
|
):
|
|
return {
|
|
"source": "user_gateway" if not ctx.is_fallback else "fallback_env",
|
|
"gateway_id": ctx.gateway_id,
|
|
"gateway_name": ctx.gateway_name,
|
|
"gateway_url": ctx.url,
|
|
"user_id": ctx.user_id,
|
|
}
|
|
|
|
|
|
@app.get(
|
|
"/collaborateurs",
|
|
response_model=List[CollaborateurDetails],
|
|
tags=["Collaborateurs"],
|
|
)
|
|
async def lister_collaborateurs(
|
|
filtre: Optional[str] = Query(None, description="Filtre sur nom/prénom"),
|
|
actifs_seulement: bool = Query(
|
|
True, description="Exclure les collaborateurs en sommeil"
|
|
),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Liste tous les collaborateurs"""
|
|
try:
|
|
collaborateurs = sage.lister_collaborateurs(filtre, actifs_seulement)
|
|
return [CollaborateurDetails(**c) for c in collaborateurs]
|
|
except Exception as e:
|
|
logger.error(f"Erreur liste collaborateurs: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get(
|
|
"/collaborateurs/{numero}",
|
|
response_model=CollaborateurDetails,
|
|
tags=["Collaborateurs"],
|
|
)
|
|
async def lire_collaborateur_detail(
|
|
numero: int,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Lit un collaborateur par son numéro"""
|
|
try:
|
|
collaborateur = sage.lire_collaborateur(numero)
|
|
|
|
if not collaborateur:
|
|
raise HTTPException(404, f"Collaborateur {numero} introuvable")
|
|
|
|
return CollaborateurDetails(**collaborateur)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture collaborateur {numero}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post(
|
|
"/collaborateurs",
|
|
response_model=CollaborateurDetails,
|
|
tags=["Collaborateurs"],
|
|
status_code=201,
|
|
)
|
|
async def creer_collaborateur(
|
|
collaborateur: CollaborateurCreate,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Crée un nouveau collaborateur"""
|
|
try:
|
|
nouveau = sage.creer_collaborateur(collaborateur.model_dump())
|
|
|
|
if not nouveau:
|
|
raise HTTPException(500, "Échec création collaborateur")
|
|
|
|
return CollaborateurDetails(**nouveau)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur création collaborateur: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.put(
|
|
"/collaborateurs/{numero}",
|
|
response_model=CollaborateurDetails,
|
|
tags=["Collaborateurs"],
|
|
)
|
|
async def modifier_collaborateur(
|
|
numero: int,
|
|
collaborateur: CollaborateurUpdate,
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Modifie un collaborateur existant"""
|
|
try:
|
|
modifie = sage.modifier_collaborateur(
|
|
numero, collaborateur.model_dump(exclude_unset=True)
|
|
)
|
|
|
|
if not modifie:
|
|
raise HTTPException(404, f"Collaborateur {numero} introuvable")
|
|
|
|
return CollaborateurDetails(**modifie)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur modification collaborateur {numero}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/societe/info", response_model=SocieteInfo, tags=["Société"])
|
|
async def obtenir_informations_societe(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
societe = sage.lire_informations_societe()
|
|
|
|
if not societe:
|
|
raise HTTPException(404, "Informations société introuvables")
|
|
|
|
return societe
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture info société: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/societe/logo", tags=["Société"])
|
|
async def obtenir_logo_societe(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Retourne le logo en tant qu'image directe"""
|
|
try:
|
|
societe = sage.lire_informations_societe()
|
|
|
|
if not societe or not societe.get("logo_base64"):
|
|
raise HTTPException(404, "Logo introuvable")
|
|
|
|
import base64
|
|
|
|
image_data = base64.b64decode(societe["logo_base64"])
|
|
|
|
return Response(content=image_data, media_type=societe["logo_content_type"])
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur récupération logo: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/societe/preview", response_class=HTMLResponse, tags=["Société"])
|
|
async def preview_societe(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Page HTML pour visualiser les infos société avec logo"""
|
|
try:
|
|
societe = sage.lire_informations_societe()
|
|
|
|
if not societe:
|
|
return "<h1>Société introuvable</h1>"
|
|
|
|
logo_html = ""
|
|
if societe.get("logo_base64"):
|
|
logo_html = f'<img src="data:{societe["logo_content_type"]};base64,{societe["logo_base64"]}" style="max-width: 300px; border: 1px solid #ccc; padding: 10px;">'
|
|
else:
|
|
logo_html = "<p>Aucun logo disponible</p>"
|
|
|
|
html = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Informations Société</title>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; margin: 40px; }}
|
|
.container {{ max-width: 800px; }}
|
|
.logo {{ margin: 20px 0; }}
|
|
.info {{ margin: 10px 0; }}
|
|
.label {{ font-weight: bold; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Informations Société</h1>
|
|
|
|
<div class="logo">
|
|
<h2>Logo</h2>
|
|
{logo_html}
|
|
</div>
|
|
|
|
<div class="info">
|
|
<span class="label">Raison sociale:</span> {societe["raison_sociale"]}
|
|
</div>
|
|
<div class="info">
|
|
<span class="label">SIRET:</span> {societe["siret"] or "N/A"}
|
|
</div>
|
|
<div class="info">
|
|
<span class="label">Adresse:</span> {societe["adresse"] or "N/A"}
|
|
</div>
|
|
<div class="info">
|
|
<span class="label">Code postal:</span> {societe["code_postal"] or "N/A"}
|
|
</div>
|
|
<div class="info">
|
|
<span class="label">Ville:</span> {societe["ville"] or "N/A"}
|
|
</div>
|
|
<div class="info">
|
|
<span class="label">Email:</span> {societe["email"] or "N/A"}
|
|
</div>
|
|
<div class="info">
|
|
<span class="label">Téléphone:</span> {societe["telephone"] or "N/A"}
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
return html
|
|
|
|
except Exception as e:
|
|
return f"<h1>Erreur</h1><p>{str(e)}</p>"
|
|
|
|
|
|
@app.post("/factures/{numero_facture}/valider", status_code=200, tags=["Factures"])
|
|
async def valider_facture(
|
|
numero_facture: str,
|
|
_: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.valider_facture(numero_facture)
|
|
logger.info(
|
|
f"Facture {numero_facture} validée: {resultat.get('action_effectuee')}"
|
|
)
|
|
return {
|
|
"success": True,
|
|
"message": resultat.get("message", "Facture validée"),
|
|
"data": resultat,
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur validation facture {numero_facture}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.post("/factures/{numero_facture}/devalider", status_code=200, tags=["Factures"])
|
|
async def devalider_facture(
|
|
numero_facture: str,
|
|
_: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.devalider_facture(numero_facture)
|
|
logger.info(
|
|
f"Facture {numero_facture} dévalidée: {resultat.get('action_effectuee')}"
|
|
)
|
|
return {
|
|
"success": True,
|
|
"message": resultat.get("message", "Facture dévalidée"),
|
|
"data": resultat,
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur dévalidation facture {numero_facture}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.get("/factures/{numero_facture}/statut-validation", tags=["Factures"])
|
|
async def get_statut_validation_facture(
|
|
numero_facture: str,
|
|
_: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.get_statut_validation(numero_facture)
|
|
return {
|
|
"success": True,
|
|
"data": resultat,
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture statut {numero_facture}: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.post("/factures/{numero_facture}/regler", status_code=200, tags=["Règlements"])
|
|
async def regler_facture(
|
|
numero_facture: str,
|
|
reglement: ReglementFactureCreate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.regler_facture(
|
|
numero_facture=numero_facture,
|
|
montant=float(reglement.montant),
|
|
mode_reglement=reglement.mode_reglement,
|
|
code_journal=reglement.code_journal,
|
|
date_reglement=reglement.date_reglement.isoformat()
|
|
if reglement.date_reglement
|
|
else None,
|
|
reference=reglement.reference or "",
|
|
libelle=reglement.libelle or "",
|
|
devise_code=reglement.devise_code,
|
|
cours_devise=float(reglement.cours_devise)
|
|
if reglement.cours_devise
|
|
else 1.0,
|
|
tva_encaissement=reglement.tva_encaissement,
|
|
compte_general=reglement.compte_general,
|
|
)
|
|
|
|
logger.info(
|
|
f"Règlement facture {numero_facture}: {reglement.montant}€ - "
|
|
f"Journal: {reglement.code_journal} - Mode: {reglement.mode_reglement}"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Règlement effectué avec succès",
|
|
"data": resultat,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur règlement facture {numero_facture}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.post("/reglements/multiple", status_code=200, tags=["Règlements"])
|
|
async def regler_factures_multiple(
|
|
reglement: ReglementMultipleCreate,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.regler_factures_client(
|
|
client_code=reglement.client_id,
|
|
montant_total=float(reglement.montant_total),
|
|
mode_reglement=reglement.mode_reglement,
|
|
date_reglement=reglement.date_reglement.isoformat()
|
|
if reglement.date_reglement
|
|
else None,
|
|
reference=reglement.reference or "",
|
|
libelle=reglement.libelle or "",
|
|
code_journal=reglement.code_journal,
|
|
numeros_factures=reglement.numeros_factures,
|
|
)
|
|
|
|
logger.info(
|
|
f"Règlement multiple client {reglement.client_id}: "
|
|
f"{resultat.get('montant_effectif', 0)}€ sur {resultat.get('nb_factures_reglees', 0)} facture(s)"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "Règlements effectués avec succès",
|
|
"data": resultat,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur règlement multiple {reglement.client_id}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/factures/{numero_facture}/reglements", tags=["Règlements"])
|
|
async def get_reglements_facture(
|
|
numero_facture: str,
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.get_reglements_facture(numero_facture)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": resultat,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture règlements {numero_facture}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/clients/{client_id}/reglements", tags=["Règlements"])
|
|
async def get_reglements_client(
|
|
client_id: str,
|
|
date_debut: Optional[datetime] = Query(None, description="Date début"),
|
|
date_fin: Optional[datetime] = Query(None, description="Date fin"),
|
|
inclure_soldees: bool = Query(True, description="Inclure les factures soldées"),
|
|
session: AsyncSession = Depends(get_session),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.get_reglements_client(
|
|
client_code=client_id,
|
|
date_debut=date_debut.isoformat() if date_debut else None,
|
|
date_fin=date_fin.isoformat() if date_fin else None,
|
|
inclure_soldees=inclure_soldees,
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": resultat,
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture règlements client {client_id}: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/journaux/banque", tags=["Règlements"])
|
|
async def get_journaux_banque(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
try:
|
|
resultat = sage.get_journaux_banque()
|
|
return {"success": True, "data": resultat}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture journaux: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/reglements/modes", tags=["Référentiels"])
|
|
async def get_modes_reglement(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Liste des modes de règlement disponibles dans Sage"""
|
|
try:
|
|
modes = sage.get_modes_reglement()
|
|
return {"success": True, "data": {"modes": modes}}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture modes règlement: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/devises", tags=["Référentiels"])
|
|
async def get_devises(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Liste des devises disponibles dans Sage"""
|
|
try:
|
|
devises = sage.get_devises()
|
|
return {"success": True, "data": {"devises": devises}}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture devises: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/journaux/tresorerie", tags=["Référentiels"])
|
|
async def get_journaux_tresorerie(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Liste des journaux de trésorerie (banque + caisse)"""
|
|
try:
|
|
journaux = sage.get_journaux_tresorerie()
|
|
return {"success": True, "data": {"journaux": journaux}}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture journaux: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/comptes-generaux", tags=["Référentiels"])
|
|
async def get_comptes_generaux(
|
|
prefixe: Optional[str] = Query(None, description="Filtre par préfixe"),
|
|
type_compte: Optional[str] = Query(
|
|
None,
|
|
description="client | fournisseur | banque | caisse | tva | produit | charge",
|
|
),
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Liste des comptes généraux"""
|
|
try:
|
|
comptes = sage.get_comptes_generaux(prefixe=prefixe, type_compte=type_compte)
|
|
return {"success": True, "data": {"comptes": comptes, "total": len(comptes)}}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture comptes généraux: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/tva/taux", tags=["Référentiels"])
|
|
async def get_tva_taux(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Liste des taux de TVA"""
|
|
try:
|
|
taux = sage.get_tva_taux()
|
|
return {"success": True, "data": {"taux": taux}}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture taux TVA: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/parametres/encaissement", tags=["Référentiels"])
|
|
async def get_parametres_encaissement(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
"""Paramètres TVA sur encaissement"""
|
|
try:
|
|
params = sage.get_parametres_encaissement()
|
|
return {"success": True, "data": params}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture paramètres encaissement: {e}")
|
|
raise HTTPException(500, str(e))
|
|
|
|
|
|
@app.get("/reglements", tags=["Règlements"])
|
|
async def get_tous_reglements(
|
|
date_debut: Optional[date] = Query(None),
|
|
date_fin: Optional[date] = Query(None),
|
|
client_code: Optional[str] = Query(None),
|
|
type_reglement: Optional[str] = Query(None),
|
|
):
|
|
"""Liste tous les règlements avec filtres optionnels"""
|
|
params = {}
|
|
if date_debut:
|
|
params["date_debut"] = date_debut.isoformat()
|
|
if date_fin:
|
|
params["date_fin"] = date_fin.isoformat()
|
|
if client_code:
|
|
params["client_code"] = client_code
|
|
if type_reglement:
|
|
params["type_reglement"] = type_reglement
|
|
|
|
return sage_client.get_tous_reglements(params)
|
|
|
|
|
|
@app.get("/reglements/facture/{facture_no}", tags=["Règlements"])
|
|
async def get_reglement_facture_detail(facture_no):
|
|
"""Détail complet d'un règlement"""
|
|
return sage_client.get_reglement_facture_detail(facture_no)
|
|
|
|
|
|
@app.get("/reglements/{rg_no}", tags=["Règlements"])
|
|
async def get_reglement_detail(rg_no):
|
|
"""Détail complet d'un règlement"""
|
|
return sage_client.get_reglement_detail(rg_no)
|
|
|
|
|
|
""" @app.get("/health", tags=["System"])
|
|
async def health_check(
|
|
sage: SageGatewayClient = Depends(get_sage_client_for_user),
|
|
):
|
|
gateway_health = sage.health()
|
|
|
|
return {
|
|
"status": "healthy",
|
|
"sage_gateway": gateway_health,
|
|
"using_gateway_id": sage.gateway_id,
|
|
"email_queue": {
|
|
"running": email_queue.running,
|
|
"workers": len(email_queue.workers),
|
|
"queue_size": email_queue.queue.qsize(),
|
|
},
|
|
"timestamp": datetime.now().isoformat(),
|
|
} """
|
|
|
|
|
|
@app.get("/", tags=["System"])
|
|
async def root():
|
|
"""
|
|
Point d'entrée de l'API
|
|
"""
|
|
return {
|
|
"api": "Sage 100c Dataven API",
|
|
"version": "3.0.0",
|
|
"status": "operational",
|
|
"documentation": {
|
|
"swagger": "/docs",
|
|
"redoc": "/redoc",
|
|
"openapi": "/openapi.json",
|
|
},
|
|
"authentication": {
|
|
"methods": [
|
|
{
|
|
"type": "JWT (Bearer Token)",
|
|
"header": "Authorization: Bearer <token>",
|
|
"obtain_token": "POST /auth/login",
|
|
"description": "Pour les utilisateurs finaux",
|
|
},
|
|
{
|
|
"type": "API Key",
|
|
"header": "X-API-Key: sdk_live_xxx",
|
|
"manage_keys": "GET /api-keys",
|
|
"description": "Pour les intégrations externes",
|
|
},
|
|
],
|
|
"note": "Les routes acceptent JWT OU API Key (au choix)",
|
|
},
|
|
"swagger_access": {
|
|
"authentication": "HTTP Basic Auth (voir /scripts/manage_security.py)",
|
|
"filtering": "Les routes visibles dépendent des tags autorisés de l'utilisateur",
|
|
},
|
|
}
|
|
|
|
|
|
@app.get("/health", tags=["System"])
|
|
async def health_check():
|
|
"""
|
|
Vérification de santé de l'API (sans authentification)
|
|
"""
|
|
return {
|
|
"status": "healthy",
|
|
"timestamp": "2025-01-21T00:00:00Z",
|
|
"services": {
|
|
"api": "operational",
|
|
"database": "connected",
|
|
"email_queue": {
|
|
"running": email_queue.running,
|
|
"workers": len(email_queue.workers)
|
|
if hasattr(email_queue, "workers")
|
|
else 0,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
@app.get("/admin/queue/status", tags=["Admin"])
|
|
async def statut_queue():
|
|
return {
|
|
"queue_size": email_queue.queue.qsize(),
|
|
"workers": len(email_queue.workers),
|
|
"running": email_queue.running,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run(
|
|
"api:app",
|
|
host=settings.api_host,
|
|
port=settings.api_port,
|
|
reload=settings.api_reload,
|
|
)
|