Another try
This commit is contained in:
parent
bd48bf5aac
commit
b66525c00e
1 changed files with 195 additions and 26 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue