Diagnostic devis
This commit is contained in:
parent
c522aa5a64
commit
3505ecfd2b
2 changed files with 281 additions and 72 deletions
195
main.py
195
main.py
|
|
@ -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
|
||||
# =====================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue