From c522aa5a64dafd02c6ff83ef69ddd64038c6b718 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Fri, 28 Nov 2025 06:14:19 +0300 Subject: [PATCH] Resolving error on getting all command list and creating a "command" --- main.py | 126 ++++++++++++++++++------- sage_connector.py | 229 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 282 insertions(+), 73 deletions(-) diff --git a/main.py b/main.py index 196a550..2879042 100644 --- a/main.py +++ b/main.py @@ -610,68 +610,124 @@ def contact_read(req: CodeRequest): @app.post("/sage/commandes/list", dependencies=[Depends(verify_token)]) def commandes_list(limit: int = 100, statut: Optional[int] = None): - """Liste toutes les commandes""" + """ + 📋 Liste toutes les commandes + ✅ CORRECTIONS: Gestion robuste des erreurs + logging détaillé + """ 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 commandes = [] index = 1 - max_iterations = limit * 3 + max_iterations = limit * 10 # Plus de marge + erreurs_consecutives = 0 + max_erreurs = 100 - while len(commandes) < limit and index < max_iterations: + logger.info(f"🔍 Recherche commandes (limit={limit}, statut={statut})") + + while ( + len(commandes) < limit + and index < max_iterations + and erreurs_consecutives < max_erreurs + ): 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() - # Filtrer commandes (type 3) - if getattr(doc, "DO_Type", -1) != 3: + # ✅ 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: index += 1 continue doc_statut = getattr(doc, "DO_Statut", 0) - if statut is None or doc_statut == statut: - # Charger client - client_code = "" - client_intitule = "" + logger.debug( + f"Index {index}: Type={doc_type}, Statut={doc_statut}, " + f"Numéro={getattr(doc, 'DO_Piece', '?')}" + ) - 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 + # Filtre statut + if statut is not None and doc_statut != statut: + index += 1 + continue - 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, - } + # ✅ CORRECTION 2: 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() + 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", "") + + commande = { + "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, + } + + 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 - index += 1 - except: - index += 1 - continue + 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} + except HTTPException: + raise except Exception as e: - logger.error(f"Erreur liste commandes: {e}") + logger.error(f"❌ Erreur liste commandes: {e}", exc_info=True) raise HTTPException(500, str(e)) diff --git a/sage_connector.py b/sage_connector.py index ea3ea2c..78f2c36 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1027,11 +1027,52 @@ class SageConnector: def transformer_document(self, numero_source, type_source, type_cible): """ Transformation avec transaction - ✅ CORRIGÉ: Gestion du statut avant transformation + ✅ CORRECTIONS: + - Validation stricte des types + - Gestion explicite des statuts Sage + - Meilleure gestion d'erreurs """ if not self.cial: raise RuntimeError("Connexion Sage non établie") + # ✅ CORRECTION 1: Convertir en int si enum passé + 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}" + ) + + # ✅ CORRECTION 2: Validation des types AVANT d'accéder à Sage + 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}" + ) + + # ✅ CORRECTION 3: Matrice de transformations autorisées par Sage + # Basé sur la doc 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", + } + + 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() + ) + ) + try: with self._com_context(), self._lock_com: # ===== LECTURE SOURCE ===== @@ -1039,49 +1080,89 @@ class SageConnector: persist_source = factory.ReadPiece(type_source, numero_source) if not persist_source: - raise ValueError(f"Document {numero_source} introuvable") + # ✅ CORRECTION 4: Chercher dans List() si ReadPiece échoue + logger.warning(f"ReadPiece échoué, recherche dans List()...") + persist_source = self._find_document_in_list( + numero_source, type_source + ) + + if not persist_source: + raise ValueError( + f"Document {numero_source} (type {type_source}) introuvable" + ) doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3") doc_source.Read() - # ✅ VÉRIFICATION STATUT + # ✅ CORRECTION 5: Vérifications de statut AVANT transformation statut_actuel = getattr(doc_source, "DO_Statut", 0) + type_reel = getattr(doc_source, "DO_Type", -1) + logger.info( - f"📊 Statut actuel du document {numero_source}: {statut_actuel}" + f"📊 Document source: type={type_reel}, statut={statut_actuel}, " + f"numéro={numero_source}" ) - # ✅ BLOQUER SI DÉJÀ TRANSFORMÉ - if statut_actuel == 5: + # Vérifier cohérence type + if type_reel != type_source: raise ValueError( - f"❌ Le document {numero_source} a déjà été transformé (statut=5)" + f"Incohérence: document {numero_source} est de type {type_reel}, " + f"pas de type {type_source}" ) - # ✅ FORCER STATUT "ACCEPTÉ" (2) SI BROUILLON (0) + # ✅ 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é + + if statut_actuel == 5: + raise ValueError( + f"Document {numero_source} déjà transformé (statut=5). " + f"Impossible de le transformer à nouveau." + ) + + if statut_actuel == 6: + raise ValueError( + f"Document {numero_source} annulé (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 logger.warning( - f"⚠️ Devis {numero_source} en brouillon (statut=0), " - f"passage à 'Accepté' (statut=2)" + f"⚠️ Devis en brouillon (statut=0), " + f"passage à 'Accepté' (statut=2) requis pour transformation" ) try: doc_source.DO_Statut = 2 # Accepté doc_source.Write() logger.info(f"✅ Statut changé: 0 → 2") - except Exception as e: - logger.warning(f"Impossible de changer le statut: {e}") - # Récupérer le client + # Re-lire pour confirmer + doc_source.Read() + nouveau_statut = getattr(doc_source, "DO_Statut", 0) + if nouveau_statut != 2: + raise RuntimeError( + f"Échec changement statut: toujours à {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." + ) + + # 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", "") - except: - pass + 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"Impossible de récupérer le client du document {numero_source}" + f"Client introuvable pour document {numero_source}" ) # ===== TRANSACTION ===== @@ -1094,8 +1175,20 @@ class SageConnector: logger.warning(f"⚠️ BeginTrans échoué: {e}") try: - # ✅ CRÉATION DOCUMENT CIBLE - process = self.cial.CreateProcess_Document(type_cible) + # ✅ CORRECTION 8: Créer le process avec le type cible VALIDÉ + logger.info(f"🔨 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}" + ) + raise RuntimeError( + f"Sage refuse de créer un document de type {type_cible}. " + f"Erreur: {e}" + ) + doc_cible = process.Document try: @@ -1107,19 +1200,21 @@ class SageConnector: logger.info(f"📄 Document cible créé (type {type_cible})") - # Associer le client + # Associer client try: factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero(client_code) - if persist_client: - 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"👤 Client {client_code} associé") + 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"👤 Client {client_code} associé") except Exception as e: logger.error(f"❌ Erreur association client: {e}") raise @@ -1146,6 +1241,7 @@ class SageConnector: factory_article = self.cial.FactoryArticle index = 1 nb_lignes = 0 + erreurs_lignes = [] while index <= 1000: try: @@ -1164,7 +1260,7 @@ class SageConnector: ligne_cible_p, "IBODocumentLigne3" ) - # Récupérer référence article + # Récupérer article article_ref = "" try: article_ref = getattr( @@ -1180,7 +1276,7 @@ class SageConnector: except: pass - # Associer article si disponible + # Associer article if article_ref: try: persist_article = factory_article.ReadReference( @@ -1236,37 +1332,61 @@ class SageConnector: 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 copiée. Erreurs: {'; '.join(erreurs_lignes)}" + ) + + logger.info(f"✅ {nb_lignes} lignes copiées") + # ===== VALIDATION ===== doc_cible.Write() - process.Process() + logger.info("💾 Document cible écrit") - numero_cible = getattr(doc_cible, "DO_Piece", "") + process.Process() + logger.info("⚙️ Process() exécuté") + + # 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: - raise RuntimeError("Numéro document cible vide") + numero_cible = getattr(doc_cible, "DO_Piece", "") + + if not numero_cible: + raise RuntimeError("Numéro document cible vide après création") # Commit if transaction_active: self.cial.CptaApplication.CommitTrans() logger.info("✅ Transaction committée") - # ✅ MAJ STATUT SOURCE → TRANSFORMÉ (5) + # MAJ statut source → Transformé try: doc_source.DO_Statut = 5 # Transformé doc_source.Write() - logger.info( - f"✅ Statut source mis à jour: {statut_actuel} → 5 (TRANSFORMÉ)" - ) + logger.info(f"✅ Statut source mis à jour: → 5 (TRANSFORMÉ)") except Exception as e: logger.warning(f"⚠️ Impossible de MAJ statut source: {e}") logger.info( - f"✅ Transformation: {numero_source} ({type_source}) → " + f"✅✅✅ TRANSFORMATION RÉUSSIE: " + f"{numero_source} ({type_source}) → " f"{numero_cible} ({type_cible}) - {nb_lignes} lignes" ) @@ -1290,6 +1410,39 @@ class SageConnector: logger.error(f"❌ Erreur transformation: {e}", exc_info=True) raise RuntimeError(f"Échec 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 + """ + try: + factory = self.cial.FactoryDocumentVente + index = 1 + + while index < 10000: + 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 + and getattr(doc, "DO_Piece", "") == numero + ): + return persist + + index += 1 + except: + index += 1 + continue + + return None + except: + return None + # ========================================================================= # CHAMPS LIBRES (US-A3) # =========================================================================