""" Validation de factures Sage 100c Module: utils/documents/validation.py (Windows Server) Version diagnostic - Introspection complète pour trouver la méthode de validation """ from typing import Dict, List import win32com.client import logging logger = logging.getLogger(__name__) # ============================================================ # FONCTIONS SQL (LECTURE) # ============================================================ def get_statut_validation(connector, numero_facture: str) -> Dict: """Retourne le statut de validation d'une facture (SQL)""" with connector._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( """ SELECT DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC, ISNULL(DO_MontantRegle, 0), CT_NumPayeur, DO_Date, DO_Ref FROM F_DOCENTETE WHERE DO_Piece = ? AND DO_Type = 6 """, (numero_facture,), ) row = cursor.fetchone() if not row: raise ValueError(f"Facture {numero_facture} introuvable") valide = int(row[0]) if row[0] is not None else 0 statut = int(row[1]) if row[1] is not None else 0 total_ht = float(row[2]) if row[2] else 0.0 total_ttc = float(row[3]) if row[3] else 0.0 montant_regle = float(row[4]) if row[4] else 0.0 client_code = row[5].strip() if row[5] else "" date_facture = row[6] reference = row[7].strip() if row[7] else "" solde = max(0.0, total_ttc - montant_regle) return { "numero_facture": numero_facture, "est_validee": valide == 1, "statut": statut, "statut_libelle": _get_statut_libelle(statut), "peut_etre_modifiee": valide == 0 and statut not in (5, 6), "peut_etre_devalidee": valide == 1 and montant_regle < 0.01 and statut not in (5, 6), "total_ht": total_ht, "total_ttc": total_ttc, "montant_regle": montant_regle, "solde_restant": solde, "client_code": client_code, "date_facture": date_facture.strftime("%Y-%m-%d") if date_facture else None, "reference": reference, } def _get_facture_info_sql(connector, numero_facture: str) -> Dict: """Récupère les infos d'une facture via SQL""" with connector._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( """ SELECT DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC, ISNULL(DO_MontantRegle, 0), CT_NumPayeur FROM F_DOCENTETE WHERE DO_Piece = ? AND DO_Type = 6 """, (numero_facture,), ) row = cursor.fetchone() if not row: return None return { "valide": int(row[0]) if row[0] is not None else 0, "statut": int(row[1]) if row[1] is not None else 0, "total_ht": float(row[2]) if row[2] else 0.0, "total_ttc": float(row[3]) if row[3] else 0.0, "montant_regle": float(row[4]) if row[4] else 0.0, "client_code": row[5].strip() if row[5] else "", } # ============================================================ # INTROSPECTION COMPLÈTE # ============================================================ def introspecter_document_complet(connector, numero_facture: str) -> Dict: """ Introspection COMPLÈTE d'un document pour découvrir toutes les méthodes et propriétés disponibles. """ if not connector.cial: raise RuntimeError("Connexion Sage non établie") result = { "numero_facture": numero_facture, "persist": {}, "IBODocumentVente3": {}, "IBODocument3": {}, "IPMDocument": {}, } try: with connector._com_context(), connector._lock_com: factory = connector.cial.FactoryDocumentVente if not factory.ExistPiece(60, numero_facture): raise ValueError(f"Facture {numero_facture} introuvable") persist = factory.ReadPiece(60, numero_facture) # 1. Attributs du persist brut persist_attrs = [a for a in dir(persist) if not a.startswith("_")] result["persist"]["all_attrs"] = persist_attrs result["persist"]["methods"] = [] result["persist"]["properties"] = [] for attr in persist_attrs: try: val = getattr(persist, attr, None) if callable(val): result["persist"]["methods"].append(attr) else: result["persist"]["properties"].append( { "name": attr, "value": str(val)[:100] if val is not None else None, } ) except Exception as e: result["persist"]["properties"].append( {"name": attr, "error": str(e)[:50]} ) # Chercher spécifiquement les attributs liés à validation/valide result["persist"]["validation_related"] = [ a for a in persist_attrs if any( x in a.lower() for x in ["valid", "lock", "confirm", "statut", "etat"] ) ] # 2. IBODocumentVente3 try: doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() doc_attrs = [a for a in dir(doc) if not a.startswith("_")] result["IBODocumentVente3"]["all_attrs"] = doc_attrs result["IBODocumentVente3"]["methods"] = [] result["IBODocumentVente3"]["properties_with_values"] = [] # Lister les méthodes for attr in doc_attrs: try: val = getattr(doc, attr, None) if callable(val): result["IBODocumentVente3"]["methods"].append(attr) except: pass # Chercher DO_* properties result["IBODocumentVente3"]["DO_properties"] = [] for attr in doc_attrs: if attr.startswith("DO_"): try: val = getattr(doc, attr, "ERROR") result["IBODocumentVente3"]["DO_properties"].append( {"name": attr, "value": str(val)[:50]} ) except Exception as e: result["IBODocumentVente3"]["DO_properties"].append( {"name": attr, "error": str(e)[:50]} ) # Chercher les attributs liés à validation result["IBODocumentVente3"]["validation_related"] = [ a for a in doc_attrs if any( x in a.lower() for x in ["valid", "lock", "confirm", "statut", "etat"] ) ] except Exception as e: result["IBODocumentVente3"]["error"] = str(e) # 3. IBODocument3 try: doc3 = win32com.client.CastTo(persist, "IBODocument3") doc3.Read() doc3_attrs = [a for a in dir(doc3) if not a.startswith("_")] result["IBODocument3"]["all_attrs"] = doc3_attrs result["IBODocument3"]["validation_related"] = [ a for a in doc3_attrs if any( x in a.lower() for x in ["valid", "lock", "confirm", "statut", "etat"] ) ] except Exception as e: result["IBODocument3"]["error"] = str(e) # 4. IPMDocument try: pmdoc = win32com.client.CastTo(persist, "IPMDocument") pmdoc_attrs = [a for a in dir(pmdoc) if not a.startswith("_")] result["IPMDocument"]["all_attrs"] = pmdoc_attrs result["IPMDocument"]["methods"] = [ a for a in pmdoc_attrs if callable(getattr(pmdoc, a, None)) ] except Exception as e: result["IPMDocument"]["error"] = str(e) # 5. Chercher FactoryDocument* sur le document result["factories_on_doc"] = [] for attr in persist_attrs: if "Factory" in attr: result["factories_on_doc"].append(attr) except Exception as e: result["global_error"] = str(e) return result def introspecter_validation(connector, numero_facture: str = None) -> Dict: """ Introspection pour découvrir les méthodes de validation. """ if not connector.cial: raise RuntimeError("Connexion Sage non établie") result = {} try: with connector._com_context(), connector._lock_com: # Tous les CreateProcess cial_attrs = [a for a in dir(connector.cial) if not a.startswith("_")] result["all_createprocess"] = [ a for a in cial_attrs if "CreateProcess" in a ] # Explorer chaque process for process_name in result["all_createprocess"]: try: process = getattr(connector.cial, process_name)() process_attrs = [a for a in dir(process) if not a.startswith("_")] result[process_name] = { "attrs": process_attrs, "has_valider": "Valider" in process_attrs or "Valid" in str(process_attrs), "has_document": "Document" in process_attrs, } except Exception as e: result[process_name] = {"error": str(e)} # Introspection document si fourni if numero_facture: result["document"] = introspecter_document_complet( connector, numero_facture ) except Exception as e: result["global_error"] = str(e) return result def valider_facture(connector, numero_facture: str) -> Dict: """ Valide une facture via SQL direct ⚠️ Contourne les règles métier Sage - à utiliser avec précaution """ logger.info(f"🔒 Validation facture {numero_facture} (SQL direct)") # Vérifications préalables with connector._get_sql_connection() as conn: cursor = conn.cursor() # Vérifier que la facture existe et peut être validée cursor.execute( """ SELECT DO_Valide, DO_Statut, DO_TotalTTC, DO_MontantRegle FROM F_DOCENTETE WHERE DO_Piece = ? AND DO_Type = 6 """, (numero_facture,), ) row = cursor.fetchone() if not row: raise ValueError(f"Facture {numero_facture} introuvable") valide_avant, statut, total_ttc, montant_regle = row if valide_avant == 1: return {"numero_facture": numero_facture, "deja_valide": True} if statut == 6: # Annulé raise ValueError("Facture annulée, validation impossible") # Valider via SQL cursor.execute( """ UPDATE F_DOCENTETE SET DO_Valide = 1, DO_Imprim = 1 WHERE DO_Piece = ? AND DO_Type = 6 """, (numero_facture,), ) conn.commit() # Vérifier cursor.execute( """ SELECT DO_Valide, DO_Imprim FROM F_DOCENTETE WHERE DO_Piece = ? AND DO_Type = 6 """, (numero_facture,), ) valide_apres, imprim_apres = cursor.fetchone() logger.info(f" SQL: DO_Valide={valide_apres}, DO_Imprim={imprim_apres}") return { "numero_facture": numero_facture, "methode": "SQL_DIRECT", "DO_Valide": valide_apres == 1, "DO_Imprim": imprim_apres == 1, "success": valide_apres == 1, "warning": "Validation SQL directe - règles métier Sage contournées", } def devalider_facture(connector, numero_facture: str) -> Dict: """ Dévalide une facture (retire le cadenas) """ if not connector.cial: raise RuntimeError("Connexion Sage non établie") logger.info(f"🔓 Dévalidation facture {numero_facture}") info = _get_facture_info_sql(connector, numero_facture) if not info: raise ValueError(f"Facture {numero_facture} introuvable") if info["statut"] == 6: raise ValueError(f"Facture {numero_facture} annulée") if info["statut"] == 5: raise ValueError( f"Facture {numero_facture} transformée, dévalidation impossible" ) if info["montant_regle"] > 0.01: raise ValueError( f"Facture {numero_facture} partiellement réglée ({info['montant_regle']:.2f}€), " "dévalidation impossible" ) if info["valide"] == 0: logger.info(f"Facture {numero_facture} déjà non validée") return _build_response_sql( connector, numero_facture, deja_valide=True, action="devalidation" ) try: with connector._com_context(), connector._lock_com: success = _valider_document_com(connector, numero_facture, valider=False) if not success: raise RuntimeError("La dévalidation COM a échoué") logger.info(f"✅ Facture {numero_facture} dévalidée") except ValueError: raise except Exception as e: logger.error(f"❌ Erreur COM dévalidation {numero_facture}: {e}", exc_info=True) raise RuntimeError(f"Échec dévalidation: {str(e)}") 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 l'opération COM" ) return _build_response_sql( connector, numero_facture, deja_valide=False, action="devalidation" ) def _valider_document_com(connector, numero_facture: str, valider: bool = True) -> bool: """ Tente de valider/dévalider un document via COM. """ erreurs = [] action = "validation" if valider else "dévalidation" valeur_cible = 1 if valider else 0 factory = connector.cial.FactoryDocumentVente 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: Accès direct à DO_Valide sur IBODocumentVente3 try: logger.info( " APPROCHE 1: Modification directe DO_Valide sur IBODocumentVente3..." ) doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() # Vérifier la valeur actuelle valeur_avant = getattr(doc, "DO_Valide", None) logger.info(f" DO_Valide avant: {valeur_avant}") # Tenter la modification doc.DO_Valide = valeur_cible doc.Write() # Relire pour vérifier doc.Read() valeur_apres = getattr(doc, "DO_Valide", None) logger.info(f" DO_Valide après: {valeur_apres}") if valeur_apres == valeur_cible: logger.info(f" ✅ DO_Valide modifié avec succès!") return True else: erreurs.append( f"DO_Valide non modifié (avant={valeur_avant}, après={valeur_apres})" ) except Exception as e: erreurs.append(f"IBODocumentVente3.DO_Valide: {e}") logger.warning(f" Erreur: {e}") # APPROCHE 2: Via IBODocument3 (interface parent) try: logger.info(" APPROCHE 2: Via IBODocument3...") doc3 = win32com.client.CastTo(persist, "IBODocument3") doc3.Read() if hasattr(doc3, "DO_Valide"): doc3.DO_Valide = valeur_cible doc3.Write() logger.info(f" IBODocument3.DO_Valide = {valeur_cible} OK") return True else: erreurs.append("IBODocument3 n'a pas DO_Valide") except Exception as e: erreurs.append(f"IBODocument3: {e}") # APPROCHE 3: Chercher un CreateProcess de validation try: logger.info(" APPROCHE 3: Recherche CreateProcess de validation...") cial_attrs = [a for a in dir(connector.cial) if "CreateProcess" in a] validation_processes = [ a for a in cial_attrs if any(x in a.lower() for x in ["valid", "confirm", "lock"]) ] logger.info(f" CreateProcess trouvés: {cial_attrs}") logger.info(f" Liés à validation: {validation_processes}") for proc_name in validation_processes: try: process = getattr(connector.cial, proc_name)() # Lister les attributs du process proc_attrs = [a for a in dir(process) if not a.startswith("_")] logger.info(f" {proc_name} attrs: {proc_attrs}") if hasattr(process, "Document"): process.Document = persist if hasattr(process, "Valider"): process.Valider = valider if hasattr(process, "Process"): process.Process() logger.info(f" {proc_name}.Process() exécuté!") return True except Exception as e: erreurs.append(f"{proc_name}: {e}") except Exception as e: erreurs.append(f"CreateProcess: {e}") # APPROCHE 4: WriteDefault avec paramètres try: logger.info(" APPROCHE 4: WriteDefault...") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() doc.DO_Valide = valeur_cible if hasattr(doc, "WriteDefault"): doc.WriteDefault() logger.info(" WriteDefault() exécuté") return True except Exception as e: erreurs.append(f"WriteDefault: {e}") logger.error(f" Toutes les approches de {action} ont échoué: {erreurs}") raise RuntimeError(f"Échec {action}: {'; '.join(erreurs[:5])}") def explorer_impression_validation(connector, numero_facture: str) -> Dict: """Explorer les méthodes d'impression/validation pour les factures""" result = {"numero_facture": numero_facture} with connector._com_context(), connector._lock_com: factory = connector.cial.FactoryDocumentVente persist = factory.ReadPiece(60, numero_facture) doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() # 1. CreateProcess_Document SANS paramètre try: process = connector.cial.CreateProcess_Document() attrs = [a for a in dir(process) if not a.startswith("_")] result["CreateProcess_Document_no_param"] = { "attrs": attrs, "print_related": [ a for a in attrs if any( x in a.lower() for x in ["print", "imprim", "edit", "model", "bgc", "valid"] ) ], } # Essayer d'assigner le document if "Document" in attrs: try: process.Document = doc result["CreateProcess_Document_no_param"]["Document_assigned"] = ( True ) except Exception as e: result["CreateProcess_Document_no_param"]["Document_error"] = str(e) if "SetDocument" in attrs: try: process.SetDocument(doc) result["CreateProcess_Document_no_param"]["SetDocument_ok"] = True except Exception as e: result["CreateProcess_Document_no_param"]["SetDocument_error"] = ( str(e) ) except Exception as e: result["CreateProcess_Document_no_param_error"] = str(e) # 2. CreateProcess_Document avec type (60 = facture) try: process = connector.cial.CreateProcess_Document(60) attrs = [a for a in dir(process) if not a.startswith("_")] result["CreateProcess_Document_type60"] = {"attrs": attrs} except Exception as e: result["CreateProcess_Document_type60_error"] = str(e) # 3. Explorer TOUS les CreateProcess cial_attrs = [a for a in dir(connector.cial) if "CreateProcess" in a] result["all_createprocess"] = cial_attrs # 4. Chercher spécifiquement impression/validation for proc_name in cial_attrs: if any( x in proc_name.lower() for x in ["imprim", "print", "edit", "valid", "confirm"] ): try: proc = getattr(connector.cial, proc_name)() proc_attrs = [a for a in dir(proc) if not a.startswith("_")] result[proc_name] = { "attrs": proc_attrs, "has_modele": [ a for a in proc_attrs if "model" in a.lower() or "bgc" in a.lower() ], "has_document": "Document" in proc_attrs or "SetDocument" in proc_attrs, } except Exception as e: result[proc_name] = {"error": str(e)} # 5. Explorer le document pour méthodes Print/Imprimer doc_attrs = [a for a in dir(doc) if not a.startswith("_")] result["doc_print_methods"] = [ a for a in doc_attrs if any(x in a.lower() for x in ["print", "imprim", "edit", "valid"]) ] # 6. Chercher IPMDocument (Process Manager) try: pm = win32com.client.CastTo(persist, "IPMDocument") pm_attrs = [a for a in dir(pm) if not a.startswith("_")] result["IPMDocument"] = { "attrs": pm_attrs, "print_related": [ a for a in pm_attrs if any( x in a.lower() for x in ["print", "imprim", "edit", "valid", "process"] ) ], } except Exception as e: result["IPMDocument_error"] = str(e) # 7. Chemin du modèle BGC result["modele_bgc_path"] = ( r"C:\Users\Public\Documents\Sage\Entreprise 100c\fr-FR\Documents standards\Gestion commerciale\Ventes\Facture client.bgc" ) return result # ============================================================ # FONCTIONS UTILITAIRES # ============================================================ def _build_response_sql( connector, numero_facture: str, deja_valide: bool, action: str ) -> Dict: """Construit la réponse via SQL""" info = _get_facture_info_sql(connector, numero_facture) 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" ) 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, } 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}") __all__ = [ "valider_facture", "devalider_facture", "get_statut_validation", "introspecter_validation", "introspecter_document_complet", ]