Update devis, Create and Update Command
This commit is contained in:
parent
876e050bff
commit
de1771749d
2 changed files with 774 additions and 0 deletions
94
main.py
94
main.py
|
|
@ -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
|
||||
# =====================================================
|
||||
|
|
|
|||
|
|
@ -3630,3 +3630,683 @@ class SageConnector:
|
|||
|
||||
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)}")
|
||||
|
||||
Loading…
Reference in a new issue