diff --git a/main.py b/main.py index 6960877..f061cbb 100644 --- a/main.py +++ b/main.py @@ -343,7 +343,7 @@ class FamilleCreate(BaseModel): def verify_token(x_sage_token: str = Header(...)): """Vérification du token d'authentification""" if x_sage_token != settings.sage_gateway_token: - logger.warning(f"❌ Token invalide reçu: {x_sage_token[:20]}...") + logger.warning(f" Token invalide reçu: {x_sage_token[:20]}...") raise HTTPException(401, "Token invalide") return True @@ -380,9 +380,9 @@ def startup(): # Validation config try: validate_settings() - logger.info("✅ Configuration validée") + logger.info(" Configuration validée") except ValueError as e: - logger.error(f"❌ Configuration invalide: {e}") + logger.error(f" Configuration invalide: {e}") raise # Connexion Sage @@ -391,9 +391,9 @@ def startup(): ) if not sage.connecter(): - raise RuntimeError("❌ Impossible de se connecter à Sage 100c") + raise RuntimeError(" Impossible de se connecter à Sage 100c") - logger.info("✅ Sage Gateway démarré et connecté") + logger.info(" Sage Gateway démarré et connecté") @app.on_event("shutdown") @@ -519,7 +519,6 @@ def creer_devis(req: DevisRequest): "client": {"code": req.client_id, "intitule": ""}, "date_devis": req.date_devis or date.today(), "date_livraison": req.date_livraison or date.today(), - "date_expedition": req.date_expedition or date.today(), "reference": req.reference, "lignes": req.lignes, } @@ -534,7 +533,7 @@ def creer_devis(req: DevisRequest): @app.post("/sage/devis/get", dependencies=[Depends(verify_token)]) def lire_devis(req: CodeRequest): try: - # ✅ Lecture complète depuis Sage (avec lignes) + # Lecture complète depuis Sage (avec lignes) devis = sage.lire_devis(req.code) if not devis: raise HTTPException(404, f"Devis {req.code} non trouvé") @@ -553,7 +552,7 @@ def devis_list( filtre: str = Query("", description="Filtre texte (numero, client)"), ): try: - # ✅ Récupération depuis le cache (instantané) + # Récupération depuis le cache (instantané) devis_list = sage.lister_tous_devis_cache(filtre) # Filtrer par statut si demandé @@ -563,12 +562,12 @@ def devis_list( # Limiter le nombre de résultats devis_list = devis_list[:limit] - logger.info(f"✅ {len(devis_list)} devis retournés depuis le cache") + logger.info(f" {len(devis_list)} devis retournés depuis le cache") return {"success": True, "data": devis_list} except Exception as e: - logger.error(f"❌ Erreur liste devis: {e}", exc_info=True) + logger.error(f" Erreur liste devis: {e}", exc_info=True) raise HTTPException(500, str(e)) @@ -590,7 +589,7 @@ def changer_statut_devis_endpoint(numero: str, nouveau_statut: int): doc.DO_Statut = nouveau_statut doc.Write() - logger.info(f"✅ Statut devis {numero}: {statut_actuel} → {nouveau_statut}") + logger.info(f" Statut devis {numero}: {statut_actuel} → {nouveau_statut}") return { "success": True, @@ -637,7 +636,7 @@ def transformer_document( f"(type {type_source}) → type {type_cible}" ) - # ✅ Matrice des transformations valides pour VOTRE Sage + # Matrice des transformations valides pour VOTRE Sage transformations_valides = { (0, 10), # Devis → Commande (10, 30), # Commande → Bon de livraison @@ -648,7 +647,7 @@ def transformer_document( if (type_source, type_cible) not in transformations_valides: logger.error( - f"❌ Transformation non autorisée: {type_source} → {type_cible}" + f" Transformation non autorisée: {type_source} → {type_cible}" ) raise HTTPException( 400, @@ -660,7 +659,7 @@ def transformer_document( resultat = sage.transformer_document(numero_source, type_source, type_cible) logger.info( - f"✅ Transformation réussie: {numero_source} → " + f" Transformation réussie: {numero_source} → " f"{resultat.get('document_cible', '?')} " f"({resultat.get('nb_lignes', 0)} lignes)" ) @@ -670,10 +669,10 @@ def transformer_document( except HTTPException: raise except ValueError as e: - logger.error(f"❌ Erreur métier transformation: {e}") + logger.error(f" Erreur métier transformation: {e}") raise HTTPException(400, str(e)) except Exception as e: - logger.error(f"❌ Erreur technique transformation: {e}", exc_info=True) + logger.error(f" Erreur technique transformation: {e}", exc_info=True) raise HTTPException(500, f"Erreur transformation: {str(e)}") @@ -729,7 +728,7 @@ def commandes_list( return {"success": True, "data": commandes} except Exception as e: - logger.error(f"❌ Erreur liste commandes: {e}", exc_info=True) + logger.error(f" Erreur liste commandes: {e}", exc_info=True) raise HTTPException(500, str(e)) @@ -747,12 +746,12 @@ def factures_list( factures = factures[:limit] - logger.info(f"✅ {len(factures)} factures retournées depuis le cache") + logger.info(f" {len(factures)} factures retournées depuis le cache") return {"success": True, "data": factures} except Exception as e: - logger.error(f"❌ Erreur liste factures: {e}", exc_info=True) + logger.error(f" Erreur liste factures: {e}", exc_info=True) raise HTTPException(500, str(e)) @@ -772,7 +771,7 @@ def lire_remise_max_client(code: str): except: pass - logger.info(f"✅ Remise max client {code}: {remise_max}%") + logger.info(f" Remise max client {code}: {remise_max}%") return { "success": True, @@ -847,15 +846,15 @@ def prospect_get(req: CodeRequest): @app.post("/sage/fournisseurs/list", dependencies=[Depends(verify_token)]) def fournisseurs_list(req: FiltreRequest): try: - # ✅ Utiliser le cache au lieu de la lecture directe + # Utiliser le cache au lieu de la lecture directe fournisseurs = sage.lister_tous_fournisseurs_cache(req.filtre) - logger.info(f"✅ {len(fournisseurs)} fournisseurs retournés depuis le cache") + logger.info(f" {len(fournisseurs)} fournisseurs retournés depuis le cache") return {"success": True, "data": fournisseurs} except Exception as e: - logger.error(f"❌ Erreur liste fournisseurs: {e}", exc_info=True) + logger.error(f" Erreur liste fournisseurs: {e}", exc_info=True) raise HTTPException(500, str(e)) @@ -865,7 +864,7 @@ def create_fournisseur_endpoint(req: FournisseurCreateRequest): # Appel au connecteur Sage resultat = sage.creer_fournisseur(req.dict()) - logger.info(f"✅ Fournisseur créé: {resultat.get('numero')}") + logger.info(f" Fournisseur créé: {resultat.get('numero')}") return {"success": True, "data": resultat} @@ -876,7 +875,7 @@ def create_fournisseur_endpoint(req: FournisseurCreateRequest): except Exception as e: # Erreur technique (ex: COM) - logger.error(f"❌ Erreur technique création fournisseur: {e}") + logger.error(f" Erreur technique création fournisseur: {e}") raise HTTPException(500, str(e)) @@ -898,7 +897,7 @@ def modifier_fournisseur_endpoint(req: FournisseurUpdateGatewayRequest): @app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)]) def fournisseur_get(req: CodeRequest): """ - ✅ NOUVEAU : Lecture d'un fournisseur par code + NOUVEAU : Lecture d'un fournisseur par code """ try: fournisseur = sage.lire_fournisseur(req.code) @@ -922,7 +921,7 @@ def avoirs_list( filtre: str = Query("", description="Filtre texte"), ): try: - # ✅ Récupération depuis le cache (instantané) + # Récupération depuis le cache (instantané) avoirs = sage.lister_tous_avoirs_cache(filtre) # Filtrer par statut si demandé @@ -932,26 +931,26 @@ def avoirs_list( # Limiter le nombre de résultats avoirs = avoirs[:limit] - logger.info(f"✅ {len(avoirs)} avoirs retournés depuis le cache") + logger.info(f" {len(avoirs)} avoirs retournés depuis le cache") return {"success": True, "data": avoirs} except Exception as e: - logger.error(f"❌ Erreur liste avoirs: {e}", exc_info=True) + logger.error(f" Erreur liste avoirs: {e}", exc_info=True) raise HTTPException(500, str(e)) @app.post("/sage/avoirs/get", dependencies=[Depends(verify_token)]) def avoir_get(req: CodeRequest): try: - # ✅ Essayer le cache d'abord + # Essayer le cache d'abord avoir = sage.lire_avoir_cache(req.code) if avoir: - logger.info(f"✅ Avoir {req.code} retourné depuis le cache") + logger.info(f" Avoir {req.code} retourné depuis le cache") return {"success": True, "data": avoir, "source": "cache"} - # ❌ Pas dans le cache → Lecture directe depuis Sage + # Pas dans le cache → Lecture directe depuis Sage logger.info(f"⚠️ Avoir {req.code} absent du cache, lecture depuis Sage...") avoir = sage.lire_avoir(req.code) @@ -977,7 +976,7 @@ def livraisons_list( filtre: str = Query("", description="Filtre texte"), ): try: - # ✅ Récupération depuis le cache (instantané) + # Récupération depuis le cache (instantané) livraisons = sage.lister_toutes_livraisons_cache(filtre) # Filtrer par statut si demandé @@ -987,26 +986,26 @@ def livraisons_list( # Limiter le nombre de résultats livraisons = livraisons[:limit] - logger.info(f"✅ {len(livraisons)} livraisons retournées depuis le cache") + logger.info(f" {len(livraisons)} livraisons retournées depuis le cache") return {"success": True, "data": livraisons} except Exception as e: - logger.error(f"❌ Erreur liste livraisons: {e}", exc_info=True) + logger.error(f" Erreur liste livraisons: {e}", exc_info=True) raise HTTPException(500, str(e)) @app.post("/sage/livraisons/get", dependencies=[Depends(verify_token)]) def livraison_get(req: CodeRequest): try: - # ✅ Essayer le cache d'abord + # Essayer le cache d'abord livraison = sage.lire_livraison_cache(req.code) if livraison: - logger.info(f"✅ Livraison {req.code} retournée depuis le cache") + logger.info(f" Livraison {req.code} retournée depuis le cache") return {"success": True, "data": livraison, "source": "cache"} - # ❌ Pas dans le cache → Lecture directe depuis Sage + # Pas dans le cache → Lecture directe depuis Sage logger.info(f"⚠️ Livraison {req.code} absente du cache, lecture depuis Sage...") livraison = sage.lire_livraison(req.code) @@ -1049,7 +1048,6 @@ def creer_commande_endpoint(req: CommandeCreateRequest): "client": {"code": req.client_id, "intitule": ""}, "date_commande": req.date_commande or date.today(), "date_livraison": req.date_livraison or date.today(), - "date_expedition": req.date_expedition or date.today(), "reference": req.reference, "lignes": req.lignes, } @@ -1092,7 +1090,6 @@ def creer_livraison_endpoint(req: LivraisonCreateGatewayRequest): "client": {"code": req.client_id, "intitule": ""}, "date_livraison": req.date_livraison or date.today(), "date_livraison_prevue": req.date_livraison or date.today(), - "date_expedition": req.date_expedition or date.today(), "reference": req.reference, "lignes": req.lignes, } @@ -1135,7 +1132,6 @@ def creer_avoir_endpoint(req: AvoirCreateGatewayRequest): "client": {"code": req.client_id, "intitule": ""}, "date_avoir": req.date_avoir or date.today(), "date_livraison": req.date_livraison or date.today(), - "date_expedition": req.date_expedition or date.today(), "reference": req.reference, "lignes": req.lignes, } @@ -1181,7 +1177,6 @@ def creer_facture_endpoint(req: FactureCreateGatewayRequest): "client": {"code": req.client_id, "intitule": ""}, "date_facture": req.date_facture or date.today(), "date_livraison": req.date_livraison or date.today(), - "date_expedition": req.date_expedition or date.today(), "reference": req.reference, "lignes": req.lignes, } @@ -1368,7 +1363,7 @@ async def stats_familles(): @app.post("/sage/documents/generate-pdf", dependencies=[Depends(verify_token)]) def generer_pdf_document(req: PDFGenerationRequest): try: - logger.info(f"📄 Génération PDF: {req.doc_id} (type={req.type_doc})") + logger.info(f" Génération PDF: {req.doc_id} (type={req.type_doc})") # Appel au connecteur Sage pdf_bytes = sage.generer_pdf_document(req.doc_id, req.type_doc) @@ -1381,7 +1376,7 @@ def generer_pdf_document(req: PDFGenerationRequest): pdf_base64 = base64.b64encode(pdf_bytes).decode("utf-8") - logger.info(f"✅ PDF généré: {len(pdf_bytes)} octets") + logger.info(f" PDF généré: {len(pdf_bytes)} octets") return { "success": True, @@ -1396,7 +1391,7 @@ def generer_pdf_document(req: PDFGenerationRequest): except HTTPException: raise except Exception as e: - logger.error(f"❌ Erreur génération PDF: {e}", exc_info=True) + logger.error(f" Erreur génération PDF: {e}", exc_info=True) raise HTTPException(500, str(e)) @@ -1424,7 +1419,7 @@ def lister_depots(): depot = win32com.client.CastTo(persist, "IBODepot3") depot.Read() - # ✅ Lire les attributs identifiés + # Lire les attributs identifiés code = "" numero = 0 intitule = "" @@ -1500,7 +1495,7 @@ def lister_depots(): depots.append(depot_info) logger.info( - f" ✅ Dépôt {index}: code='{code}', compteur={numero}, intitulé='{intitule}'" + f" Dépôt {index}: code='{code}', compteur={numero}, intitulé='{intitule}'" ) index += 1 @@ -1514,11 +1509,11 @@ def lister_depots(): ) break else: - logger.error(f"❌ Erreur inattendue index {index}: {e}") + logger.error(f" Erreur inattendue index {index}: {e}") index += 1 continue - logger.info(f"✅ {len(depots)} dépôt(s) trouvé(s)") + logger.info(f" {len(depots)} dépôt(s) trouvé(s)") if not depots: return { @@ -1540,13 +1535,13 @@ def lister_depots(): } except Exception as e: - logger.error(f"❌ Erreur lecture dépôts: {e}", exc_info=True) + logger.error(f" Erreur lecture dépôts: {e}", exc_info=True) raise HTTPException(500, f"Erreur lecture dépôts: {str(e)}") except HTTPException: raise except Exception as e: - logger.error(f"❌ Erreur: {e}", exc_info=True) + logger.error(f" Erreur: {e}", exc_info=True) raise HTTPException(500, str(e)) @@ -1569,7 +1564,7 @@ def creer_entree_stock(req: EntreeStockRequest): # Appel au connecteur resultat = sage.creer_entree_stock(entree_data) - logger.info(f"✅ [ENTREE STOCK] Créé : {resultat.get('numero')}") + logger.info(f" [ENTREE STOCK] Créé : {resultat.get('numero')}") return {"success": True, "data": resultat} @@ -1578,7 +1573,7 @@ def creer_entree_stock(req: EntreeStockRequest): raise HTTPException(400, str(e)) except Exception as e: - logger.error(f"❌ Erreur technique entrée stock : {e}", exc_info=True) + logger.error(f" Erreur technique entrée stock : {e}", exc_info=True) raise HTTPException(500, str(e)) @@ -1601,7 +1596,7 @@ def creer_sortie_stock(req: SortieStockRequest): # Appel au connecteur resultat = sage.creer_sortie_stock(sortie_data) - logger.info(f"✅ [SORTIE STOCK] Créé : {resultat.get('numero')}") + logger.info(f" [SORTIE STOCK] Créé : {resultat.get('numero')}") return {"success": True, "data": resultat} @@ -1610,7 +1605,7 @@ def creer_sortie_stock(req: SortieStockRequest): raise HTTPException(400, str(e)) except Exception as e: - logger.error(f"❌ Erreur technique sortie stock : {e}", exc_info=True) + logger.error(f" Erreur technique sortie stock : {e}", exc_info=True) raise HTTPException(500, str(e)) @@ -1627,7 +1622,7 @@ def lire_mouvement_stock(numero: str): except HTTPException: raise except Exception as e: - logger.error(f"❌ Erreur lecture mouvement : {e}") + logger.error(f" Erreur lecture mouvement : {e}") raise HTTPException(500, str(e)) @@ -1639,7 +1634,7 @@ def lister_modeles_disponibles(): return {"success": True, "data": modeles} except Exception as e: - logger.error(f"❌ Erreur listage modèles: {e}") + logger.error(f" Erreur listage modèles: {e}") raise HTTPException(500, str(e)) @@ -1651,12 +1646,12 @@ def generer_pdf_document( base64_encode: bool = Query(True, description="Retourner en base64"), ): """ - 📄 Génère un PDF d'un document Sage avec le modèle spécifié + Génère un PDF d'un document Sage avec le modèle spécifié """ try: - # ✅ LOG pour debug + # LOG pour debug logger.info( - f"📄 PDF Request: numero={numero}, type={type_doc}, modele={modele}, base64={base64_encode}" + f" PDF Request: numero={numero}, type={type_doc}, modele={modele}, base64={base64_encode}" ) # Générer le PDF @@ -1667,8 +1662,8 @@ def generer_pdf_document( if not pdf_bytes: raise HTTPException(404, f"Impossible de générer le PDF pour {numero}") - # ✅ LOG taille PDF - logger.info(f"✅ PDF généré: {len(pdf_bytes)} octets") + # LOG taille PDF + logger.info(f" PDF généré: {len(pdf_bytes)} octets") if base64_encode: # Retour en JSON avec base64 @@ -1696,20 +1691,37 @@ def generer_pdf_document( media_type="application/pdf", headers={ "Content-Disposition": f'inline; filename="{numero}.pdf"', - "Content-Length": str(len(pdf_bytes)), # ✅ Taille explicite + "Content-Length": str(len(pdf_bytes)), # Taille explicite }, ) except HTTPException: raise except ValueError as e: - logger.error(f"❌ Erreur métier: {e}") + logger.error(f" Erreur métier: {e}") raise HTTPException(400, str(e)) except Exception as e: - logger.error(f"❌ Erreur technique: {e}", exc_info=True) + logger.error(f" Erreur technique: {e}", exc_info=True) raise HTTPException(500, str(e)) +@app.get("/sage/test-API-transformation") +def generer_pdf_transformation_native(numero: str, type_doc: int): + try: + expliration = sage.generer_pdf_transformation_native(numero, type_doc) + + if not expliration: + raise HTTPException(404, f"ERROR") + + return {"success": True, "data": expliration} + + except HTTPException: + raise + except Exception as e: + logger.error(f" Erreur exploration : {e}") + raise HTTPException(500, str(e)) + + # ===================================================== # LANCEMENT # ===================================================== diff --git a/sage_connector.py b/sage_connector.py index 3673ab2..ee9df35 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -9,6 +9,8 @@ from config import settings, validate_settings import pyodbc from contextlib import contextmanager import pywintypes +import os +import glob logger = logging.getLogger(__name__) @@ -34,7 +36,7 @@ class SageConnector: # Thread-local storage pour COM self._thread_local = threading.local() - + # ========================================================================= # GESTION COM THREAD-SAFE # ========================================================================= @@ -67,7 +69,7 @@ class SageConnector: conn = pyodbc.connect(self.sql_conn_string, timeout=10) yield conn except pyodbc.Error as e: - logger.error(f"❌ Erreur SQL: {e}") + logger.error(f" Erreur SQL: {e}") raise RuntimeError(f"Erreur SQL: {str(e)}") finally: if conn: @@ -112,7 +114,7 @@ class SageConnector: self.cial.Loggable.UserPwd = self.mot_de_passe self.cial.Open() - logger.info(f"✅ Connexion COM Sage réussie: {self.chemin_base}") + logger.info(f" Connexion COM Sage réussie: {self.chemin_base}") # ======================================== # TEST CONNEXION SQL (pour lectures) @@ -122,15 +124,15 @@ class SageConnector: cursor = conn.cursor() cursor.execute("SELECT COUNT(*) FROM F_COMPTET") nb_tiers = cursor.fetchone()[0] - logger.info(f"✅ Connexion SQL réussie: {nb_tiers} tiers détectés") + logger.info(f" Connexion SQL réussie: {nb_tiers} tiers détectés") except Exception as e: - logger.warning(f"⚠️ SQL non disponible: {e}") + logger.warning(f"SQL non disponible: {e}") logger.warning(" Les lectures utiliseront COM (plus lent)") return True except Exception as e: - logger.error(f"❌ Erreur connexion Sage: {e}", exc_info=True) + logger.error(f" Erreur connexion Sage: {e}", exc_info=True) return False def deconnecter(self): @@ -191,11 +193,11 @@ class SageConnector: } ) - logger.info(f"✅ SQL: {len(fournisseurs)} fournisseurs") + logger.info(f" SQL: {len(fournisseurs)} fournisseurs") return fournisseurs except Exception as e: - logger.error(f"❌ Erreur SQL fournisseurs: {e}") + logger.error(f" Erreur SQL fournisseurs: {e}") return [] def lire_fournisseur(self, code): @@ -245,7 +247,7 @@ class SageConnector: } except Exception as e: - logger.error(f"❌ Erreur SQL fournisseur {code}: {e}") + logger.error(f" Erreur SQL fournisseur {code}: {e}") return None def creer_fournisseur(self, fournisseur_data: Dict) -> Dict: @@ -257,7 +259,7 @@ class SageConnector: # ======================================== # ÉTAPE 0 : VALIDATION & NETTOYAGE # ======================================== - logger.info("🔍 === VALIDATION DONNÉES FOURNISSEUR ===") + logger.info(" === VALIDATION DONNÉES FOURNISSEUR ===") if not fournisseur_data.get("intitule"): raise ValueError("Le champ 'intitule' est obligatoire") @@ -300,32 +302,32 @@ class SageConnector: # 🔑 CRITIQUE : Initialiser l'objet fournisseur.SetDefault() - logger.info("✅ Objet fournisseur créé et initialisé") + logger.info(" Objet fournisseur créé et initialisé") # ======================================== # ÉTAPE 2 : CHAMPS OBLIGATOIRES # ======================================== - logger.info("📝 Définition des champs obligatoires...") + logger.info(" Définition des champs obligatoires...") # 1. Intitulé (OBLIGATOIRE) fournisseur.CT_Intitule = intitule - logger.debug(f" ✅ CT_Intitule: '{intitule}'") + logger.debug(f" CT_Intitule: '{intitule}'") # 2. Type = Fournisseur (1) - # ⚠️ NOTE: Sur certaines versions Sage, CT_Type n'existe pas + # NOTE: Sur certaines versions Sage, CT_Type n'existe pas # et le type est automatiquement défini par la factory utilisée try: fournisseur.CT_Type = 1 # 1 = Fournisseur - logger.debug(" ✅ CT_Type: 1 (Fournisseur)") + logger.debug(" CT_Type: 1 (Fournisseur)") except: - logger.debug(" ⚠️ CT_Type non défini (géré par FactoryFournisseur)") + logger.debug(" CT_Type non défini (géré par FactoryFournisseur)") # 3. Qualité (pour versions récentes Sage) try: fournisseur.CT_Qualite = "FOU" - logger.debug(" ✅ CT_Qualite: 'FOU'") + logger.debug(" CT_Qualite: 'FOU'") except: - logger.debug(" ⚠️ CT_Qualite non défini (pas critique)") + logger.debug(" CT_Qualite non défini (pas critique)") # 4. Compte général principal (OBLIGATOIRE) try: @@ -340,18 +342,18 @@ class SageConnector: # Assigner l'objet CompteG fournisseur.CompteGPrinc = compte_obj - logger.debug(f" ✅ CompteGPrinc: objet '{compte}' assigné") + logger.debug(f" CompteGPrinc: objet '{compte}' assigné") else: logger.warning( - f" ⚠️ Compte {compte} introuvable - utilisation défaut" + f" Compte {compte} introuvable - utilisation défaut" ) except Exception as e: - logger.warning(f" ⚠️ Erreur CompteGPrinc: {e}") + logger.warning(f" Erreur CompteGPrinc: {e}") # 5. Numéro fournisseur (OBLIGATOIRE - générer si vide) if num_prop: fournisseur.CT_Num = num_prop - logger.debug(f" ✅ CT_Num fourni: '{num_prop}'") + logger.debug(f" CT_Num fourni: '{num_prop}'") else: # 🔑 CRITIQUE : Générer le numéro automatiquement try: @@ -359,14 +361,14 @@ class SageConnector: if hasattr(fournisseur, "SetDefaultNumPiece"): fournisseur.SetDefaultNumPiece() num_genere = getattr(fournisseur, "CT_Num", "") - logger.debug(f" ✅ CT_Num auto-généré: '{num_genere}'") + logger.debug(f" CT_Num auto-généré: '{num_genere}'") else: # Méthode 2 : GetNextNumero depuis la factory num_genere = factory_fournisseur.GetNextNumero() if num_genere: fournisseur.CT_Num = num_genere logger.debug( - f" ✅ CT_Num auto (GetNextNumero): '{num_genere}'" + f" CT_Num auto (GetNextNumero): '{num_genere}'" ) else: # Méthode 3 : Fallback - timestamp @@ -374,9 +376,9 @@ class SageConnector: num_genere = f"FOUR{int(time.time()) % 1000000}" fournisseur.CT_Num = num_genere - logger.warning(f" ⚠️ CT_Num fallback: '{num_genere}'") + logger.warning(f" CT_Num fallback: '{num_genere}'") except Exception as e: - logger.error(f" ❌ Impossible de générer CT_Num: {e}") + logger.error(f" Impossible de générer CT_Num: {e}") raise ValueError( "Impossible de générer le numéro fournisseur automatiquement" ) @@ -389,14 +391,14 @@ class SageConnector: fournisseur.N_CatCompta = 1 if hasattr(fournisseur, "N_Period"): fournisseur.N_Period = 1 - logger.debug(" ✅ Catégories (N_*) initialisées") + logger.debug(" Catégories (N_*) initialisées") except Exception as e: - logger.warning(f" ⚠️ Catégories: {e}") + logger.warning(f" Catégories: {e}") # ======================================== # ÉTAPE 3 : CHAMPS OPTIONNELS # ======================================== - logger.info("📝 Définition champs optionnels...") + logger.info(" Définition champs optionnels...") # Adresse (objet IAdresse) if any([adresse, code_postal, ville, pays]): @@ -412,9 +414,9 @@ class SageConnector: if pays: adresse_obj.Pays = pays - logger.debug(" ✅ Adresse définie") + logger.debug(" Adresse définie") except Exception as e: - logger.warning(f" ⚠️ Adresse: {e}") + logger.warning(f" Adresse: {e}") # Télécom (objet ITelecom) if telephone or email: @@ -426,24 +428,24 @@ class SageConnector: if email: telecom_obj.EMail = email - logger.debug(" ✅ Télécom défini") + logger.debug(" Télécom défini") except Exception as e: - logger.warning(f" ⚠️ Télécom: {e}") + logger.warning(f" Télécom: {e}") # Identifiants fiscaux if siret: try: fournisseur.CT_Siret = siret - logger.debug(f" ✅ SIRET: '{siret}'") + logger.debug(f" SIRET: '{siret}'") except Exception as e: - logger.warning(f" ⚠️ SIRET: {e}") + logger.warning(f" SIRET: {e}") if tva_intra: try: fournisseur.CT_Identifiant = tva_intra - logger.debug(f" ✅ TVA intra: '{tva_intra}'") + logger.debug(f" TVA intra: '{tva_intra}'") except Exception as e: - logger.warning(f" ⚠️ TVA: {e}") + logger.warning(f" TVA: {e}") # Options par défaut try: @@ -451,30 +453,30 @@ class SageConnector: fournisseur.CT_Lettrage = True if hasattr(fournisseur, "CT_Sommeil"): fournisseur.CT_Sommeil = False - logger.debug(" ✅ Options par défaut définies") + logger.debug(" Options par défaut définies") except Exception as e: - logger.debug(f" ⚠️ Options: {e}") + logger.debug(f" Options: {e}") # ======================================== # ÉTAPE 4 : VÉRIFICATION PRÉ-WRITE # ======================================== - logger.info("🔍 === DIAGNOSTIC PRÉ-WRITE ===") + logger.info(" === DIAGNOSTIC PRÉ-WRITE ===") num_avant_write = getattr(fournisseur, "CT_Num", "") if not num_avant_write: - logger.error("❌ CRITIQUE: CT_Num toujours vide !") + logger.error(" CRITIQUE: CT_Num toujours vide !") raise ValueError("Le numéro fournisseur (CT_Num) est obligatoire") - logger.info(f"✅ CT_Num confirmé: '{num_avant_write}'") + logger.info(f" CT_Num confirmé: '{num_avant_write}'") # ======================================== # ÉTAPE 5 : ÉCRITURE EN BASE # ======================================== - logger.info("💾 Écriture du fournisseur dans Sage...") + logger.info(" Écriture du fournisseur dans Sage...") try: fournisseur.Write() - logger.info("✅ Write() réussi !") + logger.info(" Write() réussi !") except Exception as e: error_detail = str(e) @@ -486,7 +488,7 @@ class SageConnector: error_detail = ( f"{sage_error.Description} (Code: {sage_error.Number})" ) - logger.error(f"❌ Erreur Sage: {error_detail}") + logger.error(f" Erreur Sage: {error_detail}") except: pass @@ -505,14 +507,14 @@ class SageConnector: try: fournisseur.Read() except Exception as e: - logger.warning(f"⚠️ Impossible de relire: {e}") + logger.warning(f"Impossible de relire: {e}") num_final = getattr(fournisseur, "CT_Num", "") if not num_final: raise RuntimeError("CT_Num vide après Write()") - logger.info(f"✅✅✅ FOURNISSEUR CRÉÉ: {num_final} - {intitule} ✅✅✅") + logger.info(f" FOURNISSEUR CRÉÉ: {num_final} - {intitule} ") # ======================================== # ÉTAPE 7 : CONSTRUCTION RÉPONSE @@ -533,18 +535,18 @@ class SageConnector: "tva_intra": tva_intra or None, } - # ⚠️ PAS DE REFRESH CACHE ICI + # PAS DE REFRESH CACHE ICI # Car lister_tous_fournisseurs() utilise FactoryFournisseur.List() # qui lit directement depuis Sage (pas de cache) return resultat except ValueError as e: - logger.error(f"❌ Erreur métier: {e}") + logger.error(f" Erreur métier: {e}") raise except Exception as e: - logger.error(f"❌ Erreur création fournisseur: {e}", exc_info=True) + logger.error(f" Erreur création fournisseur: {e}", exc_info=True) error_message = str(e) if self.cial: @@ -566,7 +568,7 @@ class SageConnector: # ======================================== # ÉTAPE 1 : CHARGER LE FOURNISSEUR EXISTANT # ======================================== - logger.info(f"🔍 Recherche fournisseur {code}...") + logger.info(f" Recherche fournisseur {code}...") factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur persist = factory_fournisseur.ReadNumero(code) @@ -574,18 +576,18 @@ class SageConnector: if not persist: raise ValueError(f"Fournisseur {code} introuvable") - fournisseur = self._cast_client(persist) # ✅ Réutiliser _cast_client + fournisseur = self._cast_client(persist) # Réutiliser _cast_client if not fournisseur: raise ValueError(f"Impossible de charger le fournisseur {code}") logger.info( - f"✅ Fournisseur {code} trouvé: {getattr(fournisseur, 'CT_Intitule', '')}" + f" Fournisseur {code} trouvé: {getattr(fournisseur, 'CT_Intitule', '')}" ) # ======================================== # ÉTAPE 2 : METTRE À JOUR LES CHAMPS FOURNIS # ======================================== - logger.info("📝 Mise à jour des champs...") + logger.info(" Mise à jour des champs...") champs_modifies = [] @@ -624,7 +626,7 @@ class SageConnector: champs_modifies.append("pays") except Exception as e: - logger.warning(f"⚠️ Erreur mise à jour adresse: {e}") + logger.warning(f"Erreur mise à jour adresse: {e}") # Télécom if "email" in fournisseur_data or "telephone" in fournisseur_data: @@ -642,7 +644,7 @@ class SageConnector: champs_modifies.append("telephone") except Exception as e: - logger.warning(f"⚠️ Erreur mise à jour télécom: {e}") + logger.warning(f"Erreur mise à jour télécom: {e}") # SIRET if "siret" in fournisseur_data: @@ -651,7 +653,7 @@ class SageConnector: fournisseur.CT_Siret = siret champs_modifies.append("siret") except Exception as e: - logger.warning(f"⚠️ Erreur mise à jour SIRET: {e}") + logger.warning(f"Erreur mise à jour SIRET: {e}") # TVA Intracommunautaire if "tva_intra" in fournisseur_data: @@ -660,10 +662,10 @@ class SageConnector: fournisseur.CT_Identifiant = tva champs_modifies.append("tva_intra") except Exception as e: - logger.warning(f"⚠️ Erreur mise à jour TVA: {e}") + logger.warning(f"Erreur mise à jour TVA: {e}") if not champs_modifies: - logger.warning("⚠️ Aucun champ à modifier") + logger.warning("Aucun champ à modifier") # Retourner les données actuelles via extraction directe return { "numero": getattr(fournisseur, "CT_Num", "").strip(), @@ -672,16 +674,16 @@ class SageConnector: "est_fournisseur": True, } - logger.info(f"📝 Champs à modifier: {', '.join(champs_modifies)}") + logger.info(f" Champs à modifier: {', '.join(champs_modifies)}") # ======================================== # ÉTAPE 3 : ÉCRIRE LES MODIFICATIONS # ======================================== - logger.info("💾 Écriture des modifications...") + logger.info(" Écriture des modifications...") try: fournisseur.Write() - logger.info("✅ Write() réussi !") + logger.info(" Write() réussi !") except Exception as e: error_detail = str(e) @@ -695,7 +697,7 @@ class SageConnector: except: pass - logger.error(f"❌ Erreur Write(): {error_detail}") + logger.error(f" Erreur Write(): {error_detail}") raise RuntimeError(f"Échec modification: {error_detail}") # ======================================== @@ -704,7 +706,7 @@ class SageConnector: fournisseur.Read() logger.info( - f"✅✅✅ FOURNISSEUR MODIFIÉ: {code} ({len(champs_modifies)} champs) ✅✅✅" + f" FOURNISSEUR MODIFIÉ: {code} ({len(champs_modifies)} champs) " ) # Extraction directe (comme lire_fournisseur) @@ -747,11 +749,11 @@ class SageConnector: return data except ValueError as e: - logger.error(f"❌ Erreur métier: {e}") + logger.error(f" Erreur métier: {e}") raise except Exception as e: - logger.error(f"❌ Erreur modification fournisseur: {e}", exc_info=True) + logger.error(f" Erreur modification fournisseur: {e}", exc_info=True) error_message = str(e) if self.cial: @@ -812,11 +814,11 @@ class SageConnector: } ) - logger.info(f"✅ SQL: {len(clients)} clients") + logger.info(f" SQL: {len(clients)} clients") return clients except Exception as e: - logger.error(f"❌ Erreur SQL clients: {e}") + logger.error(f" Erreur SQL clients: {e}") raise RuntimeError(f"Erreur lecture clients: {str(e)}") def lire_client(self, code_client): @@ -824,7 +826,7 @@ class SageConnector: with self._get_sql_connection() as conn: cursor = conn.cursor() - # ✅ MÊME REQUÊTE que lister_tous_clients (colonnes existantes uniquement) + # MÊME REQUÊTE que lister_tous_clients (colonnes existantes uniquement) cursor.execute( """ SELECT @@ -862,7 +864,7 @@ class SageConnector: } except Exception as e: - logger.error(f"❌ Erreur SQL client {code_client}: {e}") + logger.error(f" Erreur SQL client {code_client}: {e}") return None def lister_tous_articles(self, filtre="", avec_stock=True): @@ -908,7 +910,7 @@ class SageConnector: "est_actif": (row[6] == 0), "code_ean": self._safe_strip(row[7]), "type_article": row[8] if row[8] is not None else 0, - # ✅ CORRECTION : Pas de AR_Stock dans ta base ! + # CORRECTION : Pas de AR_Stock dans ta base ! "stock_reel": 0.0, "stock_mini": 0.0, "stock_maxi": 0.0, @@ -924,7 +926,7 @@ class SageConnector: # ======================================== if avec_stock and articles: logger.info( - f"📦 Enrichissement stock depuis F_ARTSTOCK pour {len(articles)} articles..." + f" Enrichissement stock depuis F_ARTSTOCK pour {len(articles)} articles..." ) try: @@ -977,7 +979,7 @@ class SageConnector: } logger.info( - f"✅ Stocks chargés pour {len(stock_map)} articles depuis F_ARTSTOCK" + f" Stocks chargés pour {len(stock_map)} articles depuis F_ARTSTOCK" ) # Enrichir les articles @@ -999,17 +1001,17 @@ class SageConnector: except Exception as e: logger.error( - f"❌ Erreur lecture F_ARTSTOCK: {e}", exc_info=True + f" Erreur lecture F_ARTSTOCK: {e}", exc_info=True ) # Ne pas lever d'exception, retourner les articles sans stock logger.info( - f"✅ SQL: {len(articles)} articles (stock={'oui' if avec_stock else 'non'})" + f" SQL: {len(articles)} articles (stock={'oui' if avec_stock else 'non'})" ) return articles except Exception as e: - logger.error(f"❌ Erreur SQL articles: {e}", exc_info=True) + logger.error(f" Erreur SQL articles: {e}", exc_info=True) raise RuntimeError(f"Erreur lecture articles: {str(e)}") def lire_article(self, reference): @@ -1073,7 +1075,7 @@ class SageConnector: # ======================================== # ÉTAPE 2 : LIRE LE STOCK DEPUIS F_ARTSTOCK (OBLIGATOIRE) # ======================================== - logger.info(f"📦 Lecture stock depuis F_ARTSTOCK pour {reference}...") + logger.info(f" Lecture stock depuis F_ARTSTOCK pour {reference}...") try: cursor.execute( @@ -1094,7 +1096,7 @@ class SageConnector: stock_row = cursor.fetchone() if stock_row: - # ✅ STOCK DEPUIS F_ARTSTOCK + # STOCK DEPUIS F_ARTSTOCK article["stock_reel"] = ( float(stock_row[0]) if stock_row[0] else 0.0 ) @@ -1123,15 +1125,15 @@ class SageConnector: ) logger.info( - f"✅ Stock trouvé dans F_ARTSTOCK: {article['stock_reel']} unités" + f" Stock trouvé dans F_ARTSTOCK: {article['stock_reel']} unités" ) else: logger.info( - f"⚠️ Aucun stock trouvé dans F_ARTSTOCK pour {reference}" + f"Aucun stock trouvé dans F_ARTSTOCK pour {reference}" ) except Exception as e: - logger.error(f"❌ Erreur lecture F_ARTSTOCK pour {reference}: {e}") + logger.error(f" Erreur lecture F_ARTSTOCK pour {reference}: {e}") # ======================================== # ÉTAPE 3 : ENRICHIR AVEC LIBELLÉ FAMILLE @@ -1157,7 +1159,7 @@ class SageConnector: return article except Exception as e: - logger.error(f"❌ Erreur SQL article {reference}: {e}") + logger.error(f" Erreur SQL article {reference}: {e}") return None def _convertir_type_pour_sql(self, type_doc: int) -> int: @@ -1209,12 +1211,12 @@ class SageConnector: if not row: logger.warning( - f"[SQL READ] ❌ Document {numero} (type={type_doc}) INTROUVABLE dans F_DOCENTETE" + f"[SQL READ] Document {numero} (type={type_doc}) INTROUVABLE dans F_DOCENTETE" ) return None numero_piece = self._safe_strip(row[0]) - logger.info(f"[SQL READ] ✅ Document trouvé: {numero_piece}") + logger.info(f"[SQL READ] Document trouvé: {numero_piece}") # ======================================== # PAS DE FILTRE PAR PRÉFIXE ICI ! @@ -1494,7 +1496,7 @@ class SageConnector: return doc except Exception as e: - logger.error(f"❌ Erreur SQL lecture document {numero}: {e}", exc_info=True) + logger.error(f" Erreur SQL lecture document {numero}: {e}", exc_info=True) return None def _lister_documents_avec_lignes_sql( @@ -1547,7 +1549,7 @@ class SageConnector: cursor.execute(query, params) entetes = cursor.fetchall() - logger.info(f"[SQL LIST] 📊 {len(entetes)} documents SQL") + logger.info(f"[SQL LIST] {len(entetes)} documents SQL") documents = [] stats = { @@ -1563,7 +1565,7 @@ class SageConnector: for idx, entete in enumerate(entetes): numero = self._safe_strip(entete.DO_Piece) logger.info( - f"[SQL LIST] [{idx+1}/{len(entetes)}] 🔄 Traitement {numero}..." + f"[SQL LIST] [{idx+1}/{len(entetes)}] Traitement {numero}..." ) try: @@ -1586,12 +1588,12 @@ class SageConnector: ) if not est_vente: logger.info( - f"[SQL LIST] ❌ {numero} : exclu (préfixe achat)" + f"[SQL LIST] {numero} : exclu (préfixe achat)" ) stats["exclus_prefixe"] += 1 continue - logger.debug(f"[SQL LIST] ✅ {numero} : préfixe OK") + logger.debug(f"[SQL LIST] {numero} : préfixe OK") # ======================================== # ÉTAPE 2 : CONSTRUIRE DOCUMENT DE BASE @@ -1742,12 +1744,12 @@ class SageConnector: } logger.debug( - f"[SQL LIST] ✅ {numero} : document de base créé" + f"[SQL LIST] {numero} : document de base créé" ) except Exception as e: logger.error( - f"[SQL LIST] ❌ {numero} : ERREUR construction base: {e}", + f"[SQL LIST] {numero} : ERREUR construction base: {e}", exc_info=True, ) stats["erreur_construction"] += 1 @@ -1980,12 +1982,12 @@ class SageConnector: ) logger.debug( - f"[SQL LIST] ✅ {numero} : {doc['nb_lignes']} lignes chargées" + f"[SQL LIST] {numero} : {doc['nb_lignes']} lignes chargées" ) except Exception as e: logger.error( - f"[SQL LIST] ⚠️ {numero} : ERREUR lignes: {e}", + f"[SQL LIST] {numero} : ERREUR lignes: {e}", exc_info=True, ) stats["erreur_lignes"] += 1 @@ -1997,12 +1999,12 @@ class SageConnector: documents.append(doc) stats["succes"] += 1 logger.info( - f"[SQL LIST] ✅✅✅ {numero} : AJOUTÉ à la liste (total: {len(documents)})" + f"[SQL LIST] {numero} : AJOUTÉ à la liste (total: {len(documents)})" ) except Exception as e: logger.error( - f"[SQL LIST] ❌❌❌ {numero} : EXCEPTION GLOBALE - DOCUMENT EXCLU: {e}", + f"[SQL LIST] {numero} : EXCEPTION GLOBALE - DOCUMENT EXCLU: {e}", exc_info=True, ) continue @@ -2011,7 +2013,7 @@ class SageConnector: # RÉSUMÉ FINAL # ======================================== logger.info(f"[SQL LIST] ═══════════════════════════") - logger.info(f"[SQL LIST] 📊 STATISTIQUES FINALES:") + logger.info(f"[SQL LIST] STATISTIQUES FINALES:") logger.info(f"[SQL LIST] Total SQL: {stats['total']}") logger.info(f"[SQL LIST] Exclus préfixe: {stats['exclus_prefixe']}") logger.info( @@ -2022,14 +2024,14 @@ class SageConnector: f"[SQL LIST] Erreur transformations: {stats['erreur_transformations']}" ) logger.info(f"[SQL LIST] Erreur liaisons: {stats['erreur_liaisons']}") - logger.info(f"[SQL LIST] ✅ SUCCÈS: {stats['succes']}") - logger.info(f"[SQL LIST] 📦 Documents retournés: {len(documents)}") + logger.info(f"[SQL LIST] SUCCÈS: {stats['succes']}") + logger.info(f"[SQL LIST] Documents retournés: {len(documents)}") logger.info(f"[SQL LIST] ═══════════════════════════") return documents except Exception as e: - logger.error(f"❌ Erreur GLOBALE listage: {e}", exc_info=True) + logger.error(f" Erreur GLOBALE listage: {e}", exc_info=True) return [] def lister_tous_devis_cache(self, filtre=""): @@ -2078,7 +2080,7 @@ class SageConnector: obj.Read() return obj except Exception as e: - logger.debug(f"❌ _cast_client échoue: {e}") # ✅ AJOUTER CE LOG + logger.debug(f" _cast_client échoue: {e}") # AJOUTER CE LOG return None def _cast_article(self, persist_obj): @@ -2095,18 +2097,18 @@ class SageConnector: try: numero = getattr(client_obj, "CT_Num", "").strip() if not numero: - logger.debug("⚠️ Objet sans CT_Num, skip") + logger.debug("Objet sans CT_Num, skip") return None except Exception as e: - logger.debug(f"❌ Erreur lecture CT_Num: {e}") + logger.debug(f" Erreur lecture CT_Num: {e}") return None try: intitule = getattr(client_obj, "CT_Intitule", "").strip() if not intitule: - logger.debug(f"⚠️ {numero} sans CT_Intitule") + logger.debug(f"{numero} sans CT_Intitule") except Exception as e: - logger.debug(f"⚠️ Erreur CT_Intitule sur {numero}: {e}") + logger.debug(f"Erreur CT_Intitule sur {numero}: {e}") intitule = "" # === 2. CONSTRUCTION OBJET DE BASE === @@ -2252,7 +2254,7 @@ class SageConnector: data["region"] = "" data["pays"] = "" except Exception as e: - logger.debug(f"⚠️ Erreur adresse sur {numero}: {e}") + logger.debug(f"Erreur adresse sur {numero}: {e}") data["adresse"] = "" data["complement"] = "" data["code_postal"] = "" @@ -2305,7 +2307,7 @@ class SageConnector: data["email"] = "" data["site_web"] = "" except Exception as e: - logger.debug(f"⚠️ Erreur telecom sur {numero}: {e}") + logger.debug(f"Erreur telecom sur {numero}: {e}") data["telephone"] = "" data["portable"] = "" data["telecopie"] = "" @@ -2425,7 +2427,7 @@ class SageConnector: return data 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) return None def _extraire_article(self, article_obj): @@ -2653,7 +2655,7 @@ class SageConnector: return data except Exception as e: - logger.error(f"❌ Erreur extraction article: {e}", exc_info=True) + logger.error(f" Erreur extraction article: {e}", exc_info=True) # Retourner structure minimale en cas d'erreur return { "reference": getattr(article_obj, "AR_Ref", "").strip(), @@ -2748,7 +2750,7 @@ class SageConnector: data["pays"] = "" data["adresse_complete"] = "" except Exception as e: - logger.debug(f"⚠️ Erreur adresse fournisseur {numero}: {e}") + logger.debug(f"Erreur adresse fournisseur {numero}: {e}") data["adresse"] = "" data["complement"] = "" data["code_postal"] = "" @@ -2783,7 +2785,7 @@ class SageConnector: data["email"] = "" data["site_web"] = "" except Exception as e: - logger.debug(f"⚠️ Erreur telecom fournisseur {numero}: {e}") + logger.debug(f"Erreur telecom fournisseur {numero}: {e}") data["telephone"] = "" data["portable"] = "" data["telecopie"] = "" @@ -2947,7 +2949,7 @@ class SageConnector: break except Exception as e: logger.debug( - f"⚠️ Erreur coordonnées bancaires fournisseur {numero}: {e}" + f"Erreur coordonnées bancaires fournisseur {numero}: {e}" ) # IBAN principal (premier de la liste) @@ -3006,7 +3008,7 @@ class SageConnector: except: break except Exception as e: - logger.debug(f"⚠️ Erreur contacts fournisseur {numero}: {e}") + logger.debug(f"Erreur contacts fournisseur {numero}: {e}") # Nombre de contacts data["nb_contacts"] = len(data["contacts"]) @@ -3052,7 +3054,7 @@ class SageConnector: return data except Exception as e: - logger.error(f"❌ Erreur extraction fournisseur: {e}", exc_info=True) + logger.error(f" Erreur extraction fournisseur: {e}", exc_info=True) # Retourner structure minimale en cas d'erreur return { "numero": getattr(fourn_obj, "CT_Num", "").strip(), @@ -3116,12 +3118,21 @@ class SageConnector: def creer_devis_enrichi(self, devis_data: dict, forcer_brouillon: bool = False): """ Crée un devis dans Sage avec support de la référence et des dates. + + Args: + devis_data: dict contenant: + - client: {code: str} + - date_devis: str ou date + - date_livraison: str ou date (optionnel) + - reference: str (optionnel) + - lignes: list[dict] + forcer_brouillon: bool, force le statut brouillon """ if not self.cial: raise RuntimeError("Connexion Sage non établie") logger.info( - f"🚀 Début création devis pour client {devis_data['client']['code']} " + f" Début création devis pour client {devis_data['client']['code']} " f"(brouillon={forcer_brouillon})" ) @@ -3131,9 +3142,9 @@ class SageConnector: try: self.cial.CptaApplication.BeginTrans() transaction_active = True - logger.debug("✅ Transaction Sage démarrée") + logger.debug(" Transaction Sage démarrée") except Exception as e: - logger.warning(f"⚠️ BeginTrans échoué: {e}") + logger.warning(f"BeginTrans échoué: {e}") try: # ===== CRÉATION DOCUMENT ===== @@ -3145,7 +3156,7 @@ class SageConnector: except: pass - logger.info("📄 Document devis créé") + logger.info(" Document devis créé") # ===== DATES ===== doc.DO_Date = pywintypes.Time( @@ -3153,26 +3164,9 @@ class SageConnector: ) if "date_livraison" in devis_data and devis_data["date_livraison"]: - try: - if hasattr(doc, 'DO_DateLivr'): - doc.DO_DateLivr = pywintypes.Time( - self.normaliser_date(devis_data["date_livraison"]) - ) - logger.info( - f"📅 Date livraison: {devis_data['date_livraison']}" - ) - else: - logger.warning( - "⚠️ DO_DateLivr non disponible dans l'API COM Sage" - ) - except AttributeError as e: - logger.warning( - f"⚠️ Impossible de définir DO_DateLivr: {e}" - ) - except Exception as e: - logger.warning( - f"⚠️ Erreur lors de la définition de la date livraison: {e}" - ) + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(devis_data["date_livraison"]) + ) # ===== CLIENT ===== factory_client = self.cial.CptaApplication.FactoryClient @@ -3182,25 +3176,25 @@ class SageConnector: if not persist_client: raise ValueError( - f"❌ Client {devis_data['client']['code']} introuvable" + f" Client {devis_data['client']['code']} introuvable" ) client_obj = self._cast_client(persist_client) if not client_obj: raise ValueError( - f"❌ Impossible de charger le client {devis_data['client']['code']}" + f" Impossible de charger le client {devis_data['client']['code']}" ) doc.SetDefaultClient(client_obj) - logger.info(f"👤 Client {devis_data['client']['code']} associé") + logger.info(f" Client {devis_data['client']['code']} associé") # ===== STATUT ===== if forcer_brouillon: doc.DO_Statut = 0 - logger.info("📊 Statut défini: 0 (Brouillon)") + logger.info(" Statut défini: 0 (Brouillon)") else: doc.DO_Statut = 2 - logger.info("📊 Statut défini: 2 (Accepté)") + logger.info(" Statut défini: 2 (Accepté)") doc.Write() @@ -3212,7 +3206,7 @@ class SageConnector: factory_article = self.cial.FactoryArticle - logger.info(f"📦 Ajout de {len(devis_data['lignes'])} lignes...") + logger.info(f" Ajout de {len(devis_data['lignes'])} lignes...") for idx, ligne_data in enumerate(devis_data["lignes"], 1): logger.debug( @@ -3226,7 +3220,7 @@ class SageConnector: if not persist_article: raise ValueError( - f"❌ Article {ligne_data['article_code']} introuvable" + f" Article {ligne_data['article_code']} introuvable" ) article_obj = win32com.client.CastTo( @@ -3239,7 +3233,7 @@ class SageConnector: if prix_sage == 0: logger.warning( - f"⚠️ Article {ligne_data['article_code']} a un prix = 0€" + f"Article {ligne_data['article_code']} a un prix = 0€" ) # Créer la ligne @@ -3290,51 +3284,52 @@ class SageConnector: ligne_obj.DL_Remise01REM_Valeur = float(remise) ligne_obj.DL_Remise01REM_Type = 0 except Exception as e: - logger.warning(f"⚠️ Remise non appliquée: {e}") + logger.warning(f"Remise non appliquée: {e}") ligne_obj.Write() - logger.info(f"✅ {len(devis_data['lignes'])} lignes écrites") + logger.info(f" {len(devis_data['lignes'])} lignes écrites") # ===== VALIDATION ===== doc.Write() # ===== PROCESS ===== if not forcer_brouillon: - logger.info("🔄 Lancement Process()...") + logger.info(" Lancement Process()...") process.Process() else: try: process.Process() - logger.info("✅ Process() appelé (brouillon)") + logger.info(" Process() appelé (brouillon)") except: - logger.debug("⚠️ Process() ignoré pour brouillon") + logger.debug("Process() ignoré pour brouillon") # ===== RÉCUPÉRATION NUMÉRO ===== numero_devis = self._recuperer_numero_devis(process, doc) if not numero_devis: - raise RuntimeError("❌ Numéro devis vide après création") + raise RuntimeError(" Numéro devis vide après création") - logger.info(f"📄 Numéro: {numero_devis}") + logger.info(f" Numéro: {numero_devis}") # ===== COMMIT ===== if transaction_active: try: self.cial.CptaApplication.CommitTrans() - logger.info("✅ Transaction committée") + logger.info(" Transaction committée") except: pass # ===== ATTENTE POUR STABILISATION ===== import time + time.sleep(0.5) # ===== RÉFÉRENCE (RECHARGER D'ABORD LE DOCUMENT) ===== if "reference" in devis_data and devis_data["reference"]: try: logger.info( - f"🔖 Application de la référence: {devis_data['reference']}" + f" Application de la référence: {devis_data['reference']}" ) # RECHARGER le document par son numéro @@ -3348,12 +3343,13 @@ class SageConnector: doc_reload.Write() time.sleep(0.5) + doc_reload.Read() - logger.info(f"✅ Référence définie: {nouvelle_reference}") + logger.info(f" Référence définie: {nouvelle_reference}") except Exception as e: logger.warning( - f"⚠️ Impossible de définir la référence: {e}", + f"Impossible de définir la référence: {e}", exc_info=True, ) @@ -3365,7 +3361,7 @@ class SageConnector: ) logger.info( - f"✅ ✅ ✅ DEVIS CRÉÉ: {numero_devis} - {doc_final_data['total_ttc']}€ TTC ✅ ✅ ✅" + f" DEVIS CRÉÉ: {numero_devis} - {doc_final_data['total_ttc']}€ TTC " ) return doc_final_data @@ -3374,16 +3370,15 @@ class SageConnector: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() - logger.error("❌ Transaction annulée (rollback)") + logger.error(" Transaction annulée (rollback)") except: pass raise except Exception as e: - logger.error(f"❌ ERREUR CRÉATION DEVIS: {e}", exc_info=True) + logger.error(f" ERREUR CRÉATION DEVIS: {e}", exc_info=True) raise RuntimeError(f"Échec création devis: {str(e)}") - - + def _recuperer_numero_devis(self, process, doc): """Récupère le numéro du devis créé via plusieurs méthodes.""" numero_devis = None @@ -3455,13 +3450,13 @@ class SageConnector: reference_final = devis_data.get("reference", "") date_livraison_final = devis_data.get("date_livraison") - logger.info(f"💰 Total HT: {total_ht}€") - logger.info(f"💰 Total TTC: {total_ttc}€") - logger.info(f"📊 Statut final: {statut_final}") + logger.info(f" Total HT: {total_ht}€") + logger.info(f" Total TTC: {total_ttc}€") + logger.info(f" Statut final: {statut_final}") if reference_final: - logger.info(f"🔖 Référence: {reference_final}") + logger.info(f" Référence: {reference_final}") if date_livraison_final: - logger.info(f"📅 Date livraison: {date_livraison_final}") + logger.info(f" Date livraison: {date_livraison_final}") return { "numero_devis": numero_devis, @@ -3491,7 +3486,7 @@ class SageConnector: getattr(doc_test, "DO_Type", -1) == 0 and getattr(doc_test, "DO_Piece", "") == numero_devis ): - logger.info(f"✅ Document trouvé à l'index {index}") + logger.info(f" Document trouvé à l'index {index}") return persist_test index += 1 @@ -3501,157 +3496,627 @@ class SageConnector: return None def modifier_devis(self, numero: str, devis_data: Dict) -> Dict: - """Modifie un devis existant dans Sage.""" + """ + Modifie un devis existant dans Sage - VERSION COMPLÈTE. + + Args: + numero: Numéro du devis + devis_data: dict contenant les champs à modifier: + - date_devis: str ou date (optionnel) + - date_livraison: str ou date (optionnel) + - reference: str (optionnel) + - statut: int (optionnel) + - lignes: list[dict] (optionnel) + """ + # ======================================== + # LOG INITIAL CRITIQUE - POUR VÉRIFIER QUE LA MÉTHODE EST APPELÉE + # ======================================== + logger.info("=" * 100) + logger.info("=" * 100) + logger.info(f" NOUVELLE MÉTHODE modifier_devis() APPELÉE POUR {numero} ") + logger.info(f" Données reçues: {devis_data}") + logger.info("=" * 100) + if not self.cial: + logger.error(" Connexion Sage non établie") raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: - logger.info(f"🔍 Recherche devis {numero}...") + logger.info("") + logger.info("=" * 80) + logger.info(f" [ÉTAPE 1] CHARGEMENT DU DEVIS {numero}") + logger.info("=" * 80) + + # Charger le devis doc = self._charger_devis(numero) - logger.info(f"✅ Devis {numero} trouvé") + logger.info(f" Devis {numero} chargé avec succès") + # Afficher l'état INITIAL + logger.info("") + self._afficher_etat_document(doc, "📸 ÉTAT INITIAL") + + # Vérifier qu'il n'est pas transformé + logger.info(" Vérification statut transformation...") self._verifier_devis_non_transforme(numero, doc) + logger.info(" Devis non transformé - modification autorisée") - champs_modifies = [] + # ======================================== + # ÉTAPE 2 : INFORMATIONS CLIENT ET LIGNES + # ======================================== + logger.info("") + logger.info("=" * 80) + logger.info(" [ÉTAPE 2] ANALYSE DOCUMENT ACTUEL") + logger.info("=" * 80) + + # Client + client_code_initial = "" + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code_initial = getattr(client_obj, "CT_Num", "").strip() + logger.info(f" Client: {client_code_initial}") + else: + logger.warning(" Objet Client non trouvé") + except Exception as e: + logger.warning(f" Impossible de lire le client: {e}") - # ===== EXTRAIRE référence et statut pour les traiter à la fin ===== + # Compter lignes + nb_lignes_initial = self._compter_lignes_document(doc) + logger.info(f" Nombre de lignes actuelles: {nb_lignes_initial}") + + # ======================================== + # ÉTAPE 3 : ANALYSE DES MODIFICATIONS DEMANDÉES + # ======================================== + logger.info("") + logger.info("=" * 80) + logger.info(" [ÉTAPE 3] ANALYSE MODIFICATIONS DEMANDÉES") + logger.info("=" * 80) + + modif_date = "date_devis" in devis_data + modif_date_livraison = "date_livraison" in devis_data + modif_statut = "statut" in devis_data + modif_ref = "reference" in devis_data + modif_lignes = "lignes" in devis_data and devis_data["lignes"] is not None + + logger.info(f" Date devis: {modif_date}") + if modif_date: + logger.info(f" → Valeur: {devis_data['date_devis']}") + + logger.info(f" Date livraison: {modif_date_livraison}") + if modif_date_livraison: + logger.info(f" → Valeur: {devis_data['date_livraison']}") + + logger.info(f" Référence: {modif_ref}") + if modif_ref: + logger.info(f" → Valeur: '{devis_data['reference']}'") + + logger.info(f" Statut: {modif_statut}") + if modif_statut: + logger.info(f" → Valeur: {devis_data['statut']}") + + logger.info(f" Lignes: {modif_lignes}") + if modif_lignes: + logger.info(f" → Nombre: {len(devis_data['lignes'])}") + for i, ligne in enumerate(devis_data['lignes'], 1): + logger.info(f" → Ligne {i}: {ligne.get('article_code')} (Qté: {ligne.get('quantite')})") + + # Reporter référence et statut après lignes devis_data_temp = devis_data.copy() reference_a_modifier = None statut_a_modifier = None - if "reference" in devis_data_temp: - reference_a_modifier = devis_data_temp.pop("reference") - logger.info( - "🔖 Modification de la référence reportée après les lignes" - ) + if modif_lignes: + logger.info("") + logger.info(" STRATÉGIE: Report référence/statut APRÈS modification lignes") + + if modif_ref: + reference_a_modifier = devis_data_temp.pop("reference") + logger.info(f" Référence '{reference_a_modifier}' reportée") + modif_ref = False - if "lignes" in devis_data and devis_data["lignes"] is not None: - if "statut" in devis_data_temp: + if modif_statut: statut_a_modifier = devis_data_temp.pop("statut") - logger.info( - "📊 Modification du statut reportée après les lignes" - ) + logger.info(f" Statut {statut_a_modifier} reporté") + modif_statut = False - # ===== MODIFIER CHAMPS SIMPLES (sauf référence et statut) ===== - champs_modifies = self._modifier_champs_simples(doc, devis_data_temp) + # ======================================== + # ÉTAPE 4 : TEST WRITE() BASIQUE + # ======================================== + logger.info("") + logger.info("=" * 80) + logger.info(" [ÉTAPE 4] TEST WRITE() BASIQUE") + logger.info("=" * 80) + logger.info("Test sans modification pour vérifier le verrouillage...") + + try: + doc.Write() + logger.info(" Write() basique OK - Document NON verrouillé") + + time.sleep(0.3) + doc.Read() + logger.info(" Read() après Write() OK") + except Exception as e: + logger.error(f" Write() basique ÉCHOUE: {e}") + logger.error(" ABANDON: Document VERROUILLÉ ou problème COM") + raise ValueError(f"Document verrouillé: {e}") + + champs_modifies = [] + + # ======================================== + # ÉTAPE 5 : MODIFICATIONS SIMPLES (sans lignes) + # ======================================== + if not modif_lignes and (modif_date or modif_date_livraison or modif_statut or modif_ref): + logger.info("") + logger.info("=" * 80) + logger.info(" [ÉTAPE 5A] MODIFICATIONS SIMPLES (sans lignes)") + logger.info("=" * 80) + + # Date devis + if modif_date: + logger.info("") + logger.info(" Modification DATE_DEVIS...") + try: + ancienne_date = getattr(doc, "DO_Date", None) + ancienne_date_str = ancienne_date.strftime("%Y-%m-%d") if ancienne_date else "None" + logger.info(f" Actuelle: {ancienne_date_str}") + + nouvelle_date = self.normaliser_date(devis_data_temp["date_devis"]) + nouvelle_date_str = nouvelle_date.strftime("%Y-%m-%d") + logger.info(f" Cible: {nouvelle_date_str}") + + doc.DO_Date = pywintypes.Time(nouvelle_date) + logger.info(" ✓ doc.DO_Date affecté") + + champs_modifies.append("date_devis") + logger.info(f" Date devis sera modifiée: {ancienne_date_str} → {nouvelle_date_str}") + except Exception as e: + logger.error(f" Erreur date devis: {e}", exc_info=True) + + # Date livraison + if modif_date_livraison: + logger.info("") + logger.info(" Modification DATE_LIVRAISON...") + try: + ancienne_date_livr = getattr(doc, "DO_DateLivr", None) + ancienne_date_livr_str = ancienne_date_livr.strftime("%Y-%m-%d") if ancienne_date_livr else "None" + logger.info(f" Actuelle: {ancienne_date_livr_str}") + + if devis_data_temp["date_livraison"]: + nouvelle_date_livr = self.normaliser_date(devis_data_temp["date_livraison"]) + nouvelle_date_livr_str = nouvelle_date_livr.strftime("%Y-%m-%d") + logger.info(f" Cible: {nouvelle_date_livr_str}") + + doc.DO_DateLivr = pywintypes.Time(nouvelle_date_livr) + logger.info(" ✓ doc.DO_DateLivr affecté") + else: + logger.info(" Cible: Effacement (None)") + doc.DO_DateLivr = None + logger.info(" ✓ doc.DO_DateLivr = None") + + champs_modifies.append("date_livraison") + logger.info(" Date livraison sera modifiée") + except Exception as e: + logger.error(f" Erreur date livraison: {e}", exc_info=True) + + # Référence + if modif_ref: + logger.info("") + logger.info(" Modification RÉFÉRENCE...") + try: + ancienne_ref = getattr(doc, "DO_Ref", "") + logger.info(f" Actuelle: '{ancienne_ref}'") + + nouvelle_ref = str(devis_data_temp["reference"]) if devis_data_temp["reference"] else "" + logger.info(f" Cible: '{nouvelle_ref}'") + + doc.DO_Ref = nouvelle_ref + logger.info(" ✓ doc.DO_Ref affecté") + + champs_modifies.append("reference") + logger.info(f" Référence sera modifiée: '{ancienne_ref}' → '{nouvelle_ref}'") + except Exception as e: + logger.error(f" Erreur référence: {e}", exc_info=True) + + # Statut + if modif_statut: + logger.info("") + logger.info(" Modification STATUT...") + try: + statut_actuel = getattr(doc, "DO_Statut", 0) + logger.info(f" Actuel: {statut_actuel}") + + nouveau_statut = int(devis_data_temp["statut"]) + logger.info(f" Cible: {nouveau_statut}") + + if nouveau_statut in [0, 1, 2, 3]: + doc.DO_Statut = nouveau_statut + logger.info(" ✓ doc.DO_Statut affecté") + + champs_modifies.append("statut") + logger.info(f" Statut sera modifié: {statut_actuel} → {nouveau_statut}") + else: + logger.warning(f" Statut {nouveau_statut} invalide (doit être 0,1,2 ou 3)") + except Exception as e: + logger.error(f" Erreur statut: {e}", exc_info=True) + + # WRITE FINAL pour modifications simples + logger.info("") + logger.info(" Write() modifications simples...") + try: + doc.Write() + logger.info(" Write() réussi") + + time.sleep(0.5) + doc.Read() + logger.info(" Read() après Write() OK") + except Exception as e: + logger.error(f" Write() a échoué: {e}", exc_info=True) + raise + + # ======================================== + # ÉTAPE 6 : REMPLACEMENT COMPLET DES LIGNES + # ======================================== + elif modif_lignes: + logger.info("") + logger.info("=" * 80) + logger.info(" [ÉTAPE 5B] REMPLACEMENT COMPLET DES LIGNES") + logger.info("=" * 80) + + # Modifier dates AVANT les lignes + if modif_date: + logger.info(" Modification date devis (avant lignes)...") + try: + nouvelle_date = self.normaliser_date(devis_data_temp["date_devis"]) + doc.DO_Date = pywintypes.Time(nouvelle_date) + champs_modifies.append("date_devis") + logger.info(f" Date: {nouvelle_date.strftime('%Y-%m-%d')}") + except Exception as e: + logger.error(f" Erreur: {e}") + + if modif_date_livraison: + logger.info(" Modification date livraison (avant lignes)...") + try: + if devis_data_temp["date_livraison"]: + nouvelle_date_livr = self.normaliser_date(devis_data_temp["date_livraison"]) + doc.DO_DateLivr = pywintypes.Time(nouvelle_date_livr) + logger.info(f" Date livraison: {nouvelle_date_livr.strftime('%Y-%m-%d')}") + else: + doc.DO_DateLivr = None + logger.info(" Date livraison effacée") + champs_modifies.append("date_livraison") + except Exception as e: + logger.error(f" Erreur: {e}") + + nouvelles_lignes = devis_data["lignes"] + nb_nouvelles = len(nouvelles_lignes) + + logger.info("") + logger.info(f" Remplacement: {nb_lignes_initial} lignes → {nb_nouvelles} lignes") + + # Récupérer factories + try: + factory_lignes = doc.FactoryDocumentLigne + except: + factory_lignes = doc.FactoryDocumentVenteLigne + + factory_article = self.cial.FactoryArticle + + # ============================================ + # SUPPRESSION DE TOUTES LES LIGNES + # ============================================ + if nb_lignes_initial > 0: + logger.info("") + logger.info(f" Suppression de {nb_lignes_initial} lignes existantes...") + + for idx in range(nb_lignes_initial, 0, -1): + try: + ligne_p = factory_lignes.List(idx) + if ligne_p: + try: + ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") + except: + ligne = win32com.client.CastTo(ligne_p, "IBODocumentVenteLigne3") + + ligne.Read() + ligne.Remove() + logger.debug(f" Ligne {idx} supprimée") + except Exception as e: + logger.warning(f" Ligne {idx} non supprimée: {e}") + + logger.info(f" {nb_lignes_initial} lignes supprimées") + + # ============================================ + # CRÉATION DES NOUVELLES LIGNES + # ============================================ + logger.info("") + logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") + + for idx, ligne_data in enumerate(nouvelles_lignes, 1): + article_code = ligne_data["article_code"] + quantite = float(ligne_data["quantite"]) + + logger.info("") + logger.info(f" Ligne {idx}/{nb_nouvelles}: {article_code}") + logger.info(f" Quantité: {quantite}") + if ligne_data.get("prix_unitaire_ht"): + logger.info(f" Prix HT: {ligne_data['prix_unitaire_ht']}€") + if ligne_data.get("remise_pourcentage"): + logger.info(f" Remise: {ligne_data['remise_pourcentage']}%") + + try: + # Charger article + persist_article = factory_article.ReadReference(article_code) + if not persist_article: + raise ValueError(f"Article {article_code} INTROUVABLE") + + article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") + article_obj.Read() + logger.info(f" ✓ Article chargé") + + # Créer ligne + ligne_persist = factory_lignes.Create() + + try: + ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") + except: + ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") + + # Associer article + try: + ligne_obj.SetDefaultArticleReference(article_code, quantite) + logger.info(f" ✓ Article associé via SetDefaultArticleReference") + except: + try: + ligne_obj.SetDefaultArticle(article_obj, quantite) + logger.info(f" ✓ Article associé via SetDefaultArticle") + except: + ligne_obj.DL_Design = ligne_data.get("designation", "") + ligne_obj.DL_Qte = quantite + logger.info(f" ✓ Article associé manuellement") + + # Prix + if ligne_data.get("prix_unitaire_ht"): + ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"]) + logger.info(f" ✓ Prix unitaire défini") + + # Remise + if ligne_data.get("remise_pourcentage", 0) > 0: + try: + ligne_obj.DL_Remise01REM_Valeur = float(ligne_data["remise_pourcentage"]) + ligne_obj.DL_Remise01REM_Type = 0 + logger.info(f" ✓ Remise définie") + except: + logger.debug(f" Remise non supportée") + + # Écrire ligne + ligne_obj.Write() + logger.info(f" Ligne {idx} créée avec succès") + + except Exception as e: + logger.error(f" ERREUR ligne {idx}: {e}", exc_info=True) + raise + + logger.info("") + logger.info(f" {nb_nouvelles} lignes créées") + + # WRITE après lignes + logger.info("") + logger.info(" Write() après remplacement lignes...") + try: + doc.Write() + logger.info(" Write() réussi") + + time.sleep(0.5) + doc.Read() + logger.info(" Read() après Write() OK") + except Exception as e: + logger.error(f" Write() a échoué: {e}", exc_info=True) + raise - # ===== MODIFICATION DES LIGNES ===== - if "lignes" in devis_data and devis_data["lignes"] is not None: - self._modifier_lignes_devis(doc, devis_data["lignes"]) champs_modifies.append("lignes") - logger.info("💾 Sauvegarde après modification des lignes...") - doc.Write() - - import time - - time.sleep(0.5) - - doc.Read() - - # ===== MODIFIER LA RÉFÉRENCE (APRÈS les lignes) ===== + # ======================================== + # ÉTAPE 7 : RÉFÉRENCE APRÈS LIGNES + # ======================================== if reference_a_modifier is not None: + logger.info("") + logger.info("=" * 80) + logger.info(" [ÉTAPE 6] MODIFICATION RÉFÉRENCE (après lignes)") + logger.info("=" * 80) + try: - ancienne_reference = getattr(doc, "DO_Ref", "") - nouvelle_reference = ( - str(reference_a_modifier) if reference_a_modifier else "" - ) + ancienne_ref = getattr(doc, "DO_Ref", "") + nouvelle_ref = str(reference_a_modifier) if reference_a_modifier else "" + + logger.info(f" Actuelle: '{ancienne_ref}'") + logger.info(f" Cible: '{nouvelle_ref}'") - doc.DO_Ref = nouvelle_reference + doc.DO_Ref = nouvelle_ref + logger.info(" ✓ doc.DO_Ref affecté") + doc.Write() - - import time - + logger.info(" ✓ Write()") + time.sleep(0.5) - doc.Read() + logger.info(" ✓ Read()") champs_modifies.append("reference") - logger.info( - f"🔖 Référence modifiée: '{ancienne_reference}' → '{nouvelle_reference}'" - ) + logger.info(f" Référence modifiée: '{ancienne_ref}' → '{nouvelle_ref}'") except Exception as e: - logger.warning(f"⚠️ Impossible de modifier la référence: {e}") + logger.error(f" Erreur référence: {e}", exc_info=True) - # ===== MODIFIER LE STATUT (EN DERNIER) ===== + # ======================================== + # ÉTAPE 8 : STATUT EN DERNIER + # ======================================== if statut_a_modifier is not None: + logger.info("") + logger.info("=" * 80) + logger.info(" [ÉTAPE 7] MODIFICATION STATUT (en dernier)") + logger.info("=" * 80) + try: statut_actuel = getattr(doc, "DO_Statut", 0) nouveau_statut = int(statut_a_modifier) + + logger.info(f" Actuel: {statut_actuel}") + logger.info(f" Cible: {nouveau_statut}") - if nouveau_statut != statut_actuel and nouveau_statut in [ - 0, - 1, - 2, - 3, - ]: + if nouveau_statut != statut_actuel and nouveau_statut in [0, 1, 2, 3]: doc.DO_Statut = nouveau_statut + logger.info(" ✓ doc.DO_Statut affecté") + doc.Write() - - import time - + logger.info(" ✓ Write()") + time.sleep(0.5) - doc.Read() + logger.info(" ✓ Read()") champs_modifies.append("statut") - logger.info( - f"📊 Statut modifié: {statut_actuel} → {nouveau_statut}" - ) + logger.info(f" Statut modifié: {statut_actuel} → {nouveau_statut}") + else: + logger.info(f" Pas de modification (identique ou invalide)") except Exception as e: - logger.warning(f"⚠️ Impossible de modifier le statut: {e}") + logger.error(f" Erreur statut: {e}", exc_info=True) + + # ======================================== + # ÉTAPE 9 : VALIDATION FINALE + # ======================================== + logger.info("") + logger.info("=" * 80) + logger.info(" [ÉTAPE 8] VALIDATION FINALE") + logger.info("=" * 80) - # ===== VALIDATION FINALE ===== - logger.info("💾 Validation finale...") try: doc.Write() - except: - pass - - import time + logger.info(" Write() final") + except Exception as e: + logger.warning(f" Write() final: {e}") time.sleep(0.5) - doc.Read() + logger.info(" Read() final") - # ===== RÉSULTAT ===== + # Afficher état final + logger.info("") + self._afficher_etat_document(doc, "📸 ÉTAT FINAL") + + # ======================================== + # ÉTAPE 10 : EXTRACTION RÉSULTAT + # ======================================== + logger.info("") + logger.info("=" * 80) + logger.info(" [ÉTAPE 9] EXTRACTION RÉSULTAT") + logger.info("=" * 80) + resultat = self._extraire_infos_devis(doc, numero, champs_modifies) + + logger.info(f" Résultat extrait:") + logger.info(f" Numéro: {resultat['numero']}") + logger.info(f" Référence: '{resultat['reference']}'") + logger.info(f" Date devis: {resultat['date_devis']}") + logger.info(f" Date livraison: {resultat['date_livraison']}") + logger.info(f" Statut: {resultat['statut']}") + logger.info(f" Total HT: {resultat['total_ht']}€") + logger.info(f" Total TTC: {resultat['total_ttc']}€") + logger.info(f" Champs modifiés: {resultat['champs_modifies']}") - logger.info(f"✅✅✅ DEVIS MODIFIÉ: {numero} ✅✅✅") - logger.info( - f"💰 Totaux: {resultat['total_ht']}€ HT / {resultat['total_ttc']}€ TTC" - ) + logger.info("") + logger.info("=" * 100) + logger.info(f" MODIFICATION DEVIS {numero} TERMINÉE AVEC SUCCÈS ") + logger.info("=" * 100) return resultat except ValueError as e: - logger.error(f"❌ Erreur métier: {e}") + logger.error(f" ERREUR MÉTIER: {e}") raise except Exception as e: - logger.error(f"❌ Erreur technique: {e}", exc_info=True) + logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True) raise RuntimeError(f"Erreur technique Sage: {str(e)}") + + def _afficher_etat_document(self, doc, titre: str): + """Affiche l'état complet d'un document.""" + logger.info("-" * 80) + logger.info(titre) + logger.info("-" * 80) + try: + logger.info(f" DO_Piece: {getattr(doc, 'DO_Piece', 'N/A')}") + logger.info(f" DO_Ref: '{getattr(doc, 'DO_Ref', 'N/A')}'") + logger.info(f" DO_Statut: {getattr(doc, 'DO_Statut', 'N/A')}") + + date_doc = getattr(doc, 'DO_Date', None) + date_str = date_doc.strftime('%Y-%m-%d') if date_doc else 'None' + logger.info(f" DO_Date: {date_str}") + + date_livr = getattr(doc, 'DO_DateLivr', None) + date_livr_str = date_livr.strftime('%Y-%m-%d') if date_livr else 'None' + logger.info(f" DO_DateLivr: {date_livr_str}") + + logger.info(f" DO_TotalHT: {getattr(doc, 'DO_TotalHT', 0)}€") + logger.info(f" DO_TotalTTC: {getattr(doc, 'DO_TotalTTC', 0)}€") + except Exception as e: + logger.error(f" Erreur affichage état: {e}") + logger.info("-" * 80) + + + def _compter_lignes_document(self, doc) -> int: + """Compte les lignes d'un document.""" + try: + try: + factory_lignes = doc.FactoryDocumentLigne + except: + factory_lignes = doc.FactoryDocumentVenteLigne + + count = 0 + index = 1 + while index <= 100: + try: + ligne_p = factory_lignes.List(index) + if ligne_p is None: + break + count += 1 + index += 1 + except: + break + return count + except Exception as e: + logger.warning(f" Erreur comptage lignes: {e}") + return 0 + def _charger_devis(self, numero: str): """Charge un devis depuis Sage.""" + logger.info(f" Chargement devis {numero}...") + factory = self.cial.FactoryDocumentVente + + # Tentative 1: ReadPiece + logger.info(" Tentative ReadPiece(0, numero)...") persist = factory.ReadPiece(0, numero) if not persist: - # Recherche dans la liste si ReadPiece échoue + logger.warning(" ReadPiece a échoué, recherche dans la liste...") persist = self._rechercher_devis_par_numero(numero, factory) if not persist: - raise ValueError(f"Devis {numero} introuvable") + raise ValueError(f" Devis {numero} INTROUVABLE") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() - + + logger.info(f" Devis {numero} chargé") return doc + def _rechercher_devis_par_numero(self, numero: str, factory): - """Recherche un devis par son numéro dans la liste.""" + """Recherche un devis par numéro dans la liste.""" + logger.info(f" Recherche de {numero} dans la liste...") + index = 1 while index < 10000: try: @@ -3666,365 +4131,51 @@ class SageConnector: getattr(doc_test, "DO_Type", -1) == 0 and getattr(doc_test, "DO_Piece", "") == numero ): + logger.info(f" Trouvé à l'index {index}") return persist_test index += 1 except: index += 1 + logger.error(f" Devis {numero} non trouvé dans la liste") return None + def _verifier_devis_non_transforme(self, numero: str, doc): - """Vérifie que le devis n'a pas déjà été transformé.""" + """Vérifie que le devis n'est pas transformé.""" verification = self.verifier_si_deja_transforme_sql(numero, 0) if verification["deja_transforme"]: docs_cibles = verification["documents_cibles"] nums = [d["numero"] for d in docs_cibles] raise ValueError( - f"❌ Devis {numero} déjà transformé en {len(docs_cibles)} document(s): {', '.join(nums)}" + f" Devis {numero} déjà transformé en {len(docs_cibles)} document(s): {', '.join(nums)}" ) statut_actuel = getattr(doc, "DO_Statut", 0) if statut_actuel == 5: - raise ValueError(f"Devis {numero} déjà transformé (statut=5)") + raise ValueError(f" Devis {numero} déjà transformé (statut=5)") - def _modifier_champs_simples(self, doc, devis_data: Dict) -> list: - """Modifie les champs simples du devis (date, dates expédition/livraison, référence, statut).""" - champs_modifies = [] - - # IMPORTANT: Relire le document pour s'assurer qu'il est à jour - try: - doc.Read() - except: - pass - - # DATE DEVIS - Modifier et sauvegarder immédiatement - if "date_devis" in devis_data: - try: - doc.DO_Date = pywintypes.Time( - self.normaliser_date(devis_data.get("date_devis")) - ) - doc.Write() - doc.Read() - - champs_modifies.append("date") - logger.info(f"📅 Date devis modifiée: {devis_data['date_devis']}") - except Exception as e: - logger.warning(f"⚠️ Impossible de modifier la date: {e}") - - if "date_livraison" in devis_data: - try: - if devis_data["date_livraison"]: - doc.DO_DateLivr = pywintypes.Time( - self.normaliser_date(devis_data["date_livraison"]) - ) - logger.info( - f"📅 Date livraison modifiée: {devis_data['date_livraison']}" - ) - else: - # Si None ou vide, effacer la date - try: - doc.DO_DateLivr = None - logger.info("📅 Date livraison effacée") - except: - pass - - doc.Write() - doc.Read() - - champs_modifies.append("date_livraison") - except Exception as e: - logger.warning(f"⚠️ Impossible de modifier la date de livraison: {e}") - - # RÉFÉRENCE - Modifier et sauvegarder immédiatement - if "reference" in devis_data: - try: - nouvelle_reference = devis_data["reference"] - ancienne_reference = getattr(doc, "DO_Ref", "") - - doc.DO_Ref = str(nouvelle_reference) if nouvelle_reference else "" - doc.Write() - doc.Read() - - champs_modifies.append("reference") - logger.info( - f"🔖 Référence: '{ancienne_reference}' → '{nouvelle_reference}'" - ) - except Exception as e: - logger.warning(f"⚠️ Impossible de modifier la référence: {e}") - - # STATUT - Modifier avec précaution (à la fin seulement) - # Le statut ne doit être modifié QUE si aucune autre modification n'est en cours - if "statut" in devis_data and "lignes" not in devis_data: - try: - statut_actuel = getattr(doc, "DO_Statut", 0) - nouveau_statut = int(devis_data["statut"]) - - # Vérifier que le changement de statut est valide - if nouveau_statut != statut_actuel: - # Statuts valides: 0=Brouillon, 1=Refusé, 2=Accepté, 3=Confirmé, 5=Transformé - if nouveau_statut in [0, 1, 2, 3]: - doc.DO_Statut = nouveau_statut - doc.Write() - doc.Read() - - champs_modifies.append("statut") - logger.info( - f"📊 Statut modifié: {statut_actuel} → {nouveau_statut}" - ) - else: - logger.warning( - f"⚠️ Statut {nouveau_statut} invalide ou non modifiable" - ) - except Exception as e: - logger.warning(f"⚠️ Impossible de modifier le statut: {e}") - - return champs_modifies - - def _modifier_lignes_devis(self, doc, nouvelles_lignes: list): - """Modifie intelligemment les lignes du devis.""" - logger.info(f"🔄 Modification intelligente des lignes...") - - # Relire le document pour s'assurer qu'il est à jour - try: - doc.Read() - except: - pass - - nb_nouvelles = len(nouvelles_lignes) - - try: - factory_lignes = doc.FactoryDocumentLigne - except: - factory_lignes = doc.FactoryDocumentVenteLigne - - factory_article = self.cial.FactoryArticle - - # Compter les lignes existantes - nb_existantes = self._compter_lignes_existantes(factory_lignes) - - logger.info(f"📊 {nb_existantes} existantes → {nb_nouvelles} nouvelles") - - # STRATÉGIE : Supprimer d'abord, puis recréer - # C'est plus sûr que de modifier en place - if nb_nouvelles != nb_existantes: - logger.info("🔄 Stratégie: Suppression puis recréation des lignes") - - # Supprimer toutes les lignes existantes - self._supprimer_toutes_les_lignes(factory_lignes, nb_existantes) - - # Sauvegarder après suppression - doc.Write() - doc.Read() - - # Recréer toutes les lignes - for idx, ligne_data in enumerate(nouvelles_lignes, 1): - self._ajouter_nouvelle_ligne( - factory_lignes, factory_article, ligne_data, idx - ) - - else: - # STRATÉGIE : Modification en place si même nombre de lignes - logger.info("🔄 Stratégie: Modification en place") - - for idx in range(1, nb_nouvelles + 1): - self._modifier_ligne_existante( - factory_lignes, factory_article, idx, nouvelles_lignes[idx - 1] - ) - - def _supprimer_toutes_les_lignes(self, factory_lignes, nb_existantes: int): - """Supprime toutes les lignes du devis.""" - logger.info(f"🗑️ Suppression de {nb_existantes} lignes...") - - # Supprimer en partant de la fin pour éviter les problèmes d'index - for idx in range(nb_existantes, 0, -1): - try: - ligne_p = factory_lignes.List(idx) - if ligne_p: - try: - ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") - except: - ligne = win32com.client.CastTo( - ligne_p, "IBODocumentVenteLigne3" - ) - - ligne.Read() - - try: - ligne.Remove() - logger.debug(f" ✅ Ligne {idx} supprimée") - except AttributeError: - # Si Remove() n'existe pas, essayer WriteDefault() - try: - ligne.WriteDefault() - logger.debug( - f" ⚠️ Ligne {idx} réinitialisée (Remove indisponible)" - ) - except: - logger.warning( - f" ⚠️ Impossible de supprimer la ligne {idx}" - ) - except Exception as e: - logger.warning(f" ⚠️ Erreur suppression ligne {idx}: {e}") - except Exception as e: - logger.debug(f" ⚠️ Ligne {idx} non accessible: {e}") - - def _modifier_ligne_existante( - self, factory_lignes, factory_article, idx: int, ligne_data: dict - ): - """Modifie une ligne existante du devis.""" - try: - ligne_p = factory_lignes.List(idx) - if not ligne_p: - raise ValueError(f"Ligne {idx} introuvable") - - try: - ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") - except: - ligne = win32com.client.CastTo(ligne_p, "IBODocumentVenteLigne3") - - ligne.Read() - - persist_article = factory_article.ReadReference(ligne_data["article_code"]) - if not persist_article: - raise ValueError(f"Article {ligne_data['article_code']} introuvable") - - article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") - article_obj.Read() - - # Réinitialiser la ligne - try: - ligne.WriteDefault() - except: - pass - - quantite = float(ligne_data["quantite"]) - - # Définir l'article - try: - ligne.SetDefaultArticleReference(ligne_data["article_code"], quantite) - except: - try: - ligne.SetDefaultArticle(article_obj, quantite) - except: - ligne.DL_Design = ligne_data.get("designation", "") - ligne.DL_Qte = quantite - - # Prix - if ligne_data.get("prix_unitaire_ht"): - ligne.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"]) - - # Remise - if ligne_data.get("remise_pourcentage", 0) > 0: - try: - ligne.DL_Remise01REM_Valeur = float( - ligne_data["remise_pourcentage"] - ) - ligne.DL_Remise01REM_Type = 0 - except: - pass - - ligne.Write() - logger.debug(f" ✅ Ligne {idx} modifiée: {ligne_data['article_code']}") - - except Exception as e: - logger.error(f" ❌ Erreur modification ligne {idx}: {e}") - raise - - def _compter_lignes_existantes(self, factory_lignes) -> int: - """Compte le nombre de lignes existantes dans le document.""" - nb_existantes = 0 - index = 1 - while index <= 100: - try: - ligne_p = factory_lignes.List(index) - if ligne_p is None: - break - nb_existantes += 1 - index += 1 - except: - break - - return nb_existantes - - def _ajouter_nouvelle_ligne( - self, factory_lignes, factory_article, ligne_data: dict, idx: int - ): - """Ajoute une nouvelle ligne au devis.""" - persist_article = factory_article.ReadReference(ligne_data["article_code"]) - if not persist_article: - raise ValueError(f"Article {ligne_data['article_code']} introuvable") - - article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") - article_obj.Read() - - ligne_persist = factory_lignes.Create() - - try: - ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") - except: - ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") - - quantite = float(ligne_data["quantite"]) - - # Définir l'article - try: - ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite) - except: - try: - ligne_obj.SetDefaultArticle(article_obj, quantite) - except: - ligne_obj.DL_Design = ligne_data.get("designation", "") - ligne_obj.DL_Qte = quantite - - # Prix - if ligne_data.get("prix_unitaire_ht"): - ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"]) - - # Remise - if ligne_data.get("remise_pourcentage", 0) > 0: - try: - ligne_obj.DL_Remise01REM_Valeur = float( - ligne_data["remise_pourcentage"] - ) - ligne_obj.DL_Remise01REM_Type = 0 - except: - pass - - ligne_obj.Write() - logger.debug(f" ✅ Ligne {idx} ajoutée") - - def _supprimer_lignes_en_trop( - self, factory_lignes, nb_existantes: int, nb_nouvelles: int - ): - """Supprime les lignes en trop du devis.""" - for idx in range(nb_existantes, nb_nouvelles, -1): - try: - ligne_p = factory_lignes.List(idx) - if ligne_p: - ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") - ligne.Read() - - try: - ligne.Remove() - except AttributeError: - ligne.WriteDefault() - except: - pass - except: - pass def _extraire_infos_devis(self, doc, numero: str, champs_modifies: list) -> Dict: - """Extrait les informations du devis modifié.""" + """Extrait les informations complètes du devis.""" total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) statut = getattr(doc, "DO_Statut", 0) reference = getattr(doc, "DO_Ref", "") - # Extraction des dates - date_livraison = None + # Date devis + date_devis = None + try: + date_doc = getattr(doc, "DO_Date", None) + if date_doc: + date_devis = date_doc.strftime("%Y-%m-%d") + except: + pass + # Date livraison + date_livraison = None try: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: @@ -4032,16 +4183,29 @@ class SageConnector: except: pass + # Client + client_code = "" + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code = getattr(client_obj, "CT_Num", "") + except: + pass + return { "numero": numero, "total_ht": total_ht, "total_ttc": total_ttc, "reference": reference, + "date_devis": date_devis, "date_livraison": date_livraison, "champs_modifies": champs_modifies, "statut": statut, + "client_code": client_code, } + def lire_devis(self, numero_devis): try: # Lire le devis via SQL @@ -4053,7 +4217,7 @@ class SageConnector: return devis except Exception as e: - logger.error(f"❌ Erreur SQL lecture devis {numero_devis}: {e}") + logger.error(f" Erreur SQL lecture devis {numero_devis}: {e}") return None def lire_document(self, numero, type_doc): @@ -4137,11 +4301,11 @@ class SageConnector: if deja_transforme: logger.info( - f"[VERIF] ✅ Document {numero_source} a {len(documents_cibles)} transformation(s)" + f"[VERIF] Document {numero_source} a {len(documents_cibles)} transformation(s)" ) else: logger.info( - f"[VERIF] ℹ️ Document {numero_source} pas encore transformé" + f"[VERIF] Document {numero_source} pas encore transformé" ) return { @@ -4572,7 +4736,7 @@ class SageConnector: type_cible = int(type_cible) logger.info( - f"[TRANSFORM] 🔄 Transformation: {numero_source} ({type_source}) → type {type_cible}" + f"[TRANSFORM] Transformation: {numero_source} ({type_source}) → type {type_cible}" ) # ======================================== @@ -4599,7 +4763,7 @@ class SageConnector: # VÉRIFICATION OPTIONNELLE DES DOUBLONS # ======================================== if verifier_doublons: - logger.info("[TRANSFORM] 🔍 Vérification des doublons...") + logger.info("[TRANSFORM] Vérification des doublons...") verif = self.peut_etre_transforme(numero_source, type_source, type_cible) if not verif["possible"]: @@ -4608,7 +4772,7 @@ class SageConnector: f"{verif['raison']}. Document(s) existant(s): {', '.join(docs)}" ) - logger.info("[TRANSFORM] ✅ Aucun doublon détecté") + logger.info("[TRANSFORM] Aucun doublon détecté") try: with self._com_context(), self._lock_com: @@ -4617,7 +4781,7 @@ class SageConnector: # ======================================== # ÉTAPE 1 : LIRE LE DOCUMENT SOURCE # ======================================== - logger.info(f"[TRANSFORM] 📄 Lecture de {numero_source}...") + logger.info(f"[TRANSFORM] Lecture de {numero_source}...") if not factory.ExistPiece(type_source, numero_source): raise ValueError(f"Document {numero_source} introuvable") @@ -4669,12 +4833,12 @@ class SageConnector: if not transformer: raise RuntimeError("Échec création transformer") - logger.info("[TRANSFORM] ✅ Transformer créé") + logger.info("[TRANSFORM] Transformer créé") # ======================================== # ÉTAPE 3 : CONFIGURATION # ======================================== - logger.info("[TRANSFORM] ⚙️ Configuration...") + logger.info("[TRANSFORM] Configuration...") # Tenter de définir ConserveDocuments if hasattr(transformer, "ConserveDocuments"): @@ -4691,11 +4855,11 @@ class SageConnector: # ======================================== # ÉTAPE 4 : AJOUTER LE DOCUMENT # ======================================== - logger.info("[TRANSFORM] ➕ Ajout du document...") + logger.info("[TRANSFORM] Ajout du document...") try: transformer.AddDocument(doc_source) - logger.info("[TRANSFORM] ✅ Document ajouté") + logger.info("[TRANSFORM] Document ajouté") except Exception as e: raise RuntimeError(f"Impossible d'ajouter le document: {e}") @@ -4732,13 +4896,13 @@ class SageConnector: # ======================================== # ÉTAPE 7 : PROCESS (TRANSFORMATION) # ======================================== - logger.info("[TRANSFORM] ⚙️ Process()...") + logger.info("[TRANSFORM] Process()...") try: transformer.Process() - logger.info("[TRANSFORM] ✅ Process() réussi") + logger.info("[TRANSFORM] Process() réussi") except Exception as e: - logger.error(f"[TRANSFORM] ❌ Erreur Process(): {e}") + logger.error(f"[TRANSFORM] Erreur Process(): {e}") erreurs = self.lire_erreurs_sage(transformer, "Transformer") if erreurs: @@ -4751,7 +4915,7 @@ class SageConnector: # ======================================== # ÉTAPE 8 : RÉCUPÉRER LES RÉSULTATS # ======================================== - logger.info("[TRANSFORM] 📦 Récupération des résultats...") + logger.info("[TRANSFORM] Récupération des résultats...") list_results = getattr(transformer, "ListDocumentsResult", None) if not list_results: @@ -4829,7 +4993,7 @@ class SageConnector: doc_principal = documents_crees[0] logger.info( - f"[TRANSFORM] ✅ SUCCÈS: {numero_source} → {doc_principal['numero']}" + f"[TRANSFORM] SUCCÈS: {numero_source} → {doc_principal['numero']}" ) logger.info( f"[TRANSFORM] • {doc_principal['nb_lignes']} ligne(s)" @@ -4865,17 +5029,17 @@ class SageConnector: except ValueError as e: # Erreur métier (validation, doublon, etc.) - logger.error(f"[TRANSFORM] ❌ Erreur métier: {e}") + logger.error(f"[TRANSFORM] Erreur métier: {e}") raise except RuntimeError as e: # Erreur technique Sage - logger.error(f"[TRANSFORM] ❌ Erreur technique: {e}") + logger.error(f"[TRANSFORM] Erreur technique: {e}") raise except Exception as e: # Erreur inattendue - logger.error(f"[TRANSFORM] ❌ Erreur inattendue: {e}", exc_info=True) + logger.error(f"[TRANSFORM] Erreur inattendue: {e}", exc_info=True) raise RuntimeError(f"Échec transformation: {str(e)}") def lire_erreurs_sage(self, obj, nom_obj=""): @@ -5134,11 +5298,11 @@ class SageConnector: } ) - logger.info(f"✅ SQL: {len(prospects)} prospects") + logger.info(f" SQL: {len(prospects)} prospects") return prospects except Exception as e: - logger.error(f"❌ Erreur SQL prospects: {e}") + logger.error(f" Erreur SQL prospects: {e}") return [] def lire_prospect(self, code_prospect): @@ -5189,7 +5353,7 @@ class SageConnector: } except Exception as e: - logger.error(f"❌ Erreur SQL prospect {code_prospect}: {e}") + logger.error(f" Erreur SQL prospect {code_prospect}: {e}") return None def lister_avoirs(self, limit=100, statut=None): @@ -5239,14 +5403,14 @@ class SageConnector: return avoirs except Exception as e: - logger.error(f"❌ Erreur SQL avoirs: {e}") + logger.error(f" Erreur SQL avoirs: {e}") return [] def lire_avoir(self, numero): return self._lire_document_sql(numero, type_doc=50) def lister_livraisons(self, limit=100, statut=None): - """📖 Liste les livraisons via SQL (méthode legacy)""" + """ Liste les livraisons via SQL (méthode legacy)""" try: with self._get_sql_connection() as conn: cursor = conn.cursor() @@ -5293,11 +5457,11 @@ class SageConnector: return livraisons except Exception as e: - logger.error(f"❌ Erreur SQL livraisons: {e}") + logger.error(f" Erreur SQL livraisons: {e}") return [] def lire_livraison(self, numero): - """📖 Lit UNE livraison via SQL (avec lignes)""" + """ Lit UNE livraison via SQL (avec lignes)""" return self._lire_document_sql(numero, type_doc=30) def creer_client(self, client_data: Dict) -> Dict: @@ -5309,7 +5473,7 @@ class SageConnector: # ======================================== # ÉTAPE 0 : VALIDATION & NETTOYAGE # ======================================== - logger.info("🔍 === VALIDATION DES DONNÉES ===") + logger.info(" === VALIDATION DES DONNÉES ===") if not client_data.get("intitule"): raise ValueError("Le champ 'intitule' est obligatoire") @@ -5349,26 +5513,26 @@ class SageConnector: # 🔑 CRITIQUE : Initialiser l'objet client.SetDefault() - logger.info("✅ Objet client créé et initialisé") + logger.info(" Objet client créé et initialisé") # ======================================== # ÉTAPE 2 : CHAMPS OBLIGATOIRES (dans l'ordre !) # ======================================== - logger.info("📝 Définition des champs obligatoires...") + logger.info(" Définition des champs obligatoires...") # 1. Intitulé (OBLIGATOIRE) client.CT_Intitule = intitule - logger.debug(f" ✅ CT_Intitule: '{intitule}'") + logger.debug(f" CT_Intitule: '{intitule}'") - # ❌ CT_Type SUPPRIMÉ (n'existe pas dans cette version) + # 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 = "CLI" - logger.debug(" ✅ CT_Qualite: 'CLI'") + logger.debug(" CT_Qualite: 'CLI'") except: - logger.debug(" ⚠️ CT_Qualite non défini (pas critique)") + logger.debug(" CT_Qualite non défini (pas critique)") # 3. Compte général principal (OBLIGATOIRE) try: @@ -5383,18 +5547,18 @@ class SageConnector: # Assigner l'objet CompteG client.CompteGPrinc = compte_obj - logger.debug(f" ✅ CompteGPrinc: objet '{compte}' assigné") + logger.debug(f" CompteGPrinc: objet '{compte}' assigné") else: logger.warning( - f" ⚠️ Compte {compte} introuvable - utilisation du compte par défaut" + f" Compte {compte} introuvable - utilisation du compte par défaut" ) except Exception as e: - logger.warning(f" ⚠️ Erreur CompteGPrinc: {e}") + logger.warning(f" Erreur CompteGPrinc: {e}") # 4. Numéro client (OBLIGATOIRE - générer si vide) if num_prop: client.CT_Num = num_prop - logger.debug(f" ✅ CT_Num fourni: '{num_prop}'") + logger.debug(f" CT_Num fourni: '{num_prop}'") else: # 🔑 CRITIQUE : Générer le numéro automatiquement try: @@ -5403,7 +5567,7 @@ class SageConnector: client.SetDefaultNumPiece() num_genere = getattr(client, "CT_Num", "") logger.debug( - f" ✅ CT_Num auto-généré (SetDefaultNumPiece): '{num_genere}'" + f" CT_Num auto-généré (SetDefaultNumPiece): '{num_genere}'" ) else: # Méthode 2 : Lire le prochain numéro depuis la souche @@ -5412,7 +5576,7 @@ class SageConnector: if num_genere: client.CT_Num = num_genere logger.debug( - f" ✅ CT_Num auto-généré (GetNextNumero): '{num_genere}'" + f" CT_Num auto-généré (GetNextNumero): '{num_genere}'" ) else: # Méthode 3 : Fallback - utiliser un timestamp @@ -5421,10 +5585,10 @@ class SageConnector: num_genere = f"CLI{int(time.time()) % 1000000}" client.CT_Num = num_genere logger.warning( - f" ⚠️ CT_Num fallback temporaire: '{num_genere}'" + f" CT_Num fallback temporaire: '{num_genere}'" ) except Exception as e: - logger.error(f" ❌ Impossible de générer CT_Num: {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." ) @@ -5452,14 +5616,14 @@ class SageConnector: if hasattr(client, "N_Risque"): client.N_Risque = 1 - logger.debug(" ✅ Catégories (N_*) initialisées") + logger.debug(" Catégories (N_*) initialisées") except Exception as e: - logger.warning(f" ⚠️ Catégories: {e}") + logger.warning(f" Catégories: {e}") # ======================================== # ÉTAPE 3 : CHAMPS OPTIONNELS MAIS RECOMMANDÉS # ======================================== - logger.info("📝 Définition champs optionnels...") + logger.info(" Définition champs optionnels...") # Adresse (objet IAdresse) if any([adresse, code_postal, ville, pays]): @@ -5475,9 +5639,9 @@ class SageConnector: if pays: adresse_obj.Pays = pays - logger.debug(" ✅ Adresse définie") + logger.debug(" Adresse définie") except Exception as e: - logger.warning(f" ⚠️ Adresse: {e}") + logger.warning(f" Adresse: {e}") # Télécom (objet ITelecom) if telephone or email: @@ -5489,24 +5653,24 @@ class SageConnector: if email: telecom_obj.EMail = email - logger.debug(" ✅ Télécom défini") + logger.debug(" Télécom défini") except Exception as e: - logger.warning(f" ⚠️ Télécom: {e}") + logger.warning(f" Télécom: {e}") # Identifiants fiscaux if siret: try: client.CT_Siret = siret - logger.debug(f" ✅ SIRET: '{siret}'") + logger.debug(f" SIRET: '{siret}'") except Exception as e: - logger.warning(f" ⚠️ SIRET: {e}") + logger.warning(f" SIRET: {e}") if tva_intra: try: client.CT_Identifiant = tva_intra - logger.debug(f" ✅ TVA intracommunautaire: '{tva_intra}'") + logger.debug(f" TVA intracommunautaire: '{tva_intra}'") except Exception as e: - logger.warning(f" ⚠️ TVA: {e}") + logger.warning(f" TVA: {e}") # Autres champs utiles (valeurs par défaut intelligentes) try: @@ -5526,14 +5690,14 @@ class SageConnector: if hasattr(client, "CT_Sommeil"): client.CT_Sommeil = False - logger.debug(" ✅ Options par défaut définies") + logger.debug(" Options par défaut définies") except Exception as e: - logger.debug(f" ⚠️ Options: {e}") + logger.debug(f" Options: {e}") # ======================================== # ÉTAPE 4 : DIAGNOSTIC PRÉ-WRITE (pour debug) # ======================================== - logger.info("🔍 === DIAGNOSTIC PRÉ-WRITE ===") + logger.info(" === DIAGNOSTIC PRÉ-WRITE ===") champs_critiques = [ ("CT_Intitule", "str"), @@ -5548,18 +5712,18 @@ class SageConnector: val = getattr(client, champ, None) if type_attendu == "object": - status = "✅ Objet défini" if val else "❌ NULL" + status = " Objet défini" if val else " NULL" else: if type_attendu == "str": status = ( - f"✅ '{val}' (len={len(val)})" if val else "❌ Vide" + f" '{val}' (len={len(val)})" if val else " Vide" ) else: - status = f"✅ {val}" + status = f" {val}" logger.info(f" {champ}: {status}") except Exception as e: - logger.error(f" {champ}: ❌ Erreur - {e}") + logger.error(f" {champ}: Erreur - {e}") # ======================================== # ÉTAPE 5 : VÉRIFICATION FINALE CT_Num @@ -5567,23 +5731,23 @@ class SageConnector: num_avant_write = getattr(client, "CT_Num", "") if not num_avant_write: logger.error( - "❌ CRITIQUE: CT_Num toujours vide après toutes les tentatives !" + " 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}'") + 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...") try: client.Write() - logger.info("✅ Write() réussi !") + logger.info(" Write() réussi !") except Exception as e: error_detail = str(e) @@ -5595,13 +5759,13 @@ class SageConnector: error_detail = ( f"{sage_error.Description} (Code: {sage_error.Number})" ) - logger.error(f"❌ Erreur Sage: {error_detail}") + logger.error(f" Erreur Sage: {error_detail}") except: pass # Analyser l'erreur spécifique if "longueur invalide" in error_detail.lower(): - logger.error("❌ ERREUR 'longueur invalide' - Dump des champs:") + logger.error(" ERREUR 'longueur invalide' - Dump des champs:") for attr in dir(client): if attr.startswith("CT_") or attr.startswith("N_"): @@ -5632,14 +5796,14 @@ class SageConnector: try: client.Read() except Exception as e: - logger.warning(f"⚠️ Impossible de relire: {e}") + logger.warning(f"Impossible de relire: {e}") num_final = getattr(client, "CT_Num", "") if not num_final: raise RuntimeError("CT_Num vide après Write()") - logger.info(f"✅✅✅ CLIENT CRÉÉ: {num_final} - {intitule} ✅✅✅") + logger.info(f" CLIENT CRÉÉ: {num_final} - {intitule} ") # ======================================== # ÉTAPE 8 : REFRESH CACHE @@ -5661,11 +5825,11 @@ class SageConnector: } except ValueError as e: - logger.error(f"❌ Erreur métier: {e}") + logger.error(f" Erreur métier: {e}") raise except Exception as e: - logger.error(f"❌ Erreur création client: {e}", exc_info=True) + logger.error(f" Erreur création client: {e}", exc_info=True) error_message = str(e) if self.cial: @@ -5687,7 +5851,7 @@ class SageConnector: # ======================================== # ÉTAPE 1 : CHARGER LE CLIENT EXISTANT # ======================================== - logger.info(f"🔍 Recherche client {code}...") + logger.info(f" Recherche client {code}...") factory_client = self.cial.CptaApplication.FactoryClient persist = factory_client.ReadNumero(code) @@ -5699,13 +5863,13 @@ class SageConnector: client.Read() logger.info( - f"✅ Client {code} trouvé: {getattr(client, 'CT_Intitule', '')}" + f" Client {code} trouvé: {getattr(client, 'CT_Intitule', '')}" ) # ======================================== # ÉTAPE 2 : METTRE À JOUR LES CHAMPS FOURNIS # ======================================== - logger.info("📝 Mise à jour des champs...") + logger.info(" Mise à jour des champs...") champs_modifies = [] @@ -5744,7 +5908,7 @@ class SageConnector: champs_modifies.append("pays") except Exception as e: - logger.warning(f"⚠️ Erreur mise à jour adresse: {e}") + logger.warning(f"Erreur mise à jour adresse: {e}") # Télécom if "email" in client_data or "telephone" in client_data: @@ -5762,7 +5926,7 @@ class SageConnector: champs_modifies.append("telephone") except Exception as e: - logger.warning(f"⚠️ Erreur mise à jour télécom: {e}") + logger.warning(f"Erreur mise à jour télécom: {e}") # SIRET if "siret" in client_data: @@ -5771,7 +5935,7 @@ class SageConnector: client.CT_Siret = siret champs_modifies.append("siret") except Exception as e: - logger.warning(f"⚠️ Erreur mise à jour SIRET: {e}") + logger.warning(f"Erreur mise à jour SIRET: {e}") # TVA Intracommunautaire if "tva_intra" in client_data: @@ -5780,22 +5944,22 @@ class SageConnector: client.CT_Identifiant = tva champs_modifies.append("tva_intra") except Exception as e: - logger.warning(f"⚠️ Erreur mise à jour TVA: {e}") + logger.warning(f"Erreur mise à jour TVA: {e}") if not champs_modifies: - logger.warning("⚠️ Aucun champ à modifier") + logger.warning("Aucun champ à modifier") return self._extraire_client(client) - logger.info(f"📝 Champs à modifier: {', '.join(champs_modifies)}") + logger.info(f" Champs à modifier: {', '.join(champs_modifies)}") # ======================================== # ÉTAPE 3 : ÉCRIRE LES MODIFICATIONS # ======================================== - logger.info("💾 Écriture des modifications...") + logger.info(" Écriture des modifications...") try: client.Write() - logger.info("✅ Write() réussi !") + logger.info(" Write() réussi !") except Exception as e: error_detail = str(e) @@ -5809,7 +5973,7 @@ class SageConnector: except: pass - logger.error(f"❌ Erreur Write(): {error_detail}") + logger.error(f" Erreur Write(): {error_detail}") raise RuntimeError(f"Échec modification: {error_detail}") # ======================================== @@ -5818,7 +5982,7 @@ class SageConnector: client.Read() logger.info( - f"✅✅✅ CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs) ✅✅✅" + f" CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs) " ) # Refresh cache @@ -5826,11 +5990,11 @@ class SageConnector: return self._extraire_client(client) except ValueError as e: - logger.error(f"❌ Erreur métier: {e}") + logger.error(f" Erreur métier: {e}") raise except Exception as e: - logger.error(f"❌ Erreur modification client: {e}", exc_info=True) + logger.error(f" Erreur modification client: {e}", exc_info=True) error_message = str(e) if self.cial: @@ -5859,7 +6023,7 @@ class SageConnector: raise RuntimeError("Connexion Sage non établie") logger.info( - f"🚀 Début création commande pour client {commande_data['client']['code']}" + f" Début création commande pour client {commande_data['client']['code']}" ) try: @@ -5868,7 +6032,7 @@ class SageConnector: try: self.cial.CptaApplication.BeginTrans() transaction_active = True - logger.debug("✅ Transaction Sage démarrée") + logger.debug(" Transaction Sage démarrée") except: pass @@ -5884,7 +6048,7 @@ class SageConnector: except: pass - logger.info("📄 Document commande créé") + logger.info(" Document commande créé") # ===== DATES ===== doc.DO_Date = pywintypes.Time( @@ -5896,7 +6060,7 @@ class SageConnector: self.normaliser_date(commande_data["date_livraison"]) ) logger.info( - f"📅 Date livraison: {commande_data['date_livraison']}" + f" Date livraison: {commande_data['date_livraison']}" ) # ===== CLIENT (CRITIQUE) ===== @@ -5916,13 +6080,13 @@ class SageConnector: doc.SetDefaultClient(client_obj) doc.Write() - logger.info(f"👤 Client {commande_data['client']['code']} associé") + logger.info(f" Client {commande_data['client']['code']} associé") # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if commande_data.get("reference"): try: doc.DO_Ref = commande_data["reference"] - logger.info(f"📖 Référence: {commande_data['reference']}") + logger.info(f" Référence: {commande_data['reference']}") except: pass @@ -5934,7 +6098,7 @@ class SageConnector: factory_article = self.cial.FactoryArticle - logger.info(f"📦 Ajout de {len(commande_data['lignes'])} lignes...") + logger.info(f" Ajout de {len(commande_data['lignes'])} lignes...") for idx, ligne_data in enumerate(commande_data["lignes"], 1): logger.info( @@ -5948,7 +6112,7 @@ class SageConnector: if not persist_article: raise ValueError( - f"❌ Article {ligne_data['article_code']} introuvable dans Sage" + f" Article {ligne_data['article_code']} introuvable dans Sage" ) article_obj = win32com.client.CastTo( @@ -5956,15 +6120,15 @@ class SageConnector: ) article_obj.Read() - # 💰 ÉTAPE 2: Récupérer le prix de vente RÉEL + # ÉTAPE 2: Récupérer le prix de vente RÉEL prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") - logger.info(f"💰 Prix Sage: {prix_sage}€") + logger.info(f" Prix Sage: {prix_sage}€") - # ✅ TOLÉRER prix = 0 (articles de service, etc.) + # TOLÉRER prix = 0 (articles de service, etc.) if prix_sage == 0: logger.warning( - f"⚠️ Article {ligne_data['article_code']} a un prix = 0€ (toléré)" + f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) # 📍 ÉTAPE 3: Créer la ligne @@ -5979,7 +6143,7 @@ class SageConnector: ligne_persist, "IBODocumentVenteLigne3" ) - # ✅ SetDefaultArticleReference + # SetDefaultArticleReference quantite = float(ligne_data["quantite"]) try: @@ -5987,27 +6151,27 @@ class SageConnector: ligne_data["article_code"], quantite ) logger.info( - f"✅ Article associé via SetDefaultArticleReference" + f" Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( - f"⚠️ SetDefaultArticleReference échoué: {e}, tentative avec objet" + f"SetDefaultArticleReference échoué: {e}, tentative avec objet" ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(f"✅ Article associé via SetDefaultArticle") + logger.info(f" Article associé via SetDefaultArticle") except Exception as e2: - logger.error(f"❌ Toutes les méthodes ont échoué") + logger.error(f" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") ) ligne_obj.DL_Qte = quantite - logger.warning("⚠️ Configuration manuelle appliquée") + logger.warning("Configuration manuelle appliquée") - # ⚙️ ÉTAPE 4: Vérifier le prix automatique + # ÉTAPE 4: Vérifier le prix automatique prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - logger.info(f"💰 Prix auto chargé: {prix_auto}€") + logger.info(f" Prix auto chargé: {prix_auto}€") # 💵 ÉTAPE 5: Ajuster le prix si nécessaire prix_a_utiliser = ligne_data.get("prix_unitaire_ht") @@ -6015,21 +6179,21 @@ class SageConnector: if prix_a_utiliser is not None and prix_a_utiliser > 0: # Prix personnalisé fourni ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) - logger.info(f"💰 Prix personnalisé: {prix_a_utiliser}€") + logger.info(f" Prix personnalisé: {prix_a_utiliser}€") elif prix_auto == 0 and prix_sage > 0: # Pas de prix auto mais prix Sage existe ligne_obj.DL_PrixUnitaire = float(prix_sage) - logger.info(f"💰 Prix Sage forcé: {prix_sage}€") + logger.info(f" Prix Sage forcé: {prix_sage}€") elif prix_auto > 0: # Prix auto OK - logger.info(f"💰 Prix auto conservé: {prix_auto}€") - # ✅ SINON: Prix reste à 0 (toléré pour services, etc.) + logger.info(f" Prix auto conservé: {prix_auto}€") + # SINON: Prix reste à 0 (toléré pour services, etc.) prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) montant_ligne = quantite * prix_final - logger.info(f"✅ {quantite} x {prix_final}€ = {montant_ligne}€") + logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") - # 🎁 Remise + # Remise remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: @@ -6039,16 +6203,16 @@ class SageConnector: 1 - remise / 100 ) logger.info( - f"🎁 Remise {remise}% → {montant_apres_remise}€" + f" Remise {remise}% → {montant_apres_remise}€" ) except Exception as e: - logger.warning(f"⚠️ Remise non appliquée: {e}") + logger.warning(f"Remise non appliquée: {e}") - # 💾 ÉTAPE 6: Écrire la ligne + # ÉTAPE 6: Écrire la ligne ligne_obj.Write() - logger.info(f"✅ Ligne {idx} écrite") + logger.info(f" Ligne {idx} écrite") - # 🔍 VÉRIFICATION + # VÉRIFICATION try: ligne_obj.Read() prix_enregistre = float( @@ -6058,10 +6222,10 @@ class SageConnector: getattr(ligne_obj, "DL_MontantHT", 0.0) ) logger.info( - f"🔍 Vérif: Prix={prix_enregistre}€, Montant HT={montant_enregistre}€" + f" Vérif: Prix={prix_enregistre}€, Montant HT={montant_enregistre}€" ) except Exception as e: - logger.warning(f"⚠️ Impossible de vérifier: {e}") + logger.warning(f"Impossible de vérifier: {e}") # ===== VALIDATION ===== doc.Write() @@ -6119,10 +6283,10 @@ class SageConnector: date_livraison_final = commande_data.get("date_livraison") logger.info( - f"✅✅✅ COMMANDE CRÉÉE: {numero_commande} - {total_ttc}€ TTC ✅✅✅" + f" COMMANDE CRÉÉE: {numero_commande} - {total_ttc}€ TTC " ) if date_livraison_final: - logger.info(f"📅 Date livraison: {date_livraison_final}") + logger.info(f" Date livraison: {date_livraison_final}") return { "numero_commande": numero_commande, @@ -6146,7 +6310,7 @@ class SageConnector: raise except Exception as e: - logger.error(f"❌ Erreur création commande: {e}", exc_info=True) + logger.error(f" Erreur création commande: {e}", exc_info=True) raise RuntimeError(f"Échec création commande: {str(e)}") def modifier_commande(self, numero: str, commande_data: Dict) -> Dict: @@ -6167,12 +6331,12 @@ class SageConnector: try: with self._com_context(), self._lock_com: - logger.info(f"🔬 === MODIFICATION COMMANDE {numero} ===") + logger.info(f" === MODIFICATION COMMANDE {numero} ===") # ======================================== # ÉTAPE 1 : CHARGER LE DOCUMENT # ======================================== - logger.info("📂 Chargement document...") + logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None @@ -6183,13 +6347,13 @@ class SageConnector: persist_test = factory.ReadPiece(type_test, numero) if persist_test: persist = persist_test - logger.info(f" ✅ Document trouvé (type={type_test})") + logger.info(f" Document trouvé (type={type_test})") break except: continue if not persist: - raise ValueError(f"❌ Commande {numero} INTROUVABLE") + raise ValueError(f" Commande {numero} INTROUVABLE") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() @@ -6197,7 +6361,7 @@ class SageConnector: statut_actuel = getattr(doc, "DO_Statut", 0) type_reel = getattr(doc, "DO_Type", -1) - logger.info(f" 📊 Type={type_reel}, Statut={statut_actuel}") + logger.info(f" Type={type_reel}, Statut={statut_actuel}") # ======================================== # ÉTAPE 2 : VÉRIFIER CLIENT INITIAL @@ -6208,14 +6372,14 @@ class SageConnector: if client_obj: client_obj.Read() client_code_initial = getattr(client_obj, "CT_Num", "").strip() - logger.info(f" 👤 Client initial: {client_code_initial}") + logger.info(f" Client initial: {client_code_initial}") else: - logger.error(" ❌ Objet Client NULL à l'état initial !") + logger.error(" Objet Client NULL à l'état initial !") except Exception as e: - logger.error(f" ❌ Erreur lecture client initial: {e}") + logger.error(f" Erreur lecture client initial: {e}") if not client_code_initial: - raise ValueError("❌ Client introuvable dans le document") + raise ValueError(" Client introuvable dans le document") # Compter les lignes initiales nb_lignes_initial = 0 @@ -6234,9 +6398,9 @@ class SageConnector: except: break - logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") + logger.info(f" Lignes initiales: {nb_lignes_initial}") except Exception as e: - logger.warning(f" ⚠️ Erreur comptage lignes: {e}") + logger.warning(f" Erreur comptage lignes: {e}") # ======================================== # ÉTAPE 3 : DÉTERMINER LES MODIFICATIONS @@ -6251,7 +6415,7 @@ class SageConnector: "lignes" in commande_data and commande_data["lignes"] is not None ) - logger.info(f"📋 Modifications demandées:") + logger.info(f"Modifications demandées:") logger.info(f" Date commande: {modif_date}") logger.info(f" Date livraison: {modif_date_livraison}") logger.info(f" Statut: {modif_statut}") @@ -6267,25 +6431,25 @@ class SageConnector: if modif_ref: reference_a_modifier = commande_data_temp.pop("reference") logger.info( - "🔖 Modification de la référence reportée après les lignes" + " Modification de la référence reportée après les lignes" ) modif_ref = False if modif_statut: statut_a_modifier = commande_data_temp.pop("statut") logger.info( - "📊 Modification du statut reportée après les lignes" + " Modification du statut reportée après les lignes" ) modif_statut = False # ======================================== # ÉTAPE 4 : TEST WRITE() BASIQUE # ======================================== - logger.info("🧪 Test Write() basique (sans modification)...") + logger.info(" Test Write() basique (sans modification)...") try: doc.Write() - logger.info(" ✅ Write() basique OK") + logger.info(" Write() basique OK") doc.Read() # Vérifier que le client est toujours là @@ -6294,17 +6458,17 @@ class SageConnector: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: - logger.info(f" ✅ Client préservé: {client_apres}") + logger.info(f" Client préservé: {client_apres}") else: logger.error( - f" ❌ Client a changé: {client_code_initial} → {client_apres}" + f" Client a changé: {client_code_initial} → {client_apres}" ) else: - logger.error(" ❌ Client devenu NULL après Write() basique") + logger.error(" Client devenu NULL après Write() basique") except Exception as e: - logger.error(f" ❌ Write() basique ÉCHOUE: {e}") - logger.error(f" ❌ ABANDON: Le document est VERROUILLÉ") + logger.error(f" Write() basique ÉCHOUE: {e}") + logger.error(f" ABANDON: Le document est VERROUILLÉ") raise ValueError( f"Document verrouillé, impossible de modifier: {e}" ) @@ -6318,10 +6482,10 @@ class SageConnector: or modif_statut or modif_ref ): - logger.info("🎯 Modifications simples (sans lignes)...") + logger.info(" Modifications simples (sans lignes)...") if modif_date: - logger.info(" 📅 Modification date commande...") + logger.info(" Modification date commande...") doc.DO_Date = pywintypes.Time( self.normaliser_date( commande_data_temp.get("date_commande") @@ -6330,38 +6494,38 @@ class SageConnector: champs_modifies.append("date") if modif_date_livraison: - logger.info(" 📅 Modification date livraison...") + logger.info(" Modification date livraison...") doc.DO_DateLivr = pywintypes.Time( self.normaliser_date(commande_data_temp["date_livraison"]) ) logger.info( - f" ✅ Date livraison: {commande_data_temp['date_livraison']}" + f" Date livraison: {commande_data_temp['date_livraison']}" ) champs_modifies.append("date_livraison") if modif_statut: - logger.info(" 📊 Modification statut...") + logger.info(" Modification statut...") nouveau_statut = commande_data_temp["statut"] doc.DO_Statut = nouveau_statut champs_modifies.append("statut") - logger.info(f" ✅ Statut défini: {nouveau_statut}") + logger.info(f" Statut défini: {nouveau_statut}") if modif_ref: - logger.info(" 📖 Modification référence...") + logger.info(" Modification référence...") try: doc.DO_Ref = commande_data_temp["reference"] champs_modifies.append("reference") logger.info( - f" ✅ Référence définie: {commande_data_temp['reference']}" + f" Référence définie: {commande_data_temp['reference']}" ) except Exception as e: - logger.warning(f" ⚠️ Référence non définie: {e}") + logger.warning(f" Référence non définie: {e}") # Écrire sans réassocier le client - logger.info(" 💾 Write() sans réassociation client...") + logger.info(" Write() sans réassociation client...") try: doc.Write() - logger.info(" ✅ Write() réussi") + logger.info(" Write() réussi") doc.Read() @@ -6371,10 +6535,10 @@ class SageConnector: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: - logger.info(f" ✅ Client préservé: {client_apres}") + logger.info(f" Client préservé: {client_apres}") else: logger.error( - f" ❌ Client perdu: {client_code_initial} → {client_apres}" + f" Client perdu: {client_code_initial} → {client_apres}" ) except Exception as e: @@ -6386,14 +6550,14 @@ class SageConnector: except: pass - logger.error(f" ❌ Write() échoue: {error_msg}") + logger.error(f" Write() échoue: {error_msg}") raise ValueError(f"Sage refuse: {error_msg}") # ======================================== # ÉTAPE 6 : REMPLACEMENT COMPLET DES LIGNES # ======================================== elif modif_lignes: - logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") + logger.info(" REMPLACEMENT COMPLET DES LIGNES...") # D'abord modifier les dates si demandées if modif_date: @@ -6403,20 +6567,20 @@ class SageConnector: ) ) champs_modifies.append("date") - logger.info(" 📅 Date commande modifiée") + logger.info(" Date commande modifiée") if modif_date_livraison: doc.DO_DateLivr = pywintypes.Time( self.normaliser_date(commande_data_temp["date_livraison"]) ) - logger.info(" 📅 Date livraison modifiée") + logger.info(" Date livraison modifiée") champs_modifies.append("date_livraison") nouvelles_lignes = commande_data["lignes"] nb_nouvelles = len(nouvelles_lignes) logger.info( - f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" + f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" ) try: @@ -6431,7 +6595,7 @@ class SageConnector: # ============================================ if nb_lignes_initial > 0: logger.info( - f" 🗑️ Suppression de {nb_lignes_initial} lignes existantes..." + f" Suppression de {nb_lignes_initial} lignes existantes..." ) # Supprimer depuis la fin pour éviter les problèmes d'index @@ -6444,21 +6608,21 @@ class SageConnector: ) ligne.Read() - # ✅ Utiliser .Remove() + # Utiliser .Remove() ligne.Remove() - logger.debug(f" ✅ Ligne {idx} supprimée") + logger.debug(f" Ligne {idx} supprimée") except Exception as e: logger.warning( - f" ⚠️ Impossible de supprimer ligne {idx}: {e}" + f" Impossible de supprimer ligne {idx}: {e}" ) # Continuer même si une suppression échoue - logger.info(" ✅ Toutes les lignes existantes supprimées") + logger.info(" Toutes les lignes existantes supprimées") # ============================================ # SOUS-ÉTAPE 2 : AJOUTER LES NOUVELLES LIGNES # ============================================ - logger.info(f" ➕ Ajout de {nb_nouvelles} nouvelles lignes...") + logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") for idx, ligne_data in enumerate(nouvelles_lignes, 1): logger.info( @@ -6523,14 +6687,14 @@ class SageConnector: # Écrire la ligne ligne_obj.Write() - logger.info(f" ✅ Ligne {idx} ajoutée") + logger.info(f" Ligne {idx} ajoutée") - logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées") # Écrire le document - logger.info(" 💾 Write() document après remplacement lignes...") + logger.info(" Write() document après remplacement lignes...") doc.Write() - logger.info(" ✅ Document écrit") + logger.info(" Document écrit") import time @@ -6543,9 +6707,9 @@ class SageConnector: if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") - logger.info(f" 👤 Client après remplacement: {client_apres}") + logger.info(f" Client après remplacement: {client_apres}") else: - logger.error(" ❌ Client NULL après remplacement") + logger.error(" Client NULL après remplacement") champs_modifies.append("lignes") @@ -6570,10 +6734,10 @@ class SageConnector: champs_modifies.append("reference") logger.info( - f"🔖 Référence modifiée: '{ancienne_reference}' → '{nouvelle_reference}'" + f" Référence modifiée: '{ancienne_reference}' → '{nouvelle_reference}'" ) except Exception as e: - logger.warning(f"⚠️ Impossible de modifier la référence: {e}") + logger.warning(f"Impossible de modifier la référence: {e}") # ======================================== # ÉTAPE 6.6 : MODIFIER STATUT (EN DERNIER) @@ -6595,15 +6759,15 @@ class SageConnector: champs_modifies.append("statut") logger.info( - f"📊 Statut modifié: {statut_actuel} → {nouveau_statut}" + f" Statut modifié: {statut_actuel} → {nouveau_statut}" ) except Exception as e: - logger.warning(f"⚠️ Impossible de modifier le statut: {e}") + logger.warning(f"Impossible de modifier le statut: {e}") # ======================================== # ÉTAPE 7 : RELECTURE ET RETOUR # ======================================== - logger.info("📊 Relecture finale...") + logger.info(" Relecture finale...") import time @@ -6632,13 +6796,13 @@ class SageConnector: except: pass - logger.info(f"✅ ✅ ✅ SUCCÈS: {numero} modifiée ✅ ✅ ✅") - logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") - logger.info(f" 👤 Client final: {client_final}") - logger.info(f" 🔖 Référence: {reference_finale}") + logger.info(f" SUCCÈS: {numero} modifiée ") + logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + logger.info(f" Client final: {client_final}") + logger.info(f" Référence: {reference_finale}") if date_livraison_final: - logger.info(f" 📅 Date livraison: {date_livraison_final}") - logger.info(f" 📝 Champs modifiés: {champs_modifies}") + logger.info(f" Date livraison: {date_livraison_final}") + logger.info(f" Champs modifiés: {champs_modifies}") return { "numero": numero, @@ -6652,11 +6816,11 @@ class SageConnector: } except ValueError as e: - logger.error(f"❌ ERREUR MÉTIER: {e}") + logger.error(f" ERREUR MÉTIER: {e}") raise except Exception as e: - logger.error(f"❌ ERREUR TECHNIQUE: {e}", exc_info=True) + logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True) error_message = str(e) if self.cial: @@ -6687,7 +6851,7 @@ class SageConnector: raise RuntimeError("Connexion Sage non établie") logger.info( - f"🚀 Début création livraison pour client {livraison_data['client']['code']}" + f" Début création livraison pour client {livraison_data['client']['code']}" ) try: @@ -6696,7 +6860,7 @@ class SageConnector: try: self.cial.CptaApplication.BeginTrans() transaction_active = True - logger.debug("✅ Transaction Sage démarrée") + logger.debug(" Transaction Sage démarrée") except: pass @@ -6712,7 +6876,7 @@ class SageConnector: except: pass - logger.info("📄 Document livraison créé") + logger.info(" Document livraison créé") # ===== DATES ===== # Date du document (DO_Date) @@ -6731,7 +6895,7 @@ class SageConnector: ) ) logger.info( - f"📅 Date livraison prévue: {livraison_data['date_livraison_prevue']}" + f" Date livraison prévue: {livraison_data['date_livraison_prevue']}" ) # ===== CLIENT (CRITIQUE) ===== @@ -6751,13 +6915,13 @@ class SageConnector: doc.SetDefaultClient(client_obj) doc.Write() - logger.info(f"👤 Client {livraison_data['client']['code']} associé") + logger.info(f" Client {livraison_data['client']['code']} associé") # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if livraison_data.get("reference"): try: doc.DO_Ref = livraison_data["reference"] - logger.info(f"📖 Référence: {livraison_data['reference']}") + logger.info(f" Référence: {livraison_data['reference']}") except: pass @@ -6770,7 +6934,7 @@ class SageConnector: factory_article = self.cial.FactoryArticle logger.info( - f"📦 Ajout de {len(livraison_data['lignes'])} lignes..." + f" Ajout de {len(livraison_data['lignes'])} lignes..." ) for idx, ligne_data in enumerate(livraison_data["lignes"], 1): @@ -6785,7 +6949,7 @@ class SageConnector: if not persist_article: raise ValueError( - f"❌ Article {ligne_data['article_code']} introuvable dans Sage" + f" Article {ligne_data['article_code']} introuvable dans Sage" ) article_obj = win32com.client.CastTo( @@ -6796,11 +6960,11 @@ class SageConnector: # Récupérer le prix de vente RÉEL prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") - logger.info(f"💰 Prix Sage: {prix_sage}€") + logger.info(f" Prix Sage: {prix_sage}€") if prix_sage == 0: logger.warning( - f"⚠️ Article {ligne_data['article_code']} a un prix = 0€ (toléré)" + f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) # Créer la ligne @@ -6822,43 +6986,43 @@ class SageConnector: ligne_data["article_code"], quantite ) logger.info( - f"✅ Article associé via SetDefaultArticleReference" + f" Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( - f"⚠️ SetDefaultArticleReference échoué: {e}, tentative avec objet" + f"SetDefaultArticleReference échoué: {e}, tentative avec objet" ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(f"✅ Article associé via SetDefaultArticle") + logger.info(f" Article associé via SetDefaultArticle") except Exception as e2: - logger.error(f"❌ Toutes les méthodes ont échoué") + logger.error(f" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") ) ligne_obj.DL_Qte = quantite - logger.warning("⚠️ Configuration manuelle appliquée") + logger.warning("Configuration manuelle appliquée") # Vérifier le prix automatique prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - logger.info(f"💰 Prix auto chargé: {prix_auto}€") + logger.info(f" Prix auto chargé: {prix_auto}€") # Ajuster le prix si nécessaire prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) - logger.info(f"💰 Prix personnalisé: {prix_a_utiliser}€") + logger.info(f" Prix personnalisé: {prix_a_utiliser}€") elif prix_auto == 0 and prix_sage > 0: ligne_obj.DL_PrixUnitaire = float(prix_sage) - logger.info(f"💰 Prix Sage forcé: {prix_sage}€") + logger.info(f" Prix Sage forcé: {prix_sage}€") elif prix_auto > 0: - logger.info(f"💰 Prix auto conservé: {prix_auto}€") + logger.info(f" Prix auto conservé: {prix_auto}€") prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) montant_ligne = quantite * prix_final - logger.info(f"✅ {quantite} x {prix_final}€ = {montant_ligne}€") + logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") # Remise remise = ligne_data.get("remise_pourcentage", 0) @@ -6870,14 +7034,14 @@ class SageConnector: 1 - remise / 100 ) logger.info( - f"🎁 Remise {remise}% → {montant_apres_remise}€" + f" Remise {remise}% → {montant_apres_remise}€" ) except Exception as e: - logger.warning(f"⚠️ Remise non appliquée: {e}") + logger.warning(f"Remise non appliquée: {e}") # Écrire la ligne ligne_obj.Write() - logger.info(f"✅ Ligne {idx} écrite") + logger.info(f" Ligne {idx} écrite") # ===== VALIDATION ===== doc.Write() @@ -6940,11 +7104,11 @@ class SageConnector: ) logger.info( - f"✅✅✅ LIVRAISON CRÉÉE: {numero_livraison} - {total_ttc}€ TTC ✅✅✅" + f" LIVRAISON CRÉÉE: {numero_livraison} - {total_ttc}€ TTC " ) if date_livraison_prevue_final: logger.info( - f"📅 Date livraison prévue: {date_livraison_prevue_final}" + f" Date livraison prévue: {date_livraison_prevue_final}" ) return { @@ -6969,7 +7133,7 @@ class SageConnector: raise except Exception as e: - logger.error(f"❌ Erreur création livraison: {e}", exc_info=True) + logger.error(f" Erreur création livraison: {e}", exc_info=True) raise RuntimeError(f"Échec création livraison: {str(e)}") def modifier_livraison(self, numero: str, livraison_data: Dict) -> Dict: @@ -6990,12 +7154,12 @@ class SageConnector: try: with self._com_context(), self._lock_com: - logger.info(f"🔬 === MODIFICATION LIVRAISON {numero} ===") + logger.info(f" === MODIFICATION LIVRAISON {numero} ===") # ======================================== # ÉTAPE 1 : CHARGER LE DOCUMENT # ======================================== - logger.info("📂 Chargement document...") + logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None @@ -7006,20 +7170,20 @@ class SageConnector: persist_test = factory.ReadPiece(type_test, numero) if persist_test: persist = persist_test - logger.info(f" ✅ Document trouvé (type={type_test})") + logger.info(f" Document trouvé (type={type_test})") break except: continue if not persist: - raise ValueError(f"❌ Livraison {numero} INTROUVABLE") + raise ValueError(f" Livraison {numero} INTROUVABLE") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() statut_actuel = getattr(doc, "DO_Statut", 0) - logger.info(f" 📊 Statut={statut_actuel}") + logger.info(f" Statut={statut_actuel}") # Vérifier qu'elle n'est pas transformée if statut_actuel == 5: @@ -7045,9 +7209,9 @@ class SageConnector: except: break - logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") + logger.info(f" Lignes initiales: {nb_lignes_initial}") except Exception as e: - logger.warning(f" ⚠️ Erreur comptage lignes: {e}") + logger.warning(f" Erreur comptage lignes: {e}") # ======================================== # ÉTAPE 2 : DÉTERMINER LES MODIFICATIONS @@ -7062,7 +7226,7 @@ class SageConnector: "lignes" in livraison_data and livraison_data["lignes"] is not None ) - logger.info(f"📋 Modifications demandées:") + logger.info(f"Modifications demandées:") logger.info(f" Date livraison: {modif_date}") logger.info(f" Date livraison prévue: {modif_date_livraison_prevue}") logger.info(f" Statut: {modif_statut}") @@ -7078,14 +7242,14 @@ class SageConnector: if modif_ref: reference_a_modifier = livraison_data_temp.pop("reference") logger.info( - "🔖 Modification de la référence reportée après les lignes" + " Modification de la référence reportée après les lignes" ) modif_ref = False if modif_statut: statut_a_modifier = livraison_data_temp.pop("statut") logger.info( - "📊 Modification du statut reportée après les lignes" + " Modification du statut reportée après les lignes" ) modif_statut = False @@ -7098,10 +7262,10 @@ class SageConnector: or modif_statut or modif_ref ): - logger.info("🎯 Modifications simples (sans lignes)...") + logger.info(" Modifications simples (sans lignes)...") if modif_date: - logger.info(" 📅 Modification date livraison...") + logger.info(" Modification date livraison...") doc.DO_Date = pywintypes.Time( self.normaliser_date( livraison_data_temp.get("date_livraison") @@ -7110,44 +7274,44 @@ class SageConnector: champs_modifies.append("date") if modif_date_livraison_prevue: - logger.info(" 📅 Modification date livraison prévue...") + logger.info(" Modification date livraison prévue...") doc.DO_DateLivr = pywintypes.Time( self.normaliser_date( livraison_data_temp["date_livraison_prevue"] ) ) logger.info( - f" ✅ Date livraison prévue: {livraison_data_temp['date_livraison_prevue']}" + f" Date livraison prévue: {livraison_data_temp['date_livraison_prevue']}" ) champs_modifies.append("date_livraison_prevue") if modif_statut: - logger.info(" 📊 Modification statut...") + logger.info(" Modification statut...") nouveau_statut = livraison_data_temp["statut"] doc.DO_Statut = nouveau_statut champs_modifies.append("statut") - logger.info(f" ✅ Statut défini: {nouveau_statut}") + logger.info(f" Statut défini: {nouveau_statut}") if modif_ref: - logger.info(" 📖 Modification référence...") + logger.info(" Modification référence...") try: doc.DO_Ref = livraison_data_temp["reference"] champs_modifies.append("reference") logger.info( - f" ✅ Référence définie: {livraison_data_temp['reference']}" + f" Référence définie: {livraison_data_temp['reference']}" ) except Exception as e: - logger.warning(f" ⚠️ Référence non définie: {e}") + logger.warning(f" Référence non définie: {e}") - logger.info(" 💾 Write()...") + logger.info(" Write()...") doc.Write() - logger.info(" ✅ Write() réussi") + logger.info(" Write() réussi") # ======================================== # ÉTAPE 4 : REMPLACEMENT COMPLET LIGNES # ======================================== elif modif_lignes: - logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") + logger.info(" REMPLACEMENT COMPLET DES LIGNES...") # D'abord modifier les dates si demandées if modif_date: @@ -7157,7 +7321,7 @@ class SageConnector: ) ) champs_modifies.append("date") - logger.info(" 📅 Date livraison modifiée") + logger.info(" Date livraison modifiée") if modif_date_livraison_prevue: doc.DO_DateLivr = pywintypes.Time( @@ -7165,14 +7329,14 @@ class SageConnector: livraison_data_temp["date_livraison_prevue"] ) ) - logger.info(" 📅 Date livraison prévue modifiée") + logger.info(" Date livraison prévue modifiée") champs_modifies.append("date_livraison_prevue") nouvelles_lignes = livraison_data["lignes"] nb_nouvelles = len(nouvelles_lignes) logger.info( - f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles" + f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles" ) try: @@ -7186,7 +7350,7 @@ class SageConnector: # SOUS-ÉTAPE 1 : SUPPRESSION TOUTES LES LIGNES # ============================================ if nb_lignes_initial > 0: - logger.info(f" 🗑️ Suppression de {nb_lignes_initial} lignes...") + logger.info(f" Suppression de {nb_lignes_initial} lignes...") # Supprimer depuis la fin pour éviter les problèmes d'index for idx in range(nb_lignes_initial, 0, -1): @@ -7198,19 +7362,19 @@ class SageConnector: ) ligne.Read() ligne.Remove() - logger.debug(f" ✅ Ligne {idx} supprimée") + logger.debug(f" Ligne {idx} supprimée") except Exception as e: logger.warning( - f" ⚠️ Erreur suppression ligne {idx}: {e}" + f" Erreur suppression ligne {idx}: {e}" ) # Continuer même si une suppression échoue - logger.info(" ✅ Toutes les lignes supprimées") + logger.info(" Toutes les lignes supprimées") # ============================================ # SOUS-ÉTAPE 2 : AJOUT NOUVELLES LIGNES # ============================================ - logger.info(f" ➕ Ajout de {nb_nouvelles} nouvelles lignes...") + logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") for idx, ligne_data in enumerate(nouvelles_lignes, 1): logger.info( @@ -7275,14 +7439,14 @@ class SageConnector: # Écrire la ligne ligne_obj.Write() - logger.info(f" ✅ Ligne {idx} ajoutée") + logger.info(f" Ligne {idx} ajoutée") - logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées") # Écrire le document - logger.info(" 💾 Write() document après remplacement lignes...") + logger.info(" Write() document après remplacement lignes...") doc.Write() - logger.info(" ✅ Document écrit") + logger.info(" Document écrit") import time @@ -7303,7 +7467,7 @@ class SageConnector: ) logger.info( - f"🔖 Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'" + f" Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'" ) doc.DO_Ref = nouvelle_reference @@ -7316,9 +7480,9 @@ class SageConnector: doc.Read() champs_modifies.append("reference") - logger.info(f" ✅ Référence modifiée avec succès") + logger.info(f" Référence modifiée avec succès") except Exception as e: - logger.warning(f"⚠️ Impossible de modifier la référence: {e}") + logger.warning(f"Impossible de modifier la référence: {e}") # ======================================== # ÉTAPE 4.6 : MODIFIER STATUT (EN DERNIER) @@ -7330,7 +7494,7 @@ class SageConnector: if nouveau_statut != statut_actuel: logger.info( - f"📊 Modification statut: {statut_actuel} → {nouveau_statut}" + f" Modification statut: {statut_actuel} → {nouveau_statut}" ) doc.DO_Statut = nouveau_statut @@ -7343,14 +7507,14 @@ class SageConnector: doc.Read() champs_modifies.append("statut") - logger.info(f" ✅ Statut modifié avec succès") + logger.info(f" Statut modifié avec succès") except Exception as e: - logger.warning(f"⚠️ Impossible de modifier le statut: {e}") + logger.warning(f"Impossible de modifier le statut: {e}") # ======================================== # ÉTAPE 5 : RELECTURE ET RETOUR # ======================================== - logger.info("📊 Relecture finale...") + logger.info(" Relecture finale...") import time @@ -7373,16 +7537,16 @@ class SageConnector: except: pass - logger.info(f"✅ ✅ ✅ SUCCÈS: {numero} modifiée ✅ ✅ ✅") - logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") - logger.info(f" 🔖 Référence: {reference_finale}") - logger.info(f" 📊 Statut: {statut_final}") + logger.info(f" SUCCÈS: {numero} modifiée ") + logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + logger.info(f" Référence: {reference_finale}") + logger.info(f" Statut: {statut_final}") if date_livraison_prevue_final: logger.info( - f" 📅 Date livraison prévue: {date_livraison_prevue_final}" + f" Date livraison prévue: {date_livraison_prevue_final}" ) - logger.info(f" 📝 Champs modifiés: {champs_modifies}") + logger.info(f" Champs modifiés: {champs_modifies}") return { "numero": numero, @@ -7395,11 +7559,11 @@ class SageConnector: } except ValueError as e: - logger.error(f"❌ ERREUR MÉTIER: {e}") + logger.error(f" ERREUR MÉTIER: {e}") raise except Exception as e: - logger.error(f"❌ ERREUR TECHNIQUE: {e}", exc_info=True) + logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True) error_message = str(e) if self.cial: @@ -7430,7 +7594,7 @@ class SageConnector: raise RuntimeError("Connexion Sage non établie") logger.info( - f"🚀 Début création avoir pour client {avoir_data['client']['code']}" + f" Début création avoir pour client {avoir_data['client']['code']}" ) try: @@ -7439,7 +7603,7 @@ class SageConnector: try: self.cial.CptaApplication.BeginTrans() transaction_active = True - logger.debug("✅ Transaction Sage démarrée") + logger.debug(" Transaction Sage démarrée") except: pass @@ -7455,7 +7619,7 @@ class SageConnector: except: pass - logger.info("📄 Document avoir créé") + logger.info(" Document avoir créé") # ===== DATES ===== doc.DO_Date = pywintypes.Time( @@ -7467,7 +7631,7 @@ class SageConnector: self.normaliser_date(avoir_data["date_livraison"]) ) logger.info( - f"📅 Date livraison: {avoir_data['date_livraison']}" + f" Date livraison: {avoir_data['date_livraison']}" ) # ===== CLIENT (CRITIQUE) ===== @@ -7487,13 +7651,13 @@ class SageConnector: doc.SetDefaultClient(client_obj) doc.Write() - logger.info(f"👤 Client {avoir_data['client']['code']} associé") + logger.info(f" Client {avoir_data['client']['code']} associé") # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if avoir_data.get("reference"): try: doc.DO_Ref = avoir_data["reference"] - logger.info(f"📖 Référence: {avoir_data['reference']}") + logger.info(f" Référence: {avoir_data['reference']}") except: pass @@ -7505,7 +7669,7 @@ class SageConnector: factory_article = self.cial.FactoryArticle - logger.info(f"📦 Ajout de {len(avoir_data['lignes'])} lignes...") + logger.info(f" Ajout de {len(avoir_data['lignes'])} lignes...") for idx, ligne_data in enumerate(avoir_data["lignes"], 1): logger.info( @@ -7519,7 +7683,7 @@ class SageConnector: if not persist_article: raise ValueError( - f"❌ Article {ligne_data['article_code']} introuvable dans Sage" + f" Article {ligne_data['article_code']} introuvable dans Sage" ) article_obj = win32com.client.CastTo( @@ -7530,11 +7694,11 @@ class SageConnector: # Récupérer le prix de vente RÉEL prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") - logger.info(f"💰 Prix Sage: {prix_sage}€") + logger.info(f" Prix Sage: {prix_sage}€") if prix_sage == 0: logger.warning( - f"⚠️ Article {ligne_data['article_code']} a un prix = 0€ (toléré)" + f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) # Créer la ligne @@ -7556,43 +7720,43 @@ class SageConnector: ligne_data["article_code"], quantite ) logger.info( - f"✅ Article associé via SetDefaultArticleReference" + f" Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( - f"⚠️ SetDefaultArticleReference échoué: {e}, tentative avec objet" + f"SetDefaultArticleReference échoué: {e}, tentative avec objet" ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(f"✅ Article associé via SetDefaultArticle") + logger.info(f" Article associé via SetDefaultArticle") except Exception as e2: - logger.error(f"❌ Toutes les méthodes ont échoué") + logger.error(f" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") ) ligne_obj.DL_Qte = quantite - logger.warning("⚠️ Configuration manuelle appliquée") + logger.warning("Configuration manuelle appliquée") # Vérifier le prix automatique prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - logger.info(f"💰 Prix auto chargé: {prix_auto}€") + logger.info(f" Prix auto chargé: {prix_auto}€") # Ajuster le prix si nécessaire prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) - logger.info(f"💰 Prix personnalisé: {prix_a_utiliser}€") + logger.info(f" Prix personnalisé: {prix_a_utiliser}€") elif prix_auto == 0 and prix_sage > 0: ligne_obj.DL_PrixUnitaire = float(prix_sage) - logger.info(f"💰 Prix Sage forcé: {prix_sage}€") + logger.info(f" Prix Sage forcé: {prix_sage}€") elif prix_auto > 0: - logger.info(f"💰 Prix auto conservé: {prix_auto}€") + logger.info(f" Prix auto conservé: {prix_auto}€") prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) montant_ligne = quantite * prix_final - logger.info(f"✅ {quantite} x {prix_final}€ = {montant_ligne}€") + logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") # Remise remise = ligne_data.get("remise_pourcentage", 0) @@ -7604,14 +7768,14 @@ class SageConnector: 1 - remise / 100 ) logger.info( - f"🎁 Remise {remise}% → {montant_apres_remise}€" + f" Remise {remise}% → {montant_apres_remise}€" ) except Exception as e: - logger.warning(f"⚠️ Remise non appliquée: {e}") + logger.warning(f"Remise non appliquée: {e}") # Écrire la ligne ligne_obj.Write() - logger.info(f"✅ Ligne {idx} écrite") + logger.info(f" Ligne {idx} écrite") # ===== VALIDATION ===== doc.Write() @@ -7670,11 +7834,11 @@ class SageConnector: date_livraison_final = avoir_data.get("date_livraison") logger.info( - f"✅✅✅ AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC ✅✅✅" + f" AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC " ) if date_livraison_final: - logger.info(f"📅 Date livraison: {date_livraison_final}") + logger.info(f" Date livraison: {date_livraison_final}") return { "numero_avoir": numero_avoir, @@ -7698,7 +7862,7 @@ class SageConnector: raise except Exception as e: - logger.error(f"❌ Erreur création avoir: {e}", exc_info=True) + logger.error(f" Erreur création avoir: {e}", exc_info=True) raise RuntimeError(f"Échec création avoir: {str(e)}") def modifier_avoir(self, numero: str, avoir_data: Dict) -> Dict: @@ -7719,12 +7883,12 @@ class SageConnector: try: with self._com_context(), self._lock_com: - logger.info(f"🔬 === MODIFICATION AVOIR {numero} ===") + logger.info(f" === MODIFICATION AVOIR {numero} ===") # ======================================== # ÉTAPE 1 : CHARGER LE DOCUMENT # ======================================== - logger.info("📂 Chargement document...") + logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None @@ -7735,13 +7899,13 @@ class SageConnector: persist_test = factory.ReadPiece(type_test, numero) if persist_test: persist = persist_test - logger.info(f" ✅ Document trouvé (type={type_test})") + logger.info(f" Document trouvé (type={type_test})") break except: continue if not persist: - raise ValueError(f"❌ Avoir {numero} INTROUVABLE") + raise ValueError(f" Avoir {numero} INTROUVABLE") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() @@ -7749,7 +7913,7 @@ class SageConnector: statut_actuel = getattr(doc, "DO_Statut", 0) type_reel = getattr(doc, "DO_Type", -1) - logger.info(f" 📊 Type={type_reel}, Statut={statut_actuel}") + logger.info(f" Type={type_reel}, Statut={statut_actuel}") # Vérifier qu'il n'est pas transformé ou annulé if statut_actuel == 5: @@ -7767,14 +7931,14 @@ class SageConnector: if client_obj: client_obj.Read() client_code_initial = getattr(client_obj, "CT_Num", "").strip() - logger.info(f" 👤 Client initial: {client_code_initial}") + logger.info(f" Client initial: {client_code_initial}") else: - logger.error(" ❌ Objet Client NULL à l'état initial !") + logger.error(" Objet Client NULL à l'état initial !") except Exception as e: - logger.error(f" ❌ Erreur lecture client initial: {e}") + logger.error(f" Erreur lecture client initial: {e}") if not client_code_initial: - raise ValueError("❌ Client introuvable dans le document") + raise ValueError(" Client introuvable dans le document") # Compter les lignes initiales nb_lignes_initial = 0 @@ -7793,9 +7957,9 @@ class SageConnector: except: break - logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") + logger.info(f" Lignes initiales: {nb_lignes_initial}") except Exception as e: - logger.warning(f" ⚠️ Erreur comptage lignes: {e}") + logger.warning(f" Erreur comptage lignes: {e}") # ======================================== # ÉTAPE 3 : DÉTERMINER LES MODIFICATIONS @@ -7810,7 +7974,7 @@ class SageConnector: "lignes" in avoir_data and avoir_data["lignes"] is not None ) - logger.info(f"📋 Modifications demandées:") + logger.info(f"Modifications demandées:") logger.info(f" Date avoir: {modif_date}") logger.info(f" Date livraison: {modif_date_livraison}") logger.info(f" Statut: {modif_statut}") @@ -7826,25 +7990,25 @@ class SageConnector: if modif_ref: reference_a_modifier = avoir_data_temp.pop("reference") logger.info( - "🔖 Modification de la référence reportée après les lignes" + " Modification de la référence reportée après les lignes" ) modif_ref = False if modif_statut: statut_a_modifier = avoir_data_temp.pop("statut") logger.info( - "📊 Modification du statut reportée après les lignes" + " Modification du statut reportée après les lignes" ) modif_statut = False # ======================================== # ÉTAPE 4 : TEST WRITE() BASIQUE # ======================================== - logger.info("🧪 Test Write() basique (sans modification)...") + logger.info(" Test Write() basique (sans modification)...") try: doc.Write() - logger.info(" ✅ Write() basique OK") + logger.info(" Write() basique OK") doc.Read() # Vérifier que le client est toujours là @@ -7853,17 +8017,17 @@ class SageConnector: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: - logger.info(f" ✅ Client préservé: {client_apres}") + logger.info(f" Client préservé: {client_apres}") else: logger.error( - f" ❌ Client a changé: {client_code_initial} → {client_apres}" + f" Client a changé: {client_code_initial} → {client_apres}" ) else: - logger.error(" ❌ Client devenu NULL après Write() basique") + logger.error(" Client devenu NULL après Write() basique") except Exception as e: - logger.error(f" ❌ Write() basique ÉCHOUE: {e}") - logger.error(f" ❌ ABANDON: Le document est VERROUILLÉ") + logger.error(f" Write() basique ÉCHOUE: {e}") + logger.error(f" ABANDON: Le document est VERROUILLÉ") raise ValueError( f"Document verrouillé, impossible de modifier: {e}" ) @@ -7877,48 +8041,48 @@ class SageConnector: or modif_statut or modif_ref ): - logger.info("🎯 Modifications simples (sans lignes)...") + logger.info(" Modifications simples (sans lignes)...") if modif_date: - logger.info(" 📅 Modification date avoir...") + logger.info(" Modification date avoir...") doc.DO_Date = pywintypes.Time( self.normaliser_date(avoir_data_temp.get("date_avoir")) ) champs_modifies.append("date") if modif_date_livraison: - logger.info(" 📅 Modification date livraison...") + logger.info(" Modification date livraison...") doc.DO_DateLivr = pywintypes.Time( self.normaliser_date(avoir_data_temp["date_livraison"]) ) logger.info( - f" ✅ Date livraison: {avoir_data_temp['date_livraison']}" + f" Date livraison: {avoir_data_temp['date_livraison']}" ) champs_modifies.append("date_livraison") if modif_statut: - logger.info(" 📊 Modification statut...") + logger.info(" Modification statut...") nouveau_statut = avoir_data_temp["statut"] doc.DO_Statut = nouveau_statut champs_modifies.append("statut") - logger.info(f" ✅ Statut défini: {nouveau_statut}") + logger.info(f" Statut défini: {nouveau_statut}") if modif_ref: - logger.info(" 📖 Modification référence...") + logger.info(" Modification référence...") try: doc.DO_Ref = avoir_data_temp["reference"] champs_modifies.append("reference") logger.info( - f" ✅ Référence définie: {avoir_data_temp['reference']}" + f" Référence définie: {avoir_data_temp['reference']}" ) except Exception as e: - logger.warning(f" ⚠️ Référence non définie: {e}") + logger.warning(f" Référence non définie: {e}") # Écrire sans réassocier le client - logger.info(" 💾 Write() sans réassociation client...") + logger.info(" Write() sans réassociation client...") try: doc.Write() - logger.info(" ✅ Write() réussi") + logger.info(" Write() réussi") doc.Read() @@ -7928,10 +8092,10 @@ class SageConnector: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: - logger.info(f" ✅ Client préservé: {client_apres}") + logger.info(f" Client préservé: {client_apres}") else: logger.error( - f" ❌ Client perdu: {client_code_initial} → {client_apres}" + f" Client perdu: {client_code_initial} → {client_apres}" ) except Exception as e: @@ -7943,14 +8107,14 @@ class SageConnector: except: pass - logger.error(f" ❌ Write() échoue: {error_msg}") + logger.error(f" Write() échoue: {error_msg}") raise ValueError(f"Sage refuse: {error_msg}") # ======================================== # ÉTAPE 6 : REMPLACEMENT COMPLET DES LIGNES # ======================================== elif modif_lignes: - logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") + logger.info(" REMPLACEMENT COMPLET DES LIGNES...") # D'abord modifier les dates si demandées if modif_date: @@ -7958,20 +8122,20 @@ class SageConnector: self.normaliser_date(avoir_data_temp.get("date_avoir")) ) champs_modifies.append("date") - logger.info(" 📅 Date avoir modifiée") + logger.info(" Date avoir modifiée") if modif_date_livraison: doc.DO_DateLivr = pywintypes.Time( self.normaliser_date(avoir_data_temp["date_livraison"]) ) - logger.info(" 📅 Date livraison modifiée") + logger.info(" Date livraison modifiée") champs_modifies.append("date_livraison") nouvelles_lignes = avoir_data["lignes"] nb_nouvelles = len(nouvelles_lignes) logger.info( - f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" + f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" ) try: @@ -7986,7 +8150,7 @@ class SageConnector: # ============================================ if nb_lignes_initial > 0: logger.info( - f" 🗑️ Suppression de {nb_lignes_initial} lignes existantes..." + f" Suppression de {nb_lignes_initial} lignes existantes..." ) # Supprimer depuis la fin pour éviter les problèmes d'index @@ -7999,21 +8163,21 @@ class SageConnector: ) ligne.Read() - # ✅ Utiliser .Remove() + # Utiliser .Remove() ligne.Remove() - logger.debug(f" ✅ Ligne {idx} supprimée") + logger.debug(f" Ligne {idx} supprimée") except Exception as e: logger.warning( - f" ⚠️ Impossible de supprimer ligne {idx}: {e}" + f" Impossible de supprimer ligne {idx}: {e}" ) # Continuer même si une suppression échoue - logger.info(" ✅ Toutes les lignes existantes supprimées") + logger.info(" Toutes les lignes existantes supprimées") # ============================================ # SOUS-ÉTAPE 2 : AJOUTER LES NOUVELLES LIGNES # ============================================ - logger.info(f" ➕ Ajout de {nb_nouvelles} nouvelles lignes...") + logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") for idx, ligne_data in enumerate(nouvelles_lignes, 1): logger.info( @@ -8078,14 +8242,14 @@ class SageConnector: # Écrire la ligne ligne_obj.Write() - logger.info(f" ✅ Ligne {idx} ajoutée") + logger.info(f" Ligne {idx} ajoutée") - logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées") # Écrire le document - logger.info(" 💾 Write() document après remplacement lignes...") + logger.info(" Write() document après remplacement lignes...") doc.Write() - logger.info(" ✅ Document écrit") + logger.info(" Document écrit") import time @@ -8098,9 +8262,9 @@ class SageConnector: if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") - logger.info(f" 👤 Client après remplacement: {client_apres}") + logger.info(f" Client après remplacement: {client_apres}") else: - logger.error(" ❌ Client NULL après remplacement") + logger.error(" Client NULL après remplacement") champs_modifies.append("lignes") @@ -8115,7 +8279,7 @@ class SageConnector: ) logger.info( - f"🔖 Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'" + f" Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'" ) doc.DO_Ref = nouvelle_reference @@ -8128,9 +8292,9 @@ class SageConnector: doc.Read() champs_modifies.append("reference") - logger.info(f" ✅ Référence modifiée avec succès") + logger.info(f" Référence modifiée avec succès") except Exception as e: - logger.warning(f"⚠️ Impossible de modifier la référence: {e}") + logger.warning(f"Impossible de modifier la référence: {e}") # ======================================== # ÉTAPE 6.6 : MODIFIER STATUT (EN DERNIER) @@ -8142,7 +8306,7 @@ class SageConnector: if nouveau_statut != statut_actuel: logger.info( - f"📊 Modification statut: {statut_actuel} → {nouveau_statut}" + f" Modification statut: {statut_actuel} → {nouveau_statut}" ) doc.DO_Statut = nouveau_statut @@ -8155,14 +8319,14 @@ class SageConnector: doc.Read() champs_modifies.append("statut") - logger.info(f" ✅ Statut modifié avec succès") + logger.info(f" Statut modifié avec succès") except Exception as e: - logger.warning(f"⚠️ Impossible de modifier le statut: {e}") + logger.warning(f"Impossible de modifier le statut: {e}") # ======================================== # ÉTAPE 7 : RELECTURE ET RETOUR # ======================================== - logger.info("📊 Relecture finale...") + logger.info(" Relecture finale...") import time @@ -8193,15 +8357,15 @@ class SageConnector: except: pass - logger.info(f"✅ ✅ ✅ SUCCÈS: {numero} modifié ✅ ✅ ✅") - logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") - logger.info(f" 👤 Client final: {client_final}") - logger.info(f" 🔖 Référence: {reference_finale}") - logger.info(f" 📊 Statut: {statut_final}") + logger.info(f" SUCCÈS: {numero} modifié ") + logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + logger.info(f" Client final: {client_final}") + logger.info(f" Référence: {reference_finale}") + logger.info(f" Statut: {statut_final}") if date_livraison_final: - logger.info(f" 📅 Date livraison: {date_livraison_final}") - logger.info(f" 📝 Champs modifiés: {champs_modifies}") + logger.info(f" Date livraison: {date_livraison_final}") + logger.info(f" Champs modifiés: {champs_modifies}") return { "numero": numero, @@ -8215,11 +8379,11 @@ class SageConnector: } except ValueError as e: - logger.error(f"❌ ERREUR MÉTIER: {e}") + logger.error(f" ERREUR MÉTIER: {e}") raise except Exception as e: - logger.error(f"❌ ERREUR TECHNIQUE: {e}", exc_info=True) + logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True) error_message = str(e) if self.cial: @@ -8250,7 +8414,7 @@ class SageConnector: raise RuntimeError("Connexion Sage non établie") logger.info( - f"🚀 Début création facture pour client {facture_data['client']['code']}" + f" Début création facture pour client {facture_data['client']['code']}" ) try: @@ -8259,7 +8423,7 @@ class SageConnector: try: self.cial.CptaApplication.BeginTrans() transaction_active = True - logger.debug("✅ Transaction Sage démarrée") + logger.debug(" Transaction Sage démarrée") except: pass @@ -8275,7 +8439,7 @@ class SageConnector: except: pass - logger.info("📄 Document facture créé") + logger.info(" Document facture créé") # ===== DATES ===== doc.DO_Date = pywintypes.Time( @@ -8290,7 +8454,7 @@ class SageConnector: self.normaliser_date(facture_data["date_livraison"]) ) logger.info( - f"📅 Date livraison: {facture_data['date_livraison']}" + f" Date livraison: {facture_data['date_livraison']}" ) # ===== CLIENT (CRITIQUE) ===== @@ -8310,18 +8474,18 @@ class SageConnector: doc.SetDefaultClient(client_obj) doc.Write() - logger.info(f"👤 Client {facture_data['client']['code']} associé") + logger.info(f" Client {facture_data['client']['code']} associé") # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if facture_data.get("reference"): try: doc.DO_Ref = facture_data["reference"] - logger.info(f"📖 Référence: {facture_data['reference']}") + logger.info(f" Référence: {facture_data['reference']}") except: pass # ===== CHAMPS SPÉCIFIQUES FACTURES ===== - logger.info("⚙️ Configuration champs spécifiques factures...") + logger.info(" Configuration champs spécifiques factures...") # Code journal (si disponible) try: @@ -8335,18 +8499,18 @@ class SageConnector: param_societe, "P_CodeJournalVte", "VTE" ) doc.DO_CodeJournal = journal_defaut - logger.info(f" ✅ Code journal: {journal_defaut}") + logger.info(f" Code journal: {journal_defaut}") except: doc.DO_CodeJournal = "VTE" - logger.info(" ✅ Code journal: VTE (défaut)") + logger.info(" Code journal: VTE (défaut)") except Exception as e: - logger.debug(f" ⚠️ Code journal: {e}") + logger.debug(f" Code journal: {e}") # Souche (si disponible) try: if hasattr(doc, "DO_Souche"): doc.DO_Souche = 0 - logger.debug(" ✅ Souche: 0 (défaut)") + logger.debug(" Souche: 0 (défaut)") except: pass @@ -8354,7 +8518,7 @@ class SageConnector: try: if hasattr(doc, "DO_Regime"): doc.DO_Regime = 0 - logger.debug(" ✅ Régime: 0 (défaut)") + logger.debug(" Régime: 0 (défaut)") except: pass @@ -8366,7 +8530,7 @@ class SageConnector: factory_article = self.cial.FactoryArticle - logger.info(f"📦 Ajout de {len(facture_data['lignes'])} lignes...") + logger.info(f" Ajout de {len(facture_data['lignes'])} lignes...") for idx, ligne_data in enumerate(facture_data["lignes"], 1): logger.info( @@ -8380,7 +8544,7 @@ class SageConnector: if not persist_article: raise ValueError( - f"❌ Article {ligne_data['article_code']} introuvable dans Sage" + f" Article {ligne_data['article_code']} introuvable dans Sage" ) article_obj = win32com.client.CastTo( @@ -8391,11 +8555,11 @@ class SageConnector: # Récupérer le prix de vente RÉEL prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") - logger.info(f"💰 Prix Sage: {prix_sage}€") + logger.info(f" Prix Sage: {prix_sage}€") if prix_sage == 0: logger.warning( - f"⚠️ Article {ligne_data['article_code']} a un prix = 0€ (toléré)" + f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) # Créer la ligne @@ -8417,43 +8581,43 @@ class SageConnector: ligne_data["article_code"], quantite ) logger.info( - f"✅ Article associé via SetDefaultArticleReference" + f" Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( - f"⚠️ SetDefaultArticleReference échoué: {e}, tentative avec objet" + f"SetDefaultArticleReference échoué: {e}, tentative avec objet" ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(f"✅ Article associé via SetDefaultArticle") + logger.info(f" Article associé via SetDefaultArticle") except Exception as e2: - logger.error(f"❌ Toutes les méthodes ont échoué") + logger.error(f" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") ) ligne_obj.DL_Qte = quantite - logger.warning("⚠️ Configuration manuelle appliquée") + logger.warning("Configuration manuelle appliquée") # Vérifier le prix automatique prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - logger.info(f"💰 Prix auto chargé: {prix_auto}€") + logger.info(f" Prix auto chargé: {prix_auto}€") # Ajuster le prix si nécessaire prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) - logger.info(f"💰 Prix personnalisé: {prix_a_utiliser}€") + logger.info(f" Prix personnalisé: {prix_a_utiliser}€") elif prix_auto == 0 and prix_sage > 0: ligne_obj.DL_PrixUnitaire = float(prix_sage) - logger.info(f"💰 Prix Sage forcé: {prix_sage}€") + logger.info(f" Prix Sage forcé: {prix_sage}€") elif prix_auto > 0: - logger.info(f"💰 Prix auto conservé: {prix_auto}€") + logger.info(f" Prix auto conservé: {prix_auto}€") prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) montant_ligne = quantite * prix_final - logger.info(f"✅ {quantite} x {prix_final}€ = {montant_ligne}€") + logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") # Remise remise = ligne_data.get("remise_pourcentage", 0) @@ -8465,22 +8629,22 @@ class SageConnector: 1 - remise / 100 ) logger.info( - f"🎁 Remise {remise}% → {montant_apres_remise}€" + f" Remise {remise}% → {montant_apres_remise}€" ) except Exception as e: - logger.warning(f"⚠️ Remise non appliquée: {e}") + logger.warning(f"Remise non appliquée: {e}") # Écrire la ligne ligne_obj.Write() - logger.info(f"✅ Ligne {idx} écrite") + logger.info(f" Ligne {idx} écrite") # ===== VALIDATION FINALE ===== - logger.info("💾 Validation facture...") + logger.info(" Validation facture...") # Réassocier le client avant validation (critique pour factures) try: doc.SetClient(client_obj) - logger.debug(" ✅ Client réassocié avant validation") + logger.debug(" Client réassocié avant validation") except: try: doc.SetDefaultClient(client_obj) @@ -8489,12 +8653,12 @@ class SageConnector: doc.Write() - logger.info("🔄 Process()...") + logger.info(" Process()...") process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() - logger.info("✅ Transaction committée") + logger.info(" Transaction committée") # ===== RÉCUPÉRATION NUMÉRO ===== time.sleep(2) @@ -8517,7 +8681,7 @@ class SageConnector: if not numero_facture: raise RuntimeError("Numéro facture vide après création") - logger.info(f"📄 Numéro facture: {numero_facture}") + logger.info(f" Numéro facture: {numero_facture}") # ===== RELECTURE ===== factory_doc = self.cial.FactoryDocumentVente @@ -8551,11 +8715,11 @@ class SageConnector: date_livraison_final = facture_data.get("date_livraison") logger.info( - f"✅✅✅ FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC ✅✅✅" + f" FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC " ) if date_livraison_final: - logger.info(f"📅 Date livraison: {date_livraison_final}") + logger.info(f" Date livraison: {date_livraison_final}") return { "numero_facture": numero_facture, @@ -8574,13 +8738,13 @@ class SageConnector: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() - logger.error("❌ Transaction annulée (rollback)") + logger.error(" Transaction annulée (rollback)") except: pass raise except Exception as e: - logger.error(f"❌ Erreur création facture: {e}", exc_info=True) + logger.error(f" Erreur création facture: {e}", exc_info=True) raise RuntimeError(f"Échec création facture: {str(e)}") def modifier_facture(self, numero: str, facture_data: Dict) -> Dict: @@ -8601,12 +8765,12 @@ class SageConnector: try: with self._com_context(), self._lock_com: - logger.info(f"🔬 === MODIFICATION FACTURE {numero} ===") + logger.info(f" === MODIFICATION FACTURE {numero} ===") # ======================================== # ÉTAPE 1 : CHARGER LE DOCUMENT # ======================================== - logger.info("📂 Chargement document...") + logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None @@ -8617,13 +8781,13 @@ class SageConnector: persist_test = factory.ReadPiece(type_test, numero) if persist_test: persist = persist_test - logger.info(f" ✅ Document trouvé (type={type_test})") + logger.info(f" Document trouvé (type={type_test})") break except: continue if not persist: - raise ValueError(f"❌ Facture {numero} INTROUVABLE") + raise ValueError(f" Facture {numero} INTROUVABLE") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() @@ -8631,7 +8795,7 @@ class SageConnector: statut_actuel = getattr(doc, "DO_Statut", 0) type_reel = getattr(doc, "DO_Type", -1) - logger.info(f" 📊 Type={type_reel}, Statut={statut_actuel}") + logger.info(f" Type={type_reel}, Statut={statut_actuel}") # Vérifier qu'elle n'est pas transformée ou annulée if statut_actuel == 5: @@ -8649,14 +8813,14 @@ class SageConnector: if client_obj: client_obj.Read() client_code_initial = getattr(client_obj, "CT_Num", "").strip() - logger.info(f" 👤 Client initial: {client_code_initial}") + logger.info(f" Client initial: {client_code_initial}") else: - logger.error(" ❌ Objet Client NULL à l'état initial !") + logger.error(" Objet Client NULL à l'état initial !") except Exception as e: - logger.error(f" ❌ Erreur lecture client initial: {e}") + logger.error(f" Erreur lecture client initial: {e}") if not client_code_initial: - raise ValueError("❌ Client introuvable dans le document") + raise ValueError(" Client introuvable dans le document") # Compter les lignes initiales nb_lignes_initial = 0 @@ -8675,9 +8839,9 @@ class SageConnector: except: break - logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") + logger.info(f" Lignes initiales: {nb_lignes_initial}") except Exception as e: - logger.warning(f" ⚠️ Erreur comptage lignes: {e}") + logger.warning(f" Erreur comptage lignes: {e}") # ======================================== # ÉTAPE 3 : DÉTERMINER LES MODIFICATIONS @@ -8692,7 +8856,7 @@ class SageConnector: "lignes" in facture_data and facture_data["lignes"] is not None ) - logger.info(f"📋 Modifications demandées:") + logger.info(f"Modifications demandées:") logger.info(f" Date facture: {modif_date}") logger.info(f" Date livraison: {modif_date_livraison}") logger.info(f" Statut: {modif_statut}") @@ -8708,25 +8872,25 @@ class SageConnector: if modif_ref: reference_a_modifier = facture_data_temp.pop("reference") logger.info( - "🔖 Modification de la référence reportée après les lignes" + " Modification de la référence reportée après les lignes" ) modif_ref = False if modif_statut: statut_a_modifier = facture_data_temp.pop("statut") logger.info( - "📊 Modification du statut reportée après les lignes" + " Modification du statut reportée après les lignes" ) modif_statut = False # ======================================== # ÉTAPE 4 : TEST WRITE() BASIQUE # ======================================== - logger.info("🧪 Test Write() basique (sans modification)...") + logger.info(" Test Write() basique (sans modification)...") try: doc.Write() - logger.info(" ✅ Write() basique OK") + logger.info(" Write() basique OK") doc.Read() # Vérifier que le client est toujours là @@ -8735,17 +8899,17 @@ class SageConnector: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: - logger.info(f" ✅ Client préservé: {client_apres}") + logger.info(f" Client préservé: {client_apres}") else: logger.error( - f" ❌ Client a changé: {client_code_initial} → {client_apres}" + f" Client a changé: {client_code_initial} → {client_apres}" ) else: - logger.error(" ❌ Client devenu NULL après Write() basique") + logger.error(" Client devenu NULL après Write() basique") except Exception as e: - logger.error(f" ❌ Write() basique ÉCHOUE: {e}") - logger.error(f" ❌ ABANDON: Le document est VERROUILLÉ") + logger.error(f" Write() basique ÉCHOUE: {e}") + logger.error(f" ABANDON: Le document est VERROUILLÉ") raise ValueError( f"Document verrouillé, impossible de modifier: {e}" ) @@ -8759,48 +8923,48 @@ class SageConnector: or modif_statut or modif_ref ): - logger.info("🎯 Modifications simples (sans lignes)...") + logger.info(" Modifications simples (sans lignes)...") if modif_date: - logger.info(" 📅 Modification date facture...") + logger.info(" Modification date facture...") doc.DO_Date = pywintypes.Time( self.normaliser_date(facture_data_temp.get("date_facture")) ) champs_modifies.append("date") if modif_date_livraison: - logger.info(" 📅 Modification date livraison...") + logger.info(" Modification date livraison...") doc.DO_DateLivr = pywintypes.Time( self.normaliser_date(facture_data_temp["date_livraison"]) ) logger.info( - f" ✅ Date livraison: {facture_data_temp['date_livraison']}" + f" Date livraison: {facture_data_temp['date_livraison']}" ) champs_modifies.append("date_livraison") if modif_statut: - logger.info(" 📊 Modification statut...") + logger.info(" Modification statut...") nouveau_statut = facture_data_temp["statut"] doc.DO_Statut = nouveau_statut champs_modifies.append("statut") - logger.info(f" ✅ Statut défini: {nouveau_statut}") + logger.info(f" Statut défini: {nouveau_statut}") if modif_ref: - logger.info(" 📖 Modification référence...") + logger.info(" Modification référence...") try: doc.DO_Ref = facture_data_temp["reference"] champs_modifies.append("reference") logger.info( - f" ✅ Référence définie: {facture_data_temp['reference']}" + f" Référence définie: {facture_data_temp['reference']}" ) except Exception as e: - logger.warning(f" ⚠️ Référence non définie: {e}") + logger.warning(f" Référence non définie: {e}") # Écrire sans réassocier le client - logger.info(" 💾 Write() sans réassociation client...") + logger.info(" Write() sans réassociation client...") try: doc.Write() - logger.info(" ✅ Write() réussi") + logger.info(" Write() réussi") doc.Read() @@ -8810,10 +8974,10 @@ class SageConnector: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: - logger.info(f" ✅ Client préservé: {client_apres}") + logger.info(f" Client préservé: {client_apres}") else: logger.error( - f" ❌ Client perdu: {client_code_initial} → {client_apres}" + f" Client perdu: {client_code_initial} → {client_apres}" ) except Exception as e: @@ -8825,14 +8989,14 @@ class SageConnector: except: pass - logger.error(f" ❌ Write() échoue: {error_msg}") + logger.error(f" Write() échoue: {error_msg}") raise ValueError(f"Sage refuse: {error_msg}") # ======================================== # ÉTAPE 6 : REMPLACEMENT COMPLET DES LIGNES # ======================================== elif modif_lignes: - logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") + logger.info(" REMPLACEMENT COMPLET DES LIGNES...") # D'abord modifier les dates si demandées if modif_date: @@ -8840,20 +9004,20 @@ class SageConnector: self.normaliser_date(facture_data_temp.get("date_facture")) ) champs_modifies.append("date") - logger.info(" 📅 Date facture modifiée") + logger.info(" Date facture modifiée") if modif_date_livraison: doc.DO_DateLivr = pywintypes.Time( self.normaliser_date(facture_data_temp["date_livraison"]) ) - logger.info(" 📅 Date livraison modifiée") + logger.info(" Date livraison modifiée") champs_modifies.append("date_livraison") nouvelles_lignes = facture_data["lignes"] nb_nouvelles = len(nouvelles_lignes) logger.info( - f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" + f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" ) try: @@ -8868,7 +9032,7 @@ class SageConnector: # ============================================ if nb_lignes_initial > 0: logger.info( - f" 🗑️ Suppression de {nb_lignes_initial} lignes existantes..." + f" Suppression de {nb_lignes_initial} lignes existantes..." ) # Supprimer depuis la fin pour éviter les problèmes d'index @@ -8881,21 +9045,21 @@ class SageConnector: ) ligne.Read() - # ✅ Utiliser .Remove() + # Utiliser .Remove() ligne.Remove() - logger.debug(f" ✅ Ligne {idx} supprimée") + logger.debug(f" Ligne {idx} supprimée") except Exception as e: logger.warning( - f" ⚠️ Impossible de supprimer ligne {idx}: {e}" + f" Impossible de supprimer ligne {idx}: {e}" ) # Continuer même si une suppression échoue - logger.info(" ✅ Toutes les lignes existantes supprimées") + logger.info(" Toutes les lignes existantes supprimées") # ============================================ # SOUS-ÉTAPE 2 : AJOUTER LES NOUVELLES LIGNES # ============================================ - logger.info(f" ➕ Ajout de {nb_nouvelles} nouvelles lignes...") + logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") for idx, ligne_data in enumerate(nouvelles_lignes, 1): logger.info( @@ -8960,14 +9124,14 @@ class SageConnector: # Écrire la ligne ligne_obj.Write() - logger.info(f" ✅ Ligne {idx} ajoutée") + logger.info(f" Ligne {idx} ajoutée") - logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées") # Écrire le document - logger.info(" 💾 Write() document après remplacement lignes...") + logger.info(" Write() document après remplacement lignes...") doc.Write() - logger.info(" ✅ Document écrit") + logger.info(" Document écrit") import time @@ -8980,9 +9144,9 @@ class SageConnector: if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") - logger.info(f" 👤 Client après remplacement: {client_apres}") + logger.info(f" Client après remplacement: {client_apres}") else: - logger.error(" ❌ Client NULL après remplacement") + logger.error(" Client NULL après remplacement") champs_modifies.append("lignes") @@ -8997,7 +9161,7 @@ class SageConnector: ) logger.info( - f"🔖 Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'" + f" Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'" ) doc.DO_Ref = nouvelle_reference @@ -9010,9 +9174,9 @@ class SageConnector: doc.Read() champs_modifies.append("reference") - logger.info(f" ✅ Référence modifiée avec succès") + logger.info(f" Référence modifiée avec succès") except Exception as e: - logger.warning(f"⚠️ Impossible de modifier la référence: {e}") + logger.warning(f"Impossible de modifier la référence: {e}") # ======================================== # ÉTAPE 6.6 : MODIFIER STATUT (EN DERNIER) @@ -9024,7 +9188,7 @@ class SageConnector: if nouveau_statut != statut_actuel: logger.info( - f"📊 Modification statut: {statut_actuel} → {nouveau_statut}" + f" Modification statut: {statut_actuel} → {nouveau_statut}" ) doc.DO_Statut = nouveau_statut @@ -9037,14 +9201,14 @@ class SageConnector: doc.Read() champs_modifies.append("statut") - logger.info(f" ✅ Statut modifié avec succès") + logger.info(f" Statut modifié avec succès") except Exception as e: - logger.warning(f"⚠️ Impossible de modifier le statut: {e}") + logger.warning(f"Impossible de modifier le statut: {e}") # ======================================== # ÉTAPE 7 : RELECTURE ET RETOUR # ======================================== - logger.info("📊 Relecture finale...") + logger.info(" Relecture finale...") import time @@ -9075,15 +9239,15 @@ class SageConnector: except: pass - logger.info(f"✅ ✅ ✅ SUCCÈS: {numero} modifiée ✅ ✅ ✅") - logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") - logger.info(f" 👤 Client final: {client_final}") - logger.info(f" 🔖 Référence: {reference_finale}") - logger.info(f" 📊 Statut: {statut_final}") + logger.info(f" SUCCÈS: {numero} modifiée ") + logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + logger.info(f" Client final: {client_final}") + logger.info(f" Référence: {reference_finale}") + logger.info(f" Statut: {statut_final}") if date_livraison_final: - logger.info(f" 📅 Date livraison: {date_livraison_final}") - logger.info(f" 📝 Champs modifiés: {champs_modifies}") + logger.info(f" Date livraison: {date_livraison_final}") + logger.info(f" Champs modifiés: {champs_modifies}") return { "numero": numero, @@ -9097,11 +9261,11 @@ class SageConnector: } except ValueError as e: - logger.error(f"❌ ERREUR MÉTIER: {e}") + logger.error(f" ERREUR MÉTIER: {e}") raise except Exception as e: - logger.error(f"❌ ERREUR TECHNIQUE: {e}", exc_info=True) + logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True) error_message = str(e) if self.cial: @@ -9737,7 +9901,7 @@ class SageConnector: # ======================================== # ÉTAPE 16 : FORCER LES VALEURS DE STOCK DEPUIS F_ARTSTOCK # ======================================== - # ✅ 1. STOCK (forcer les valeurs depuis F_ARTSTOCK) + # 1. STOCK (forcer les valeurs depuis F_ARTSTOCK) resultat["stock_reel"] = stock_total if stock_mini: @@ -9751,23 +9915,23 @@ class SageConnector: resultat["stock_reserve"] = 0.0 resultat["stock_commande"] = 0.0 - # ✅ 2. PRIX + # 2. PRIX if prix_vente is not None: resultat["prix_vente"] = float(prix_vente) if prix_achat is not None: resultat["prix_achat"] = float(prix_achat) - # ✅ 3. DESCRIPTION + # 3. DESCRIPTION if description: resultat["description"] = description - # ✅ 4. CODE EAN + # 4. CODE EAN if code_ean: resultat["code_ean"] = str(code_ean) resultat["code_barre"] = str(code_ean) - # ✅ 5. FAMILLE + # 5. FAMILLE if famille_code_personnalise and famille_trouvee: resultat["famille_code"] = famille_code_personnalise try: @@ -9779,7 +9943,7 @@ class SageConnector: except: pass - # ✅ 6. INFOS DÉPÔTS + # 6. INFOS DÉPÔTS if stocks_par_depot: resultat["stocks_par_depot"] = stocks_par_depot resultat["depot_principal"] = { @@ -9787,17 +9951,17 @@ class SageConnector: "intitule": depot_a_utiliser["intitule"], } - # ✅ 7. SUIVI DE STOCK + # 7. SUIVI DE STOCK resultat["suivi_stock_active"] = stock_defini - # ✅ 8. AVERTISSEMENT SI STOCK NON DÉFINI + # 8. AVERTISSEMENT SI STOCK NON DÉFINI if has_stock_values and not stock_defini and stock_erreur: resultat["avertissement"] = ( f"Stock demandé mais non défini dans F_ARTSTOCK : {stock_erreur}" ) logger.info( - f"[EXTRACTION] ✅ Article extrait et enrichi avec {len(resultat)} champs" + f"[EXTRACTION] Article extrait et enrichi avec {len(resultat)} champs" ) return resultat @@ -10204,7 +10368,7 @@ class SageConnector: def creer_famille(self, famille_data: dict) -> dict: """ - ✅ Crée une nouvelle famille d'articles dans Sage 100c + Crée une nouvelle famille d'articles dans Sage 100c **RESTRICTION : Seules les familles de type DÉTAIL peuvent être créées** Les familles de type Total doivent être créées manuellement dans Sage. @@ -10251,7 +10415,7 @@ class SageConnector: logger.info(f"[FAMILLE] Code : {code}") logger.info(f"[FAMILLE] Intitulé : {intitule}") - # ✅ NOUVEAU : Avertir si l'utilisateur demande un type Total + # NOUVEAU : Avertir si l'utilisateur demande un type Total type_demande = famille_data.get("type", 0) if type_demande == 1: logger.warning( @@ -10303,9 +10467,9 @@ class SageConnector: famille.FA_CodeFamille = code famille.FA_Intitule = intitule - # ✅ CRITIQUE : FORCER Type = 0 (Détail) + # CRITIQUE : FORCER Type = 0 (Détail) try: - famille.FA_Type = 0 # ✅ Toujours Détail + famille.FA_Type = 0 # Toujours Détail logger.info(f"[FAMILLE] Type : 0 (Détail)") except Exception as e: logger.warning(f"[FAMILLE] FA_Type non défini : {e}") @@ -10376,7 +10540,7 @@ class SageConnector: resultat = { "code": getattr(famille, "FA_CodeFamille", "").strip(), "intitule": getattr(famille, "FA_Intitule", "").strip(), - "type": 0, # ✅ Toujours Détail + "type": 0, # Toujours Détail "type_libelle": "Détail", } @@ -10453,7 +10617,7 @@ class SageConnector: if "FA_Type" in colonnes_disponibles: if not inclure_totaux: - query += " AND FA_Type = 0" # ✅ Seulement Détail + query += " AND FA_Type = 0" # Seulement Détail logger.info("[SQL] Filtre : FA_Type = 0 (Détail uniquement)") else: logger.info("[SQL] Filtre : TOUS les types (Détail + Total)") @@ -10681,7 +10845,7 @@ class SageConnector: resultat["est_total"] = fa_type == 1 resultat["est_detail"] = fa_type == 0 - # ⚠️ Avertissement si famille Total + # Avertissement si famille Total if fa_type == 1: resultat["avertissement"] = ( "Cette famille est de type 'Total' (agrégation comptable) " @@ -10995,10 +11159,10 @@ class SageConnector: factory_article_stock = ( article_full.FactoryArticleStock ) - logger.info(" ✅ FactoryArticleStock trouvée") + logger.info(" FactoryArticleStock trouvée") except AttributeError: logger.warning( - " ❌ FactoryArticleStock non disponible" + " FactoryArticleStock non disponible" ) if factory_article_stock: @@ -11082,7 +11246,7 @@ class SageConnector: logger.info(" Nouvel ArticleStock créé") except Exception as e: logger.error( - f" ❌ Impossible de créer ArticleStock: {e}" + f" Impossible de créer ArticleStock: {e}" ) raise @@ -11111,7 +11275,7 @@ class SageConnector: float(stock_mini), ) logger.info( - f" ✅ Stock mini défini via {prop_name}: {stock_mini}" + f" Stock mini défini via {prop_name}: {stock_mini}" ) break except AttributeError: @@ -11143,7 +11307,7 @@ class SageConnector: float(stock_maxi), ) logger.info( - f" ✅ Stock maxi défini via {prop_name}: {stock_maxi}" + f" Stock maxi défini via {prop_name}: {stock_maxi}" ) break except AttributeError: @@ -11162,11 +11326,11 @@ class SageConnector: try: stock_trouve.Write() logger.info( - f" ✅ ArticleStock sauvegardé" + f" ArticleStock sauvegardé" ) except Exception as e: logger.error( - f" ❌ Erreur Write() ArticleStock: {e}" + f" Erreur Write() ArticleStock: {e}" ) raise @@ -11251,7 +11415,7 @@ class SageConnector: ) except Exception as e: logger.error( - f" ❌ Impossible de créer DepotStock: {e}" + f" Impossible de créer DepotStock: {e}" ) # Mettre à jour @@ -11262,7 +11426,7 @@ class SageConnector: stock_mini ) logger.info( - f" ✅ DepotStock.AS_QteMini = {stock_mini}" + f" DepotStock.AS_QteMini = {stock_mini}" ) except Exception as e: logger.warning( @@ -11275,7 +11439,7 @@ class SageConnector: stock_maxi ) logger.info( - f" ✅ DepotStock.AS_QteMaxi = {stock_maxi}" + f" DepotStock.AS_QteMaxi = {stock_maxi}" ) except Exception as e: logger.warning( @@ -11285,11 +11449,11 @@ class SageConnector: try: stock_depot_trouve.Write() logger.info( - " ✅ DepotStock sauvegardé" + " DepotStock sauvegardé" ) except Exception as e: logger.error( - f" ❌ DepotStock Write() échoué: {e}" + f" DepotStock Write() échoué: {e}" ) except Exception as e: @@ -11318,7 +11482,7 @@ class SageConnector: doc.Read() numero = getattr(doc, "DO_Piece", "") - logger.info(f"[STOCK] ✅ Document finalisé: {numero}") + logger.info(f"[STOCK] Document finalisé: {numero}") # ======================================== # ÉTAPE 6 : VÉRIFICATION VIA COM @@ -11389,9 +11553,9 @@ class SageConnector: if transaction_active: try: self.cial.CptaApplication.CommitTrans() - logger.info(f"[STOCK] ✅ Transaction committée") + logger.info(f"[STOCK] Transaction committée") except: - logger.info(f"[STOCK] ✅ Changements sauvegardés") + logger.info(f"[STOCK] Changements sauvegardés") return { "article_ref": article_ref, @@ -11407,7 +11571,7 @@ class SageConnector: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() - logger.info(f"[STOCK] ❌ Transaction annulée") + logger.info(f"[STOCK] Transaction annulée") except: pass @@ -11630,7 +11794,7 @@ class SageConnector: ) logger.info( - f"[STOCK] ✅ Méthode 1 réussie : {stock_info['stock_total']} unités" + f"[STOCK] Méthode 1 réussie : {stock_info['stock_total']} unités" ) return stock_info @@ -11656,7 +11820,7 @@ class SageConnector: ) stock_trouve = True logger.info( - f"[STOCK] ✅ Méthode 2 réussie via {attr_stock}" + f"[STOCK] Méthode 2 réussie via {attr_stock}" ) break except: @@ -11675,7 +11839,7 @@ class SageConnector: if not calcul_complet: # Méthodes rapides ont échoué, mais calcul complet non demandé logger.warning( - f"[STOCK] ⚠️ Méthodes rapides échouées pour {reference}" + f"[STOCK] Méthodes rapides échouées pour {reference}" ) stock_info["methode_lecture"] = "ÉCHEC - Méthodes rapides échouées" @@ -11690,9 +11854,9 @@ class SageConnector: return stock_info - # ⚠️ ATTENTION : Cette partie est ULTRA-LENTE (20+ minutes) + # ATTENTION : Cette partie est ULTRA-LENTE (20+ minutes) logger.warning( - "[STOCK] ⚠️ CALCUL DEPUIS MOUVEMENTS ACTIVÉ - PEUT PRENDRE 20+ MINUTES" + "[STOCK] CALCUL DEPUIS MOUVEMENTS ACTIVÉ - PEUT PRENDRE 20+ MINUTES" ) # [Le reste du code de calcul depuis mouvements reste inchangé...] @@ -11827,9 +11991,9 @@ class SageConnector: logger.error(f"[DEBUG] Erreur : {e}", exc_info=True) raise """ - 📦 Lit le stock d'un article - VERSION CORRIGÉE + Lit le stock d'un article - VERSION CORRIGÉE - ✅ CORRECTIONS : + CORRECTIONS : 1. Cherche d'abord via ArticleStock 2. Puis via DepotStock si disponible 3. Calcule le total même si aucun dépôt n'est trouvé @@ -12114,7 +12278,7 @@ class SageConnector: logger.info(f"[STOCK] Article : {ar_design}") logger.info(f"[STOCK] AR_SuiviStock : {ar_suivi}") - # ⚠️ VÉRIFIER LE STOCK DISPONIBLE + # VÉRIFIER LE STOCK DISPONIBLE stock_dispo = self.verifier_stock_suffisant( article_ref, quantite, None ) @@ -12172,14 +12336,14 @@ class SageConnector: article_ref, float(quantite) ) article_lie = True - logger.info(f"[STOCK] ✅ SetDefaultArticleReference()") + logger.info(f"[STOCK] SetDefaultArticleReference()") except: try: ligne_obj.SetDefaultArticle( article_obj, float(quantite) ) article_lie = True - logger.info(f"[STOCK] ✅ SetDefaultArticle()") + logger.info(f"[STOCK] SetDefaultArticle()") except: pass @@ -12194,11 +12358,11 @@ class SageConnector: if numero_lot and ar_suivi == 2: try: ligne_obj.SetDefaultLot(numero_lot) - logger.info(f"[STOCK] ✅ Lot défini") + logger.info(f"[STOCK] Lot défini") except: try: ligne_obj.LS_NoSerie = numero_lot - logger.info(f"[STOCK] ✅ Lot via LS_NoSerie") + logger.info(f"[STOCK] Lot via LS_NoSerie") except: pass @@ -12214,7 +12378,7 @@ class SageConnector: # ÉCRIRE LA LIGNE # ======================================== ligne_obj.Write() - logger.info(f"[STOCK] ✅ Write() réussi") + logger.info(f"[STOCK] Write() réussi") # Vérification ligne_obj.Read() @@ -12231,7 +12395,7 @@ class SageConnector: except: pass - logger.info(f"[STOCK] ✅ LIGNE {idx} COMPLÈTE") + logger.info(f"[STOCK] LIGNE {idx} COMPLÈTE") stocks_mis_a_jour.append( { @@ -12251,12 +12415,12 @@ class SageConnector: doc.Read() numero = getattr(doc, "DO_Piece", "") - logger.info(f"[STOCK] ✅ Document finalisé : {numero}") + logger.info(f"[STOCK] Document finalisé : {numero}") # Commit try: self.cial.CptaApplication.CommitTrans() - logger.info(f"[STOCK] ✅ Transaction committée") + logger.info(f"[STOCK] Transaction committée") except: pass @@ -12415,7 +12579,7 @@ class SageConnector: with self._get_sql_connection() as conn: cursor = conn.cursor() - # ✅ LOCK pour éviter les race conditions + # LOCK pour éviter les race conditions cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE") cursor.execute("BEGIN TRANSACTION") @@ -12453,9 +12617,9 @@ class SageConnector: def lister_modeles_crystal(self) -> Dict: """ - 📋 Liste les modèles en scannant le répertoire Sage + Liste les modèles en scannant le répertoire Sage - ✅ FONCTIONNE MÊME SI FactoryEtat N'EXISTE PAS + FONCTIONNE MÊME SI FactoryEtat N'EXISTE PAS """ try: logger.info("[MODELES] Scan du répertoire des modèles...") @@ -12470,7 +12634,7 @@ class SageConnector: chemin_base = self.chemin_base if chemin_base: # Extraire le répertoire Sage - import os + dossier_sage = os.path.dirname(os.path.dirname(chemin_base)) chemins_possibles.insert(0, os.path.join(dossier_sage, "Modeles")) @@ -12484,10 +12648,6 @@ class SageConnector: "autres": [], } - # Scanner les répertoires - import os - import glob - for chemin in chemins_possibles: if not os.path.exists(chemin): continue @@ -12544,42 +12704,11 @@ class SageConnector: logger.error(f"[MODELES] Erreur: {e}", exc_info=True) raise RuntimeError(f"Erreur listage modèles: {str(e)}") - def _detecter_methodes_impression(self, doc) -> dict: - """🔍 Détecte les méthodes d'impression disponibles""" - methodes = {} - - # Tester FactoryEtat - try: - factory_etat = self.cial.CptaApplication.FactoryEtat - if factory_etat: - methodes["FactoryEtat"] = True - except: - try: - factory_etat = self.cial.FactoryEtat - if factory_etat: - methodes["FactoryEtat"] = True - except: - pass - - # Tester Imprimer() - if hasattr(doc, "Imprimer"): - methodes["Imprimer"] = True - - # Tester Print() - if hasattr(doc, "Print"): - methodes["Print"] = True - - # Tester ExportPDF() - if hasattr(doc, "ExportPDF"): - methodes["ExportPDF"] = True - - return methodes - def generer_pdf_document( self, numero: str, type_doc: int, modele: str = None ) -> bytes: """ - 📄 Génération PDF Sage 100c - UTILISE LES .BGC DIRECTEMENT + Génération PDF Sage 100c - UTILISE LES .BGC DIRECTEMENT Args: numero: Numéro document (ex: "FA00123") @@ -12612,19 +12741,19 @@ class SageConnector: doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() - logger.info(f"[PDF] ✅ Document chargé") + logger.info(f"[PDF] Document chargé") # ======================================== # 2. DÉTERMINER LE MODÈLE .BGC # ======================================== chemin_modele = self._determiner_modele(type_doc, modele) - logger.info(f"[PDF] 📄 Modèle: {os.path.basename(chemin_modele)}") + logger.info(f"[PDF] Modèle: {os.path.basename(chemin_modele)}") logger.info(f"[PDF] 📁 Chemin: {chemin_modele}") # ======================================== # 3. VÉRIFIER QUE LE FICHIER EXISTE # ======================================== - import os + if not os.path.exists(chemin_modele): raise ValueError(f"Modèle introuvable: {chemin_modele}") @@ -12652,7 +12781,7 @@ class SageConnector: numero, type_doc, chemin_modele, pdf_path ) if pdf_bytes: - logger.info("[PDF] ✅ Méthode 1 réussie (Crystal Runtime)") + logger.info("[PDF] Méthode 1 réussie (Crystal Runtime)") except Exception as e: logger.warning(f"[PDF] Méthode 1 échouée: {e}") @@ -12667,7 +12796,7 @@ class SageConnector: numero, type_doc, chemin_modele, pdf_path ) if pdf_bytes: - logger.info("[PDF] ✅ Méthode 2 réussie (DLL Sage)") + logger.info("[PDF] Méthode 2 réussie (DLL Sage)") except Exception as e: logger.warning(f"[PDF] Méthode 2 échouée: {e}") @@ -12682,7 +12811,7 @@ class SageConnector: numero, type_doc, chemin_modele, pdf_path ) if pdf_bytes: - logger.info("[PDF] ✅ Méthode 3 réussie (Sage Viewer)") + logger.info("[PDF] Méthode 3 réussie (Sage Viewer)") except Exception as e: logger.warning(f"[PDF] Méthode 3 échouée: {e}") @@ -12690,13 +12819,13 @@ class SageConnector: # MÉTHODE 4 : Python reportlab (FALLBACK) # ======================================== if not pdf_bytes: - logger.warning("[PDF] ⚠️ TOUTES LES MÉTHODES .BGC ONT ÉCHOUÉ") + logger.warning("[PDF] TOUTES LES MÉTHODES .BGC ONT ÉCHOUÉ") logger.info("[PDF] 🔷 Méthode 4: PDF Custom (fallback)...") try: pdf_bytes = self._generer_pdf_custom(doc, numero, type_doc) if pdf_bytes: - logger.info("[PDF] ✅ Méthode 4 réussie (PDF custom)") + logger.info("[PDF] Méthode 4 réussie (PDF custom)") except Exception as e: logger.error(f"[PDF] Méthode 4 échouée: {e}") @@ -12711,10 +12840,10 @@ class SageConnector: if not pdf_bytes: raise RuntimeError( - "❌ ÉCHEC GÉNÉRATION PDF\n\n" - "🔍 DIAGNOSTIC:\n" - f"- Modèle .bgc trouvé: ✅ ({os.path.basename(chemin_modele)})\n" - f"- Crystal Reports installé: ❌ NON DÉTECTÉ\n\n" + " ÉCHEC GÉNÉRATION PDF\n\n" + " DIAGNOSTIC:\n" + f"- Modèle .bgc trouvé: ({os.path.basename(chemin_modele)})\n" + f"- Crystal Reports installé: NON DÉTECTÉ\n\n" "💡 SOLUTIONS:\n" "1. Installer SAP Crystal Reports Runtime (gratuit):\n" " https://www.sap.com/products/technology-platform/crystal-reports/trial.html\n" @@ -12729,7 +12858,7 @@ class SageConnector: if len(pdf_bytes) < 500: raise RuntimeError("PDF généré trop petit (probablement corrompu)") - logger.info(f"[PDF] ✅✅✅ SUCCÈS: {len(pdf_bytes):,} octets") + logger.info(f"[PDF] SUCCÈS: {len(pdf_bytes):,} octets") return pdf_bytes except ValueError as e: @@ -12742,7 +12871,7 @@ class SageConnector: def _generer_pdf_crystal_runtime(self, numero, type_doc, chemin_modele, pdf_path): """🔷 Méthode 1: Crystal Reports Runtime API""" try: - import os + # Essayer différentes ProgID Crystal Reports prog_ids_crystal = [ @@ -12761,18 +12890,18 @@ class SageConnector: try: crystal = win32com.client.Dispatch(prog_id) prog_id_utilisee = prog_id - logger.info(f" ✅ Crystal trouvé: {prog_id}") + logger.info(f" Crystal trouvé: {prog_id}") break except Exception as e: logger.debug(f" {prog_id}: {e}") continue if not crystal: - logger.info(" ❌ Aucune ProgID Crystal trouvée") + logger.info(" Aucune ProgID Crystal trouvée") return None # Ouvrir le rapport .bgc - logger.info(f" 📂 Ouverture: {os.path.basename(chemin_modele)}") + logger.info(f" Ouverture: {os.path.basename(chemin_modele)}") rapport = crystal.OpenReport(chemin_modele) # Configurer la connexion SQL @@ -12794,11 +12923,11 @@ class SageConnector: pass # Appliquer le filtre Crystal Reports - logger.info(f" 🔍 Filtre: DO_Piece = '{numero}'") + logger.info(f" Filtre: DO_Piece = '{numero}'") rapport.RecordSelectionFormula = f"{{F_DOCENTETE.DO_Piece}} = '{numero}'" # Exporter en PDF - logger.info(" 📄 Export PDF...") + logger.info(" Export PDF...") rapport.ExportOptions.DestinationType = 1 # crEDTDiskFile rapport.ExportOptions.FormatType = 31 # crEFTPortableDocFormat (PDF) rapport.ExportOptions.DiskFileName = pdf_path @@ -12818,7 +12947,7 @@ class SageConnector: with open(pdf_path, "rb") as f: return f.read() - logger.warning(" ⚠️ Fichier PDF non créé") + logger.warning(" Fichier PDF non créé") return None except Exception as e: @@ -12828,7 +12957,7 @@ class SageConnector: def _generer_pdf_crystal_sage_dll(self, numero, type_doc, chemin_modele, pdf_path): """🔷 Méthode 2: Utiliser les DLL Crystal de Sage directement""" try: - import os + import ctypes # Chercher les DLL Crystal dans le dossier Sage @@ -12846,10 +12975,10 @@ class SageConnector: break if not dll_trouvee: - logger.info(" ❌ DLL Crystal Sage non trouvée") + logger.info(" DLL Crystal Sage non trouvée") return None - logger.info(f" ✅ DLL trouvée: {dll_trouvee}") + logger.info(f" DLL trouvée: {dll_trouvee}") # Charger la DLL crpe = ctypes.cdll.LoadLibrary(dll_trouvee) @@ -12859,7 +12988,7 @@ class SageConnector: job_handle = crpe.PEOpenPrintJob(chemin_modele.encode()) if job_handle == 0: - logger.warning(" ⚠️ Impossible d'ouvrir le rapport") + logger.warning(" Impossible d'ouvrir le rapport") return None # Définir les paramètres de connexion @@ -12888,7 +13017,7 @@ class SageConnector: def _generer_pdf_sage_viewer(self, numero, type_doc, chemin_modele, pdf_path): """🔷 Méthode 3: Sage Reports Viewer (si installé)""" try: - import os + # Chercher l'exécutable Sage Reports executables_possibles = [ @@ -12908,10 +13037,10 @@ class SageConnector: break if not exe_trouve: - logger.info(" ❌ SageReports.exe non trouvé") + logger.info(" SageReports.exe non trouvé") return None - logger.info(f" ✅ SageReports trouvé: {exe_trouve}") + logger.info(f" SageReports trouvé: {exe_trouve}") # Lancer en ligne de commande avec paramètres import subprocess @@ -12929,7 +13058,7 @@ class SageConnector: "/silent", ] - logger.info(" 🚀 Lancement SageReports...") + logger.info(" Lancement SageReports...") result = subprocess.run(cmd, capture_output=True, timeout=30) import time @@ -12940,7 +13069,7 @@ class SageConnector: with open(pdf_path, "rb") as f: return f.read() - logger.warning(" ⚠️ PDF non généré par SageReports") + logger.warning(" PDF non généré par SageReports") return None except Exception as e: @@ -13082,7 +13211,7 @@ class SageConnector: def _determiner_modele(self, type_doc: int, modele_demande: str = None) -> str: """ - 🔍 Détermine le chemin du modèle Crystal Reports à utiliser + Détermine le chemin du modèle Crystal Reports à utiliser Args: type_doc: Type Sage (0=devis, 60=facture, etc.) @@ -13135,309 +13264,477 @@ class SageConnector: return modele_std["chemin_complet"] - def diagnostiquer_impression_approfondi(self): - """🔬 Diagnostic ultra-complet pour trouver les objets d'impression""" + + def generer_pdf_via_sage_com(self, numero: str, type_doc: int, modele: str = None) -> bytes: + """ + 🎯 NOUVELLE MÉTHODE : Utilise l'API COM Sage pour exporter en PDF + + Sage 100c expose des méthodes pour imprimer/exporter les documents. + Cette méthode utilise exactement le même processus que l'interface graphique. + """ + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + import os + import tempfile + import time + try: with self._com_context(), self._lock_com: - logger.info("=" * 80) - logger.info("DIAGNOSTIC IMPRESSION APPROFONDI") - logger.info("=" * 80) - - objets_a_tester = [ - ("self.cial", self.cial), - ("CptaApplication", self.cial.CptaApplication), - ] - - # Charger un document pour tester - try: - factory = self.cial.FactoryDocumentVente - persist = factory.List(1) - if persist: - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - objets_a_tester.append(("Document", doc)) - except: - pass - - for nom_objet, objet in objets_a_tester: - logger.info(f"\n{'='*60}") - logger.info(f"OBJET: {nom_objet}") - logger.info(f"{'='*60}") - - # Chercher tous les attributs qui contiennent "print", "etat", "bilan", "crystal", "report" - mots_cles = [ - "print", - "etat", - "bilan", - "crystal", - "report", - "pdf", - "export", - "impression", - "imprimer", - ] - - attributs_trouves = [] - - for attr in dir(objet): - if attr.startswith("_"): - continue - - attr_lower = attr.lower() - - # Vérifier si contient un des mots-clés - if any(mot in attr_lower for mot in mots_cles): - try: - val = getattr(objet, attr) - type_val = type(val).__name__ - is_callable = callable(val) - - attributs_trouves.append( - { - "nom": attr, - "type": type_val, - "callable": is_callable, - } - ) - - logger.info( - f" ✅ TROUVÉ: {attr} ({type_val}) {'[MÉTHODE]' if is_callable else '[PROPRIÉTÉ]'}" - ) - - except Exception as e: - logger.debug(f" Erreur {attr}: {e}") - - if not attributs_trouves: - logger.warning( - f" ❌ Aucun objet d'impression trouvé sur {nom_objet}" - ) - - # Tester des noms de méthodes spécifiques - logger.info(f"\n{'='*60}") - logger.info("TESTS DIRECTS") - logger.info(f"{'='*60}") - - methodes_a_tester = [ - ("self.cial.BilanEtat", lambda: self.cial.BilanEtat), - ("self.cial.Etat", lambda: self.cial.Etat), - ( - "self.cial.CptaApplication.BilanEtat", - lambda: self.cial.CptaApplication.BilanEtat, - ), - ( - "self.cial.CptaApplication.Etat", - lambda: self.cial.CptaApplication.Etat, - ), - ("self.cial.FactoryEtat", lambda: self.cial.FactoryEtat), - ( - "self.cial.CptaApplication.FactoryEtat", - lambda: self.cial.CptaApplication.FactoryEtat, - ), - ] - - for nom, getter in methodes_a_tester: - try: - obj = getter() - logger.info(f" ✅ {nom} EXISTE : {type(obj).__name__}") - except AttributeError as e: - logger.info(f" ❌ {nom} N'EXISTE PAS : {e}") - except Exception as e: - logger.info(f" ⚠️ {nom} ERREUR : {e}") - - logger.info("=" * 80) - - return {"diagnostic": "terminé"} - - except Exception as e: - logger.error(f"Erreur diagnostic: {e}", exc_info=True) - raise - - def lister_objets_com_disponibles(self): - """🔍 Liste tous les objets COM disponibles dans Sage""" - try: - with self._com_context(), self._lock_com: - objets_trouves = {"cial": [], "cpta_application": [], "document": []} - - # 1. Objets sur self.cial - for attr in dir(self.cial): - if not attr.startswith("_"): - try: - obj = getattr(self.cial, attr) - objets_trouves["cial"].append( - { - "nom": attr, - "type": str(type(obj)), - "callable": callable(obj), - } - ) - except: - pass - - # 2. Objets sur CptaApplication - try: - cpta = self.cial.CptaApplication - for attr in dir(cpta): - if not attr.startswith("_"): - try: - obj = getattr(cpta, attr) - objets_trouves["cpta_application"].append( - { - "nom": attr, - "type": str(type(obj)), - "callable": callable(obj), - } - ) - except: - pass - except: - pass - - # 3. Objets sur un document - try: - factory = self.cial.FactoryDocumentVente - persist = factory.List(1) - if persist: - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - for attr in dir(doc): - if not attr.startswith("_"): - try: - obj = getattr(doc, attr) - objets_trouves["document"].append( - { - "nom": attr, - "type": str(type(obj)), - "callable": callable(obj), - } - ) - except: - pass - except: - pass - - return objets_trouves - - except Exception as e: - logger.error(f"Erreur listage objets COM: {e}", exc_info=True) - raise - - def explorer_methodes_impression(self): - """Explore toutes les méthodes d'impression disponibles""" - try: - with self._com_context(), self._lock_com: - # Charger un document de test - factory = self.cial.FactoryDocumentVente - persist = factory.List(1) - - if not persist: - return {"error": "Aucun document trouvé"} - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - methods = {} - - # Tester différentes signatures de Print - signatures_to_test = [ - "Print", - "PrintToFile", - "Export", - "ExportToPDF", - "SaveAs", - "GeneratePDF", - ] - - for method_name in signatures_to_test: - if hasattr(doc, method_name): - try: - # Essayer d'appeler pour voir les paramètres - method = getattr(doc, method_name) - methods[method_name] = { - "exists": True, - "callable": callable(method), - } - except: - methods[method_name] = { - "exists": True, - "error": "Access error", - } - - return methods - - except Exception as e: - return {"error": str(e)} - - def generer_pdf_document_via_print(self, numero: str, type_doc: int) -> bytes: - """Utilise la méthode Print() native des documents Sage""" - try: - with self._com_context(), self._lock_com: - # Charger le document + logger.info(f"[PDF-COM] Génération PDF via API Sage COM") + logger.info(f"[PDF-COM] Document: {numero} (type={type_doc})") + + # 1. Charger le document factory = self.cial.FactoryDocumentVente persist = factory.ReadPiece(type_doc, numero) - + if not persist: persist = self._find_document_in_list(numero, type_doc) - + if not persist: raise ValueError(f"Document {numero} introuvable") - + doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() - - # Créer un fichier temporaire - import tempfile - import os - + + # 2. Déterminer le modèle + chemin_modele = self._determiner_modele(type_doc, modele) + logger.info(f"[PDF-COM] Modèle: {os.path.basename(chemin_modele)}") + + # 3. Créer fichier temporaire temp_dir = tempfile.gettempdir() - pdf_path = os.path.join(temp_dir, f"document_{numero}.pdf") - - # Utiliser Print() avec destination fichier PDF - # Les codes de destination typiques dans Sage : - # 0 = Imprimante par défaut - # 1 = Aperçu - # 2 = Fichier - # 6 = PDF (dans certaines versions) - + pdf_path = os.path.join(temp_dir, f"sage_{numero}_{int(time.time())}.pdf") + + # ======================================== + # MÉTHODE A : Utiliser BiPrint (si disponible) + # ======================================== try: - # Tentative 1 : Print() avec paramètres - doc.Print(Destination=6, FileName=pdf_path, Preview=False) - except: - # Tentative 2 : Print() simplifié - try: + logger.info("[PDF-COM] Tentative avec BiPrint...") + + # BiPrint est l'objet Sage pour l'impression + bi_print = self.cial.BiPrint + + # Configurer l'impression + bi_print.Destination = 4 # 4 = Fichier PDF + bi_print.FileName = pdf_path + bi_print.Model = chemin_modele + + # Lancer l'impression + bi_print.Preview = False + bi_print.Launch(doc) + + # Attendre la création du fichier + max_wait = 30 + waited = 0 + while not os.path.exists(pdf_path) and waited < max_wait: + time.sleep(0.5) + waited += 0.5 + + if os.path.exists(pdf_path) and os.path.getsize(pdf_path) > 0: + with open(pdf_path, 'rb') as f: + pdf_bytes = f.read() + os.remove(pdf_path) + logger.info(f"[PDF-COM] ✅ BiPrint réussi ({len(pdf_bytes)} octets)") + return pdf_bytes + + except Exception as e: + logger.warning(f"[PDF-COM] BiPrint échoué: {e}") + + # ======================================== + # MÉTHODE B : Utiliser Print() direct + # ======================================== + try: + logger.info("[PDF-COM] Tentative avec Print() direct...") + + # Certains objets Sage ont une méthode Print() + if hasattr(doc, 'Print'): doc.Print( - pdf_path - ) # Certaines versions acceptent juste le chemin - except: - # Tentative 3 : PrintToFile() - try: - doc.PrintToFile(pdf_path) - except AttributeError: - raise RuntimeError("Aucune méthode d'impression disponible") - - # Lire le fichier PDF - import time - - max_wait = 10 - waited = 0 - while not os.path.exists(pdf_path) and waited < max_wait: - time.sleep(0.5) - waited += 0.5 - - if not os.path.exists(pdf_path): - raise RuntimeError("Le fichier PDF n'a pas été généré") - - with open(pdf_path, "rb") as f: - pdf_bytes = f.read() - - # Nettoyer + Model=chemin_modele, + Destination=4, # PDF + FileName=pdf_path, + Preview=False + ) + + time.sleep(2) + + if os.path.exists(pdf_path) and os.path.getsize(pdf_path) > 0: + with open(pdf_path, 'rb') as f: + pdf_bytes = f.read() + os.remove(pdf_path) + logger.info(f"[PDF-COM] ✅ Print() réussi ({len(pdf_bytes)} octets)") + return pdf_bytes + + except Exception as e: + logger.warning(f"[PDF-COM] Print() échoué: {e}") + + # ======================================== + # MÉTHODE C : Utiliser CrystalPrint (interface Crystal dans Sage) + # ======================================== try: - os.remove(pdf_path) - except: - pass - - return pdf_bytes - + logger.info("[PDF-COM] Tentative avec CrystalPrint...") + + # Sage utilise un wrapper Crystal Reports + crystal_print = self.cial.CrystalPrint + + crystal_print.ReportFileName = chemin_modele + crystal_print.SelectionFormula = f"{{F_DOCENTETE.DO_Piece}} = '{numero}'" + crystal_print.Destination = 4 # PDF + crystal_print.DestinationPath = pdf_path + + crystal_print.Execute() + + time.sleep(2) + + if os.path.exists(pdf_path) and os.path.getsize(pdf_path) > 0: + with open(pdf_path, 'rb') as f: + pdf_bytes = f.read() + os.remove(pdf_path) + logger.info(f"[PDF-COM] ✅ CrystalPrint réussi ({len(pdf_bytes)} octets)") + return pdf_bytes + + except Exception as e: + logger.warning(f"[PDF-COM] CrystalPrint échoué: {e}") + + # ======================================== + # MÉTHODE D : Explorer les propriétés de l'objet Sage + # ======================================== + logger.info("[PDF-COM] Exploration des méthodes disponibles...") + + # Lister toutes les méthodes disponibles + for attr_name in dir(doc): + if 'print' in attr_name.lower() or 'export' in attr_name.lower(): + logger.info(f" Méthode trouvée: {attr_name}") + + for attr_name in dir(self.cial): + if 'print' in attr_name.lower() or 'crystal' in attr_name.lower(): + logger.info(f" Objet Cial: {attr_name}") + + raise RuntimeError( + "Aucune méthode COM Sage trouvée pour générer le PDF.\n" + "Le diagnostic a listé les méthodes disponibles ci-dessus." + ) + except Exception as e: - logger.error(f"Erreur génération PDF via Print(): {e}") + logger.error(f"[PDF-COM] Erreur: {e}", exc_info=True) raise + + + def explorer_api_sage(self): + """ + 🔍 Fonction de diagnostic pour explorer l'API COM Sage + """ + logger.info("="*60) + logger.info("🔍 EXPLORATION DE L'API COM SAGE") + logger.info("="*60) + + # Explorer l'objet Cial + logger.info("\n📦 Propriétés de self.cial:") + for attr in dir(self.cial): + if not attr.startswith('_'): + try: + obj = getattr(self.cial, attr) + logger.info(f" {attr}: {type(obj).__name__}") + except: + logger.info(f" {attr}: (inaccessible)") + + # Chercher spécifiquement les objets liés à l'impression + logger.info("\n🖨️ Objets liés à l'impression:") + for attr in dir(self.cial): + if any(keyword in attr.lower() for keyword in ['print', 'export', 'crystal', 'report', 'pdf']): + logger.info(f" ✅ {attr}") + + # Explorer un document + try: + factory = self.cial.FactoryDocumentVente + persist = factory.List(1) # Premier document + + if persist: + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + logger.info("\n📄 Méthodes du document:") + for attr in dir(doc): + if any(keyword in attr.lower() for keyword in ['print', 'export', 'pdf']): + logger.info(f" ✅ {attr}") + + except Exception as e: + logger.error(f"Erreur exploration document: {e}") + + + """ + 🎯 GÉNÉRATION PDF via l'API Transformation native de Sage + Cette méthode NE NÉCESSITE PAS Crystal Reports + """ + + def generer_pdf_transformation_native(self, numero: str, type_doc: int) -> bytes: + """ + Génère un PDF en utilisant l'objet Transformation natif de Sage + + Cette méthode utilise l'API interne de Sage qui ne dépend pas de Crystal Reports + + Args: + numero: Numéro document (ex: "FA00123") + type_doc: Type Sage (0=devis, 10=BC, 30=BL, 60=facture, 50=avoir) + + Returns: + bytes: Contenu PDF + """ + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + try: + with self._com_context(), self._lock_com: + logger.info("="*60) + logger.info("🎯 GÉNÉRATION PDF VIA TRANSFORMATION NATIVE") + logger.info("="*60) + + # ========================================== + # 1. CHARGER LE DOCUMENT + # ========================================== + logger.info(f"📄 Chargement document {numero} (type={type_doc})") + + factory = self.cial.FactoryDocumentVente + persist = factory.ReadPiece(type_doc, numero) + + if not persist: + persist = self._find_document_in_list(numero, type_doc) + + if not persist: + raise ValueError(f"Document {numero} introuvable") + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + logger.info("✅ Document chargé") + + # ========================================== + # 2. ACCÉDER À L'OBJET TRANSFORMATION.VENTE + # ========================================== + logger.info("🔄 Accès objet Transformation.Vente...") + + transformation = self.cial.Transformation.Vente + logger.info(f"✅ Objet TransformationVente: {type(transformation).__name__}") + + # Explorer les méthodes disponibles + logger.info("📋 Méthodes TransformationVente disponibles:") + for attr in dir(transformation): + if not attr.startswith('_') and callable(getattr(transformation, attr, None)): + logger.info(f" - {attr}") + + # ========================================== + # 3. CRÉER FICHIER TEMPORAIRE + # ========================================== + import tempfile + import time + + temp_dir = tempfile.gettempdir() + pdf_path = os.path.join(temp_dir, f"sage_{numero}_{int(time.time())}.pdf") + + logger.info(f"📁 Fichier temporaire: {pdf_path}") + + # ========================================== + # 4. MÉTHODES POSSIBLES DE TRANSFORMATION + # ========================================== + + # Méthode A: TransformToFile + try: + logger.info("🔷 Tentative: TransformToFile...") + + # Paramètres possibles + transformation.TransformToFile( + persist, # Document source + pdf_path, # Fichier destination + 1 # Format (1=PDF, possiblement) + ) + + time.sleep(1) + + if os.path.exists(pdf_path) and os.path.getsize(pdf_path) > 0: + with open(pdf_path, 'rb') as f: + pdf_bytes = f.read() + os.remove(pdf_path) + logger.info(f"✅ PDF généré: {len(pdf_bytes):,} octets") + return pdf_bytes + + except AttributeError: + logger.info(" ❌ TransformToFile n'existe pas") + except Exception as e: + logger.info(f" ❌ TransformToFile échoué: {e}") + + # Méthode B: Transform + Export + try: + logger.info("🔷 Tentative: Transform + Export...") + + result = transformation.Transform(persist) + + if hasattr(result, 'ExportToPDF'): + result.ExportToPDF(pdf_path) + elif hasattr(result, 'Export'): + result.Export(pdf_path, 'PDF') + + time.sleep(1) + + if os.path.exists(pdf_path) and os.path.getsize(pdf_path) > 0: + with open(pdf_path, 'rb') as f: + pdf_bytes = f.read() + os.remove(pdf_path) + logger.info(f"✅ PDF généré: {len(pdf_bytes):,} octets") + return pdf_bytes + + except AttributeError: + logger.info(" ❌ Transform n'existe pas") + except Exception as e: + logger.info(f" ❌ Transform échoué: {e}") + + # Méthode C: CreatePDF + try: + logger.info("🔷 Tentative: CreatePDF...") + + pdf_bytes = transformation.CreatePDF(persist) + + if pdf_bytes and len(pdf_bytes) > 0: + logger.info(f"✅ PDF généré: {len(pdf_bytes):,} octets") + return pdf_bytes + + except AttributeError: + logger.info(" ❌ CreatePDF n'existe pas") + except Exception as e: + logger.info(f" ❌ CreatePDF échoué: {e}") + + # ========================================== + # 5. FALLBACK: EXPLORER TOUTES LES MÉTHODES + # ========================================== + logger.info("🔍 Exploration exhaustive...") + + for attr in dir(transformation): + if not attr.startswith('_'): + try: + method = getattr(transformation, attr) + if callable(method): + logger.info(f" Essai: {attr}") + + # Essayer d'appeler avec le document + result = method(persist) + + # Si c'est un objet, chercher des méthodes d'export + if result and hasattr(result, '__class__'): + for sub_attr in dir(result): + if 'pdf' in sub_attr.lower() or 'export' in sub_attr.lower(): + logger.info(f" → Trouvé: {sub_attr}") + except: + pass + + # Si on arrive ici, aucune méthode n'a fonctionné + logger.warning("❌ Aucune méthode Transformation disponible") + return None + + except Exception as e: + logger.error(f"Erreur transformation native: {e}", exc_info=True) + return None + + + def explorer_transformation_complete(self): + """ + 🔬 Exploration complète de l'objet Transformation + À lancer pour comprendre comment il fonctionne + """ + try: + with self._com_context(), self._lock_com: + transformation = self.cial.Transformation + + print("\n" + "="*60) + print("🔬 ANALYSE OBJET TRANSFORMATION") + print("="*60) + + print(f"\nType: {type(transformation).__name__}") + print(f"Classe: {transformation.__class__.__name__}") + + # Toutes les propriétés + print("\n📋 PROPRIÉTÉS TRANSFORMATION:") + for attr in dir(transformation): + if not attr.startswith('_'): + try: + obj = getattr(transformation, attr) + obj_type = type(obj).__name__ + + if not callable(obj): + print(f" {attr}: {obj_type}") + except Exception as e: + print(f" {attr}: (inaccessible) - {e}") + + # Toutes les méthodes + print("\n🔧 MÉTHODES TRANSFORMATION:") + for attr in dir(transformation): + if not attr.startswith('_'): + try: + obj = getattr(transformation, attr) + if callable(obj): + print(f" {attr}()") + except: + pass + + # ========================================== + # EXPLORER TRANSFORMATION.VENTE (IMPORTANT!) + # ========================================== + print("\n" + "="*60) + print("🔬 ANALYSE TRANSFORMATION.VENTE (Documents vente)") + print("="*60) + + transfo_vente = transformation.Vente + print(f"\nType: {type(transfo_vente).__name__}") + print(f"Classe: {transfo_vente.__class__.__name__}") + + print("\n📋 PROPRIÉTÉS TRANSFORMATION.VENTE:") + for attr in dir(transfo_vente): + if not attr.startswith('_'): + try: + obj = getattr(transfo_vente, attr) + obj_type = type(obj).__name__ + + if not callable(obj): + print(f" {attr}: {obj_type}") + try: + val = str(obj) + if len(val) < 100: + print(f" = {val}") + except: + pass + except Exception as e: + print(f" {attr}: (inaccessible) - {e}") + + print("\n🔧 MÉTHODES TRANSFORMATION.VENTE:") + for attr in dir(transfo_vente): + if not attr.startswith('_'): + try: + obj = getattr(transfo_vente, attr) + if callable(obj): + print(f" ✅ {attr}()") + except: + pass + + # ========================================== + # EXPLORER TRANSFORMATION.ACHAT + # ========================================== + print("\n" + "="*60) + print("🔬 ANALYSE TRANSFORMATION.ACHAT (Documents achat)") + print("="*60) + + transfo_achat = transformation.Achat + print(f"\nType: {type(transfo_achat).__name__}") + + print("\n🔧 MÉTHODES TRANSFORMATION.ACHAT:") + for attr in dir(transfo_achat): + if not attr.startswith('_'): + try: + obj = getattr(transfo_achat, attr) + if callable(obj): + print(f" ✅ {attr}()") + except: + pass + + print("\n" + "="*60) + + except Exception as e: + logger.error(f"Erreur exploration: {e}", exc_info=True) + + \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..d634775 --- /dev/null +++ b/test.py @@ -0,0 +1,316 @@ +""" +🧪 TEST GÉNÉRATION PDF AVEC CRYSTAL REPORTS +À lancer immédiatement après installation Crystal +""" + +import win32com.client +import os +import time +import logging + +logger = logging.getLogger(__name__) + + +def test_crystal_pdf_simple(): + """ + Test rapide de Crystal Reports - sans Sage + Vérifie juste que Crystal peut s'instancier et exporter + """ + print("\n" + "="*70) + print("🧪 TEST CRYSTAL REPORTS - INSTANCIATION SIMPLE") + print("="*70) + + try: + # 1. Instancier Crystal + print("\n1. Instanciation Crystal Runtime...") + + prog_ids = [ + "CrystalRuntime.Application.140", + "CrystalRuntime.Application.13", + "CrystalRuntime.Application.12", + ] + + crystal = None + prog_id_utilisee = None + + for prog_id in prog_ids: + try: + crystal = win32com.client.Dispatch(prog_id) + prog_id_utilisee = prog_id + print(f" ✅ {prog_id}") + break + except Exception as e: + print(f" ❌ {prog_id}: {e}") + + if not crystal: + print("\n❌ ÉCHEC : Aucune version de Crystal disponible") + print(" → Vérifier installation") + print(" → Redémarrer le serveur si juste installé") + return False + + print(f"\n✅ Crystal opérationnel : {prog_id_utilisee}") + + # 2. Vérifier méthodes disponibles + print("\n2. Vérification méthodes d'export...") + + methodes_requises = ['OpenReport', 'Export'] + + for methode in methodes_requises: + if hasattr(crystal, methode): + print(f" ✅ {methode}()") + else: + print(f" ❌ {methode}() manquante") + + print("\n✅ Crystal Reports est prêt pour générer des PDF !") + return True + + except Exception as e: + print(f"\n❌ ERREUR : {e}") + return False + + +def tester_pdf_avec_sage(sage_connector, numero: str, type_doc: int): + """ + Test de génération PDF via Sage avec Crystal + + Args: + sage_connector: Instance de ton SageConnector + numero: Numéro document (ex: "FA00123") + type_doc: Type document (0=devis, 60=facture, etc.) + + Returns: + tuple: (success: bool, pdf_bytes: bytes, message: str) + """ + print("\n" + "="*70) + print(f"🧪 TEST GÉNÉRATION PDF SAGE + CRYSTAL") + print(f" Document: {numero} (type={type_doc})") + print("="*70) + + try: + # 1. Vérifier connexion Sage + if not sage_connector.cial: + return False, None, "Connexion Sage non établie" + + print("\n1. ✅ Connexion Sage OK") + + # 2. Vérifier Crystal + print("\n2. Vérification Crystal...") + + prog_ids = [ + "CrystalRuntime.Application.140", + "CrystalRuntime.Application.13", + "CrystalRuntime.Application.12", + ] + + crystal_ok = False + for prog_id in prog_ids: + try: + test = win32com.client.Dispatch(prog_id) + crystal_ok = True + print(f" ✅ Crystal disponible : {prog_id}") + break + except: + pass + + if not crystal_ok: + return False, None, "Crystal Reports non disponible - Redémarrer le serveur" + + # 3. Chercher modèle .BGC + print("\n3. Recherche modèle Crystal...") + + modeles = sage_connector.lister_modeles_crystal() + + # Déterminer catégorie + mapping = {0: "devis", 10: "commandes", 30: "livraisons", 60: "factures", 50: "avoirs"} + categorie = mapping.get(type_doc, "autres") + + if categorie not in modeles or not modeles[categorie]: + return False, None, f"Aucun modèle trouvé pour type {type_doc}" + + modele = modeles[categorie][0] + chemin_modele = modele["chemin_complet"] + + print(f" ✅ Modèle : {modele['fichier']}") + print(f" 📁 Chemin : {chemin_modele}") + + if not os.path.exists(chemin_modele): + return False, None, f"Fichier modèle introuvable : {chemin_modele}" + + # 4. Générer PDF + print("\n4. Génération PDF...") + print(" ⏳ Traitement en cours...") + + start_time = time.time() + + try: + # Utiliser ta méthode existante + pdf_bytes = sage_connector.generer_pdf_document( + numero=numero, + type_doc=type_doc, + modele=modele["fichier"] + ) + + elapsed = time.time() - start_time + + if pdf_bytes and len(pdf_bytes) > 500: + print(f"\n✅ PDF GÉNÉRÉ AVEC SUCCÈS !") + print(f" 📊 Taille : {len(pdf_bytes):,} octets") + print(f" ⏱️ Temps : {elapsed:.2f}s") + + return True, pdf_bytes, "Succès" + else: + return False, None, "PDF généré mais vide ou corrompu" + + except Exception as e: + elapsed = time.time() - start_time + print(f"\n❌ ÉCHEC après {elapsed:.2f}s") + print(f" Erreur : {e}") + + return False, None, str(e) + + except Exception as e: + logger.error(f"Erreur test PDF : {e}", exc_info=True) + return False, None, str(e) + + +def analyser_erreur_generation(erreur_msg: str): + """ + Analyse une erreur de génération PDF et propose solutions + """ + print("\n" + "="*70) + print("🔍 ANALYSE DE L'ERREUR") + print("="*70) + + erreur_lower = erreur_msg.lower() + + if "access" in erreur_lower or "permission" in erreur_lower: + print(""" +❌ PROBLÈME : Permissions fichiers + +💡 SOLUTIONS : + 1. Vérifier que le dossier temporaire est accessible : + icacls "C:\\Windows\\Temp" /grant Everyone:(OI)(CI)F /T + + 2. Vérifier permissions modèles .BGC : + icacls "C:\\Users\\Public\\Documents\\Sage" /grant Everyone:(OI)(CI)R /T + + 3. Exécuter l'application avec droits admin (temporairement) + """) + + elif "not found" in erreur_lower or "introuvable" in erreur_lower: + print(""" +❌ PROBLÈME : Fichier modèle introuvable + +💡 SOLUTIONS : + 1. Vérifier que le modèle .BGC existe : + dir "C:\\Users\\Public\\Documents\\Sage\\Entreprise 100c\\*.bgc" /s + + 2. Spécifier le chemin complet du modèle + + 3. Utiliser un modèle par défaut Sage + """) + + elif "com" in erreur_lower or "dispatch" in erreur_lower: + print(""" +❌ PROBLÈME : Erreur COM / Crystal Runtime + +💡 SOLUTIONS : + 1. REDÉMARRER LE SERVEUR (recommandé) + shutdown /r /t 60 + + 2. Réenregistrer les DLL Crystal : + cd "C:\\Program Files\\SAP BusinessObjects\\Crystal Reports for .NET Framework 4.0\\Common\\SAP BusinessObjects Enterprise XI 4.0\\win64_x64" + regsvr32 /s crpe32.dll + regsvr32 /s crxf_pdf.dll + + 3. Vérifier que les services Crystal sont démarrés : + sc start "SAP Crystal Reports Processing Server" + """) + + elif "database" in erreur_lower or "connexion" in erreur_lower: + print(""" +❌ PROBLÈME : Connexion base de données + +💡 SOLUTIONS : + 1. Le modèle Crystal doit pouvoir se connecter à la base Sage + + 2. Vérifier les paramètres de connexion dans le modèle + + 3. Utiliser l'API Sage pour passer les données au lieu de + connecter Crystal directement à la base + """) + + else: + print(f""" +❌ ERREUR : {erreur_msg} + +💡 SOLUTIONS GÉNÉRIQUES : + 1. Redémarrer le serveur + 2. Vérifier les logs détaillés + 3. Tester avec un modèle Crystal simple + 4. Utiliser PDF custom en attendant + """) + + print("="*70) + + +# Routes API pour tests +def creer_routes_test(app, sage_connector): + """ + Ajouter ces routes dans main.py pour tester facilement + """ + + @app.get("/sage/test-crystal-simple") + async def test_crystal_simple(): + """Test Crystal sans Sage""" + success = test_crystal_pdf_simple() + return { + "success": success, + "message": "Crystal opérationnel" if success else "Crystal non disponible" + } + + @app.get("/sage/test-pdf-complet") + async def test_pdf_complet( + numero: str = "FA00123", + type_doc: int = 60 + ): + """Test génération PDF complète avec Sage + Crystal""" + + success, pdf_bytes, message = tester_pdf_avec_sage( + sage_connector, numero, type_doc + ) + + if success: + from fastapi.responses import Response + return Response( + content=pdf_bytes, + media_type="application/pdf", + headers={ + "Content-Disposition": f"attachment; filename={numero}.pdf" + } + ) + else: + # Analyser erreur + analyser_erreur_generation(message) + + from fastapi import HTTPException + raise HTTPException( + status_code=500, + detail={ + "error": message, + "recommandation": "Consulter les logs pour analyse détaillée" + } + ) + + +if __name__ == "__main__": + print("🚀 LANCEMENT TESTS CRYSTAL REPORTS") + + # Test 1 : Crystal seul + crystal_ok = test_crystal_pdf_simple() + + if not crystal_ok: + print("\n⚠️ Crystal non opérationnel") + print(" → Redémarrer le serveur et relancer") + else: + print("\n✅ Crystal OK - Prêt pour génération PDF avec Sage") \ No newline at end of file