Update devis, Create and Update Command

This commit is contained in:
Fanilo-Nantenaina 2025-12-06 17:02:30 +03:00
parent 876e050bff
commit de1771749d
2 changed files with 774 additions and 0 deletions

94
main.py
View file

@ -127,6 +127,26 @@ class FournisseurUpdateGatewayRequest(BaseModel):
code: str
fournisseur_data: Dict
class DevisUpdateGatewayRequest(BaseModel):
"""Modèle pour modification devis côté gateway"""
numero: str
devis_data: Dict
class CommandeCreateRequest(BaseModel):
"""Création d'une commande"""
client_id: str
date_commande: Optional[date] = None
reference: Optional[str] = None
lignes: List[Dict]
class CommandeUpdateGatewayRequest(BaseModel):
"""Modèle pour modification commande côté gateway"""
numero: str
commande_data: Dict
# =====================================================
# SÉCURITÉ
# =====================================================
@ -2745,6 +2765,80 @@ def livraison_get(req: CodeRequest):
logger.error(f"Erreur lecture livraison: {e}")
raise HTTPException(500, str(e))
@app.post("/sage/devis/update", dependencies=[Depends(verify_token)])
def modifier_devis_endpoint(req: DevisUpdateGatewayRequest):
"""
Modification d'un devis dans Sage
Permet de modifier:
- La date du devis
- Les lignes (remplace toutes les lignes)
- Le statut
"""
try:
resultat = sage.modifier_devis(req.numero, req.devis_data)
return {"success": True, "data": resultat}
except ValueError as e:
logger.warning(f"Erreur métier modification devis: {e}")
raise HTTPException(404, str(e))
except Exception as e:
logger.error(f"Erreur technique modification devis: {e}")
raise HTTPException(500, str(e))
# =====================================================
# ENDPOINTS - CRÉATION ET MODIFICATION COMMANDES
# =====================================================
@app.post("/sage/commandes/create", dependencies=[Depends(verify_token)])
def creer_commande_endpoint(req: CommandeCreateRequest):
"""
Création d'une commande (Bon de commande) dans Sage
"""
try:
# Transformer en format attendu par sage_connector
commande_data = {
"client": {"code": req.client_id, "intitule": ""},
"date_commande": req.date_commande or date.today(),
"reference": req.reference,
"lignes": req.lignes,
}
resultat = sage.creer_commande_enrichi(commande_data)
return {"success": True, "data": resultat}
except ValueError as e:
logger.warning(f"Erreur métier création commande: {e}")
raise HTTPException(400, str(e))
except Exception as e:
logger.error(f"Erreur technique création commande: {e}")
raise HTTPException(500, str(e))
@app.post("/sage/commandes/update", dependencies=[Depends(verify_token)])
def modifier_commande_endpoint(req: CommandeUpdateGatewayRequest):
"""
Modification d'une commande dans Sage
Permet de modifier:
- La date de la commande
- Les lignes (remplace toutes les lignes)
- Le statut
- La référence externe
"""
try:
resultat = sage.modifier_commande(req.numero, req.commande_data)
return {"success": True, "data": resultat}
except ValueError as e:
logger.warning(f"Erreur métier modification commande: {e}")
raise HTTPException(404, str(e))
except Exception as e:
logger.error(f"Erreur technique modification commande: {e}")
raise HTTPException(500, str(e))
# =====================================================
# LANCEMENT
# =====================================================

View file

