from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse, HTMLResponse, Response from fastapi.encoders import jsonable_encoder 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, ApiKeyMiddleware 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): 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", version="3.0.0", description="Configuration multi-tenant des connexions Sage Gateway", lifespan=lifespan, openapi_tags=TAGS_METADATA, ) """ app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_methods=["GET", "POST", "PUT", "DELETE"], allow_headers=["*"], allow_credentials=True, ) """ def custom_openapi(): if app.openapi_schema: return app.openapi_schema openapi_schema = app.openapi() # Définir deux schémas de sécurité openapi_schema["components"]["securitySchemes"] = { "HTTPBearer": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}, "ApiKeyAuth": {"type": "apiKey", "in": "header", "name": "X-API-Key"}, } openapi_schema["security"] = [{"HTTPBearer": []}, {"ApiKeyAuth": []}] app.openapi_schema = openapi_schema return app.openapi_schema """ # Après app = FastAPI(...), ajouter: app.openapi = custom_openapi """ setup_cors(app, mode="open") app.add_middleware(SwaggerAuthMiddleware) app.add_middleware(ApiKeyMiddleware) 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), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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(...), user: User = Depends(get_current_user), 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"), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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" ), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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"), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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é" ), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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": """

Bonjour {{CT_Intitule}},

La facture {{DO_Piece}} du {{DO_Date}} d'un montant de {{DO_TotalTTC}}€ TTC reste impayée.

Merci de régulariser dans les meilleurs délais.

Cordialement,

""", "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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), ): 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, user: User = Depends(get_current_user), ): 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, user: User = Depends(get_current_user), ): 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, user: User = Depends(get_current_user), ): 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, user: User = Depends(get_current_user), ): 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, user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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é"), user: User = Depends(get_current_user), 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)"), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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)"), user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), ): 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, user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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é"), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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" ), user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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, user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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 "

Société introuvable

" logo_html = "" if societe.get("logo_base64"): logo_html = f'' else: logo_html = "

Aucun logo disponible

" html = f""" Informations Société

Informations Société

Raison sociale: {societe["raison_sociale"]}
SIRET: {societe["siret"] or "N/A"}
Adresse: {societe["adresse"] or "N/A"}
Code postal: {societe["code_postal"] or "N/A"}
Ville: {societe["ville"] or "N/A"}
Email: {societe["email"] or "N/A"}
Téléphone: {societe["telephone"] or "N/A"}
""" return html except Exception as e: return f"

Erreur

{str(e)}

" @app.post("/factures/{numero_facture}/valider", status_code=200, tags=["Factures"]) async def valider_facture( numero_facture: str, _: AsyncSession = Depends(get_session), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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), user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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", ), user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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( user: User = Depends(get_current_user), 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(): return { "api": "Sage 100c Dataven - VPS Linux", "version": "3.0.0", "documentation": "/docs (authentification requise)", "health": "/health", "authentication": { "methods": [ { "type": "JWT", "header": "Authorization: Bearer ", "endpoint": "/api/auth/login", }, { "type": "API Key", "header": "X-API-Key: sdk_live_xxx", "endpoint": "/api/api-keys", }, ] }, } @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, )