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: # Filtrer les CreateProcess et méthodes liées à la validation
erreurs.append(f"Direct: {e}") 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: except Exception as e:
erreurs.append(f"Direct: {e}") result["global_error"] = str(e)
# Méthode 2: Via setattr return result
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
def _get_document_com(connector, numero_facture: str): # ============================================================
""" # FONCTIONS COM (ÉCRITURE) - À adapter selon l'introspection
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}")
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,67 +354,166 @@ 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")
)
else:
message = (
"Facture déjà non validée"
if deja_valide
else "Facture dévalidée avec succès"
)
return { persist = factory.ReadPiece(60, numero_facture)
"numero_facture": numero_facture, if not persist:
"est_validee": info["valide"] == 1 if info else False, raise ValueError(f"Impossible de lire la facture {numero_facture}")
"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 1: CreateProcess_ValiderBon
try:
process = connector.cial.CreateProcess_ValiderBon()
logger.info(f" Tentative via CreateProcess_ValiderBon...")
def _get_statut_libelle(statut: int) -> str: # Ajouter le document au process
"""Retourne le libellé d'un statut de document""" process.Document = persist
statuts = {
0: "Brouillon",
1: "Confirmé",
2: "En cours",
3: "Imprimé",
4: "Suspendu",
5: "Transformé",
6: "Annulé",
}
return statuts.get(statut, f"Statut {statut}")
# Définir l'action (valider ou dévalider)
try:
process.Valider = valider
except Exception:
pass
__all__ = [ # Vérifier si on peut traiter
"valider_facture", can_process = getattr(process, "CanProcess", True)
"devalider_facture", if can_process:
"get_statut_validation", 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", "valider_facture",
"devalider_facture", "devalider_facture",
"get_statut_validation", "get_statut_validation",
"introspecter_validation",
] ]