Diagnostic devis

This commit is contained in:
Fanilo-Nantenaina 2025-11-28 06:23:19 +03:00
parent c522aa5a64
commit 3505ecfd2b
2 changed files with 281 additions and 72 deletions

195
main.py
View file

@ -866,6 +866,201 @@ def cache_info_get():
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# Script à ajouter temporairement dans main.py pour diagnostiquer
@app.get("/sage/devis/{numero}/diagnostic", dependencies=[Depends(verify_token)])
def diagnostiquer_devis(numero: str):
"""
ENDPOINT DE DIAGNOSTIC: Affiche TOUTES les infos d'un devis
Permet de comprendre pourquoi un devis ne peut pas être transformé
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
# Essayer ReadPiece
persist = factory.ReadPiece(0, numero)
# Si échec, chercher dans List()
if not persist:
logger.info(f"[DIAG] ReadPiece echoue, recherche dans List()...")
index = 1
while index < 10000:
try:
persist_test = factory.List(index)
if persist_test is None:
break
doc_test = win32com.client.CastTo(
persist_test, "IBODocumentVente3"
)
doc_test.Read()
if (
getattr(doc_test, "DO_Type", -1) == 0
and getattr(doc_test, "DO_Piece", "") == numero
):
persist = persist_test
logger.info(f"[DIAG] Trouve a l'index {index}")
break
index += 1
except:
index += 1
if not persist:
raise HTTPException(404, f"Devis {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
# EXTRACTION COMPLÈTE
diagnostic = {
"numero": getattr(doc, "DO_Piece", ""),
"type": getattr(doc, "DO_Type", -1),
"statut": getattr(doc, "DO_Statut", -1),
"statut_libelle": {
0: "Brouillon",
1: "Soumis",
2: "Accepte",
3: "Realise partiellement",
4: "Realise totalement",
5: "Transforme",
6: "Annule",
}.get(getattr(doc, "DO_Statut", -1), "Inconnu"),
"date": str(getattr(doc, "DO_Date", "")),
"total_ht": float(getattr(doc, "DO_TotalHT", 0.0)),
"total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)),
"est_transformable": False,
"raison_blocage": None,
}
# Client
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
diagnostic["client_code"] = getattr(
client_obj, "CT_Num", ""
).strip()
diagnostic["client_intitule"] = getattr(
client_obj, "CT_Intitule", ""
).strip()
except Exception as e:
diagnostic["erreur_client"] = str(e)
# Lignes
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
lignes = []
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
ligne.Read()
article_ref = ""
try:
article_ref = getattr(ligne, "AR_Ref", "").strip()
if not article_ref:
article_obj = getattr(ligne, "Article", None)
if article_obj:
article_obj.Read()
article_ref = getattr(article_obj, "AR_Ref", "").strip()
except:
pass
lignes.append(
{
"index": index,
"article": article_ref,
"designation": getattr(ligne, "DL_Design", ""),
"quantite": float(getattr(ligne, "DL_Qte", 0.0)),
"prix_unitaire": float(
getattr(ligne, "DL_PrixUnitaire", 0.0)
),
"montant_ht": float(getattr(ligne, "DL_MontantHT", 0.0)),
}
)
index += 1
except:
break
diagnostic["nb_lignes"] = len(lignes)
diagnostic["lignes"] = lignes
# ANALYSE TRANSFORMABILITÉ
statut = diagnostic["statut"]
if statut == 5:
diagnostic["raison_blocage"] = "Document deja transforme (statut=5)"
elif statut == 6:
diagnostic["raison_blocage"] = "Document annule (statut=6)"
elif statut in [3, 4]:
diagnostic["raison_blocage"] = (
f"Document deja realise partiellement ou totalement (statut={statut}). "
f"Une commande/BL/facture existe probablement deja."
)
diagnostic["suggestion"] = (
"Cherchez les documents lies a ce devis dans Sage. "
"Il a peut-etre deja ete transforme manuellement."
)
elif statut == 0:
diagnostic["est_transformable"] = True
diagnostic["action_requise"] = (
"Statut 'Brouillon'. Le systeme le passera automatiquement a 'Accepte' "
"avant transformation."
)
elif statut == 2:
diagnostic["est_transformable"] = True
diagnostic["action_requise"] = (
"Statut 'Accepte'. Transformation possible."
)
else:
diagnostic["raison_blocage"] = f"Statut inconnu ou non gere: {statut}"
# Champs libres (pour Universign, etc.)
champs_libres = {}
try:
for champ in ["UniversignID", "DerniereRelance", "DO_Ref"]:
try:
valeur = getattr(doc, f"DO_{champ}", None)
if valeur:
champs_libres[champ] = str(valeur)
except:
pass
except:
pass
if champs_libres:
diagnostic["champs_libres"] = champs_libres
logger.info(
f"[DIAG] Devis {numero}: statut={statut}, transformable={diagnostic['est_transformable']}"
)
return {"success": True, "diagnostic": diagnostic}
except HTTPException:
raise
except Exception as e:
logger.error(f"[DIAG] Erreur diagnostic: {e}", exc_info=True)
raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# LANCEMENT # LANCEMENT
# ===================================================== # =====================================================

View file

@ -1026,25 +1026,27 @@ class SageConnector:
def transformer_document(self, numero_source, type_source, type_cible): def transformer_document(self, numero_source, type_source, type_cible):
""" """
Transformation avec transaction Transformation de document avec gestion complète des statuts
CORRECTIONS:
- Validation stricte des types CORRECTIONS:
- Gestion explicite des statuts Sage - Pas d'émojis dans les logs
- Meilleure gestion d'erreurs - Validation stricte des statuts Sage
- Gestion des documents "Réalisé partiellement"
- Meilleure détection d'erreurs
""" """
if not self.cial: if not self.cial:
raise RuntimeError("Connexion Sage non établie") raise RuntimeError("Connexion Sage non etablie")
# ✅ CORRECTION 1: Convertir en int si enum passé # Convertir en int si enum
type_source = int(type_source) type_source = int(type_source)
type_cible = int(type_cible) type_cible = int(type_cible)
logger.info( logger.info(
f"🔄 Transformation demandée: {numero_source} " f"[TRANSFORM] Demande: {numero_source} "
f"(type {type_source}) type {type_cible}" f"(type {type_source}) -> type {type_cible}"
) )
# ✅ CORRECTION 2: Validation des types AVANT d'accéder à Sage # Validation des types
types_valides = {0, 1, 2, 3, 4, 5} types_valides = {0, 1, 2, 3, 4, 5}
if type_source not in types_valides or type_cible not in types_valides: if type_source not in types_valides or type_cible not in types_valides:
raise ValueError( raise ValueError(
@ -1052,24 +1054,23 @@ class SageConnector:
f"Valeurs valides: {types_valides}" f"Valeurs valides: {types_valides}"
) )
# ✅ CORRECTION 3: Matrice de transformations autorisées par Sage # Matrice de transformations Sage 100c
# Basé sur la doc Sage 100c
transformations_autorisees = { transformations_autorisees = {
(0, 3): "Devis Commande", (0, 3): "Devis -> Commande",
(0, 1): "Devis Bon de livraison", (0, 1): "Devis -> Bon de livraison",
(3, 1): "Commande Bon de livraison", (3, 1): "Commande -> Bon de livraison",
(3, 4): "Commande → Préparation", (3, 4): "Commande -> Preparation",
(1, 5): "Bon de livraison Facture", (1, 5): "Bon de livraison -> Facture",
(4, 1): "Préparation → Bon de livraison", (4, 1): "Preparation -> Bon de livraison",
} }
if (type_source, type_cible) not in transformations_autorisees: if (type_source, type_cible) not in transformations_autorisees:
raise ValueError( raise ValueError(
f"❌ Transformation non autorisée par Sage: " f"Transformation non autorisee par Sage: "
f"{type_source} {type_cible}. " f"{type_source} -> {type_cible}. "
f"Transformations valides:\n" f"Valides: "
+ "\n".join( + ", ".join(
f" - {k}: {v}" for k, v in transformations_autorisees.items() f"{k[0]}->{k[1]}" for k in transformations_autorisees.keys()
) )
) )
@ -1080,8 +1081,7 @@ class SageConnector:
persist_source = factory.ReadPiece(type_source, numero_source) persist_source = factory.ReadPiece(type_source, numero_source)
if not persist_source: if not persist_source:
# ✅ CORRECTION 4: Chercher dans List() si ReadPiece échoue logger.warning(f"ReadPiece failed, searching in List()...")
logger.warning(f"ReadPiece échoué, recherche dans List()...")
persist_source = self._find_document_in_list( persist_source = self._find_document_in_list(
numero_source, type_source numero_source, type_source
) )
@ -1094,60 +1094,72 @@ class SageConnector:
doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3") doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3")
doc_source.Read() doc_source.Read()
# ✅ CORRECTION 5: Vérifications de statut AVANT transformation # VÉRIFICATIONS STATUT
statut_actuel = getattr(doc_source, "DO_Statut", 0) statut_actuel = getattr(doc_source, "DO_Statut", 0)
type_reel = getattr(doc_source, "DO_Type", -1) type_reel = getattr(doc_source, "DO_Type", -1)
logger.info( logger.info(
f"📊 Document source: type={type_reel}, statut={statut_actuel}, " f"[TRANSFORM] Document source: type={type_reel}, "
f"numéro={numero_source}" f"statut={statut_actuel}, numero={numero_source}"
) )
# Vérifier cohérence type # Vérifier cohérence type
if type_reel != type_source: if type_reel != type_source:
raise ValueError( raise ValueError(
f"Incohérence: document {numero_source} est de type {type_reel}, " f"Incoherence: document {numero_source} est de type {type_reel}, "
f"pas de type {type_source}" f"pas de type {type_source}"
) )
# ✅ CORRECTION 6: Règles de statut Sage pour transformations # RÈGLES DE STATUT SAGE
# Statuts Sage: 0=Brouillon, 1=Soumis, 2=Accepté, 3=Réalisé partiellement, # 0=Brouillon, 1=Soumis, 2=Accepte, 3=Realise partiellement,
# 4=Réalisé totalement, 5=Transformé, 6=Annulé # 4=Realise totalement, 5=Transforme, 6=Annule
# CORRECTION CRITIQUE: Statut 3 = "Réalisé partiellement"
# Cela signifie qu'une partie du document a déjà été transformée
# mais pas tout. Sage REFUSE de créer un nouveau document dans ce cas.
if statut_actuel == 5: if statut_actuel == 5:
raise ValueError( raise ValueError(
f"Document {numero_source} déjà transformé (statut=5). " f"Document {numero_source} deja transforme (statut=5). "
f"Impossible de le transformer à nouveau." f"Impossible de le transformer a nouveau."
) )
if statut_actuel == 6: if statut_actuel == 6:
raise ValueError( raise ValueError(
f"Document {numero_source} annulé (statut=6). " f"Document {numero_source} annule (statut=6). "
f"Impossible de le transformer." f"Impossible de le transformer."
) )
# ✅ CORRECTION 7: Forcer statut "Accepté" si nécessaire # CORRECTION: Statut 3 ou 4 = document déjà réalisé/livré
if type_source == 0 and statut_actuel == 0: # Devis brouillon if statut_actuel in [3, 4]:
raise ValueError(
f"Document {numero_source} deja realise (statut={statut_actuel}). "
f"Ce document a deja ete transforme partiellement ou totalement. "
f"Verifiez si une commande/BL/facture n'existe pas deja pour ce document."
)
# Forcer statut "Accepté" si brouillon
if type_source == 0 and statut_actuel == 0:
logger.warning( logger.warning(
f"⚠️ Devis en brouillon (statut=0), " f"[TRANSFORM] Devis en brouillon (statut=0), "
f"passage à 'Accepté' (statut=2) requis pour transformation" f"passage a 'Accepte' (statut=2)"
) )
try: try:
doc_source.DO_Statut = 2 # Accepté doc_source.DO_Statut = 2
doc_source.Write() doc_source.Write()
logger.info(f"✅ Statut changé: 0 → 2") logger.info(f"[TRANSFORM] Statut change: 0 -> 2")
# Re-lire pour confirmer # Re-lire
doc_source.Read() doc_source.Read()
nouveau_statut = getattr(doc_source, "DO_Statut", 0) nouveau_statut = getattr(doc_source, "DO_Statut", 0)
if nouveau_statut != 2: if nouveau_statut != 2:
raise RuntimeError( raise RuntimeError(
f"Échec changement statut: toujours à {nouveau_statut}" f"Echec changement statut: toujours a {nouveau_statut}"
) )
except Exception as e: except Exception as e:
raise RuntimeError( raise RuntimeError(
f"Impossible de changer le statut du devis: {e}. " f"Impossible de changer le statut du devis: {e}. "
f"Le devis doit être accepté avant transformation." f"Le devis doit etre accepte avant transformation."
) )
# Récupérer client # Récupérer client
@ -1170,22 +1182,23 @@ class SageConnector:
try: try:
self.cial.CptaApplication.BeginTrans() self.cial.CptaApplication.BeginTrans()
transaction_active = True transaction_active = True
logger.debug("✅ Transaction démarrée") logger.debug("[TRANSFORM] Transaction demarree")
except Exception as e: except Exception as e:
logger.warning(f"⚠️ BeginTrans échoué: {e}") logger.warning(f"[TRANSFORM] BeginTrans echoue: {e}")
try: try:
# ✅ CORRECTION 8: Créer le process avec le type cible VALIDÉ # CRÉATION DOCUMENT CIBLE
logger.info(f"🔨 CreateProcess_Document({type_cible})...") logger.info(f"[TRANSFORM] CreateProcess_Document({type_cible})...")
try: try:
process = self.cial.CreateProcess_Document(type_cible) process = self.cial.CreateProcess_Document(type_cible)
except Exception as e: except Exception as e:
logger.error( logger.error(
f"❌ CreateProcess_Document échoué pour type {type_cible}: {e}" f"[TRANSFORM] CreateProcess_Document echoue pour type {type_cible}: {e}"
) )
raise RuntimeError( raise RuntimeError(
f"Sage refuse de créer un document de type {type_cible}. " f"Sage refuse de creer un document de type {type_cible}. "
f"Verifiez la configuration Sage et les permissions. "
f"Erreur: {e}" f"Erreur: {e}"
) )
@ -1198,7 +1211,7 @@ class SageConnector:
except: except:
pass pass
logger.info(f"📄 Document cible créé (type {type_cible})") logger.info(f"[TRANSFORM] Document cible cree (type {type_cible})")
# Associer client # Associer client
try: try:
@ -1214,9 +1227,9 @@ class SageConnector:
client_obj_cible.Read() client_obj_cible.Read()
doc_cible.SetDefaultClient(client_obj_cible) doc_cible.SetDefaultClient(client_obj_cible)
doc_cible.Write() doc_cible.Write()
logger.info(f"👤 Client {client_code} associé") logger.info(f"[TRANSFORM] Client {client_code} associe")
except Exception as e: except Exception as e:
logger.error(f" Erreur association client: {e}") logger.error(f"[TRANSFORM] Erreur association client: {e}")
raise raise
# Date # Date
@ -1340,17 +1353,17 @@ class SageConnector:
if nb_lignes == 0: if nb_lignes == 0:
raise RuntimeError( raise RuntimeError(
f"Aucune ligne copiée. Erreurs: {'; '.join(erreurs_lignes)}" f"Aucune ligne copiee. Erreurs: {'; '.join(erreurs_lignes)}"
) )
logger.info(f"{nb_lignes} lignes copiées") logger.info(f"[TRANSFORM] {nb_lignes} lignes copiees")
# ===== VALIDATION ===== # ===== VALIDATION =====
doc_cible.Write() doc_cible.Write()
logger.info("💾 Document cible écrit") logger.info("[TRANSFORM] Document cible ecrit")
process.Process() process.Process()
logger.info("⚙️ Process() exécuté") logger.info("[TRANSFORM] Process() execute")
# Récupérer numéro # Récupérer numéro
numero_cible = None numero_cible = None
@ -1369,24 +1382,28 @@ class SageConnector:
numero_cible = getattr(doc_cible, "DO_Piece", "") numero_cible = getattr(doc_cible, "DO_Piece", "")
if not numero_cible: if not numero_cible:
raise RuntimeError("Numéro document cible vide après création") raise RuntimeError("Numero document cible vide apres creation")
# Commit # Commit
if transaction_active: if transaction_active:
self.cial.CptaApplication.CommitTrans() self.cial.CptaApplication.CommitTrans()
logger.info("✅ Transaction committée") logger.info("[TRANSFORM] Transaction committee")
# MAJ statut source Transformé # MAJ statut source -> Transformé
try: try:
doc_source.DO_Statut = 5 # Transformé doc_source.DO_Statut = 5
doc_source.Write() doc_source.Write()
logger.info(f"✅ Statut source mis à jour: → 5 (TRANSFORMÉ)") logger.info(
f"[TRANSFORM] Statut source mis a jour: -> 5 (TRANSFORME)"
)
except Exception as e: except Exception as e:
logger.warning(f"⚠️ Impossible de MAJ statut source: {e}") logger.warning(
f"[TRANSFORM] Impossible de MAJ statut source: {e}"
)
logger.info( logger.info(
f"✅✅✅ TRANSFORMATION RÉUSSIE: " f"[TRANSFORM] SUCCES: "
f"{numero_source} ({type_source}) " f"{numero_source} ({type_source}) -> "
f"{numero_cible} ({type_cible}) - {nb_lignes} lignes" f"{numero_cible} ({type_cible}) - {nb_lignes} lignes"
) )
@ -1401,20 +1418,17 @@ class SageConnector:
if transaction_active: if transaction_active:
try: try:
self.cial.CptaApplication.RollbackTrans() self.cial.CptaApplication.RollbackTrans()
logger.error("❌ Transaction annulée") logger.error("[TRANSFORM] Transaction annulee")
except: except:
pass pass
raise raise
except Exception as e: except Exception as e:
logger.error(f"❌ Erreur transformation: {e}", exc_info=True) logger.error(f"[TRANSFORM] Erreur: {e}", exc_info=True)
raise RuntimeError(f"Échec transformation: {str(e)}") raise RuntimeError(f"Echec transformation: {str(e)}")
def _find_document_in_list(self, numero, type_doc): def _find_document_in_list(self, numero, type_doc):
""" """Cherche un document dans List() si ReadPiece échoue"""
NOUVEAU: Cherche un document dans List() si ReadPiece échoue
Utile pour les documents en brouillon
"""
try: try:
factory = self.cial.FactoryDocumentVente factory = self.cial.FactoryDocumentVente
index = 1 index = 1