from typing import Dict import win32com.client import logging logger = logging.getLogger(__name__) def valider_facture(self, numero_facture: str) -> Dict: """ Valide une facture (pose le cadenas) Une facture validée ne peut plus être modifiée dans Sage. DO_Valide passe de 0 à 1. """ if not self.cial: raise RuntimeError("Connexion Sage non établie") logger.info(f"🔒 Validation facture {numero_facture}") try: with self._com_context(), self._lock_com: factory = self.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}") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() valide_actuel = getattr(doc, "DO_Valide", 0) statut = getattr(doc, "DO_Statut", 0) if statut == 6: raise ValueError( f"Facture {numero_facture} annulée, validation impossible" ) if valide_actuel == 1: logger.info(f"Facture {numero_facture} déjà validée") return _build_response(doc, numero_facture, deja_valide=True) doc.DO_Valide = 1 doc.Write() doc.Read() nouveau_valide = getattr(doc, "DO_Valide", 0) if nouveau_valide != 1: raise RuntimeError("Échec validation: DO_Valide non modifié") logger.info(f"✅ Facture {numero_facture} validée") return _build_response(doc, numero_facture, deja_valide=False) except ValueError: raise except Exception as e: logger.error(f"❌ Erreur validation {numero_facture}: {e}", exc_info=True) raise RuntimeError(f"Échec validation: {str(e)}") def devalider_facture(self, numero_facture: str) -> Dict: """ Dévalide une facture (retire le cadenas) Attention: Une facture avec règlements ne peut pas être dévalidée. DO_Valide passe de 1 à 0. """ if not self.cial: raise RuntimeError("Connexion Sage non établie") logger.info(f"🔓 Dévalidation facture {numero_facture}") try: with self._com_context(), self._lock_com: montant_regle = _get_montant_regle(self, numero_facture) if montant_regle > 0.01: raise ValueError( f"Facture {numero_facture} partiellement réglée ({montant_regle}€), " "dévalidation impossible" ) factory = self.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}") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() valide_actuel = getattr(doc, "DO_Valide", 0) statut = getattr(doc, "DO_Statut", 0) if statut == 6: raise ValueError(f"Facture {numero_facture} annulée") if statut == 5: raise ValueError( f"Facture {numero_facture} transformée, dévalidation impossible" ) if valide_actuel == 0: logger.info(f"Facture {numero_facture} déjà non validée") return _build_response( doc, numero_facture, deja_valide=False, action="devalidation" ) doc.DO_Valide = 0 doc.Write() doc.Read() nouveau_valide = getattr(doc, "DO_Valide", 0) if nouveau_valide != 0: raise RuntimeError("Échec dévalidation: DO_Valide non modifié") logger.info(f"✅ Facture {numero_facture} dévalidée") return _build_response( doc, numero_facture, deja_valide=False, action="devalidation" ) except ValueError: raise except Exception as e: logger.error(f"❌ Erreur dévalidation {numero_facture}: {e}", exc_info=True) raise RuntimeError(f"Échec dévalidation: {str(e)}") def get_statut_validation(self, numero_facture: str) -> Dict: """ Retourne le statut de validation d'une facture """ if not self.cial: raise RuntimeError("Connexion Sage non établie") with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( """ SELECT DO_Valide, DO_Statut, DO_TotalTTC, DO_MontantRegle, 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_ttc = float(row[2]) if row[2] else 0.0 montant_regle = float(row[3]) if row[3] else 0.0 client_code = row[4].strip() if row[4] else "" date_facture = row[5] reference = row[6].strip() if row[6] 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_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_montant_regle(self, numero_facture: str) -> float: """Récupère le montant déjà réglé d'une facture""" with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( """ SELECT ISNULL(SUM(DR_MontantRegle), 0) FROM F_REGLECH WHERE DO_Piece = ? AND DO_Type = 6 """, (numero_facture,), ) row = cursor.fetchone() return float(row[0]) if row else 0.0 def _build_response( doc, numero_facture: str, deja_valide: bool, action: str = "validation" ) -> Dict: """Construit la réponse de validation/dévalidation""" valide = getattr(doc, "DO_Valide", 0) statut = getattr(doc, "DO_Statut", 0) total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) client_code = "" try: client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_code = getattr(client_obj, "CT_Num", "").strip() except Exception: pass 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": valide == 1, "statut": statut, "statut_libelle": _get_statut_libelle(statut), "total_ht": total_ht, "total_ttc": total_ttc, "client_code": client_code, "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", ]