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))
# 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
# =====================================================

View file

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