From e95e550044330a8cf41f53b486c601a1327f7494 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Mon, 8 Dec 2025 17:57:41 +0300 Subject: [PATCH] style: Reformat method calls and remove unnecessary blank lines for improved code consistency. --- api.py | 894 ++++++++++++++++++++++++------------------------- sage_client.py | 164 ++++----- 2 files changed, 519 insertions(+), 539 deletions(-) diff --git a/api.py b/api.py index 778a4c2..c5ec086 100644 --- a/api.py +++ b/api.py @@ -45,70 +45,38 @@ from sage_client import sage_client TAGS_METADATA = [ { "name": "Clients", - "description": "Gestion des clients (recherche, création, modification)" - }, - { - "name": "Articles", - "description": "Gestion des articles et produits" - }, - { - "name": "Devis", - "description": "Création, consultation et gestion des devis" + "description": "Gestion des clients (recherche, création, modification)", }, + {"name": "Articles", "description": "Gestion des articles et produits"}, + {"name": "Devis", "description": "Création, consultation et gestion des devis"}, { "name": "Commandes", - "description": "Création, consultation et gestion des commandes" + "description": "Création, consultation et gestion des commandes", }, { "name": "Livraisons", - "description": "Création, consultation et gestion des bons de livraison" + "description": "Création, consultation et gestion des bons de livraison", }, { "name": "Factures", - "description": "Création, consultation et gestion des factures" - }, - { - "name": "Avoirs", - "description": "Création, consultation et gestion des avoirs" - }, - { - "name": "Fournisseurs", - "description": "Gestion des fournisseurs" - }, - { - "name": "Prospects", - "description": "Gestion des prospects" + "description": "Création, consultation et gestion des factures", }, + {"name": "Avoirs", "description": "Création, consultation et gestion des avoirs"}, + {"name": "Fournisseurs", "description": "Gestion des fournisseurs"}, + {"name": "Prospects", "description": "Gestion des prospects"}, { "name": "Workflows", - "description": "Transformations de documents (devis→commande, commande→facture, etc.)" + "description": "Transformations de documents (devis→commande, commande→facture, etc.)", }, - { - "name": "Signatures", - "description": "Signature électronique via Universign" - }, - { - "name": "Emails", - "description": "Envoi d'emails, templates et logs d'envoi" - }, - { - "name": "Validation", - "description": "Validation de données (remises, etc.)" - }, - { - "name": "Admin", - "description": "🔧 Administration système (cache, queue)" - }, - { - "name": "System", - "description": "🏥 Health checks et informations système" - }, - { - "name": "Debug", - "description": "🐛 Routes de debug et diagnostics" - } + {"name": "Signatures", "description": "Signature électronique via Universign"}, + {"name": "Emails", "description": "Envoi d'emails, templates et logs d'envoi"}, + {"name": "Validation", "description": "Validation de données (remises, etc.)"}, + {"name": "Admin", "description": "🔧 Administration système (cache, queue)"}, + {"name": "System", "description": "🏥 Health checks et informations système"}, + {"name": "Debug", "description": "🐛 Routes de debug et diagnostics"}, ] + # ===================================================== # ENUMS # ===================================================== @@ -164,7 +132,7 @@ class LigneDevis(BaseModel): quantite: float prix_unitaire_ht: Optional[float] = None remise_pourcentage: Optional[float] = 0.0 - + @field_validator("article_code", mode="before") def strip_insecables(cls, v): return v.replace("\xa0", "").strip() @@ -227,10 +195,11 @@ class ClientCreateAPIRequest(BaseModel): telephone: Optional[str] = None siret: Optional[str] = None tva_intra: Optional[str] = None - + class ClientUpdateRequest(BaseModel): """Modèle pour modification d'un client existant""" + intitule: Optional[str] = Field(None, min_length=1, max_length=69) adresse: Optional[str] = Field(None, max_length=35) code_postal: Optional[str] = Field(None, max_length=9) @@ -240,7 +209,7 @@ class ClientUpdateRequest(BaseModel): telephone: Optional[str] = Field(None, max_length=21) siret: Optional[str] = Field(None, max_length=14) tva_intra: Optional[str] = Field(None, max_length=25) - + class Config: json_schema_extra = { "example": { @@ -249,10 +218,10 @@ class ClientUpdateRequest(BaseModel): "code_postal": "75008", "ville": "Paris", "email": "nouveau@email.fr", - "telephone": "0198765432" + "telephone": "0198765432", } } - + from pydantic import BaseModel from typing import List, Optional @@ -282,9 +251,15 @@ class UserResponse(BaseModel): class FournisseurCreateAPIRequest(BaseModel): - intitule: str = Field(..., min_length=1, max_length=69, description="Raison sociale du fournisseur") - compte_collectif: str = Field("401000", description="Compte comptable fournisseur (ex: 401000)") - num: Optional[str] = Field(None, max_length=17, description="Code fournisseur souhaité (optionnel)") + intitule: str = Field( + ..., min_length=1, max_length=69, description="Raison sociale du fournisseur" + ) + compte_collectif: str = Field( + "401000", description="Compte comptable fournisseur (ex: 401000)" + ) + num: Optional[str] = Field( + None, max_length=17, description="Code fournisseur souhaité (optionnel)" + ) adresse: Optional[str] = Field(None, max_length=35) code_postal: Optional[str] = Field(None, max_length=9) ville: Optional[str] = Field(None, max_length=35) @@ -293,7 +268,7 @@ class FournisseurCreateAPIRequest(BaseModel): telephone: Optional[str] = Field(None, max_length=21) siret: Optional[str] = Field(None, max_length=14) tva_intra: Optional[str] = Field(None, max_length=25) - + class Config: json_schema_extra = { "example": { @@ -307,12 +282,14 @@ class FournisseurCreateAPIRequest(BaseModel): "email": "contact@acmesupplies.fr", "telephone": "0145678901", "siret": "12345678901234", - "tva_intra": "FR12345678901" + "tva_intra": "FR12345678901", } } - + + class FournisseurUpdateRequest(BaseModel): """Modèle pour modification d'un fournisseur existant""" + intitule: Optional[str] = Field(None, min_length=1, max_length=69) adresse: Optional[str] = Field(None, max_length=35) code_postal: Optional[str] = Field(None, max_length=9) @@ -322,22 +299,24 @@ class FournisseurUpdateRequest(BaseModel): telephone: Optional[str] = Field(None, max_length=21) siret: Optional[str] = Field(None, max_length=14) tva_intra: Optional[str] = Field(None, max_length=25) - + class Config: json_schema_extra = { "example": { "intitule": "ACME SUPPLIES MODIFIÉ", "email": "nouveau@acme.fr", - "telephone": "0198765432" + "telephone": "0198765432", } } - + + class DevisUpdateRequest(BaseModel): """Modèle pour modification d'un devis existant""" + date_devis: Optional[date] = None lignes: Optional[List[LigneDevis]] = None statut: Optional[int] = Field(None, ge=0, le=6) - + class Config: json_schema_extra = { "example": { @@ -347,21 +326,22 @@ class DevisUpdateRequest(BaseModel): "article_code": "ART001", "quantite": 5.0, "prix_unitaire_ht": 100.0, - "remise_pourcentage": 10.0 + "remise_pourcentage": 10.0, } ], - "statut": 2 + "statut": 2, } } class LigneCommande(BaseModel): """Ligne de commande""" + article_code: str quantite: float prix_unitaire_ht: Optional[float] = None remise_pourcentage: Optional[float] = 0.0 - + @field_validator("article_code", mode="before") def strip_insecables(cls, v): return v.replace("\xa0", "").strip() @@ -369,11 +349,12 @@ class LigneCommande(BaseModel): class CommandeCreateRequest(BaseModel): """Création d'une commande""" + client_id: str date_commande: Optional[date] = None lignes: List[LigneCommande] reference: Optional[str] = None # Référence externe - + class Config: json_schema_extra = { "example": { @@ -385,20 +366,21 @@ class CommandeCreateRequest(BaseModel): "article_code": "ART001", "quantite": 10.0, "prix_unitaire_ht": 50.0, - "remise_pourcentage": 5.0 + "remise_pourcentage": 5.0, } - ] + ], } } class CommandeUpdateRequest(BaseModel): """Modification d'une commande existante""" + date_commande: Optional[date] = None lignes: Optional[List[LigneCommande]] = None statut: Optional[int] = Field(None, ge=0, le=6) reference: Optional[str] = None - + class Config: json_schema_extra = { "example": { @@ -406,21 +388,22 @@ class CommandeUpdateRequest(BaseModel): { "article_code": "ART001", "quantite": 15.0, - "prix_unitaire_ht": 45.0 + "prix_unitaire_ht": 45.0, } ], - "statut": 2 + "statut": 2, } } class LigneLivraison(BaseModel): """Ligne de livraison""" + article_code: str quantite: float prix_unitaire_ht: Optional[float] = None remise_pourcentage: Optional[float] = 0.0 - + @field_validator("article_code", mode="before") def strip_insecables(cls, v): return v.replace("\xa0", "").strip() @@ -428,11 +411,12 @@ class LigneLivraison(BaseModel): class LivraisonCreateRequest(BaseModel): """Création d'une livraison""" + client_id: str date_livraison: Optional[date] = None lignes: List[LigneLivraison] reference: Optional[str] = None - + class Config: json_schema_extra = { "example": { @@ -444,20 +428,21 @@ class LivraisonCreateRequest(BaseModel): "article_code": "ART001", "quantite": 10.0, "prix_unitaire_ht": 50.0, - "remise_pourcentage": 5.0 + "remise_pourcentage": 5.0, } - ] + ], } } class LivraisonUpdateRequest(BaseModel): """Modification d'une livraison existante""" + date_livraison: Optional[date] = None lignes: Optional[List[LigneLivraison]] = None statut: Optional[int] = Field(None, ge=0, le=6) reference: Optional[str] = None - + class Config: json_schema_extra = { "example": { @@ -465,20 +450,22 @@ class LivraisonUpdateRequest(BaseModel): { "article_code": "ART001", "quantite": 15.0, - "prix_unitaire_ht": 45.0 + "prix_unitaire_ht": 45.0, } ], - "statut": 2 + "statut": 2, } } + class LigneAvoir(BaseModel): """Ligne d'avoir""" + article_code: str quantite: float prix_unitaire_ht: Optional[float] = None remise_pourcentage: Optional[float] = 0.0 - + @field_validator("article_code", mode="before") def strip_insecables(cls, v): return v.replace("\xa0", "").strip() @@ -486,11 +473,12 @@ class LigneAvoir(BaseModel): class AvoirCreateRequest(BaseModel): """Création d'un avoir""" + client_id: str date_avoir: Optional[date] = None lignes: List[LigneAvoir] reference: Optional[str] = None - + class Config: json_schema_extra = { "example": { @@ -502,20 +490,21 @@ class AvoirCreateRequest(BaseModel): "article_code": "ART001", "quantite": 5.0, "prix_unitaire_ht": 50.0, - "remise_pourcentage": 0.0 + "remise_pourcentage": 0.0, } - ] + ], } } class AvoirUpdateRequest(BaseModel): """Modification d'un avoir existant""" + date_avoir: Optional[date] = None lignes: Optional[List[LigneAvoir]] = None statut: Optional[int] = Field(None, ge=0, le=6) reference: Optional[str] = None - + class Config: json_schema_extra = { "example": { @@ -523,20 +512,22 @@ class AvoirUpdateRequest(BaseModel): { "article_code": "ART001", "quantite": 10.0, - "prix_unitaire_ht": 45.0 + "prix_unitaire_ht": 45.0, } ], - "statut": 2 + "statut": 2, } } - + + class LigneFacture(BaseModel): """Ligne de facture""" + article_code: str quantite: float prix_unitaire_ht: Optional[float] = None remise_pourcentage: Optional[float] = 0.0 - + @field_validator("article_code", mode="before") def strip_insecables(cls, v): return v.replace("\xa0", "").strip() @@ -544,11 +535,12 @@ class LigneFacture(BaseModel): class FactureCreateRequest(BaseModel): """Création d'une facture""" + client_id: str date_facture: Optional[date] = None lignes: List[LigneFacture] reference: Optional[str] = None - + class Config: json_schema_extra = { "example": { @@ -560,20 +552,21 @@ class FactureCreateRequest(BaseModel): "article_code": "ART001", "quantite": 10.0, "prix_unitaire_ht": 50.0, - "remise_pourcentage": 5.0 + "remise_pourcentage": 5.0, } - ] + ], } } class FactureUpdateRequest(BaseModel): """Modification d'une facture existante""" + date_facture: Optional[date] = None lignes: Optional[List[LigneFacture]] = None statut: Optional[int] = Field(None, ge=0, le=6) reference: Optional[str] = None - + class Config: json_schema_extra = { "example": { @@ -581,14 +574,14 @@ class FactureUpdateRequest(BaseModel): { "article_code": "ART001", "quantite": 15.0, - "prix_unitaire_ht": 45.0 + "prix_unitaire_ht": 45.0, } ], - "statut": 2 + "statut": 2, } } - - + + # ===================================================== # SERVICES EXTERNES (Universign) # ===================================================== @@ -741,7 +734,7 @@ app = FastAPI( version="2.0.0", description="API de gestion commerciale - VPS Linux", lifespan=lifespan, - openapi_tags=TAGS_METADATA + openapi_tags=TAGS_METADATA, ) app.add_middleware( @@ -769,28 +762,26 @@ async def rechercher_clients(query: Optional[str] = Query(None)): logger.error(f"Erreur recherche clients: {e}") raise HTTPException(500, str(e)) + @app.get("/clients/{code}", tags=["Clients"]) async def lire_client_detail(code: str): """ 📄 Lecture détaillée d'un client par son code - + Args: code: Code du client (ex: "CLI000001", "SARL", etc.) - + Returns: Toutes les informations du client """ try: client = sage_client.lire_client(code) - + if not client: raise HTTPException(404, f"Client {code} introuvable") - - return { - "success": True, - "data": client - } - + + return {"success": True, "data": client} + except HTTPException: raise except Exception as e: @@ -802,18 +793,18 @@ async def lire_client_detail(code: str): async def modifier_client( code: str, client_update: ClientUpdateRequest, - session: AsyncSession = Depends(get_session) + session: AsyncSession = Depends(get_session), ): """ ✏️ Modification d'un client existant - + Args: code: Code du client à modifier client_update: Champs à mettre à jour (seuls les champs fournis seront modifiés) - + Returns: Client modifié avec ses nouvelles valeurs - + Example: PUT /clients/SARL { @@ -823,16 +814,18 @@ async def modifier_client( """ try: # Appel à la gateway Windows - resultat = sage_client.modifier_client(code, client_update.dict(exclude_none=True)) - + resultat = sage_client.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 + "client": resultat, } - + except ValueError as e: # Erreur métier (client introuvable, etc.) logger.warning(f"Erreur métier modification client {code}: {e}") @@ -841,32 +834,33 @@ async def modifier_client( # Erreur technique 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: ClientCreateAPIRequest, - session: AsyncSession = Depends(get_session) + client: ClientCreateAPIRequest, session: AsyncSession = Depends(get_session) ): """ ➕ Création d'un nouveau client dans Sage 100c """ try: nouveau_client = sage_client.creer_client(client.dict()) - + logger.info(f"✅ Client créé via API: {nouveau_client.get('numero')}") - + return { "success": True, "message": "Client créé avec succès", - "data": nouveau_client + "data": nouveau_client, } - + except Exception as e: logger.error(f"Erreur lors de la création du client: {e}") # On renvoie une 400 si c'est une erreur métier (ex: doublon), sinon 500 status = 400 if "existe déjà" in str(e) else 500 raise HTTPException(status, str(e)) + @app.get("/articles", response_model=List[ArticleResponse], tags=["Articles"]) async def rechercher_articles(query: Optional[str] = Query(None)): """🔍 Recherche articles via gateway Windows""" @@ -920,25 +914,25 @@ async def creer_devis(devis: DevisRequest): async def modifier_devis( id: str, devis_update: DevisUpdateRequest, - session: AsyncSession = Depends(get_session) + session: AsyncSession = Depends(get_session), ): """ ✏️ Modification d'un devis existant - + **Champs modifiables:** - `date_devis`: Nouvelle date du devis - `lignes`: Nouvelles lignes (remplace toutes les lignes existantes) - `statut`: Nouveau statut (0=Brouillon, 2=Accepté, 5=Transformé, 6=Annulé) - + **Note importante:** - Si `lignes` est fourni, TOUTES les lignes existantes seront remplacées - Pour ajouter/supprimer une ligne, il faut envoyer TOUTES les lignes - Un devis transformé (statut=5) ne peut plus être modifié - + Args: id: Numéro du devis à modifier devis_update: Champs à mettre à jour - + Returns: Devis modifié avec ses nouvelles valeurs """ @@ -947,20 +941,19 @@ async def modifier_devis( devis_existant = sage_client.lire_devis(id) if not devis_existant: raise HTTPException(404, f"Devis {id} introuvable") - + # Vérifier qu'il n'est pas déjà transformé if devis_existant.get("statut") == 5: raise HTTPException( - 400, - f"Le devis {id} a déjà été transformé et ne peut plus être modifié" + 400, f"Le devis {id} a déjà été transformé et ne peut plus être modifié" ) - + # Construire les données de mise à jour 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"] = [ { @@ -971,51 +964,50 @@ async def modifier_devis( } for l in devis_update.lignes ] - + if devis_update.statut is not None: update_data["statut"] = devis_update.statut - + # Appel à la gateway Windows resultat = sage_client.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 + "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: CommandeCreateRequest, - session: AsyncSession = Depends(get_session) + commande: CommandeCreateRequest, session: AsyncSession = Depends(get_session) ): """ ➕ Création d'une nouvelle commande (Bon de commande) - + **Workflow typique:** 1. Création d'un devis → transformation en commande (automatique) 2. OU création directe d'une commande (cette route) - + **Champs obligatoires:** - `client_id`: Code du client - `lignes`: Liste des lignes (min 1) - + **Champs optionnels:** - `date_commande`: Date de la commande (par défaut: aujourd'hui) - `reference`: Référence externe (ex: numéro de commande client) - + Args: commande: Données de la commande à créer - + Returns: Commande créée avec son numéro et ses totaux """ @@ -1024,14 +1016,12 @@ async def creer_commande( client = sage_client.lire_client(commande.client_id) if not client: raise HTTPException(404, f"Client {commande.client_id} introuvable") - + # Préparer les données pour la gateway commande_data = { "client_id": commande.client_id, "date_commande": ( - commande.date_commande.isoformat() - if commande.date_commande - else None + commande.date_commande.isoformat() if commande.date_commande else None ), "reference": commande.reference, "lignes": [ @@ -1044,12 +1034,12 @@ async def creer_commande( for l in commande.lignes ], } - + # Appel à la gateway Windows resultat = sage_client.creer_commande(commande_data) - + logger.info(f"✅ Commande créée: {resultat.get('numero_commande')}") - + return { "success": True, "message": "Commande créée avec succès", @@ -1060,10 +1050,10 @@ async def creer_commande( "total_ht": resultat["total_ht"], "total_ttc": resultat["total_ttc"], "nb_lignes": resultat["nb_lignes"], - "reference": commande.reference - } + "reference": commande.reference, + }, } - + except HTTPException: raise except Exception as e: @@ -1075,62 +1065,60 @@ async def creer_commande( async def modifier_commande( id: str, commande_update: CommandeUpdateRequest, - session: AsyncSession = Depends(get_session) + session: AsyncSession = Depends(get_session), ): """ ✏️ Modification d'une commande existante - + **Champs modifiables:** - `date_commande`: Nouvelle date - `lignes`: Nouvelles lignes (remplace toutes les lignes existantes) - `statut`: Nouveau statut - `reference`: Référence externe - + **Restrictions:** - Une commande transformée (statut=5) ne peut plus être modifiée - Une commande annulée (statut=6) ne peut plus être modifiée - + **Note importante:** Si `lignes` est fourni, TOUTES les lignes existantes seront remplacées - + Args: id: Numéro de la commande à modifier commande_update: Champs à mettre à jour - + Returns: Commande modifiée avec ses nouvelles valeurs """ try: # Vérifier que la commande existe commande_existante = sage_client.lire_document( - id, - settings.SAGE_TYPE_BON_COMMANDE + id, settings.SAGE_TYPE_BON_COMMANDE ) - + if not commande_existante: raise HTTPException(404, f"Commande {id} introuvable") - + # Vérifier le statut statut_actuel = commande_existante.get("statut", 0) - + if statut_actuel == 5: raise HTTPException( 400, - f"La commande {id} a déjà été transformée et ne peut plus être modifiée" + f"La commande {id} a déjà été transformée et ne peut plus être modifiée", ) - + if statut_actuel == 6: raise HTTPException( - 400, - f"La commande {id} est annulée et ne peut plus être modifiée" + 400, f"La commande {id} est annulée et ne peut plus être modifiée" ) - + # Construire les données de mise à jour 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"] = [ { @@ -1141,30 +1129,30 @@ async def modifier_commande( } for l 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 - + # Appel à la gateway Windows resultat = sage_client.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 + "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( @@ -1199,22 +1187,22 @@ async def lister_devis( async def lire_devis(id: str): """ 📄 Lecture d'un devis via gateway Windows - + Returns: Devis complet avec: - Toutes les informations standards - lignes: Lignes du devis - a_deja_ete_transforme: ✅ Booléen indiquant si le devis a été transformé - documents_cibles: ✅ Liste des documents créés depuis ce devis - + ✅ ENRICHI: Inclut maintenant l'information de transformation """ try: devis = sage_client.lire_devis(id) - + if not devis: raise HTTPException(404, f"Devis {id} introuvable") - + # Log informatif if devis.get("a_deja_ete_transforme"): docs = devis.get("documents_cibles", []) @@ -1222,12 +1210,9 @@ async def lire_devis(id: str): f"📊 Devis {id} a été transformé en " f"{len(docs)} document(s): {[d['numero'] for d in docs]}" ) - - return { - "success": True, - "data": devis - } - + + return {"success": True, "data": devis} + except HTTPException: raise except Exception as e: @@ -1252,33 +1237,37 @@ async def telecharger_devis_pdf(id: str): 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") + 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"), ): """ 📄 Téléchargement PDF d'un document (route généralisée) - + **Types de documents supportés:** - `0`: Devis - `10`: Bon de commande - `30`: Bon de livraison - `60`: Facture - `50`: Bon d'avoir - + **Exemple d'utilisation:** - `GET /documents/0/DE00001/pdf` → PDF du devis DE00001 - `GET /documents/60/FA00001/pdf` → PDF de la facture FA00001 - + **Retour:** - Fichier PDF prêt à télécharger - Nom de fichier formaté selon le type de document - + Args: type_doc: Type de document Sage (0-60) numero: Numéro du document - + Returns: StreamingResponse avec le PDF """ @@ -1291,50 +1280,50 @@ async def telecharger_document_pdf( 30: "BonLivraison", 40: "BonRetour", 50: "Avoir", - 60: "Facture" + 60: "Facture", } - + # Vérifier que le type est valide if type_doc not in types_labels: raise HTTPException( - 400, + 400, f"Type de document invalide: {type_doc}. " - f"Types valides: {list(types_labels.keys())}" + f"Types valides: {list(types_labels.keys())}", ) - + label = types_labels[type_doc] - + logger.info(f"📄 Génération PDF: {label} {numero} (type={type_doc})") - + # Appel à sage_client pour générer le PDF pdf_bytes = sage_client.generer_pdf_document(numero, type_doc) - + if not pdf_bytes: - raise HTTPException( - 500, - f"Le PDF du document {numero} est vide" - ) - + raise HTTPException(500, f"Le PDF du document {numero} est vide") + logger.info(f"✅ PDF généré: {len(pdf_bytes)} octets") - + # Nom de fichier formaté 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)) - } + "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) + 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: EmailEnvoiRequest, session: AsyncSession = Depends(get_session) @@ -1391,26 +1380,28 @@ async def envoyer_devis_email( @app.put("/devis/{id}/statut", tags=["Devis"]) async def changer_statut_devis( - id: str, - nouveau_statut: int = Query(..., ge=0, le=6, description="0=Brouillon, 2=Accepté, 5=Transformé, 6=Annulé") + id: str, + nouveau_statut: int = Query( + ..., ge=0, le=6, description="0=Brouillon, 2=Accepté, 5=Transformé, 6=Annulé" + ), ): """ 📊 Changement de statut d'un devis - + **Statuts possibles:** - 0: Brouillon - 2: Accepté/Validé - 5: Transformé (automatique lors d'une transformation) - 6: Annulé - + **Restrictions:** - Un devis transformé (5) ne peut plus changer de statut - Un devis annulé (6) ne peut plus changer de statut - + Args: id: Numéro du devis nouveau_statut: Nouveau statut (0-6) - + Returns: Confirmation du changement avec ancien et nouveau statut """ @@ -1419,34 +1410,33 @@ async def changer_statut_devis( devis_existant = sage_client.lire_devis(id) if not devis_existant: raise HTTPException(404, f"Devis {id} introuvable") - + statut_actuel = devis_existant.get("statut", 0) - + # Vérifications de cohérence if statut_actuel == 5: raise HTTPException( 400, - f"Le devis {id} a déjà été transformé et ne peut plus changer de statut" + f"Le devis {id} a déjà été transformé et ne peut plus changer de statut", ) - + if statut_actuel == 6: raise HTTPException( - 400, - f"Le devis {id} est annulé et ne peut plus changer de statut" + 400, f"Le devis {id} est annulé et ne peut plus changer de statut" ) - + resultat = sage_client.changer_statut_devis(id, nouveau_statut) - + logger.info(f"✅ Statut devis {id} changé: {statut_actuel} → {nouveau_statut}") - + return { "success": True, "devis_id": id, "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}" + "message": f"Statut mis à jour: {statut_actuel} → {nouveau_statut}", } - + except HTTPException: raise except Exception as e: @@ -1458,6 +1448,7 @@ async def changer_statut_devis( # ENDPOINTS - Commandes (WORKFLOW SANS RESSAISIE) # ===================================================== + @app.get("/commandes/{id}", tags=["Commandes"]) async def lire_commande(id: str): """📄 Lecture d'une commande avec ses lignes""" @@ -1471,7 +1462,7 @@ async def lire_commande(id: str): 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( @@ -1512,7 +1503,9 @@ async def devis_vers_commande(id: str, session: AsyncSession = Depends(get_sessi sage_client.changer_statut_devis(id, nouveau_statut=5) logger.info(f"✅ Statut devis {id} mis à jour: 5 (Transformé)") except Exception as e: - logger.warning(f"⚠️ Impossible de mettre à jour le statut du devis {id}: {e}") + logger.warning( + f"⚠️ Impossible de mettre à jour le statut du devis {id}: {e}" + ) # On continue même si la MAJ statut échoue # Étape 3: Logger la transformation @@ -1938,7 +1931,9 @@ async def envoyer_emails_lot( # ===================================================== -@app.post("/devis/valider-remise", response_model=BaremeRemiseResponse, tags=["Validation"]) +@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), @@ -2101,62 +2096,60 @@ async def lister_factures( async def lire_facture_detail(numero: str): """ 📄 Lecture détaillée d'une facture avec ses lignes - + Args: numero: Numéro de la facture (ex: "FA000001") - + Returns: Facture complète avec lignes, client, totaux, etc. """ try: facture = sage_client.lire_document(numero, TypeDocument.FACTURE) - + if not facture: raise HTTPException(404, f"Facture {numero} introuvable") - - return { - "success": True, - "data": facture - } - + + 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 RelanceFactureRequest(BaseModel): doc_id: str message_personnalise: Optional[str] = None + @app.post("/factures", status_code=201, tags=["Factures"]) async def creer_facture( - facture: FactureCreateRequest, - session: AsyncSession = Depends(get_session) + facture: FactureCreateRequest, session: AsyncSession = Depends(get_session) ): """ ➕ Création d'une facture - + **Workflow typique:** 1. Commande → Livraison → Facture (transformations successives) 2. OU création directe d'une facture (cette route) - + **Champs obligatoires:** - `client_id`: Code du client - `lignes`: Liste des lignes (min 1) - + **Champs optionnels:** - `date_facture`: Date de la facture (par défaut: aujourd'hui) - `reference`: Référence externe (ex: numéro de commande client) - + **Notes importantes:** - Les factures peuvent avoir des champs obligatoires supplémentaires selon la configuration Sage - Le statut initial est généralement 2 (Accepté/Validé) - Les factures sont soumises aux règles de numérotation strictes - + Args: facture: Données de la facture à créer - + Returns: Facture créée avec son numéro et ses totaux """ @@ -2165,14 +2158,12 @@ async def creer_facture( client = sage_client.lire_client(facture.client_id) if not client: raise HTTPException(404, f"Client {facture.client_id} introuvable") - + # Préparer les données pour la gateway facture_data = { "client_id": facture.client_id, "date_facture": ( - facture.date_facture.isoformat() - if facture.date_facture - else None + facture.date_facture.isoformat() if facture.date_facture else None ), "reference": facture.reference, "lignes": [ @@ -2185,12 +2176,12 @@ async def creer_facture( for l in facture.lignes ], } - + # Appel à la gateway Windows resultat = sage_client.creer_facture(facture_data) - + logger.info(f"✅ Facture créée: {resultat.get('numero_facture')}") - + return { "success": True, "message": "Facture créée avec succès", @@ -2201,10 +2192,10 @@ async def creer_facture( "total_ht": resultat["total_ht"], "total_ttc": resultat["total_ttc"], "nb_lignes": resultat["nb_lignes"], - "reference": facture.reference - } + "reference": facture.reference, + }, } - + except HTTPException: raise except Exception as e: @@ -2216,64 +2207,60 @@ async def creer_facture( async def modifier_facture( id: str, facture_update: FactureUpdateRequest, - session: AsyncSession = Depends(get_session) + session: AsyncSession = Depends(get_session), ): """ ✏️ Modification d'une facture existante - + **Champs modifiables:** - `date_facture`: Nouvelle date - `lignes`: Nouvelles lignes (remplace toutes les lignes existantes) - `statut`: Nouveau statut - `reference`: Référence externe - + **Restrictions IMPORTANTES:** - Une facture transformée (statut=5) ne peut plus être modifiée - Une facture annulée (statut=6) ne peut plus être modifiée - ⚠️ **ATTENTION**: Les factures comptabilisées peuvent être verrouillées par Sage - Certaines factures peuvent être en lecture seule selon les droits utilisateur - + **Note importante:** Si `lignes` est fourni, TOUTES les lignes existantes seront remplacées - + Args: id: Numéro de la facture à modifier facture_update: Champs à mettre à jour - + Returns: Facture modifiée avec ses nouvelles valeurs """ try: # Vérifier que la facture existe - facture_existante = sage_client.lire_document( - id, - settings.SAGE_TYPE_FACTURE - ) - + facture_existante = sage_client.lire_document(id, settings.SAGE_TYPE_FACTURE) + if not facture_existante: raise HTTPException(404, f"Facture {id} introuvable") - + # Vérifier le statut statut_actuel = facture_existante.get("statut", 0) - + if statut_actuel == 5: raise HTTPException( 400, - f"La facture {id} a déjà été transformée et ne peut plus être modifiée" + f"La facture {id} a déjà été transformée et ne peut plus être modifiée", ) - + if statut_actuel == 6: raise HTTPException( - 400, - f"La facture {id} est annulée et ne peut plus être modifiée" + 400, f"La facture {id} est annulée et ne peut plus être modifiée" ) - + # Construire les données de mise à jour 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"] = [ { @@ -2284,30 +2271,30 @@ async def modifier_facture( } for l 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 - + # Appel à la gateway Windows resultat = sage_client.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 + "facture": resultat, } - + except HTTPException: raise except Exception as e: logger.error(f"Erreur modification facture {id}: {e}") raise HTTPException(500, str(e)) - + # Templates email (si pas déjà définis) templates_email_db = { @@ -2772,39 +2759,40 @@ async def rechercher_fournisseurs(query: Optional[str] = Query(None)): try: # ✅ APPEL DIRECT vers Windows (pas de cache) fournisseurs = sage_client.lister_fournisseurs(filtre=query or "") - + logger.info(f"✅ {len(fournisseurs)} fournisseurs retournés depuis Windows") - + if len(fournisseurs) == 0: logger.warning("⚠️ Aucun fournisseur retourné - vérifier la gateway Windows") - + return 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: FournisseurCreateAPIRequest, - session: AsyncSession = Depends(get_session) + session: AsyncSession = Depends(get_session), ): """ ➕ Création d'un nouveau fournisseur dans Sage 100c - + **Champs obligatoires:** - `intitule`: Raison sociale (max 69 caractères) - + **Champs optionnels:** - `compte_collectif`: Compte comptable (défaut: 401000) - `num`: Code fournisseur personnalisé (auto-généré si vide) - `adresse`, `code_postal`, `ville`, `pays` - `email`, `telephone` - `siret`, `tva_intra` - + **Retour:** - Fournisseur créé avec son numéro définitif - + **Erreurs possibles:** - 400: Fournisseur existe déjà (doublon) - 500: Erreur technique Sage @@ -2812,41 +2800,42 @@ async def ajouter_fournisseur( try: # Appel à la gateway Windows via sage_client nouveau_fournisseur = sage_client.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 + "data": nouveau_fournisseur, } - + except ValueError as e: # Erreur métier (doublon, validation) logger.warning(f"⚠️ Erreur métier création fournisseur: {e}") raise HTTPException(400, str(e)) - + except Exception as e: # Erreur technique (COM, connexion) logger.error(f"❌ Erreur technique création fournisseur: {e}") raise HTTPException(500, str(e)) + @app.put("/fournisseurs/{code}", tags=["Fournisseurs"]) async def modifier_fournisseur( code: str, fournisseur_update: FournisseurUpdateRequest, - session: AsyncSession = Depends(get_session) + session: AsyncSession = Depends(get_session), ): """ ✏️ Modification d'un fournisseur existant - + Args: code: Code du fournisseur à modifier fournisseur_update: Champs à mettre à jour (seuls les champs fournis seront modifiés) - + Returns: Fournisseur modifié avec ses nouvelles valeurs - + Example: PUT /fournisseurs/DUPONT { @@ -2856,16 +2845,18 @@ async def modifier_fournisseur( """ try: # Appel à la gateway Windows - resultat = sage_client.modifier_fournisseur(code, fournisseur_update.dict(exclude_none=True)) - + resultat = sage_client.modifier_fournisseur( + code, fournisseur_update.dict(exclude_none=True) + ) + logger.info(f"✅ Fournisseur {code} modifié avec succès") - + return { "success": True, "message": f"Fournisseur {code} modifié avec succès", - "fournisseur": resultat + "fournisseur": resultat, } - + except ValueError as e: # Erreur métier (fournisseur introuvable, etc.) logger.warning(f"Erreur métier modification fournisseur {code}: {e}") @@ -2874,8 +2865,8 @@ async def modifier_fournisseur( # Erreur technique 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): """📄 Lecture d'un fournisseur par code""" @@ -2921,31 +2912,31 @@ async def lire_avoir(numero: str): 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: AvoirCreateRequest, - session: AsyncSession = Depends(get_session) + avoir: AvoirCreateRequest, session: AsyncSession = Depends(get_session) ): """ ➕ Création d'un avoir (Bon d'avoir) - + **Workflow typique:** 1. Retour marchandise → création d'un avoir 2. Geste commercial → création directe d'un avoir (cette route) - + **Champs obligatoires:** - `client_id`: Code du client - `lignes`: Liste des lignes (min 1) - + **Champs optionnels:** - `date_avoir`: Date de l'avoir (par défaut: aujourd'hui) - `reference`: Référence externe (ex: numéro de retour) - + **Note:** Les montants des avoirs sont généralement négatifs (crédits) - + Args: avoir: Données de l'avoir à créer - + Returns: Avoir créé avec son numéro et ses totaux """ @@ -2954,15 +2945,11 @@ async def creer_avoir( client = sage_client.lire_client(avoir.client_id) if not client: raise HTTPException(404, f"Client {avoir.client_id} introuvable") - + # Préparer les données pour la gateway avoir_data = { "client_id": avoir.client_id, - "date_avoir": ( - avoir.date_avoir.isoformat() - if avoir.date_avoir - else None - ), + "date_avoir": (avoir.date_avoir.isoformat() if avoir.date_avoir else None), "reference": avoir.reference, "lignes": [ { @@ -2974,12 +2961,12 @@ async def creer_avoir( for l in avoir.lignes ], } - + # Appel à la gateway Windows resultat = sage_client.creer_avoir(avoir_data) - + logger.info(f"✅ Avoir créé: {resultat.get('numero_avoir')}") - + return { "success": True, "message": "Avoir créé avec succès", @@ -2990,10 +2977,10 @@ async def creer_avoir( "total_ht": resultat["total_ht"], "total_ttc": resultat["total_ttc"], "nb_lignes": resultat["nb_lignes"], - "reference": avoir.reference - } + "reference": avoir.reference, + }, } - + except HTTPException: raise except Exception as e: @@ -3005,59 +2992,57 @@ async def creer_avoir( async def modifier_avoir( id: str, avoir_update: AvoirUpdateRequest, - session: AsyncSession = Depends(get_session) + session: AsyncSession = Depends(get_session), ): """ ✏️ Modification d'un avoir existant - + **Champs modifiables:** - `date_avoir`: Nouvelle date - `lignes`: Nouvelles lignes (remplace toutes les lignes existantes) - `statut`: Nouveau statut - `reference`: Référence externe - + **Restrictions:** - Un avoir transformé (statut=5) ne peut plus être modifié - Un avoir annulé (statut=6) ne peut plus être modifié - + **Note importante:** Si `lignes` est fourni, TOUTES les lignes existantes seront remplacées - + Args: id: Numéro de l'avoir à modifier avoir_update: Champs à mettre à jour - + Returns: Avoir modifié avec ses nouvelles valeurs """ try: # Vérifier que l'avoir existe avoir_existant = sage_client.lire_avoir(id) - + if not avoir_existant: raise HTTPException(404, f"Avoir {id} introuvable") - + # Vérifier le statut statut_actuel = avoir_existant.get("statut", 0) - + if statut_actuel == 5: raise HTTPException( - 400, - f"L'avoir {id} a déjà été transformé et ne peut plus être modifié" + 400, f"L'avoir {id} a déjà été transformé et ne peut plus être modifié" ) - + if statut_actuel == 6: raise HTTPException( - 400, - f"L'avoir {id} est annulé et ne peut plus être modifié" + 400, f"L'avoir {id} est annulé et ne peut plus être modifié" ) - + # Construire les données de mise à jour 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"] = [ { @@ -3068,31 +3053,31 @@ async def modifier_avoir( } for l 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 - + # Appel à la gateway Windows resultat = sage_client.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 + "avoir": resultat, } - + except HTTPException: raise except Exception as e: logger.error(f"Erreur modification avoir {id}: {e}") raise HTTPException(500, str(e)) - - + + # ===================================================== # ENDPOINTS - LIVRAISONS # ===================================================== @@ -3123,22 +3108,22 @@ async def lire_livraison(numero: str): 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: LivraisonCreateRequest, - session: AsyncSession = Depends(get_session) + livraison: LivraisonCreateRequest, session: AsyncSession = Depends(get_session) ): """ ➕ Création d'une nouvelle livraison (Bon de livraison) - + **Workflow typique:** 1. Création d'une commande → transformation en livraison (automatique) 2. OU création directe d'une livraison (cette route) - + **Champs obligatoires:** - `client_id`: Code du client - `lignes`: Liste des lignes (min 1) - + **Champs optionnels:** - `date_livraison`: Date de la livraison (par défaut: aujourd'hui) - `reference`: Référence externe (ex: numéro de commande client) @@ -3148,13 +3133,13 @@ async def creer_livraison( client = sage_client.lire_client(livraison.client_id) if not client: raise HTTPException(404, f"Client {livraison.client_id} introuvable") - + # Préparer les données pour la gateway livraison_data = { "client_id": livraison.client_id, "date_livraison": ( - livraison.date_livraison.isoformat() - if livraison.date_livraison + livraison.date_livraison.isoformat() + if livraison.date_livraison else None ), "reference": livraison.reference, @@ -3168,12 +3153,12 @@ async def creer_livraison( for l in livraison.lignes ], } - + # Appel à la gateway Windows resultat = sage_client.creer_livraison(livraison_data) - + logger.info(f"✅ Livraison créée: {resultat.get('numero_livraison')}") - + return { "success": True, "message": "Livraison créée avec succès", @@ -3184,10 +3169,10 @@ async def creer_livraison( "total_ht": resultat["total_ht"], "total_ttc": resultat["total_ttc"], "nb_lignes": resultat["nb_lignes"], - "reference": livraison.reference - } + "reference": livraison.reference, + }, } - + except HTTPException: raise except Exception as e: @@ -3199,17 +3184,17 @@ async def creer_livraison( async def modifier_livraison( id: str, livraison_update: LivraisonUpdateRequest, - session: AsyncSession = Depends(get_session) + session: AsyncSession = Depends(get_session), ): """ ✏️ Modification d'une livraison existante - + **Champs modifiables:** - `date_livraison`: Nouvelle date - `lignes`: Nouvelles lignes (remplace toutes les lignes existantes) - `statut`: Nouveau statut - `reference`: Référence externe - + **Restrictions:** - Une livraison transformée (statut=5) ne peut plus être modifiée - Une livraison annulée (statut=6) ne peut plus être modifiée @@ -3217,31 +3202,30 @@ async def modifier_livraison( try: # Vérifier que la livraison existe livraison_existante = sage_client.lire_livraison(id) - + if not livraison_existante: raise HTTPException(404, f"Livraison {id} introuvable") - + # Vérifier le statut statut_actuel = livraison_existante.get("statut", 0) - + if statut_actuel == 5: raise HTTPException( 400, - f"La livraison {id} a déjà été transformée et ne peut plus être modifiée" + f"La livraison {id} a déjà été transformée et ne peut plus être modifiée", ) - + if statut_actuel == 6: raise HTTPException( - 400, - f"La livraison {id} est annulée et ne peut plus être modifiée" + 400, f"La livraison {id} est annulée et ne peut plus être modifiée" ) - + # Construire les données de mise à jour 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"] = [ { @@ -3252,24 +3236,24 @@ async def modifier_livraison( } for l 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 - + # Appel à la gateway Windows resultat = sage_client.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 + "livraison": resultat, } - + except HTTPException: raise except Exception as e: @@ -3321,24 +3305,26 @@ async def livraison_vers_facture(id: str, session: AsyncSession = Depends(get_se @app.post("/workflow/devis/{id}/to-facture", tags=["Workflows"]) -async def devis_vers_facture_direct(id: str, session: AsyncSession = Depends(get_session)): +async def devis_vers_facture_direct( + id: str, session: AsyncSession = Depends(get_session) +): """ 🔧 Transformation Devis → Facture (DIRECT, sans commande) - + ✅ Utilise les VRAIS types Sage (0 → 60) ✅ Met à jour le statut du devis source à 5 (Transformé) - + **Workflow raccourci** : Permet de facturer directement depuis un devis sans passer par la création d'une commande. - + **Cas d'usage** : - Prestations de services facturées directement - Petites commandes sans besoin de suivi intermédiaire - Ventes au comptoir - + Args: id: Numéro du devis source - + Returns: Informations de la facture créée """ @@ -3347,15 +3333,15 @@ async def devis_vers_facture_direct(id: str, session: AsyncSession = Depends(get devis_existant = sage_client.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, + 400, f"Le devis {id} a déjà été transformé (statut=5). " - f"Vérifiez les documents déjà créés depuis ce devis." + f"Vérifiez les documents déjà créés depuis ce devis.", ) - + # Étape 2: Transformation resultat = sage_client.transformer_document( numero_source=id, @@ -3368,7 +3354,9 @@ async def devis_vers_facture_direct(id: str, session: AsyncSession = Depends(get sage_client.changer_statut_devis(id, nouveau_statut=5) logger.info(f"✅ Statut devis {id} mis à jour: 5 (Transformé)") except Exception as e: - logger.warning(f"⚠️ Impossible de mettre à jour le statut du devis {id}: {e}") + logger.warning( + f"⚠️ Impossible de mettre à jour le statut du devis {id}: {e}" + ) # On continue même si la MAJ statut échoue # Étape 4: Logger la transformation @@ -3408,55 +3396,56 @@ async def devis_vers_facture_direct(id: str, session: AsyncSession = Depends(get @app.post("/workflow/commande/{id}/to-livraison", tags=["Workflows"]) -async def commande_vers_livraison(id: str, session: AsyncSession = Depends(get_session)): +async def commande_vers_livraison( + id: str, session: AsyncSession = Depends(get_session) +): """ 🔧 Transformation Commande → Bon de livraison - + ✅ Utilise les VRAIS types Sage (10 → 30) - + **Workflow typique** : Après validation d'une commande, génère le bon de livraison pour préparer l'expédition. - + **Cas d'usage** : - Préparation d'une expédition - Génération du bordereau de livraison - Suivi logistique - + **Workflow complet** : 1. Devis → Commande (via `/workflow/devis/{id}/to-commande`) 2. **Commande → Livraison** (cette route) 3. Livraison → Facture (via `/workflow/livraison/{id}/to-facture`) - + Args: id: Numéro de la commande source - + Returns: Informations du bon de livraison créé """ try: # Étape 1: Vérifier que la commande existe commande_existante = sage_client.lire_document( - id, - settings.SAGE_TYPE_BON_COMMANDE + id, settings.SAGE_TYPE_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à." + 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." + f"La commande {id} est annulée (statut=6) et ne peut pas être transformée.", ) - + # Étape 2: Transformation resultat = sage_client.transformer_document( numero_source=id, @@ -3498,7 +3487,7 @@ async def commande_vers_livraison(id: str, session: AsyncSession = Depends(get_s except Exception as e: logger.error(f"❌ Erreur transformation commande→livraison: {e}", exc_info=True) raise HTTPException(500, str(e)) - + @app.get("/debug/users", response_model=List[UserResponse], tags=["Debug"]) async def lister_utilisateurs_debug( @@ -3845,6 +3834,7 @@ async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_ses "traceback": str(e.__class__.__name__), } + @app.get("/debug/fournisseurs/cache", tags=["Debug"]) async def debug_cache_fournisseurs(): """ @@ -3853,7 +3843,7 @@ async def debug_cache_fournisseurs(): try: # Appeler la gateway Windows pour récupérer l'info cache cache_info = sage_client.get_cache_info() - + # Tenter de lister les fournisseurs try: fournisseurs = sage_client.lister_fournisseurs(filtre="") @@ -3863,27 +3853,32 @@ async def debug_cache_fournisseurs(): nb_fournisseurs = -1 exemple = [] error = str(e) - + return { "success": True, "cache_info_windows": cache_info, "test_liste_fournisseurs": { "nb_fournisseurs": nb_fournisseurs, "exemples": exemple, - "erreur": error if nb_fournisseurs == -1 else None + "erreur": error if nb_fournisseurs == -1 else None, }, "diagnostic": { "gateway_accessible": cache_info is not None, - "cache_fournisseurs_existe": "fournisseurs" in cache_info if cache_info else False, + "cache_fournisseurs_existe": ( + "fournisseurs" in cache_info if cache_info else False + ), "probleme_probable": ( - "Cache fournisseurs non initialisé côté Windows" - if cache_info and "fournisseurs" not in cache_info - else "OK" if nb_fournisseurs > 0 - else "Erreur lors de la récupération" - ) - } + "Cache fournisseurs non initialisé côté Windows" + if cache_info and "fournisseurs" not in cache_info + else ( + "OK" + if nb_fournisseurs > 0 + else "Erreur lors de la récupération" + ) + ), + }, } - + except Exception as e: logger.error(f"❌ Erreur debug cache: {e}", exc_info=True) raise HTTPException(500, str(e)) @@ -3897,18 +3892,19 @@ async def force_refresh_fournisseurs(): try: # Appeler la gateway Windows pour forcer le refresh resultat = sage_client.refresh_cache() - + # Attendre 2 secondes import time + time.sleep(2) - + # Récupérer le cache info après refresh cache_info = sage_client.get_cache_info() - + # Tester la liste fournisseurs = sage_client.lister_fournisseurs(filtre="") nb_fournisseurs = len(fournisseurs) if fournisseurs else 0 - + return { "success": True, "refresh_result": resultat, @@ -3916,17 +3912,17 @@ async def force_refresh_fournisseurs(): "nb_fournisseurs_maintenant": nb_fournisseurs, "exemples": fournisseurs[:3] if fournisseurs else [], "message": ( - f"✅ Refresh OK : {nb_fournisseurs} fournisseurs disponibles" - if nb_fournisseurs > 0 + f"✅ Refresh OK : {nb_fournisseurs} fournisseurs disponibles" + if nb_fournisseurs > 0 else "❌ Problème : aucun fournisseur après refresh" - ) + ), } - + except Exception as e: logger.error(f"❌ Erreur force refresh: {e}", exc_info=True) raise HTTPException(500, str(e)) - - + + # ===================================================== # LANCEMENT # ===================================================== diff --git a/sage_client.py b/sage_client.py index e45c6eb..d03e009 100644 --- a/sage_client.py +++ b/sage_client.py @@ -278,32 +278,32 @@ class SageGatewayClient: def creer_fournisseur(self, fournisseur_data: Dict) -> Dict: """ Envoie la requête de création de fournisseur à la gateway Windows. - + Args: fournisseur_data: Dict contenant intitule, compte_collectif, etc. - + Returns: Fournisseur créé avec son numéro définitif """ return self._post("/sage/fournisseurs/create", fournisseur_data).get("data", {}) - + def modifier_fournisseur(self, code: str, fournisseur_data: Dict) -> Dict: """ ✏️ Modification d'un fournisseur existant - + Args: code: Code du fournisseur à modifier fournisseur_data: Dictionnaire contenant les champs à modifier (seuls les champs présents seront mis à jour) - + Returns: Fournisseur modifié """ - return self._post("/sage/fournisseurs/update", { - "code": code, - "fournisseur_data": fournisseur_data - }).get("data", {}) - + return self._post( + "/sage/fournisseurs/update", + {"code": code, "fournisseur_data": fournisseur_data}, + ).get("data", {}) + # ===================================================== # AVOIRS # ===================================================== @@ -357,7 +357,7 @@ class SageGatewayClient: return r.json() except: return {"status": "down"} - + def creer_client(self, client_data: Dict) -> Dict: """ Envoie la requête de création de client à la gateway Windows. @@ -365,48 +365,45 @@ class SageGatewayClient: """ # On appelle la route définie dans main.py return self._post("/sage/clients/create", client_data).get("data", {}) - + def modifier_client(self, code: str, client_data: Dict) -> Dict: """ ✏️ Modification d'un client existant - + Args: code: Code du client à modifier client_data: Dictionnaire contenant les champs à modifier (seuls les champs présents seront mis à jour) - + Returns: Client modifié """ - return self._post("/sage/clients/update", { - "code": code, - "client_data": client_data - }).get("data", {}) - - + return self._post( + "/sage/clients/update", {"code": code, "client_data": client_data} + ).get("data", {}) + def modifier_devis(self, numero: str, devis_data: Dict) -> Dict: """ ✏️ Modification d'un devis existant - + Args: numero: Numéro du devis à modifier devis_data: Dictionnaire contenant les champs à modifier: - date_devis (str, optional): Nouvelle date - lignes (List, optional): Nouvelles lignes - statut (int, optional): Nouveau statut - + Returns: Devis modifié avec totaux recalculés """ - return self._post("/sage/devis/update", { - "numero": numero, - "devis_data": devis_data - }).get("data", {}) - + return self._post( + "/sage/devis/update", {"numero": numero, "devis_data": devis_data} + ).get("data", {}) + def creer_commande(self, commande_data: Dict) -> Dict: """ ➕ Création d'une nouvelle commande (Bon de commande) - + Args: commande_data: Dictionnaire contenant: - client_id (str): Code du client @@ -417,17 +414,16 @@ class SageGatewayClient: - quantite (float) - prix_unitaire_ht (float, optional) - remise_pourcentage (float, optional) - + Returns: Commande créée avec son numéro et ses totaux """ return self._post("/sage/commandes/create", commande_data).get("data", {}) - - + def modifier_commande(self, numero: str, commande_data: Dict) -> Dict: """ ✏️ Modification d'une commande existante - + Args: numero: Numéro de la commande à modifier commande_data: Dictionnaire contenant les champs à modifier: @@ -435,36 +431,33 @@ class SageGatewayClient: - lignes (List, optional): Nouvelles lignes - statut (int, optional): Nouveau statut - reference (str, optional): Nouvelle référence - + Returns: Commande modifiée avec totaux recalculés """ - return self._post("/sage/commandes/update", { - "numero": numero, - "commande_data": commande_data - }).get("data", {}) - - + return self._post( + "/sage/commandes/update", {"numero": numero, "commande_data": commande_data} + ).get("data", {}) + def creer_livraison(self, livraison_data: Dict) -> Dict: """ ➕ Création d'une nouvelle livraison (Bon de livraison) """ return self._post("/sage/livraisons/create", livraison_data).get("data", {}) - def modifier_livraison(self, numero: str, livraison_data: Dict) -> Dict: """ ✏️ Modification d'une livraison existante """ - return self._post("/sage/livraisons/update", { - "numero": numero, - "livraison_data": livraison_data - }).get("data", {}) - + return self._post( + "/sage/livraisons/update", + {"numero": numero, "livraison_data": livraison_data}, + ).get("data", {}) + def creer_avoir(self, avoir_data: Dict) -> Dict: """ ➕ Création d'un avoir (Bon d'avoir) - + Args: avoir_data: Dictionnaire contenant: - client_id (str): Code du client @@ -475,17 +468,16 @@ class SageGatewayClient: - quantite (float) - prix_unitaire_ht (float, optional) - remise_pourcentage (float, optional) - + Returns: Avoir créé avec son numéro et ses totaux """ return self._post("/sage/avoirs/create", avoir_data).get("data", {}) - def modifier_avoir(self, numero: str, avoir_data: Dict) -> Dict: """ ✏️ Modification d'un avoir existant - + Args: numero: Numéro de l'avoir à modifier avoir_data: Dictionnaire contenant les champs à modifier: @@ -493,20 +485,18 @@ class SageGatewayClient: - lignes (List, optional): Nouvelles lignes - statut (int, optional): Nouveau statut - reference (str, optional): Nouvelle référence - + Returns: Avoir modifié avec totaux recalculés """ - return self._post("/sage/avoirs/update", { - "numero": numero, - "avoir_data": avoir_data - }).get("data", {}) - - + return self._post( + "/sage/avoirs/update", {"numero": numero, "avoir_data": avoir_data} + ).get("data", {}) + def creer_facture(self, facture_data: Dict) -> Dict: """ ➕ Création d'une facture - + Args: facture_data: Dictionnaire contenant: - client_id (str): Code du client @@ -517,17 +507,16 @@ class SageGatewayClient: - quantite (float) - prix_unitaire_ht (float, optional) - remise_pourcentage (float, optional) - + Returns: Facture créée avec son numéro et ses totaux """ return self._post("/sage/factures/create", facture_data).get("data", {}) - def modifier_facture(self, numero: str, facture_data: Dict) -> Dict: """ ✏️ Modification d'une facture existante - + Args: numero: Numéro de la facture à modifier facture_data: Dictionnaire contenant les champs à modifier: @@ -535,21 +524,20 @@ class SageGatewayClient: - lignes (List, optional): Nouvelles lignes - statut (int, optional): Nouveau statut - reference (str, optional): Nouvelle référence - + Returns: Facture modifiée avec totaux recalculés """ - return self._post("/sage/factures/update", { - "numero": numero, - "facture_data": facture_data - }).get("data", {}) - + return self._post( + "/sage/factures/update", {"numero": numero, "facture_data": facture_data} + ).get("data", {}) + def generer_pdf_document(self, doc_id: str, type_doc: int) -> bytes: """ 🆕 Génère le PDF d'un document via la gateway Windows (route généralisée) - + **Cette méthode remplace les appels spécifiques par type de document** - + Args: doc_id: Numéro du document (ex: "DE00001", "FA00001") type_doc: Type de document Sage: @@ -558,14 +546,14 @@ class SageGatewayClient: - 30: Bon de livraison - 60: Facture - 50: Bon d'avoir - + Returns: bytes: Contenu du PDF (binaire) - + Raises: ValueError: Si le PDF retourné est vide RuntimeError: Si erreur de communication avec la gateway - + Example: >>> pdf_bytes = sage_client.generer_pdf_document("DE00001", 0) >>> with open("devis.pdf", "wb") as f: @@ -573,59 +561,55 @@ class SageGatewayClient: """ try: logger.info(f"📄 Demande génération PDF: doc_id={doc_id}, type={type_doc}") - + # Appel HTTP vers la gateway Windows r = requests.post( f"{self.url}/sage/documents/generate-pdf", - json={ - "doc_id": doc_id, - "type_doc": type_doc - }, + json={"doc_id": doc_id, "type_doc": type_doc}, headers=self.headers, - timeout=60 # Timeout élevé pour génération PDF + timeout=60, # Timeout élevé pour génération PDF ) - + r.raise_for_status() - + import base64 - + response_data = r.json() - + # Vérifier que la réponse contient bien le PDF if not response_data.get("success"): error_msg = response_data.get("error", "Erreur inconnue") raise RuntimeError(f"Gateway a retourné une erreur: {error_msg}") - + pdf_base64 = response_data.get("data", {}).get("pdf_base64", "") - + if not pdf_base64: raise ValueError( f"PDF vide retourné par la gateway pour {doc_id} (type {type_doc})" ) - + # Décoder le base64 pdf_bytes = base64.b64decode(pdf_base64) - + logger.info(f"✅ PDF décodé: {len(pdf_bytes)} octets") - + return pdf_bytes - + except requests.exceptions.Timeout: logger.error(f"⏱️ Timeout génération PDF pour {doc_id}") raise RuntimeError( f"Timeout lors de la génération du PDF (>60s). " f"Le document {doc_id} est peut-être trop volumineux." ) - + except requests.exceptions.RequestException as e: logger.error(f"❌ Erreur HTTP génération PDF: {e}") raise RuntimeError(f"Erreur de communication avec la gateway: {str(e)}") - + except Exception as e: logger.error(f"❌ Erreur génération PDF: {e}", exc_info=True) raise - # Instance globale sage_client = SageGatewayClient()