Updated devis creation logics to not automatically be on status 0
This commit is contained in:
parent
9d569a8e03
commit
c4ddf03ae5
1 changed files with 74 additions and 188 deletions
|
|
@ -1206,16 +1206,22 @@ class SageConnector:
|
||||||
# CRÉATION DEVIS (US-A1) - VERSION TRANSACTIONNELLE
|
# 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
|
Création de devis avec option brouillon/validé
|
||||||
✅ SOLUTION FINALE: Utilisation de SetDefaultArticle()
|
|
||||||
|
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:
|
if not self.cial:
|
||||||
raise RuntimeError("Connexion Sage non établie")
|
raise RuntimeError("Connexion Sage non établie")
|
||||||
|
|
||||||
logger.info(
|
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:
|
try:
|
||||||
|
|
@ -1258,7 +1264,7 @@ class SageConnector:
|
||||||
doc.DO_Date = pywintypes.Time(date_obj)
|
doc.DO_Date = pywintypes.Time(date_obj)
|
||||||
logger.info(f"📅 Date définie: {date_obj.date()}")
|
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
|
factory_client = self.cial.CptaApplication.FactoryClient
|
||||||
persist_client = factory_client.ReadNumero(
|
persist_client = factory_client.ReadNumero(
|
||||||
devis_data["client"]["code"]
|
devis_data["client"]["code"]
|
||||||
|
|
@ -1275,14 +1281,13 @@ 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 AVEC SetDefaultArticle() =====
|
# ===== LIGNES =====
|
||||||
try:
|
try:
|
||||||
factory_lignes = doc.FactoryDocumentLigne
|
factory_lignes = doc.FactoryDocumentLigne
|
||||||
except:
|
except:
|
||||||
|
|
@ -1297,7 +1302,7 @@ class SageConnector:
|
||||||
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
|
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(
|
persist_article = factory_article.ReadReference(
|
||||||
ligne_data["article_code"]
|
ligne_data["article_code"]
|
||||||
)
|
)
|
||||||
|
|
@ -1312,17 +1317,10 @@ class SageConnector:
|
||||||
)
|
)
|
||||||
article_obj.Read()
|
article_obj.Read()
|
||||||
|
|
||||||
# 💰 ÉTAPE 2: Récupérer le prix de vente RÉEL
|
|
||||||
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
||||||
designation_sage = getattr(article_obj, "AR_Design", "")
|
designation_sage = getattr(article_obj, "AR_Design", "")
|
||||||
logger.info(f"💰 Prix Sage: {prix_sage}€")
|
|
||||||
|
|
||||||
if prix_sage == 0:
|
# Créer la ligne
|
||||||
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()
|
ligne_persist = factory_lignes.Create()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -1334,114 +1332,75 @@ class SageConnector:
|
||||||
ligne_persist, "IBODocumentVenteLigne3"
|
ligne_persist, "IBODocumentVenteLigne3"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ✅✅✅ SOLUTION FINALE: SetDefaultArticleReference avec 2 paramètres ✅✅✅
|
# Associer article
|
||||||
quantite = float(ligne_data["quantite"])
|
quantite = float(ligne_data["quantite"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Méthode 1: Via référence (plus simple et plus fiable)
|
|
||||||
ligne_obj.SetDefaultArticleReference(
|
ligne_obj.SetDefaultArticleReference(
|
||||||
ligne_data["article_code"], quantite
|
ligne_data["article_code"], quantite
|
||||||
)
|
)
|
||||||
logger.info(
|
except:
|
||||||
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:
|
try:
|
||||||
# Méthode 2: Via objet article
|
|
||||||
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
||||||
logger.info(
|
except:
|
||||||
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 = (
|
ligne_obj.DL_Design = (
|
||||||
designation_sage or ligne_data["designation"]
|
designation_sage or ligne_data.get("designation", "")
|
||||||
)
|
)
|
||||||
ligne_obj.DL_Qte = quantite
|
ligne_obj.DL_Qte = quantite
|
||||||
logger.warning("⚠️ Configuration manuelle appliquée")
|
|
||||||
|
|
||||||
# ⚙️ ÉTAPE 4: Vérifier le prix automatique chargé
|
# Prix
|
||||||
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")
|
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
|
||||||
|
if prix_a_utiliser:
|
||||||
if prix_a_utiliser is not None and prix_a_utiliser > 0:
|
|
||||||
# Prix personnalisé fourni
|
|
||||||
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
|
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
|
||||||
logger.info(f"💰 Prix personnalisé: {prix_a_utiliser}€")
|
elif prix_sage > 0:
|
||||||
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)
|
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))
|
# Remise
|
||||||
montant_ligne = quantite * prix_final
|
|
||||||
logger.info(f"✅ {quantite} x {prix_final}€ = {montant_ligne}€")
|
|
||||||
|
|
||||||
# 🎁 Remise
|
|
||||||
remise = ligne_data.get("remise_pourcentage", 0)
|
remise = ligne_data.get("remise_pourcentage", 0)
|
||||||
if remise > 0:
|
if remise > 0:
|
||||||
try:
|
try:
|
||||||
ligne_obj.DL_Remise01REM_Valeur = float(remise)
|
ligne_obj.DL_Remise01REM_Valeur = float(remise)
|
||||||
ligne_obj.DL_Remise01REM_Type = 0
|
ligne_obj.DL_Remise01REM_Type = 0
|
||||||
montant_apres_remise = montant_ligne * (
|
except:
|
||||||
1 - remise / 100
|
pass
|
||||||
)
|
|
||||||
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()
|
ligne_obj.Write()
|
||||||
logger.info(f"✅ Ligne {idx} écrite")
|
logger.info(f"✅ Ligne {idx} écrite")
|
||||||
|
|
||||||
# 🔍 VÉRIFICATION: Relire la ligne pour confirmer
|
# ===== STATUT ET VALIDATION =====
|
||||||
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...")
|
|
||||||
doc.Write()
|
doc.Write()
|
||||||
|
|
||||||
logger.info("🔄 Lancement du traitement (Process)...")
|
# 🔑 DIFFÉRENCE CRITIQUE ICI
|
||||||
|
if valider:
|
||||||
|
# Option 1: VALIDER le devis (statut 2)
|
||||||
|
logger.info("🔄 Validation du devis (Process)...")
|
||||||
process.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)")
|
||||||
|
|
||||||
|
# ===== COMMIT =====
|
||||||
|
if transaction_active:
|
||||||
|
self.cial.CptaApplication.CommitTrans()
|
||||||
|
logger.info("✅ Transaction committée")
|
||||||
|
|
||||||
# ===== RÉCUPÉRATION NUMÉRO =====
|
# ===== RÉCUPÉRATION NUMÉRO =====
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
numero_devis = None
|
numero_devis = None
|
||||||
try:
|
try:
|
||||||
doc_result = process.DocumentResult
|
doc_result = process.DocumentResult
|
||||||
|
|
@ -1451,109 +1410,37 @@ class SageConnector:
|
||||||
)
|
)
|
||||||
doc_result.Read()
|
doc_result.Read()
|
||||||
numero_devis = getattr(doc_result, "DO_Piece", "")
|
numero_devis = getattr(doc_result, "DO_Piece", "")
|
||||||
logger.info(
|
except:
|
||||||
f"📄 Numéro (via DocumentResult): {numero_devis}"
|
pass
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"⚠️ DocumentResult non accessible: {e}")
|
|
||||||
|
|
||||||
if not numero_devis:
|
if not numero_devis:
|
||||||
|
doc.Read()
|
||||||
numero_devis = getattr(doc, "DO_Piece", "")
|
numero_devis = getattr(doc, "DO_Piece", "")
|
||||||
logger.info(f"📄 Numéro (via Document): {numero_devis}")
|
|
||||||
|
|
||||||
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 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 =====
|
# ===== 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 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"]
|
|
||||||
)
|
|
||||||
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 = win32com.client.CastTo(
|
doc_final = win32com.client.CastTo(
|
||||||
persist_reread, "IBODocumentVente3"
|
persist_reread, "IBODocumentVente3"
|
||||||
)
|
)
|
||||||
doc_final.Read()
|
doc_final.Read()
|
||||||
|
|
||||||
# ===== 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))
|
||||||
client_code_final = getattr(doc_final, "CT_Num", "")
|
statut_final = getattr(doc_final, "DO_Statut", 0)
|
||||||
date_finale = getattr(doc_final, "DO_Date", None)
|
else:
|
||||||
|
total_ht = 0.0
|
||||||
logger.info(f"💰 Total HT: {total_ht}€")
|
total_ttc = 0.0
|
||||||
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}€"
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"✅ ✅ ✅ DEVIS CRÉÉ: {numero_devis} - {total_ttc}€ TTC ✅ ✅ ✅"
|
f"✅✅✅ DEVIS CRÉÉ: {numero_devis} - {total_ttc}€ TTC "
|
||||||
|
f"(statut={statut_final}) ✅✅✅"
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1561,10 +1448,9 @@ class SageConnector:
|
||||||
"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": client_code_final,
|
"client_code": devis_data["client"]["code"],
|
||||||
"date_devis": (
|
"date_devis": str(date_obj.date()),
|
||||||
str(date_finale) if date_finale else str(date_obj.date())
|
"statut": statut_final, # ✅ AJOUT
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue