Another try

This commit is contained in:
fanilo 2026-01-15 09:25:52 +01:00
parent bd48bf5aac
commit b66525c00e

View file

@ -1,5 +1,14 @@
"""
Validation de factures Sage 100c
Module: utils/documents/validation.py (Windows Server)
COM pour écriture, SQL pour lecture
Solution robuste avec late binding pour éviter les erreurs intermittentes
"""
from typing import Dict
import win32com.client
import win32com.client.dynamic
import logging
logger = logging.getLogger(__name__)
@ -20,7 +29,7 @@ def get_statut_validation(connector, numero_facture: str) -> Dict:
"""
SELECT
DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC,
DO_MontantRegle, CT_NumPayeur, DO_Date, DO_Ref
ISNULL(DO_MontantRegle, 0), CT_NumPayeur, DO_Date, DO_Ref
FROM F_DOCENTETE
WHERE DO_Piece = ? AND DO_Type = 6
""",
@ -71,10 +80,9 @@ def _get_facture_info_sql(connector, numero_facture: str) -> Dict:
"""
SELECT
DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC,
ISNULL((SELECT SUM(DR_MontantRegle) FROM F_REGLECH
WHERE DO_Piece = d.DO_Piece AND DO_Type = 6), 0) as MontantRegle,
ISNULL(DO_MontantRegle, 0) as MontantRegle,
CT_NumPayeur
FROM F_DOCENTETE d
FROM F_DOCENTETE
WHERE DO_Piece = ? AND DO_Type = 6
""",
(numero_facture,),
@ -94,7 +102,120 @@ def _get_facture_info_sql(connector, numero_facture: str) -> Dict:
}
# ============================================================
# FONCTIONS COM (ÉCRITURE)
# ============================================================
def _set_do_valide_com(doc, valeur: int) -> bool:
"""
Définit DO_Valide via COM avec plusieurs méthodes de fallback
Retourne True si réussi
"""
erreurs = []
# Méthode 1: Accès direct (early binding)
try:
doc.DO_Valide = valeur
doc.Write()
logger.debug(" DO_Valide défini via accès direct")
return True
except AttributeError as e:
erreurs.append(f"Direct: {e}")
except Exception as e:
erreurs.append(f"Direct: {e}")
# Méthode 2: Via setattr
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):
"""
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:
"""
Valide une facture (pose le cadenas)
- Vérifications: SQL
- Modification DO_Valide: COM
- Réponse: SQL
"""
if not connector.cial:
raise RuntimeError("Connexion Sage non établie")
@ -117,18 +238,14 @@ def valider_facture(connector, numero_facture: str) -> Dict:
# 2. Modification via COM
try:
with connector._com_context(), connector._lock_com:
factory = connector.cial.FactoryDocumentVente
persist = factory.ReadPiece(60, numero_facture)
doc, iface = _get_document_com(connector, numero_facture)
logger.debug(f" Interface utilisée: {iface}")
if not persist:
raise ValueError(f"Impossible de lire la facture {numero_facture}")
success = _set_do_valide_com(doc, 1)
if not success:
raise RuntimeError("Impossible de définir DO_Valide")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
doc.DO_Valide = 1
doc.Write()
logger.info(f"✅ Facture {numero_facture} validée")
logger.info(f"✅ Facture {numero_facture} validée (COM via {iface})")
except ValueError:
raise
@ -139,7 +256,7 @@ def valider_facture(connector, numero_facture: str) -> Dict:
# 3. Vérification et réponse via SQL
info_apres = _get_facture_info_sql(connector, numero_facture)
if info_apres and info_apres["valide"] != 1:
raise RuntimeError("Échec validation: DO_Valide non modifié")
raise RuntimeError("Échec validation: DO_Valide non modifié après Write()")
return _build_response_sql(
connector, numero_facture, deja_valide=False, action="validation"
@ -187,18 +304,14 @@ def devalider_facture(connector, numero_facture: str) -> Dict:
# 2. Modification via COM
try:
with connector._com_context(), connector._lock_com:
factory = connector.cial.FactoryDocumentVente
persist = factory.ReadPiece(60, numero_facture)
doc, iface = _get_document_com(connector, numero_facture)
logger.debug(f" Interface utilisée: {iface}")
if not persist:
raise ValueError(f"Impossible de lire la facture {numero_facture}")
success = _set_do_valide_com(doc, 0)
if not success:
raise RuntimeError("Impossible de définir DO_Valide")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
doc.DO_Valide = 0
doc.Write()
logger.info(f"✅ Facture {numero_facture} dévalidée")
logger.info(f"✅ Facture {numero_facture} dévalidée (COM via {iface})")
except ValueError:
raise
@ -209,7 +322,7 @@ def devalider_facture(connector, numero_facture: str) -> Dict:
# 3. Vérification et réponse via SQL
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é")
raise RuntimeError("Échec dévalidation: DO_Valide non modifié après Write()")
return _build_response_sql(
connector, numero_facture, deja_valide=False, action="devalidation"
@ -221,6 +334,62 @@ def devalider_facture(connector, numero_facture: str) -> Dict:
# ============================================================
def _build_response_sql(
connector, numero_facture: str, deja_valide: bool, action: str
) -> Dict:
"""Construit la réponse via SQL (pas COM)"""
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",
]
# ============================================================
# FONCTIONS UTILITAIRES
# ============================================================
def _build_response_sql(
connector, numero_facture: str, deja_valide: bool, action: str
) -> Dict: