Made GET fournisseurs

This commit is contained in:
Fanilo-Nantenaina 2025-12-06 14:53:16 +03:00
parent 7ca64e2ea6
commit 3aadc67abf
2 changed files with 299 additions and 1285 deletions

1099
main.py

File diff suppressed because it is too large Load diff

View file

@ -35,11 +35,6 @@ class SageConnector:
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] = {}
# ✅ 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 # 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
@ -119,13 +114,6 @@ class SageConnector:
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(
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
self._start_refresh_thread() self._start_refresh_thread()
@ -177,13 +165,6 @@ class SageConnector:
if age.total_seconds() > self._cache_ttl_minutes * 60: if age.total_seconds() > self._cache_ttl_minutes * 60:
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:
pythoncom.CoUninitialize() pythoncom.CoUninitialize()
@ -222,7 +203,7 @@ class SageConnector:
if obj: if obj:
data = self._extraire_client(obj) data = self._extraire_client(obj)
# ✅ INCLURE TOUS LES TYPES (clients, prospects, fournisseurs) # ✅ INCLURE TOUS LES TYPES (clients, prospects)
clients.append(data) clients.append(data)
clients_dict[data["numero"]] = data clients_dict[data["numero"]] = data
erreurs_consecutives = 0 erreurs_consecutives = 0
@ -246,11 +227,10 @@ class SageConnector:
# 📊 Statistiques détaillées # 📊 Statistiques détaillées
nb_clients = sum(1 for c in clients if c.get("type") == 0 and not c.get("est_prospect")) 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_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( logger.info(
f"✅ Cache actualisé: {len(clients)} tiers " 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: except Exception as e:
@ -305,168 +285,183 @@ 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): 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: if not self.cial:
logger.error("❌ self.cial est None") logger.error("❌ self.cial est None")
# ✅ INITIALISER UN CACHE VIDE même en cas d'erreur return []
self._cache_fournisseurs = []
self._cache_fournisseurs_dict = {}
self._cache_fournisseurs_last_update = None
return
fournisseurs = [] fournisseurs = []
fournisseurs_dict = {}
try: try:
with self._com_context(), self._lock_com: with self._com_context(), self._lock_com:
logger.info("=" * 80) logger.info(f"🔍 Lecture fournisseurs via FactoryFournisseur (filtre='{filtre}')")
logger.info("🔄 DÉBUT REFRESH CACHE FOURNISSEURS")
logger.info("=" * 80)
# ✅ Accéder à FactoryFournisseur
try:
factory = self.cial.CptaApplication.FactoryFournisseur 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 index = 1
max_iterations = 10000
erreurs_consecutives = 0 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: filtre_lower = filtre.lower() if filtre else ""
persist = None
# ✅ ÉTAPE 1 : Lire l'élément (avec gestion d'erreur simple) while index < max_iterations and erreurs_consecutives < max_erreurs:
try: try:
persist = factory.List(index) 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: if persist is None:
logger.info(f"Fin de liste à l'index {index}") logger.debug(f"Fin de liste à l'index {index}")
break break
# ✅ ÉTAPE 2 : Cast (avec gestion d'erreur simple) # Cast
obj = None fourn = self._cast_client(persist)
if fourn:
# ✅✅✅ EXTRACTION DIRECTE (pas de _extraire_client) ✅✅✅
try: try:
obj = self._cast_client(persist) numero = getattr(fourn, "CT_Num", "").strip()
except Exception as e: intitule = getattr(fourn, "CT_Intitule", "").strip()
logger.debug(f"⚠️ Index {index}: _cast_client() échoue - {e}")
if not numero:
logger.debug(f"Index {index}: CT_Num vide, skip")
erreurs_consecutives += 1 erreurs_consecutives += 1
index += 1 index += 1
continue continue
if obj is None: # Construction objet minimal
logger.debug(f"⚠️ Index {index}: _cast_client retourne None (skip)") data = {
index += 1 "numero": numero,
continue "intitule": intitule,
"type": 1, # Fournisseur
"est_fournisseur": True
}
# ✅ ÉTAPE 3 : Extraire # Champs optionnels (avec gestion d'erreur)
data = None
try: try:
data = self._extraire_client(obj) adresse_obj = getattr(fourn, "Adresse", None)
except Exception as e: if adresse_obj:
logger.debug(f"⚠️ Index {index}: _extraire_client() échoue - {e}") data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
erreurs_consecutives += 1 data["code_postal"] = getattr(adresse_obj, "CodePostal", "").strip()
index += 1 data["ville"] = getattr(adresse_obj, "Ville", "").strip()
continue except:
data["adresse"] = ""
data["code_postal"] = ""
data["ville"] = ""
# ✅ ÉTAPE 4 : Vérifier les données try:
if not data or not data.get("numero"): telecom_obj = getattr(fourn, "Telecom", None)
logger.debug(f"⚠️ Index {index}: données invalides (skip)") if telecom_obj:
index += 1 data["telephone"] = getattr(telecom_obj, "Telephone", "").strip()
continue data["email"] = getattr(telecom_obj, "EMail", "").strip()
except:
data["telephone"] = ""
data["email"] = ""
# ✅ SUCCÈS : Marquer et stocker # Filtrer si nécessaire
data["est_fournisseur"] = True if not filtre_lower or \
filtre_lower in numero.lower() or \
filtre_lower in intitule.lower():
fournisseurs.append(data) fournisseurs.append(data)
fournisseurs_dict[data["numero"]] = data logger.debug(f"✅ Fournisseur ajouté: {numero} - {intitule}")
erreurs_consecutives = 0 # ✅ Reset compteur
# Log progression tous les 10 erreurs_consecutives = 0
if len(fournisseurs) % 10 == 0:
logger.info(f"{len(fournisseurs)} fournisseurs chargés...") except Exception as e:
logger.debug(f"⚠️ Erreur extraction index {index}: {e}")
erreurs_consecutives += 1
else:
erreurs_consecutives += 1
index += 1 index += 1
# ✅ TOUJOURS stocker dans les attributs (même si vide) except Exception as e:
with self._lock_clients: logger.debug(f"⚠️ Erreur index {index}: {e}")
self._cache_fournisseurs = fournisseurs erreurs_consecutives += 1
self._cache_fournisseurs_dict = fournisseurs_dict index += 1
self._cache_fournisseurs_last_update = datetime.now()
logger.info("=" * 80) if erreurs_consecutives >= max_erreurs:
logger.info(f"✅ CACHE FOURNISSEURS ACTUALISÉ: {len(fournisseurs)} fournisseurs") logger.warning(f"⚠️ Arrêt après {max_erreurs} erreurs consécutives")
logger.info("=" * 80) break
# ✅ Exemples logger.info(f"{len(fournisseurs)} fournisseurs retournés")
if len(fournisseurs) > 0: return fournisseurs
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: except Exception as e:
logger.error(f"❌ ERREUR GLOBALE refresh fournisseurs: {e}", exc_info=True) logger.error(f"❌ Erreur liste fournisseurs: {e}", exc_info=True)
# ✅ INITIALISER UN CACHE VIDE en cas d'erreur critique return []
with self._lock_clients:
self._cache_fournisseurs = []
self._cache_fournisseurs_dict = {}
self._cache_fournisseurs_last_update = None
def lister_tous_fournisseurs(self, filtre=""): def lire_fournisseur(self, code):
""" """
CORRIGÉ : Liste les fournisseurs depuis le cache dédié NOUVEAU : Lecture d'un fournisseur par code
Utilise FactoryFournisseur.ReadNumero() directement
""" """
# Si le cache fournisseurs n'existe pas ou est vide, le créer if not self.cial:
if not hasattr(self, '_cache_fournisseurs') or len(self._cache_fournisseurs) == 0: return None
logger.info("Cache fournisseurs vide, chargement initial...")
self._refresh_cache_fournisseurs()
with self._lock_clients: try:
if not filtre: with self._com_context(), self._lock_com:
result = self._cache_fournisseurs.copy() factory = self.cial.CptaApplication.FactoryFournisseur
logger.info(f"Liste fournisseurs sans filtre: {len(result)} résultats") persist = factory.ReadNumero(code)
return result
filtre_lower = filtre.lower() if not persist:
result = [ logger.warning(f"Fournisseur {code} introuvable")
f for f in self._cache_fournisseurs return None
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
fourn = self._cast_client(persist)
def lire_fournisseur(self, code_fournisseur): if not fourn:
""" return None
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: # Extraction directe (même logique que lister_tous_fournisseurs)
return self._cache_fournisseurs_dict.get(code_fournisseur) 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
except Exception as e:
logger.error(f"❌ Erreur lecture fournisseur {code}: {e}")
return None
# ========================================================================= # =========================================================================
# API PUBLIQUE (ultra-rapide grâce au cache) # API PUBLIQUE (ultra-rapide grâce au cache)
@ -515,13 +510,8 @@ class SageConnector:
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é") logger.info("✅ Cache actualisé")
# ✅ AJOUT
if hasattr(self, '_refresh_cache_fournisseurs'):
self._refresh_cache_fournisseurs()
logger.info("Cache actualisé") 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 info["ttl_minutes"] = self._cache_ttl_minutes
return info return info
@ -602,83 +576,106 @@ class SageConnector:
# ========================================================================= # =========================================================================
def _extraire_client(self, client_obj): 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 des champs peuvent être manquants
"""
try: try:
# ✅ LOGS DÉTAILLÉS # === 1. CHAMPS OBLIGATOIRES ===
try: 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: except Exception as e:
logger.error(f"❌ Erreur lecture CT_Num: {e}") logger.debug(f"❌ Erreur lecture CT_Num: {e}")
raise return None
try: 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: except Exception as e:
logger.error(f"❌ Erreur lecture CT_Intitule sur {numero}: {e}") logger.debug(f"⚠️ Erreur CT_Intitule sur {numero}: {e}")
raise intitule = ""
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
# === 2. CONSTRUCTION OBJET MINIMAL ===
data = { data = {
"numero": numero, "numero": numero,
"intitule": intitule, "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: try:
adresse = getattr(client_obj, "Adresse", None) adresse = getattr(client_obj, "Adresse", None)
if adresse: if adresse:
data["adresse"] = getattr(adresse, "Adresse", "") try:
data["code_postal"] = getattr(adresse, "CodePostal", "") data["adresse"] = getattr(adresse, "Adresse", "").strip()
data["ville"] = getattr(adresse, "Ville", "") 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: except Exception as e:
logger.debug(f"⚠️ Erreur adresse sur {numero}: {e}") logger.debug(f"⚠️ Erreur adresse sur {numero}: {e}")
data["adresse"] = ""
data["code_postal"] = ""
data["ville"] = ""
# Telecom (non critique) # === 5. 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", "") try:
data["email"] = getattr(telecom, "EMail", "") data["telephone"] = getattr(telecom, "Telephone", "").strip()
except:
data["telephone"] = ""
try:
data["email"] = getattr(telecom, "EMail", "").strip()
except:
data["email"] = ""
except Exception as e: except Exception as e:
logger.debug(f"⚠️ Erreur telecom sur {numero}: {e}") logger.debug(f"⚠️ Erreur telecom sur {numero}: {e}")
data["telephone"] = ""
data["email"] = ""
return data return data
except Exception as e: except Exception as e:
logger.error(f"❌ ERREUR GLOBALE _extraire_client: {e}", exc_info=True) logger.error(f"❌ ERREUR GLOBALE _extraire_client: {e}", exc_info=True)
raise return None
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),
}
# ========================================================================= # =========================================================================
# CRÉATION DEVIS (US-A1) - VERSION TRANSACTIONNELLE # CRÉATION DEVIS (US-A1) - VERSION TRANSACTIONNELLE