Push simple

This commit is contained in:
Fanilo-Nantenaina 2025-12-06 12:57:19 +03:00
parent be7bc287c0
commit 7ca64e2ea6
2 changed files with 972 additions and 97 deletions

683
main.py
View file

@ -2535,19 +2535,69 @@ def prospect_get(req: CodeRequest):
# ENDPOINTS - FOURNISSEURS # ENDPOINTS - FOURNISSEURS
# ===================================================== # =====================================================
@app.post("/sage/fournisseurs/list", dependencies=[Depends(verify_token)]) @app.post("/sage/fournisseurs/list", dependencies=[Depends(verify_token)])
def fournisseurs_list(req: FiltreRequest): def fournisseurs_list_direct(req: FiltreRequest):
"""📋 Liste tous les fournisseurs (CT_Type=1)""" """
ENDPOINT DIRECT : Liste fournisseurs SANS passer par le cache
Lecture directe depuis FactoryFournisseur - toujours à jour
"""
try: try:
fournisseurs = sage.lister_tous_fournisseurs(req.filtre) if not sage or not sage.cial:
return {"success": True, "data": fournisseurs} 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: except Exception as e:
logger.error(f"Erreur liste fournisseurs: {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)
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)]) @app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)])
def fournisseur_get(req: CodeRequest): def fournisseur_get(req: CodeRequest):
"""📄 Lecture d'un fournisseur par code""" """
NOUVEAU : Lecture d'un fournisseur par code
"""
try: try:
fournisseur = sage.lire_fournisseur(req.code) fournisseur = sage.lire_fournisseur(req.code)
if not fournisseur: if not fournisseur:
@ -2981,6 +3031,627 @@ def diagnostiquer_longueurs_champs():
raise HTTPException(500, str(e)) 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 # LANCEMENT
# ===================================================== # =====================================================

View file