@ -3629,4 +3629,684 @@ class SageConnector:
pass
raise RuntimeError(f"Erreur technique Sage: {error_message}")
def modifier_devis(self, numero: str, devis_data: Dict) -> Dict:
"""
Modification d'un devis existant dans Sage
Permet de modifier la date, les lignes et le statut.
Si les lignes sont modifiées, elles remplacent TOUTES les lignes existantes.
"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
# ========================================
# ÉTAPE 1 : CHARGER LE DEVIS EXISTANT
# ========================================
logger.info(f"🔍 Recherche devis {numero}...")
factory = self.cial.FactoryDocumentVente
persist = factory.ReadPiece(0, numero)
# Si ReadPiece échoue, chercher dans List()
if not persist:
index = 1
while index < 10000:
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):
persist = persist_test
break
index += 1
except:
index += 1
if not persist:
raise ValueError(f"Devis {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
logger.info(f"✅ Devis {numero} trouvé")
# Vérifier le statut (ne pas modifier si déjà transformé)
statut_actuel = getattr(doc, "DO_Statut", 0)
if statut_actuel == 5:
raise ValueError(f"Le devis {numero} a déjà été transformé")
# ========================================
# ÉTAPE 2 : METTRE À JOUR LES CHAMPS
# ========================================
champs_modifies = []
# Mise à jour de la date
if "date_devis" in devis_data:
import pywintypes
date_str = devis_data["date_devis"]
if isinstance(date_str, str):
date_obj = datetime.fromisoformat(date_str)
else:
date_obj = date_str
doc.DO_Date = pywintypes.Time(date_obj)
champs_modifies.append("date")
logger.info(f"📅 Date modifiée: {date_obj.date()}")
# Mise à jour du statut
if "statut" in devis_data:
nouveau_statut = devis_data["statut"]
doc.DO_Statut = nouveau_statut
champs_modifies.append("statut")
logger.info(f"📊 Statut modifié: {statut_actuel}{nouveau_statut}")
# Écriture des modifications de base
if champs_modifies:
doc.Write()
# ========================================
# ÉTAPE 3 : REMPLACEMENT DES LIGNES (si demandé)
# ========================================
if "lignes" in devis_data and devis_data["lignes"] is not None:
logger.info(f"🔄 Remplacement des lignes...")
# Supprimer TOUTES les lignes existantes
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
# Compter et supprimer les lignes existantes
index_ligne = 1
while index_ligne <= 100:
try:
ligne_p = factory_lignes.List(index_ligne)
if ligne_p is None:
break
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
ligne.Read()
ligne.Delete()
index_ligne += 1
except:
break
logger.info(f"🗑️ {index_ligne - 1} ligne(s) supprimée(s)")
# Ajouter les nouvelles lignes
factory_article = self.cial.FactoryArticle
for idx, ligne_data in enumerate(devis_data["lignes"], 1):
logger.info(f" Ajout ligne {idx}: {ligne_data['article_code']}")
# Charger l'article
persist_article = factory_article.ReadReference(ligne_data["article_code"])
if not persist_article:
raise ValueError(f"Article {ligne_data['article_code']} introuvable")
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3")
article_obj.Read()
# Créer la nouvelle ligne
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3")
except:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
# Associer article
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
# Définir le prix (si fourni)
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"])
# Définir la remise (si fournie)
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(ligne_data["remise_pourcentage"])
ligne_obj.DL_Remise01REM_Type = 0
except:
pass
ligne_obj.Write()
logger.info(f"{len(devis_data['lignes'])} nouvelle(s) ligne(s) ajoutée(s)")
champs_modifies.append("lignes")
# ========================================
# ÉTAPE 4 : VALIDATION FINALE
# ========================================
doc.Write()
# Attente indexation
time.sleep(1)
# Relecture pour récupérer les totaux
doc.Read()
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
logger.info(f"✅✅✅ DEVIS MODIFIÉ: {numero} ({', '.join(champs_modifies)}) ✅✅✅")
logger.info(f"💰 Nouveaux totaux: {total_ht}€ HT / {total_ttc}€ TTC")
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"champs_modifies": champs_modifies,
"statut": getattr(doc, "DO_Statut", 0)
}
except ValueError as e:
logger.error(f"❌ Erreur métier: {e}")
raise
except Exception as e:
logger.error(f"❌ Erreur modification devis: {e}", exc_info=True)
raise RuntimeError(f"Erreur technique Sage: {str(e)}")
def creer_commande_enrichi(self, commande_data: dict) -> Dict:
"""
Création d'une commande (type 10 = Bon de commande)
Similaire à creer_devis_enrichi mais pour les commandes.
Utilise CreateProcess_Document(10) au lieu de (0).
"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(f"🚀 Début création commande pour client {commande_data['client']['code']}")
try:
with self._com_context(), self._lock_com:
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("✅ Transaction Sage démarrée")
except:
pass
try:
# Création document COMMANDE (type 10)
process = self.cial.CreateProcess_Document(settings.SAGE_TYPE_BON_COMMANDE)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info("📄 Document commande créé")
# Date
import pywintypes
if isinstance(commande_data["date_commande"], str):
date_obj = datetime.fromisoformat(commande_data["date_commande"])
elif isinstance(commande_data["date_commande"], date):
date_obj = datetime.combine(commande_data["date_commande"], datetime.min.time())
else:
date_obj = datetime.now()
doc.DO_Date = pywintypes.Time(date_obj)
logger.info(f"📅 Date définie: {date_obj.date()}")
# Client (CRITIQUE)
factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(commande_data["client"]["code"])
if not persist_client:
raise ValueError(f"Client {commande_data['client']['code']} introuvable")
client_obj = self._cast_client(persist_client)
if not client_obj:
raise ValueError(f"Impossible de charger le client")
doc.SetDefaultClient(client_obj)
doc.Write()
logger.info(f"👤 Client {commande_data['client']['code']} associé")
# Référence externe (optionnelle)
if commande_data.get("reference"):
try:
doc.DO_Ref = commande_data["reference"]
logger.info(f"🔖 Référence: {commande_data['reference']}")
except:
pass
# Lignes
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(f"📦 Ajout de {len(commande_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(commande_data["lignes"], 1):
logger.info(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
# 🔍 É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
doc.Write()
process.Process()
if transaction_active:
self.cial.CptaApplication.CommitTrans()
# Récupération numéro
time.sleep(2)
numero_commande = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3")
doc_result.Read()
numero_commande = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_commande:
numero_commande = getattr(doc, "DO_Piece", "")
# Relecture
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(settings.SAGE_TYPE_BON_COMMANDE, numero_commande)
if persist_reread:
doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3")
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
else:
total_ht = 0.0
total_ttc = 0.0
logger.info(f"✅✅✅ COMMANDE CRÉÉE: {numero_commande} - {total_ttc}€ TTC ✅✅✅")
return {
"numero_commande": numero_commande,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(commande_data["lignes"]),
"client_code": commande_data["client"]["code"],
"date_commande": str(date_obj.date()),
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
raise
except Exception as e:
logger.error(f"❌ Erreur création commande: {e}", exc_info=True)
raise RuntimeError(f"Échec création commande: {str(e)}")
def modifier_commande(self, numero: str, commande_data: Dict) -> Dict:
"""
Modification d'une commande existante
Code similaire à modifier_devis mais pour type 10 (Bon de commande)
"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
# ========================================
# ÉTAPE 1 : CHARGER LE DEVIS EXISTANT
# ========================================
logger.info(f"🔍 Recherche devis {numero}...")
factory = self.cial.FactoryDocumentVente
persist = factory.ReadPiece(10, numero)
# Si ReadPiece échoue, chercher dans List()
if not persist:
index = 1
while index < 10000:
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):
persist = persist_test
break
index += 1
except:
index += 1
if not persist:
raise ValueError(f"Devis {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
logger.info(f"✅ Devis {numero} trouvé")
# Vérifier le statut (ne pas modifier si déjà transformé)
statut_actuel = getattr(doc, "DO_Statut", 0)
if statut_actuel == 5:
raise ValueError(f"Le devis {numero} a déjà été transformé")
# ========================================
# ÉTAPE 2 : METTRE À JOUR LES CHAMPS
# ========================================
champs_modifies = []
# Mise à jour de la date
if "date_devis" in commande_data:
import pywintypes
date_str = commande_data["date_devis"]
if isinstance(date_str, str):
date_obj = datetime.fromisoformat(date_str)
else:
date_obj = date_str
doc.DO_Date = pywintypes.Time(date_obj)
champs_modifies.append("date")
logger.info(f"📅 Date modifiée: {date_obj.date()}")
# Mise à jour du statut
if "statut" in commande_data:
nouveau_statut = commande_data["statut"]
doc.DO_Statut = nouveau_statut
champs_modifies.append("statut")
logger.info(f"📊 Statut modifié: {statut_actuel}{nouveau_statut}")
# Écriture des modifications de base
if champs_modifies:
doc.Write()
# ========================================
# ÉTAPE 3 : REMPLACEMENT DES LIGNES (si demandé)
# ========================================
if "lignes" in commande_data and commande_data["lignes"] is not None:
logger.info(f"🔄 Remplacement des lignes...")
# Supprimer TOUTES les lignes existantes
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
# Compter et supprimer les lignes existantes
index_ligne = 1
while index_ligne <= 100:
try:
ligne_p = factory_lignes.List(index_ligne)
if ligne_p is None:
break
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
ligne.Read()
ligne.Delete()
index_ligne += 1
except:
break
logger.info(f"🗑️ {index_ligne - 1} ligne(s) supprimée(s)")
# Ajouter les nouvelles lignes
factory_article = self.cial.FactoryArticle
for idx, ligne_data in enumerate(commande_data["lignes"], 1):
logger.info(f" Ajout ligne {idx}: {ligne_data['article_code']}")
# Charger l'article
persist_article = factory_article.ReadReference(ligne_data["article_code"])
if not persist_article:
raise ValueError(f"Article {ligne_data['article_code']} introuvable")
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3")
article_obj.Read()
# Créer la nouvelle ligne
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3")
except:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
# Associer article
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
# Définir le prix (si fourni)
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"])
# Définir la remise (si fournie)
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(ligne_data["remise_pourcentage"])
ligne_obj.DL_Remise01REM_Type = 0
except:
pass
ligne_obj.Write()
logger.info(f"{len(commande_data['lignes'])} nouvelle(s) ligne(s) ajoutée(s)")
champs_modifies.append("lignes")
# ========================================
# ÉTAPE 4 : VALIDATION FINALE
# ========================================
doc.Write()
# Attente indexation
time.sleep(1)
# Relecture pour récupérer les totaux
doc.Read()
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
logger.info(f"✅✅✅ DEVIS MODIFIÉ: {numero} ({', '.join(champs_modifies)}) ✅✅✅")
logger.info(f"💰 Nouveaux totaux: {total_ht}€ HT / {total_ttc}€ TTC")
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"champs_modifies": champs_modifies,
"statut": getattr(doc, "DO_Statut", 0)
}
except ValueError as e:
logger.error(f"❌ Erreur métier: {e}")
raise
except Exception as e:
logger.error(f"❌ Erreur modification devis: {e}", exc_info=True)
raise RuntimeError(f"Erreur technique Sage: {str(e)}")