Revert "feat: Refactor devis status update to sage_connector with an updated API route and enforce default status 0 for new devis."

This reverts commit c4d2185c22.
This commit is contained in:
Fanilo-Nantenaina 2025-11-28 05:20:43 +03:00
parent c4d2185c22
commit 62077b5862
2 changed files with 285 additions and 161 deletions

40
main.py
View file

@ -243,11 +243,12 @@ def lire_devis(req: CodeRequest):
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/sage/devis/{id}/statut", dependencies=[Depends(verify_token)]) @app.post("/sage/devis/statut", dependencies=[Depends(verify_token)])
def changer_statut_devis(doc_id: str, req: StatutRequest): def changer_statut_devis(doc_id: str, req: StatutRequest):
"""Changement de statut d'un devis""" """Changement de statut d'un devis"""
try: try:
devis_status = sage.changer_statut_devis(doc_id, req.nouveau_statut) # Implémenter via sage_connector
# (À ajouter dans sage_connector si manquant)
return {"success": True, "message": "Statut mis à jour"} return {"success": True, "message": "Statut mis à jour"}
except Exception as e: except Exception as e:
logger.error(f"Erreur MAJ statut: {e}") logger.error(f"Erreur MAJ statut: {e}")
@ -445,6 +446,41 @@ def devis_list(
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/sage/devis/statut", dependencies=[Depends(verify_token)])
def changer_statut_devis_endpoint(numero: str, nouveau_statut: int):
"""Change le statut d'un devis"""
try:
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
persist = factory.ReadPiece(0, numero)
if not persist:
raise HTTPException(404, f"Devis {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
doc.DO_Statut = nouveau_statut
doc.Write()
logger.info(f"✅ Statut devis {numero}: {statut_actuel}{nouveau_statut}")
return {
"success": True,
"data": {
"numero": numero,
"statut_ancien": statut_actuel,
"statut_nouveau": nouveau_statut,
},
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur changement statut: {e}")
raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# ENDPOINTS - DOCUMENTS # ENDPOINTS - DOCUMENTS
# ===================================================== # =====================================================

View file

@ -436,8 +436,8 @@ class SageConnector:
def creer_devis_enrichi(self, devis_data: dict): def creer_devis_enrichi(self, devis_data: dict):
""" """
Création de devis avec statut DEVIS (0) par défaut Création de devis avec transaction Sage
CORRECTION: Force le statut à 0 (Devis) après création SOLUTION FINALE: Utilisation de SetDefaultArticle()
""" """
if not self.cial: if not self.cial:
raise RuntimeError("Connexion Sage non établie") raise RuntimeError("Connexion Sage non établie")
@ -484,12 +484,9 @@ class SageConnector:
date_obj = datetime.now() date_obj = datetime.now()
doc.DO_Date = pywintypes.Time(date_obj) doc.DO_Date = pywintypes.Time(date_obj)
logger.info(f"📅 Date définie: {date_obj.date()}")
# ✅✅✅ CORRECTION CRITIQUE: Forcer le statut à 0 (Devis) # ===== CLIENT (CRITIQUE: Doit être défini AVANT les lignes) =====
doc.DO_Statut = 0
logger.info("📋 Statut forcé à 0 (DEVIS)")
# ===== CLIENT =====
factory_client = self.cial.CptaApplication.FactoryClient factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero( persist_client = factory_client.ReadNumero(
devis_data["client"]["code"] devis_data["client"]["code"]
@ -506,13 +503,14 @@ class SageConnector:
f"❌ Impossible de charger le client {devis_data['client']['code']}" f"❌ Impossible de charger le client {devis_data['client']['code']}"
) )
# ✅ CRITIQUE: Associer le client au document
doc.SetDefaultClient(client_obj) doc.SetDefaultClient(client_obj)
doc.Write() doc.Write()
logger.info( logger.info(
f"👤 Client {devis_data['client']['code']} associé et document écrit" f"👤 Client {devis_data['client']['code']} associé et document écrit"
) )
# ===== LIGNES (code existant inchangé) ===== # ===== LIGNES AVEC SetDefaultArticle() =====
try: try:
factory_lignes = doc.FactoryDocumentLigne factory_lignes = doc.FactoryDocumentLigne
except: except:
@ -523,20 +521,155 @@ class SageConnector:
logger.info(f"📦 Ajout de {len(devis_data['lignes'])} lignes...") logger.info(f"📦 Ajout de {len(devis_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(devis_data["lignes"], 1): for idx, ligne_data in enumerate(devis_data["lignes"], 1):
# ... (code existant pour les lignes - inchangé) logger.info(
pass # Remplacer par votre code existant f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
# ===== VALIDATION ===== # 🔍 ÉTAPE 1: Charger l'article RÉEL depuis Sage pour le prix
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f"❌ Article {ligne_data['article_code']} introuvable dans Sage"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
# 💰 ÉTAPE 2: Récupérer le prix de vente RÉEL
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
logger.info(f"💰 Prix Sage: {prix_sage}")
if prix_sage == 0:
logger.warning(
f"⚠️ ATTENTION: Article {ligne_data['article_code']} a un prix de vente = 0€"
)
# 📝 ÉTAPE 3: Créer la ligne de devis
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
# ✅✅✅ SOLUTION FINALE: SetDefaultArticleReference avec 2 paramètres ✅✅✅
quantite = float(ligne_data["quantite"])
try:
# Méthode 1: Via référence (plus simple et plus fiable)
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
f"✅ Article associé via SetDefaultArticleReference('{ligne_data['article_code']}', {quantite})"
)
except Exception as e:
logger.warning(
f"⚠️ SetDefaultArticleReference échoué: {e}, tentative avec objet article"
)
try:
# Méthode 2: Via objet article
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(
f"✅ Article associé via SetDefaultArticle(obj, {quantite})"
)
except Exception as e2:
logger.error(
f"❌ Toutes les méthodes d'association ont échoué"
)
# Fallback: définir manuellement
ligne_obj.DL_Design = (
designation_sage or ligne_data["designation"]
)
ligne_obj.DL_Qte = quantite
logger.warning("⚠️ Configuration manuelle appliquée")
# ⚙️ ÉTAPE 4: Vérifier le prix automatique chargé
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
logger.info(f"💰 Prix auto chargé: {prix_auto}")
# 💵 ÉTAPE 5: Ajuster le prix si nécessaire
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
# Prix personnalisé fourni
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
logger.info(f"💰 Prix personnalisé: {prix_a_utiliser}")
elif prix_auto == 0:
# Pas de prix auto, forcer le prix Sage
if prix_sage == 0:
raise ValueError(
f"Prix nul pour article {ligne_data['article_code']}"
)
ligne_obj.DL_PrixUnitaire = float(prix_sage)
logger.info(f"💰 Prix Sage forcé: {prix_sage}")
else:
# Prix auto correct, on le garde
logger.info(f"💰 Prix auto conservé: {prix_auto}")
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
montant_ligne = quantite * prix_final
logger.info(f"{quantite} x {prix_final}€ = {montant_ligne}")
# 🎁 Remise
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
montant_apres_remise = montant_ligne * (
1 - remise / 100
)
logger.info(
f"🎁 Remise {remise}% → {montant_apres_remise}"
)
except Exception as e:
logger.warning(f"⚠️ Remise non appliquée: {e}")
# 💾 ÉTAPE 6: Écrire la ligne
ligne_obj.Write()
logger.info(f"✅ Ligne {idx} écrite")
# 🔍 VÉRIFICATION: Relire la ligne pour confirmer
try:
ligne_obj.Read()
prix_enregistre = float(
getattr(ligne_obj, "DL_PrixUnitaire", 0.0)
)
montant_enregistre = float(
getattr(ligne_obj, "DL_MontantHT", 0.0)
)
logger.info(
f"🔍 Vérif: Prix={prix_enregistre}€, Montant HT={montant_enregistre}"
)
if montant_enregistre == 0:
logger.error(
f"❌ PROBLÈME: Montant enregistré = 0 pour ligne {idx}"
)
else:
logger.info(f"✅ Ligne {idx} OK: {montant_enregistre}")
except Exception as e:
logger.warning(f"⚠️ Impossible de vérifier la ligne: {e}")
# ===== VALIDATION DOCUMENT =====
logger.info("💾 Écriture finale du document...") logger.info("💾 Écriture finale du document...")
# ✅ RE-FORCER le statut avant validation finale
doc.DO_Statut = 0
doc.Write() doc.Write()
logger.info("🔄 Lancement du traitement (Process)...") logger.info("🔄 Lancement du traitement (Process)...")
process.Process() process.Process()
# ===== VÉRIFICATION POST-CRÉATION ===== # ===== RÉCUPÉRATION NUMÉRO =====
numero_devis = None numero_devis = None
try: try:
doc_result = process.DocumentResult doc_result = process.DocumentResult
@ -546,17 +679,6 @@ class SageConnector:
) )
doc_result.Read() doc_result.Read()
numero_devis = getattr(doc_result, "DO_Piece", "") numero_devis = getattr(doc_result, "DO_Piece", "")
# ✅ VÉRIFIER et CORRIGER le statut si nécessaire
statut_actuel = getattr(doc_result, "DO_Statut", -1)
if statut_actuel != 0:
logger.warning(
f"⚠️ Statut inattendu: {statut_actuel}, correction..."
)
doc_result.DO_Statut = 0
doc_result.Write()
logger.info("✅ Statut corrigé à 0")
logger.info( logger.info(
f"📄 Numéro (via DocumentResult): {numero_devis}" f"📄 Numéro (via DocumentResult): {numero_devis}"
) )
@ -570,30 +692,34 @@ class SageConnector:
if not numero_devis: if not numero_devis:
raise RuntimeError("❌ Numéro devis vide après création") raise RuntimeError("❌ Numéro devis vide après création")
# ===== COMMIT ===== # ===== COMMIT TRANSACTION =====
if transaction_active: if transaction_active:
self.cial.CptaApplication.CommitTrans() self.cial.CptaApplication.CommitTrans()
logger.info("✅ Transaction committée") logger.info("✅ Transaction committée")
# ===== ATTENTE + RELECTURE ===== # ===== ATTENTE INDEXATION =====
logger.info("⏳ Attente indexation Sage (2s)...") logger.info("⏳ Attente indexation Sage (2s)...")
time.sleep(2) time.sleep(2)
# ===== RELECTURE COMPLÈTE =====
logger.info("🔍 Relecture complète du document...") logger.info("🔍 Relecture complète du document...")
factory_doc = self.cial.FactoryDocumentVente factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(0, numero_devis) persist_reread = factory_doc.ReadPiece(0, numero_devis)
if not persist_reread: if not persist_reread:
logger.error(f"❌ Impossible de relire le devis {numero_devis}") logger.error(f"❌ Impossible de relire le devis {numero_devis}")
# Fallback # Fallback: retourner les totaux calculés
total_calcule = sum(
l.get("montant_ligne_ht", 0) for l in devis_data["lignes"]
)
logger.warning(f"⚠️ Utilisation total calculé: {total_calcule}")
return { return {
"numero_devis": numero_devis, "numero_devis": numero_devis,
"total_ht": 0.0, "total_ht": total_calcule,
"total_ttc": 0.0, "total_ttc": round(total_calcule * 1.20, 2),
"nb_lignes": len(devis_data["lignes"]), "nb_lignes": len(devis_data["lignes"]),
"client_code": devis_data["client"]["code"], "client_code": devis_data["client"]["code"],
"date_devis": str(date_obj.date()), "date_devis": str(date_obj.date()),
"statut": 0,
} }
doc_final = win32com.client.CastTo( doc_final = win32com.client.CastTo(
@ -601,23 +727,72 @@ class SageConnector:
) )
doc_final.Read() doc_final.Read()
# ===== EXTRACTION ===== # ===== EXTRACTION TOTAUX =====
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
statut_final = getattr(doc_final, "DO_Statut", 0) client_code_final = getattr(doc_final, "CT_Num", "")
date_finale = getattr(doc_final, "DO_Date", None)
logger.info(f"💰 Total HT: {total_ht}") logger.info(f"💰 Total HT: {total_ht}")
logger.info(f"💰 Total TTC: {total_ttc}") logger.info(f"💰 Total TTC: {total_ttc}")
logger.info(f"📋 Statut final: {statut_final}")
# ===== DIAGNOSTIC EN CAS D'ANOMALIE =====
if total_ht == 0 and total_ttc > 0:
logger.warning("⚠️ Anomalie: Total HT = 0 mais Total TTC > 0")
logger.info("🔍 Lecture des lignes pour diagnostic...")
try:
factory_lignes_verif = doc_final.FactoryDocumentLigne
except:
factory_lignes_verif = doc_final.FactoryDocumentVenteLigne
index = 1
total_calcule = 0.0
while index <= 20:
try:
ligne_p = factory_lignes_verif.List(index)
if ligne_p is None:
break
ligne_verif = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne_verif.Read()
montant = float(
getattr(ligne_verif, "DL_MontantHT", 0.0)
)
logger.info(
f" Ligne {index}: Montant HT = {montant}"
)
total_calcule += montant
index += 1
except:
break
logger.info(f"📊 Total calculé manuellement: {total_calcule}")
if total_calcule > 0:
total_ht = total_calcule
total_ttc = round(total_ht * 1.20, 2)
logger.info(
f"✅ Correction appliquée: HT={total_ht}€, TTC={total_ttc}"
)
logger.info(
f"✅ ✅ ✅ DEVIS CRÉÉ: {numero_devis} - {total_ttc}€ TTC ✅ ✅ ✅"
)
return { return {
"numero_devis": numero_devis, "numero_devis": numero_devis,
"total_ht": total_ht, "total_ht": total_ht,
"total_ttc": total_ttc, "total_ttc": total_ttc,
"nb_lignes": len(devis_data["lignes"]), "nb_lignes": len(devis_data["lignes"]),
"client_code": devis_data["client"]["code"], "client_code": client_code_final,
"date_devis": str(date_obj.date()), "date_devis": (
"statut": statut_final, str(date_finale) if date_finale else str(date_obj.date())
),
} }
except Exception as e: except Exception as e:
@ -630,80 +805,9 @@ class SageConnector:
raise raise
except Exception as e: except Exception as e:
logger.error(f" ERREUR CRÉATION DEVIS: {e}", exc_info=True) logger.error(f" ❌ ❌ ERREUR CRÉATION DEVIS: {e}", exc_info=True)
raise RuntimeError(f"Échec création devis: {str(e)}") raise RuntimeError(f"Échec création devis: {str(e)}")
def changer_statut_devis(self, numero_devis, nouveau_statut):
"""
NOUVEAU: Change le statut d'un devis
Statuts Sage:
- 0: Devis
- 2: Accepté
- 5: Transformé
- 6: Refusé
"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
factory = self.cial.FactoryDocumentVente
# Essayer ReadPiece d'abord
persist = factory.ReadPiece(0, numero_devis)
# Si échec, chercher dans la liste (brouillons)
if not persist:
index = 1
while index < 5000:
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_devis
):
persist = persist_test
break
index += 1
except:
index += 1
if not persist:
raise ValueError(f"Devis {numero_devis} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
# Changement de statut
doc.DO_Statut = nouveau_statut
doc.Write()
logger.info(
f"✅ Statut devis {numero_devis}: {statut_actuel}{nouveau_statut}"
)
return {
"numero": numero_devis,
"statut_ancien": statut_actuel,
"statut_nouveau": nouveau_statut,
}
except Exception as e:
logger.error(f"❌ Erreur changement statut: {e}", exc_info=True)
raise RuntimeError(f"Échec changement statut: {str(e)}")
# ========================================================================= # =========================================================================
# LECTURE DEVIS # LECTURE DEVIS
# ========================================================================= # =========================================================================
@ -922,7 +1026,8 @@ class SageConnector:
def transformer_document(self, numero_source, type_source, type_cible): def transformer_document(self, numero_source, type_source, type_cible):
""" """
Transformer un document Transformation avec transaction
CORRIGÉ: Utilise CreateProcess_Document au lieu de CreateProcess_DocumentVente
""" """
if not self.cial: if not self.cial:
raise RuntimeError("Connexion Sage non établie") raise RuntimeError("Connexion Sage non établie")
@ -939,7 +1044,7 @@ class SageConnector:
doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3") doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3")
doc_source.Read() doc_source.Read()
# Récupérer client # Récupérer le client
client_code = "" client_code = ""
try: try:
client_obj = getattr(doc_source, "Client", None) client_obj = getattr(doc_source, "Client", None)
@ -959,11 +1064,12 @@ class SageConnector:
try: try:
self.cial.CptaApplication.BeginTrans() self.cial.CptaApplication.BeginTrans()
transaction_active = True transaction_active = True
logger.debug("✅ Transaction démarrée")
except Exception as e: except Exception as e:
logger.warning(f"⚠️ BeginTrans échoué: {e}") logger.warning(f"⚠️ BeginTrans échoué: {e}")
try: try:
# ✅ CORRECTION: Utiliser CreateProcess_Document (pas CreateProcess_DocumentVente) # ✅ CORRECTION: CreateProcess_Document (sans Vente)
process = self.cial.CreateProcess_Document(type_cible) process = self.cial.CreateProcess_Document(type_cible)
doc_cible = process.Document doc_cible = process.Document
@ -976,20 +1082,22 @@ class SageConnector:
logger.info(f"📄 Document cible créé (type {type_cible})") logger.info(f"📄 Document cible créé (type {type_cible})")
# Associer client # Associer le client
factory_client = self.cial.CptaApplication.FactoryClient try:
persist_client = factory_client.ReadNumero(client_code) factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(client_code)
if not persist_client: if persist_client:
raise ValueError(f"Client {client_code} introuvable") client_obj_cible = win32com.client.CastTo(
persist_client, "IBOClient3"
client_obj_cible = win32com.client.CastTo( )
persist_client, "IBOClient3" client_obj_cible.Read()
) doc_cible.SetDefaultClient(client_obj_cible)
client_obj_cible.Read() doc_cible.Write()
doc_cible.SetDefaultClient(client_obj_cible) logger.info(f"👤 Client {client_code} associé")
doc_cible.Write() except Exception as e:
logger.info(f"👤 Client {client_code} associé") logger.error(f"❌ Erreur association client: {e}")
raise
# Date # Date
import pywintypes import pywintypes
@ -1002,14 +1110,6 @@ class SageConnector:
except: except:
pass pass
# ✅ STATUT INITIAL pour commande/facture
if type_cible == 3: # Commande
doc_cible.DO_Statut = 0 # En cours
elif type_cible == 5: # Facture
doc_cible.DO_Statut = 0 # Non réglée
doc_cible.Write()
# Copie lignes # Copie lignes
try: try:
factory_lignes_source = doc_source.FactoryDocumentLigne factory_lignes_source = doc_source.FactoryDocumentLigne
@ -1039,7 +1139,7 @@ class SageConnector:
ligne_cible_p, "IBODocumentLigne3" ligne_cible_p, "IBODocumentLigne3"
) )
# Article # Récupérer référence article
article_ref = "" article_ref = ""
try: try:
article_ref = getattr( article_ref = getattr(
@ -1055,7 +1155,7 @@ class SageConnector:
except: except:
pass pass
# Associer article # Associer article si disponible
if article_ref: if article_ref:
try: try:
persist_article = factory_article.ReadReference( persist_article = factory_article.ReadReference(
@ -1113,26 +1213,14 @@ class SageConnector:
except Exception as e: except Exception as e:
logger.debug(f"Erreur ligne {index}: {e}") logger.debug(f"Erreur ligne {index}: {e}")
index += 1 index += 1
if index > 1000:
break
# Validation finale # Validation
doc_cible.Write() doc_cible.Write()
process.Process() process.Process()
# Récupération numéro numero_cible = getattr(doc_cible, "DO_Piece", "")
numero_cible = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_cible = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_cible:
numero_cible = getattr(doc_cible, "DO_Piece", "")
if not numero_cible: if not numero_cible:
raise RuntimeError("Numéro document cible vide") raise RuntimeError("Numéro document cible vide")
@ -1142,17 +1230,17 @@ class SageConnector:
self.cial.CptaApplication.CommitTrans() self.cial.CptaApplication.CommitTrans()
logger.info("✅ Transaction committée") logger.info("✅ Transaction committée")
# ✅ MAJ statut source (devis → transformé) # MAJ statut source si transformation devis → commande
if type_source == 0 and type_cible == 3: try:
try: if type_source == 0 and type_cible == 3:
doc_source.DO_Statut = 5 # Transformé doc_source.DO_Statut = 5 # Transformé
doc_source.Write() doc_source.Write()
logger.info("✅ Statut source: TRANSFORMÉ (5)") logger.info(f"✅ Statut source mis à jour: TRANSFORMÉ (5)")
except Exception as e: except Exception as e:
logger.warning(f"Impossible de MAJ statut source: {e}") logger.debug(f"Impossible de MAJ statut source: {e}")
logger.info( logger.info(
f"✅ Transformation: {numero_source} ({type_source}) → {numero_cible} ({type_cible}), {nb_lignes} lignes" f"✅ Transformation: {numero_source} ({type_source}) → {numero_cible} ({type_cible})"
) )
return { return {