From 7ee3751ee2d3da68fa77afe8d8fe640ff3d22c19 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 27 Nov 2025 12:44:39 +0300 Subject: [PATCH] feat: add endpoint to list devis with filters and update devis status endpoint signature and logic. --- main.py | 375 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 344 insertions(+), 31 deletions(-) diff --git a/main.py b/main.py index e6c17ca..c1fc480 100644 --- a/main.py +++ b/main.py @@ -15,14 +15,12 @@ from sage_connector import SageConnector # ===================================================== logging.basicConfig( level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler("sage_gateway.log"), - logging.StreamHandler() - ] + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.FileHandler("sage_gateway.log"), logging.StreamHandler()], ) logger = logging.getLogger(__name__) + # ===================================================== # ENUMS # ===================================================== @@ -34,34 +32,43 @@ class TypeDocument(int, Enum): PREPARATION = 4 FACTURE = 5 + # ===================================================== # MODÈLES # ===================================================== class FiltreRequest(BaseModel): filtre: Optional[str] = "" + class CodeRequest(BaseModel): code: str + class ChampLibreRequest(BaseModel): doc_id: str type_doc: int nom_champ: str valeur: str + class DevisRequest(BaseModel): client_id: str date_devis: Optional[date] = None - lignes: List[Dict] # Format: {article_code, quantite, prix_unitaire_ht, remise_pourcentage} + lignes: List[ + Dict + ] # Format: {article_code, quantite, prix_unitaire_ht, remise_pourcentage} + class TransformationRequest(BaseModel): numero_source: str type_source: int type_cible: int + class StatutRequest(BaseModel): nouveau_statut: int + # ===================================================== # SÉCURITÉ # ===================================================== @@ -72,13 +79,14 @@ def verify_token(x_sage_token: str = Header(...)): raise HTTPException(401, "Token invalide") return True + # ===================================================== # APPLICATION # ===================================================== app = FastAPI( title="Sage Gateway - Windows Server", version="1.0.0", - description="Passerelle d'accès à Sage 100c pour VPS Linux" + description="Passerelle d'accès à Sage 100c pour VPS Linux", ) app.add_middleware( @@ -86,20 +94,21 @@ app.add_middleware( allow_origins=settings.cors_origins, allow_methods=["*"], allow_headers=["*"], - allow_credentials=True + allow_credentials=True, ) sage: Optional[SageConnector] = None + # ===================================================== # LIFECYCLE # ===================================================== @app.on_event("startup") def startup(): global sage - + logger.info("🚀 Démarrage Sage Gateway Windows...") - + # Validation config try: validate_settings() @@ -107,25 +116,25 @@ def startup(): except ValueError as e: logger.error(f"❌ Configuration invalide: {e}") raise - + # Connexion Sage sage = SageConnector( - settings.chemin_base, - settings.utilisateur, - settings.mot_de_passe + settings.chemin_base, settings.utilisateur, settings.mot_de_passe ) - + if not sage.connecter(): raise RuntimeError("❌ Impossible de se connecter à Sage 100c") - + logger.info("✅ Sage Gateway démarré et connecté") + @app.on_event("shutdown") def shutdown(): if sage: sage.deconnecter() logger.info("👋 Sage Gateway arrêté") + # ===================================================== # ENDPOINTS - SYSTÈME # ===================================================== @@ -136,9 +145,10 @@ def health(): "status": "ok", "sage_connected": sage is not None and sage.cial is not None, "cache_info": sage.get_cache_info() if sage else None, - "timestamp": datetime.now().isoformat() + "timestamp": datetime.now().isoformat(), } + # ===================================================== # ENDPOINTS - CLIENTS # ===================================================== @@ -152,6 +162,7 @@ def clients_list(req: FiltreRequest): logger.error(f"Erreur liste clients: {e}") raise HTTPException(500, str(e)) + @app.post("/sage/clients/get", dependencies=[Depends(verify_token)]) def client_get(req: CodeRequest): """Lecture d'un client par code""" @@ -166,6 +177,7 @@ def client_get(req: CodeRequest): logger.error(f"Erreur lecture client: {e}") raise HTTPException(500, str(e)) + # ===================================================== # ENDPOINTS - ARTICLES # ===================================================== @@ -179,6 +191,7 @@ def articles_list(req: FiltreRequest): logger.error(f"Erreur liste articles: {e}") raise HTTPException(500, str(e)) + @app.post("/sage/articles/get", dependencies=[Depends(verify_token)]) def article_get(req: CodeRequest): """Lecture d'un article par référence""" @@ -193,6 +206,7 @@ def article_get(req: CodeRequest): logger.error(f"Erreur lecture article: {e}") raise HTTPException(500, str(e)) + # ===================================================== # ENDPOINTS - DEVIS # ===================================================== @@ -204,15 +218,16 @@ def creer_devis(req: DevisRequest): devis_data = { "client": {"code": req.client_id, "intitule": ""}, "date_devis": req.date_devis or date.today(), - "lignes": req.lignes + "lignes": req.lignes, } - + resultat = sage.creer_devis_enrichi(devis_data) return {"success": True, "data": resultat} except Exception as e: logger.error(f"Erreur création devis: {e}") raise HTTPException(500, str(e)) + @app.post("/sage/devis/get", dependencies=[Depends(verify_token)]) def lire_devis(req: CodeRequest): """Lecture d'un devis""" @@ -227,6 +242,7 @@ def lire_devis(req: CodeRequest): logger.error(f"Erreur lecture devis: {e}") raise HTTPException(500, str(e)) + @app.post("/sage/devis/statut", dependencies=[Depends(verify_token)]) def changer_statut_devis(doc_id: str, req: StatutRequest): """Changement de statut d'un devis""" @@ -238,6 +254,128 @@ def changer_statut_devis(doc_id: str, req: StatutRequest): logger.error(f"Erreur MAJ statut: {e}") raise HTTPException(500, str(e)) + +@app.post("/sage/devis/list", dependencies=[Depends(verify_token)]) +def devis_list(limit: int = 100, statut: Optional[int] = None): + """ + Liste tous les devis avec filtres optionnels + Utilise le cache pour performances optimales + """ + try: + with sage._com_context(), sage._lock_com: + factory = sage.cial.FactoryDocumentVente + devis_list = [] + index = 1 + max_iterations = limit * 3 + erreurs_consecutives = 0 + max_erreurs = 50 + + while ( + len(devis_list) < limit + and index < max_iterations + and erreurs_consecutives < max_erreurs + ): + try: + persist = factory.List(index) + if persist is None: + break + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + # Filtrer uniquement devis (type 0) + if getattr(doc, "DO_Type", -1) != 0: + index += 1 + continue + + doc_statut = getattr(doc, "DO_Statut", 0) + + # Filtre statut + if statut is not None and doc_statut != statut: + index += 1 + continue + + # Charger client via .Client + client_code = "" + client_intitule = "" + + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code = getattr(client_obj, "CT_Num", "").strip() + client_intitule = getattr( + client_obj, "CT_Intitule", "" + ).strip() + except Exception as e: + logger.debug(f"Erreur chargement client: {e}") + + devis_list.append( + { + "numero": getattr(doc, "DO_Piece", ""), + "date": str(getattr(doc, "DO_Date", "")), + "client_code": client_code, + "client_intitule": client_intitule, + "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), + "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), + "statut": doc_statut, + } + ) + + erreurs_consecutives = 0 + index += 1 + + except Exception as e: + erreurs_consecutives += 1 + logger.debug(f"Erreur index {index}: {e}") + index += 1 + + if erreurs_consecutives >= max_erreurs: + break + + logger.info(f"✅ {len(devis_list)} devis retournés") + return {"success": True, "data": devis_list} + + except Exception as e: + logger.error(f"Erreur liste devis: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/sage/devis/statut", dependencies=[Depends(verify_token)]) +def changer_statut_devis_endpoint(numero: str, nouveau_statut: int): + """Change le statut d'un devis""" + try: + with sage._com_context(), sage._lock_com: + factory = sage.cial.FactoryDocumentVente + persist = factory.ReadPiece(0, numero) + + if not persist: + raise HTTPException(404, f"Devis {numero} introuvable") + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + statut_actuel = getattr(doc, "DO_Statut", 0) + doc.DO_Statut = nouveau_statut + doc.Write() + + logger.info(f"✅ Statut devis {numero}: {statut_actuel} → {nouveau_statut}") + + return { + "success": True, + "data": { + "numero": numero, + "statut_ancien": statut_actuel, + "statut_nouveau": nouveau_statut, + }, + } + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur changement statut: {e}") + raise HTTPException(500, str(e)) + + # ===================================================== # ENDPOINTS - DOCUMENTS # ===================================================== @@ -255,35 +393,33 @@ def lire_document(numero: str, type_doc: int): logger.error(f"Erreur lecture document: {e}") raise HTTPException(500, str(e)) + @app.post("/sage/documents/transform", dependencies=[Depends(verify_token)]) def transformer_document(req: TransformationRequest): """Transformation de document (devis → commande, etc.)""" try: resultat = sage.transformer_document( - req.numero_source, - req.type_source, - req.type_cible + req.numero_source, req.type_source, req.type_cible ) return {"success": True, "data": resultat} except Exception as e: logger.error(f"Erreur transformation: {e}") raise HTTPException(500, str(e)) + @app.post("/sage/documents/champ-libre", dependencies=[Depends(verify_token)]) def maj_champ_libre(req: ChampLibreRequest): """Mise à jour d'un champ libre""" try: success = sage.mettre_a_jour_champ_libre( - req.doc_id, - req.type_doc, - req.nom_champ, - req.valeur + req.doc_id, req.type_doc, req.nom_champ, req.valeur ) return {"success": success} except Exception as e: logger.error(f"Erreur MAJ champ libre: {e}") raise HTTPException(500, str(e)) + # ===================================================== # ENDPOINTS - CONTACTS # ===================================================== @@ -301,6 +437,181 @@ def contact_read(req: CodeRequest): logger.error(f"Erreur lecture contact: {e}") raise HTTPException(500, str(e)) + +@app.post("/sage/commandes/list", dependencies=[Depends(verify_token)]) +def commandes_list(limit: int = 100, statut: Optional[int] = None): + """Liste toutes les commandes""" + try: + with sage._com_context(), sage._lock_com: + factory = sage.cial.FactoryDocumentVente + commandes = [] + index = 1 + max_iterations = limit * 3 + + while len(commandes) < limit and index < max_iterations: + try: + persist = factory.List(index) + if persist is None: + break + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + # Filtrer commandes (type 3) + if getattr(doc, "DO_Type", -1) != 3: + index += 1 + continue + + doc_statut = getattr(doc, "DO_Statut", 0) + + if statut is None or doc_statut == statut: + # Charger client + client_code = "" + client_intitule = "" + + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code = getattr(client_obj, "CT_Num", "").strip() + client_intitule = getattr( + client_obj, "CT_Intitule", "" + ).strip() + except: + pass + + commandes.append( + { + "numero": getattr(doc, "DO_Piece", ""), + "date": str(getattr(doc, "DO_Date", "")), + "client_code": client_code, + "client_intitule": client_intitule, + "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), + "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), + "statut": doc_statut, + } + ) + + index += 1 + except: + index += 1 + continue + + logger.info(f"✅ {len(commandes)} commandes retournées") + return {"success": True, "data": commandes} + + except Exception as e: + logger.error(f"Erreur liste commandes: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/sage/factures/list", dependencies=[Depends(verify_token)]) +def factures_list(limit: int = 100, statut: Optional[int] = None): + """Liste toutes les factures""" + try: + with sage._com_context(), sage._lock_com: + factory = sage.cial.FactoryDocumentVente + factures = [] + index = 1 + max_iterations = limit * 3 + + while len(factures) < limit and index < max_iterations: + try: + persist = factory.List(index) + if persist is None: + break + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + # Filtrer factures (type 5) + if getattr(doc, "DO_Type", -1) != 5: + index += 1 + continue + + doc_statut = getattr(doc, "DO_Statut", 0) + + if statut is None or doc_statut == statut: + # Charger client + client_code = "" + client_intitule = "" + + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code = getattr(client_obj, "CT_Num", "").strip() + client_intitule = getattr( + client_obj, "CT_Intitule", "" + ).strip() + except: + pass + + # Champ libre dernière relance + derniere_relance = None + try: + derniere_relance = getattr(doc, "DO_DerniereRelance", None) + except: + pass + + factures.append( + { + "numero": getattr(doc, "DO_Piece", ""), + "date": str(getattr(doc, "DO_Date", "")), + "client_code": client_code, + "client_intitule": client_intitule, + "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), + "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), + "statut": doc_statut, + "derniere_relance": ( + str(derniere_relance) if derniere_relance else None + ), + } + ) + + index += 1 + except: + index += 1 + continue + + logger.info(f"✅ {len(factures)} factures retournées") + return {"success": True, "data": factures} + + except Exception as e: + logger.error(f"Erreur liste factures: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/sage/client/remise-max", dependencies=[Depends(verify_token)]) +def lire_remise_max_client(code: str): + """Récupère la remise max autorisée pour un client""" + try: + client_obj = sage._lire_client_obj(code) + + if not client_obj: + raise HTTPException(404, f"Client {code} introuvable") + + remise_max = 10.0 # Défaut + + try: + remise_max = float(getattr(client_obj, "CT_RemiseMax", 10.0)) + except: + pass + + logger.info(f"✅ Remise max client {code}: {remise_max}%") + + return { + "success": True, + "data": {"client_code": code, "remise_max": remise_max}, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture remise: {e}") + raise HTTPException(500, str(e)) + + # ===================================================== # ENDPOINTS - ADMIN # ===================================================== @@ -312,21 +623,23 @@ def refresh_cache(): return { "success": True, "message": "Cache actualisé", - "info": sage.get_cache_info() + "info": sage.get_cache_info(), } except Exception as e: logger.error(f"Erreur refresh cache: {e}") raise HTTPException(500, str(e)) + @app.get("/sage/cache/info", dependencies=[Depends(verify_token)]) -def cache_info(): - """Informations sur le cache""" +def cache_info_get(): + """Informations sur le cache (endpoint GET)""" try: return {"success": True, "data": sage.get_cache_info()} except Exception as e: logger.error(f"Erreur info cache: {e}") raise HTTPException(500, str(e)) + # ===================================================== # LANCEMENT # ===================================================== @@ -336,5 +649,5 @@ if __name__ == "__main__": host=settings.api_host, port=settings.api_port, reload=False, # Pas de reload en production - log_level="info" - ) \ No newline at end of file + log_level="info", + )