from typing import Dict import win32com.client import logging logger = logging.getLogger(__name__) 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 "", } def introspecter_document_complet(connector, numero_facture: str) -> Dict: 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 Exception: 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: 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: 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 = 0 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: 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: 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(" 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_toutes_interfaces_validation(connector, numero_facture: str) -> Dict: """Explorer TOUTES les interfaces possibles pour trouver un setter DO_Valide""" result = {"numero_facture": numero_facture, "interfaces": {}} with connector._com_context(), connector._lock_com: factory = connector.cial.FactoryDocumentVente persist = factory.ReadPiece(60, numero_facture) # Liste des interfaces à tester interfaces = [ "IBODocumentVente3", "IBODocument3", "IBIPersistObject", "IBIDocument", "IPMDocument", "IDispatch", ] for iface_name in interfaces: try: obj = win32com.client.CastTo(persist, iface_name) if hasattr(obj, "Read"): obj.Read() oleobj = obj._oleobj_ type_info = oleobj.GetTypeInfo() type_attr = type_info.GetTypeAttr() props = {} for i in range(type_attr.cFuncs): func_desc = type_info.GetFuncDesc(i) names = type_info.GetNames(func_desc.memid) if names and names[0] in ( "DO_Valide", "DO_Imprim", "Valider", "Validate", "Lock", ): props[names[0]] = { "memid": func_desc.memid, "invkind": func_desc.invkind, # invkind: 1=METHOD, 2=GET, 4=PUT, 8=PUTREF "has_setter": (func_desc.invkind & 4) == 4, } result["interfaces"][iface_name] = { "success": True, "properties": props, } except Exception as e: result["interfaces"][iface_name] = {"error": str(e)[:100]} # Explorer aussi FactoryDocumentVente pour des méthodes de validation try: factory_attrs = [a for a in dir(factory) if not a.startswith("_")] result["factory_methods"] = [ a for a in factory_attrs if any(x in a.lower() for x in ["valid", "lock", "confirm", "imprim"]) ] except Exception: pass return result 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", ]