@ -29,13 +29,18 @@ class SageConnector:
self.mot_de_passe = mot_de_passe self.mot_de_passe = mot_de_passe
self.cial = None self.cial = None
# Cache # Caches existants
self._cache_clients: List[Dict] = [] self._cache_clients: List[Dict] = []
self._cache_articles: List[Dict] = [] self._cache_articles: List[Dict] = []
self._cache_clients_dict: Dict[str, Dict] = {} self._cache_clients_dict: Dict[str, Dict] = {}
self._cache_articles_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_clients_last_update: Optional[datetime] = None
self._cache_articles_last_update: Optional[datetime] = None self._cache_articles_last_update: Optional[datetime] = None
self._cache_ttl_minutes = 15 self._cache_ttl_minutes = 15
@ -44,10 +49,10 @@ class SageConnector:
self._refresh_thread: Optional[threading.Thread] = None self._refresh_thread: Optional[threading.Thread] = None
self._stop_refresh = threading.Event() self._stop_refresh = threading.Event()
# Locks thread-safe # Locks
self._lock_clients = threading.RLock() self._lock_clients = threading.RLock()
self._lock_articles = 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() self._thread_local = threading.local()
@ -102,22 +107,24 @@ class SageConnector:
"""Connexion initiale à Sage""" """Connexion initiale à Sage"""
try: try:
with self._com_context(): with self._com_context():
self.cial = win32com.client.gencache.EnsureDispatch( self.cial = win32com.client.gencache.EnsureDispatch("Objets100c.Cial.Stream")
"Objets100c.Cial.Stream"
)
self.cial.Name = self.chemin_base self.cial.Name = self.chemin_base
self.cial.Loggable.UserName = self.utilisateur self.cial.Loggable.UserName = self.utilisateur
self.cial.Loggable.UserPwd = self.mot_de_passe self.cial.Loggable.UserPwd = self.mot_de_passe
self.cial.Open() 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 # Chargement initial du cache
logger.info("Chargement initial du cache...") logger.info("📦 Chargement initial du cache...")
self._refresh_cache_clients() self._refresh_cache_clients()
self._refresh_cache_articles() self._refresh_cache_articles()
self._refresh_cache_fournisseurs() # ✅ CETTE LIGNE DOIT ÊTRE LÀ
logger.info( 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 # Démarrage du thread d'actualisation
@ -126,7 +133,7 @@ class SageConnector:
return True return True
except Exception as e: 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 return False
def deconnecter(self): def deconnecter(self):
@ -152,32 +159,32 @@ class SageConnector:
"""Démarre le thread d'actualisation automatique""" """Démarre le thread d'actualisation automatique"""
def refresh_loop(): def refresh_loop():
# Initialiser COM pour ce thread worker
pythoncom.CoInitialize() pythoncom.CoInitialize()
try: try:
while not self._stop_refresh.is_set(): while not self._stop_refresh.is_set():
time.sleep(60) # Vérifier toutes les minutes time.sleep(60)
# Clients # Clients
if self._cache_clients_last_update: if self._cache_clients_last_update:
age = datetime.now() - self._cache_clients_last_update age = datetime.now() - self._cache_clients_last_update
if age.total_seconds() > self._cache_ttl_minutes * 60: if age.total_seconds() > self._cache_ttl_minutes * 60:
logger.info(
f"Actualisation cache clients (âge: {age.seconds//60}min)"
)
self._refresh_cache_clients() self._refresh_cache_clients()
# Articles # Articles
if self._cache_articles_last_update: if self._cache_articles_last_update:
age = datetime.now() - self._cache_articles_last_update age = datetime.now() - self._cache_articles_last_update
if age.total_seconds() > self._cache_ttl_minutes * 60: if age.total_seconds() > self._cache_ttl_minutes * 60:
logger.info(
f"Actualisation cache articles (âge: {age.seconds//60}min)"
)
self._refresh_cache_articles() 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: finally:
# Nettoyer COM en fin de thread
pythoncom.CoUninitialize() pythoncom.CoUninitialize()
self._refresh_thread = threading.Thread( self._refresh_thread = threading.Thread(
@ -298,6 +305,169 @@ class SageConnector:
except Exception as e: except Exception as e:
logger.error(f" Erreur refresh articles: {e}", exc_info=True) 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) # API PUBLIQUE (ultra-rapide grâce au cache)
# ========================================================================= # =========================================================================
@ -342,15 +512,23 @@ class SageConnector:
def forcer_actualisation_cache(self): def forcer_actualisation_cache(self):
"""Force l'actualisation immédiate du cache (endpoint admin)""" """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_clients()
self._refresh_cache_articles() 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é") logger.info("Cache actualisé")
def get_cache_info(self): def get_cache_info(self):
"""Retourne les infos du cache (endpoint monitoring)""" """Retourne les infos du cache (endpoint monitoring)"""
with self._lock_clients, self._lock_articles: with self._lock_clients:
return { info = {
"clients": { "clients": {
"count": len(self._cache_clients), "count": len(self._cache_clients),
"last_update": ( "last_update": (
@ -359,10 +537,7 @@ class SageConnector:
else None else None
), ),
"age_minutes": ( "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 if self._cache_clients_last_update
else None else None
), ),
@ -375,16 +550,31 @@ class SageConnector:
else None else None
), ),
"age_minutes": ( "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 if self._cache_articles_last_update
else None 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 # CAST HELPERS
@ -395,7 +585,8 @@ class SageConnector:
obj = win32com.client.CastTo(persist_obj, "IBOClient3") obj = win32com.client.CastTo(persist_obj, "IBOClient3")
obj.Read() obj.Read()
return obj return obj
except: except Exception as e:
logger.debug(f"❌ _cast_client échoue: {e}") # ✅ AJOUTER CE LOG
return None return None
def _cast_article(self, persist_obj): def _cast_article(self, persist_obj):
@ -412,32 +603,73 @@ class SageConnector:
def _extraire_client(self, client_obj): def _extraire_client(self, client_obj):
"""MISE À JOUR : Extraction avec détection prospect ET type""" """MISE À JOUR : Extraction avec détection prospect ET type"""
try:
# ✅ 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 = { data = {
"numero": getattr(client_obj, "CT_Num", ""), "numero": numero,
"intitule": getattr(client_obj, "CT_Intitule", ""), "intitule": intitule,
"type": getattr(client_obj, "CT_Type", 0), # ✅ 0=Client/Prospect, 1=Fournisseur "type": type_tiers,
"est_prospect": getattr(client_obj, "CT_Prospect", 0) == 1, # ✅ Indicateur prospect "qualite": qualite,
"est_prospect": prospect,
"est_fournisseur": qualite in [2, 3] if qualite is not None else False,
} }
# Adresse (non critique)
try: try:
adresse = getattr(client_obj, "Adresse", None) adresse = getattr(client_obj, "Adresse", None)
if adresse: if adresse:
data["adresse"] = getattr(adresse, "Adresse", "") data["adresse"] = getattr(adresse, "Adresse", "")
data["code_postal"] = getattr(adresse, "CodePostal", "") data["code_postal"] = getattr(adresse, "CodePostal", "")
data["ville"] = getattr(adresse, "Ville", "") data["ville"] = getattr(adresse, "Ville", "")
except: except Exception as e:
pass logger.debug(f"⚠️ Erreur adresse sur {numero}: {e}")
# Telecom (non critique)
try: try:
telecom = getattr(client_obj, "Telecom", None) telecom = getattr(client_obj, "Telecom", None)
if telecom: if telecom:
data["telephone"] = getattr(telecom, "Telephone", "") data["telephone"] = getattr(telecom, "Telephone", "")
data["email"] = getattr(telecom, "EMail", "") data["email"] = getattr(telecom, "EMail", "")
except: except Exception as e:
pass logger.debug(f"⚠️ Erreur telecom sur {numero}: {e}")
return data return data
except Exception as e:
logger.error(f"❌ ERREUR GLOBALE _extraire_client: {e}", exc_info=True)
raise
def _extraire_article(self, article_obj): def _extraire_article(self, article_obj):
return { return {
"reference": getattr(article_obj, "AR_Ref", ""), "reference": getattr(article_obj, "AR_Ref", ""),
@ -1881,34 +2113,6 @@ class SageConnector:
return prospect return prospect
return None 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) # EXTRACTION CLIENTS (Mise à jour pour inclure prospects)
# ========================================================================= # =========================================================================