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:

-

🔑 Réinitialisation de mot de passe

+

Réinitialisation de mot de passe

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:

-

✅ Mot de passe modifié

+

Mot de passe modifié

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.