From 3aadc67abf0621e5f5b317a5dc991538b0181c60 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Sat, 6 Dec 2025 14:53:16 +0300 Subject: [PATCH] Made GET fournisseurs --- main.py | 1119 +++------------------------------------------ sage_connector.py | 465 ++++++++++--------- 2 files changed, 299 insertions(+), 1285 deletions(-) diff --git a/main.py b/main.py index 3aed6f6..9025c11 100644 --- a/main.py +++ b/main.py @@ -756,8 +756,8 @@ 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 - ✅ CORRECTION: Filtre sur type 60 (FACTURE) + 📋 Liste toutes les factures avec leurs lignes + ✅ CORRECTION: Filtre sur type 60 (FACTURE) + chargement des lignes """ try: with sage._com_context(), sage._lock_com: @@ -792,23 +792,65 @@ def factures_list(limit: int = 100, statut: Optional[int] = None): if client_obj: client_obj.Read() client_code = getattr(client_obj, "CT_Num", "").strip() - client_intitule = getattr( - client_obj, "CT_Intitule", "" - ).strip() + client_intitule = getattr(client_obj, "CT_Intitule", "").strip() 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, - } - ) + # ✅ NOUVEAU : Charger les lignes + lignes = [] + try: + factory_lignes = getattr(doc, "FactoryDocumentLigne", None) + if not factory_lignes: + factory_lignes = getattr(doc, "FactoryDocumentVenteLigne", None) + + if factory_lignes: + ligne_index = 1 + while ligne_index <= 100: + try: + ligne_persist = factory_lignes.List(ligne_index) + if ligne_persist is None: + break + + ligne = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") + ligne.Read() + + # Charger rĂ©fĂ©rence article + 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({ + "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)) + }) + + ligne_index += 1 + except Exception as e: + logger.debug(f"Erreur ligne {ligne_index}: {e}") + break + except Exception as e: + logger.debug(f"Erreur chargement lignes: {e}") + + 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, + "lignes": lignes # ✅ AJOUT + }) index += 1 except: @@ -2535,64 +2577,24 @@ def prospect_get(req: CodeRequest): # ENDPOINTS - FOURNISSEURS # ===================================================== @app.post("/sage/fournisseurs/list", dependencies=[Depends(verify_token)]) -def fournisseurs_list_direct(req: FiltreRequest): +def fournisseurs_list(req: FiltreRequest): """ - ⚡ ENDPOINT DIRECT : Liste fournisseurs SANS passer par le cache + ⚡ ENDPOINT DIRECT : Liste fournisseurs via FactoryFournisseur - Lecture directe depuis FactoryFournisseur - toujours Ă  jour + ✅ Utilise la mĂ©thode corrigĂ©e qui ne dĂ©pend pas du cache """ try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") + # ✅ Appel direct Ă  la mĂ©thode corrigĂ©e + fournisseurs = sage.lister_tous_fournisseurs(req.filtre) + + logger.info(f"✅ {len(fournisseurs)} fournisseurs retournĂ©s via endpoint") + + return {"success": True, "data": fournisseurs} - with sage._com_context(), sage._lock_com: - factory = sage.cial.CptaApplication.FactoryFournisseur - fournisseurs = [] - index = 1 - max_iterations = 10000 - erreurs_consecutives = 0 - max_erreurs = 50 - - filtre_lower = req.filtre.lower() if req.filtre else "" - - logger.info(f"🔍 Lecture directe fournisseurs (filtre='{req.filtre}')") - - while index < max_iterations and erreurs_consecutives < max_erreurs: - try: - persist = factory.List(index) - if persist is None: - break - - obj = sage._cast_client(persist) - if obj: - data = sage._extraire_client(obj) - - # Appliquer le filtre si nĂ©cessaire - if not filtre_lower or \ - filtre_lower in data["numero"].lower() or \ - filtre_lower in data["intitule"].lower(): - fournisseurs.append(data) - - erreurs_consecutives = 0 - - index += 1 - - except Exception as e: - erreurs_consecutives += 1 - index += 1 - - if erreurs_consecutives >= max_erreurs: - logger.warning(f"⚠ ArrĂȘt aprĂšs {max_erreurs} erreurs consĂ©cutives") - break - - logger.info(f"✅ {len(fournisseurs)} fournisseurs retournĂ©s (lecture directe)") - return {"success": True, "data": fournisseurs} - except Exception as e: - logger.error(f"❌ Erreur lecture directe fournisseurs: {e}", exc_info=True) + logger.error(f"❌ Erreur liste fournisseurs: {e}", exc_info=True) raise HTTPException(500, str(e)) - @app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)]) def fournisseur_get(req: CodeRequest): """ @@ -2667,991 +2669,6 @@ def livraison_get(req: CodeRequest): logger.error(f"Erreur lecture livraison: {e}") raise HTTPException(500, str(e)) -# À ajouter dans main.py (Windows Gateway) - -@app.get("/sage/diagnostic/fournisseurs-detection", dependencies=[Depends(verify_token)]) -def diagnostiquer_detection_fournisseurs(): - """ - 🔍 DIAGNOSTIC : DĂ©couvre comment identifier les fournisseurs dans votre Sage - - Teste plusieurs mĂ©thodes : - - CT_Type = 1 (mĂ©thode classique) - - CT_Qualite (mĂ©thode moderne : 0=Aucune, 1=Client, 2=Fournisseur, 3=Client+Fournisseur) - - CT_TypeTiers - - PrĂ©sence dans FactoryFournisseur - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - factory_client = sage.cial.CptaApplication.FactoryClient - - resultats = { - "methodes_detectees": [], - "tiers_analyses": [], - "recommandation": None - } - - # Scanner les 50 premiers tiers - index = 1 - while index <= 50: - try: - persist = factory_client.List(index) - if persist is None: - break - - tiers = sage._cast_client(persist) - if not tiers: - index += 1 - continue - - analyse = { - "index": index, - "numero": getattr(tiers, "CT_Num", ""), - "intitule": getattr(tiers, "CT_Intitule", ""), - "ct_type": getattr(tiers, "CT_Type", None), - "ct_prospect": getattr(tiers, "CT_Prospect", None), - "methodes_identifiees": [] - } - - # MĂ©thode 1 : CT_Qualite (la plus courante aujourd'hui) - try: - qualite = getattr(tiers, "CT_Qualite", None) - if qualite is not None: - analyse["ct_qualite"] = qualite - analyse["ct_qualite_libelle"] = { - 0: "Aucune", - 1: "Client uniquement", - 2: "Fournisseur uniquement", - 3: "Client ET Fournisseur" - }.get(qualite, f"Inconnu ({qualite})") - - if qualite in [2, 3]: - analyse["methodes_identifiees"].append("CT_Qualite (2 ou 3)") - except: - pass - - # MĂ©thode 2 : CT_TypeTiers - try: - type_tiers = getattr(tiers, "CT_TypeTiers", None) - if type_tiers is not None: - analyse["ct_type_tiers"] = type_tiers - except: - pass - - # MĂ©thode 3 : VĂ©rifier si accessible via FactoryFournisseur - try: - factory_fourn = sage.cial.CptaApplication.FactoryFournisseur - persist_fourn = factory_fourn.ReadNumero(analyse["numero"]) - - if persist_fourn: - analyse["methodes_identifiees"].append("FactoryFournisseur.ReadNumero()") - analyse["accessible_via_factory_fournisseur"] = True - except: - analyse["accessible_via_factory_fournisseur"] = False - - # MĂ©thode 4 : CT_Type = 1 (ancienne mĂ©thode) - if analyse["ct_type"] == 1: - analyse["methodes_identifiees"].append("CT_Type = 1") - - resultats["tiers_analyses"].append(analyse) - index += 1 - - except Exception as e: - logger.debug(f"Erreur analyse tiers {index}: {e}") - index += 1 - - # Analyser les mĂ©thodes dĂ©tectĂ©es - fournisseurs_via_qualite = [ - t for t in resultats["tiers_analyses"] - if t.get("ct_qualite") in [2, 3] - ] - - fournisseurs_via_type = [ - t for t in resultats["tiers_analyses"] - if t.get("ct_type") == 1 - ] - - fournisseurs_via_factory = [ - t for t in resultats["tiers_analyses"] - if t.get("accessible_via_factory_fournisseur") == True - ] - - resultats["statistiques"] = { - "total_tiers_scannes": len(resultats["tiers_analyses"]), - "fournisseurs_via_ct_qualite": len(fournisseurs_via_qualite), - "fournisseurs_via_ct_type": len(fournisseurs_via_type), - "fournisseurs_via_factory": len(fournisseurs_via_factory) - } - - # Exemples de fournisseurs dĂ©tectĂ©s - if fournisseurs_via_qualite: - resultats["exemples_fournisseurs_qualite"] = fournisseurs_via_qualite[:5] - - if fournisseurs_via_factory: - resultats["exemples_fournisseurs_factory"] = fournisseurs_via_factory[:5] - - # Recommandation - if fournisseurs_via_qualite: - resultats["recommandation"] = { - "methode": "CT_Qualite", - "condition": "CT_Qualite IN (2, 3)", - "description": "Votre Sage utilise le champ CT_Qualite pour distinguer clients/fournisseurs" - } - elif fournisseurs_via_factory: - resultats["recommandation"] = { - "methode": "FactoryFournisseur", - "condition": "Accessible via FactoryFournisseur.ReadNumero()", - "description": "Utilisez FactoryFournisseur au lieu de FactoryClient" - } - elif fournisseurs_via_type: - resultats["recommandation"] = { - "methode": "CT_Type", - "condition": "CT_Type = 1", - "description": "MĂ©thode classique (rare aujourd'hui)" - } - else: - resultats["recommandation"] = { - "methode": "AUCUNE", - "description": "Aucun fournisseur dĂ©tectĂ© dans les 50 premiers tiers", - "suggestion": "VĂ©rifiez si des fournisseurs existent dans Sage, ou augmentez le scan" - } - - logger.info(f"[DIAG] Fournisseurs dĂ©tectĂ©s: {resultats['statistiques']}") - - return { - "success": True, - "diagnostic": resultats - } - - except Exception as e: - logger.error(f"[DIAG] Erreur diagnostic fournisseurs: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.get("/sage/diagnostic/longueurs-client", dependencies=[Depends(verify_token)]) -def diagnostiquer_longueurs_champs(): - """ - 🔍 DIAGNOSTIC : DĂ©couvre les longueurs maximales autorisĂ©es pour chaque champ - - Teste en dĂ©finissant des valeurs de diffĂ©rentes longueurs et en appelant Write() - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - factory_client = sage.cial.CptaApplication.FactoryClient - - # CrĂ©er un client de test - persist = factory_client.Create() - client = win32com.client.CastTo(persist, "IBOClient3") - client.SetDefault() - - diagnostic = { - "champs_testes": [] - } - - # Liste des champs texte Ă  tester - champs_a_tester = [ - ("CT_Intitule", "SociĂ©tĂ© Test"), - ("CT_Num", "TEST001"), - ("CT_Qualite", "CLI"), - ("CT_Raccourci", "TST"), - ("CT_Contact", "Jean Dupont"), - ("CT_Siret", "12345678901234"), - ("CT_Identifiant", "FR12345678901"), - ("CT_Ape", "6201Z"), - ("CT_Coface", "123456"), - ] - - for champ, valeur_test in champs_a_tester: - try: - if not hasattr(client, champ): - diagnostic["champs_testes"].append({ - "champ": champ, - "existe": False - }) - continue - - # Tester diffĂ©rentes longueurs - longueurs_testees = [] - - for longueur in [10, 20, 35, 50, 69, 100]: - try: - # CrĂ©er une chaĂźne de cette longueur - test_val = valeur_test[:longueur].ljust(longueur, 'X') - - # Essayer de dĂ©finir - setattr(client, champ, test_val) - - # Relire - val_relue = getattr(client, champ, "") - - longueurs_testees.append({ - "longueur": longueur, - "accepte": True, - "valeur_tronquee": len(val_relue) < longueur, - "longueur_reelle": len(val_relue) - }) - except Exception as e: - longueurs_testees.append({ - "longueur": longueur, - "accepte": False, - "erreur": str(e)[:100] - }) - - # Trouver la longueur max acceptĂ©e - longueurs_acceptees = [ - lt["longueur_reelle"] for lt in longueurs_testees - if lt.get("accepte") - ] - - diagnostic["champs_testes"].append({ - "champ": champ, - "existe": True, - "longueur_max": max(longueurs_acceptees) if longueurs_acceptees else 0, - "details": longueurs_testees - }) - - except Exception as e: - diagnostic["champs_testes"].append({ - "champ": champ, - "existe": True, - "erreur_test": str(e)[:200] - }) - - # ======================================== - # TEST CRITIQUE : Valeurs par dĂ©faut problĂ©matiques - # ======================================== - diagnostic["valeurs_par_defaut"] = {} - - # RĂ©cupĂ©rer TOUTES les valeurs par dĂ©faut aprĂšs SetDefault() - for attr in dir(client): - if attr.startswith("CT_") or attr.startswith("N_"): - try: - val = getattr(client, attr, None) - - if isinstance(val, str) and len(val) > 0: - diagnostic["valeurs_par_defaut"][attr] = { - "valeur": val, - "longueur": len(val) - } - except: - pass - - # ======================================== - # TEST : Simuler la crĂ©ation avec valeurs minimales - # ======================================== - try: - # RĂ©initialiser - persist2 = factory_client.Create() - client2 = win32com.client.CastTo(persist2, "IBOClient3") - client2.SetDefault() - - # DĂ©finir UNIQUEMENT les champs absolument minimaux - client2.CT_Intitule = "TEST" - client2.CT_Type = 0 - - # Essayer d'assigner le compte - try: - factory_compte = sage.cial.CptaApplication.FactoryCompteG - persist_compte = factory_compte.ReadNumero("411000") - - if persist_compte: - compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3") - compte_obj.Read() - client2.CompteGPrinc = compte_obj - - diagnostic["test_compte"] = { - "compte_trouve": True, - "compte_numero": "411000" - } - else: - diagnostic["test_compte"] = { - "compte_trouve": False, - "erreur": "Compte 411000 introuvable" - } - except Exception as e: - diagnostic["test_compte"] = { - "erreur": str(e) - } - - # Tenter le Write() (sans commit) - try: - client2.Write() - - diagnostic["test_write_minimal"] = { - "succes": True, - "message": "✅ Write() rĂ©ussi avec configuration minimale" - } - - # Lire le numĂ©ro gĂ©nĂ©rĂ© - num_genere = getattr(client2, "CT_Num", "") - diagnostic["test_write_minimal"]["numero_genere"] = num_genere - - # ⚠ ATTENTION : Supprimer le client de test si on ne veut pas le garder - # Pour le moment, on le laisse en commentaire - # client2.Remove() - - except Exception as e: - diagnostic["test_write_minimal"] = { - "succes": False, - "erreur": str(e), - "erreur_sage": None - } - - # RĂ©cupĂ©rer l'erreur Sage dĂ©taillĂ©e - try: - sage_error = sage.cial.CptaApplication.LastError - if sage_error: - diagnostic["test_write_minimal"]["erreur_sage"] = { - "description": sage_error.Description, - "numero": sage_error.Number - } - except: - pass - - except Exception as e: - diagnostic["test_write_minimal"] = { - "succes": False, - "erreur_init": str(e) - } - - logger.info("[DIAG] Analyse longueurs terminĂ©e") - - return { - "success": True, - "diagnostic": diagnostic - } - - except Exception as e: - logger.error(f"[DIAG] Erreur diagnostic longueurs: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -# À ajouter dans main.py (Windows Gateway) - -@app.get("/sage/diagnostic/fournisseurs-analyse-complete", dependencies=[Depends(verify_token)]) -def analyser_fournisseurs_complet(): - """ - 🔍 DIAGNOSTIC ULTRA-COMPLET : DĂ©couverte fournisseurs - - Teste TOUTES les mĂ©thodes possibles pour identifier les fournisseurs : - 1. CT_Type = 1 (mĂ©thode classique) - 2. CT_Qualite = 2 ou 3 (mĂ©thode moderne) - 3. FactoryFournisseur (mĂ©thode directe) - 4. CT_TypeTiers - 5. Analyse des champs disponibles - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - - diagnostic = { - "timestamp": datetime.now().isoformat(), - "methodes_testees": [], - "fournisseurs_detectes": {}, - "tiers_analyses": [], - "champs_disponibles": {}, - "recommendation": None - } - - # ============================================= - # MÉTHODE 1 : FactoryFournisseur (prioritaire) - # ============================================= - logger.info("[DIAG] Test FactoryFournisseur...") - - try: - factory_fourn = sage.cial.CptaApplication.FactoryFournisseur - fournisseurs_factory = [] - - index = 1 - while index <= 100: # Scanner 100 premiers - try: - persist = factory_fourn.List(index) - if persist is None: - break - - fourn = sage._cast_client(persist) - if fourn: - fournisseurs_factory.append({ - "numero": getattr(fourn, "CT_Num", ""), - "intitule": getattr(fourn, "CT_Intitule", ""), - "ct_type": getattr(fourn, "CT_Type", None), - "ct_qualite": getattr(fourn, "CT_Qualite", None), - "methode": "FactoryFournisseur" - }) - - index += 1 - except Exception as e: - logger.debug(f"Erreur index {index}: {e}") - break - - diagnostic["methodes_testees"].append({ - "methode": "FactoryFournisseur", - "succes": True, - "nb_fournisseurs": len(fournisseurs_factory), - "disponible": True - }) - - if fournisseurs_factory: - diagnostic["fournisseurs_detectes"]["factory"] = fournisseurs_factory[:10] - logger.info(f"✅ {len(fournisseurs_factory)} fournisseurs via FactoryFournisseur") - - except Exception as e: - diagnostic["methodes_testees"].append({ - "methode": "FactoryFournisseur", - "succes": False, - "erreur": str(e), - "disponible": False - }) - logger.info(f"❌ FactoryFournisseur indisponible: {e}") - - # ============================================= - # MÉTHODE 2 : Analyse FactoryClient avec tous les champs - # ============================================= - logger.info("[DIAG] Analyse complĂšte FactoryClient...") - - factory_client = sage.cial.CptaApplication.FactoryClient - - fournisseurs_ct_type = [] - fournisseurs_ct_qualite = [] - tous_tiers = [] - champs_vus = set() - - index = 1 - while index <= 100: # Scanner 100 premiers tiers - try: - persist = factory_client.List(index) - if persist is None: - break - - tiers = sage._cast_client(persist) - if not tiers: - index += 1 - continue - - # Extraction COMPLÈTE de tous les champs - tiers_info = { - "index": index, - "numero": getattr(tiers, "CT_Num", ""), - "intitule": getattr(tiers, "CT_Intitule", ""), - "champs": {} - } - - # Scanner TOUS les attributs CT_* - for attr in dir(tiers): - if attr.startswith("CT_") and not callable(getattr(tiers, attr, None)): - try: - valeur = getattr(tiers, attr, None) - if valeur is not None: - tiers_info["champs"][attr] = { - "valeur": str(valeur), - "type": type(valeur).__name__ - } - champs_vus.add(attr) - except: - pass - - # Analyses spĂ©cifiques - ct_type = getattr(tiers, "CT_Type", None) - ct_qualite = getattr(tiers, "CT_Qualite", None) - ct_prospect = getattr(tiers, "CT_Prospect", None) - - tiers_info["ct_type"] = ct_type - tiers_info["ct_qualite"] = ct_qualite - tiers_info["ct_prospect"] = ct_prospect - - # Identifier si c'est un fournisseur selon diffĂ©rentes mĂ©thodes - tiers_info["analyses"] = {} - - # Test CT_Type = 1 - if ct_type == 1: - tiers_info["analyses"]["ct_type_1"] = True - fournisseurs_ct_type.append(tiers_info) - - # Test CT_Qualite - if ct_qualite is not None: - tiers_info["ct_qualite_libelle"] = { - 0: "Aucune", - 1: "Client uniquement", - 2: "Fournisseur uniquement", - 3: "Client ET Fournisseur" - }.get(ct_qualite, f"Inconnu ({ct_qualite})") - - if ct_qualite in [2, 3]: - tiers_info["analyses"]["ct_qualite_2_3"] = True - fournisseurs_ct_qualite.append(tiers_info) - - # Test via FactoryFournisseur.ReadNumero - try: - factory_fourn = sage.cial.CptaApplication.FactoryFournisseur - persist_test = factory_fourn.ReadNumero(tiers_info["numero"]) - if persist_test: - tiers_info["analyses"]["factory_fournisseur_accessible"] = True - except: - tiers_info["analyses"]["factory_fournisseur_accessible"] = False - - tous_tiers.append(tiers_info) - index += 1 - - except Exception as e: - logger.debug(f"Erreur tiers {index}: {e}") - index += 1 - - diagnostic["tiers_analyses"] = tous_tiers[:20] # Garder 20 exemples - diagnostic["champs_disponibles"] = sorted(list(champs_vus)) - - # Statistiques - diagnostic["statistiques"] = { - "total_tiers_scannes": len(tous_tiers), - "fournisseurs_ct_type_1": len(fournisseurs_ct_type), - "fournisseurs_ct_qualite_2_3": len(fournisseurs_ct_qualite), - "champs_ct_detectes": len(champs_vus) - } - - # Exemples de fournisseurs dĂ©tectĂ©s - if fournisseurs_ct_type: - diagnostic["fournisseurs_detectes"]["ct_type"] = [ - { - "numero": t["numero"], - "intitule": t["intitule"], - "ct_type": t["ct_type"] - } - for t in fournisseurs_ct_type[:10] - ] - - if fournisseurs_ct_qualite: - diagnostic["fournisseurs_detectes"]["ct_qualite"] = [ - { - "numero": t["numero"], - "intitule": t["intitule"], - "ct_qualite": t["ct_qualite"], - "libelle": t["ct_qualite_libelle"] - } - for t in fournisseurs_ct_qualite[:10] - ] - - # ============================================= - # MÉTHODE 3 : Analyse des champs disponibles - # ============================================= - logger.info("[DIAG] Analyse des champs disponibles...") - - # Prendre un tiers exemple et lister TOUS ses attributs - if tous_tiers: - tiers_exemple = tous_tiers[0] - - try: - persist_ex = factory_client.ReadNumero(tiers_exemple["numero"]) - if persist_ex: - obj_ex = sage._cast_client(persist_ex) - - tous_attributs = {} - for attr in dir(obj_ex): - if not attr.startswith("_") and not callable(getattr(obj_ex, attr, None)): - try: - val = getattr(obj_ex, attr, None) - if val is not None: - tous_attributs[attr] = { - "type": type(val).__name__, - "valeur_exemple": str(val)[:50] - } - except: - pass - - diagnostic["attributs_complets_exemple"] = tous_attributs - - except Exception as e: - diagnostic["erreur_analyse_attributs"] = str(e) - - # ============================================= - # RECOMMANDATION FINALE - # ============================================= - logger.info("[DIAG] GĂ©nĂ©ration recommandation...") - - if "factory" in diagnostic["fournisseurs_detectes"]: - diagnostic["recommendation"] = { - "methode": "FactoryFournisseur", - "code_exemple": """ -# Utiliser FactoryFournisseur directement -factory_fourn = sage.cial.CptaApplication.FactoryFournisseur -persist = factory_fourn.List(index) # ou .ReadNumero(code) - """, - "implementation": "Modifier lister_tous_fournisseurs() pour utiliser FactoryFournisseur au lieu de FactoryClient", - "priorite": "HAUTE - MĂ©thode la plus fiable" - } - - elif fournisseurs_ct_qualite: - diagnostic["recommendation"] = { - "methode": "CT_Qualite", - "condition": "CT_Qualite IN (2, 3)", - "code_exemple": """ -# Filtrer sur CT_Qualite -qualite = getattr(tiers, "CT_Qualite", None) -if qualite in [2, 3]: # 2=Fournisseur, 3=Client+Fournisseur - # C'est un fournisseur - """, - "implementation": "Modifier _extraire_client() et le cache pour utiliser CT_Qualite", - "priorite": "MOYENNE - MĂ©thode moderne" - } - - elif fournisseurs_ct_type: - diagnostic["recommendation"] = { - "methode": "CT_Type", - "condition": "CT_Type = 1", - "code_exemple": """ -# Filtrer sur CT_Type (ancienne mĂ©thode) -type_tiers = getattr(tiers, "CT_Type", 0) -if type_tiers == 1: # 1=Fournisseur - # C'est un fournisseur - """, - "implementation": "La mĂ©thode actuelle devrait fonctionner", - "priorite": "BASSE - MĂ©thode classique" - } - - else: - diagnostic["recommendation"] = { - "methode": "AUCUNE", - "message": "Aucune mĂ©thode n'a permis d'identifier des fournisseurs", - "actions": [ - "VĂ©rifier si des fournisseurs existent dans Sage", - "Augmenter le nombre de tiers scannĂ©s (actuellement 100)", - "VĂ©rifier les permissions de l'utilisateur Sage", - "Consulter la documentation Sage pour votre version" - ] - } - - # ============================================= - # CODE DE CORRECTION SUGGÉRÉ - # ============================================= - if "factory" in diagnostic["fournisseurs_detectes"]: - diagnostic["code_correction"] = { - "fichier": "sage_connector.py", - "fonction": "lister_tous_fournisseurs", - "code": """ -def lister_tous_fournisseurs(self, filtre=""): - '''Liste tous les fournisseurs via FactoryFournisseur''' - if not self.cial: - return [] - - fournisseurs = [] - - try: - with self._com_context(), self._lock_com: - factory = self.cial.CptaApplication.FactoryFournisseur # ✅ CORRECTION - index = 1 - max_iterations = 10000 - - while index < max_iterations: - try: - persist = factory.List(index) - if persist is None: - break - - obj = self._cast_client(persist) - if obj: - data = self._extraire_client(obj) - - # Filtrer si nĂ©cessaire - if not filtre or \\ - filtre.lower() in data["numero"].lower() or \\ - filtre.lower() in data["intitule"].lower(): - fournisseurs.append(data) - - index += 1 - - except: - index += 1 - continue - - logger.info(f"✅ {len(fournisseurs)} fournisseurs retournĂ©s") - return fournisseurs - - except Exception as e: - logger.error(f"❌ Erreur liste fournisseurs: {e}") - return [] - """ - } - - elif fournisseurs_ct_qualite: - diagnostic["code_correction"] = { - "fichier": "sage_connector.py", - "fonction": "_extraire_client + _refresh_cache_clients", - "code": """ -# Modifier _extraire_client pour inclure CT_Qualite -def _extraire_client(self, client_obj): - data = { - "numero": getattr(client_obj, "CT_Num", ""), - "intitule": getattr(client_obj, "CT_Intitule", ""), - "type": getattr(client_obj, "CT_Type", 0), - "qualite": getattr(client_obj, "CT_Qualite", 0), # ✅ AJOUT - "est_prospect": getattr(client_obj, "CT_Prospect", 0) == 1, - "est_fournisseur": getattr(client_obj, "CT_Qualite", 0) in [2, 3] # ✅ AJOUT - } - # ... reste du code - return data - -# Modifier lister_tous_fournisseurs -def lister_tous_fournisseurs(self, filtre=""): - with self._lock_clients: - if not filtre: - return [c for c in self._cache_clients if c.get("est_fournisseur")] # ✅ MODIFICATION - - filtre_lower = filtre.lower() - return [ - c for c in self._cache_clients - if c.get("est_fournisseur") and # ✅ MODIFICATION - (filtre_lower in c["numero"].lower() or - filtre_lower in c["intitule"].lower()) - ] - """ - } - - logger.info("[DIAG] ✅ Analyse complĂšte terminĂ©e") - - return { - "success": True, - "diagnostic": diagnostic - } - - except Exception as e: - logger.error(f"[DIAG] ❌ Erreur: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.get("/sage/diagnostic/fournisseurs-rapide", dependencies=[Depends(verify_token)]) -def test_fournisseurs_rapide(): - """ - ⚡ DIAGNOSTIC RAPIDE : Test des 3 mĂ©thodes principales - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - resultats = {} - - # Test 1 : FactoryFournisseur - try: - factory = sage.cial.CptaApplication.FactoryFournisseur - count = 0 - index = 1 - - while index <= 10: - persist = factory.List(index) - if persist is None: - break - count += 1 - index += 1 - - resultats["FactoryFournisseur"] = { - "disponible": True, - "nb_trouves": count, - "statut": "✅ FONCTIONNEL" if count > 0 else "⚠ Aucun fournisseur" - } - except Exception as e: - resultats["FactoryFournisseur"] = { - "disponible": False, - "erreur": str(e), - "statut": "❌ INDISPONIBLE" - } - - # Test 2 : CT_Qualite - factory_client = sage.cial.CptaApplication.FactoryClient - count_qualite = 0 - index = 1 - - while index <= 50: - try: - persist = factory_client.List(index) - if persist is None: - break - - tiers = sage._cast_client(persist) - if tiers: - qualite = getattr(tiers, "CT_Qualite", None) - if qualite in [2, 3]: - count_qualite += 1 - - index += 1 - except: - break - - resultats["CT_Qualite"] = { - "nb_trouves": count_qualite, - "statut": "✅ FONCTIONNEL" if count_qualite > 0 else "⚠ Aucun fournisseur" - } - - # Test 3 : CT_Type - count_type = 0 - index = 1 - - while index <= 50: - try: - persist = factory_client.List(index) - if persist is None: - break - - tiers = sage._cast_client(persist) - if tiers: - type_tiers = getattr(tiers, "CT_Type", 0) - if type_tiers == 1: - count_type += 1 - - index += 1 - except: - break - - resultats["CT_Type"] = { - "nb_trouves": count_type, - "statut": "✅ FONCTIONNEL" if count_type > 0 else "⚠ Aucun fournisseur" - } - - # Recommandation - if resultats["FactoryFournisseur"].get("nb_trouves", 0) > 0: - recommandation = "Utiliser FactoryFournisseur (mĂ©thode la plus fiable)" - elif count_qualite > 0: - recommandation = "Utiliser CT_Qualite (mĂ©thode moderne)" - elif count_type > 0: - recommandation = "Utiliser CT_Type (mĂ©thode classique)" - else: - recommandation = "⚠ AUCUNE MÉTHODE - VĂ©rifier si des fournisseurs existent dans Sage" - - return { - "success": True, - "resultats": resultats, - "recommandation": recommandation, - "actions_suivantes": [ - "Appeler /sage/diagnostic/fournisseurs-analyse-complete pour plus de dĂ©tails", - "VĂ©rifier dans Sage si des fournisseurs existent", - "Appliquer la correction suggĂ©rĂ©e selon la mĂ©thode fonctionnelle" - ] - } - - except Exception as e: - logger.error(f"[DIAG] Erreur: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.get("/sage/debug/fournisseurs-direct", dependencies=[Depends(verify_token)]) -def debug_fournisseurs_direct(): - """ - 🔍 Test direct : Lecture des fournisseurs SANS passer par le cache - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - factory = sage.cial.CptaApplication.FactoryFournisseur - - fournisseurs_direct = [] - index = 1 - - # Lire les 10 premiers directement - while index <= 10: - try: - persist = factory.List(index) - if persist is None: - break - - obj = sage._cast_client(persist) - if obj: - fournisseurs_direct.append({ - "numero": getattr(obj, "CT_Num", ""), - "intitule": getattr(obj, "CT_Intitule", ""), - "type": getattr(obj, "CT_Type", -1) - }) - - index += 1 - except Exception as e: - logger.debug(f"Erreur index {index}: {e}") - break - - # VĂ©rifier l'Ă©tat du cache - cache_existe = hasattr(sage, '_cache_fournisseurs') - cache_count = len(sage._cache_fournisseurs) if cache_existe else 0 - cache_last_update = ( - sage._cache_fournisseurs_last_update.isoformat() - if cache_existe and sage._cache_fournisseurs_last_update - else None - ) - - return { - "success": True, - "lecture_directe": { - "nb_fournisseurs": len(fournisseurs_direct), - "exemples": fournisseurs_direct - }, - "etat_cache": { - "cache_existe": cache_existe, - "cache_count": cache_count, - "last_update": cache_last_update, - "attributs_sage_connector": [ - attr for attr in dir(sage) - if 'fournisseur' in attr.lower() - ] - }, - "diagnostic": { - "factory_fournisseur_ok": len(fournisseurs_direct) > 0, - "cache_initialise": cache_existe and cache_count > 0, - "probleme": ( - "Cache non initialisĂ© - appeler sage.connecter() ou _refresh_cache_fournisseurs()" - if not cache_existe or cache_count == 0 - else "Tout est OK" - ) - } - } - - except Exception as e: - logger.error(f"❌ Erreur debug direct: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.post("/sage/debug/fournisseurs-init-cache", dependencies=[Depends(verify_token)]) -def init_cache_fournisseurs_force(): - """ - 🔧 Force l'initialisation du cache fournisseurs (si la mĂ©thode existe) - """ - try: - if not sage: - raise HTTPException(503, "Service Sage indisponible") - - # VĂ©rifier que la mĂ©thode existe - if not hasattr(sage, '_refresh_cache_fournisseurs'): - return { - "success": False, - "erreur": "La mĂ©thode _refresh_cache_fournisseurs() n'existe pas dans sage_connector.py", - "solution": "VĂ©rifier que le code a bien Ă©tĂ© appliquĂ© et redĂ©marrer main.py" - } - - # Appeler la mĂ©thode - logger.info("🔄 Initialisation forcĂ©e du cache fournisseurs...") - sage._refresh_cache_fournisseurs() - - # VĂ©rifier le rĂ©sultat - cache_count = len(sage._cache_fournisseurs) if hasattr(sage, '_cache_fournisseurs') else 0 - - return { - "success": True, - "cache_initialise": cache_count > 0, - "nb_fournisseurs": cache_count, - "exemples": sage._cache_fournisseurs[:3] if cache_count > 0 else [], - "message": ( - f"✅ Cache initialisĂ© : {cache_count} fournisseurs" - if cache_count > 0 - else "❌ Échec : cache toujours vide aprĂšs refresh" - ) - } - - except Exception as e: - logger.error(f"❌ Erreur init cache: {e}", exc_info=True) - raise HTTPException(500, str(e)) - # ===================================================== # LANCEMENT # ===================================================== diff --git a/sage_connector.py b/sage_connector.py index 14bc010..776ab36 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -35,11 +35,6 @@ class SageConnector: self._cache_clients_dict: Dict[str, Dict] = {} self._cache_articles_dict: Dict[str, Dict] = {} - # ✅ NOUVEAU : Cache fournisseurs dĂ©diĂ© - self._cache_fournisseurs: List[Dict] = [] - self._cache_fournisseurs_dict: Dict[str, Dict] = {} - self._cache_fournisseurs_last_update: Optional[datetime] = None - # MĂ©tadonnĂ©es cache existantes self._cache_clients_last_update: Optional[datetime] = None self._cache_articles_last_update: Optional[datetime] = None @@ -119,13 +114,6 @@ class SageConnector: logger.info("📩 Chargement initial du cache...") self._refresh_cache_clients() self._refresh_cache_articles() - self._refresh_cache_fournisseurs() # ✅ CETTE LIGNE DOIT ÊTRE LÀ - - logger.info( - f"✅ Cache initialisĂ©: {len(self._cache_clients)} clients, " - f"{len(self._cache_articles)} articles, " - f"{len(self._cache_fournisseurs)} fournisseurs" # ✅ AJOUT - ) # DĂ©marrage du thread d'actualisation self._start_refresh_thread() @@ -176,13 +164,6 @@ class SageConnector: age = datetime.now() - self._cache_articles_last_update if age.total_seconds() > self._cache_ttl_minutes * 60: self._refresh_cache_articles() - - # ✅ AJOUT : Fournisseurs - if hasattr(self, '_cache_fournisseurs_last_update') and self._cache_fournisseurs_last_update: - age = datetime.now() - self._cache_fournisseurs_last_update - if age.total_seconds() > self._cache_ttl_minutes * 60: - logger.info(f"🔄 Actualisation cache fournisseurs (Ăąge: {age.seconds//60}min)") - self._refresh_cache_fournisseurs() finally: pythoncom.CoUninitialize() @@ -222,7 +203,7 @@ class SageConnector: if obj: data = self._extraire_client(obj) - # ✅ INCLURE TOUS LES TYPES (clients, prospects, fournisseurs) + # ✅ INCLURE TOUS LES TYPES (clients, prospects) clients.append(data) clients_dict[data["numero"]] = data erreurs_consecutives = 0 @@ -246,11 +227,10 @@ class SageConnector: # 📊 Statistiques dĂ©taillĂ©es nb_clients = sum(1 for c in clients if c.get("type") == 0 and not c.get("est_prospect")) nb_prospects = sum(1 for c in clients if c.get("type") == 0 and c.get("est_prospect")) - nb_fournisseurs = sum(1 for c in clients if c.get("type") == 1) logger.info( f"✅ Cache actualisĂ©: {len(clients)} tiers " - f"({nb_clients} clients, {nb_prospects} prospects, {nb_fournisseurs} fournisseurs)" + f"({nb_clients} clients, {nb_prospects} prospects)" ) except Exception as e: @@ -304,170 +284,185 @@ class SageConnector: except Exception as e: logger.error(f" Erreur refresh articles: {e}", exc_info=True) - - def _refresh_cache_fournisseurs(self): + + def lister_tous_fournisseurs(self, filtre=""): """ - ✅ CORRIGÉ FINAL : Actualise le cache des fournisseurs via FactoryFournisseur + ✅ CORRECTION FINALE : Liste fournisseurs SANS passer par _extraire_client() + + BYPASS TOTAL de _extraire_client() car : + - Les objets fournisseurs n'ont pas les mĂȘmes champs que les clients + - _extraire_client() plante sur "CT_Qualite" (n'existe pas sur fournisseurs) + - Le diagnostic fournisseurs-analyse-complete fonctionne SANS _extraire_client() + + → On fait EXACTEMENT comme le diagnostic qui marche """ if not self.cial: logger.error("❌ self.cial est None") - # ✅ INITIALISER UN CACHE VIDE mĂȘme en cas d'erreur - self._cache_fournisseurs = [] - self._cache_fournisseurs_dict = {} - self._cache_fournisseurs_last_update = None - return + return [] fournisseurs = [] - fournisseurs_dict = {} try: with self._com_context(), self._lock_com: - logger.info("=" * 80) - logger.info("🔄 DÉBUT REFRESH CACHE FOURNISSEURS") - logger.info("=" * 80) - - # ✅ AccĂ©der Ă  FactoryFournisseur - try: - factory = self.cial.CptaApplication.FactoryFournisseur - logger.info("✅ FactoryFournisseur accessible") - except Exception as e: - logger.error(f"❌ Impossible d'accĂ©der Ă  FactoryFournisseur: {e}") - # ✅ INITIALISER UN CACHE VIDE - with self._lock_clients: - self._cache_fournisseurs = [] - self._cache_fournisseurs_dict = {} - self._cache_fournisseurs_last_update = None - return + logger.info(f"🔍 Lecture fournisseurs via FactoryFournisseur (filtre='{filtre}')") + factory = self.cial.CptaApplication.FactoryFournisseur index = 1 + max_iterations = 10000 erreurs_consecutives = 0 - max_erreurs = 10 # ✅ RÉDUIT pour Ă©viter de bloquer le dĂ©marrage + max_erreurs = 50 - while index < 10000 and erreurs_consecutives < max_erreurs: - persist = None - - # ✅ ÉTAPE 1 : Lire l'Ă©lĂ©ment (avec gestion d'erreur simple) + filtre_lower = filtre.lower() if filtre else "" + + while 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 + + # Cast + fourn = self._cast_client(persist) + + if fourn: + # ✅✅✅ EXTRACTION DIRECTE (pas de _extraire_client) ✅✅✅ + try: + numero = getattr(fourn, "CT_Num", "").strip() + intitule = getattr(fourn, "CT_Intitule", "").strip() + + if not numero: + logger.debug(f"Index {index}: CT_Num vide, skip") + erreurs_consecutives += 1 + index += 1 + continue + + # Construction objet minimal + data = { + "numero": numero, + "intitule": intitule, + "type": 1, # Fournisseur + "est_fournisseur": True + } + + # Champs optionnels (avec gestion d'erreur) + try: + adresse_obj = getattr(fourn, "Adresse", None) + if adresse_obj: + data["adresse"] = getattr(adresse_obj, "Adresse", "").strip() + data["code_postal"] = getattr(adresse_obj, "CodePostal", "").strip() + data["ville"] = getattr(adresse_obj, "Ville", "").strip() + except: + data["adresse"] = "" + data["code_postal"] = "" + data["ville"] = "" + + try: + telecom_obj = getattr(fourn, "Telecom", None) + if telecom_obj: + data["telephone"] = getattr(telecom_obj, "Telephone", "").strip() + data["email"] = getattr(telecom_obj, "EMail", "").strip() + except: + data["telephone"] = "" + data["email"] = "" + + # Filtrer si nĂ©cessaire + if not filtre_lower or \ + filtre_lower in numero.lower() or \ + filtre_lower in intitule.lower(): + fournisseurs.append(data) + logger.debug(f"✅ Fournisseur ajoutĂ©: {numero} - {intitule}") + + erreurs_consecutives = 0 + + except Exception as e: + logger.debug(f"⚠ Erreur extraction index {index}: {e}") + erreurs_consecutives += 1 + else: + erreurs_consecutives += 1 + + index += 1 + except Exception as e: - logger.debug(f"⚠ Index {index}: factory.List() Ă©choue - {e}") + logger.debug(f"⚠ Erreur index {index}: {e}") erreurs_consecutives += 1 index += 1 - continue - - if persist is None: - logger.info(f"✅ Fin de liste Ă  l'index {index}") - break - - # ✅ ÉTAPE 2 : Cast (avec gestion d'erreur simple) - obj = None - try: - obj = self._cast_client(persist) - except Exception as e: - logger.debug(f"⚠ Index {index}: _cast_client() Ă©choue - {e}") - erreurs_consecutives += 1 - index += 1 - continue - - if obj is None: - logger.debug(f"⚠ Index {index}: _cast_client retourne None (skip)") - index += 1 - continue - - # ✅ ÉTAPE 3 : Extraire - data = None - try: - data = self._extraire_client(obj) - except Exception as e: - logger.debug(f"⚠ Index {index}: _extraire_client() Ă©choue - {e}") - erreurs_consecutives += 1 - index += 1 - continue - - # ✅ ÉTAPE 4 : VĂ©rifier les donnĂ©es - if not data or not data.get("numero"): - logger.debug(f"⚠ Index {index}: donnĂ©es invalides (skip)") - index += 1 - continue - - # ✅ SUCCÈS : Marquer et stocker - data["est_fournisseur"] = True - fournisseurs.append(data) - fournisseurs_dict[data["numero"]] = data - erreurs_consecutives = 0 # ✅ Reset compteur - - # Log progression tous les 10 - if len(fournisseurs) % 10 == 0: - logger.info(f" ✅ {len(fournisseurs)} fournisseurs chargĂ©s...") - - index += 1 - - # ✅ TOUJOURS stocker dans les attributs (mĂȘme si vide) - with self._lock_clients: - self._cache_fournisseurs = fournisseurs - self._cache_fournisseurs_dict = fournisseurs_dict - self._cache_fournisseurs_last_update = datetime.now() - - logger.info("=" * 80) - logger.info(f"✅ CACHE FOURNISSEURS ACTUALISÉ: {len(fournisseurs)} fournisseurs") - logger.info("=" * 80) - - # ✅ Exemples - if len(fournisseurs) > 0: - logger.info("Exemples de fournisseurs chargĂ©s:") - for f in fournisseurs[:3]: - logger.info(f" - {f['numero']}: {f['intitule']}") - else: - logger.warning("⚠ AUCUN FOURNISSEUR CHARGÉ") - logger.warning(f"Erreurs consĂ©cutives finales: {erreurs_consecutives}") - logger.warning(f"Dernier index testĂ©: {index}") + + if erreurs_consecutives >= max_erreurs: + logger.warning(f"⚠ ArrĂȘt aprĂšs {max_erreurs} erreurs consĂ©cutives") + break + logger.info(f"✅ {len(fournisseurs)} fournisseurs retournĂ©s") + return fournisseurs + except Exception as e: - logger.error(f"❌ ERREUR GLOBALE refresh fournisseurs: {e}", exc_info=True) - # ✅ INITIALISER UN CACHE VIDE en cas d'erreur critique - with self._lock_clients: - self._cache_fournisseurs = [] - self._cache_fournisseurs_dict = {} - self._cache_fournisseurs_last_update = None + logger.error(f"❌ Erreur liste fournisseurs: {e}", exc_info=True) + return [] + + def lire_fournisseur(self, code): + """ + ✅ NOUVEAU : Lecture d'un fournisseur par code + + Utilise FactoryFournisseur.ReadNumero() directement + """ + if not self.cial: + return None + + try: + with self._com_context(), self._lock_com: + factory = self.cial.CptaApplication.FactoryFournisseur + persist = factory.ReadNumero(code) - def lister_tous_fournisseurs(self, filtre=""): - """ - ✅ CORRIGÉ : Liste les fournisseurs depuis le cache dĂ©diĂ© - """ - # Si le cache fournisseurs n'existe pas ou est vide, le crĂ©er - if not hasattr(self, '_cache_fournisseurs') or len(self._cache_fournisseurs) == 0: - logger.info("Cache fournisseurs vide, chargement initial...") - self._refresh_cache_fournisseurs() + if not persist: + logger.warning(f"Fournisseur {code} introuvable") + return None + + fourn = self._cast_client(persist) + + if not fourn: + return None + + # Extraction directe (mĂȘme logique que lister_tous_fournisseurs) + numero = getattr(fourn, "CT_Num", "").strip() + intitule = getattr(fourn, "CT_Intitule", "").strip() + + data = { + "numero": numero, + "intitule": intitule, + "type": 1, + "est_fournisseur": True + } + + # Adresse + try: + adresse_obj = getattr(fourn, "Adresse", None) + if adresse_obj: + data["adresse"] = getattr(adresse_obj, "Adresse", "").strip() + data["code_postal"] = getattr(adresse_obj, "CodePostal", "").strip() + data["ville"] = getattr(adresse_obj, "Ville", "").strip() + except: + data["adresse"] = "" + data["code_postal"] = "" + data["ville"] = "" + + # TĂ©lĂ©com + try: + telecom_obj = getattr(fourn, "Telecom", None) + if telecom_obj: + data["telephone"] = getattr(telecom_obj, "Telephone", "").strip() + data["email"] = getattr(telecom_obj, "EMail", "").strip() + except: + data["telephone"] = "" + data["email"] = "" + + logger.info(f"✅ Fournisseur {code} lu: {intitule}") + return data - with self._lock_clients: - if not filtre: - result = self._cache_fournisseurs.copy() - logger.info(f"Liste fournisseurs sans filtre: {len(result)} rĂ©sultats") - return result - - filtre_lower = filtre.lower() - result = [ - f for f in self._cache_fournisseurs - if filtre_lower in f["numero"].lower() or - filtre_lower in f["intitule"].lower() - ] - logger.info(f"Liste fournisseurs avec filtre '{filtre}': {len(result)} rĂ©sultats") - return result - - - def lire_fournisseur(self, code_fournisseur): - """ - ✅ CORRIGÉ : Lecture depuis le cache fournisseurs - """ - # Si le cache fournisseurs n'existe pas, le crĂ©er - if not hasattr(self, '_cache_fournisseurs_dict') or not self._cache_fournisseurs_dict: - self._refresh_cache_fournisseurs() + except Exception as e: + logger.error(f"❌ Erreur lecture fournisseur {code}: {e}") + return None - with self._lock_clients: - return self._cache_fournisseurs_dict.get(code_fournisseur) - - # ========================================================================= # API PUBLIQUE (ultra-rapide grĂące au cache) # ========================================================================= @@ -515,13 +510,8 @@ class SageConnector: logger.info("🔄 Actualisation forcĂ©e du cache...") self._refresh_cache_clients() self._refresh_cache_articles() - self._refresh_cache_fournisseurs() # ✅ AJOUT logger.info("✅ Cache actualisĂ©") - # ✅ AJOUT - if hasattr(self, '_refresh_cache_fournisseurs'): - self._refresh_cache_fournisseurs() - logger.info("Cache actualisĂ©") @@ -557,22 +547,6 @@ class SageConnector: } } - # ✅ AJOUT : Info fournisseurs - if hasattr(self, '_cache_fournisseurs'): - info["fournisseurs"] = { - "count": len(self._cache_fournisseurs), - "last_update": ( - self._cache_fournisseurs_last_update.isoformat() - if self._cache_fournisseurs_last_update - else None - ), - "age_minutes": ( - (datetime.now() - self._cache_fournisseurs_last_update).total_seconds() / 60 - if self._cache_fournisseurs_last_update - else None - ), - } - info["ttl_minutes"] = self._cache_ttl_minutes return info @@ -602,83 +576,106 @@ class SageConnector: # ========================================================================= def _extraire_client(self, client_obj): - """MISE À JOUR : Extraction avec dĂ©tection prospect ET type""" + """ + ✅ CORRECTION : Extraction ULTRA-ROBUSTE pour clients ET fournisseurs + GĂšre tous les cas oĂč des champs peuvent ĂȘtre manquants + """ try: - # ✅ LOGS DÉTAILLÉS + # === 1. CHAMPS OBLIGATOIRES === try: - numero = getattr(client_obj, "CT_Num", "") + numero = getattr(client_obj, "CT_Num", "").strip() + if not numero: + logger.debug("⚠ Objet sans CT_Num, skip") + return None except Exception as e: - logger.error(f"❌ Erreur lecture CT_Num: {e}") - raise + logger.debug(f"❌ Erreur lecture CT_Num: {e}") + return None try: - intitule = getattr(client_obj, "CT_Intitule", "") + intitule = getattr(client_obj, "CT_Intitule", "").strip() + if not intitule: + logger.debug(f"⚠ {numero} sans CT_Intitule") except Exception as e: - logger.error(f"❌ Erreur lecture CT_Intitule sur {numero}: {e}") - raise - - try: - type_tiers = getattr(client_obj, "CT_Type", 0) - except Exception as e: - logger.error(f"❌ Erreur lecture CT_Type sur {numero}: {e}") - type_tiers = 0 - - try: - qualite = getattr(client_obj, "CT_Qualite", None) - except Exception as e: - logger.debug(f"⚠ Erreur lecture CT_Qualite sur {numero}: {e}") - qualite = None - - try: - prospect = getattr(client_obj, "CT_Prospect", 0) == 1 - except Exception as e: - logger.debug(f"⚠ Erreur lecture CT_Prospect sur {numero}: {e}") - prospect = False + logger.debug(f"⚠ Erreur CT_Intitule sur {numero}: {e}") + intitule = "" + # === 2. CONSTRUCTION OBJET MINIMAL === data = { "numero": numero, "intitule": intitule, - "type": type_tiers, - "qualite": qualite, - "est_prospect": prospect, - "est_fournisseur": qualite in [2, 3] if qualite is not None else False, } - - # Adresse (non critique) + + # === 3. CHAMPS OPTIONNELS (avec try-except individuels) === + + # Type + try: + data["type"] = getattr(client_obj, "CT_Type", 0) + except: + data["type"] = 0 + + # QualitĂ© + try: + qualite = getattr(client_obj, "CT_Qualite", None) + data["qualite"] = qualite + data["est_fournisseur"] = qualite in [2, 3] if qualite is not None else False + except: + data["qualite"] = None + data["est_fournisseur"] = False + + # Prospect + try: + data["est_prospect"] = getattr(client_obj, "CT_Prospect", 0) == 1 + except: + data["est_prospect"] = False + + # === 4. ADRESSE (non critique) === try: adresse = getattr(client_obj, "Adresse", None) if adresse: - data["adresse"] = getattr(adresse, "Adresse", "") - data["code_postal"] = getattr(adresse, "CodePostal", "") - data["ville"] = getattr(adresse, "Ville", "") + try: + data["adresse"] = getattr(adresse, "Adresse", "").strip() + except: + data["adresse"] = "" + + try: + data["code_postal"] = getattr(adresse, "CodePostal", "").strip() + except: + data["code_postal"] = "" + + try: + data["ville"] = getattr(adresse, "Ville", "").strip() + except: + data["ville"] = "" except Exception as e: logger.debug(f"⚠ Erreur adresse sur {numero}: {e}") - - # Telecom (non critique) + data["adresse"] = "" + data["code_postal"] = "" + data["ville"] = "" + + # === 5. TELECOM (non critique) === try: telecom = getattr(client_obj, "Telecom", None) if telecom: - data["telephone"] = getattr(telecom, "Telephone", "") - data["email"] = getattr(telecom, "EMail", "") + try: + data["telephone"] = getattr(telecom, "Telephone", "").strip() + except: + data["telephone"] = "" + + try: + data["email"] = getattr(telecom, "EMail", "").strip() + except: + data["email"] = "" except Exception as e: logger.debug(f"⚠ Erreur telecom sur {numero}: {e}") - + data["telephone"] = "" + data["email"] = "" + return data except Exception as e: logger.error(f"❌ ERREUR GLOBALE _extraire_client: {e}", exc_info=True) - raise - - def _extraire_article(self, article_obj): - return { - "reference": getattr(article_obj, "AR_Ref", ""), - "designation": getattr(article_obj, "AR_Design", ""), - "prix_vente": getattr(article_obj, "AR_PrixVen", 0.0), - "prix_achat": getattr(article_obj, "AR_PrixAch", 0.0), - "stock_reel": getattr(article_obj, "AR_Stock", 0.0), - "stock_mini": getattr(article_obj, "AR_StockMini", 0.0), - } + return None # ========================================================================= # CRÉATION DEVIS (US-A1) - VERSION TRANSACTIONNELLE