diff --git a/main.py b/main.py index bea9c35..7df753b 100644 --- a/main.py +++ b/main.py @@ -2757,7 +2757,209 @@ def diagnostiquer_detection_fournisseurs(): except Exception as e: logger.error(f"[DIAG] Erreur diagnostic fournisseurs: {e}", exc_info=True) raise HTTPException(500, str(e)) + +@app.get("/sage/diagnostic/longueurs-client", dependencies=[Depends(verify_token)]) +def diagnostiquer_longueurs_champs(): + """ + 🔍 DIAGNOSTIC : Découvre les longueurs maximales autorisées pour chaque champ + + Teste en définissant des valeurs de différentes longueurs et en appelant Write() + """ + try: + if not sage or not sage.cial: + raise HTTPException(503, "Service Sage indisponible") + + with sage._com_context(), sage._lock_com: + factory_client = sage.cial.CptaApplication.FactoryClient + + # Créer un client de test + persist = factory_client.Create() + client = win32com.client.CastTo(persist, "IBOClient3") + client.SetDefault() + + diagnostic = { + "champs_testes": [] + } + + # Liste des champs texte à tester + champs_a_tester = [ + ("CT_Intitule", "Société Test"), + ("CT_Num", "TEST001"), + ("CT_Qualite", "CLI"), + ("CT_Raccourci", "TST"), + ("CT_Contact", "Jean Dupont"), + ("CT_Siret", "12345678901234"), + ("CT_Identifiant", "FR12345678901"), + ("CT_Ape", "6201Z"), + ("CT_Coface", "123456"), + ] + + for champ, valeur_test in champs_a_tester: + try: + if not hasattr(client, champ): + diagnostic["champs_testes"].append({ + "champ": champ, + "existe": False + }) + continue + + # Tester différentes longueurs + longueurs_testees = [] + + for longueur in [10, 20, 35, 50, 69, 100]: + try: + # Créer une chaîne de cette longueur + test_val = valeur_test[:longueur].ljust(longueur, 'X') + + # Essayer de définir + setattr(client, champ, test_val) + + # Relire + val_relue = getattr(client, champ, "") + + longueurs_testees.append({ + "longueur": longueur, + "accepte": True, + "valeur_tronquee": len(val_relue) < longueur, + "longueur_reelle": len(val_relue) + }) + except Exception as e: + longueurs_testees.append({ + "longueur": longueur, + "accepte": False, + "erreur": str(e)[:100] + }) + + # Trouver la longueur max acceptée + longueurs_acceptees = [ + lt["longueur_reelle"] for lt in longueurs_testees + if lt.get("accepte") + ] + + diagnostic["champs_testes"].append({ + "champ": champ, + "existe": True, + "longueur_max": max(longueurs_acceptees) if longueurs_acceptees else 0, + "details": longueurs_testees + }) + + except Exception as e: + diagnostic["champs_testes"].append({ + "champ": champ, + "existe": True, + "erreur_test": str(e)[:200] + }) + + # ======================================== + # TEST CRITIQUE : Valeurs par défaut problématiques + # ======================================== + diagnostic["valeurs_par_defaut"] = {} + + # Récupérer TOUTES les valeurs par défaut après SetDefault() + for attr in dir(client): + if attr.startswith("CT_") or attr.startswith("N_"): + try: + val = getattr(client, attr, None) + + if isinstance(val, str) and len(val) > 0: + diagnostic["valeurs_par_defaut"][attr] = { + "valeur": val, + "longueur": len(val) + } + except: + pass + + # ======================================== + # TEST : Simuler la création avec valeurs minimales + # ======================================== + try: + # Réinitialiser + persist2 = factory_client.Create() + client2 = win32com.client.CastTo(persist2, "IBOClient3") + client2.SetDefault() + + # Définir UNIQUEMENT les champs absolument minimaux + client2.CT_Intitule = "TEST" + client2.CT_Type = 0 + + # Essayer d'assigner le compte + try: + factory_compte = sage.cial.CptaApplication.FactoryCompteG + persist_compte = factory_compte.ReadNumero("411000") + + if persist_compte: + compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3") + compte_obj.Read() + client2.CompteGPrinc = compte_obj + + diagnostic["test_compte"] = { + "compte_trouve": True, + "compte_numero": "411000" + } + else: + diagnostic["test_compte"] = { + "compte_trouve": False, + "erreur": "Compte 411000 introuvable" + } + except Exception as e: + diagnostic["test_compte"] = { + "erreur": str(e) + } + + # Tenter le Write() (sans commit) + try: + client2.Write() + + diagnostic["test_write_minimal"] = { + "succes": True, + "message": "✅ Write() réussi avec configuration minimale" + } + + # Lire le numéro généré + num_genere = getattr(client2, "CT_Num", "") + diagnostic["test_write_minimal"]["numero_genere"] = num_genere + + # ⚠️ ATTENTION : Supprimer le client de test si on ne veut pas le garder + # Pour le moment, on le laisse en commentaire + # client2.Remove() + + except Exception as e: + diagnostic["test_write_minimal"] = { + "succes": False, + "erreur": str(e), + "erreur_sage": None + } + + # Récupérer l'erreur Sage détaillée + try: + sage_error = sage.cial.CptaApplication.LastError + if sage_error: + diagnostic["test_write_minimal"]["erreur_sage"] = { + "description": sage_error.Description, + "numero": sage_error.Number + } + except: + pass + + except Exception as e: + diagnostic["test_write_minimal"] = { + "succes": False, + "erreur_init": str(e) + } + + logger.info("[DIAG] Analyse longueurs terminée") + + return { + "success": True, + "diagnostic": diagnostic + } + + except Exception as e: + logger.error(f"[DIAG] Erreur diagnostic longueurs: {e}", exc_info=True) + raise HTTPException(500, str(e)) + + # ===================================================== # LANCEMENT # ===================================================== diff --git a/sage_connector.py b/sage_connector.py index 14a8625..540bc35 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -186,7 +186,10 @@ class SageConnector: self._refresh_thread.start() 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: return @@ -200,6 +203,8 @@ class SageConnector: erreurs_consecutives = 0 max_erreurs = 50 + logger.info("🔄 Actualisation cache clients/fournisseurs/prospects...") + while index < 10000 and erreurs_consecutives < max_erreurs: try: persist = factory.List(index) @@ -209,6 +214,8 @@ class SageConnector: obj = self._cast_client(persist) if obj: data = self._extraire_client(obj) + + # ✅ INCLURE TOUS LES TYPES (clients, prospects, fournisseurs) clients.append(data) clients_dict[data["numero"]] = data erreurs_consecutives = 0 @@ -229,11 +236,19 @@ class SageConnector: self._cache_clients_dict = clients_dict 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: - 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): """Actualise le cache des articles""" if not self.cial: @@ -396,10 +411,12 @@ 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), + "type": getattr(client_obj, "CT_Type", 0), # ✅ 0=Client/Prospect, 1=Fournisseur + "est_prospect": getattr(client_obj, "CT_Prospect", 0) == 1, # ✅ Indicateur prospect } try: @@ -2378,7 +2395,7 @@ class SageConnector: def creer_client(self, client_data: Dict) -> Dict: """ 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: raise RuntimeError("Connexion Sage non établie") @@ -2386,17 +2403,16 @@ class SageConnector: try: with self._com_context(), self._lock_com: # ======================================== - # ÉTAPE 0 : VALIDATION PRÉALABLE + # ÉTAPE 0 : VALIDATION & NETTOYAGE # ======================================== logger.info("🔍 === VALIDATION DES DONNÉES ===") - # Intitulé obligatoire if not client_data.get("intitule"): raise ValueError("Le champ 'intitule' est obligatoire") - # Tronquer TOUS les champs texte + # Nettoyage et troncature 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() adresse = str(client_data.get("adresse", ""))[:35].strip() @@ -2410,111 +2426,153 @@ class SageConnector: siret = str(client_data.get("siret", ""))[:14].strip() tva_intra = str(client_data.get("tva_intra", ""))[:25].strip() - # Log des valeurs tronquées - logger.info(f" intitule: '{intitule}' (len={len(intitule)}/69)") - logger.info(f" num: '{num_prop or 'AUTO'}' (len={len(num_prop)}/17)") - 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)") + logger.info(f" intitule: '{intitule}' (len={len(intitule)})") + logger.info(f" num: '{num_prop or 'AUTO'}' (len={len(num_prop)})") + logger.info(f" compte: '{compte}' (len={len(compte)})") # ======================================== # ÉTAPE 1 : CRÉATION OBJET CLIENT # ======================================== factory_client = self.cial.CptaApplication.FactoryClient + persist = factory_client.Create() - - if not persist: - raise RuntimeError("Factory.Create() a retourné None") - 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) client.CT_Intitule = intitule logger.debug(f" ✅ CT_Intitule: '{intitule}'") - # 2. Qualité (OBLIGATOIRE pour distinguer Client/Fournisseur) - # CT_Qualite : 0=Aucune, 1=Client uniquement, 2=Fournisseur uniquement, 3=Client ET Fournisseur + # ❌ CT_Type SUPPRIMÉ (n'existe pas dans cette version) + # client.CT_Type = 0 # <-- LIGNE SUPPRIMÉE + + # 2. Qualité (important pour filtrage Client/Fournisseur) try: - client.CT_Qualite = 1 # 1 = Client uniquement - logger.debug(" ✅ CT_Qualite: 1 (Client)") - except AttributeError: - # Fallback sur CT_Type si CT_Qualite n'existe pas - try: - client.CT_Type = 0 - logger.debug(" ✅ CT_Type: 0 (Client - ancienne version)") - except AttributeError: - logger.warning(" ⚠️ Ni CT_Qualite ni CT_Type disponible") + client.CT_Qualite = "CLI" + logger.debug(" ✅ CT_Qualite: 'CLI'") + except: + logger.debug(" ⚠️ CT_Qualite non défini (pas critique)") - # 3. Compte collectif - if compte: - client.CT_CompteG = compte - logger.debug(f" ✅ CT_CompteG: '{compte}'") + # 3. Compte général principal (OBLIGATOIRE) + try: + factory_compte = self.cial.CptaApplication.FactoryCompteG + 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: 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: - client.N_CatTarif = 1 - client.N_CatCompta = 1 - client.N_Period = 1 - client.N_Expedition = 1 - client.N_Condition = 1 - client.N_Risque = 1 - logger.debug(" ✅ Catégories initialisées") + # Catégorie tarifaire (obligatoire) + if hasattr(client, 'N_CatTarif'): + client.N_CatTarif = 1 + + # Catégorie comptable (obligatoire) + if hasattr(client, 'N_CatCompta'): + 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: logger.warning(f" ⚠️ Catégories: {e}") # ======================================== - # ÉTAPE 3 : CHAMPS OPTIONNELS + # ÉTAPE 3 : CHAMPS OPTIONNELS MAIS RECOMMANDÉS # ======================================== logger.info("📝 Définition champs optionnels...") - # Adresse + # Adresse (objet IAdresse) if any([adresse, code_postal, ville, pays]): try: - adresse_obj = getattr(client, "Adresse", None) - if adresse_obj: - if adresse: - adresse_obj.Adresse = adresse - if code_postal: - adresse_obj.CodePostal = code_postal - if ville: - adresse_obj.Ville = ville - if pays: - adresse_obj.Pays = pays - logger.debug(" ✅ Adresse définie") + adresse_obj = client.Adresse + + if adresse: + adresse_obj.Adresse = adresse + if code_postal: + adresse_obj.CodePostal = code_postal + if ville: + adresse_obj.Ville = ville + if pays: + adresse_obj.Pays = pays + + logger.debug(" ✅ Adresse définie") except Exception as e: logger.warning(f" ⚠️ Adresse: {e}") - # Télécom + # Télécom (objet ITelecom) if telephone or email: try: - telecom_obj = getattr(client, "Telecom", None) - if telecom_obj: - if telephone: - telecom_obj.Telephone = telephone - if email: - telecom_obj.EMail = email - logger.debug(" ✅ Télécom défini") + telecom_obj = client.Telecom + + if telephone: + telecom_obj.Telephone = telephone + if email: + telecom_obj.EMail = email + + logger.debug(" ✅ Télécom défini") except Exception as e: logger.warning(f" ⚠️ Télécom: {e}") - # Identifiants + # Identifiants fiscaux if siret: try: client.CT_Siret = siret @@ -2525,38 +2583,76 @@ class SageConnector: if tva_intra: try: client.CT_Identifiant = tva_intra - logger.debug(f" ✅ TVA: '{tva_intra}'") + logger.debug(f" ✅ TVA intracommunautaire: '{tva_intra}'") except Exception as e: logger.warning(f" ⚠️ TVA: {e}") - # NumPayeur (= NumClient si défini) - if num_prop: - try: - client.CT_NumPayeur = num_prop - logger.debug(f" ✅ CT_NumPayeur: '{num_prop}'") - except Exception as e: - logger.warning(f" ⚠️ CT_NumPayeur: {e}") + # Autres champs utiles (valeurs par défaut intelligentes) + try: + # Type de facturation (1 = facture normale) + if hasattr(client, 'CT_Facture'): + client.CT_Facture = 1 + + # 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 ===") champs_critiques = [ - "CT_Intitule", "CT_Num", "CT_Type", "CT_CompteG", - "CT_NumPayeur", "N_CatTarif", "N_CatCompta" + ("CT_Intitule", "str"), + ("CT_Num", "str"), + ("CompteGPrinc", "object"), + ("N_CatTarif", "int"), + ("N_CatCompta", "int"), ] - for champ in champs_critiques: + for champ, type_attendu in champs_critiques: try: - val = getattr(client, champ, "N/A") - longueur = len(str(val)) if val not in [None, "N/A"] else 0 - logger.info(f" {champ}: {repr(val)} (len={longueur})") + val = getattr(client, champ, None) + + 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: - 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...") @@ -2565,43 +2661,39 @@ class SageConnector: logger.info("✅ Write() réussi !") except Exception as e: - # Récupérer erreur Sage détaillée error_detail = str(e) - sage_error_desc = None + # Récupérer l'erreur Sage détaillée try: sage_error = self.cial.CptaApplication.LastError if sage_error: - sage_error_desc = sage_error.Description - error_detail = f"{sage_error_desc} (Code: {sage_error.Number})" + error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" logger.error(f"❌ Erreur Sage: {error_detail}") except: pass - # Dump COMPLET des champs pour debug - logger.error("❌ ÉCHEC Write() - DUMP COMPLET:") + # Analyser l'erreur spécifique + 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 (attr.startswith("CT_") or - 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}") + if "doublon" in error_detail.lower() or "existe" in error_detail.lower(): + raise ValueError(f"Ce client existe déjà : {error_detail}") raise RuntimeError(f"Échec Write(): {error_detail}") # ======================================== - # ÉTAPE 6 : RELECTURE & FINALISATION + # ÉTAPE 7 : RELECTURE & FINALISATION # ======================================== try: client.Read() @@ -2613,19 +2705,10 @@ class SageConnector: if not num_final: 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} ✅✅✅") # ======================================== - # ÉTAPE 7 : REFRESH CACHE + # ÉTAPE 8 : REFRESH CACHE # ======================================== self._refresh_cache_clients() @@ -2633,7 +2716,7 @@ class SageConnector: "numero": num_final, "intitule": intitule, "compte_collectif": compte, - "type": 0, + "type": 0, # Par défaut client "adresse": adresse or None, "code_postal": code_postal or None, "ville": ville or None, @@ -2645,7 +2728,6 @@ class SageConnector: } except ValueError as e: - # Erreur métier (doublon, validation) logger.error(f"❌ Erreur métier: {e}") raise @@ -2661,4 +2743,5 @@ class SageConnector: except: pass - raise RuntimeError(f"Erreur technique Sage: {error_message}") \ No newline at end of file + raise RuntimeError(f"Erreur technique Sage: {error_message}") + \ No newline at end of file