diff --git a/sage_connector.py b/sage_connector.py index b307c54..4beea31 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1206,16 +1206,22 @@ class SageConnector: # CRÉATION DEVIS (US-A1) - VERSION TRANSACTIONNELLE # ========================================================================= - def creer_devis_enrichi(self, devis_data: dict): + def creer_devis_enrichi(self, devis_data: dict, valider: bool = False): """ - Création de devis avec transaction Sage - ✅ SOLUTION FINALE: Utilisation de SetDefaultArticle() + Création de devis avec option brouillon/validé + + Args: + devis_data: Données du devis + valider: Si True, valide le devis (statut 2). Si False, reste en brouillon (statut 0) + + ✅ CORRECTION: Par défaut, crée un BROUILLON (statut 0) """ if not self.cial: raise RuntimeError("Connexion Sage non établie") logger.info( - f"🚀 Début création devis pour client {devis_data['client']['code']}" + f"🚀 Début création devis pour client {devis_data['client']['code']} " + f"(valider={valider})" ) try: @@ -1258,7 +1264,7 @@ class SageConnector: doc.DO_Date = pywintypes.Time(date_obj) logger.info(f"📅 Date définie: {date_obj.date()}") - # ===== CLIENT (CRITIQUE: Doit être défini AVANT les lignes) ===== + # ===== CLIENT ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( devis_data["client"]["code"] @@ -1275,14 +1281,13 @@ 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 AVEC SetDefaultArticle() ===== + # ===== LIGNES ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -1297,7 +1302,7 @@ class SageConnector: f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) - # 🔍 ÉTAPE 1: Charger l'article RÉEL depuis Sage pour le prix + # Charger l'article persist_article = factory_article.ReadReference( ligne_data["article_code"] ) @@ -1312,17 +1317,10 @@ class SageConnector: ) 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 + # Créer la ligne ligne_persist = factory_lignes.Create() try: @@ -1334,114 +1332,75 @@ class SageConnector: ligne_persist, "IBODocumentVenteLigne3" ) - # ✅✅✅ SOLUTION FINALE: SetDefaultArticleReference avec 2 paramètres ✅✅✅ + # Associer article 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" - ) + except: 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 + except: ligne_obj.DL_Design = ( - designation_sage or ligne_data["designation"] + designation_sage or ligne_data.get("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 prix_a_utiliser = ligne_data.get("prix_unitaire_ht") - - if prix_a_utiliser is not None and prix_a_utiliser > 0: - # Prix personnalisé fourni + if prix_a_utiliser: 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']}" - ) + elif prix_sage > 0: 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 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}") + except: + pass - # 💾 É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...") + # ===== STATUT ET VALIDATION ===== doc.Write() + + # 🔑 DIFFÉRENCE CRITIQUE ICI + if valider: + # Option 1: VALIDER le devis (statut 2) + logger.info("🔄 Validation du devis (Process)...") + process.Process() + + # Le Process() met généralement le statut à 2 + doc.Read() + statut_final = getattr(doc, "DO_Statut", 0) + logger.info(f"✅ Devis validé, statut: {statut_final}") + + else: + # Option 2: BROUILLON (statut 0) + logger.info("📝 Devis créé en BROUILLON (pas de Process)...") + + # Ne PAS appeler Process() pour garder en brouillon + # Forcer le statut à 0 + doc.DO_Statut = 0 + doc.Write() + + statut_final = 0 + logger.info("✅ Devis en brouillon (statut 0)") - logger.info("🔄 Lancement du traitement (Process)...") - process.Process() + # ===== COMMIT ===== + if transaction_active: + self.cial.CptaApplication.CommitTrans() + logger.info("✅ Transaction committée") # ===== RÉCUPÉRATION NUMÉRO ===== + time.sleep(2) + numero_devis = None try: doc_result = process.DocumentResult @@ -1451,109 +1410,37 @@ class SageConnector: ) doc_result.Read() numero_devis = getattr(doc_result, "DO_Piece", "") - logger.info( - f"📄 Numéro (via DocumentResult): {numero_devis}" - ) - except Exception as e: - logger.warning(f"⚠️ DocumentResult non accessible: {e}") + except: + pass if not numero_devis: + doc.Read() numero_devis = getattr(doc, "DO_Piece", "") - logger.info(f"📄 Numéro (via Document): {numero_devis}") if not numero_devis: raise RuntimeError("❌ Numéro devis vide après création") - # ===== COMMIT TRANSACTION ===== - if transaction_active: - self.cial.CptaApplication.CommitTrans() - logger.info("✅ Transaction committée") - - # ===== 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: retourner les totaux calculés - total_calcule = sum( - l.get("montant_ligne_ht", 0) for l in devis_data["lignes"] + if persist_reread: + doc_final = win32com.client.CastTo( + persist_reread, "IBODocumentVente3" ) - logger.warning(f"⚠️ Utilisation total calculé: {total_calcule}€") - return { - "numero_devis": numero_devis, - "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()), - } + doc_final.Read() - doc_final = win32com.client.CastTo( - persist_reread, "IBODocumentVente3" - ) - doc_final.Read() - - # ===== EXTRACTION TOTAUX ===== - total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.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}€") - - # ===== 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}€" - ) + 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) + else: + total_ht = 0.0 + total_ttc = 0.0 logger.info( - f"✅ ✅ ✅ DEVIS CRÉÉ: {numero_devis} - {total_ttc}€ TTC ✅ ✅ ✅" + f"✅✅✅ DEVIS CRÉÉ: {numero_devis} - {total_ttc}€ TTC " + f"(statut={statut_final}) ✅✅✅" ) return { @@ -1561,10 +1448,9 @@ class SageConnector: "total_ht": total_ht, "total_ttc": total_ttc, "nb_lignes": len(devis_data["lignes"]), - "client_code": client_code_final, - "date_devis": ( - str(date_finale) if date_finale else str(date_obj.date()) - ), + "client_code": devis_data["client"]["code"], + "date_devis": str(date_obj.date()), + "statut": statut_final, # ✅ AJOUT } except Exception as e: @@ -1577,9 +1463,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)}") - + # ========================================================================= # LECTURE DEVIS # =========================================================================