Success : create client made with success
This commit is contained in:
parent
6aebf00653
commit
3013f6589c
2 changed files with 414 additions and 129 deletions
202
main.py
202
main.py
|
|
@ -2757,7 +2757,209 @@ def diagnostiquer_detection_fournisseurs():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[DIAG] Erreur diagnostic fournisseurs: {e}", exc_info=True)
|
logger.error(f"[DIAG] Erreur diagnostic fournisseurs: {e}", exc_info=True)
|
||||||
raise HTTPException(500, str(e))
|
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))
|
||||||
|
|
||||||
|
|
||||||
# =====================================================
|
# =====================================================
|
||||||
# LANCEMENT
|
# LANCEMENT
|
||||||
# =====================================================
|
# =====================================================
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,10 @@ class SageConnector:
|
||||||
self._refresh_thread.start()
|
self._refresh_thread.start()
|
||||||
|
|
||||||
def _refresh_cache_clients(self):
|
def _refresh_cache_clients(self):
|
||||||
"""Actualise le cache des clients"""
|
"""
|
||||||
|
Actualise le cache des clients ET fournisseurs
|
||||||
|
Charge TOUS les tiers (CT_Type=0 ET CT_Type=1)
|
||||||
|
"""
|
||||||
if not self.cial:
|
if not self.cial:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -200,6 +203,8 @@ class SageConnector:
|
||||||
erreurs_consecutives = 0
|
erreurs_consecutives = 0
|
||||||
max_erreurs = 50
|
max_erreurs = 50
|
||||||
|
|
||||||
|
logger.info("🔄 Actualisation cache clients/fournisseurs/prospects...")
|
||||||
|
|
||||||
while index < 10000 and erreurs_consecutives < max_erreurs:
|
while index < 10000 and erreurs_consecutives < max_erreurs:
|
||||||
try:
|
try:
|
||||||
persist = factory.List(index)
|
persist = factory.List(index)
|
||||||
|
|
@ -209,6 +214,8 @@ class SageConnector:
|
||||||
obj = self._cast_client(persist)
|
obj = self._cast_client(persist)
|
||||||
if obj:
|
if obj:
|
||||||
data = self._extraire_client(obj)
|
data = self._extraire_client(obj)
|
||||||
|
|
||||||
|
# ✅ INCLURE TOUS LES TYPES (clients, prospects, fournisseurs)
|
||||||
clients.append(data)
|
clients.append(data)
|
||||||
clients_dict[data["numero"]] = data
|
clients_dict[data["numero"]] = data
|
||||||
erreurs_consecutives = 0
|
erreurs_consecutives = 0
|
||||||
|
|
@ -229,11 +236,19 @@ class SageConnector:
|
||||||
self._cache_clients_dict = clients_dict
|
self._cache_clients_dict = clients_dict
|
||||||
self._cache_clients_last_update = datetime.now()
|
self._cache_clients_last_update = datetime.now()
|
||||||
|
|
||||||
logger.info(f" Cache clients actualisé: {len(clients)} clients")
|
# 📊 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)"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f" Erreur refresh clients: {e}", exc_info=True)
|
logger.error(f"❌ Erreur refresh clients: {e}", exc_info=True)
|
||||||
|
|
||||||
def _refresh_cache_articles(self):
|
def _refresh_cache_articles(self):
|
||||||
"""Actualise le cache des articles"""
|
"""Actualise le cache des articles"""
|
||||||
if not self.cial:
|
if not self.cial:
|
||||||
|
|
@ -396,10 +411,12 @@ class SageConnector:
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
def _extraire_client(self, client_obj):
|
def _extraire_client(self, client_obj):
|
||||||
|
"""MISE À JOUR : Extraction avec détection prospect ET type"""
|
||||||
data = {
|
data = {
|
||||||
"numero": getattr(client_obj, "CT_Num", ""),
|
"numero": getattr(client_obj, "CT_Num", ""),
|
||||||
"intitule": getattr(client_obj, "CT_Intitule", ""),
|
"intitule": getattr(client_obj, "CT_Intitule", ""),
|
||||||
"type": getattr(client_obj, "CT_Type", 0),
|
"type": getattr(client_obj, "CT_Type", 0), # ✅ 0=Client/Prospect, 1=Fournisseur
|
||||||
|
"est_prospect": getattr(client_obj, "CT_Prospect", 0) == 1, # ✅ Indicateur prospect
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -2378,7 +2395,7 @@ class SageConnector:
|
||||||
def creer_client(self, client_data: Dict) -> Dict:
|
def creer_client(self, client_data: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
Crée un nouveau client dans Sage 100c via l'API COM.
|
Crée un nouveau client dans Sage 100c via l'API COM.
|
||||||
✅ Validation STRICTE des longueurs + Diagnostic exhaustif
|
✅ VERSION CORRIGÉE : CT_Type supprimé (n'existe pas dans cette version)
|
||||||
"""
|
"""
|
||||||
if not self.cial:
|
if not self.cial:
|
||||||
raise RuntimeError("Connexion Sage non établie")
|
raise RuntimeError("Connexion Sage non établie")
|
||||||
|
|
@ -2386,17 +2403,16 @@ class SageConnector:
|
||||||
try:
|
try:
|
||||||
with self._com_context(), self._lock_com:
|
with self._com_context(), self._lock_com:
|
||||||
# ========================================
|
# ========================================
|
||||||
# ÉTAPE 0 : VALIDATION PRÉALABLE
|
# ÉTAPE 0 : VALIDATION & NETTOYAGE
|
||||||
# ========================================
|
# ========================================
|
||||||
logger.info("🔍 === VALIDATION DES DONNÉES ===")
|
logger.info("🔍 === VALIDATION DES DONNÉES ===")
|
||||||
|
|
||||||
# Intitulé obligatoire
|
|
||||||
if not client_data.get("intitule"):
|
if not client_data.get("intitule"):
|
||||||
raise ValueError("Le champ 'intitule' est obligatoire")
|
raise ValueError("Le champ 'intitule' est obligatoire")
|
||||||
|
|
||||||
# Tronquer TOUS les champs texte
|
# Nettoyage et troncature
|
||||||
intitule = str(client_data["intitule"])[:69].strip()
|
intitule = str(client_data["intitule"])[:69].strip()
|
||||||
num_prop = str(client_data.get("num", "")).upper()[:17].strip()
|
num_prop = str(client_data.get("num", "")).upper()[:17].strip() if client_data.get("num") else ""
|
||||||
compte = str(client_data.get("compte_collectif", "411000"))[:13].strip()
|
compte = str(client_data.get("compte_collectif", "411000"))[:13].strip()
|
||||||
|
|
||||||
adresse = str(client_data.get("adresse", ""))[:35].strip()
|
adresse = str(client_data.get("adresse", ""))[:35].strip()
|
||||||
|
|
@ -2410,111 +2426,153 @@ class SageConnector:
|
||||||
siret = str(client_data.get("siret", ""))[:14].strip()
|
siret = str(client_data.get("siret", ""))[:14].strip()
|
||||||
tva_intra = str(client_data.get("tva_intra", ""))[:25].strip()
|
tva_intra = str(client_data.get("tva_intra", ""))[:25].strip()
|
||||||
|
|
||||||
# Log des valeurs tronquées
|
logger.info(f" intitule: '{intitule}' (len={len(intitule)})")
|
||||||
logger.info(f" intitule: '{intitule}' (len={len(intitule)}/69)")
|
logger.info(f" num: '{num_prop or 'AUTO'}' (len={len(num_prop)})")
|
||||||
logger.info(f" num: '{num_prop or 'AUTO'}' (len={len(num_prop)}/17)")
|
logger.info(f" compte: '{compte}' (len={len(compte)})")
|
||||||
logger.info(f" compte: '{compte}' (len={len(compte)}/13)")
|
|
||||||
logger.info(f" adresse: '{adresse}' (len={len(adresse)}/35)")
|
|
||||||
logger.info(f" code_postal: '{code_postal}' (len={len(code_postal)}/9)")
|
|
||||||
logger.info(f" ville: '{ville}' (len={len(ville)}/35)")
|
|
||||||
logger.info(f" pays: '{pays}' (len={len(pays)}/35)")
|
|
||||||
logger.info(f" telephone: '{telephone}' (len={len(telephone)}/21)")
|
|
||||||
logger.info(f" email: '{email}' (len={len(email)}/69)")
|
|
||||||
logger.info(f" siret: '{siret}' (len={len(siret)}/14)")
|
|
||||||
logger.info(f" tva_intra: '{tva_intra}' (len={len(tva_intra)}/25)")
|
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# ÉTAPE 1 : CRÉATION OBJET CLIENT
|
# ÉTAPE 1 : CRÉATION OBJET CLIENT
|
||||||
# ========================================
|
# ========================================
|
||||||
factory_client = self.cial.CptaApplication.FactoryClient
|
factory_client = self.cial.CptaApplication.FactoryClient
|
||||||
|
|
||||||
persist = factory_client.Create()
|
persist = factory_client.Create()
|
||||||
|
|
||||||
if not persist:
|
|
||||||
raise RuntimeError("Factory.Create() a retourné None")
|
|
||||||
|
|
||||||
client = win32com.client.CastTo(persist, "IBOClient3")
|
client = win32com.client.CastTo(persist, "IBOClient3")
|
||||||
logger.info("✅ Objet client créé")
|
|
||||||
|
# 🔑 CRITIQUE : Initialiser l'objet
|
||||||
|
client.SetDefault()
|
||||||
|
|
||||||
|
logger.info("✅ Objet client créé et initialisé")
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# ÉTAPE 2 : CHAMPS OBLIGATOIRES MINIMAUX
|
# ÉTAPE 2 : CHAMPS OBLIGATOIRES (dans l'ordre !)
|
||||||
# ========================================
|
# ========================================
|
||||||
logger.info("📝 Définition champs obligatoires...")
|
logger.info("📝 Définition des champs obligatoires...")
|
||||||
|
|
||||||
# 1. Intitulé (OBLIGATOIRE)
|
# 1. Intitulé (OBLIGATOIRE)
|
||||||
client.CT_Intitule = intitule
|
client.CT_Intitule = intitule
|
||||||
logger.debug(f" ✅ CT_Intitule: '{intitule}'")
|
logger.debug(f" ✅ CT_Intitule: '{intitule}'")
|
||||||
|
|
||||||
# 2. Qualité (OBLIGATOIRE pour distinguer Client/Fournisseur)
|
# ❌ CT_Type SUPPRIMÉ (n'existe pas dans cette version)
|
||||||
# CT_Qualite : 0=Aucune, 1=Client uniquement, 2=Fournisseur uniquement, 3=Client ET Fournisseur
|
# client.CT_Type = 0 # <-- LIGNE SUPPRIMÉE
|
||||||
|
|
||||||
|
# 2. Qualité (important pour filtrage Client/Fournisseur)
|
||||||
try:
|
try:
|
||||||
client.CT_Qualite = 1 # 1 = Client uniquement
|
client.CT_Qualite = "CLI"
|
||||||
logger.debug(" ✅ CT_Qualite: 1 (Client)")
|
logger.debug(" ✅ CT_Qualite: 'CLI'")
|
||||||
except AttributeError:
|
except:
|
||||||
# Fallback sur CT_Type si CT_Qualite n'existe pas
|
logger.debug(" ⚠️ CT_Qualite non défini (pas critique)")
|
||||||
try:
|
|
||||||
client.CT_Type = 0
|
|
||||||
logger.debug(" ✅ CT_Type: 0 (Client - ancienne version)")
|
|
||||||
except AttributeError:
|
|
||||||
logger.warning(" ⚠️ Ni CT_Qualite ni CT_Type disponible")
|
|
||||||
|
|
||||||
# 3. Compte collectif
|
# 3. Compte général principal (OBLIGATOIRE)
|
||||||
if compte:
|
try:
|
||||||
client.CT_CompteG = compte
|
factory_compte = self.cial.CptaApplication.FactoryCompteG
|
||||||
logger.debug(f" ✅ CT_CompteG: '{compte}'")
|
persist_compte = factory_compte.ReadNumero(compte)
|
||||||
|
|
||||||
|
if persist_compte:
|
||||||
|
compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3")
|
||||||
|
compte_obj.Read()
|
||||||
|
|
||||||
|
# Assigner l'objet CompteG
|
||||||
|
client.CompteGPrinc = compte_obj
|
||||||
|
logger.debug(f" ✅ CompteGPrinc: objet '{compte}' assigné")
|
||||||
|
else:
|
||||||
|
logger.warning(f" ⚠️ Compte {compte} introuvable - utilisation du compte par défaut")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f" ⚠️ Erreur CompteGPrinc: {e}")
|
||||||
|
|
||||||
# 4. Numéro client (optionnel, auto si vide)
|
# 4. Numéro client (OBLIGATOIRE - générer si vide)
|
||||||
if num_prop:
|
if num_prop:
|
||||||
client.CT_Num = num_prop
|
client.CT_Num = num_prop
|
||||||
logger.debug(f" ✅ CT_Num: '{num_prop}'")
|
logger.debug(f" ✅ CT_Num fourni: '{num_prop}'")
|
||||||
|
else:
|
||||||
|
# 🔑 CRITIQUE : Générer le numéro automatiquement
|
||||||
|
try:
|
||||||
|
# Méthode 1 : Utiliser SetDefaultNumPiece (si disponible)
|
||||||
|
if hasattr(client, 'SetDefaultNumPiece'):
|
||||||
|
client.SetDefaultNumPiece()
|
||||||
|
num_genere = getattr(client, "CT_Num", "")
|
||||||
|
logger.debug(f" ✅ CT_Num auto-généré (SetDefaultNumPiece): '{num_genere}'")
|
||||||
|
else:
|
||||||
|
# Méthode 2 : Lire le prochain numéro depuis la souche
|
||||||
|
factory_client = self.cial.CptaApplication.FactoryClient
|
||||||
|
num_genere = factory_client.GetNextNumero()
|
||||||
|
if num_genere:
|
||||||
|
client.CT_Num = num_genere
|
||||||
|
logger.debug(f" ✅ CT_Num auto-généré (GetNextNumero): '{num_genere}'")
|
||||||
|
else:
|
||||||
|
# Méthode 3 : Fallback - utiliser un timestamp
|
||||||
|
import time
|
||||||
|
num_genere = f"CLI{int(time.time()) % 1000000}"
|
||||||
|
client.CT_Num = num_genere
|
||||||
|
logger.warning(f" ⚠️ CT_Num fallback temporaire: '{num_genere}'")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" ❌ Impossible de générer CT_Num: {e}")
|
||||||
|
raise ValueError("Impossible de générer le numéro client automatiquement. Veuillez fournir un numéro manuellement.")
|
||||||
|
|
||||||
# 5. Catégories (valeurs par défaut)
|
# 5. Catégories tarifaires (valeurs par défaut)
|
||||||
try:
|
try:
|
||||||
client.N_CatTarif = 1
|
# Catégorie tarifaire (obligatoire)
|
||||||
client.N_CatCompta = 1
|
if hasattr(client, 'N_CatTarif'):
|
||||||
client.N_Period = 1
|
client.N_CatTarif = 1
|
||||||
client.N_Expedition = 1
|
|
||||||
client.N_Condition = 1
|
# Catégorie comptable (obligatoire)
|
||||||
client.N_Risque = 1
|
if hasattr(client, 'N_CatCompta'):
|
||||||
logger.debug(" ✅ Catégories initialisées")
|
client.N_CatCompta = 1
|
||||||
|
|
||||||
|
# Autres catégories
|
||||||
|
if hasattr(client, 'N_Period'):
|
||||||
|
client.N_Period = 1
|
||||||
|
|
||||||
|
if hasattr(client, 'N_Expedition'):
|
||||||
|
client.N_Expedition = 1
|
||||||
|
|
||||||
|
if hasattr(client, 'N_Condition'):
|
||||||
|
client.N_Condition = 1
|
||||||
|
|
||||||
|
if hasattr(client, 'N_Risque'):
|
||||||
|
client.N_Risque = 1
|
||||||
|
|
||||||
|
logger.debug(" ✅ Catégories (N_*) initialisées")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f" ⚠️ Catégories: {e}")
|
logger.warning(f" ⚠️ Catégories: {e}")
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# ÉTAPE 3 : CHAMPS OPTIONNELS
|
# ÉTAPE 3 : CHAMPS OPTIONNELS MAIS RECOMMANDÉS
|
||||||
# ========================================
|
# ========================================
|
||||||
logger.info("📝 Définition champs optionnels...")
|
logger.info("📝 Définition champs optionnels...")
|
||||||
|
|
||||||
# Adresse
|
# Adresse (objet IAdresse)
|
||||||
if any([adresse, code_postal, ville, pays]):
|
if any([adresse, code_postal, ville, pays]):
|
||||||
try:
|
try:
|
||||||
adresse_obj = getattr(client, "Adresse", None)
|
adresse_obj = client.Adresse
|
||||||
if adresse_obj:
|
|
||||||
if adresse:
|
if adresse:
|
||||||
adresse_obj.Adresse = adresse
|
adresse_obj.Adresse = adresse
|
||||||
if code_postal:
|
if code_postal:
|
||||||
adresse_obj.CodePostal = code_postal
|
adresse_obj.CodePostal = code_postal
|
||||||
if ville:
|
if ville:
|
||||||
adresse_obj.Ville = ville
|
adresse_obj.Ville = ville
|
||||||
if pays:
|
if pays:
|
||||||
adresse_obj.Pays = pays
|
adresse_obj.Pays = pays
|
||||||
logger.debug(" ✅ Adresse définie")
|
|
||||||
|
logger.debug(" ✅ Adresse définie")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f" ⚠️ Adresse: {e}")
|
logger.warning(f" ⚠️ Adresse: {e}")
|
||||||
|
|
||||||
# Télécom
|
# Télécom (objet ITelecom)
|
||||||
if telephone or email:
|
if telephone or email:
|
||||||
try:
|
try:
|
||||||
telecom_obj = getattr(client, "Telecom", None)
|
telecom_obj = client.Telecom
|
||||||
if telecom_obj:
|
|
||||||
if telephone:
|
if telephone:
|
||||||
telecom_obj.Telephone = telephone
|
telecom_obj.Telephone = telephone
|
||||||
if email:
|
if email:
|
||||||
telecom_obj.EMail = email
|
telecom_obj.EMail = email
|
||||||
logger.debug(" ✅ Télécom défini")
|
|
||||||
|
logger.debug(" ✅ Télécom défini")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f" ⚠️ Télécom: {e}")
|
logger.warning(f" ⚠️ Télécom: {e}")
|
||||||
|
|
||||||
# Identifiants
|
# Identifiants fiscaux
|
||||||
if siret:
|
if siret:
|
||||||
try:
|
try:
|
||||||
client.CT_Siret = siret
|
client.CT_Siret = siret
|
||||||
|
|
@ -2525,38 +2583,76 @@ class SageConnector:
|
||||||
if tva_intra:
|
if tva_intra:
|
||||||
try:
|
try:
|
||||||
client.CT_Identifiant = tva_intra
|
client.CT_Identifiant = tva_intra
|
||||||
logger.debug(f" ✅ TVA: '{tva_intra}'")
|
logger.debug(f" ✅ TVA intracommunautaire: '{tva_intra}'")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f" ⚠️ TVA: {e}")
|
logger.warning(f" ⚠️ TVA: {e}")
|
||||||
|
|
||||||
# NumPayeur (= NumClient si défini)
|
# Autres champs utiles (valeurs par défaut intelligentes)
|
||||||
if num_prop:
|
try:
|
||||||
try:
|
# Type de facturation (1 = facture normale)
|
||||||
client.CT_NumPayeur = num_prop
|
if hasattr(client, 'CT_Facture'):
|
||||||
logger.debug(f" ✅ CT_NumPayeur: '{num_prop}'")
|
client.CT_Facture = 1
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f" ⚠️ CT_NumPayeur: {e}")
|
# Lettrage automatique activé
|
||||||
|
if hasattr(client, 'CT_Lettrage'):
|
||||||
|
client.CT_Lettrage = True
|
||||||
|
|
||||||
|
# Pas de prospect
|
||||||
|
if hasattr(client, 'CT_Prospect'):
|
||||||
|
client.CT_Prospect = False
|
||||||
|
|
||||||
|
# Client actif (pas en sommeil)
|
||||||
|
if hasattr(client, 'CT_Sommeil'):
|
||||||
|
client.CT_Sommeil = False
|
||||||
|
|
||||||
|
logger.debug(" ✅ Options par défaut définies")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f" ⚠️ Options: {e}")
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# ÉTAPE 4 : DIAGNOSTIC PRÉ-WRITE
|
# ÉTAPE 4 : DIAGNOSTIC PRÉ-WRITE (pour debug)
|
||||||
# ========================================
|
# ========================================
|
||||||
logger.info("🔍 === DIAGNOSTIC PRÉ-WRITE ===")
|
logger.info("🔍 === DIAGNOSTIC PRÉ-WRITE ===")
|
||||||
|
|
||||||
champs_critiques = [
|
champs_critiques = [
|
||||||
"CT_Intitule", "CT_Num", "CT_Type", "CT_CompteG",
|
("CT_Intitule", "str"),
|
||||||
"CT_NumPayeur", "N_CatTarif", "N_CatCompta"
|
("CT_Num", "str"),
|
||||||
|
("CompteGPrinc", "object"),
|
||||||
|
("N_CatTarif", "int"),
|
||||||
|
("N_CatCompta", "int"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for champ in champs_critiques:
|
for champ, type_attendu in champs_critiques:
|
||||||
try:
|
try:
|
||||||
val = getattr(client, champ, "N/A")
|
val = getattr(client, champ, None)
|
||||||
longueur = len(str(val)) if val not in [None, "N/A"] else 0
|
|
||||||
logger.info(f" {champ}: {repr(val)} (len={longueur})")
|
if type_attendu == "object":
|
||||||
|
status = "✅ Objet défini" if val else "❌ NULL"
|
||||||
|
else:
|
||||||
|
if type_attendu == "str":
|
||||||
|
status = f"✅ '{val}' (len={len(val)})" if val else "❌ Vide"
|
||||||
|
else:
|
||||||
|
status = f"✅ {val}"
|
||||||
|
|
||||||
|
logger.info(f" {champ}: {status}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f" {champ}: Erreur lecture - {e}")
|
logger.error(f" {champ}: ❌ Erreur - {e}")
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# ÉTAPE 5 : ÉCRITURE EN BASE
|
# ÉTAPE 5 : VÉRIFICATION FINALE CT_Num
|
||||||
|
# ========================================
|
||||||
|
num_avant_write = getattr(client, "CT_Num", "")
|
||||||
|
if not num_avant_write:
|
||||||
|
logger.error("❌ CRITIQUE: CT_Num toujours vide après toutes les tentatives !")
|
||||||
|
raise ValueError(
|
||||||
|
"Le numéro client (CT_Num) est obligatoire mais n'a pas pu être généré. "
|
||||||
|
"Veuillez fournir un numéro manuellement via le paramètre 'num'."
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"✅ CT_Num confirmé avant Write(): '{num_avant_write}'")
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# ÉTAPE 6 : ÉCRITURE EN BASE
|
||||||
# ========================================
|
# ========================================
|
||||||
logger.info("💾 Écriture du client dans Sage...")
|
logger.info("💾 Écriture du client dans Sage...")
|
||||||
|
|
||||||
|
|
@ -2565,43 +2661,39 @@ class SageConnector:
|
||||||
logger.info("✅ Write() réussi !")
|
logger.info("✅ Write() réussi !")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Récupérer erreur Sage détaillée
|
|
||||||
error_detail = str(e)
|
error_detail = str(e)
|
||||||
sage_error_desc = None
|
|
||||||
|
|
||||||
|
# Récupérer l'erreur Sage détaillée
|
||||||
try:
|
try:
|
||||||
sage_error = self.cial.CptaApplication.LastError
|
sage_error = self.cial.CptaApplication.LastError
|
||||||
if sage_error:
|
if sage_error:
|
||||||
sage_error_desc = sage_error.Description
|
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
|
||||||
error_detail = f"{sage_error_desc} (Code: {sage_error.Number})"
|
|
||||||
logger.error(f"❌ Erreur Sage: {error_detail}")
|
logger.error(f"❌ Erreur Sage: {error_detail}")
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Dump COMPLET des champs pour debug
|
# Analyser l'erreur spécifique
|
||||||
logger.error("❌ ÉCHEC Write() - DUMP COMPLET:")
|
if "longueur invalide" in error_detail.lower():
|
||||||
|
logger.error("❌ ERREUR 'longueur invalide' - Dump des champs:")
|
||||||
|
|
||||||
|
for attr in dir(client):
|
||||||
|
if attr.startswith("CT_") or attr.startswith("N_"):
|
||||||
|
try:
|
||||||
|
val = getattr(client, attr, None)
|
||||||
|
if isinstance(val, str):
|
||||||
|
logger.error(f" {attr}: '{val}' (len={len(val)})")
|
||||||
|
elif val is not None and not callable(val):
|
||||||
|
logger.error(f" {attr}: {val} (type={type(val).__name__})")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
for attr in dir(client):
|
if "doublon" in error_detail.lower() or "existe" in error_detail.lower():
|
||||||
if (attr.startswith("CT_") or
|
raise ValueError(f"Ce client existe déjà : {error_detail}")
|
||||||
attr.startswith("N_") or
|
|
||||||
attr.startswith("cbMarq")):
|
|
||||||
try:
|
|
||||||
val = getattr(client, attr, None)
|
|
||||||
if val is not None and not callable(val):
|
|
||||||
longueur = len(str(val)) if isinstance(val, str) else "N/A"
|
|
||||||
logger.error(f" {attr}: {repr(val)} (type={type(val).__name__}, len={longueur})")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Tester si c'est un doublon
|
|
||||||
if sage_error_desc and ("doublon" in sage_error_desc.lower() or
|
|
||||||
"existe" in sage_error_desc.lower()):
|
|
||||||
raise ValueError(f"Ce client existe déjà: {sage_error_desc}")
|
|
||||||
|
|
||||||
raise RuntimeError(f"Échec Write(): {error_detail}")
|
raise RuntimeError(f"Échec Write(): {error_detail}")
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# ÉTAPE 6 : RELECTURE & FINALISATION
|
# ÉTAPE 7 : RELECTURE & FINALISATION
|
||||||
# ========================================
|
# ========================================
|
||||||
try:
|
try:
|
||||||
client.Read()
|
client.Read()
|
||||||
|
|
@ -2613,19 +2705,10 @@ class SageConnector:
|
||||||
if not num_final:
|
if not num_final:
|
||||||
raise RuntimeError("CT_Num vide après Write()")
|
raise RuntimeError("CT_Num vide après Write()")
|
||||||
|
|
||||||
# Si auto-numérotation, définir CT_NumPayeur maintenant
|
|
||||||
if not num_prop and num_final:
|
|
||||||
try:
|
|
||||||
client.CT_NumPayeur = num_final
|
|
||||||
client.Write()
|
|
||||||
logger.debug(f"✅ CT_NumPayeur auto-défini: {num_final}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"⚠️ CT_NumPayeur final: {e}")
|
|
||||||
|
|
||||||
logger.info(f"✅✅✅ CLIENT CRÉÉ: {num_final} - {intitule} ✅✅✅")
|
logger.info(f"✅✅✅ CLIENT CRÉÉ: {num_final} - {intitule} ✅✅✅")
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# ÉTAPE 7 : REFRESH CACHE
|
# ÉTAPE 8 : REFRESH CACHE
|
||||||
# ========================================
|
# ========================================
|
||||||
self._refresh_cache_clients()
|
self._refresh_cache_clients()
|
||||||
|
|
||||||
|
|
@ -2633,7 +2716,7 @@ class SageConnector:
|
||||||
"numero": num_final,
|
"numero": num_final,
|
||||||
"intitule": intitule,
|
"intitule": intitule,
|
||||||
"compte_collectif": compte,
|
"compte_collectif": compte,
|
||||||
"type": 0,
|
"type": 0, # Par défaut client
|
||||||
"adresse": adresse or None,
|
"adresse": adresse or None,
|
||||||
"code_postal": code_postal or None,
|
"code_postal": code_postal or None,
|
||||||
"ville": ville or None,
|
"ville": ville or None,
|
||||||
|
|
@ -2645,7 +2728,6 @@ class SageConnector:
|
||||||
}
|
}
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
# Erreur métier (doublon, validation)
|
|
||||||
logger.error(f"❌ Erreur métier: {e}")
|
logger.error(f"❌ Erreur métier: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
@ -2661,4 +2743,5 @@ class SageConnector:
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
raise RuntimeError(f"Erreur technique Sage: {error_message}")
|
raise RuntimeError(f"Erreur technique Sage: {error_message}")
|
||||||
|
|
||||||
Loading…
Reference in a new issue