diff --git a/config.py b/config.py index f068d00..cb59340 100644 --- a/config.py +++ b/config.py @@ -1,14 +1,20 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from typing import Optional, List + class Settings(BaseSettings): model_config = SettingsConfigDict( - env_file=".env", - env_file_encoding="utf-8", - case_sensitive=False, - extra="ignore" + env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore" ) + SAGE_TYPE_DEVIS: int = 0 + SAGE_TYPE_BON_COMMANDE: int = 10 + SAGE_TYPE_PREPARATION: int = 20 + SAGE_TYPE_BON_LIVRAISON: int = 30 + SAGE_TYPE_BON_RETOUR: int = 40 + SAGE_TYPE_BON_AVOIR: int = 50 + SAGE_TYPE_FACTURE: int = 60 + # === SAGE 100c (Windows uniquement) === chemin_base: str utilisateur: str = "Administrateur" @@ -27,16 +33,18 @@ class Settings(BaseSettings): # === API Windows === api_host: str = "0.0.0.0" api_port: int = 8000 - + # === CORS === cors_origins: List[str] = ["*"] + settings = Settings() + def validate_settings(): """Validation au démarrage""" if not settings.chemin_base or not settings.mot_de_passe: raise ValueError("❌ CHEMIN_BASE et MOT_DE_PASSE requis dans .env") if not settings.sage_gateway_token: raise ValueError("❌ SAGE_GATEWAY_TOKEN requis (doit être identique sur Linux)") - return True \ No newline at end of file + return True diff --git a/main.py b/main.py index de540b7..cf7715d 100644 --- a/main.py +++ b/main.py @@ -490,33 +490,28 @@ def lire_document(numero: str, type_doc: int): @app.post("/sage/documents/transform", dependencies=[Depends(verify_token)]) def transformer_document( numero_source: str = Query(..., description="Numéro du document source"), - type_source: int = Query( - ..., - ge=0, - le=5, - description="Type document source (0=Devis, 3=Commande, 5=Facture)", - ), - type_cible: int = Query(..., ge=0, le=5, description="Type document cible"), + type_source: int = Query(..., description="Type document source"), + type_cible: int = Query(..., description="Type document cible"), ): """ - 🔧 Transformation de document (devis → commande → facture) + 🔧 Transformation de document - ✅ CORRECTION FINALE: Query params au lieu de body JSON + ✅ CORRECTION : Utilise les VRAIS types Sage Dataven - Types de documents: + Types valides : - 0: Devis - - 1: Bon de livraison - - 2: Bon de retour - - 3: Commande - - 4: Préparation - - 5: Facture + - 10: Bon de commande + - 20: Préparation + - 30: Bon de livraison + - 40: Bon de retour + - 50: Bon d'avoir + - 60: Facture - Transformations valides: - - Devis (0) → Commande (3) - - Devis (0) → Facture (5) - - Commande (3) → Bon livraison (1) - - Commande (3) → Facture (5) - - Bon livraison (1) → Facture (5) + Transformations autorisées : + - Devis (0) → Commande (10) + - Commande (10) → Bon livraison (30) + - Commande (10) → Facture (60) + - Bon livraison (30) → Facture (60) """ try: logger.info( @@ -524,13 +519,13 @@ def transformer_document( f"(type {type_source}) → type {type_cible}" ) - # Validation des transformations autorisées + # ✅ Matrice des transformations valides pour VOTRE Sage transformations_valides = { - (0, 3), # Devis → Commande - (0, 5), # Devis → Facture - (3, 1), # Commande → Bon de livraison - (3, 5), # Commande → Facture - (1, 5), # Bon de livraison → Facture + (0, 10), # Devis → Commande + (10, 30), # Commande → Bon de livraison + (10, 60), # Commande → Facture + (30, 60), # Bon de livraison → Facture + (0, 60), # Devis → Facture (si autorisé) } if (type_source, type_cible) not in transformations_valides: @@ -557,11 +552,9 @@ def transformer_document( except HTTPException: raise except ValueError as e: - # Erreurs métier (document introuvable, déjà transformé, etc.) logger.error(f"❌ Erreur métier transformation: {e}") raise HTTPException(400, str(e)) except Exception as e: - # Erreurs techniques (COM, Sage, etc.) logger.error(f"❌ Erreur technique transformation: {e}", exc_info=True) raise HTTPException(500, f"Erreur transformation: {str(e)}") @@ -612,7 +605,7 @@ def contact_read(req: CodeRequest): def commandes_list(limit: int = 100, statut: Optional[int] = None): """ 📋 Liste toutes les commandes - ✅ CORRECTIONS: Gestion robuste des erreurs + logging détaillé + ✅ CORRECTION: Filtre sur type 10 (BON_COMMANDE) """ try: if not sage or not sage.cial: @@ -622,7 +615,7 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None): factory = sage.cial.FactoryDocumentVente commandes = [] index = 1 - max_iterations = limit * 10 # Plus de marge + max_iterations = limit * 10 erreurs_consecutives = 0 max_erreurs = 100 @@ -636,33 +629,26 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None): try: persist = factory.List(index) if persist is None: - logger.debug(f"Fin de liste à l'index {index}") break doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() - # ✅ CORRECTION 1: Vérifier type de document doc_type = getattr(doc, "DO_Type", -1) - # ⚠️ CRITIQUE: Vérifier que c'est bien une commande (type 3) - if doc_type != 3: + # ✅ CRITIQUE : Filtrer sur type 10 (BON_COMMANDE) + if doc_type != settings.SAGE_TYPE_BON_COMMANDE: index += 1 continue doc_statut = getattr(doc, "DO_Statut", 0) - logger.debug( - f"Index {index}: Type={doc_type}, Statut={doc_statut}, " - f"Numéro={getattr(doc, 'DO_Piece', '?')}" - ) - # Filtre statut if statut is not None and doc_statut != statut: index += 1 continue - # ✅ CORRECTION 2: Charger client via .Client + # Charger client client_code = "" client_intitule = "" @@ -674,20 +660,8 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None): client_intitule = getattr( client_obj, "CT_Intitule", "" ).strip() - logger.debug(f" Client: {client_code} - {client_intitule}") except Exception as e: - logger.debug(f" Erreur chargement client: {e}") - # Fallback sur cache si code disponible - if not client_code: - try: - client_code = getattr(doc, "CT_Num", "").strip() - except: - pass - - if client_code and not client_intitule: - client_cache = sage.lire_client(client_code) - if client_cache: - client_intitule = client_cache.get("intitule", "") + logger.debug(f"Erreur chargement client: {e}") commande = { "numero": getattr(doc, "DO_Piece", ""), @@ -700,27 +674,17 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None): } commandes.append(commande) - logger.debug(f" ✅ Commande ajoutée: {commande['numero']}") - 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: - logger.warning( - f"⚠️ Arrêt après {max_erreurs} erreurs consécutives" - ) break - nb_avec_client = sum(1 for c in commandes if c["client_intitule"]) - logger.info( - f"✅ {len(commandes)} commandes retournées " - f"({nb_avec_client} avec client)" - ) + logger.info(f"✅ {len(commandes)} commandes retournées") return {"success": True, "data": commandes} @@ -733,7 +697,10 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None): @app.post("/sage/factures/list", dependencies=[Depends(verify_token)]) def factures_list(limit: int = 100, statut: Optional[int] = None): - """Liste toutes les factures""" + """ + 📋 Liste toutes les factures + ✅ CORRECTION: Filtre sur type 60 (FACTURE) + """ try: with sage._com_context(), sage._lock_com: factory = sage.cial.FactoryDocumentVente @@ -750,8 +717,8 @@ def factures_list(limit: int = 100, statut: Optional[int] = None): doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() - # Filtrer factures (type 5) - if getattr(doc, "DO_Type", -1) != 5: + # ✅ CRITIQUE: Filtrer factures (type 60) + if getattr(doc, "DO_Type", -1) != settings.SAGE_TYPE_FACTURE: index += 1 continue @@ -773,13 +740,6 @@ def factures_list(limit: int = 100, statut: Optional[int] = None): 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", ""), @@ -789,9 +749,6 @@ def factures_list(limit: int = 100, statut: Optional[int] = None): "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 - ), } ) diff --git a/sage_connector.py b/sage_connector.py index 94a9386..aa222bf 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1027,19 +1027,11 @@ class SageConnector: def transformer_document(self, numero_source, type_source, type_cible): """ Transformation de document avec la méthode NATIVE de Sage - - CHANGEMENT MAJEUR: - - Utilise TransformInto() au lieu de CreateProcess_Document() - - Méthode officielle Sage pour les transformations - - Gère automatiquement les numéros, statuts, et lignes - - Documentation Sage: - IBODocumentVente3.TransformInto(DO_Type: int) -> IBODocumentVente3 + ✅ CORRECTION : Utilise les VRAIS types Sage Dataven """ if not self.cial: raise RuntimeError("Connexion Sage non etablie") - # Convertir en int si enum type_source = int(type_source) type_cible = int(type_cible) @@ -1048,24 +1040,28 @@ class SageConnector: f"(type {type_source}) -> type {type_cible}" ) - # Validation des types - types_valides = {0, 1, 2, 3, 4, 5} - if type_source not in types_valides or type_cible not in types_valides: - raise ValueError( - f"Types invalides: source={type_source}, cible={type_cible}. " - f"Valeurs valides: {types_valides}" - ) - - # Matrice de transformations Sage 100c + # ✅ Matrice de transformations pour VOTRE installation Sage transformations_autorisees = { - (0, 3): "Devis -> Commande", - (0, 1): "Devis -> Bon de livraison", - (0, 5): "Devis -> Facture", # Peut être supporté selon config - (3, 1): "Commande -> Bon de livraison", - (3, 4): "Commande -> Preparation", - (3, 5): "Commande -> Facture", # Direct si autorisé - (1, 5): "Bon de livraison -> Facture", - (4, 1): "Preparation -> Bon de livraison", + ( + settings.SAGE_TYPE_DEVIS, + settings.SAGE_TYPE_BON_COMMANDE, + ): "Devis -> Commande", # 0 → 10 + ( + settings.SAGE_TYPE_BON_COMMANDE, + settings.SAGE_TYPE_BON_LIVRAISON, + ): "Commande -> Bon de livraison", # 10 → 30 + ( + settings.SAGE_TYPE_BON_COMMANDE, + settings.SAGE_TYPE_FACTURE, + ): "Commande -> Facture", # 10 → 60 + ( + settings.SAGE_TYPE_BON_LIVRAISON, + settings.SAGE_TYPE_FACTURE, + ): "Bon de livraison -> Facture", # 30 → 60 + ( + settings.SAGE_TYPE_DEVIS, + settings.SAGE_TYPE_FACTURE, + ): "Devis -> Facture", # 0 → 60 (si autorisé) } if (type_source, type_cible) not in transformations_autorisees: @@ -1135,8 +1131,8 @@ class SageConnector: f"Ce document a deja ete transforme partiellement ou totalement." ) - # Forcer statut "Accepté" si brouillon - if type_source == 0 and statut_actuel == 0: + # Forcer statut "Accepté" si brouillon (uniquement pour devis) + if type_source == settings.SAGE_TYPE_DEVIS and statut_actuel == 0: logger.warning( f"[TRANSFORM] Devis en brouillon (statut=0), " f"passage a 'Accepte' (statut=2)" @@ -1146,7 +1142,6 @@ class SageConnector: doc_source.Write() logger.info(f"[TRANSFORM] Statut change: 0 -> 2") - # Re-lire doc_source.Read() nouveau_statut = getattr(doc_source, "DO_Statut", 0) if nouveau_statut != 2: @@ -1162,20 +1157,16 @@ class SageConnector: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug("[TRANSFORM] Transaction demarree") - except AttributeError: - # BeginTrans n'existe pas sur cette version + except: logger.debug( "[TRANSFORM] BeginTrans non disponible, continue sans transaction" ) - except Exception as e: - logger.warning(f"[TRANSFORM] BeginTrans echoue: {e}") try: # ✅✅✅ MÉTHODE NATIVE SAGE: TransformInto() ✅✅✅ logger.info(f"[TRANSFORM] Appel TransformInto({type_cible})...") try: - # La méthode TransformInto() retourne le nouveau document doc_cible = doc_source.TransformInto(type_cible) if doc_cible is None: @@ -1186,7 +1177,6 @@ class SageConnector: logger.info("[TRANSFORM] TransformInto() execute avec succes") - # Cast vers le bon type try: doc_cible = win32com.client.CastTo( doc_cible, "IBODocumentVente3" @@ -1194,10 +1184,7 @@ class SageConnector: except: pass - # Lire le document cible doc_cible.Read() - - # Récupérer le numéro numero_cible = getattr(doc_cible, "DO_Piece", "") if not numero_cible: @@ -1228,40 +1215,30 @@ class SageConnector: ) except AttributeError as e: - # TransformInto() n'existe pas sur cette version de Sage logger.error(f"[TRANSFORM] TransformInto() non disponible: {e}") raise RuntimeError( - f"La methode TransformInto() n'est pas disponible sur votre version de Sage 100c. " - f"Vous devez soit: " - f"(1) Mettre a jour Sage, ou " - f"(2) Activer le module de gestion commerciale pour les commandes, ou " - f"(3) Utiliser l'interface Sage manuellement pour les transformations." + f"La methode TransformInto() n'est pas disponible. " + f"Causes possibles:\n" + f"1. Le module n'est pas active dans votre licence Sage\n" + f"2. L'utilisateur n'a pas les droits\n" + f"3. La transformation {type_source}→{type_cible} n'est pas supportee" ) except Exception as e: logger.error(f"[TRANSFORM] TransformInto() echoue: {e}") - # Essayer de déterminer la cause if "Valeur invalide" in str(e): raise RuntimeError( f"Sage refuse la transformation vers le type {type_cible}. " - f"Causes possibles:\n" - f"1. Le module 'Commandes' n'est pas active dans votre licence Sage\n" - f"2. L'utilisateur n'a pas les droits sur ce type de document\n" - f"3. La configuration Sage bloque ce type de transformation\n" - f"4. Il manque des parametres obligatoires (depot, tarif, etc.)\n\n" - f"Verifiez dans Sage: Fichier > Autorisations > Gestion Commerciale" - ) - elif "Acces refuse" in str(e) or "Access denied" in str(e): - raise RuntimeError( - f"Acces refuse pour creer une commande (type {type_cible}). " - f"Verifiez les droits utilisateur dans Sage: " - f"Fichier > Autorisations > Votre utilisateur" + f"Verifiez:\n" + f"1. Que le module est active (Commandes, Factures...)\n" + f"2. Les droits utilisateur\n" + f"3. Que le type {type_cible} existe dans votre Sage\n" + f"4. Les parametres obligatoires (depot, tarif, etc.)" ) else: raise RuntimeError( - f"Erreur Sage lors de la transformation: {e}\n" - f"Consultez les logs Sage pour plus de details." + f"Erreur Sage lors de la transformation: {e}" ) # Commit transaction @@ -1272,9 +1249,9 @@ class SageConnector: except: pass - # MAJ statut source -> Transformé + # MAJ statut source → Transformé try: - doc_source.Read() # Re-lire au cas où + doc_source.Read() doc_source.DO_Statut = 5 doc_source.Write() logger.info(