From 62077b58629499f0247bd7c29e3b1a9522afff59 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Fri, 28 Nov 2025 05:20:43 +0300 Subject: [PATCH] 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 c4d2185c220070982264e6285db17866358e7203. --- main.py | 40 ++++- sage_connector.py | 406 ++++++++++++++++++++++++++++------------------ 2 files changed, 285 insertions(+), 161 deletions(-) diff --git a/main.py b/main.py index 7a9223c..ba90551 100644 --- a/main.py +++ b/main.py @@ -243,11 +243,12 @@ def lire_devis(req: CodeRequest): 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): """Changement de statut d'un devis""" 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"} except Exception as e: logger.error(f"Erreur MAJ statut: {e}") @@ -445,6 +446,41 @@ def devis_list( 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 # ===================================================== diff --git a/sage_connector.py b/sage_connector.py index e315ff1..7f3c735 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -436,8 +436,8 @@ class SageConnector: def creer_devis_enrichi(self, devis_data: dict): """ - Création de devis avec statut DEVIS (0) par défaut - ✅ CORRECTION: Force le statut à 0 (Devis) après création + Création de devis avec transaction Sage + ✅ SOLUTION FINALE: Utilisation de SetDefaultArticle() """ if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -484,12 +484,9 @@ class SageConnector: date_obj = datetime.now() doc.DO_Date = pywintypes.Time(date_obj) + logger.info(f"📅 Date définie: {date_obj.date()}") - # ✅✅✅ CORRECTION CRITIQUE: Forcer le statut à 0 (Devis) - doc.DO_Statut = 0 - logger.info("📋 Statut forcé à 0 (DEVIS)") - - # ===== CLIENT ===== + # ===== CLIENT (CRITIQUE: Doit être défini AVANT les lignes) ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( devis_data["client"]["code"] @@ -506,13 +503,14 @@ class SageConnector: f"❌ Impossible de charger le client {devis_data['client']['code']}" ) + # ✅ CRITIQUE: Associer le client au document doc.SetDefaultClient(client_obj) doc.Write() logger.info( f"👤 Client {devis_data['client']['code']} associé et document écrit" ) - # ===== LIGNES (code existant inchangé) ===== + # ===== LIGNES AVEC SetDefaultArticle() ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -523,20 +521,155 @@ class SageConnector: logger.info(f"📦 Ajout de {len(devis_data['lignes'])} lignes...") for idx, ligne_data in enumerate(devis_data["lignes"], 1): - # ... (code existant pour les lignes - inchangé) - pass # Remplacer par votre code existant + logger.info( + 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...") - - # ✅ RE-FORCER le statut avant validation finale - doc.DO_Statut = 0 doc.Write() logger.info("🔄 Lancement du traitement (Process)...") process.Process() - # ===== VÉRIFICATION POST-CRÉATION ===== + # ===== RÉCUPÉRATION NUMÉRO ===== numero_devis = None try: doc_result = process.DocumentResult @@ -546,17 +679,6 @@ class SageConnector: ) doc_result.Read() 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( f"📄 Numéro (via DocumentResult): {numero_devis}" ) @@ -570,30 +692,34 @@ class SageConnector: if not numero_devis: raise RuntimeError("❌ Numéro devis vide après création") - # ===== COMMIT ===== + # ===== COMMIT TRANSACTION ===== if transaction_active: self.cial.CptaApplication.CommitTrans() logger.info("✅ Transaction committée") - # ===== ATTENTE + RELECTURE ===== + # ===== ATTENTE INDEXATION ===== logger.info("⏳ Attente indexation Sage (2s)...") time.sleep(2) + # ===== RELECTURE COMPLÈTE ===== logger.info("🔍 Relecture complète du document...") factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece(0, numero_devis) if not persist_reread: 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 { "numero_devis": numero_devis, - "total_ht": 0.0, - "total_ttc": 0.0, + "total_ht": total_calcule, + "total_ttc": round(total_calcule * 1.20, 2), "nb_lignes": len(devis_data["lignes"]), "client_code": devis_data["client"]["code"], "date_devis": str(date_obj.date()), - "statut": 0, } doc_final = win32com.client.CastTo( @@ -601,23 +727,72 @@ class SageConnector: ) doc_final.Read() - # ===== EXTRACTION ===== + # ===== EXTRACTION TOTAUX ===== total_ht = float(getattr(doc_final, "DO_TotalHT", 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 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 { "numero_devis": numero_devis, "total_ht": total_ht, "total_ttc": total_ttc, "nb_lignes": len(devis_data["lignes"]), - "client_code": devis_data["client"]["code"], - "date_devis": str(date_obj.date()), - "statut": statut_final, + "client_code": client_code_final, + "date_devis": ( + str(date_finale) if date_finale else str(date_obj.date()) + ), } except Exception as e: @@ -630,80 +805,9 @@ class SageConnector: raise 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)}") - 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 # ========================================================================= @@ -922,7 +1026,8 @@ class SageConnector: 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: raise RuntimeError("Connexion Sage non établie") @@ -939,7 +1044,7 @@ class SageConnector: doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3") doc_source.Read() - # Récupérer client + # Récupérer le client client_code = "" try: client_obj = getattr(doc_source, "Client", None) @@ -959,11 +1064,12 @@ class SageConnector: try: self.cial.CptaApplication.BeginTrans() transaction_active = True + logger.debug("✅ Transaction démarrée") except Exception as e: logger.warning(f"⚠️ BeginTrans échoué: {e}") try: - # ✅ CORRECTION: Utiliser CreateProcess_Document (pas CreateProcess_DocumentVente) + # ✅ CORRECTION: CreateProcess_Document (sans Vente) process = self.cial.CreateProcess_Document(type_cible) doc_cible = process.Document @@ -976,20 +1082,22 @@ class SageConnector: logger.info(f"📄 Document cible créé (type {type_cible})") - # Associer client - factory_client = self.cial.CptaApplication.FactoryClient - persist_client = factory_client.ReadNumero(client_code) + # Associer le client + try: + factory_client = self.cial.CptaApplication.FactoryClient + persist_client = factory_client.ReadNumero(client_code) - if not persist_client: - raise ValueError(f"Client {client_code} introuvable") - - client_obj_cible = win32com.client.CastTo( - persist_client, "IBOClient3" - ) - client_obj_cible.Read() - doc_cible.SetDefaultClient(client_obj_cible) - doc_cible.Write() - logger.info(f"👤 Client {client_code} associé") + if persist_client: + client_obj_cible = win32com.client.CastTo( + persist_client, "IBOClient3" + ) + client_obj_cible.Read() + doc_cible.SetDefaultClient(client_obj_cible) + doc_cible.Write() + logger.info(f"👤 Client {client_code} associé") + except Exception as e: + logger.error(f"❌ Erreur association client: {e}") + raise # Date import pywintypes @@ -1002,14 +1110,6 @@ class SageConnector: except: 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 try: factory_lignes_source = doc_source.FactoryDocumentLigne @@ -1039,7 +1139,7 @@ class SageConnector: ligne_cible_p, "IBODocumentLigne3" ) - # Article + # Récupérer référence article article_ref = "" try: article_ref = getattr( @@ -1055,7 +1155,7 @@ class SageConnector: except: pass - # Associer article + # Associer article si disponible if article_ref: try: persist_article = factory_article.ReadReference( @@ -1113,26 +1213,14 @@ class SageConnector: except Exception as e: logger.debug(f"Erreur ligne {index}: {e}") index += 1 + if index > 1000: + break - # Validation finale + # Validation doc_cible.Write() process.Process() - # Récupération numéro - 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", "") + numero_cible = getattr(doc_cible, "DO_Piece", "") if not numero_cible: raise RuntimeError("Numéro document cible vide") @@ -1142,17 +1230,17 @@ class SageConnector: self.cial.CptaApplication.CommitTrans() logger.info("✅ Transaction committée") - # ✅ MAJ statut source (devis → transformé) - if type_source == 0 and type_cible == 3: - try: + # MAJ statut source si transformation devis → commande + try: + if type_source == 0 and type_cible == 3: doc_source.DO_Statut = 5 # Transformé doc_source.Write() - logger.info("✅ Statut source: TRANSFORMÉ (5)") - except Exception as e: - logger.warning(f"Impossible de MAJ statut source: {e}") + logger.info(f"✅ Statut source mis à jour: TRANSFORMÉ (5)") + except Exception as e: + logger.debug(f"Impossible de MAJ statut source: {e}") 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 {