From 92c79f1362e6e9e782c708a103bf9d17e589fddc Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Fri, 28 Nov 2025 06:30:32 +0300 Subject: [PATCH] Diagnostic document transformation error --- main.py | 216 +++++++++++++++++++++++++++++ sage_connector.py | 338 +++++++++++++++------------------------------- 2 files changed, 328 insertions(+), 226 deletions(-) diff --git a/main.py b/main.py index fac166c..bc60d10 100644 --- a/main.py +++ b/main.py @@ -1061,6 +1061,222 @@ def diagnostiquer_devis(numero: str): raise HTTPException(500, str(e)) +@app.get("/sage/diagnostic/configuration", dependencies=[Depends(verify_token)]) +def diagnostic_configuration(): + """ + DIAGNOSTIC COMPLET de la configuration Sage + + Teste: + - Quelles méthodes COM sont disponibles + - Quels types de documents sont autorisés + - Quelles permissions l'utilisateur a + - Version de Sage + """ + try: + if not sage or not sage.cial: + raise HTTPException(503, "Service Sage indisponible") + + with sage._com_context(), sage._lock_com: + diagnostic = { + "connexion": "OK", + "chemin_base": sage.chemin_base, + "utilisateur": sage.utilisateur, + } + + # Version Sage + try: + version = getattr(sage.cial, "Version", "Inconnue") + diagnostic["version_sage"] = str(version) + except: + diagnostic["version_sage"] = "Non disponible" + + # Test des méthodes disponibles sur IBSCIALApplication3 + methodes_disponibles = [] + methodes_a_tester = [ + "CreateProcess_Document", + "FactoryDocumentVente", + "FactoryArticle", + "CptaApplication", + "BeginTrans", + "CommitTrans", + "RollbackTrans", + ] + + for methode in methodes_a_tester: + try: + if hasattr(sage.cial, methode): + methodes_disponibles.append(methode) + except: + pass + + diagnostic["methodes_cial_disponibles"] = methodes_disponibles + + # Test des types de documents autorisés + types_autorises = [] + types_bloques = [] + + for type_doc in range(6): # 0-5 + try: + # Essayer de créer un process (sans le valider) + process = sage.cial.CreateProcess_Document(type_doc) + if process: + types_autorises.append( + { + "type": type_doc, + "libelle": { + 0: "Devis", + 1: "Bon de livraison", + 2: "Bon de retour", + 3: "Commande", + 4: "Preparation", + 5: "Facture", + }[type_doc], + } + ) + # Ne pas valider, juste tester + del process + except Exception as e: + types_bloques.append( + { + "type": type_doc, + "libelle": { + 0: "Devis", + 1: "Bon de livraison", + 2: "Bon de retour", + 3: "Commande", + 4: "Preparation", + 5: "Facture", + }[type_doc], + "erreur": str(e)[:200], + } + ) + + diagnostic["types_documents_autorises"] = types_autorises + diagnostic["types_documents_bloques"] = types_bloques + + # Test TransformInto() sur un devis test + try: + factory = sage.cial.FactoryDocumentVente + + # Chercher n'importe quel devis + index = 1 + devis_test = None + + while index < 100: + try: + persist = factory.List(index) + if persist is None: + break + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + if getattr(doc, "DO_Type", -1) == 0: # Devis + devis_test = doc + break + + index += 1 + except: + index += 1 + + if devis_test: + # Tester si TransformInto existe + if hasattr(devis_test, "TransformInto"): + diagnostic["transforminto_disponible"] = True + diagnostic["transforminto_test"] = "Methode existe (non testee)" + else: + diagnostic["transforminto_disponible"] = False + diagnostic["transforminto_test"] = ( + "Methode TransformInto() inexistante" + ) + else: + diagnostic["transforminto_disponible"] = ( + "Impossible de tester (aucun devis trouve)" + ) + + except Exception as e: + diagnostic["transforminto_disponible"] = False + diagnostic["transforminto_erreur"] = str(e) + + # Modules Sage actifs + try: + # Tester l'accès aux différentes factories + modules = {} + + try: + sage.cial.FactoryDocumentVente + modules["Ventes"] = "OK" + except: + modules["Ventes"] = "INACCESSIBLE" + + try: + sage.cial.CptaApplication.FactoryClient + modules["Clients"] = "OK" + except: + modules["Clients"] = "INACCESSIBLE" + + try: + sage.cial.FactoryArticle + modules["Articles"] = "OK" + except: + modules["Articles"] = "INACCESSIBLE" + + diagnostic["modules_actifs"] = modules + except Exception as e: + diagnostic["modules_actifs_erreur"] = str(e) + + # Compter documents existants + try: + counts = {} + factory = sage.cial.FactoryDocumentVente + + for type_doc in range(6): + count = 0 + index = 1 + + while index < 1000: + try: + persist = factory.List(index) + if persist is None: + break + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + if getattr(doc, "DO_Type", -1) == type_doc: + count += 1 + + index += 1 + except: + index += 1 + break + + counts[ + { + 0: "Devis", + 1: "Bons_livraison", + 2: "Bons_retour", + 3: "Commandes", + 4: "Preparations", + 5: "Factures", + }[type_doc] + ] = count + + diagnostic["documents_existants"] = counts + except Exception as e: + diagnostic["documents_existants_erreur"] = str(e) + + logger.info("[DIAG] Configuration Sage analysee") + + return {"success": True, "diagnostic": diagnostic} + + except HTTPException: + raise + except Exception as e: + logger.error(f"[DIAG] Erreur diagnostic config: {e}", exc_info=True) + raise HTTPException(500, str(e)) + + # ===================================================== # LANCEMENT # ===================================================== diff --git a/sage_connector.py b/sage_connector.py index 14e4579..94a9386 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1026,13 +1026,15 @@ class SageConnector: def transformer_document(self, numero_source, type_source, type_cible): """ - Transformation de document avec gestion complète des statuts + Transformation de document avec la méthode NATIVE de Sage - CORRECTIONS: - - Pas d'émojis dans les logs - - Validation stricte des statuts Sage - - Gestion des documents "Réalisé partiellement" - - Meilleure détection d'erreurs + 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 """ if not self.cial: raise RuntimeError("Connexion Sage non etablie") @@ -1058,8 +1060,10 @@ class SageConnector: 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", } @@ -1081,7 +1085,9 @@ class SageConnector: persist_source = factory.ReadPiece(type_source, numero_source) if not persist_source: - logger.warning(f"ReadPiece failed, searching in List()...") + logger.warning( + f"[TRANSFORM] ReadPiece failed, searching in List()..." + ) persist_source = self._find_document_in_list( numero_source, type_source ) @@ -1110,14 +1116,7 @@ class SageConnector: f"pas de type {type_source}" ) - # RÈGLES DE STATUT SAGE - # 0=Brouillon, 1=Soumis, 2=Accepte, 3=Realise partiellement, - # 4=Realise totalement, 5=Transforme, 6=Annule - - # CORRECTION CRITIQUE: Statut 3 = "Réalisé partiellement" - # Cela signifie qu'une partie du document a déjà été transformée - # mais pas tout. Sage REFUSE de créer un nouveau document dans ce cas. - + # RÈGLES DE STATUT if statut_actuel == 5: raise ValueError( f"Document {numero_source} deja transforme (statut=5). " @@ -1130,12 +1129,10 @@ class SageConnector: f"Impossible de le transformer." ) - # CORRECTION: Statut 3 ou 4 = document déjà réalisé/livré if statut_actuel in [3, 4]: raise ValueError( f"Document {numero_source} deja realise (statut={statut_actuel}). " - f"Ce document a deja ete transforme partiellement ou totalement. " - f"Verifiez si une commande/BL/facture n'existe pas deja pour ce document." + f"Ce document a deja ete transforme partiellement ou totalement." ) # Forcer statut "Accepté" si brouillon @@ -1157,25 +1154,7 @@ class SageConnector: f"Echec changement statut: toujours a {nouveau_statut}" ) except Exception as e: - raise RuntimeError( - f"Impossible de changer le statut du devis: {e}. " - f"Le devis doit etre accepte avant transformation." - ) - - # Récupérer client - client_code = "" - try: - client_obj = getattr(doc_source, "Client", None) - if client_obj: - client_obj.Read() - client_code = getattr(client_obj, "CT_Num", "").strip() - except Exception as e: - logger.error(f"Erreur lecture client: {e}") - - if not client_code: - raise ValueError( - f"Client introuvable pour document {numero_source}" - ) + raise RuntimeError(f"Impossible de changer le statut: {e}") # ===== TRANSACTION ===== transaction_active = False @@ -1183,214 +1162,119 @@ class SageConnector: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug("[TRANSFORM] Transaction demarree") + except AttributeError: + # BeginTrans n'existe pas sur cette version + logger.debug( + "[TRANSFORM] BeginTrans non disponible, continue sans transaction" + ) except Exception as e: logger.warning(f"[TRANSFORM] BeginTrans echoue: {e}") try: - # CRÉATION DOCUMENT CIBLE - logger.info(f"[TRANSFORM] CreateProcess_Document({type_cible})...") + # ✅✅✅ MÉTHODE NATIVE SAGE: TransformInto() ✅✅✅ + logger.info(f"[TRANSFORM] Appel TransformInto({type_cible})...") try: - process = self.cial.CreateProcess_Document(type_cible) - except Exception as e: - logger.error( - f"[TRANSFORM] CreateProcess_Document echoue pour type {type_cible}: {e}" - ) - raise RuntimeError( - f"Sage refuse de creer un document de type {type_cible}. " - f"Verifiez la configuration Sage et les permissions. " - f"Erreur: {e}" - ) + # La méthode TransformInto() retourne le nouveau document + doc_cible = doc_source.TransformInto(type_cible) - doc_cible = process.Document + if doc_cible is None: + raise RuntimeError( + "TransformInto() a retourne None. " + "Verifiez la configuration Sage et les autorisations." + ) - try: - doc_cible = win32com.client.CastTo( - doc_cible, "IBODocumentVente3" - ) - except: - pass + logger.info("[TRANSFORM] TransformInto() execute avec succes") - logger.info(f"[TRANSFORM] Document cible cree (type {type_cible})") - - # Associer client - try: - factory_client = self.cial.CptaApplication.FactoryClient - persist_client = factory_client.ReadNumero(client_code) - - if not persist_client: - raise ValueError(f"Client {client_code} introuvable") - - client_obj_cible = win32com.client.CastTo( - persist_client, "IBOClient3" - ) - client_obj_cible.Read() - doc_cible.SetDefaultClient(client_obj_cible) - doc_cible.Write() - logger.info(f"[TRANSFORM] Client {client_code} associe") - except Exception as e: - logger.error(f"[TRANSFORM] Erreur association client: {e}") - raise - - # Date - import pywintypes - - doc_cible.DO_Date = pywintypes.Time(datetime.now()) - - # Référence - try: - doc_cible.DO_Ref = f"Trans. {numero_source}" - except: - pass - - # ===== COPIE LIGNES ===== - try: - factory_lignes_source = doc_source.FactoryDocumentLigne - factory_lignes_cible = doc_cible.FactoryDocumentLigne - except: - factory_lignes_source = doc_source.FactoryDocumentVenteLigne - factory_lignes_cible = doc_cible.FactoryDocumentVenteLigne - - factory_article = self.cial.FactoryArticle - index = 1 - nb_lignes = 0 - erreurs_lignes = [] - - while index <= 1000: + # Cast vers le bon type try: - ligne_source_p = factory_lignes_source.List(index) - if ligne_source_p is None: - break - - ligne_source = win32com.client.CastTo( - ligne_source_p, "IBODocumentLigne3" + doc_cible = win32com.client.CastTo( + doc_cible, "IBODocumentVente3" ) - ligne_source.Read() + except: + pass - # Créer ligne cible - ligne_cible_p = factory_lignes_cible.Create() - ligne_cible = win32com.client.CastTo( - ligne_cible_p, "IBODocumentLigne3" - ) + # Lire le document cible + doc_cible.Read() - # Récupérer article - article_ref = "" - try: - article_ref = getattr( - ligne_source, "AR_Ref", "" - ).strip() - if not article_ref: - article_obj = getattr(ligne_source, "Article", None) - if article_obj: - article_obj.Read() - article_ref = getattr( - article_obj, "AR_Ref", "" - ).strip() - except: - pass - - # Associer article - if article_ref: - try: - persist_article = factory_article.ReadReference( - article_ref - ) - if persist_article: - article_obj = win32com.client.CastTo( - persist_article, "IBOArticle3" - ) - article_obj.Read() - - quantite = float( - getattr(ligne_source, "DL_Qte", 1.0) - ) - - try: - ligne_cible.SetDefaultArticleReference( - article_ref, quantite - ) - except: - ligne_cible.SetDefaultArticle( - article_obj, quantite - ) - except Exception as e: - logger.debug( - f"Erreur association article {article_ref}: {e}" - ) - - # Copier propriétés - ligne_cible.DL_Design = getattr( - ligne_source, "DL_Design", "" - ) - ligne_cible.DL_Qte = float( - getattr(ligne_source, "DL_Qte", 0.0) - ) - ligne_cible.DL_PrixUnitaire = float( - getattr(ligne_source, "DL_PrixUnitaire", 0.0) - ) - - # Remise - try: - remise = float( - getattr(ligne_source, "DL_Remise01REM_Valeur", 0.0) - ) - if remise > 0: - ligne_cible.DL_Remise01REM_Valeur = remise - ligne_cible.DL_Remise01REM_Type = 0 - except: - pass - - ligne_cible.Write() - nb_lignes += 1 - index += 1 - - except Exception as e: - erreurs_lignes.append(f"Ligne {index}: {str(e)}") - logger.debug(f"Erreur ligne {index}: {e}") - index += 1 - if index > 1000: - break - - if nb_lignes == 0: - raise RuntimeError( - f"Aucune ligne copiee. Erreurs: {'; '.join(erreurs_lignes)}" - ) - - logger.info(f"[TRANSFORM] {nb_lignes} lignes copiees") - - # ===== VALIDATION ===== - doc_cible.Write() - logger.info("[TRANSFORM] Document cible ecrit") - - process.Process() - logger.info("[TRANSFORM] Process() execute") - - # Récupérer numéro - numero_cible = None - try: - doc_result = process.DocumentResult - if doc_result: - doc_result = win32com.client.CastTo( - doc_result, "IBODocumentVente3" - ) - doc_result.Read() - numero_cible = getattr(doc_result, "DO_Piece", "") - except: - pass - - if not numero_cible: + # Récupérer le numéro numero_cible = getattr(doc_cible, "DO_Piece", "") - if not numero_cible: - raise RuntimeError("Numero document cible vide apres creation") + if not numero_cible: + raise RuntimeError( + "Numero document cible vide apres transformation" + ) - # Commit + # Compter les lignes + try: + factory_lignes = doc_cible.FactoryDocumentLigne + except: + factory_lignes = doc_cible.FactoryDocumentVenteLigne + + nb_lignes = 0 + index = 1 + while index <= 1000: + try: + ligne_p = factory_lignes.List(index) + if ligne_p is None: + break + nb_lignes += 1 + index += 1 + except: + break + + logger.info( + f"[TRANSFORM] Document cible cree: {numero_cible} avec {nb_lignes} lignes" + ) + + 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." + ) + + 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" + ) + else: + raise RuntimeError( + f"Erreur Sage lors de la transformation: {e}\n" + f"Consultez les logs Sage pour plus de details." + ) + + # Commit transaction if transaction_active: - self.cial.CptaApplication.CommitTrans() - logger.info("[TRANSFORM] Transaction committee") + try: + self.cial.CptaApplication.CommitTrans() + logger.info("[TRANSFORM] Transaction committee") + except: + pass # MAJ statut source -> Transformé try: + doc_source.Read() # Re-lire au cas où doc_source.DO_Statut = 5 doc_source.Write() logger.info( @@ -1446,6 +1330,7 @@ class SageConnector: getattr(doc, "DO_Type", -1) == type_doc and getattr(doc, "DO_Piece", "") == numero ): + logger.info(f"[TRANSFORM] Document trouve a l'index {index}") return persist index += 1 @@ -1454,7 +1339,8 @@ class SageConnector: continue return None - except: + except Exception as e: + logger.error(f"[TRANSFORM] Erreur recherche document: {e}") return None # =========================================================================