Another try with introspection

This commit is contained in:
fanilo 2026-01-15 09:33:16 +01:00
parent b66525c00e
commit 68c70de7d9
3 changed files with 301 additions and 155 deletions

10
main.py
View file

@ -1725,6 +1725,16 @@ def introspection_com():
raise HTTPException(500, str(e)) 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__": if __name__ == "__main__":
uvicorn.run( uvicorn.run(
"main:app", "main:app",

View file

@ -107,6 +107,7 @@ from utils.documents.validations import (
valider_facture as _valider, valider_facture as _valider,
devalider_facture as _devalider, devalider_facture as _devalider,
get_statut_validation as _get_statut, get_statut_validation as _get_statut,
introspecter_validation as _introspect,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -8094,3 +8095,6 @@ class SageConnector:
def get_statut_validation(self, numero_facture: str) -> dict: def get_statut_validation(self, numero_facture: str) -> dict:
return _get_statut(self, numero_facture) return _get_statut(self, numero_facture)
def introspecter_validation(self, numero_facture: str = None) -> dict:
return _introspect(self, numero_facture)

View file

@ -3,12 +3,10 @@ Validation de factures Sage 100c
Module: utils/documents/validation.py (Windows Server) Module: utils/documents/validation.py (Windows Server)
COM pour écriture, SQL pour lecture COM pour écriture, SQL pour lecture
Solution robuste avec late binding pour éviter les erreurs intermittentes
""" """
from typing import Dict from typing import Dict
import win32com.client import win32com.client
import win32com.client.dynamic
import logging import logging
logger = logging.getLogger(__name__) 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: try:
doc.DO_Valide = valeur with connector._com_context(), connector._lock_com:
doc.Write() # 1. Méthodes sur cial (BSCIALApplication100c)
logger.debug(" DO_Valide défini via accès direct") cial_attrs = [a for a in dir(connector.cial) if not a.startswith("_")]
return True
except AttributeError as e:
erreurs.append(f"Direct: {e}")
except Exception as e:
erreurs.append(f"Direct: {e}")
# Méthode 2: Via setattr # Filtrer les CreateProcess et méthodes liées à la validation
try: result["cial_createprocess"] = [
setattr(doc, "DO_Valide", valeur) a
doc.Write() for a in cial_attrs
logger.debug(" DO_Valide défini via setattr") if "Process" in a or "Valider" in a or "Valid" in a
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
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: # 2. Si un numéro de facture est fourni, inspecter le document
try: if numero_facture:
doc = win32com.client.CastTo(persist, iface) factory = connector.cial.FactoryDocumentVente
doc.Read() if factory.ExistPiece(60, numero_facture):
logger.debug(f" Document casté vers {iface}") persist = factory.ReadPiece(60, numero_facture)
return doc, iface
except Exception as e:
logger.debug(f" Cast {iface} échoué: {e}")
continue
# Fallback: utiliser persist directement # 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: try:
persist.Read() doc = win32com.client.CastTo(persist, "IBODocumentVente3")
logger.debug(" Utilisation de persist directement (sans cast)") doc.Read()
return persist, "persist" 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: except Exception as e:
raise RuntimeError(f"Impossible d'accéder au document: {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:
result["global_error"] = str(e)
return result
# ============================================================
# FONCTIONS COM (ÉCRITURE) - À adapter selon l'introspection
# ============================================================
def valider_facture(connector, numero_facture: str) -> Dict: 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) Valide une facture (pose le cadenas)
- Vérifications: SQL - Vérifications: SQL
- Modification DO_Valide: COM - Modification: COM (via Process ou méthode appropriée)
- Réponse: SQL - Réponse: SQL
""" """
if not connector.cial: if not connector.cial:
@ -238,14 +274,11 @@ def valider_facture(connector, numero_facture: str) -> Dict:
# 2. Modification via COM # 2. Modification via COM
try: try:
with connector._com_context(), connector._lock_com: with connector._com_context(), connector._lock_com:
doc, iface = _get_document_com(connector, numero_facture) success = _valider_document_com(connector, numero_facture, valider=True)
logger.debug(f" Interface utilisée: {iface}")
success = _set_do_valide_com(doc, 1)
if not success: 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: except ValueError:
raise raise
@ -256,7 +289,9 @@ def valider_facture(connector, numero_facture: str) -> Dict:
# 3. Vérification et réponse via SQL # 3. Vérification et réponse via SQL
info_apres = _get_facture_info_sql(connector, numero_facture) info_apres = _get_facture_info_sql(connector, numero_facture)
if info_apres and info_apres["valide"] != 1: 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( return _build_response_sql(
connector, numero_facture, deja_valide=False, action="validation" 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) Dévalide une facture (retire le cadenas)
- Vérifications: SQL - Vérifications: SQL
- Modification DO_Valide: COM - Modification: COM (via Process ou méthode appropriée)
- Réponse: SQL - Réponse: SQL
""" """
if not connector.cial: if not connector.cial:
@ -304,14 +339,11 @@ def devalider_facture(connector, numero_facture: str) -> Dict:
# 2. Modification via COM # 2. Modification via COM
try: try:
with connector._com_context(), connector._lock_com: with connector._com_context(), connector._lock_com:
doc, iface = _get_document_com(connector, numero_facture) success = _valider_document_com(connector, numero_facture, valider=False)
logger.debug(f" Interface utilisée: {iface}")
success = _set_do_valide_com(doc, 0)
if not success: 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: except ValueError:
raise raise
@ -322,68 +354,167 @@ def devalider_facture(connector, numero_facture: str) -> Dict:
# 3. Vérification et réponse via SQL # 3. Vérification et réponse via SQL
info_apres = _get_facture_info_sql(connector, numero_facture) info_apres = _get_facture_info_sql(connector, numero_facture)
if info_apres and info_apres["valide"] != 0: 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( return _build_response_sql(
connector, numero_facture, deja_valide=False, action="devalidation" connector, numero_facture, deja_valide=False, action="devalidation"
) )
# ============================================================ def _valider_document_com(connector, numero_facture: str, valider: bool = True) -> bool:
# FONCTIONS UTILITAIRES """
# ============================================================ 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( Retourne True si réussi.
connector, numero_facture: str, deja_valide: bool, action: str """
) -> Dict: erreurs = []
"""Construit la réponse via SQL (pas COM)""" valeur_cible = 1 if valider else 0
info = _get_facture_info_sql(connector, numero_facture) action = "validation" if valider else "dévalidation"
if action == "validation": factory = connector.cial.FactoryDocumentVente
message = ( if not factory.ExistPiece(60, numero_facture):
"Facture déjà validée" if deja_valide else "Facture validée avec succès" raise ValueError(f"Facture {numero_facture} introuvable")
)
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...")
# Ajouter le document au process
process.Document = persist
# Définir l'action (valider ou dévalider)
try:
process.Valider = valider
except Exception:
pass
# 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: else:
message = ( # Lire les erreurs
"Facture déjà non validée" try:
if deja_valide errors = process.Errors
else "Facture dévalidée avec succès" 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}")
return { except Exception as e:
"numero_facture": numero_facture, erreurs.append(f"SetFieldValue cast: {e}")
"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,
}
# APPROCHE 4: Accès direct via différentes interfaces
def _get_statut_libelle(statut: int) -> str: interfaces = [
"""Retourne le libellé d'un statut de document""" "IBODocumentVente3",
statuts = { "IBODocumentVente2",
0: "Brouillon", "IBODocumentVente",
1: "Confirmé", "IBODocument3",
2: "En cours",
3: "Imprimé",
4: "Suspendu",
5: "Transformé",
6: "Annulé",
}
return statuts.get(statut, f"Statut {statut}")
__all__ = [
"valider_facture",
"devalider_facture",
"get_statut_validation",
] ]
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])}")
# ============================================================ # ============================================================
# FONCTIONS UTILITAIRES # FONCTIONS UTILITAIRES
@ -438,4 +569,5 @@ __all__ = [
"valider_facture", "valider_facture",
"devalider_facture", "devalider_facture",
"get_statut_validation", "get_statut_validation",
"introspecter_validation",
] ]