diff --git a/main.py b/main.py index 1ac24ae..3aed6f6 100644 --- a/main.py +++ b/main.py @@ -2535,19 +2535,69 @@ def prospect_get(req: CodeRequest): # ENDPOINTS - FOURNISSEURS # ===================================================== @app.post("/sage/fournisseurs/list", dependencies=[Depends(verify_token)]) -def fournisseurs_list(req: FiltreRequest): - """📋 Liste tous les fournisseurs (CT_Type=1)""" +def fournisseurs_list_direct(req: FiltreRequest): + """ + ⚡ ENDPOINT DIRECT : Liste fournisseurs SANS passer par le cache + + Lecture directe depuis FactoryFournisseur - toujours Ă  jour + """ try: - fournisseurs = sage.lister_tous_fournisseurs(req.filtre) - return {"success": True, "data": fournisseurs} + 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 = [] + 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 liste fournisseurs: {e}") + logger.error(f"❌ Erreur lecture directe fournisseurs: {e}", exc_info=True) raise HTTPException(500, str(e)) @app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)]) def fournisseur_get(req: CodeRequest): - """📄 Lecture d'un fournisseur par code""" + """ + ✅ NOUVEAU : Lecture d'un fournisseur par code + """ try: fournisseur = sage.lire_fournisseur(req.code) if not fournisseur: @@ -2981,6 +3031,627 @@ def diagnostiquer_longueurs_champs(): 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 5e102d9..14bc010 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -29,13 +29,18 @@ class SageConnector: self.mot_de_passe = mot_de_passe self.cial = None - # Cache + # Caches existants self._cache_clients: List[Dict] = [] self._cache_articles: List[Dict] = [] self._cache_clients_dict: Dict[str, Dict] = {} self._cache_articles_dict: Dict[str, Dict] = {} - # MĂ©tadonnĂ©es cache + # ✅ 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 self._cache_ttl_minutes = 15 @@ -44,12 +49,12 @@ class SageConnector: self._refresh_thread: Optional[threading.Thread] = None self._stop_refresh = threading.Event() - # Locks thread-safe + # Locks self._lock_clients = threading.RLock() self._lock_articles = threading.RLock() - self._lock_com = threading.RLock() # Lock pour accĂšs COM + self._lock_com = threading.RLock() - # Thread-local storage pour COM + # Thread-local storage pour COM self._thread_local = threading.local() # ========================================================================= @@ -102,22 +107,24 @@ class SageConnector: """Connexion initiale Ă  Sage""" try: with self._com_context(): - self.cial = win32com.client.gencache.EnsureDispatch( - "Objets100c.Cial.Stream" - ) + self.cial = win32com.client.gencache.EnsureDispatch("Objets100c.Cial.Stream") self.cial.Name = self.chemin_base self.cial.Loggable.UserName = self.utilisateur self.cial.Loggable.UserPwd = self.mot_de_passe self.cial.Open() - logger.info(f"Connexion Sage rĂ©ussie: {self.chemin_base}") + logger.info(f"✅ Connexion Sage rĂ©ussie: {self.chemin_base}") # Chargement initial du cache - logger.info("Chargement initial du cache...") + 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, {len(self._cache_articles)} articles" + 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 @@ -126,7 +133,7 @@ class SageConnector: return True except Exception as e: - logger.error(f"Erreur connexion Sage: {e}", exc_info=True) + logger.error(f"❌ Erreur connexion Sage: {e}", exc_info=True) return False def deconnecter(self): @@ -150,36 +157,36 @@ class SageConnector: def _start_refresh_thread(self): """DĂ©marre le thread d'actualisation automatique""" - + def refresh_loop(): - # Initialiser COM pour ce thread worker pythoncom.CoInitialize() - + try: while not self._stop_refresh.is_set(): - time.sleep(60) # VĂ©rifier toutes les minutes - + time.sleep(60) + # Clients if self._cache_clients_last_update: age = datetime.now() - self._cache_clients_last_update if age.total_seconds() > self._cache_ttl_minutes * 60: - logger.info( - f"Actualisation cache clients (Ăąge: {age.seconds//60}min)" - ) self._refresh_cache_clients() - + # Articles if self._cache_articles_last_update: age = datetime.now() - self._cache_articles_last_update if age.total_seconds() > self._cache_ttl_minutes * 60: - logger.info( - f"Actualisation cache articles (Ăąge: {age.seconds//60}min)" - ) 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: - # Nettoyer COM en fin de thread pythoncom.CoUninitialize() - + self._refresh_thread = threading.Thread( target=refresh_loop, daemon=True, name="SageCacheRefresh" ) @@ -298,6 +305,169 @@ class SageConnector: except Exception as e: logger.error(f" Erreur refresh articles: {e}", exc_info=True) + def _refresh_cache_fournisseurs(self): + """ + ✅ CORRIGÉ FINAL : Actualise le cache des fournisseurs via FactoryFournisseur + """ + 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 + + 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 + + index = 1 + erreurs_consecutives = 0 + max_erreurs = 10 # ✅ RÉDUIT pour Ă©viter de bloquer le dĂ©marrage + + while index < 10000 and erreurs_consecutives < max_erreurs: + persist = None + + # ✅ ÉTAPE 1 : Lire l'Ă©lĂ©ment (avec gestion d'erreur simple) + try: + persist = factory.List(index) + except Exception as e: + logger.debug(f"⚠ Index {index}: factory.List() Ă©choue - {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}") + + 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 + + + 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() + + 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() + + with self._lock_clients: + return self._cache_fournisseurs_dict.get(code_fournisseur) + + # ========================================================================= # API PUBLIQUE (ultra-rapide grĂące au cache) # ========================================================================= @@ -342,15 +512,23 @@ class SageConnector: def forcer_actualisation_cache(self): """Force l'actualisation immĂ©diate du cache (endpoint admin)""" - logger.info("Actualisation forcĂ©e du cache...") + 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Ă©") + def get_cache_info(self): """Retourne les infos du cache (endpoint monitoring)""" - with self._lock_clients, self._lock_articles: - return { + with self._lock_clients: + info = { "clients": { "count": len(self._cache_clients), "last_update": ( @@ -359,10 +537,7 @@ class SageConnector: else None ), "age_minutes": ( - ( - datetime.now() - self._cache_clients_last_update - ).total_seconds() - / 60 + (datetime.now() - self._cache_clients_last_update).total_seconds() / 60 if self._cache_clients_last_update else None ), @@ -375,17 +550,32 @@ class SageConnector: else None ), "age_minutes": ( - ( - datetime.now() - self._cache_articles_last_update - ).total_seconds() - / 60 + (datetime.now() - self._cache_articles_last_update).total_seconds() / 60 if self._cache_articles_last_update else None ), - }, - "ttl_minutes": self._cache_ttl_minutes, + } } - + + # ✅ 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 + # ========================================================================= # CAST HELPERS # ========================================================================= @@ -395,7 +585,8 @@ class SageConnector: obj = win32com.client.CastTo(persist_obj, "IBOClient3") obj.Read() return obj - except: + except Exception as e: + logger.debug(f"❌ _cast_client Ă©choue: {e}") # ✅ AJOUTER CE LOG return None def _cast_article(self, persist_obj): @@ -412,31 +603,72 @@ class SageConnector: def _extraire_client(self, client_obj): """MISE À JOUR : Extraction avec dĂ©tection prospect ET type""" - data = { - "numero": getattr(client_obj, "CT_Num", ""), - "intitule": getattr(client_obj, "CT_Intitule", ""), - "type": getattr(client_obj, "CT_Type", 0), # ✅ 0=Client/Prospect, 1=Fournisseur - "est_prospect": getattr(client_obj, "CT_Prospect", 0) == 1, # ✅ Indicateur prospect - } - + 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", "") - except: - pass + # ✅ LOGS DÉTAILLÉS + try: + numero = getattr(client_obj, "CT_Num", "") + except Exception as e: + logger.error(f"❌ Erreur lecture CT_Num: {e}") + raise + + try: + intitule = getattr(client_obj, "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 + + 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, + } - try: - telecom = getattr(client_obj, "Telecom", None) - if telecom: - data["telephone"] = getattr(telecom, "Telephone", "") - data["email"] = getattr(telecom, "EMail", "") - except: - pass + # 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", "") + except Exception as e: + logger.debug(f"⚠ Erreur adresse sur {numero}: {e}") - return data + # Telecom (non critique) + try: + telecom = getattr(client_obj, "Telecom", None) + if telecom: + data["telephone"] = getattr(telecom, "Telephone", "") + data["email"] = getattr(telecom, "EMail", "") + except Exception as e: + logger.debug(f"⚠ Erreur telecom sur {numero}: {e}") + + 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 { @@ -1881,34 +2113,6 @@ class SageConnector: return prospect return None - # ========================================================================= - # FOURNISSEURS (CT_Type = 1) - # ========================================================================= - def lister_tous_fournisseurs(self, filtre=""): - """Liste tous les fournisseurs depuis le cache""" - with self._lock_clients: - if not filtre: - return [c for c in self._cache_clients if c.get("type") == 1] - - filtre_lower = filtre.lower() - return [ - c - for c in self._cache_clients - if c.get("type") == 1 - and ( - filtre_lower in c["numero"].lower() - or filtre_lower in c["intitule"].lower() - ) - ] - - def lire_fournisseur(self, code_fournisseur): - """Retourne un fournisseur depuis le cache""" - with self._lock_clients: - fournisseur = self._cache_clients_dict.get(code_fournisseur) - if fournisseur and fournisseur.get("type") == 1: - return fournisseur - return None - # ========================================================================= # EXTRACTION CLIENTS (Mise Ă  jour pour inclure prospects) # =========================================================================