Trying again

This commit is contained in:
fanilo 2026-01-15 09:42:11 +01:00
parent 68c70de7d9
commit 7d58a607f5

View file

@ -2,15 +2,22 @@
Validation de factures Sage 100c 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 (late binding), SQL pour lecture
Solution: Contourner le cache gen_py avec late binding pur
""" """
from typing import Dict from typing import Dict
import win32com.client import win32com.client
import pythoncom
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Constantes COM pour Invoke
DISPATCH_PROPERTYPUT = 4
DISPATCH_PROPERTYGET = 2
DISPATCH_METHOD = 1
# ============================================================ # ============================================================
# FONCTIONS SQL (LECTURE) # FONCTIONS SQL (LECTURE)
@ -101,156 +108,202 @@ def _get_facture_info_sql(connector, numero_facture: str) -> Dict:
# ============================================================ # ============================================================
# INTROSPECTION - Pour découvrir les méthodes de validation # LATE BINDING - Contourner le cache gen_py
# ============================================================ # ============================================================
def introspecter_validation(connector, numero_facture: str = None) -> Dict: def _get_dynamic_dispatch(com_object):
""" """
Introspection pour découvrir les méthodes de validation disponibles dans Sage. Convertit un objet COM early-binding en late-binding dynamique.
Cela contourne le cache gen_py qui marque certaines propriétés en read-only.
Appeler cette fonction pour voir quelles méthodes/process sont disponibles
pour valider un document.
""" """
if not connector.cial:
raise RuntimeError("Connexion Sage non établie")
result = {}
try: try:
with connector._com_context(), connector._lock_com: # Récupérer l'objet COM brut et créer un dispatch dynamique
# 1. Méthodes sur cial (BSCIALApplication100c) oleobj = com_object._oleobj_
cial_attrs = [a for a in dir(connector.cial) if not a.startswith("_")] return win32com.client.Dispatch(oleobj.QueryInterface(pythoncom.IID_IDispatch))
except Exception:
return com_object
# 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 def _set_property_via_invoke(com_object, prop_name: str, value) -> bool:
if numero_facture: """
Définit une propriété COM via Invoke direct (DISPATCH_PROPERTYPUT).
Contourne les restrictions du cache gen_py.
"""
try:
oleobj = com_object._oleobj_
# Récupérer le DISPID de la propriété
dispid = oleobj.GetIDsOfNames(0, prop_name)
if isinstance(dispid, tuple):
dispid = dispid[0]
# Invoke avec PROPERTYPUT
oleobj.Invoke(dispid, 0, DISPATCH_PROPERTYPUT, True, value)
return True
except Exception as e:
logger.debug(f" Invoke PROPERTYPUT échoué pour {prop_name}: {e}")
return False
def _call_method_via_invoke(com_object, method_name: str) -> bool:
"""
Appelle une méthode COM via Invoke direct.
"""
try:
oleobj = com_object._oleobj_
dispid = oleobj.GetIDsOfNames(0, method_name)
if isinstance(dispid, tuple):
dispid = dispid[0]
oleobj.Invoke(dispid, 0, DISPATCH_METHOD, True)
return True
except Exception as e:
logger.debug(f" Invoke METHOD échoué pour {method_name}: {e}")
return False
# ============================================================
# FONCTIONS COM (ÉCRITURE) avec late binding
# ============================================================
def _valider_document_com(connector, numero_facture: str, valider: bool = True) -> bool:
"""
Valide ou dévalide un document via COM en late binding.
Utilise Invoke direct pour contourner les restrictions gen_py.
"""
erreurs = []
valeur_cible = 1 if valider else 0
valeur_bool = True if valider else False
action = "validation" if valider else "dévalidation"
factory = connector.cial.FactoryDocumentVente factory = connector.cial.FactoryDocumentVente
if factory.ExistPiece(60, numero_facture): if not factory.ExistPiece(60, numero_facture):
raise ValueError(f"Facture {numero_facture} introuvable")
persist = factory.ReadPiece(60, numero_facture) persist = factory.ReadPiece(60, numero_facture)
if not persist:
raise ValueError(f"Impossible de lire la facture {numero_facture}")
# Attributs du persist brut # APPROCHE 1: Late binding via Dispatch dynamique
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:
logger.info(" Tentative late binding dynamique...")
# Convertir en dispatch dynamique (late binding)
doc_dyn = _get_dynamic_dispatch(persist)
doc_dyn.Read()
# Lire la valeur actuelle
current = doc_dyn.DO_Valide
logger.info(" DO_Valide actuel (late): {current}")
# Modifier
doc_dyn.DO_Valide = valeur_bool
doc_dyn.Write()
logger.info(" Late binding dynamique réussi!")
return True
except Exception as e:
erreurs.append(f"Late binding dynamique: {e}")
logger.debug(f" Late binding dynamique échoué: {e}")
# APPROCHE 2: Invoke direct avec PROPERTYPUT
try:
logger.info(" Tentative Invoke PROPERTYPUT...")
# Cast puis invoke
doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read() doc.Read()
doc_attrs = [a for a in dir(doc) if not a.startswith("_")]
# Attributs liés à la validation # Essayer avec valeur bool
result["doc_attrs_validation"] = [ success = _set_property_via_invoke(doc, "DO_Valide", valeur_bool)
a if success:
for a in doc_attrs _call_method_via_invoke(doc, "Write")
if "valide" in a.lower() logger.info(" Invoke PROPERTYPUT (bool) réussi!")
or "valid" in a.lower() return True
or "lock" in a.lower()
or "statut" in a.lower()
]
# Toutes les méthodes callable # Essayer avec valeur int
result["doc_methods"] = [ success = _set_property_via_invoke(doc, "DO_Valide", valeur_cible)
a if success:
for a in doc_attrs _call_method_via_invoke(doc, "Write")
if callable(getattr(doc, a, None)) and not a.startswith("_") logger.info(" Invoke PROPERTYPUT (int) réussi!")
] return True
# Valeur actuelle de DO_Valide et DO_Statut erreurs.append("Invoke PROPERTYPUT échoué pour bool et int")
except Exception as e:
erreurs.append(f"Invoke PROPERTYPUT: {e}")
logger.debug(f" Invoke PROPERTYPUT échoué: {e}")
# APPROCHE 3: Dispatch frais sans cache
try: try:
result["DO_Valide_current"] = getattr( logger.info(" Tentative Dispatch frais (sans cache)...")
doc, "DO_Valide", "NOT_FOUND"
)
except Exception as e:
result["DO_Valide_error"] = str(e)
# Créer un nouveau Dispatch sans utiliser le cache gen_py
# En passant par l'IDispatch de l'objet
oleobj = persist._oleobj_
idisp = oleobj.QueryInterface(pythoncom.IID_IDispatch)
# Créer un wrapper dynamique
doc_fresh = win32com.client.dynamic.Dispatch(idisp)
doc_fresh.Read()
logger.info(" DO_Valide actuel: {doc_fresh.DO_Valide}")
doc_fresh.DO_Valide = valeur_bool
doc_fresh.Write()
logger.info(" Dispatch frais réussi!")
return True
except Exception as e:
erreurs.append(f"Dispatch frais: {e}")
logger.debug(f" Dispatch frais échoué: {e}")
# APPROCHE 4: Via IBPersistDocument
try: try:
result["DO_Statut_current"] = getattr( logger.info(" Tentative via IBPersistDocument...")
doc, "DO_Statut", "NOT_FOUND"
) doc = win32com.client.CastTo(persist, "IBPersistDocument")
except Exception as e: doc.Read()
result["DO_Statut_error"] = str(e)
doc_dyn = _get_dynamic_dispatch(doc)
doc_dyn.DO_Valide = valeur_bool
doc_dyn.Write()
logger.info(" IBPersistDocument réussi!")
return True
except Exception as e: except Exception as e:
result["cast_error"] = str(e) erreurs.append(f"IBPersistDocument: {e}")
logger.debug(f" IBPersistDocument échoué: {e}")
# Essayer IBPersistDocument # APPROCHE 5: Accès direct sans Cast
try: try:
persist_doc = win32com.client.CastTo( logger.info(" Tentative accès direct sans Cast...")
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 persist.Read()
for process_name in result.get("cial_createprocess", []): persist_dyn = _get_dynamic_dispatch(persist)
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 logger.info(" DO_Valide actuel: {persist_dyn.DO_Valide}")
validation_processes = [ persist_dyn.DO_Valide = valeur_bool
"CreateProcess_ValiderBon", persist_dyn.Write()
"CreateProcess_ValiderDocument",
"CreateProcess_ValiderPiece", logger.info(" Accès direct sans Cast réussi!")
"CreateProcess_Validation", return True
]
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:
result["global_error"] = str(e) erreurs.append(f"Direct sans Cast: {e}")
logger.debug(f" Direct sans Cast échoué: {e}")
return result logger.error(f" Toutes les approches de {action} ont échoué: {erreurs}")
raise RuntimeError(f"Échec {action}: {'; '.join(erreurs[:3])}")
# ============================================================
# FONCTIONS COM (ÉCRITURE) - À adapter selon l'introspection
# ============================================================
def valider_facture(connector, numero_facture: str) -> Dict: def valider_facture(connector, numero_facture: str) -> Dict:
""" """
Valide une facture (pose le cadenas) Valide une facture (pose le cadenas)
- Vérifications: SQL
- Modification: COM (via Process ou méthode appropriée)
- Réponse: SQL
""" """
if not connector.cial: if not connector.cial:
raise RuntimeError("Connexion Sage non établie") raise RuntimeError("Connexion Sage non établie")
@ -271,7 +324,7 @@ def valider_facture(connector, numero_facture: str) -> Dict:
connector, numero_facture, deja_valide=True, action="validation" connector, numero_facture, deja_valide=True, action="validation"
) )
# 2. Modification via COM # 2. Modification via COM (late binding)
try: try:
with connector._com_context(), connector._lock_com: with connector._com_context(), connector._lock_com:
success = _valider_document_com(connector, numero_facture, valider=True) success = _valider_document_com(connector, numero_facture, valider=True)
@ -301,10 +354,6 @@ def valider_facture(connector, numero_facture: str) -> Dict:
def devalider_facture(connector, numero_facture: str) -> Dict: 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
- Modification: COM (via Process ou méthode appropriée)
- Réponse: SQL
""" """
if not connector.cial: if not connector.cial:
raise RuntimeError("Connexion Sage non établie") raise RuntimeError("Connexion Sage non établie")
@ -336,7 +385,7 @@ def devalider_facture(connector, numero_facture: str) -> Dict:
connector, numero_facture, deja_valide=True, action="devalidation" connector, numero_facture, deja_valide=True, action="devalidation"
) )
# 2. Modification via COM # 2. Modification via COM (late binding)
try: try:
with connector._com_context(), connector._lock_com: with connector._com_context(), connector._lock_com:
success = _valider_document_com(connector, numero_facture, valider=False) success = _valider_document_com(connector, numero_facture, valider=False)
@ -363,157 +412,75 @@ def devalider_facture(connector, numero_facture: str) -> Dict:
) )
def _valider_document_com(connector, numero_facture: str, valider: bool = True) -> bool: # ============================================================
# INTROSPECTION
# ============================================================
def introspecter_validation(connector, numero_facture: str = None) -> Dict:
""" """
Valide ou dévalide un document via COM. Introspection pour découvrir les méthodes de validation disponibles.
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
Retourne True si réussi.
""" """
erreurs = [] if not connector.cial:
valeur_cible = 1 if valider else 0 raise RuntimeError("Connexion Sage non établie")
action = "validation" if valider else "dévalidation"
factory = connector.cial.FactoryDocumentVente result = {}
if not factory.ExistPiece(60, numero_facture):
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:
# 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: try:
logger.info( with connector._com_context(), connector._lock_com:
f" Tentative via SetFieldValue('DO_Valide', {valeur_cible})..." cial_attrs = [a for a in dir(connector.cial) if not a.startswith("_")]
) result["cial_createprocess"] = [
doc.SetFieldValue("DO_Valide", valeur_cible) a
doc.Write() for a in cial_attrs
logger.info(f" SetFieldValue réussi") if "Process" in a or "Valider" in a or "Valid" in a
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: if numero_facture:
try: factory = connector.cial.FactoryDocumentVente
doc = win32com.client.CastTo(persist, iface) if factory.ExistPiece(60, numero_facture):
doc.Read() persist = factory.ReadPiece(60, numero_facture)
logger.info(f" Tentative accès direct DO_Valide via {iface}...") # Test late binding
# Essayer d'accéder et modifier
try: try:
current_val = doc.DO_Valide doc_dyn = _get_dynamic_dispatch(persist)
logger.info(f" Valeur actuelle: {current_val}") doc_dyn.Read()
doc.DO_Valide = valeur_cible
doc.Write() # Lister les attributs via late binding
logger.info(f" Accès direct via {iface} réussi") result["late_binding_test"] = {
return True "DO_Valide_readable": True,
except AttributeError as e: "DO_Valide_value": doc_dyn.DO_Valide,
erreurs.append(f"{iface} DO_Valide AttributeError: {e}") }
# Tester si writable
try:
original = doc_dyn.DO_Valide
doc_dyn.DO_Valide = (
original # Essayer de réécrire la même valeur
)
result["late_binding_test"]["DO_Valide_writable"] = True
except Exception as e: except Exception as e:
erreurs.append(f"{iface} DO_Valide: {e}") result["late_binding_test"]["DO_Valide_writable"] = False
result["late_binding_test"]["write_error"] = str(e)
except Exception as e: except Exception as e:
erreurs.append(f"Cast {iface}: {e}") result["late_binding_error"] = str(e)
# APPROCHE 5: Via IPMDocument si disponible # Test des process
for process_name in result.get("cial_createprocess", []):
if process_name.startswith("CreateProcess"):
try: try:
doc = win32com.client.CastTo(persist, "IPMDocument") process = getattr(connector.cial, process_name)()
logger.info(f" Tentative via IPMDocument...") process_attrs = [
a for a in dir(process) if not a.startswith("_")
if valider: ]
doc.Valider() result[f"{process_name}_attrs"] = process_attrs
else:
doc.Devalider()
doc.Write()
logger.info(f" IPMDocument.{'Valider' if valider else 'Devalider'}() réussi")
return True
except Exception as e: except Exception as e:
erreurs.append(f"IPMDocument: {e}") result[f"{process_name}_error"] = str(e)
logger.error(f" Toutes les approches de {action} ont échoué: {erreurs}") except Exception as e:
raise RuntimeError(f"Échec {action}: {'; '.join(erreurs[:5])}") result["global_error"] = str(e)
return result
# ============================================================ # ============================================================
@ -524,7 +491,7 @@ def _valider_document_com(connector, numero_facture: str, valider: bool = True)
def _build_response_sql( def _build_response_sql(
connector, numero_facture: str, deja_valide: bool, action: str connector, numero_facture: str, deja_valide: bool, action: str
) -> Dict: ) -> Dict:
"""Construit la réponse via SQL (pas COM)""" """Construit la réponse via SQL"""
info = _get_facture_info_sql(connector, numero_facture) info = _get_facture_info_sql(connector, numero_facture)
if action == "validation": if action == "validation":