feat: Add API endpoints and SageClient methods for managing prospects, suppliers, credit notes, and delivery notes.

This commit is contained in:
Fanilo-Nantenaina 2025-12-04 13:47:28 +03:00
parent a73bdc4d9e
commit b4a76579b8
2 changed files with 288 additions and 127 deletions

209
api.py
View file

@ -154,8 +154,10 @@ from datetime import datetime
# MODÈLES PYDANTIC POUR USERS # MODÈLES PYDANTIC POUR USERS
# ===================================================== # =====================================================
class UserResponse(BaseModel): class UserResponse(BaseModel):
"""Modèle de réponse pour un utilisateur""" """Modèle de réponse pour un utilisateur"""
id: str id: str
email: str email: str
nom: str nom: str
@ -170,6 +172,7 @@ class UserResponse(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
# ===================================================== # =====================================================
# SERVICES EXTERNES (Universign) # SERVICES EXTERNES (Universign)
# ===================================================== # =====================================================
@ -1583,13 +1586,132 @@ async def statut_queue():
} }
# =====================================================
# ENDPOINTS - PROSPECTS
# =====================================================
@app.get("/prospects", tags=["Prospects"])
async def rechercher_prospects(query: Optional[str] = Query(None)):
"""🔍 Recherche prospects via gateway Windows"""
try:
prospects = sage_client.lister_prospects(filtre=query or "")
return prospects
except Exception as e:
logger.error(f"Erreur recherche prospects: {e}")
raise HTTPException(500, str(e))
@app.get("/prospects/{code}", tags=["Prospects"])
async def lire_prospect(code: str):
"""📄 Lecture d'un prospect par code"""
try:
prospect = sage_client.lire_prospect(code)
if not prospect:
raise HTTPException(404, f"Prospect {code} introuvable")
return prospect
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur lecture prospect: {e}")
raise HTTPException(500, str(e))
# =====================================================
# ENDPOINTS - FOURNISSEURS
# =====================================================
@app.get("/fournisseurs", tags=["Fournisseurs"])
async def rechercher_fournisseurs(query: Optional[str] = Query(None)):
"""🔍 Recherche fournisseurs via gateway Windows"""
try:
fournisseurs = sage_client.lister_fournisseurs(filtre=query or "")
return fournisseurs
except Exception as e:
logger.error(f"Erreur recherche fournisseurs: {e}")
raise HTTPException(500, str(e))
@app.get("/fournisseurs/{code}", tags=["Fournisseurs"])
async def lire_fournisseur(code: str):
"""📄 Lecture d'un fournisseur par code"""
try:
fournisseur = sage_client.lire_fournisseur(code)
if not fournisseur:
raise HTTPException(404, f"Fournisseur {code} introuvable")
return fournisseur
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur lecture fournisseur: {e}")
raise HTTPException(500, str(e))
# =====================================================
# ENDPOINTS - AVOIRS
# =====================================================
@app.get("/avoirs", tags=["Avoirs"])
async def lister_avoirs(
limit: int = Query(100, le=1000), statut: Optional[int] = Query(None)
):
"""📋 Liste tous les avoirs via gateway Windows"""
try:
avoirs = sage_client.lister_avoirs(limit=limit, statut=statut)
return avoirs
except Exception as e:
logger.error(f"Erreur liste avoirs: {e}")
raise HTTPException(500, str(e))
@app.get("/avoirs/{numero}", tags=["Avoirs"])
async def lire_avoir(numero: str):
"""📄 Lecture d'un avoir avec ses lignes"""
try:
avoir = sage_client.lire_avoir(numero)
if not avoir:
raise HTTPException(404, f"Avoir {numero} introuvable")
return avoir
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur lecture avoir: {e}")
raise HTTPException(500, str(e))
# =====================================================
# ENDPOINTS - LIVRAISONS
# =====================================================
@app.get("/livraisons", tags=["Livraisons"])
async def lister_livraisons(
limit: int = Query(100, le=1000), statut: Optional[int] = Query(None)
):
"""📋 Liste tous les bons de livraison via gateway Windows"""
try:
livraisons = sage_client.lister_livraisons(limit=limit, statut=statut)
return livraisons
except Exception as e:
logger.error(f"Erreur liste livraisons: {e}")
raise HTTPException(500, str(e))
@app.get("/livraisons/{numero}", tags=["Livraisons"])
async def lire_livraison(numero: str):
"""📄 Lecture d'une livraison avec ses lignes"""
try:
livraison = sage_client.lire_livraison(numero)
if not livraison:
raise HTTPException(404, f"Livraison {numero} introuvable")
return livraison
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur lecture livraison: {e}")
raise HTTPException(500, str(e))
@app.get("/debug/users", response_model=List[UserResponse], tags=["Debug"]) @app.get("/debug/users", response_model=List[UserResponse], tags=["Debug"])
async def lister_utilisateurs_debug( async def lister_utilisateurs_debug(
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
limit: int = Query(100, le=1000), limit: int = Query(100, le=1000),
role: Optional[str] = Query(None), role: Optional[str] = Query(None),
verified_only: bool = Query(False) verified_only: bool = Query(False),
): ):
""" """
🔓 **ROUTE DEBUG** - Liste tous les utilisateurs inscrits 🔓 **ROUTE DEBUG** - Liste tous les utilisateurs inscrits
@ -1629,7 +1751,8 @@ async def lister_utilisateurs_debug(
# Conversion en réponse # Conversion en réponse
users_response = [] users_response = []
for user in users: for user in users:
users_response.append(UserResponse( users_response.append(
UserResponse(
id=user.id, id=user.id,
email=user.email, email=user.email,
nom=user.nom, nom=user.nom,
@ -1639,10 +1762,13 @@ async def lister_utilisateurs_debug(
is_active=user.is_active, is_active=user.is_active,
created_at=user.created_at.isoformat() if user.created_at else "", created_at=user.created_at.isoformat() if user.created_at else "",
last_login=user.last_login.isoformat() if user.last_login else None, last_login=user.last_login.isoformat() if user.last_login else None,
failed_login_attempts=user.failed_login_attempts or 0 failed_login_attempts=user.failed_login_attempts or 0,
)) )
)
logger.info(f"📋 Liste utilisateurs retournée: {len(users_response)} résultat(s)") logger.info(
f"📋 Liste utilisateurs retournée: {len(users_response)} résultat(s)"
)
return users_response return users_response
@ -1652,9 +1778,7 @@ async def lister_utilisateurs_debug(
@app.get("/debug/users/stats", tags=["Debug"]) @app.get("/debug/users/stats", tags=["Debug"])
async def statistiques_utilisateurs( async def statistiques_utilisateurs(session: AsyncSession = Depends(get_session)):
session: AsyncSession = Depends(get_session)
):
""" """
📊 **ROUTE DEBUG** - Statistiques sur les utilisateurs 📊 **ROUTE DEBUG** - Statistiques sur les utilisateurs
@ -1690,7 +1814,7 @@ async def statistiques_utilisateurs(
"utilisateurs_actifs": active, "utilisateurs_actifs": active,
"utilisateurs_non_verifies": total - verified, "utilisateurs_non_verifies": total - verified,
"repartition_roles": roles_stats, "repartition_roles": roles_stats,
"taux_verification": f"{(verified/total*100):.1f}%" if total > 0 else "0%" "taux_verification": f"{(verified/total*100):.1f}%" if total > 0 else "0%",
} }
except Exception as e: except Exception as e:
@ -1700,8 +1824,7 @@ async def statistiques_utilisateurs(
@app.get("/debug/users/{user_id}", response_model=UserResponse, tags=["Debug"]) @app.get("/debug/users/{user_id}", response_model=UserResponse, tags=["Debug"])
async def lire_utilisateur_debug( async def lire_utilisateur_debug(
user_id: str, user_id: str, session: AsyncSession = Depends(get_session)
session: AsyncSession = Depends(get_session)
): ):
""" """
👤 **ROUTE DEBUG** - Détails d'un utilisateur par ID 👤 **ROUTE DEBUG** - Détails d'un utilisateur par ID
@ -1729,7 +1852,7 @@ async def lire_utilisateur_debug(
is_active=user.is_active, is_active=user.is_active,
created_at=user.created_at.isoformat() if user.created_at else "", created_at=user.created_at.isoformat() if user.created_at else "",
last_login=user.last_login.isoformat() if user.last_login else None, last_login=user.last_login.isoformat() if user.last_login else None,
failed_login_attempts=user.failed_login_attempts or 0 failed_login_attempts=user.failed_login_attempts or 0,
) )
except HTTPException: except HTTPException:
@ -1739,8 +1862,6 @@ async def lire_utilisateur_debug(
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# À ajouter dans api.py dans la section Debug
@app.get("/debug/database/check", tags=["Debug"]) @app.get("/debug/database/check", tags=["Debug"])
async def verifier_integrite_database(session: AsyncSession = Depends(get_session)): async def verifier_integrite_database(session: AsyncSession = Depends(get_session)):
""" """
@ -1757,17 +1878,15 @@ async def verifier_integrite_database(session: AsyncSession = Depends(get_sessio
# === TABLE USERS === # === TABLE USERS ===
# Compter tous les users # Compter tous les users
total_users = await session.execute(select(func.count(User.id))) total_users = await session.execute(select(func.count(User.id)))
diagnostics["users"] = { diagnostics["users"] = {"total": total_users.scalar(), "details": []}
"total": total_users.scalar(),
"details": []
}
# Lister tous les users avec détails # Lister tous les users avec détails
all_users = await session.execute(select(User)) all_users = await session.execute(select(User))
users_list = all_users.scalars().all() users_list = all_users.scalars().all()
for u in users_list: for u in users_list:
diagnostics["users"]["details"].append({ diagnostics["users"]["details"].append(
{
"id": u.id, "id": u.id,
"email": u.email, "email": u.email,
"nom": f"{u.prenom} {u.nom}", "nom": f"{u.prenom} {u.nom}",
@ -1777,39 +1896,33 @@ async def verifier_integrite_database(session: AsyncSession = Depends(get_sessio
"created_at": u.created_at.isoformat() if u.created_at else None, "created_at": u.created_at.isoformat() if u.created_at else None,
"has_reset_token": u.reset_token is not None, "has_reset_token": u.reset_token is not None,
"has_verification_token": u.verification_token is not None, "has_verification_token": u.verification_token is not None,
}) }
)
# === TABLE REFRESH_TOKENS === # === TABLE REFRESH_TOKENS ===
total_tokens = await session.execute(select(func.count(RefreshToken.id))) total_tokens = await session.execute(select(func.count(RefreshToken.id)))
diagnostics["refresh_tokens"] = { diagnostics["refresh_tokens"] = {"total": total_tokens.scalar()}
"total": total_tokens.scalar()
}
# === TABLE LOGIN_ATTEMPTS === # === TABLE LOGIN_ATTEMPTS ===
total_attempts = await session.execute(select(func.count(LoginAttempt.id))) total_attempts = await session.execute(select(func.count(LoginAttempt.id)))
diagnostics["login_attempts"] = { diagnostics["login_attempts"] = {"total": total_attempts.scalar()}
"total": total_attempts.scalar()
}
# === TABLE EMAIL_LOGS === # === TABLE EMAIL_LOGS ===
total_emails = await session.execute(select(func.count(EmailLog.id))) total_emails = await session.execute(select(func.count(EmailLog.id)))
diagnostics["email_logs"] = { diagnostics["email_logs"] = {"total": total_emails.scalar()}
"total": total_emails.scalar()
}
# === TABLE SIGNATURE_LOGS === # === TABLE SIGNATURE_LOGS ===
total_signatures = await session.execute(select(func.count(SignatureLog.id))) total_signatures = await session.execute(select(func.count(SignatureLog.id)))
diagnostics["signature_logs"] = { diagnostics["signature_logs"] = {"total": total_signatures.scalar()}
"total": total_signatures.scalar()
}
# === VÉRIFIER LES FICHIERS SQLITE === # === VÉRIFIER LES FICHIERS SQLITE ===
import os import os
db_file = "sage_dataven.db" db_file = "sage_dataven.db"
diagnostics["database_file"] = { diagnostics["database_file"] = {
"exists": os.path.exists(db_file), "exists": os.path.exists(db_file),
"size_bytes": os.path.getsize(db_file) if os.path.exists(db_file) else 0, "size_bytes": os.path.getsize(db_file) if os.path.exists(db_file) else 0,
"path": os.path.abspath(db_file) "path": os.path.abspath(db_file),
} }
# === TESTER UNE REQUÊTE RAW SQL === # === TESTER UNE REQUÊTE RAW SQL ===
@ -1817,18 +1930,15 @@ async def verifier_integrite_database(session: AsyncSession = Depends(get_sessio
raw_count = await session.execute(text("SELECT COUNT(*) FROM users")) raw_count = await session.execute(text("SELECT COUNT(*) FROM users"))
diagnostics["raw_sql_check"] = { diagnostics["raw_sql_check"] = {
"users_count": raw_count.scalar(), "users_count": raw_count.scalar(),
"status": "✅ Connexion DB OK" "status": "✅ Connexion DB OK",
} }
except Exception as e: except Exception as e:
diagnostics["raw_sql_check"] = { diagnostics["raw_sql_check"] = {"status": "❌ Erreur", "error": str(e)}
"status": "❌ Erreur",
"error": str(e)
}
return { return {
"success": True, "success": True,
"timestamp": datetime.now().isoformat(), "timestamp": datetime.now().isoformat(),
"diagnostics": diagnostics "diagnostics": diagnostics,
} }
except Exception as e: except Exception as e:
@ -1860,7 +1970,7 @@ async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_ses
role="user", role="user",
is_verified=True, is_verified=True,
is_active=True, is_active=True,
created_at=datetime.now() created_at=datetime.now(),
) )
session.add(test_user) session.add(test_user)
@ -1871,16 +1981,14 @@ async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_ses
logger.info(f"✅ ÉTAPE 1: User créé - {user_id}") logger.info(f"✅ ÉTAPE 1: User créé - {user_id}")
# === ÉTAPE 2: LECTURE === # === ÉTAPE 2: LECTURE ===
result = await session.execute( result = await session.execute(select(User).where(User.id == user_id))
select(User).where(User.id == user_id)
)
loaded_user = result.scalar_one_or_none() loaded_user = result.scalar_one_or_none()
if not loaded_user: if not loaded_user:
return { return {
"success": False, "success": False,
"error": "❌ User introuvable après création !", "error": "❌ User introuvable après création !",
"step": "LECTURE" "step": "LECTURE",
} }
logger.info(f"✅ ÉTAPE 2: User chargé - {loaded_user.email}") logger.info(f"✅ ÉTAPE 2: User chargé - {loaded_user.email}")
@ -1898,9 +2006,7 @@ async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_ses
logger.info(f"✅ ÉTAPE 3: User modifié") logger.info(f"✅ ÉTAPE 3: User modifié")
# === ÉTAPE 4: RE-LECTURE === # === ÉTAPE 4: RE-LECTURE ===
result2 = await session.execute( result2 = await session.execute(select(User).where(User.id == user_id))
select(User).where(User.id == user_id)
)
reloaded_user = result2.scalar_one_or_none() reloaded_user = result2.scalar_one_or_none()
if not reloaded_user: if not reloaded_user:
@ -1908,7 +2014,7 @@ async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_ses
"success": False, "success": False,
"error": "❌ User DISPARU après modification !", "error": "❌ User DISPARU après modification !",
"step": "RE-LECTURE", "step": "RE-LECTURE",
"user_id": user_id "user_id": user_id,
} }
logger.info(f"✅ ÉTAPE 4: User re-chargé - {reloaded_user.email}") logger.info(f"✅ ÉTAPE 4: User re-chargé - {reloaded_user.email}")
@ -1929,8 +2035,8 @@ async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_ses
"2. Lecture", "2. Lecture",
"3. Modification (reset password simulé)", "3. Modification (reset password simulé)",
"4. Re-lecture (vérification persistance)", "4. Re-lecture (vérification persistance)",
"5. Suppression (cleanup)" "5. Suppression (cleanup)",
] ],
} }
except Exception as e: except Exception as e:
@ -1942,9 +2048,10 @@ async def tester_persistance_utilisateur(session: AsyncSession = Depends(get_ses
return { return {
"success": False, "success": False,
"error": str(e), "error": str(e),
"traceback": str(e.__class__.__name__) "traceback": str(e.__class__.__name__),
} }
# ===================================================== # =====================================================
# LANCEMENT # LANCEMENT
# ===================================================== # =====================================================

View file

@ -256,6 +256,60 @@ class SageGatewayClient:
logger.error(f"Erreur génération PDF: {e}") logger.error(f"Erreur génération PDF: {e}")
raise raise
# =====================================================
# PROSPECTS
# =====================================================
def lister_prospects(self, filtre: str = "") -> List[Dict]:
"""Liste tous les prospects avec filtre optionnel"""
return self._post("/sage/prospects/list", {"filtre": filtre}).get("data", [])
def lire_prospect(self, code: str) -> Optional[Dict]:
"""Lecture d'un prospect par code"""
return self._post("/sage/prospects/get", {"code": code}).get("data")
# =====================================================
# FOURNISSEURS
# =====================================================
def lister_fournisseurs(self, filtre: str = "") -> List[Dict]:
"""Liste tous les fournisseurs avec filtre optionnel"""
return self._post("/sage/fournisseurs/list", {"filtre": filtre}).get("data", [])
def lire_fournisseur(self, code: str) -> Optional[Dict]:
"""Lecture d'un fournisseur par code"""
return self._post("/sage/fournisseurs/get", {"code": code}).get("data")
# =====================================================
# AVOIRS
# =====================================================
def lister_avoirs(
self, limit: int = 100, statut: Optional[int] = None
) -> List[Dict]:
"""Liste tous les avoirs"""
payload = {"limit": limit}
if statut is not None:
payload["statut"] = statut
return self._post("/sage/avoirs/list", payload).get("data", [])
def lire_avoir(self, numero: str) -> Optional[Dict]:
"""Lecture d'un avoir avec ses lignes"""
return self._post("/sage/avoirs/get", {"code": numero}).get("data")
# =====================================================
# LIVRAISONS
# =====================================================
def lister_livraisons(
self, limit: int = 100, statut: Optional[int] = None
) -> List[Dict]:
"""Liste tous les bons de livraison"""
payload = {"limit": limit}
if statut is not None:
payload["statut"] = statut
return self._post("/sage/livraisons/list", payload).get("data", [])
def lire_livraison(self, numero: str) -> Optional[Dict]:
"""Lecture d'une livraison avec ses lignes"""
return self._post("/sage/livraisons/get", {"code": numero}).get("data")
# ===================================================== # =====================================================
# CACHE (ADMIN) # CACHE (ADMIN)
# ===================================================== # =====================================================