diff --git a/Dockerfile b/Dockerfile
index 7e49ad0..0348090 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,7 +11,7 @@ RUN pip install --no-cache-dir --upgrade pip \
# Copier le reste du projet
COPY . .
-# ✅ Créer dossier persistant pour SQLite avec bonnes permissions
+# Créer dossier persistant pour SQLite avec bonnes permissions
RUN mkdir -p /app/data && chmod 777 /app/data
# Exposer le port
diff --git a/api.py b/api.py
index a7bbacb..bff3fe0 100644
--- a/api.py
+++ b/api.py
@@ -30,6 +30,8 @@ from database import (
from email_queue import email_queue
from sage_client import sage_client
+from schemas import TiersDetails, TypeTiers
+
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
@@ -1582,7 +1584,7 @@ class ClientCreateRequest(BaseModel):
def to_sage_dict(self) -> dict:
"""
Convertit le modèle en dictionnaire compatible avec creer_client()
- ✅ Mapping 1:1 avec les paramètres réels de la fonction
+ Mapping 1:1 avec les paramètres réels de la fonction
"""
stat01 = self.statistique01 or self.secteur
@@ -1701,8 +1703,8 @@ class ClientCreateRequest(BaseModel):
class ClientUpdateRequest(BaseModel):
"""
Modèle pour modification d'un client existant
- ✅ TOUS les champs de ClientCreateRequest sont modifiables
- ✅ TOUS optionnels (seuls les champs fournis sont modifiés)
+ TOUS les champs de ClientCreateRequest sont modifiables
+ TOUS optionnels (seuls les champs fournis sont modifiés)
"""
intitule: Optional[str] = Field(None, max_length=69)
@@ -2842,7 +2844,7 @@ templates_signature_email = {
|
- 🔐 Signature certifiée : Ce document a été signé avec une signature
+ Signature certifiée : Ce document a été signé avec une signature
électronique qualifiée, ayant la même valeur juridique qu'une signature manuscrite
conformément au règlement eIDAS.
@@ -3004,7 +3006,7 @@ async def universign_envoyer(
api_url = settings.universign_api_url
auth = (api_key, "")
- logger.info(f"🔐 Démarrage processus Universign pour {email}")
+ logger.info(f" Démarrage processus Universign pour {email}")
logger.info(f"📌 Document: {doc_id} ({doc_data.get('type_label')})")
if not pdf_bytes or len(pdf_bytes) == 0:
@@ -3089,7 +3091,7 @@ async def universign_envoyer(
field_id = response.json().get("id")
logger.info(f"Champ créé: {field_id}")
- logger.info("👤 ÉTAPE 5/6 : Liaison signataire au champ")
+ logger.info(" ÉTAPE 5/6 : Liaison signataire au champ")
response = requests.post(
f"{api_url}/transactions/{transaction_id}/signatures", # /signatures pas /signers
@@ -3144,7 +3146,7 @@ async def universign_envoyer(
logger.info("URL récupérée")
- logger.info("📧 Préparation email")
+ logger.info(" Préparation email")
template = templates_signature_email["demande_signature"]
@@ -4137,7 +4139,7 @@ async def webhook_universign(
email_queue.enqueue(email_log.id)
logger.info(
- f"📧 Email de confirmation envoyé: {signature_log.email_signataire}"
+ f" Email de confirmation envoyé: {signature_log.email_signataire}"
)
elif event_type == "transaction.refused":
@@ -4238,7 +4240,7 @@ async def relancer_signatures_automatique(session: AsyncSession = Depends(get_se
nb_relances += 1
logger.info(
- f"📧 Relance envoyée: {signature.document_id} ({signature.nb_relances}/3)"
+ f" Relance envoyée: {signature.document_id} ({signature.nb_relances}/3)"
)
except Exception as e:
@@ -6083,6 +6085,42 @@ async def definir_contact_defaut(numero: str, contact_numero: int):
raise HTTPException(500, str(e))
+@app.get("/tiers", response_model=List[TiersDetails], tags=["Tiers"])
+async def obtenir_tiers(
+ type_tiers: Optional[TypeTiers] = Query(
+ None,
+ description="Filtre par type: client, fournisseur, prospect, ou all"
+ ),
+ query: Optional[str] = Query(
+ None,
+ description="Recherche sur code ou intitulé"
+ )
+):
+ try:
+ tiers = sage_client.lister_tiers(
+ type_tiers=type_tiers.value if type_tiers else None,
+ filtre=query or ""
+ )
+ return [TiersDetails(**t) for t in tiers]
+ except Exception as e:
+ logger.error(f" Erreur recherche tiers: {e}")
+ raise HTTPException(500, str(e))
+
+
+@app.get("/tiers/{code}", response_model=TiersDetails, tags=["Tiers"])
+async def lire_tiers_detail(code: str):
+ try:
+ tiers = sage_client.lire_tiers(code)
+ if not tiers:
+ raise HTTPException(404, f"Tiers {code} introuvable")
+ return TiersDetails(**tiers)
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f" Erreur lecture tiers {code}: {e}")
+ raise HTTPException(500, str(e))
+
+
if __name__ == "__main__":
uvicorn.run(
"api:app",
diff --git a/core/dependencies.py b/core/dependencies.py
index 7f8a5f9..69b6751 100644
--- a/core/dependencies.py
+++ b/core/dependencies.py
@@ -5,7 +5,7 @@ from sqlalchemy import select
from database import get_session, User
from security.auth import decode_token
from typing import Optional
-from datetime import datetime # ✅ AJOUT MANQUANT - C'ÉTAIT LE BUG !
+from datetime import datetime # AJOUT MANQUANT - C'ÉTAIT LE BUG !
security = HTTPBearer()
@@ -14,14 +14,6 @@ async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
session: AsyncSession = Depends(get_session),
) -> User:
- """
- Dépendance FastAPI pour extraire l'utilisateur du JWT
-
- Usage dans un endpoint:
- @app.get("/protected")
- async def protected_route(user: User = Depends(get_current_user)):
- return {"user_id": user.id}
- """
token = credentials.credentials
# Décoder le token
@@ -73,7 +65,7 @@ async def get_current_user(
detail="Email non vérifié. Consultez votre boîte de réception.",
)
- # ✅ FIX: Vérifier si le compte est verrouillé (maintenant datetime est importé!)
+ # FIX: Vérifier si le compte est verrouillé (maintenant datetime est importé!)
if user.locked_until and user.locked_until > datetime.now():
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
diff --git a/create_admin.py b/create_admin.py
index 41f11b7..41b3da3 100644
--- a/create_admin.py
+++ b/create_admin.py
@@ -27,20 +27,20 @@ async def create_admin():
"""Crée un utilisateur admin"""
print("\n" + "=" * 60)
- print("🔐 Création d'un compte administrateur")
+ print(" Création d'un compte administrateur")
print("=" * 60 + "\n")
# Saisie des informations
email = input("Email de l'admin: ").strip().lower()
if not email or "@" not in email:
- print("❌ Email invalide")
+ print(" Email invalide")
return False
prenom = input("Prénom: ").strip()
nom = input("Nom: ").strip()
if not prenom or not nom:
- print("❌ Prénom et nom requis")
+ print(" Prénom et nom requis")
return False
# Mot de passe avec validation
@@ -55,9 +55,9 @@ async def create_admin():
if password == confirm:
break
else:
- print("❌ Les mots de passe ne correspondent pas\n")
+ print(" Les mots de passe ne correspondent pas\n")
else:
- print(f"❌ {error_msg}\n")
+ print(f" {error_msg}\n")
# Vérifier si l'email existe déjà
async with async_session_factory() as session:
@@ -67,7 +67,7 @@ async def create_admin():
existing = result.scalar_one_or_none()
if existing:
- print(f"\n❌ Un utilisateur avec l'email {email} existe déjà")
+ print(f"\n Un utilisateur avec l'email {email} existe déjà")
return False
# Créer l'admin
@@ -86,12 +86,12 @@ async def create_admin():
session.add(admin)
await session.commit()
- print("\n✅ Administrateur créé avec succès!")
- print(f"📧 Email: {email}")
- print(f"👤 Nom: {prenom} {nom}")
- print(f"🔑 Rôle: admin")
- print(f"🆔 ID: {admin.id}")
- print("\n💡 Vous pouvez maintenant vous connecter à l'API\n")
+ print("\n Administrateur créé avec succès!")
+ print(f" Email: {email}")
+ print(f" Nom: {prenom} {nom}")
+ print(f" Rôle: admin")
+ print(f" ID: {admin.id}")
+ print("\n Vous pouvez maintenant vous connecter à l'API\n")
return True
@@ -101,9 +101,9 @@ if __name__ == "__main__":
result = asyncio.run(create_admin())
sys.exit(0 if result else 1)
except KeyboardInterrupt:
- print("\n\n❌ Création annulée")
+ print("\n\n Création annulée")
sys.exit(1)
except Exception as e:
- print(f"\n❌ Erreur: {e}")
+ print(f"\n Erreur: {e}")
logger.exception("Détails:")
sys.exit(1)
diff --git a/database/db_config.py b/database/db_config.py
index f5bc0b4..eb7d347 100644
--- a/database/db_config.py
+++ b/database/db_config.py
@@ -27,17 +27,17 @@ async_session_factory = async_sessionmaker(
async def init_db():
"""
Crée toutes les tables dans la base de données
- ⚠️ Utilise create_all qui ne crée QUE les tables manquantes
+ Utilise create_all qui ne crée QUE les tables manquantes
"""
try:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
- logger.info("✅ Base de données initialisée avec succès")
- logger.info(f"📍 Fichier DB: {DATABASE_URL}")
+ logger.info(" Base de données initialisée avec succès")
+ logger.info(f" Fichier DB: {DATABASE_URL}")
except Exception as e:
- logger.error(f"❌ Erreur initialisation DB: {e}")
+ logger.error(f" Erreur initialisation DB: {e}")
raise
@@ -53,4 +53,4 @@ async def get_session() -> AsyncSession:
async def close_db():
"""Ferme proprement toutes les connexions"""
await engine.dispose()
- logger.info("🔌 Connexions DB fermées")
+ logger.info(" Connexions DB fermées")
diff --git a/email_queue.py b/email_queue.py
index 344f986..94e975e 100644
--- a/email_queue.py
+++ b/email_queue.py
@@ -20,9 +20,6 @@ logger = logging.getLogger(__name__)
class EmailQueue:
- """
- Queue d'emails avec workers threadés et retry automatique
- """
def __init__(self):
self.queue = queue.Queue()
@@ -45,23 +42,23 @@ class EmailQueue:
worker.start()
self.workers.append(worker)
- logger.info(f"✅ Queue email démarrée avec {num_workers} worker(s)")
+ logger.info(f" Queue email démarrée avec {num_workers} worker(s)")
def stop(self):
"""Arrête les workers proprement"""
- logger.info("🛑 Arrêt de la queue email...")
+ logger.info(" Arrêt de la queue email...")
self.running = False
try:
self.queue.join()
- logger.info("✅ Queue email arrêtée proprement")
+ logger.info(" Queue email arrêtée proprement")
except:
- logger.warning("⚠️ Timeout lors de l'arrêt de la queue")
+ logger.warning(" Timeout lors de l'arrêt de la queue")
def enqueue(self, email_log_id: str):
"""Ajoute un email dans la queue"""
self.queue.put(email_log_id)
- logger.debug(f"📨 Email {email_log_id} ajouté à la queue")
+ logger.debug(f" Email {email_log_id} ajouté à la queue")
def _worker(self):
"""Worker qui traite les emails dans un thread"""
@@ -80,7 +77,7 @@ class EmailQueue:
except queue.Empty:
continue
except Exception as e:
- logger.error(f"❌ Erreur worker: {e}", exc_info=True)
+ logger.error(f" Erreur worker: {e}", exc_info=True)
try:
self.queue.task_done()
except:
@@ -94,7 +91,7 @@ class EmailQueue:
from sqlalchemy import select
if not self.session_factory:
- logger.error("❌ session_factory non configuré")
+ logger.error(" session_factory non configuré")
return
async with self.session_factory() as session:
@@ -104,7 +101,7 @@ class EmailQueue:
email_log = result.scalar_one_or_none()
if not email_log:
- logger.error(f"❌ Email log {email_log_id} introuvable")
+ logger.error(f" Email log {email_log_id} introuvable")
return
email_log.statut = StatutEmail.EN_COURS
@@ -117,7 +114,7 @@ class EmailQueue:
email_log.statut = StatutEmail.ENVOYE
email_log.date_envoi = datetime.now()
email_log.derniere_erreur = None
- logger.info(f"✅ Email envoyé: {email_log.destinataire}")
+ logger.info(f" Email envoyé: {email_log.destinataire}")
except Exception as e:
email_log.statut = StatutEmail.ERREUR
@@ -134,10 +131,10 @@ class EmailQueue:
timer.start()
logger.warning(
- f"⚠️ Retry prévu dans {delay}s pour {email_log.destinataire}"
+ f" Retry prévu dans {delay}s pour {email_log.destinataire}"
)
else:
- logger.error(f"❌ Échec définitif: {email_log.destinataire} - {e}")
+ logger.error(f" Échec définitif: {email_log.destinataire} - {e}")
await session.commit()
@@ -176,20 +173,20 @@ class EmailQueue:
logger.info(f"📎 PDF attaché: {doc_id}.pdf")
except Exception as e:
- logger.error(f"❌ Erreur génération PDF {doc_id}: {e}")
+ logger.error(f" Erreur génération PDF {doc_id}: {e}")
await asyncio.to_thread(self._send_smtp, msg)
def _generate_pdf(self, doc_id: str, type_doc: int) -> bytes:
if not self.sage_client:
- logger.error("❌ sage_client non configuré")
+ logger.error(" sage_client non configuré")
raise Exception("sage_client non disponible")
try:
doc = self.sage_client.lire_document(doc_id, type_doc)
except Exception as e:
- logger.error(f"❌ Erreur récupération document {doc_id}: {e}")
+ logger.error(f" Erreur récupération document {doc_id}: {e}")
raise Exception(f"Document {doc_id} inaccessible")
if not doc:
@@ -284,11 +281,10 @@ class EmailQueue:
pdf.save()
buffer.seek(0)
- logger.info(f"✅ PDF généré: {doc_id}.pdf")
+ logger.info(f" PDF généré: {doc_id}.pdf")
return buffer.read()
def _send_smtp(self, msg):
- """Envoi SMTP bloquant (appelé depuis asyncio.to_thread)"""
try:
with smtplib.SMTP(
settings.smtp_host, settings.smtp_port, timeout=30
diff --git a/init_db.py b/init_db.py
index 7f5c174..2f1d61a 100644
--- a/init_db.py
+++ b/init_db.py
@@ -14,7 +14,7 @@ from pathlib import Path
# Ajouter le répertoire parent au path pour les imports
sys.path.insert(0, str(Path(__file__).parent))
-from database import init_db # ✅ Import depuis database/__init__.py
+from database import init_db # Import depuis database/__init__.py
import logging
logging.basicConfig(level=logging.INFO)
@@ -32,8 +32,8 @@ async def main():
# Créer les tables
await init_db()
- print("\n✅ Base de données créée avec succès!")
- print(f"📍 Fichier: sage_dataven.db")
+ print("\n Base de données créée avec succès!")
+ print(f" Fichier: sage_dataven.db")
print("\n📊 Tables créées:")
print(" ├─ email_logs (Journalisation emails)")
@@ -53,7 +53,7 @@ async def main():
return True
except Exception as e:
- print(f"\n❌ Erreur lors de l'initialisation: {e}")
+ print(f"\n Erreur lors de l'initialisation: {e}")
logger.exception("Détails de l'erreur:")
return False
diff --git a/routes/auth.py b/routes/auth.py
index 54406c1..c59bd20 100644
--- a/routes/auth.py
+++ b/routes/auth.py
@@ -95,12 +95,7 @@ async def log_login_attempt(
async def check_rate_limit(
session: AsyncSession, email: str, ip: str
) -> tuple[bool, str]:
- """
- Vérifie si l'utilisateur/IP est rate limité
- Returns:
- (is_allowed, error_message)
- """
time_window = datetime.now() - timedelta(minutes=15)
result = await session.execute(
@@ -126,13 +121,7 @@ async def register(
request: Request,
session: AsyncSession = Depends(get_session),
):
- """
- 📝 Inscription d'un nouvel utilisateur
- - Valide le mot de passe
- - Crée le compte (non vérifié)
- - Envoie email de vérification
- """
result = await session.execute(select(User).where(User.email == data.email))
existing_user = result.scalar_one_or_none()
@@ -170,7 +159,7 @@ async def register(
if not email_sent:
logger.warning(f"Échec envoi email vérification pour {data.email}")
- logger.info(f"✅ Nouvel utilisateur inscrit: {data.email} (ID: {new_user.id})")
+ logger.info(f" Nouvel utilisateur inscrit: {data.email} (ID: {new_user.id})")
return {
"success": True,
@@ -183,7 +172,7 @@ async def register(
@router.get("/verify-email")
async def verify_email_get(token: str, session: AsyncSession = Depends(get_session)):
"""
- ✅ Vérification de l'email via lien cliquable (GET)
+ Vérification de l'email via lien cliquable (GET)
Utilisé quand l'utilisateur clique sur le lien dans l'email
"""
result = await session.execute(select(User).where(User.verification_token == token))
@@ -207,11 +196,11 @@ async def verify_email_get(token: str, session: AsyncSession = Depends(get_sessi
user.verification_token_expires = None
await session.commit()
- logger.info(f"✅ Email vérifié: {user.email}")
+ logger.info(f" Email vérifié: {user.email}")
return {
"success": True,
- "message": "✅ Email vérifié avec succès ! Vous pouvez maintenant vous connecter.",
+ "message": " Email vérifié avec succès ! Vous pouvez maintenant vous connecter.",
"email": user.email,
}
@@ -221,7 +210,7 @@ async def verify_email_post(
data: VerifyEmailRequest, session: AsyncSession = Depends(get_session)
):
"""
- ✅ Vérification de l'email via API (POST)
+ Vérification de l'email via API (POST)
Utilisé pour les appels programmatiques depuis le frontend
"""
result = await session.execute(
@@ -246,7 +235,7 @@ async def verify_email_post(
user.verification_token_expires = None
await session.commit()
- logger.info(f"✅ Email vérifié: {user.email}")
+ logger.info(f" Email vérifié: {user.email}")
return {
"success": True,
@@ -260,9 +249,6 @@ async def resend_verification(
request: Request,
session: AsyncSession = Depends(get_session),
):
- """
- 🔄 Renvoyer l'email de vérification
- """
result = await session.execute(select(User).where(User.email == data.email.lower()))
user = result.scalar_one_or_none()
@@ -292,11 +278,6 @@ async def resend_verification(
async def login(
data: LoginRequest, request: Request, session: AsyncSession = Depends(get_session)
):
- """
- 🔐 Connexion utilisateur
-
- Retourne access_token (30min) et refresh_token (7 jours)
- """
ip = request.client.host if request.client else "unknown"
user_agent = request.headers.get("user-agent", "unknown")
@@ -388,7 +369,7 @@ async def login(
await log_login_attempt(session, data.email.lower(), ip, user_agent, True)
- logger.info(f"✅ Connexion réussie: {user.email}")
+ logger.info(f" Connexion réussie: {user.email}")
return TokenResponse(
access_token=access_token,
@@ -401,9 +382,7 @@ async def login(
async def refresh_access_token(
data: RefreshTokenRequest, session: AsyncSession = Depends(get_session)
):
- """
- 🔄 Renouvellement du access_token via refresh_token
- """
+
payload = decode_token(data.refresh_token)
if not payload or payload.get("type") != "refresh":
raise HTTPException(
@@ -446,7 +425,7 @@ async def refresh_access_token(
{"sub": user.id, "email": user.email, "role": user.role}
)
- logger.info(f"🔄 Token rafraîchi: {user.email}")
+ logger.info(f" Token rafraîchi: {user.email}")
return TokenResponse(
access_token=new_access_token,
@@ -461,9 +440,7 @@ async def forgot_password(
request: Request,
session: AsyncSession = Depends(get_session),
):
- """
- 🔑 Demande de réinitialisation de mot de passe
- """
+
result = await session.execute(select(User).where(User.email == data.email.lower()))
user = result.scalar_one_or_none()
@@ -485,7 +462,7 @@ async def forgot_password(
)
AuthEmailService.send_password_reset_email(user.email, reset_token, frontend_url)
- logger.info(f"📧 Reset password demandé: {user.email}")
+ logger.info(f" Reset password demandé: {user.email}")
return {
"success": True,
@@ -497,9 +474,7 @@ async def forgot_password(
async def reset_password(
data: ResetPasswordRequest, session: AsyncSession = Depends(get_session)
):
- """
- 🔐 Réinitialisation du mot de passe avec token
- """
+
result = await session.execute(select(User).where(User.reset_token == data.token))
user = result.scalar_one_or_none()
@@ -528,7 +503,7 @@ async def reset_password(
AuthEmailService.send_password_changed_notification(user.email)
- logger.info(f"🔐 Mot de passe réinitialisé: {user.email}")
+ logger.info(f" Mot de passe réinitialisé: {user.email}")
return {
"success": True,
@@ -542,9 +517,7 @@ async def logout(
session: AsyncSession = Depends(get_session),
user: User = Depends(get_current_user),
):
- """
- 🚪 Déconnexion (révocation du refresh token)
- """
+
token_hash = hash_token(data.refresh_token)
result = await session.execute(
@@ -566,9 +539,7 @@ async def logout(
@router.get("/me")
async def get_current_user_info(user: User = Depends(get_current_user)):
- """
- 👤 Récupération du profil utilisateur
- """
+
return {
"id": user.id,
"email": user.email,
diff --git a/sage_client.py b/sage_client.py
index aba8338..0384be1 100644
--- a/sage_client.py
+++ b/sage_client.py
@@ -32,7 +32,7 @@ class SageGatewayClient:
except requests.exceptions.RequestException as e:
if attempt == retries - 1:
logger.error(
- f"❌ Échec après {retries} tentatives sur {endpoint}: {e}"
+ f" Échec après {retries} tentatives sur {endpoint}: {e}"
)
raise
time.sleep(2**attempt)
@@ -54,7 +54,7 @@ class SageGatewayClient:
except requests.exceptions.RequestException as e:
if attempt == retries - 1:
logger.error(
- f"❌ Échec GET après {retries} tentatives sur {endpoint}: {e}"
+ f" Échec GET après {retries} tentatives sur {endpoint}: {e}"
)
raise
time.sleep(2**attempt)
@@ -108,7 +108,7 @@ class SageGatewayClient:
r.raise_for_status()
return r.json().get("data", {})
except requests.exceptions.RequestException as e:
- logger.error(f"❌ Erreur changement statut: {e}")
+ logger.error(f" Erreur changement statut: {e}")
raise
def lire_document(self, numero: str, type_doc: int) -> Optional[Dict]:
@@ -134,7 +134,7 @@ class SageGatewayClient:
r.raise_for_status()
return r.json().get("data", {})
except requests.exceptions.RequestException as e:
- logger.error(f"❌ Erreur transformation: {e}")
+ logger.error(f" Erreur transformation: {e}")
raise
def mettre_a_jour_champ_libre(
@@ -352,7 +352,7 @@ class SageGatewayClient:
pdf_bytes = base64.b64decode(pdf_base64)
- logger.info(f"✅ PDF décodé: {len(pdf_bytes)} octets")
+ logger.info(f" PDF décodé: {len(pdf_bytes)} octets")
return pdf_bytes
@@ -364,11 +364,11 @@ class SageGatewayClient:
)
except requests.exceptions.RequestException as e:
- logger.error(f"❌ Erreur HTTP génération PDF: {e}")
+ logger.error(f" Erreur HTTP génération PDF: {e}")
raise RuntimeError(f"Erreur de communication avec la gateway: {str(e)}")
except Exception as e:
- logger.error(f"❌ Erreur génération PDF: {e}", exc_info=True)
+ logger.error(f" Erreur génération PDF: {e}", exc_info=True)
raise
def creer_article(self, article_data: Dict) -> Dict:
@@ -420,7 +420,7 @@ class SageGatewayClient:
r.raise_for_status()
return r.json().get("data", {})
except requests.exceptions.RequestException as e:
- logger.error(f"❌ Erreur listage modèles: {e}")
+ logger.error(f" Erreur listage modèles: {e}")
raise
def generer_pdf_document(
@@ -446,7 +446,7 @@ class SageGatewayClient:
return r.content
except requests.exceptions.RequestException as e:
- logger.error(f"❌ Erreur génération PDF: {e}")
+ logger.error(f" Erreur génération PDF: {e}")
raise
@@ -476,16 +476,6 @@ class SageGatewayClient:
def supprimer_contact(self, numero: str, contact_numero: int) -> Dict:
- """
- Supprime un contact
-
- Args:
- numero: Code du client
- contact_numero: Numéro unique du contact
-
- Returns:
- Dictionnaire avec le statut de la suppression
- """
return self._post("/sage/contacts/delete", {
"numero": numero,
"contact_numero": contact_numero
@@ -493,19 +483,22 @@ class SageGatewayClient:
def definir_contact_defaut(self, numero: str, contact_numero: int) -> Dict:
- """
- Définit un contact comme contact par défaut du client
-
- Args:
- numero: Code du client
- contact_numero: Numéro unique du contact à définir comme par défaut
-
- Returns:
- Dictionnaire avec les données du client mis à jour
- """
return self._post("/sage/contacts/set-default", {
"numero": numero,
"contact_numero": contact_numero
})
+
+
+ def lister_tiers(self, type_tiers: Optional[str] = None, filtre: str = "") -> List[Dict]:
+ return self._post("/sage/tiers/list", {
+ "type_tiers": type_tiers,
+ "filtre": filtre
+ }).get("data", [])
+
+
+ def lire_tiers(self, code: str) -> Optional[Dict]:
+ return self._post("/sage/tiers/get", {
+ "code": code
+ }).get("data")
sage_client = SageGatewayClient()
diff --git a/schemas/__init__.py b/schemas/__init__.py
new file mode 100644
index 0000000..c987039
--- /dev/null
+++ b/schemas/__init__.py
@@ -0,0 +1,9 @@
+from schemas.tiers.tiers import (TiersDetails,)
+from schemas.tiers.type_tiers import (TypeTiers,)
+from schemas.tiers.contact import (Contact,)
+
+__all__ = [
+ "TiersDetails",
+ "Contact",
+ "TypeTiers",
+]
\ No newline at end of file
diff --git a/schemas/tiers/contact.py b/schemas/tiers/contact.py
new file mode 100644
index 0000000..8c8bcc1
--- /dev/null
+++ b/schemas/tiers/contact.py
@@ -0,0 +1,41 @@
+from typing import Optional, ClassVar
+from pydantic import BaseModel, Field, validator
+
+class Contact(BaseModel):
+ """Contact associé à un tiers"""
+ numero: Optional[str] = Field(None, description="Code du tiers parent (CT_Num)")
+ contact_numero: Optional[int] = Field(None, description="Numéro unique du contact (CT_No)")
+ n_contact: Optional[int] = Field(None, description="Numéro de référence contact (N_Contact)")
+
+ civilite: Optional[str] = Field(None, description="Civilité : M., Mme, Mlle (CT_Civilite)")
+ nom: Optional[str] = Field(None, description="Nom de famille (CT_Nom)")
+ prenom: Optional[str] = Field(None, description="Prénom (CT_Prenom)")
+ fonction: Optional[str] = Field(None, description="Fonction/Titre (CT_Fonction)")
+
+ service_code: Optional[int] = Field(None, description="Code du service (N_Service)")
+
+ telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)")
+ portable: Optional[str] = Field(None, description="Téléphone mobile (CT_TelPortable)")
+ telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)")
+ email: Optional[str] = Field(None, description="Adresse email (CT_EMail)")
+
+ facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)")
+ linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)")
+ skype: Optional[str] = Field(None, description="Identifiant Skype (CT_Skype)")
+
+ est_defaut: Optional[bool] = Field(False, description="Contact par défaut")
+
+ civilite_map: ClassVar[dict] = {
+ 0: "M.",
+ 1: "Mme",
+ 2: "Mlle",
+ 3: "Société",
+ }
+
+ @validator("civilite", pre=True, always=True)
+ def convert_civilite(cls, v):
+ if v is None:
+ return v
+ if isinstance(v, int):
+ return cls.civilite_map.get(v, str(v))
+ return v
\ No newline at end of file
diff --git a/schemas/tiers/tiers.py b/schemas/tiers/tiers.py
new file mode 100644
index 0000000..69bfa79
--- /dev/null
+++ b/schemas/tiers/tiers.py
@@ -0,0 +1,104 @@
+from typing import List, Optional
+from pydantic import BaseModel, Field
+from schemas import Contact
+
+class TiersDetails(BaseModel):
+ # IDENTIFICATION
+ numero: Optional[str] = Field(None, description="Code tiers (CT_Num)")
+ intitule: Optional[str] = Field(None, description="Raison sociale ou Nom complet (CT_Intitule)")
+ type_tiers: Optional[int] = Field(None, description="Type : 0=Client, 1=Fournisseur (CT_Type)")
+ qualite: Optional[str] = Field(None, description="Qualité Sage : CLI, FOU, PRO (CT_Qualite)")
+ classement: Optional[str] = Field(None, description="Code de classement (CT_Classement)")
+ raccourci: Optional[str] = Field(None, description="Code raccourci 7 car. (CT_Raccourci)")
+ siret: Optional[str] = Field(None, description="N° SIRET 14 chiffres (CT_Siret)")
+ tva_intra: Optional[str] = Field(None, description="N° TVA intracommunautaire (CT_Identifiant)")
+ code_naf: Optional[str] = Field(None, description="Code NAF/APE (CT_Ape)")
+
+ # ADRESSE
+ contact: Optional[str] = Field(None, description="Nom du contact principal (CT_Contact)")
+ adresse: Optional[str] = Field(None, description="Adresse ligne 1 (CT_Adresse)")
+ complement: Optional[str] = Field(None, description="Complément d'adresse (CT_Complement)")
+ code_postal: Optional[str] = Field(None, description="Code postal (CT_CodePostal)")
+ ville: Optional[str] = Field(None, description="Ville (CT_Ville)")
+ region: Optional[str] = Field(None, description="Région/État (CT_CodeRegion)")
+ pays: Optional[str] = Field(None, description="Pays (CT_Pays)")
+
+ # TELECOM
+ telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)")
+ telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)")
+ email: Optional[str] = Field(None, description="Email principal (CT_EMail)")
+ site_web: Optional[str] = Field(None, description="Site web (CT_Site)")
+ facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)")
+ linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)")
+
+ # TAUX
+ taux01: Optional[float] = Field(None, description="Taux personnalisé 1 (CT_Taux01)")
+ taux02: Optional[float] = Field(None, description="Taux personnalisé 2 (CT_Taux02)")
+ taux03: Optional[float] = Field(None, description="Taux personnalisé 3 (CT_Taux03)")
+ taux04: Optional[float] = Field(None, description="Taux personnalisé 4 (CT_Taux04)")
+
+ # STATISTIQUES
+ statistique01: Optional[str] = Field(None, description="Statistique 1 (CT_Statistique01)")
+ statistique02: Optional[str] = Field(None, description="Statistique 2 (CT_Statistique02)")
+ statistique03: Optional[str] = Field(None, description="Statistique 3 (CT_Statistique03)")
+ statistique04: Optional[str] = Field(None, description="Statistique 4 (CT_Statistique04)")
+ statistique05: Optional[str] = Field(None, description="Statistique 5 (CT_Statistique05)")
+ statistique06: Optional[str] = Field(None, description="Statistique 6 (CT_Statistique06)")
+ statistique07: Optional[str] = Field(None, description="Statistique 7 (CT_Statistique07)")
+ statistique08: Optional[str] = Field(None, description="Statistique 8 (CT_Statistique08)")
+ statistique09: Optional[str] = Field(None, description="Statistique 9 (CT_Statistique09)")
+ statistique10: Optional[str] = Field(None, description="Statistique 10 (CT_Statistique10)")
+
+ # COMMERCIAL
+ encours_autorise: Optional[float] = Field(None, description="Encours maximum autorisé (CT_Encours)")
+ assurance_credit: Optional[float] = Field(None, description="Montant assurance crédit (CT_Assurance)")
+ langue: Optional[int] = Field(None, description="Code langue 0=FR, 1=EN (CT_Langue)")
+ commercial_code: Optional[int] = Field(None, description="Code du commercial (CO_No)")
+
+ # FACTURATION
+ lettrage_auto: Optional[bool] = Field(None, description="Lettrage automatique (CT_Lettrage)")
+ est_actif: Optional[bool] = Field(None, description="True si actif (CT_Sommeil=0)")
+ type_facture: Optional[int] = Field(None, description="Type facture 0=Facture, 1=BL (CT_Facture)")
+ est_prospect: Optional[bool] = Field(None, description="True si prospect (CT_Prospect=1)")
+ bl_en_facture: Optional[int] = Field(None, description="Imprimer BL en facture (CT_BLFact)")
+ saut_page: Optional[int] = Field(None, description="Saut de page sur documents (CT_Saut)")
+ validation_echeance: Optional[int] = Field(None, description="Valider les échéances (CT_ValidEch)")
+ controle_encours: Optional[int] = Field(None, description="Contrôler l'encours (CT_ControlEnc)")
+ exclure_relance: Optional[bool] = Field(None, description="Exclure des relances (CT_NotRappel)")
+ exclure_penalites: Optional[bool] = Field(None, description="Exclure des pénalités (CT_NotPenal)")
+ bon_a_payer: Optional[int] = Field(None, description="Bon à payer obligatoire (CT_BonAPayer)")
+
+ # LOGISTIQUE
+ priorite_livraison: Optional[int] = Field(None, description="Priorité livraison (CT_PrioriteLivr)")
+ livraison_partielle: Optional[int] = Field(None, description="Livraison partielle (CT_LivrPartielle)")
+ delai_transport: Optional[int] = Field(None, description="Délai transport jours (CT_DelaiTransport)")
+ delai_appro: Optional[int] = Field(None, description="Délai appro jours (CT_DelaiAppro)")
+
+ # COMMENTAIRE
+ commentaire: Optional[str] = Field(None, description="Commentaire libre (CT_Commentaire)")
+
+ # ANALYTIQUE
+ section_analytique: Optional[str] = Field(None, description="Section analytique (CA_Num)")
+
+ # ORGANISATION / SURVEILLANCE
+ mode_reglement_code: Optional[int] = Field(None, description="Code mode règlement (MR_No)")
+ surveillance_active: Optional[bool] = Field(None, description="Surveillance financière (CT_Surveillance)")
+ coface: Optional[str] = Field(None, description="Code Coface 25 car. (CT_Coface)")
+ forme_juridique: Optional[str] = Field(None, description="Forme juridique SA, SARL (CT_SvFormeJuri)")
+ effectif: Optional[str] = Field(None, description="Nombre d'employés (CT_SvEffectif)")
+ sv_regularite: Optional[str] = Field(None, description="Régularité paiements (CT_SvRegul)")
+ sv_cotation: Optional[str] = Field(None, description="Cotation crédit (CT_SvCotation)")
+ sv_objet_maj: Optional[str] = Field(None, description="Objet dernière MAJ (CT_SvObjetMaj)")
+ sv_chiffre_affaires: Optional[float] = Field(None, description="Chiffre d'affaires (CT_SvCA)")
+ sv_resultat: Optional[float] = Field(None, description="Résultat financier (CT_SvResultat)")
+
+ # COMPTE GENERAL ET CATEGORIES
+ compte_general: Optional[str] = Field(None, description="Compte général principal (CG_NumPrinc)")
+ categorie_tarif: Optional[int] = Field(None, description="Catégorie tarifaire (N_CatTarif)")
+ categorie_compta: Optional[int] = Field(None, description="Catégorie comptable (N_CatCompta)")
+
+ # CONTACTS
+ contacts: Optional[List[Contact]] = Field(
+ default_factory=list,
+ description="Liste des contacts du tiers"
+ )
\ No newline at end of file
diff --git a/schemas/tiers/type_tiers.py b/schemas/tiers/type_tiers.py
new file mode 100644
index 0000000..a14b3e7
--- /dev/null
+++ b/schemas/tiers/type_tiers.py
@@ -0,0 +1,6 @@
+class TypeTiers(str, Enum):
+ """Types de tiers possibles"""
+ ALL = "all"
+ CLIENT = "client"
+ FOURNISSEUR = "fournisseur"
+ PROSPECT = "prospect"
\ No newline at end of file
diff --git a/services/email_service.py b/services/email_service.py
index 7bb7661..b234a5f 100644
--- a/services/email_service.py
+++ b/services/email_service.py
@@ -32,11 +32,11 @@ class AuthEmailService:
server.send_message(msg)
- logger.info(f"✅ Email envoyé: {subject} → {to}")
+ logger.info(f" Email envoyé: {subject} → {to}")
return True
except Exception as e:
- logger.error(f"❌ Erreur envoi email: {e}")
+ logger.error(f" Erreur envoi email: {e}")
return False
@staticmethod
@@ -91,7 +91,7 @@ class AuthEmailService:
- ⚠️ Ce lien expire dans 24 heures
+ Ce lien expire dans 24 heures
@@ -107,7 +107,7 @@ class AuthEmailService:
"""
return AuthEmailService._send_email(
- email, "🔐 Vérifiez votre adresse email - Sage Dataven", html_body
+ email, " Vérifiez votre adresse email - Sage Dataven", html_body
)
@staticmethod
@@ -146,7 +146,7 @@ class AuthEmailService:
Demande de réinitialisation
@@ -162,7 +162,7 @@ class AuthEmailService:
- ⚠️ Ce lien expire dans 1 heure
+ Ce lien expire dans 1 heure
@@ -178,7 +178,7 @@ class AuthEmailService:
"""
return AuthEmailService._send_email(
- email, "🔐 Réinitialisation de votre mot de passe - Sage Dataven", html_body
+ email, " Réinitialisation de votre mot de passe - Sage Dataven", html_body
)
@staticmethod
@@ -199,14 +199,14 @@ class AuthEmailService:
Votre mot de passe a été changé avec succès
Ce message confirme que le mot de passe de votre compte Sage Dataven a été modifié.
- ⚠️ Si vous n'êtes pas à l'origine de ce changement, contactez immédiatement notre support.
+ Si vous n'êtes pas à l'origine de ce changement, contactez immédiatement notre support.
|