diff --git a/main.py b/main.py index 2879042..fac166c 100644 --- a/main.py +++ b/main.py @@ -866,6 +866,201 @@ def cache_info_get(): raise HTTPException(500, str(e)) +# Script à ajouter temporairement dans main.py pour diagnostiquer + + +@app.get("/sage/devis/{numero}/diagnostic", dependencies=[Depends(verify_token)]) +def diagnostiquer_devis(numero: str): + """ + ENDPOINT DE DIAGNOSTIC: Affiche TOUTES les infos d'un devis + + Permet de comprendre pourquoi un devis ne peut pas être transformé + """ + try: + if not sage or not sage.cial: + raise HTTPException(503, "Service Sage indisponible") + + with sage._com_context(), sage._lock_com: + factory = sage.cial.FactoryDocumentVente + + # Essayer ReadPiece + persist = factory.ReadPiece(0, numero) + + # Si échec, chercher dans List() + if not persist: + logger.info(f"[DIAG] ReadPiece echoue, recherche dans List()...") + index = 1 + while index < 10000: + try: + persist_test = factory.List(index) + if persist_test is None: + break + + doc_test = win32com.client.CastTo( + persist_test, "IBODocumentVente3" + ) + doc_test.Read() + + if ( + getattr(doc_test, "DO_Type", -1) == 0 + and getattr(doc_test, "DO_Piece", "") == numero + ): + persist = persist_test + logger.info(f"[DIAG] Trouve a l'index {index}") + break + + index += 1 + except: + index += 1 + + if not persist: + raise HTTPException(404, f"Devis {numero} introuvable") + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + # EXTRACTION COMPLÈTE + diagnostic = { + "numero": getattr(doc, "DO_Piece", ""), + "type": getattr(doc, "DO_Type", -1), + "statut": getattr(doc, "DO_Statut", -1), + "statut_libelle": { + 0: "Brouillon", + 1: "Soumis", + 2: "Accepte", + 3: "Realise partiellement", + 4: "Realise totalement", + 5: "Transforme", + 6: "Annule", + }.get(getattr(doc, "DO_Statut", -1), "Inconnu"), + "date": str(getattr(doc, "DO_Date", "")), + "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), + "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), + "est_transformable": False, + "raison_blocage": None, + } + + # Client + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + diagnostic["client_code"] = getattr( + client_obj, "CT_Num", "" + ).strip() + diagnostic["client_intitule"] = getattr( + client_obj, "CT_Intitule", "" + ).strip() + except Exception as e: + diagnostic["erreur_client"] = str(e) + + # Lignes + try: + factory_lignes = doc.FactoryDocumentLigne + except: + factory_lignes = doc.FactoryDocumentVenteLigne + + lignes = [] + index = 1 + while index <= 100: + try: + ligne_p = factory_lignes.List(index) + if ligne_p is None: + break + + ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") + ligne.Read() + + article_ref = "" + try: + article_ref = getattr(ligne, "AR_Ref", "").strip() + if not article_ref: + article_obj = getattr(ligne, "Article", None) + if article_obj: + article_obj.Read() + article_ref = getattr(article_obj, "AR_Ref", "").strip() + except: + pass + + lignes.append( + { + "index": index, + "article": article_ref, + "designation": getattr(ligne, "DL_Design", ""), + "quantite": float(getattr(ligne, "DL_Qte", 0.0)), + "prix_unitaire": float( + getattr(ligne, "DL_PrixUnitaire", 0.0) + ), + "montant_ht": float(getattr(ligne, "DL_MontantHT", 0.0)), + } + ) + + index += 1 + except: + break + + diagnostic["nb_lignes"] = len(lignes) + diagnostic["lignes"] = lignes + + # ANALYSE TRANSFORMABILITÉ + statut = diagnostic["statut"] + + if statut == 5: + diagnostic["raison_blocage"] = "Document deja transforme (statut=5)" + elif statut == 6: + diagnostic["raison_blocage"] = "Document annule (statut=6)" + elif statut in [3, 4]: + diagnostic["raison_blocage"] = ( + f"Document deja realise partiellement ou totalement (statut={statut}). " + f"Une commande/BL/facture existe probablement deja." + ) + diagnostic["suggestion"] = ( + "Cherchez les documents lies a ce devis dans Sage. " + "Il a peut-etre deja ete transforme manuellement." + ) + elif statut == 0: + diagnostic["est_transformable"] = True + diagnostic["action_requise"] = ( + "Statut 'Brouillon'. Le systeme le passera automatiquement a 'Accepte' " + "avant transformation." + ) + elif statut == 2: + diagnostic["est_transformable"] = True + diagnostic["action_requise"] = ( + "Statut 'Accepte'. Transformation possible." + ) + else: + diagnostic["raison_blocage"] = f"Statut inconnu ou non gere: {statut}" + + # Champs libres (pour Universign, etc.) + champs_libres = {} + try: + for champ in ["UniversignID", "DerniereRelance", "DO_Ref"]: + try: + valeur = getattr(doc, f"DO_{champ}", None) + if valeur: + champs_libres[champ] = str(valeur) + except: + pass + except: + pass + + if champs_libres: + diagnostic["champs_libres"] = champs_libres + + logger.info( + f"[DIAG] Devis {numero}: statut={statut}, transformable={diagnostic['est_transformable']}" + ) + + return {"success": True, "diagnostic": diagnostic} + + except HTTPException: + raise + except Exception as e: + logger.error(f"[DIAG] Erreur diagnostic: {e}", exc_info=True) + raise HTTPException(500, str(e)) + + # ===================================================== # LANCEMENT # ===================================================== diff --git a/sage_connector.py b/sage_connector.py index 78f2c36..14e4579 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1026,25 +1026,27 @@ class SageConnector: def transformer_document(self, numero_source, type_source, type_cible): """ - Transformation avec transaction - ✅ CORRECTIONS: - - Validation stricte des types - - Gestion explicite des statuts Sage - - Meilleure gestion d'erreurs + Transformation de document avec gestion complète des statuts + + CORRECTIONS: + - Pas d'émojis dans les logs + - Validation stricte des statuts Sage + - Gestion des documents "Réalisé partiellement" + - Meilleure détection d'erreurs """ if not self.cial: - raise RuntimeError("Connexion Sage non établie") + raise RuntimeError("Connexion Sage non etablie") - # ✅ CORRECTION 1: Convertir en int si enum passé + # Convertir en int si enum type_source = int(type_source) type_cible = int(type_cible) logger.info( - f"🔄 Transformation demandée: {numero_source} " - f"(type {type_source}) → type {type_cible}" + f"[TRANSFORM] Demande: {numero_source} " + f"(type {type_source}) -> type {type_cible}" ) - # ✅ CORRECTION 2: Validation des types AVANT d'accéder à Sage + # 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( @@ -1052,24 +1054,23 @@ class SageConnector: f"Valeurs valides: {types_valides}" ) - # ✅ CORRECTION 3: Matrice de transformations autorisées par Sage - # Basé sur la doc Sage 100c + # Matrice de transformations Sage 100c transformations_autorisees = { - (0, 3): "Devis → Commande", - (0, 1): "Devis → Bon de livraison", - (3, 1): "Commande → Bon de livraison", - (3, 4): "Commande → Préparation", - (1, 5): "Bon de livraison → Facture", - (4, 1): "Préparation → Bon de livraison", + (0, 3): "Devis -> Commande", + (0, 1): "Devis -> Bon de livraison", + (3, 1): "Commande -> Bon de livraison", + (3, 4): "Commande -> Preparation", + (1, 5): "Bon de livraison -> Facture", + (4, 1): "Preparation -> Bon de livraison", } if (type_source, type_cible) not in transformations_autorisees: raise ValueError( - f"❌ Transformation non autorisée par Sage: " - f"{type_source} → {type_cible}. " - f"Transformations valides:\n" - + "\n".join( - f" - {k}: {v}" for k, v in transformations_autorisees.items() + f"Transformation non autorisee par Sage: " + f"{type_source} -> {type_cible}. " + f"Valides: " + + ", ".join( + f"{k[0]}->{k[1]}" for k in transformations_autorisees.keys() ) ) @@ -1080,8 +1081,7 @@ class SageConnector: persist_source = factory.ReadPiece(type_source, numero_source) if not persist_source: - # ✅ CORRECTION 4: Chercher dans List() si ReadPiece échoue - logger.warning(f"ReadPiece échoué, recherche dans List()...") + logger.warning(f"ReadPiece failed, searching in List()...") persist_source = self._find_document_in_list( numero_source, type_source ) @@ -1094,60 +1094,72 @@ class SageConnector: doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3") doc_source.Read() - # ✅ CORRECTION 5: Vérifications de statut AVANT transformation + # VÉRIFICATIONS STATUT statut_actuel = getattr(doc_source, "DO_Statut", 0) type_reel = getattr(doc_source, "DO_Type", -1) logger.info( - f"📊 Document source: type={type_reel}, statut={statut_actuel}, " - f"numéro={numero_source}" + f"[TRANSFORM] Document source: type={type_reel}, " + f"statut={statut_actuel}, numero={numero_source}" ) # Vérifier cohérence type if type_reel != type_source: raise ValueError( - f"Incohérence: document {numero_source} est de type {type_reel}, " + f"Incoherence: document {numero_source} est de type {type_reel}, " f"pas de type {type_source}" ) - # ✅ CORRECTION 6: Règles de statut Sage pour transformations - # Statuts Sage: 0=Brouillon, 1=Soumis, 2=Accepté, 3=Réalisé partiellement, - # 4=Réalisé totalement, 5=Transformé, 6=Annulé + # 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. if statut_actuel == 5: raise ValueError( - f"Document {numero_source} déjà transformé (statut=5). " - f"Impossible de le transformer à nouveau." + f"Document {numero_source} deja transforme (statut=5). " + f"Impossible de le transformer a nouveau." ) if statut_actuel == 6: raise ValueError( - f"Document {numero_source} annulé (statut=6). " + f"Document {numero_source} annule (statut=6). " f"Impossible de le transformer." ) - # ✅ CORRECTION 7: Forcer statut "Accepté" si nécessaire - if type_source == 0 and statut_actuel == 0: # Devis brouillon + # 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." + ) + + # Forcer statut "Accepté" si brouillon + if type_source == 0 and statut_actuel == 0: logger.warning( - f"⚠️ Devis en brouillon (statut=0), " - f"passage à 'Accepté' (statut=2) requis pour transformation" + f"[TRANSFORM] Devis en brouillon (statut=0), " + f"passage a 'Accepte' (statut=2)" ) try: - doc_source.DO_Statut = 2 # Accepté + doc_source.DO_Statut = 2 doc_source.Write() - logger.info(f"✅ Statut changé: 0 → 2") + logger.info(f"[TRANSFORM] Statut change: 0 -> 2") - # Re-lire pour confirmer + # Re-lire doc_source.Read() nouveau_statut = getattr(doc_source, "DO_Statut", 0) if nouveau_statut != 2: raise RuntimeError( - f"Échec changement statut: toujours à {nouveau_statut}" + 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 être accepté avant transformation." + f"Le devis doit etre accepte avant transformation." ) # Récupérer client @@ -1170,22 +1182,23 @@ class SageConnector: try: self.cial.CptaApplication.BeginTrans() transaction_active = True - logger.debug("✅ Transaction démarrée") + logger.debug("[TRANSFORM] Transaction demarree") except Exception as e: - logger.warning(f"⚠️ BeginTrans échoué: {e}") + logger.warning(f"[TRANSFORM] BeginTrans echoue: {e}") try: - # ✅ CORRECTION 8: Créer le process avec le type cible VALIDÉ - logger.info(f"🔨 CreateProcess_Document({type_cible})...") + # CRÉATION DOCUMENT CIBLE + logger.info(f"[TRANSFORM] CreateProcess_Document({type_cible})...") try: process = self.cial.CreateProcess_Document(type_cible) except Exception as e: logger.error( - f"❌ CreateProcess_Document échoué pour type {type_cible}: {e}" + f"[TRANSFORM] CreateProcess_Document echoue pour type {type_cible}: {e}" ) raise RuntimeError( - f"Sage refuse de créer un document de type {type_cible}. " + f"Sage refuse de creer un document de type {type_cible}. " + f"Verifiez la configuration Sage et les permissions. " f"Erreur: {e}" ) @@ -1198,7 +1211,7 @@ class SageConnector: except: pass - logger.info(f"📄 Document cible créé (type {type_cible})") + logger.info(f"[TRANSFORM] Document cible cree (type {type_cible})") # Associer client try: @@ -1214,9 +1227,9 @@ class SageConnector: client_obj_cible.Read() doc_cible.SetDefaultClient(client_obj_cible) doc_cible.Write() - logger.info(f"👤 Client {client_code} associé") + logger.info(f"[TRANSFORM] Client {client_code} associe") except Exception as e: - logger.error(f"❌ Erreur association client: {e}") + logger.error(f"[TRANSFORM] Erreur association client: {e}") raise # Date @@ -1340,17 +1353,17 @@ class SageConnector: if nb_lignes == 0: raise RuntimeError( - f"Aucune ligne copiée. Erreurs: {'; '.join(erreurs_lignes)}" + f"Aucune ligne copiee. Erreurs: {'; '.join(erreurs_lignes)}" ) - logger.info(f"✅ {nb_lignes} lignes copiées") + logger.info(f"[TRANSFORM] {nb_lignes} lignes copiees") # ===== VALIDATION ===== doc_cible.Write() - logger.info("💾 Document cible écrit") + logger.info("[TRANSFORM] Document cible ecrit") process.Process() - logger.info("⚙️ Process() exécuté") + logger.info("[TRANSFORM] Process() execute") # Récupérer numéro numero_cible = None @@ -1369,24 +1382,28 @@ class SageConnector: numero_cible = getattr(doc_cible, "DO_Piece", "") if not numero_cible: - raise RuntimeError("Numéro document cible vide après création") + raise RuntimeError("Numero document cible vide apres creation") # Commit if transaction_active: self.cial.CptaApplication.CommitTrans() - logger.info("✅ Transaction committée") + logger.info("[TRANSFORM] Transaction committee") - # MAJ statut source → Transformé + # MAJ statut source -> Transformé try: - doc_source.DO_Statut = 5 # Transformé + doc_source.DO_Statut = 5 doc_source.Write() - logger.info(f"✅ Statut source mis à jour: → 5 (TRANSFORMÉ)") + logger.info( + f"[TRANSFORM] Statut source mis a jour: -> 5 (TRANSFORME)" + ) except Exception as e: - logger.warning(f"⚠️ Impossible de MAJ statut source: {e}") + logger.warning( + f"[TRANSFORM] Impossible de MAJ statut source: {e}" + ) logger.info( - f"✅✅✅ TRANSFORMATION RÉUSSIE: " - f"{numero_source} ({type_source}) → " + f"[TRANSFORM] SUCCES: " + f"{numero_source} ({type_source}) -> " f"{numero_cible} ({type_cible}) - {nb_lignes} lignes" ) @@ -1401,20 +1418,17 @@ class SageConnector: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() - logger.error("❌ Transaction annulée") + logger.error("[TRANSFORM] Transaction annulee") except: pass raise except Exception as e: - logger.error(f"❌ Erreur transformation: {e}", exc_info=True) - raise RuntimeError(f"Échec transformation: {str(e)}") + logger.error(f"[TRANSFORM] Erreur: {e}", exc_info=True) + raise RuntimeError(f"Echec transformation: {str(e)}") def _find_document_in_list(self, numero, type_doc): - """ - ✅ NOUVEAU: Cherche un document dans List() si ReadPiece échoue - Utile pour les documents en brouillon - """ + """Cherche un document dans List() si ReadPiece échoue""" try: factory = self.cial.FactoryDocumentVente index = 1