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 from typing import Dict
import win32com.client import win32com.client
import win32com.client.dynamic
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -20,7 +29,7 @@ def get_statut_validation(connector, numero_facture: str) -> Dict:
""" """
SELECT SELECT
DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC, 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 FROM F_DOCENTETE
WHERE DO_Piece = ? AND DO_Type = 6 WHERE DO_Piece = ? AND DO_Type = 6
""", """,
@ -71,10 +80,9 @@ def _get_facture_info_sql(connector, numero_facture: str) -> Dict:
""" """
SELECT SELECT
DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC, DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC,
ISNULL((SELECT SUM(DR_MontantRegle) FROM F_REGLECH ISNULL(DO_MontantRegle, 0) as MontantRegle,
WHERE DO_Piece = d.DO_Piece AND DO_Type = 6), 0) as MontantRegle,
CT_NumPayeur CT_NumPayeur
FROM F_DOCENTETE d FROM F_DOCENTETE
WHERE DO_Piece = ? AND DO_Type = 6 WHERE DO_Piece = ? AND DO_Type = 6
""", """,
(numero_facture,), (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: 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: if not connector.cial:
raise RuntimeError("Connexion Sage non établie") raise RuntimeError("Connexion Sage non établie")
@ -117,18 +238,14 @@ def valider_facture(connector, numero_facture: str) -> Dict:
# 2. Modification via COM # 2. Modification via COM
try: try:
with connector._com_context(), connector._lock_com: with connector._com_context(), connector._lock_com:
factory = connector.cial.FactoryDocumentVente doc, iface = _get_document_com(connector, numero_facture)
persist = factory.ReadPiece(60, numero_facture) logger.debug(f" Interface utilisée: {iface}")
if not persist: success = _set_do_valide_com(doc, 1)
raise ValueError(f"Impossible de lire la facture {numero_facture}") if not success:
raise RuntimeError("Impossible de définir DO_Valide")
doc = win32com.client.CastTo(persist, "IBODocumentVente3") logger.info(f"✅ Facture {numero_facture} validée (COM via {iface})")
doc.Read()
doc.DO_Valide = 1
doc.Write()
logger.info(f"✅ Facture {numero_facture} validée")
except ValueError: except ValueError:
raise raise
@ -139,7 +256,7 @@ def valider_facture(connector, numero_facture: str) -> Dict:
# 3. Vérification et réponse via SQL # 3. Vérification et réponse via SQL
info_apres = _get_facture_info_sql(connector, numero_facture) info_apres = _get_facture_info_sql(connector, numero_facture)
if info_apres and info_apres["valide"] != 1: 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( return _build_response_sql(
connector, numero_facture, deja_valide=False, action="validation" connector, numero_facture, deja_valide=False, action="validation"
@ -187,18 +304,14 @@ def devalider_facture(connector, numero_facture: str) -> Dict:
# 2. Modification via COM # 2. Modification via COM
try: try:
with connector._com_context(), connector._lock_com: with connector._com_context(), connector._lock_com:
factory = connector.cial.FactoryDocumentVente doc, iface = _get_document_com(connector, numero_facture)
persist = factory.ReadPiece(60, numero_facture) logger.debug(f" Interface utilisée: {iface}")
if not persist: success = _set_do_valide_com(doc, 0)
raise ValueError(f"Impossible de lire la facture {numero_facture}") if not success:
raise RuntimeError("Impossible de définir DO_Valide")
doc = win32com.client.CastTo(persist, "IBODocumentVente3") logger.info(f"✅ Facture {numero_facture} dévalidée (COM via {iface})")
doc.Read()
doc.DO_Valide = 0
doc.Write()
logger.info(f"✅ Facture {numero_facture} dévalidée")
except ValueError: except ValueError:
raise raise
@ -209,7 +322,7 @@ def devalider_facture(connector, numero_facture: str) -> Dict:
# 3. Vérification et réponse via SQL # 3. Vérification et réponse via SQL
info_apres = _get_facture_info_sql(connector, numero_facture) info_apres = _get_facture_info_sql(connector, numero_facture)
if info_apres and info_apres["valide"] != 0: 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( return _build_response_sql(
connector, numero_facture, deja_valide=False, action="devalidation" 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( def _build_response_sql(
connector, numero_facture: str, deja_valide: bool, action: str connector, numero_facture: str, deja_valide: bool, action: str
) -> Dict: ) -> Dict: