Created endpoints for "avoirs"

This commit is contained in:
Fanilo-Nantenaina 2025-12-08 09:30:57 +03:00
parent 5c374892d0
commit 5a5b6307b9
2 changed files with 511 additions and 0 deletions

59
main.py
View file

@ -159,6 +159,18 @@ class LivraisonUpdateGatewayRequest(BaseModel):
numero: str
livraison_data: Dict
class AvoirCreateGatewayRequest(BaseModel):
"""Création d'un avoir côté gateway"""
client_id: str
date_avoir: Optional[date] = None
lignes: List[Dict]
reference: Optional[str] = None
class AvoirUpdateGatewayRequest(BaseModel):
"""Modèle pour modification avoir côté gateway"""
numero: str
avoir_data: Dict
# =====================================================
# SÉCURITÉ
@ -2899,6 +2911,53 @@ def modifier_livraison_endpoint(req: LivraisonUpdateGatewayRequest):
logger.error(f"Erreur technique modification livraison: {e}")
raise HTTPException(500, str(e))
@app.post("/sage/avoirs/create", dependencies=[Depends(verify_token)])
def creer_avoir_endpoint(req: AvoirCreateGatewayRequest):
"""
Création d'un avoir (Bon d'avoir) dans Sage
"""
try:
# Vérifier que le client existe
client = sage.lire_client(req.client_id)
if not client:
raise HTTPException(404, f"Client {req.client_id} introuvable")
# Préparer les données pour le connecteur
avoir_data = {
"client": {"code": req.client_id, "intitule": ""},
"date_avoir": req.date_avoir or date.today(),
"reference": req.reference,
"lignes": req.lignes,
}
resultat = sage.creer_avoir_enrichi(avoir_data)
return {"success": True, "data": resultat}
except ValueError as e:
logger.warning(f"Erreur métier création avoir: {e}")
raise HTTPException(400, str(e))
except Exception as e:
logger.error(f"Erreur technique création avoir: {e}")
raise HTTPException(500, str(e))
@app.post("/sage/avoirs/update", dependencies=[Depends(verify_token)])
def modifier_avoir_endpoint(req: AvoirUpdateGatewayRequest):
"""
Modification d'un avoir dans Sage
"""
try:
resultat = sage.modifier_avoir(req.numero, req.avoir_data)
return {"success": True, "data": resultat}
except ValueError as e:
logger.warning(f"Erreur métier modification avoir: {e}")
raise HTTPException(404, str(e))
except Exception as e:
logger.error(f"Erreur technique modification avoir: {e}")
raise HTTPException(500, str(e))
# =====================================================
# LANCEMENT
# =====================================================

View file

@ -4935,6 +4935,458 @@ class SageConnector:
raise RuntimeError(f"Erreur Sage: {str(e)}")
def creer_avoir_enrichi(self, avoir_data: dict) -> Dict:
"""
Création d'un avoir (type 50 = Bon d'avoir)
Gestion identique aux commandes/devis/livraisons
"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(f"🚀 Début création avoir pour client {avoir_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 AVOIR (type 50)
process = self.cial.CreateProcess_Document(settings.SAGE_TYPE_BON_AVOIR)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info("📄 Document avoir créé")
# Date
import pywintypes
if isinstance(avoir_data["date_avoir"], str):
date_obj = datetime.fromisoformat(avoir_data["date_avoir"])
elif isinstance(avoir_data["date_avoir"], date):
date_obj = datetime.combine(avoir_data["date_avoir"], 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(avoir_data["client"]["code"])
if not persist_client:
raise ValueError(f"Client {avoir_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 {avoir_data['client']['code']} associé")
# Référence externe (optionnelle)
if avoir_data.get("reference"):
try:
doc.DO_Ref = avoir_data["reference"]
logger.info(f"📖 Référence: {avoir_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(avoir_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(avoir_data["lignes"], 1):
logger.info(f"--- Ligne {idx}: {ligne_data['article_code']} ---")
# Charger l'article RÉEL depuis Sage
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()
# 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"⚠️ Article {ligne_data['article_code']} a un prix = 0€ (toléré)")
# Créer la ligne
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3")
except:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite)
logger.info(f"✅ Article associé via SetDefaultArticleReference")
except Exception as e:
logger.warning(f"⚠️ SetDefaultArticleReference échoué: {e}, tentative avec objet")
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f"✅ Article associé via SetDefaultArticle")
except Exception as e2:
logger.error(f"❌ Toutes les méthodes ont échoué")
ligne_obj.DL_Design = designation_sage or ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
logger.warning("⚠️ Configuration manuelle appliquée")
# Vérifier le prix automatique
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
logger.info(f"💰 Prix auto chargé: {prix_auto}")
# 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:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
logger.info(f"💰 Prix personnalisé: {prix_a_utiliser}")
elif prix_auto == 0 and prix_sage > 0:
ligne_obj.DL_PrixUnitaire = float(prix_sage)
logger.info(f"💰 Prix Sage forcé: {prix_sage}")
elif prix_auto > 0:
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}")
# Écrire la ligne
ligne_obj.Write()
logger.info(f"✅ Ligne {idx} écrite")
# Validation
doc.Write()
process.Process()
if transaction_active:
self.cial.CptaApplication.CommitTrans()
# Récupération numéro
time.sleep(2)
numero_avoir = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3")
doc_result.Read()
numero_avoir = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_avoir:
numero_avoir = getattr(doc, "DO_Piece", "")
# Relecture
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(settings.SAGE_TYPE_BON_AVOIR, numero_avoir)
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"✅✅✅ AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC ✅✅✅")
return {
"numero_avoir": numero_avoir,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(avoir_data["lignes"]),
"client_code": avoir_data["client"]["code"],
"date_avoir": 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 avoir: {e}", exc_info=True)
raise RuntimeError(f"Échec création avoir: {str(e)}")
def modifier_avoir(self, numero: str, avoir_data: Dict) -> Dict:
"""
Modification d'un avoir existant
🔧 STRATÉGIE REMPLACEMENT LIGNES:
- Si nouvelles lignes fournies Supprime TOUTES les anciennes puis ajoute les nouvelles
- Utilise .Remove() pour la suppression
"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f"🔬 === MODIFICATION AVOIR {numero} ===")
# ÉTAPE 1 : CHARGER LE DOCUMENT
logger.info("📂 Chargement document...")
factory = self.cial.FactoryDocumentVente
persist = None
# Chercher le document
for type_test in [50, settings.SAGE_TYPE_BON_AVOIR]:
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
logger.info(f" ✅ Document trouvé (type={type_test})")
break
except:
continue
if not persist:
raise ValueError(f"❌ Avoir {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
logger.info(f" 📊 Statut={statut_actuel}")
# Vérifier qu'il n'est pas transformé
if statut_actuel == 5:
raise ValueError(f"L'avoir {numero} a déjà été transformé")
if statut_actuel == 6:
raise ValueError(f"L'avoir {numero} est annulé")
# Compter les lignes initiales
nb_lignes_initial = 0
try:
factory_lignes = getattr(doc, "FactoryDocumentLigne", None) or getattr(doc, "FactoryDocumentVenteLigne", None)
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes_initial += 1
index += 1
except:
break
logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}")
except Exception as e:
logger.warning(f" ⚠️ Erreur comptage lignes: {e}")
# ÉTAPE 2 : DÉTERMINER LES MODIFICATIONS
champs_modifies = []
modif_date = "date_avoir" in avoir_data
modif_statut = "statut" in avoir_data
modif_ref = "reference" in avoir_data
modif_lignes = "lignes" in avoir_data and avoir_data["lignes"] is not None
logger.info(f"📋 Modifications demandées:")
logger.info(f" Date: {modif_date}")
logger.info(f" Statut: {modif_statut}")
logger.info(f" Référence: {modif_ref}")
logger.info(f" Lignes: {modif_lignes}")
# ÉTAPE 3 : MODIFICATIONS SIMPLES
if not modif_lignes and (modif_date or modif_statut or modif_ref):
logger.info("🎯 Modifications simples (sans lignes)...")
if modif_date:
import pywintypes
date_str = avoir_data["date_avoir"]
if isinstance(date_str, str):
date_obj = datetime.fromisoformat(date_str)
elif isinstance(date_str, date):
date_obj = datetime.combine(date_str, datetime.min.time())
else:
date_obj = date_str
doc.DO_Date = pywintypes.Time(date_obj)
champs_modifies.append("date")
logger.info(f" ✅ Date définie: {date_obj.date()}")
if modif_statut:
nouveau_statut = avoir_data["statut"]
doc.DO_Statut = nouveau_statut
champs_modifies.append("statut")
logger.info(f" ✅ Statut défini: {nouveau_statut}")
if modif_ref:
try:
doc.DO_Ref = avoir_data["reference"]
champs_modifies.append("reference")
logger.info(f" ✅ Référence définie")
except Exception as e:
logger.warning(f" ⚠️ Référence non définie: {e}")
doc.Write()
logger.info(" ✅ Write() réussi")
# ÉTAPE 4 : REMPLACEMENT COMPLET LIGNES
elif modif_lignes:
logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...")
nouvelles_lignes = avoir_data["lignes"]
nb_nouvelles = len(nouvelles_lignes)
logger.info(f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles")
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
# SUPPRESSION TOUTES LES LIGNES
if nb_lignes_initial > 0:
logger.info(f" 🗑️ Suppression de {nb_lignes_initial} lignes...")
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
ligne.Read()
ligne.Remove()
logger.debug(f" ✅ Ligne {idx} supprimée")
except Exception as e:
logger.warning(f" ⚠️ Erreur suppression ligne {idx}: {e}")
logger.info(" ✅ Toutes les lignes supprimées")
# AJOUT NOUVELLES LIGNES
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
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()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3")
except:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
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
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"])
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.debug(f" ✅ Ligne {idx} ajoutée")
logger.info(f"{nb_nouvelles} nouvelles lignes ajoutées")
doc.Write()
champs_modifies.append("lignes")
# ÉTAPE 5 : RELECTURE ET RETOUR
import time
time.sleep(1)
doc.Read()
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
logger.info(f"✅✅✅ AVOIR MODIFIÉ: {numero} ✅✅✅")
logger.info(f" 💰 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 technique: {e}", exc_info=True)
raise RuntimeError(f"Erreur Sage: {str(e)}")
except ValueError as e:
logger.error(f"❌ Erreur métier: {e}")
raise
except Exception as e:
logger.error(f"❌ Erreur technique: {e}", exc_info=True)
raise RuntimeError(f"Erreur Sage: {str(e)}")