From 68c70de7d9f0005b86b6b8ad6e3dcc9a90081071 Mon Sep 17 00:00:00 2001 From: fanilo Date: Thu, 15 Jan 2026 09:33:16 +0100 Subject: [PATCH] Another try with introspection --- main.py | 10 + sage_connector.py | 4 + utils/documents/validations.py | 442 +++++++++++++++++++++------------ 3 files changed, 301 insertions(+), 155 deletions(-) diff --git a/main.py b/main.py index a3c941d..f35fe08 100644 --- a/main.py +++ b/main.py @@ -1725,6 +1725,16 @@ def introspection_com(): raise HTTPException(500, str(e)) +@app.get("/sage/debug/introspection-validation") +def introspection_validation(numero_facture: str = None): + try: + resultat = sage.introspecter_validation(numero_facture) + return {"success": True, "data": resultat} + except Exception as e: + logger.error(f"Erreur introspection: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + + if __name__ == "__main__": uvicorn.run( "main:app", diff --git a/sage_connector.py b/sage_connector.py index 93bcb9a..71d6b50 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -107,6 +107,7 @@ from utils.documents.validations import ( valider_facture as _valider, devalider_facture as _devalider, get_statut_validation as _get_statut, + introspecter_validation as _introspect, ) logger = logging.getLogger(__name__) @@ -8094,3 +8095,6 @@ class SageConnector: def get_statut_validation(self, numero_facture: str) -> dict: return _get_statut(self, numero_facture) + + def introspecter_validation(self, numero_facture: str = None) -> dict: + return _introspect(self, numero_facture) diff --git a/utils/documents/validations.py b/utils/documents/validations.py index 7ce7c0a..8062704 100644 --- a/utils/documents/validations.py +++ b/utils/documents/validations.py @@ -3,12 +3,10 @@ Validation de factures Sage 100c Module: utils/documents/validation.py (Windows Server) COM pour écriture, SQL pour lecture -Solution robuste avec late binding pour éviter les erreurs intermittentes """ from typing import Dict import win32com.client -import win32com.client.dynamic import logging logger = logging.getLogger(__name__) @@ -103,109 +101,147 @@ def _get_facture_info_sql(connector, numero_facture: str) -> Dict: # ============================================================ -# FONCTIONS COM (ÉCRITURE) +# INTROSPECTION - Pour découvrir les méthodes de validation # ============================================================ -def _set_do_valide_com(doc, valeur: int) -> bool: +def introspecter_validation(connector, numero_facture: str = None) -> Dict: """ - Définit DO_Valide via COM avec plusieurs méthodes de fallback + Introspection pour découvrir les méthodes de validation disponibles dans Sage. - Retourne True si réussi + Appeler cette fonction pour voir quelles méthodes/process sont disponibles + pour valider un document. """ - erreurs = [] + if not connector.cial: + raise RuntimeError("Connexion Sage non établie") + + result = {} - # Méthode 1: Accès direct (early binding) try: - doc.DO_Valide = valeur - doc.Write() - logger.debug(" DO_Valide défini via accès direct") - return True - except AttributeError as e: - erreurs.append(f"Direct: {e}") + with connector._com_context(), connector._lock_com: + # 1. Méthodes sur cial (BSCIALApplication100c) + cial_attrs = [a for a in dir(connector.cial) if not a.startswith("_")] + + # Filtrer les CreateProcess et méthodes liées à la validation + result["cial_createprocess"] = [ + a + for a in cial_attrs + if "Process" in a or "Valider" in a or "Valid" in a + ] + + # 2. Si un numéro de facture est fourni, inspecter le document + if numero_facture: + factory = connector.cial.FactoryDocumentVente + if factory.ExistPiece(60, numero_facture): + persist = factory.ReadPiece(60, numero_facture) + + # Attributs du persist brut + persist_attrs = [a for a in dir(persist) if not a.startswith("_")] + result["persist_attrs_validation"] = [ + a + for a in persist_attrs + if "valide" in a.lower() + or "valid" in a.lower() + or "lock" in a.lower() + or "statut" in a.lower() + ] + result["persist_attrs_all"] = persist_attrs + + # Cast vers IBODocumentVente3 + try: + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + doc_attrs = [a for a in dir(doc) if not a.startswith("_")] + + # Attributs liés à la validation + result["doc_attrs_validation"] = [ + a + for a in doc_attrs + if "valide" in a.lower() + or "valid" in a.lower() + or "lock" in a.lower() + or "statut" in a.lower() + ] + + # Toutes les méthodes callable + result["doc_methods"] = [ + a + for a in doc_attrs + if callable(getattr(doc, a, None)) and not a.startswith("_") + ] + + # Valeur actuelle de DO_Valide et DO_Statut + try: + result["DO_Valide_current"] = getattr( + doc, "DO_Valide", "NOT_FOUND" + ) + except Exception as e: + result["DO_Valide_error"] = str(e) + + try: + result["DO_Statut_current"] = getattr( + doc, "DO_Statut", "NOT_FOUND" + ) + except Exception as e: + result["DO_Statut_error"] = str(e) + + except Exception as e: + result["cast_error"] = str(e) + + # Essayer IBPersistDocument + try: + persist_doc = win32com.client.CastTo( + persist, "IBPersistDocument" + ) + persist_doc_attrs = [ + a for a in dir(persist_doc) if not a.startswith("_") + ] + result["IBPersistDocument_attrs"] = persist_doc_attrs + except Exception as e: + result["IBPersistDocument_error"] = str(e) + + # 3. Explorer chaque CreateProcess pertinent + for process_name in result.get("cial_createprocess", []): + if process_name.startswith("CreateProcess"): + try: + process = getattr(connector.cial, process_name)() + process_attrs = [ + a for a in dir(process) if not a.startswith("_") + ] + result[f"{process_name}_attrs"] = process_attrs + except Exception as e: + result[f"{process_name}_error"] = str(e) + + # 4. Chercher spécifiquement CreateProcess_ValiderBon ou similaire + validation_processes = [ + "CreateProcess_ValiderBon", + "CreateProcess_ValiderDocument", + "CreateProcess_ValiderPiece", + "CreateProcess_Validation", + ] + for vp in validation_processes: + if vp not in result: + try: + process = getattr(connector.cial, vp)() + process_attrs = [ + a for a in dir(process) if not a.startswith("_") + ] + result[f"{vp}_attrs"] = process_attrs + result[f"{vp}_exists"] = True + except AttributeError: + result[f"{vp}_exists"] = False + except Exception as e: + result[f"{vp}_error"] = str(e) + except Exception as e: - erreurs.append(f"Direct: {e}") + result["global_error"] = str(e) - # Méthode 2: Via setattr - try: - setattr(doc, "DO_Valide", valeur) - doc.Write() - logger.debug(" DO_Valide défini via setattr") - return True - except Exception as e: - erreurs.append(f"setattr: {e}") - - # Méthode 3: Late binding - récupérer l'objet COM sous-jacent - try: - # Accès via _oleobj_ (objet COM brut) - disp = win32com.client.dynamic.Dispatch(doc._oleobj_) - disp.DO_Valide = valeur - disp.Write() - logger.debug(" DO_Valide défini via late binding _oleobj_") - return True - except Exception as e: - erreurs.append(f"Late _oleobj_: {e}") - - # Méthode 4: Invoke direct sur la propriété - try: - import pythoncom - from win32com.client import VARIANT - - # DISPATCH_PROPERTYPUT = 4 - doc._oleobj_.Invoke( - doc._oleobj_.GetIDsOfNames(0, "DO_Valide")[0], 0, 4, 0, valeur - ) - doc.Write() - logger.debug(" DO_Valide défini via Invoke") - return True - except Exception as e: - erreurs.append(f"Invoke: {e}") - - logger.error(f" Toutes les méthodes ont échoué: {erreurs}") - return False + return result -def _get_document_com(connector, numero_facture: str): - """ - Récupère le document COM avec plusieurs tentatives d'interface - - Retourne (doc, interface_utilisee) ou lève une exception - """ - factory = connector.cial.FactoryDocumentVente - - if not factory.ExistPiece(60, numero_facture): - raise ValueError(f"Facture {numero_facture} introuvable dans Sage") - - persist = factory.ReadPiece(60, numero_facture) - if not persist: - raise ValueError(f"Impossible de lire la facture {numero_facture}") - - # Essayer plusieurs interfaces par ordre de préférence - interfaces = [ - "IBODocumentVente3", - "IBODocumentVente2", - "IBODocumentVente", - "IBODocument3", - "IBPersistDocument", - ] - - for iface in interfaces: - try: - doc = win32com.client.CastTo(persist, iface) - doc.Read() - logger.debug(f" Document casté vers {iface}") - return doc, iface - except Exception as e: - logger.debug(f" Cast {iface} échoué: {e}") - continue - - # Fallback: utiliser persist directement - try: - persist.Read() - logger.debug(" Utilisation de persist directement (sans cast)") - return persist, "persist" - except Exception as e: - raise RuntimeError(f"Impossible d'accéder au document: {e}") +# ============================================================ +# FONCTIONS COM (ÉCRITURE) - À adapter selon l'introspection +# ============================================================ def valider_facture(connector, numero_facture: str) -> Dict: @@ -213,7 +249,7 @@ def valider_facture(connector, numero_facture: str) -> Dict: Valide une facture (pose le cadenas) - Vérifications: SQL - - Modification DO_Valide: COM + - Modification: COM (via Process ou méthode appropriée) - Réponse: SQL """ if not connector.cial: @@ -238,14 +274,11 @@ def valider_facture(connector, numero_facture: str) -> Dict: # 2. Modification via COM try: with connector._com_context(), connector._lock_com: - doc, iface = _get_document_com(connector, numero_facture) - logger.debug(f" Interface utilisée: {iface}") - - success = _set_do_valide_com(doc, 1) + success = _valider_document_com(connector, numero_facture, valider=True) if not success: - raise RuntimeError("Impossible de définir DO_Valide") + raise RuntimeError("La validation COM a échoué") - logger.info(f"✅ Facture {numero_facture} validée (COM via {iface})") + logger.info(f"✅ Facture {numero_facture} validée") except ValueError: raise @@ -256,7 +289,9 @@ def valider_facture(connector, numero_facture: str) -> Dict: # 3. Vérification et réponse via SQL info_apres = _get_facture_info_sql(connector, numero_facture) if info_apres and info_apres["valide"] != 1: - raise RuntimeError("Échec validation: DO_Valide non modifié après Write()") + raise RuntimeError( + "Échec validation: DO_Valide non modifié après l'opération COM" + ) return _build_response_sql( connector, numero_facture, deja_valide=False, action="validation" @@ -268,7 +303,7 @@ def devalider_facture(connector, numero_facture: str) -> Dict: Dévalide une facture (retire le cadenas) - Vérifications: SQL - - Modification DO_Valide: COM + - Modification: COM (via Process ou méthode appropriée) - Réponse: SQL """ if not connector.cial: @@ -304,14 +339,11 @@ def devalider_facture(connector, numero_facture: str) -> Dict: # 2. Modification via COM try: with connector._com_context(), connector._lock_com: - doc, iface = _get_document_com(connector, numero_facture) - logger.debug(f" Interface utilisée: {iface}") - - success = _set_do_valide_com(doc, 0) + success = _valider_document_com(connector, numero_facture, valider=False) if not success: - raise RuntimeError("Impossible de définir DO_Valide") + raise RuntimeError("La dévalidation COM a échoué") - logger.info(f"✅ Facture {numero_facture} dévalidée (COM via {iface})") + logger.info(f"✅ Facture {numero_facture} dévalidée") except ValueError: raise @@ -322,67 +354,166 @@ def devalider_facture(connector, numero_facture: str) -> Dict: # 3. Vérification et réponse via SQL info_apres = _get_facture_info_sql(connector, numero_facture) if info_apres and info_apres["valide"] != 0: - raise RuntimeError("Échec dévalidation: DO_Valide non modifié après Write()") + raise RuntimeError( + "Échec dévalidation: DO_Valide non modifié après l'opération COM" + ) return _build_response_sql( connector, numero_facture, deja_valide=False, action="devalidation" ) -# ============================================================ -# FONCTIONS UTILITAIRES -# ============================================================ +def _valider_document_com(connector, numero_facture: str, valider: bool = True) -> bool: + """ + Valide ou dévalide un document via COM. + Essaie plusieurs approches dans l'ordre: + 1. CreateProcess_ValiderBon (si disponible) + 2. Méthode Valider() sur le document + 3. SetFieldValue sur DO_Valide + 4. Accès direct à DO_Valide -def _build_response_sql( - connector, numero_facture: str, deja_valide: bool, action: str -) -> Dict: - """Construit la réponse via SQL (pas COM)""" - info = _get_facture_info_sql(connector, numero_facture) + Retourne True si réussi. + """ + erreurs = [] + valeur_cible = 1 if valider else 0 + action = "validation" if valider else "dévalidation" - if action == "validation": - message = ( - "Facture déjà validée" if deja_valide else "Facture validée avec succès" - ) - else: - message = ( - "Facture déjà non validée" - if deja_valide - else "Facture dévalidée avec succès" - ) + factory = connector.cial.FactoryDocumentVente + if not factory.ExistPiece(60, numero_facture): + raise ValueError(f"Facture {numero_facture} introuvable") - return { - "numero_facture": numero_facture, - "est_validee": info["valide"] == 1 if info else False, - "statut": info["statut"] if info else 0, - "statut_libelle": _get_statut_libelle(info["statut"]) if info else "Inconnu", - "total_ht": info["total_ht"] if info else 0.0, - "total_ttc": info["total_ttc"] if info else 0.0, - "client_code": info["client_code"] if info else "", - "message": message, - "action_effectuee": not deja_valide, - } + persist = factory.ReadPiece(60, numero_facture) + if not persist: + raise ValueError(f"Impossible de lire la facture {numero_facture}") + # APPROCHE 1: CreateProcess_ValiderBon + try: + process = connector.cial.CreateProcess_ValiderBon() + logger.info(f" Tentative via CreateProcess_ValiderBon...") -def _get_statut_libelle(statut: int) -> str: - """Retourne le libellé d'un statut de document""" - statuts = { - 0: "Brouillon", - 1: "Confirmé", - 2: "En cours", - 3: "Imprimé", - 4: "Suspendu", - 5: "Transformé", - 6: "Annulé", - } - return statuts.get(statut, f"Statut {statut}") + # Ajouter le document au process + process.Document = persist + # Définir l'action (valider ou dévalider) + try: + process.Valider = valider + except Exception: + pass -__all__ = [ - "valider_facture", - "devalider_facture", - "get_statut_validation", -] + # Vérifier si on peut traiter + can_process = getattr(process, "CanProcess", True) + if can_process: + process.Process() + logger.info(f" CreateProcess_ValiderBon.Process() réussi") + return True + else: + # Lire les erreurs + try: + errors = process.Errors + if errors and errors.Count > 0: + for i in range(1, errors.Count + 1): + erreurs.append(f"ValiderBon error: {errors.Item(i)}") + except Exception: + pass + + except AttributeError: + erreurs.append("CreateProcess_ValiderBon n'existe pas") + except Exception as e: + erreurs.append(f"CreateProcess_ValiderBon: {e}") + + # APPROCHE 2: Méthode Valider() ou Devalider() sur le document + try: + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + if valider: + method_names = ["Valider", "Validate", "Lock", "SetValide"] + else: + method_names = ["Devalider", "Devalidate", "Unlock", "SetDevalide"] + + for method_name in method_names: + try: + method = getattr(doc, method_name, None) + if method and callable(method): + logger.info(f" Tentative via doc.{method_name}()...") + method() + doc.Write() + logger.info(f" doc.{method_name}() réussi") + return True + except Exception as e: + erreurs.append(f"doc.{method_name}: {e}") + + except Exception as e: + erreurs.append(f"Cast IBODocumentVente3: {e}") + + # APPROCHE 3: SetFieldValue (méthode générique Sage) + try: + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + try: + logger.info( + f" Tentative via SetFieldValue('DO_Valide', {valeur_cible})..." + ) + doc.SetFieldValue("DO_Valide", valeur_cible) + doc.Write() + logger.info(f" SetFieldValue réussi") + return True + except Exception as e: + erreurs.append(f"SetFieldValue: {e}") + + except Exception as e: + erreurs.append(f"SetFieldValue cast: {e}") + + # APPROCHE 4: Accès direct via différentes interfaces + interfaces = [ + "IBODocumentVente3", + "IBODocumentVente2", + "IBODocumentVente", + "IBODocument3", + ] + + for iface in interfaces: + try: + doc = win32com.client.CastTo(persist, iface) + doc.Read() + + logger.info(f" Tentative accès direct DO_Valide via {iface}...") + + # Essayer d'accéder et modifier + try: + current_val = doc.DO_Valide + logger.info(f" Valeur actuelle: {current_val}") + doc.DO_Valide = valeur_cible + doc.Write() + logger.info(f" Accès direct via {iface} réussi") + return True + except AttributeError as e: + erreurs.append(f"{iface} DO_Valide AttributeError: {e}") + except Exception as e: + erreurs.append(f"{iface} DO_Valide: {e}") + + except Exception as e: + erreurs.append(f"Cast {iface}: {e}") + + # APPROCHE 5: Via IPMDocument si disponible + try: + doc = win32com.client.CastTo(persist, "IPMDocument") + logger.info(f" Tentative via IPMDocument...") + + if valider: + doc.Valider() + else: + doc.Devalider() + doc.Write() + logger.info(f" IPMDocument.{'Valider' if valider else 'Devalider'}() réussi") + return True + except Exception as e: + erreurs.append(f"IPMDocument: {e}") + + logger.error(f" Toutes les approches de {action} ont échoué: {erreurs}") + raise RuntimeError(f"Échec {action}: {'; '.join(erreurs[:5])}") # ============================================================ @@ -438,4 +569,5 @@ __all__ = [ "valider_facture", "devalider_facture", "get_statut_validation", + "introspecter_validation", ]