refactor: Add date to datetime import and apply minor formatting adjustments.

This commit is contained in:
Fanilo-Nantenaina 2025-11-27 18:09:23 +03:00
parent efa4edcae0
commit 96c9c5e7df

View file

@ -1,6 +1,6 @@
import win32com.client import win32com.client
import pythoncom # AJOUT CRITIQUE import pythoncom # AJOUT CRITIQUE
from datetime import datetime, timedelta from datetime import datetime, timedelta, date
from typing import Dict, List, Optional from typing import Dict, List, Optional
import threading import threading
import time import time
@ -9,6 +9,7 @@ from contextlib import contextmanager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SageConnector: class SageConnector:
""" """
Connecteur Sage 100c avec gestion COM threading correcte Connecteur Sage 100c avec gestion COM threading correcte
@ -63,11 +64,13 @@ class SageConnector:
Chaque thread doit initialiser COM avant d'utiliser les objets Sage. Chaque thread doit initialiser COM avant d'utiliser les objets Sage.
""" """
# Vérifier si COM est déjà initialisé pour ce thread # Vérifier si COM est déjà initialisé pour ce thread
if not hasattr(self._thread_local, 'com_initialized'): if not hasattr(self._thread_local, "com_initialized"):
try: try:
pythoncom.CoInitialize() pythoncom.CoInitialize()
self._thread_local.com_initialized = True self._thread_local.com_initialized = True
logger.debug(f"COM initialisé pour thread {threading.current_thread().name}") logger.debug(
f"COM initialisé pour thread {threading.current_thread().name}"
)
except Exception as e: except Exception as e:
logger.error(f"Erreur initialisation COM: {e}") logger.error(f"Erreur initialisation COM: {e}")
raise raise
@ -80,11 +83,13 @@ class SageConnector:
def _cleanup_com_thread(self): def _cleanup_com_thread(self):
"""Nettoie COM pour le thread actuel (à appeler à la fin)""" """Nettoie COM pour le thread actuel (à appeler à la fin)"""
if hasattr(self._thread_local, 'com_initialized'): if hasattr(self._thread_local, "com_initialized"):
try: try:
pythoncom.CoUninitialize() pythoncom.CoUninitialize()
delattr(self._thread_local, 'com_initialized') delattr(self._thread_local, "com_initialized")
logger.debug(f"COM nettoyé pour thread {threading.current_thread().name}") logger.debug(
f"COM nettoyé pour thread {threading.current_thread().name}"
)
except: except:
pass pass
@ -96,7 +101,9 @@ class SageConnector:
"""Connexion initiale à Sage""" """Connexion initiale à Sage"""
try: try:
with self._com_context(): with self._com_context():
self.cial = win32com.client.gencache.EnsureDispatch("Objets100c.Cial.Stream") self.cial = win32com.client.gencache.EnsureDispatch(
"Objets100c.Cial.Stream"
)
self.cial.Name = self.chemin_base self.cial.Name = self.chemin_base
self.cial.Loggable.UserName = self.utilisateur self.cial.Loggable.UserName = self.utilisateur
self.cial.Loggable.UserPwd = self.mot_de_passe self.cial.Loggable.UserPwd = self.mot_de_passe
@ -108,7 +115,9 @@ class SageConnector:
logger.info("Chargement initial du cache...") logger.info("Chargement initial du cache...")
self._refresh_cache_clients() self._refresh_cache_clients()
self._refresh_cache_articles() self._refresh_cache_articles()
logger.info(f"Cache initialisé: {len(self._cache_clients)} clients, {len(self._cache_articles)} articles") logger.info(
f"Cache initialisé: {len(self._cache_clients)} clients, {len(self._cache_articles)} articles"
)
# Démarrage du thread d'actualisation # Démarrage du thread d'actualisation
self._start_refresh_thread() self._start_refresh_thread()
@ -140,6 +149,7 @@ class SageConnector:
def _start_refresh_thread(self): def _start_refresh_thread(self):
"""Démarre le thread d'actualisation automatique""" """Démarre le thread d'actualisation automatique"""
def refresh_loop(): def refresh_loop():
# Initialiser COM pour ce thread worker # Initialiser COM pour ce thread worker
pythoncom.CoInitialize() pythoncom.CoInitialize()
@ -152,20 +162,26 @@ class SageConnector:
if self._cache_clients_last_update: if self._cache_clients_last_update:
age = datetime.now() - self._cache_clients_last_update age = datetime.now() - self._cache_clients_last_update
if age.total_seconds() > self._cache_ttl_minutes * 60: if age.total_seconds() > self._cache_ttl_minutes * 60:
logger.info(f"Actualisation cache clients (âge: {age.seconds//60}min)") logger.info(
f"Actualisation cache clients (âge: {age.seconds//60}min)"
)
self._refresh_cache_clients() self._refresh_cache_clients()
# Articles # Articles
if self._cache_articles_last_update: if self._cache_articles_last_update:
age = datetime.now() - self._cache_articles_last_update age = datetime.now() - self._cache_articles_last_update
if age.total_seconds() > self._cache_ttl_minutes * 60: if age.total_seconds() > self._cache_ttl_minutes * 60:
logger.info(f"Actualisation cache articles (âge: {age.seconds//60}min)") logger.info(
f"Actualisation cache articles (âge: {age.seconds//60}min)"
)
self._refresh_cache_articles() self._refresh_cache_articles()
finally: finally:
# Nettoyer COM en fin de thread # Nettoyer COM en fin de thread
pythoncom.CoUninitialize() pythoncom.CoUninitialize()
self._refresh_thread = threading.Thread(target=refresh_loop, daemon=True, name="SageCacheRefresh") self._refresh_thread = threading.Thread(
target=refresh_loop, daemon=True, name="SageCacheRefresh"
)
self._refresh_thread.start() self._refresh_thread.start()
def _refresh_cache_clients(self): def _refresh_cache_clients(self):
@ -193,7 +209,7 @@ class SageConnector:
if obj: if obj:
data = self._extraire_client(obj) data = self._extraire_client(obj)
clients.append(data) clients.append(data)
clients_dict[data['numero']] = data clients_dict[data["numero"]] = data
erreurs_consecutives = 0 erreurs_consecutives = 0
index += 1 index += 1
@ -202,7 +218,9 @@ class SageConnector:
erreurs_consecutives += 1 erreurs_consecutives += 1
index += 1 index += 1
if erreurs_consecutives >= max_erreurs: if erreurs_consecutives >= max_erreurs:
logger.warning(f"Arrêt refresh clients après {max_erreurs} erreurs") logger.warning(
f"Arrêt refresh clients après {max_erreurs} erreurs"
)
break break
with self._lock_clients: with self._lock_clients:
@ -240,7 +258,7 @@ class SageConnector:
if obj: if obj:
data = self._extraire_article(obj) data = self._extraire_article(obj)
articles.append(data) articles.append(data)
articles_dict[data['reference']] = data articles_dict[data["reference"]] = data
erreurs_consecutives = 0 erreurs_consecutives = 0
index += 1 index += 1
@ -249,7 +267,9 @@ class SageConnector:
erreurs_consecutives += 1 erreurs_consecutives += 1
index += 1 index += 1
if erreurs_consecutives >= max_erreurs: if erreurs_consecutives >= max_erreurs:
logger.warning(f"Arrêt refresh articles après {max_erreurs} erreurs") logger.warning(
f"Arrêt refresh articles après {max_erreurs} erreurs"
)
break break
with self._lock_articles: with self._lock_articles:
@ -274,9 +294,10 @@ class SageConnector:
filtre_lower = filtre.lower() filtre_lower = filtre.lower()
return [ return [
c for c in self._cache_clients c
if filtre_lower in c['numero'].lower() or for c in self._cache_clients
filtre_lower in c['intitule'].lower() if filtre_lower in c["numero"].lower()
or filtre_lower in c["intitule"].lower()
] ]
def lire_client(self, code_client): def lire_client(self, code_client):
@ -292,9 +313,10 @@ class SageConnector:
filtre_lower = filtre.lower() filtre_lower = filtre.lower()
return [ return [
a for a in self._cache_articles a
if filtre_lower in a['reference'].lower() or for a in self._cache_articles
filtre_lower in a['designation'].lower() if filtre_lower in a["reference"].lower()
or filtre_lower in a["designation"].lower()
] ]
def lire_article(self, reference): def lire_article(self, reference):
@ -315,15 +337,37 @@ class SageConnector:
return { return {
"clients": { "clients": {
"count": len(self._cache_clients), "count": len(self._cache_clients),
"last_update": self._cache_clients_last_update.isoformat() if self._cache_clients_last_update else None, "last_update": (
"age_minutes": (datetime.now() - self._cache_clients_last_update).total_seconds() / 60 if self._cache_clients_last_update else None self._cache_clients_last_update.isoformat()
if self._cache_clients_last_update
else None
),
"age_minutes": (
(
datetime.now() - self._cache_clients_last_update
).total_seconds()
/ 60
if self._cache_clients_last_update
else None
),
}, },
"articles": { "articles": {
"count": len(self._cache_articles), "count": len(self._cache_articles),
"last_update": self._cache_articles_last_update.isoformat() if self._cache_articles_last_update else None, "last_update": (
"age_minutes": (datetime.now() - self._cache_articles_last_update).total_seconds() / 60 if self._cache_articles_last_update else None self._cache_articles_last_update.isoformat()
if self._cache_articles_last_update
else None
),
"age_minutes": (
(
datetime.now() - self._cache_articles_last_update
).total_seconds()
/ 60
if self._cache_articles_last_update
else None
),
}, },
"ttl_minutes": self._cache_ttl_minutes "ttl_minutes": self._cache_ttl_minutes,
} }
# ========================================================================= # =========================================================================
@ -354,7 +398,7 @@ class SageConnector:
data = { data = {
"numero": getattr(client_obj, "CT_Num", ""), "numero": getattr(client_obj, "CT_Num", ""),
"intitule": getattr(client_obj, "CT_Intitule", ""), "intitule": getattr(client_obj, "CT_Intitule", ""),
"type": getattr(client_obj, "CT_Type", 0) "type": getattr(client_obj, "CT_Type", 0),
} }
try: try:
@ -383,7 +427,7 @@ class SageConnector:
"prix_vente": getattr(article_obj, "AR_PrixVen", 0.0), "prix_vente": getattr(article_obj, "AR_PrixVen", 0.0),
"prix_achat": getattr(article_obj, "AR_PrixAch", 0.0), "prix_achat": getattr(article_obj, "AR_PrixAch", 0.0),
"stock_reel": getattr(article_obj, "AR_Stock", 0.0), "stock_reel": getattr(article_obj, "AR_Stock", 0.0),
"stock_mini": getattr(article_obj, "AR_StockMini", 0.0) "stock_mini": getattr(article_obj, "AR_StockMini", 0.0),
} }
# ========================================================================= # =========================================================================
@ -398,7 +442,9 @@ class SageConnector:
if not self.cial: if not self.cial:
raise RuntimeError("Connexion Sage non établie") raise RuntimeError("Connexion Sage non établie")
logger.info(f"🚀 Début création devis pour client {devis_data['client']['code']}") logger.info(
f"🚀 Début création devis pour client {devis_data['client']['code']}"
)
try: try:
with self._com_context(), self._lock_com: with self._com_context(), self._lock_com:
@ -425,13 +471,15 @@ class SageConnector:
# ===== DATE ===== # ===== DATE =====
import pywintypes import pywintypes
if isinstance(devis_data['date_devis'], str): if isinstance(devis_data["date_devis"], str):
try: try:
date_obj = datetime.fromisoformat(devis_data['date_devis']) date_obj = datetime.fromisoformat(devis_data["date_devis"])
except: except:
date_obj = datetime.now() date_obj = datetime.now()
elif isinstance(devis_data['date_devis'], date): elif isinstance(devis_data["date_devis"], date):
date_obj = datetime.combine(devis_data['date_devis'], datetime.min.time()) date_obj = datetime.combine(
devis_data["date_devis"], datetime.min.time()
)
else: else:
date_obj = datetime.now() date_obj = datetime.now()
@ -440,19 +488,27 @@ class SageConnector:
# ===== CLIENT (CRITIQUE: Doit être défini AVANT les lignes) ===== # ===== CLIENT (CRITIQUE: Doit être défini AVANT les lignes) =====
factory_client = self.cial.CptaApplication.FactoryClient factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(devis_data['client']['code']) persist_client = factory_client.ReadNumero(
devis_data["client"]["code"]
)
if not persist_client: if not persist_client:
raise ValueError(f"❌ Client {devis_data['client']['code']} introuvable") raise ValueError(
f"❌ Client {devis_data['client']['code']} introuvable"
)
client_obj = self._cast_client(persist_client) client_obj = self._cast_client(persist_client)
if not client_obj: if not client_obj:
raise ValueError(f"❌ Impossible de charger le client {devis_data['client']['code']}") raise ValueError(
f"❌ Impossible de charger le client {devis_data['client']['code']}"
)
# ✅ CRITIQUE: Associer le client au document # ✅ CRITIQUE: Associer le client au document
doc.SetDefaultClient(client_obj) doc.SetDefaultClient(client_obj)
doc.Write() doc.Write()
logger.info(f"👤 Client {devis_data['client']['code']} associé et document écrit") logger.info(
f"👤 Client {devis_data['client']['code']} associé et document écrit"
)
# ===== LIGNES AVEC SetDefaultArticle() ===== # ===== LIGNES AVEC SetDefaultArticle() =====
try: try:
@ -464,16 +520,24 @@ 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):
logger.info(f"--- Ligne {idx}: {ligne_data['article_code']} ---") logger.info(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
# 🔍 ÉTAPE 1: Charger l'article RÉEL depuis Sage pour le prix # 🔍 ÉTAPE 1: Charger l'article RÉEL depuis Sage pour le prix
persist_article = factory_article.ReadReference(ligne_data['article_code']) persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article: if not persist_article:
raise ValueError(f"❌ Article {ligne_data['article_code']} introuvable dans Sage") raise ValueError(
f"❌ Article {ligne_data['article_code']} introuvable dans Sage"
)
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read() article_obj.Read()
# 💰 ÉTAPE 2: Récupérer le prix de vente RÉEL # 💰 ÉTAPE 2: Récupérer le prix de vente RÉEL
@ -482,33 +546,51 @@ class SageConnector:
logger.info(f"💰 Prix Sage: {prix_sage}") logger.info(f"💰 Prix Sage: {prix_sage}")
if prix_sage == 0: if prix_sage == 0:
logger.warning(f"⚠️ ATTENTION: Article {ligne_data['article_code']} a un prix de vente = 0€") logger.warning(
f"⚠️ ATTENTION: Article {ligne_data['article_code']} a un prix de vente = 0€"
)
# 📝 ÉTAPE 3: Créer la ligne de devis # 📝 ÉTAPE 3: Créer la ligne de devis
ligne_persist = factory_lignes.Create() ligne_persist = factory_lignes.Create()
try: try:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except: except:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
# ✅✅✅ SOLUTION FINALE: SetDefaultArticleReference avec 2 paramètres ✅✅✅ # ✅✅✅ SOLUTION FINALE: SetDefaultArticleReference avec 2 paramètres ✅✅✅
quantite = float(ligne_data['quantite']) quantite = float(ligne_data["quantite"])
try: try:
# Méthode 1: Via référence (plus simple et plus fiable) # Méthode 1: Via référence (plus simple et plus fiable)
ligne_obj.SetDefaultArticleReference(ligne_data['article_code'], quantite) ligne_obj.SetDefaultArticleReference(
logger.info(f"✅ Article associé via SetDefaultArticleReference('{ligne_data['article_code']}', {quantite})") ligne_data["article_code"], quantite
)
logger.info(
f"✅ Article associé via SetDefaultArticleReference('{ligne_data['article_code']}', {quantite})"
)
except Exception as e: except Exception as e:
logger.warning(f"⚠️ SetDefaultArticleReference échoué: {e}, tentative avec objet article") logger.warning(
f"⚠️ SetDefaultArticleReference échoué: {e}, tentative avec objet article"
)
try: try:
# Méthode 2: Via objet article # Méthode 2: Via objet article
ligne_obj.SetDefaultArticle(article_obj, quantite) ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f"✅ Article associé via SetDefaultArticle(obj, {quantite})") logger.info(
f"✅ Article associé via SetDefaultArticle(obj, {quantite})"
)
except Exception as e2: except Exception as e2:
logger.error(f"❌ Toutes les méthodes d'association ont échoué") logger.error(
f"❌ Toutes les méthodes d'association ont échoué"
)
# Fallback: définir manuellement # Fallback: définir manuellement
ligne_obj.DL_Design = designation_sage or ligne_data['designation'] ligne_obj.DL_Design = (
designation_sage or ligne_data["designation"]
)
ligne_obj.DL_Qte = quantite ligne_obj.DL_Qte = quantite
logger.warning("⚠️ Configuration manuelle appliquée") logger.warning("⚠️ Configuration manuelle appliquée")
@ -517,7 +599,7 @@ class SageConnector:
logger.info(f"💰 Prix auto chargé: {prix_auto}") logger.info(f"💰 Prix auto chargé: {prix_auto}")
# 💵 ÉTAPE 5: Ajuster le prix si nécessaire # 💵 É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 is not None and prix_a_utiliser > 0: if prix_a_utiliser is not None and prix_a_utiliser > 0:
# Prix personnalisé fourni # Prix personnalisé fourni
@ -526,7 +608,9 @@ class SageConnector:
elif prix_auto == 0: elif prix_auto == 0:
# Pas de prix auto, forcer le prix Sage # Pas de prix auto, forcer le prix Sage
if prix_sage == 0: if prix_sage == 0:
raise ValueError(f"Prix nul pour article {ligne_data['article_code']}") 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}") logger.info(f"💰 Prix Sage forcé: {prix_sage}")
else: else:
@ -538,13 +622,17 @@ class SageConnector:
logger.info(f"{quantite} x {prix_final}€ = {montant_ligne}") logger.info(f"{quantite} x {prix_final}€ = {montant_ligne}")
# 🎁 Remise # 🎁 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 * (1 - remise / 100) montant_apres_remise = montant_ligne * (
logger.info(f"🎁 Remise {remise}% → {montant_apres_remise}") 1 - remise / 100
)
logger.info(
f"🎁 Remise {remise}% → {montant_apres_remise}"
)
except Exception as e: except Exception as e:
logger.warning(f"⚠️ Remise non appliquée: {e}") logger.warning(f"⚠️ Remise non appliquée: {e}")
@ -555,12 +643,20 @@ class SageConnector:
# 🔍 VÉRIFICATION: Relire la ligne pour confirmer # 🔍 VÉRIFICATION: Relire la ligne pour confirmer
try: try:
ligne_obj.Read() ligne_obj.Read()
prix_enregistre = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) prix_enregistre = float(
montant_enregistre = float(getattr(ligne_obj, "DL_MontantHT", 0.0)) getattr(ligne_obj, "DL_PrixUnitaire", 0.0)
logger.info(f"🔍 Vérif: Prix={prix_enregistre}€, Montant HT={montant_enregistre}") )
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: if montant_enregistre == 0:
logger.error(f"❌ PROBLÈME: Montant enregistré = 0 pour ligne {idx}") logger.error(
f"❌ PROBLÈME: Montant enregistré = 0 pour ligne {idx}"
)
else: else:
logger.info(f"✅ Ligne {idx} OK: {montant_enregistre}") logger.info(f"✅ Ligne {idx} OK: {montant_enregistre}")
except Exception as e: except Exception as e:
@ -578,10 +674,14 @@ class SageConnector:
try: try:
doc_result = process.DocumentResult doc_result = process.DocumentResult
if doc_result: if doc_result:
doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3") doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read() doc_result.Read()
numero_devis = getattr(doc_result, "DO_Piece", "") numero_devis = getattr(doc_result, "DO_Piece", "")
logger.info(f"📄 Numéro (via DocumentResult): {numero_devis}") logger.info(
f"📄 Numéro (via DocumentResult): {numero_devis}"
)
except Exception as e: except Exception as e:
logger.warning(f"⚠️ DocumentResult non accessible: {e}") logger.warning(f"⚠️ DocumentResult non accessible: {e}")
@ -609,18 +709,22 @@ class SageConnector:
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: retourner les totaux calculés # Fallback: retourner les totaux calculés
total_calcule = sum(l.get('montant_ligne_ht', 0) for l in devis_data['lignes']) total_calcule = sum(
l.get("montant_ligne_ht", 0) for l in devis_data["lignes"]
)
logger.warning(f"⚠️ Utilisation total calculé: {total_calcule}") logger.warning(f"⚠️ Utilisation total calculé: {total_calcule}")
return { return {
"numero_devis": numero_devis, "numero_devis": numero_devis,
"total_ht": total_calcule, "total_ht": total_calcule,
"total_ttc": round(total_calcule * 1.20, 2), "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()),
} }
doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3") doc_final = win32com.client.CastTo(
persist_reread, "IBODocumentVente3"
)
doc_final.Read() doc_final.Read()
# ===== EXTRACTION TOTAUX ===== # ===== EXTRACTION TOTAUX =====
@ -650,11 +754,17 @@ class SageConnector:
if ligne_p is None: if ligne_p is None:
break break
ligne_verif = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") ligne_verif = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne_verif.Read() ligne_verif.Read()
montant = float(getattr(ligne_verif, "DL_MontantHT", 0.0)) montant = float(
logger.info(f" Ligne {index}: Montant HT = {montant}") getattr(ligne_verif, "DL_MontantHT", 0.0)
)
logger.info(
f" Ligne {index}: Montant HT = {montant}"
)
total_calcule += montant total_calcule += montant
index += 1 index += 1
@ -666,17 +776,23 @@ class SageConnector:
if total_calcule > 0: if total_calcule > 0:
total_ht = total_calcule total_ht = total_calcule
total_ttc = round(total_ht * 1.20, 2) total_ttc = round(total_ht * 1.20, 2)
logger.info(f"✅ Correction appliquée: HT={total_ht}€, TTC={total_ttc}") logger.info(
f"✅ Correction appliquée: HT={total_ht}€, TTC={total_ttc}"
)
logger.info(f"✅ ✅ ✅ DEVIS CRÉÉ: {numero_devis} - {total_ttc}€ 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": client_code_final, "client_code": client_code_final,
"date_devis": str(date_finale) if date_finale else str(date_obj.date()) "date_devis": (
str(date_finale) if date_finale else str(date_obj.date())
),
} }
except Exception as e: except Exception as e:
@ -720,11 +836,15 @@ class SageConnector:
if persist_test is None: if persist_test is None:
break break
doc_test = win32com.client.CastTo(persist_test, "IBODocumentVente3") doc_test = win32com.client.CastTo(
persist_test, "IBODocumentVente3"
)
doc_test.Read() doc_test.Read()
if (getattr(doc_test, "DO_Type", -1) == 0 and if (
getattr(doc_test, "DO_Piece", "") == numero_devis): getattr(doc_test, "DO_Type", -1) == 0
and getattr(doc_test, "DO_Piece", "") == numero_devis
):
persist = persist_test persist = persist_test
break break
@ -749,7 +869,9 @@ class SageConnector:
client_obj.Read() client_obj.Read()
client_code = getattr(client_obj, "CT_Num", "").strip() client_code = getattr(client_obj, "CT_Num", "").strip()
client_intitule = getattr(client_obj, "CT_Intitule", "").strip() client_intitule = getattr(client_obj, "CT_Intitule", "").strip()
logger.debug(f"Client chargé via .Client: {client_code} - {client_intitule}") logger.debug(
f"Client chargé via .Client: {client_code} - {client_intitule}"
)
except Exception as e: except Exception as e:
logger.debug(f"Erreur chargement client: {e}") logger.debug(f"Erreur chargement client: {e}")
@ -757,7 +879,7 @@ class SageConnector:
if client_code: if client_code:
client_obj_cache = self.lire_client(client_code) client_obj_cache = self.lire_client(client_code)
if client_obj_cache: if client_obj_cache:
client_intitule = client_obj_cache.get('intitule', '') client_intitule = client_obj_cache.get("intitule", "")
devis = { devis = {
"numero": getattr(doc, "DO_Piece", ""), "numero": getattr(doc, "DO_Piece", ""),
@ -767,7 +889,7 @@ class SageConnector:
"total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)),
"total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)),
"statut": getattr(doc, "DO_Statut", 0), "statut": getattr(doc, "DO_Statut", 0),
"lignes": [] "lignes": [],
} }
# Lecture des lignes # Lecture des lignes
@ -783,7 +905,9 @@ class SageConnector:
if ligne_persist is None: if ligne_persist is None:
break break
ligne = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") ligne = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
ligne.Read() ligne.Read()
# ✅✅✅ CHARGEMENT ARTICLE VIA .Article ✅✅✅ # ✅✅✅ CHARGEMENT ARTICLE VIA .Article ✅✅✅
@ -798,25 +922,39 @@ class SageConnector:
article_obj = getattr(ligne, "Article", None) article_obj = getattr(ligne, "Article", None)
if article_obj: if article_obj:
article_obj.Read() article_obj.Read()
article_ref = getattr(article_obj, "AR_Ref", "").strip() article_ref = getattr(
logger.debug(f"Article chargé via .Article: {article_ref}") article_obj, "AR_Ref", ""
).strip()
logger.debug(
f"Article chargé via .Article: {article_ref}"
)
except Exception as e: except Exception as e:
logger.debug(f"Erreur chargement article ligne {index}: {e}") logger.debug(
f"Erreur chargement article ligne {index}: {e}"
)
devis["lignes"].append({ devis["lignes"].append(
{
"article": article_ref, "article": article_ref,
"designation": getattr(ligne, "DL_Design", ""), "designation": getattr(ligne, "DL_Design", ""),
"quantite": float(getattr(ligne, "DL_Qte", 0.0)), "quantite": float(getattr(ligne, "DL_Qte", 0.0)),
"prix_unitaire": float(getattr(ligne, "DL_PrixUnitaire", 0.0)), "prix_unitaire": float(
"montant_ht": float(getattr(ligne, "DL_MontantHT", 0.0)) getattr(ligne, "DL_PrixUnitaire", 0.0)
}) ),
"montant_ht": float(
getattr(ligne, "DL_MontantHT", 0.0)
),
}
)
index += 1 index += 1
except Exception as e: except Exception as e:
logger.debug(f"Erreur lecture ligne {index}: {e}") logger.debug(f"Erreur lecture ligne {index}: {e}")
break break
logger.info(f"✅ Devis {numero_devis} lu: {len(devis['lignes'])} lignes, {devis['total_ttc']:.2f}€, client: {client_intitule}") logger.info(
f"✅ Devis {numero_devis} lu: {len(devis['lignes'])} lignes, {devis['total_ttc']:.2f}€, client: {client_intitule}"
)
return devis return devis
except Exception as e: except Exception as e:
@ -856,12 +994,14 @@ class SageConnector:
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
ligne.Read() ligne.Read()
lignes.append({ lignes.append(
{
"designation": getattr(ligne, "DL_Design", ""), "designation": getattr(ligne, "DL_Design", ""),
"quantite": getattr(ligne, "DL_Qte", 0.0), "quantite": getattr(ligne, "DL_Qte", 0.0),
"prix_unitaire": getattr(ligne, "DL_PrixUnitaire", 0.0), "prix_unitaire": getattr(ligne, "DL_PrixUnitaire", 0.0),
"montant_ht": getattr(ligne, "DL_MontantHT", 0.0) "montant_ht": getattr(ligne, "DL_MontantHT", 0.0),
}) }
)
index += 1 index += 1
except: except:
@ -874,7 +1014,7 @@ class SageConnector:
"client_intitule": getattr(doc, "CT_Intitule", ""), "client_intitule": getattr(doc, "CT_Intitule", ""),
"total_ht": getattr(doc, "DO_TotalHT", 0.0), "total_ht": getattr(doc, "DO_TotalHT", 0.0),
"total_ttc": getattr(doc, "DO_TotalTTC", 0.0), "total_ttc": getattr(doc, "DO_TotalTTC", 0.0),
"lignes": lignes "lignes": lignes,
} }
except Exception as e: except Exception as e:
logger.error(f" Erreur lecture document: {e}") logger.error(f" Erreur lecture document: {e}")
@ -884,7 +1024,6 @@ class SageConnector:
# TRANSFORMATION (US-A2) # TRANSFORMATION (US-A2)
# ========================================================================= # =========================================================================
def transformer_document(self, numero_source, type_source, type_cible): def transformer_document(self, numero_source, type_source, type_cible):
""" """
Transformation avec transaction Transformation avec transaction
@ -916,7 +1055,9 @@ class SageConnector:
pass pass
if not client_code: if not client_code:
raise ValueError(f"Impossible de récupérer le client du document {numero_source}") raise ValueError(
f"Impossible de récupérer le client du document {numero_source}"
)
# Transaction # Transaction
transaction_active = False transaction_active = False
@ -933,7 +1074,9 @@ class SageConnector:
doc_cible = process.Document doc_cible = process.Document
try: try:
doc_cible = win32com.client.CastTo(doc_cible, "IBODocumentVente3") doc_cible = win32com.client.CastTo(
doc_cible, "IBODocumentVente3"
)
except: except:
pass pass
@ -945,7 +1088,9 @@ class SageConnector:
persist_client = factory_client.ReadNumero(client_code) persist_client = factory_client.ReadNumero(client_code)
if persist_client: if persist_client:
client_obj_cible = win32com.client.CastTo(persist_client, "IBOClient3") client_obj_cible = win32com.client.CastTo(
persist_client, "IBOClient3"
)
client_obj_cible.Read() client_obj_cible.Read()
doc_cible.SetDefaultClient(client_obj_cible) doc_cible.SetDefaultClient(client_obj_cible)
doc_cible.Write() doc_cible.Write()
@ -956,6 +1101,7 @@ class SageConnector:
# Date # Date
import pywintypes import pywintypes
doc_cible.DO_Date = pywintypes.Time(datetime.now()) doc_cible.DO_Date = pywintypes.Time(datetime.now())
# Référence # Référence
@ -982,50 +1128,78 @@ class SageConnector:
if ligne_source_p is None: if ligne_source_p is None:
break break
ligne_source = win32com.client.CastTo(ligne_source_p, "IBODocumentLigne3") ligne_source = win32com.client.CastTo(
ligne_source_p, "IBODocumentLigne3"
)
ligne_source.Read() ligne_source.Read()
# Créer ligne cible # Créer ligne cible
ligne_cible_p = factory_lignes_cible.Create() ligne_cible_p = factory_lignes_cible.Create()
ligne_cible = win32com.client.CastTo(ligne_cible_p, "IBODocumentLigne3") ligne_cible = win32com.client.CastTo(
ligne_cible_p, "IBODocumentLigne3"
)
# Récupérer référence article # Récupérer référence article
article_ref = "" article_ref = ""
try: try:
article_ref = getattr(ligne_source, "AR_Ref", "").strip() article_ref = getattr(
ligne_source, "AR_Ref", ""
).strip()
if not article_ref: if not article_ref:
article_obj = getattr(ligne_source, "Article", None) article_obj = getattr(ligne_source, "Article", None)
if article_obj: if article_obj:
article_obj.Read() article_obj.Read()
article_ref = getattr(article_obj, "AR_Ref", "").strip() article_ref = getattr(
article_obj, "AR_Ref", ""
).strip()
except: except:
pass pass
# Associer article si disponible # Associer article si disponible
if article_ref: if article_ref:
try: try:
persist_article = factory_article.ReadReference(article_ref) persist_article = factory_article.ReadReference(
article_ref
)
if persist_article: if persist_article:
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read() article_obj.Read()
quantite = float(getattr(ligne_source, "DL_Qte", 1.0)) quantite = float(
getattr(ligne_source, "DL_Qte", 1.0)
)
try: try:
ligne_cible.SetDefaultArticleReference(article_ref, quantite) ligne_cible.SetDefaultArticleReference(
article_ref, quantite
)
except: except:
ligne_cible.SetDefaultArticle(article_obj, quantite) ligne_cible.SetDefaultArticle(
article_obj, quantite
)
except Exception as e: except Exception as e:
logger.debug(f"Erreur association article {article_ref}: {e}") logger.debug(
f"Erreur association article {article_ref}: {e}"
)
# Copier propriétés # Copier propriétés
ligne_cible.DL_Design = getattr(ligne_source, "DL_Design", "") ligne_cible.DL_Design = getattr(
ligne_cible.DL_Qte = float(getattr(ligne_source, "DL_Qte", 0.0)) ligne_source, "DL_Design", ""
ligne_cible.DL_PrixUnitaire = float(getattr(ligne_source, "DL_PrixUnitaire", 0.0)) )
ligne_cible.DL_Qte = float(
getattr(ligne_source, "DL_Qte", 0.0)
)
ligne_cible.DL_PrixUnitaire = float(
getattr(ligne_source, "DL_PrixUnitaire", 0.0)
)
# Remise # Remise
try: try:
remise = float(getattr(ligne_source, "DL_Remise01REM_Valeur", 0.0)) remise = float(
getattr(ligne_source, "DL_Remise01REM_Valeur", 0.0)
)
if remise > 0: if remise > 0:
ligne_cible.DL_Remise01REM_Valeur = remise ligne_cible.DL_Remise01REM_Valeur = remise
ligne_cible.DL_Remise01REM_Type = 0 ligne_cible.DL_Remise01REM_Type = 0
@ -1065,13 +1239,15 @@ class SageConnector:
except Exception as e: except Exception as e:
logger.debug(f"Impossible de MAJ statut source: {e}") logger.debug(f"Impossible de MAJ statut source: {e}")
logger.info(f"✅ Transformation: {numero_source} ({type_source}) → {numero_cible} ({type_cible})") logger.info(
f"✅ Transformation: {numero_source} ({type_source}) → {numero_cible} ({type_cible})"
)
return { return {
"success": True, "success": True,
"document_source": numero_source, "document_source": numero_source,
"document_cible": numero_cible, "document_cible": numero_cible,
"nb_lignes": nb_lignes "nb_lignes": nb_lignes,
} }
except Exception as e: except Exception as e:
@ -1163,7 +1339,7 @@ class SageConnector:
"client_intitule": getattr(client, "CT_Intitule", ""), "client_intitule": getattr(client, "CT_Intitule", ""),
"email": None, "email": None,
"nom": None, "nom": None,
"telephone": None "telephone": None,
} }
# Email principal depuis Telecom # Email principal depuis Telecom
@ -1177,7 +1353,10 @@ class SageConnector:
# Nom du contact # Nom du contact
try: try:
contact_info["nom"] = getattr(client, "CT_Contact", "") or contact_info["client_intitule"] contact_info["nom"] = (
getattr(client, "CT_Contact", "")
or contact_info["client_intitule"]
)
except: except:
contact_info["nom"] = contact_info["client_intitule"] contact_info["nom"] = contact_info["client_intitule"]
@ -1199,8 +1378,5 @@ class SageConnector:
""" """
date_relance = datetime.now().strftime("%Y-%m-%d %H:%M:%S") date_relance = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return self.mettre_a_jour_champ_libre( return self.mettre_a_jour_champ_libre(
doc_id, doc_id, type_doc, "DerniereRelance", date_relance
type_doc,
"DerniereRelance",
date_relance
) )