feat(tiers): add tiers schemas and API endpoints
This commit is contained in:
parent
8859152379
commit
1d78c6b46b
14 changed files with 295 additions and 145 deletions
|
|
@ -11,7 +11,7 @@ RUN pip install --no-cache-dir --upgrade pip \
|
||||||
# Copier le reste du projet
|
# Copier le reste du projet
|
||||||
COPY . .
|
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
|
RUN mkdir -p /app/data && chmod 777 /app/data
|
||||||
|
|
||||||
# Exposer le port
|
# Exposer le port
|
||||||
|
|
|
||||||
56
api.py
56
api.py
|
|
@ -30,6 +30,8 @@ from database import (
|
||||||
from email_queue import email_queue
|
from email_queue import email_queue
|
||||||
from sage_client import sage_client
|
from sage_client import sage_client
|
||||||
|
|
||||||
|
from schemas import TiersDetails, TypeTiers
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
|
@ -1582,7 +1584,7 @@ class ClientCreateRequest(BaseModel):
|
||||||
def to_sage_dict(self) -> dict:
|
def to_sage_dict(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Convertit le modèle en dictionnaire compatible avec creer_client()
|
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
|
stat01 = self.statistique01 or self.secteur
|
||||||
|
|
||||||
|
|
@ -1701,8 +1703,8 @@ class ClientCreateRequest(BaseModel):
|
||||||
class ClientUpdateRequest(BaseModel):
|
class ClientUpdateRequest(BaseModel):
|
||||||
"""
|
"""
|
||||||
Modèle pour modification d'un client existant
|
Modèle pour modification d'un client existant
|
||||||
✅ TOUS les champs de ClientCreateRequest sont modifiables
|
TOUS les champs de ClientCreateRequest sont modifiables
|
||||||
✅ TOUS optionnels (seuls les champs fournis sont modifiés)
|
TOUS optionnels (seuls les champs fournis sont modifiés)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
intitule: Optional[str] = Field(None, max_length=69)
|
intitule: Optional[str] = Field(None, max_length=69)
|
||||||
|
|
@ -2842,7 +2844,7 @@ templates_signature_email = {
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 15px;">
|
<td style="padding: 15px;">
|
||||||
<p style="color: #2c5282; font-size: 13px; line-height: 1.5; margin: 0;">
|
<p style="color: #2c5282; font-size: 13px; line-height: 1.5; margin: 0;">
|
||||||
🔐 <strong>Signature certifiée :</strong> Ce document a été signé avec une signature
|
<strong>Signature certifiée :</strong> Ce document a été signé avec une signature
|
||||||
électronique qualifiée, ayant la même valeur juridique qu'une signature manuscrite
|
électronique qualifiée, ayant la même valeur juridique qu'une signature manuscrite
|
||||||
conformément au règlement eIDAS.
|
conformément au règlement eIDAS.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -3004,7 +3006,7 @@ async def universign_envoyer(
|
||||||
api_url = settings.universign_api_url
|
api_url = settings.universign_api_url
|
||||||
auth = (api_key, "")
|
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')})")
|
logger.info(f"📌 Document: {doc_id} ({doc_data.get('type_label')})")
|
||||||
|
|
||||||
if not pdf_bytes or len(pdf_bytes) == 0:
|
if not pdf_bytes or len(pdf_bytes) == 0:
|
||||||
|
|
@ -3089,7 +3091,7 @@ async def universign_envoyer(
|
||||||
field_id = response.json().get("id")
|
field_id = response.json().get("id")
|
||||||
logger.info(f"Champ créé: {field_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(
|
response = requests.post(
|
||||||
f"{api_url}/transactions/{transaction_id}/signatures", # /signatures pas /signers
|
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("URL récupérée")
|
||||||
|
|
||||||
logger.info("📧 Préparation email")
|
logger.info(" Préparation email")
|
||||||
|
|
||||||
template = templates_signature_email["demande_signature"]
|
template = templates_signature_email["demande_signature"]
|
||||||
|
|
||||||
|
|
@ -4137,7 +4139,7 @@ async def webhook_universign(
|
||||||
email_queue.enqueue(email_log.id)
|
email_queue.enqueue(email_log.id)
|
||||||
|
|
||||||
logger.info(
|
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":
|
elif event_type == "transaction.refused":
|
||||||
|
|
@ -4238,7 +4240,7 @@ async def relancer_signatures_automatique(session: AsyncSession = Depends(get_se
|
||||||
nb_relances += 1
|
nb_relances += 1
|
||||||
|
|
||||||
logger.info(
|
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:
|
except Exception as e:
|
||||||
|
|
@ -6083,6 +6085,42 @@ async def definir_contact_defaut(numero: str, contact_numero: int):
|
||||||
raise HTTPException(500, str(e))
|
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__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"api:app",
|
"api:app",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from sqlalchemy import select
|
||||||
from database import get_session, User
|
from database import get_session, User
|
||||||
from security.auth import decode_token
|
from security.auth import decode_token
|
||||||
from typing import Optional
|
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()
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
|
@ -14,14 +14,6 @@ async def get_current_user(
|
||||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
) -> User:
|
) -> 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
|
token = credentials.credentials
|
||||||
|
|
||||||
# Décoder le token
|
# 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.",
|
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():
|
if user.locked_until and user.locked_until > datetime.now():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
|
|
||||||
|
|
@ -27,20 +27,20 @@ async def create_admin():
|
||||||
"""Crée un utilisateur admin"""
|
"""Crée un utilisateur admin"""
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("🔐 Création d'un compte administrateur")
|
print(" Création d'un compte administrateur")
|
||||||
print("=" * 60 + "\n")
|
print("=" * 60 + "\n")
|
||||||
|
|
||||||
# Saisie des informations
|
# Saisie des informations
|
||||||
email = input("Email de l'admin: ").strip().lower()
|
email = input("Email de l'admin: ").strip().lower()
|
||||||
if not email or "@" not in email:
|
if not email or "@" not in email:
|
||||||
print("❌ Email invalide")
|
print(" Email invalide")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
prenom = input("Prénom: ").strip()
|
prenom = input("Prénom: ").strip()
|
||||||
nom = input("Nom: ").strip()
|
nom = input("Nom: ").strip()
|
||||||
|
|
||||||
if not prenom or not nom:
|
if not prenom or not nom:
|
||||||
print("❌ Prénom et nom requis")
|
print(" Prénom et nom requis")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Mot de passe avec validation
|
# Mot de passe avec validation
|
||||||
|
|
@ -55,9 +55,9 @@ async def create_admin():
|
||||||
if password == confirm:
|
if password == confirm:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print("❌ Les mots de passe ne correspondent pas\n")
|
print(" Les mots de passe ne correspondent pas\n")
|
||||||
else:
|
else:
|
||||||
print(f"❌ {error_msg}\n")
|
print(f" {error_msg}\n")
|
||||||
|
|
||||||
# Vérifier si l'email existe déjà
|
# Vérifier si l'email existe déjà
|
||||||
async with async_session_factory() as session:
|
async with async_session_factory() as session:
|
||||||
|
|
@ -67,7 +67,7 @@ async def create_admin():
|
||||||
existing = result.scalar_one_or_none()
|
existing = result.scalar_one_or_none()
|
||||||
|
|
||||||
if existing:
|
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
|
return False
|
||||||
|
|
||||||
# Créer l'admin
|
# Créer l'admin
|
||||||
|
|
@ -86,12 +86,12 @@ async def create_admin():
|
||||||
session.add(admin)
|
session.add(admin)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
print("\n✅ Administrateur créé avec succès!")
|
print("\n Administrateur créé avec succès!")
|
||||||
print(f"📧 Email: {email}")
|
print(f" Email: {email}")
|
||||||
print(f"👤 Nom: {prenom} {nom}")
|
print(f" Nom: {prenom} {nom}")
|
||||||
print(f"🔑 Rôle: admin")
|
print(f" Rôle: admin")
|
||||||
print(f"🆔 ID: {admin.id}")
|
print(f" ID: {admin.id}")
|
||||||
print("\n💡 Vous pouvez maintenant vous connecter à l'API\n")
|
print("\n Vous pouvez maintenant vous connecter à l'API\n")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -101,9 +101,9 @@ if __name__ == "__main__":
|
||||||
result = asyncio.run(create_admin())
|
result = asyncio.run(create_admin())
|
||||||
sys.exit(0 if result else 1)
|
sys.exit(0 if result else 1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n\n❌ Création annulée")
|
print("\n\n Création annulée")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n❌ Erreur: {e}")
|
print(f"\n Erreur: {e}")
|
||||||
logger.exception("Détails:")
|
logger.exception("Détails:")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
||||||
|
|
@ -27,17 +27,17 @@ async_session_factory = async_sessionmaker(
|
||||||
async def init_db():
|
async def init_db():
|
||||||
"""
|
"""
|
||||||
Crée toutes les tables dans la base de données
|
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:
|
try:
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
logger.info("✅ Base de données initialisée avec succès")
|
logger.info(" Base de données initialisée avec succès")
|
||||||
logger.info(f"📍 Fichier DB: {DATABASE_URL}")
|
logger.info(f" Fichier DB: {DATABASE_URL}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Erreur initialisation DB: {e}")
|
logger.error(f" Erreur initialisation DB: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -53,4 +53,4 @@ async def get_session() -> AsyncSession:
|
||||||
async def close_db():
|
async def close_db():
|
||||||
"""Ferme proprement toutes les connexions"""
|
"""Ferme proprement toutes les connexions"""
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
logger.info("🔌 Connexions DB fermées")
|
logger.info(" Connexions DB fermées")
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,6 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class EmailQueue:
|
class EmailQueue:
|
||||||
"""
|
|
||||||
Queue d'emails avec workers threadés et retry automatique
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.queue = queue.Queue()
|
self.queue = queue.Queue()
|
||||||
|
|
@ -45,23 +42,23 @@ class EmailQueue:
|
||||||
worker.start()
|
worker.start()
|
||||||
self.workers.append(worker)
|
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):
|
def stop(self):
|
||||||
"""Arrête les workers proprement"""
|
"""Arrête les workers proprement"""
|
||||||
logger.info("🛑 Arrêt de la queue email...")
|
logger.info(" Arrêt de la queue email...")
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.queue.join()
|
self.queue.join()
|
||||||
logger.info("✅ Queue email arrêtée proprement")
|
logger.info(" Queue email arrêtée proprement")
|
||||||
except:
|
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):
|
def enqueue(self, email_log_id: str):
|
||||||
"""Ajoute un email dans la queue"""
|
"""Ajoute un email dans la queue"""
|
||||||
self.queue.put(email_log_id)
|
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):
|
def _worker(self):
|
||||||
"""Worker qui traite les emails dans un thread"""
|
"""Worker qui traite les emails dans un thread"""
|
||||||
|
|
@ -80,7 +77,7 @@ class EmailQueue:
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Erreur worker: {e}", exc_info=True)
|
logger.error(f" Erreur worker: {e}", exc_info=True)
|
||||||
try:
|
try:
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
except:
|
except:
|
||||||
|
|
@ -94,7 +91,7 @@ class EmailQueue:
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
if not self.session_factory:
|
if not self.session_factory:
|
||||||
logger.error("❌ session_factory non configuré")
|
logger.error(" session_factory non configuré")
|
||||||
return
|
return
|
||||||
|
|
||||||
async with self.session_factory() as session:
|
async with self.session_factory() as session:
|
||||||
|
|
@ -104,7 +101,7 @@ class EmailQueue:
|
||||||
email_log = result.scalar_one_or_none()
|
email_log = result.scalar_one_or_none()
|
||||||
|
|
||||||
if not email_log:
|
if not email_log:
|
||||||
logger.error(f"❌ Email log {email_log_id} introuvable")
|
logger.error(f" Email log {email_log_id} introuvable")
|
||||||
return
|
return
|
||||||
|
|
||||||
email_log.statut = StatutEmail.EN_COURS
|
email_log.statut = StatutEmail.EN_COURS
|
||||||
|
|
@ -117,7 +114,7 @@ class EmailQueue:
|
||||||
email_log.statut = StatutEmail.ENVOYE
|
email_log.statut = StatutEmail.ENVOYE
|
||||||
email_log.date_envoi = datetime.now()
|
email_log.date_envoi = datetime.now()
|
||||||
email_log.derniere_erreur = None
|
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:
|
except Exception as e:
|
||||||
email_log.statut = StatutEmail.ERREUR
|
email_log.statut = StatutEmail.ERREUR
|
||||||
|
|
@ -134,10 +131,10 @@ class EmailQueue:
|
||||||
timer.start()
|
timer.start()
|
||||||
|
|
||||||
logger.warning(
|
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:
|
else:
|
||||||
logger.error(f"❌ Échec définitif: {email_log.destinataire} - {e}")
|
logger.error(f" Échec définitif: {email_log.destinataire} - {e}")
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
@ -176,20 +173,20 @@ class EmailQueue:
|
||||||
logger.info(f"📎 PDF attaché: {doc_id}.pdf")
|
logger.info(f"📎 PDF attaché: {doc_id}.pdf")
|
||||||
|
|
||||||
except Exception as e:
|
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)
|
await asyncio.to_thread(self._send_smtp, msg)
|
||||||
|
|
||||||
def _generate_pdf(self, doc_id: str, type_doc: int) -> bytes:
|
def _generate_pdf(self, doc_id: str, type_doc: int) -> bytes:
|
||||||
|
|
||||||
if not self.sage_client:
|
if not self.sage_client:
|
||||||
logger.error("❌ sage_client non configuré")
|
logger.error(" sage_client non configuré")
|
||||||
raise Exception("sage_client non disponible")
|
raise Exception("sage_client non disponible")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
doc = self.sage_client.lire_document(doc_id, type_doc)
|
doc = self.sage_client.lire_document(doc_id, type_doc)
|
||||||
except Exception as e:
|
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")
|
raise Exception(f"Document {doc_id} inaccessible")
|
||||||
|
|
||||||
if not doc:
|
if not doc:
|
||||||
|
|
@ -284,11 +281,10 @@ class EmailQueue:
|
||||||
pdf.save()
|
pdf.save()
|
||||||
buffer.seek(0)
|
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()
|
return buffer.read()
|
||||||
|
|
||||||
def _send_smtp(self, msg):
|
def _send_smtp(self, msg):
|
||||||
"""Envoi SMTP bloquant (appelé depuis asyncio.to_thread)"""
|
|
||||||
try:
|
try:
|
||||||
with smtplib.SMTP(
|
with smtplib.SMTP(
|
||||||
settings.smtp_host, settings.smtp_port, timeout=30
|
settings.smtp_host, settings.smtp_port, timeout=30
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from pathlib import Path
|
||||||
# Ajouter le répertoire parent au path pour les imports
|
# Ajouter le répertoire parent au path pour les imports
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
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
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
@ -32,8 +32,8 @@ async def main():
|
||||||
# Créer les tables
|
# Créer les tables
|
||||||
await init_db()
|
await init_db()
|
||||||
|
|
||||||
print("\n✅ Base de données créée avec succès!")
|
print("\n Base de données créée avec succès!")
|
||||||
print(f"📍 Fichier: sage_dataven.db")
|
print(f" Fichier: sage_dataven.db")
|
||||||
|
|
||||||
print("\n📊 Tables créées:")
|
print("\n📊 Tables créées:")
|
||||||
print(" ├─ email_logs (Journalisation emails)")
|
print(" ├─ email_logs (Journalisation emails)")
|
||||||
|
|
@ -53,7 +53,7 @@ async def main():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
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:")
|
logger.exception("Détails de l'erreur:")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,12 +95,7 @@ async def log_login_attempt(
|
||||||
async def check_rate_limit(
|
async def check_rate_limit(
|
||||||
session: AsyncSession, email: str, ip: str
|
session: AsyncSession, email: str, ip: str
|
||||||
) -> tuple[bool, 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)
|
time_window = datetime.now() - timedelta(minutes=15)
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
|
|
@ -126,13 +121,7 @@ async def register(
|
||||||
request: Request,
|
request: Request,
|
||||||
session: AsyncSession = Depends(get_session),
|
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))
|
result = await session.execute(select(User).where(User.email == data.email))
|
||||||
existing_user = result.scalar_one_or_none()
|
existing_user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
@ -170,7 +159,7 @@ async def register(
|
||||||
if not email_sent:
|
if not email_sent:
|
||||||
logger.warning(f"Échec envoi email vérification pour {data.email}")
|
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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|
@ -183,7 +172,7 @@ async def register(
|
||||||
@router.get("/verify-email")
|
@router.get("/verify-email")
|
||||||
async def verify_email_get(token: str, session: AsyncSession = Depends(get_session)):
|
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
|
Utilisé quand l'utilisateur clique sur le lien dans l'email
|
||||||
"""
|
"""
|
||||||
result = await session.execute(select(User).where(User.verification_token == token))
|
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
|
user.verification_token_expires = None
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
logger.info(f"✅ Email vérifié: {user.email}")
|
logger.info(f" Email vérifié: {user.email}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"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,
|
"email": user.email,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,7 +210,7 @@ async def verify_email_post(
|
||||||
data: VerifyEmailRequest, session: AsyncSession = Depends(get_session)
|
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
|
Utilisé pour les appels programmatiques depuis le frontend
|
||||||
"""
|
"""
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
|
|
@ -246,7 +235,7 @@ async def verify_email_post(
|
||||||
user.verification_token_expires = None
|
user.verification_token_expires = None
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
logger.info(f"✅ Email vérifié: {user.email}")
|
logger.info(f" Email vérifié: {user.email}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|
@ -260,9 +249,6 @@ async def resend_verification(
|
||||||
request: Request,
|
request: Request,
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
🔄 Renvoyer l'email de vérification
|
|
||||||
"""
|
|
||||||
result = await session.execute(select(User).where(User.email == data.email.lower()))
|
result = await session.execute(select(User).where(User.email == data.email.lower()))
|
||||||
user = result.scalar_one_or_none()
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
@ -292,11 +278,6 @@ async def resend_verification(
|
||||||
async def login(
|
async def login(
|
||||||
data: LoginRequest, request: Request, session: AsyncSession = Depends(get_session)
|
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"
|
ip = request.client.host if request.client else "unknown"
|
||||||
user_agent = request.headers.get("user-agent", "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)
|
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(
|
return TokenResponse(
|
||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
|
|
@ -401,9 +382,7 @@ async def login(
|
||||||
async def refresh_access_token(
|
async def refresh_access_token(
|
||||||
data: RefreshTokenRequest, session: AsyncSession = Depends(get_session)
|
data: RefreshTokenRequest, session: AsyncSession = Depends(get_session)
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
🔄 Renouvellement du access_token via refresh_token
|
|
||||||
"""
|
|
||||||
payload = decode_token(data.refresh_token)
|
payload = decode_token(data.refresh_token)
|
||||||
if not payload or payload.get("type") != "refresh":
|
if not payload or payload.get("type") != "refresh":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -446,7 +425,7 @@ async def refresh_access_token(
|
||||||
{"sub": user.id, "email": user.email, "role": user.role}
|
{"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(
|
return TokenResponse(
|
||||||
access_token=new_access_token,
|
access_token=new_access_token,
|
||||||
|
|
@ -461,9 +440,7 @@ async def forgot_password(
|
||||||
request: Request,
|
request: Request,
|
||||||
session: AsyncSession = Depends(get_session),
|
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()))
|
result = await session.execute(select(User).where(User.email == data.email.lower()))
|
||||||
user = result.scalar_one_or_none()
|
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)
|
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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|
@ -497,9 +474,7 @@ async def forgot_password(
|
||||||
async def reset_password(
|
async def reset_password(
|
||||||
data: ResetPasswordRequest, session: AsyncSession = Depends(get_session)
|
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))
|
result = await session.execute(select(User).where(User.reset_token == data.token))
|
||||||
user = result.scalar_one_or_none()
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
@ -528,7 +503,7 @@ async def reset_password(
|
||||||
|
|
||||||
AuthEmailService.send_password_changed_notification(user.email)
|
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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|
@ -542,9 +517,7 @@ async def logout(
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
user: User = Depends(get_current_user),
|
user: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
🚪 Déconnexion (révocation du refresh token)
|
|
||||||
"""
|
|
||||||
token_hash = hash_token(data.refresh_token)
|
token_hash = hash_token(data.refresh_token)
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
|
|
@ -566,9 +539,7 @@ async def logout(
|
||||||
|
|
||||||
@router.get("/me")
|
@router.get("/me")
|
||||||
async def get_current_user_info(user: User = Depends(get_current_user)):
|
async def get_current_user_info(user: User = Depends(get_current_user)):
|
||||||
"""
|
|
||||||
👤 Récupération du profil utilisateur
|
|
||||||
"""
|
|
||||||
return {
|
return {
|
||||||
"id": user.id,
|
"id": user.id,
|
||||||
"email": user.email,
|
"email": user.email,
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class SageGatewayClient:
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
if attempt == retries - 1:
|
if attempt == retries - 1:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"❌ Échec après {retries} tentatives sur {endpoint}: {e}"
|
f" Échec après {retries} tentatives sur {endpoint}: {e}"
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
time.sleep(2**attempt)
|
time.sleep(2**attempt)
|
||||||
|
|
@ -54,7 +54,7 @@ class SageGatewayClient:
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
if attempt == retries - 1:
|
if attempt == retries - 1:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"❌ Échec GET après {retries} tentatives sur {endpoint}: {e}"
|
f" Échec GET après {retries} tentatives sur {endpoint}: {e}"
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
time.sleep(2**attempt)
|
time.sleep(2**attempt)
|
||||||
|
|
@ -108,7 +108,7 @@ class SageGatewayClient:
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json().get("data", {})
|
return r.json().get("data", {})
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logger.error(f"❌ Erreur changement statut: {e}")
|
logger.error(f" Erreur changement statut: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def lire_document(self, numero: str, type_doc: int) -> Optional[Dict]:
|
def lire_document(self, numero: str, type_doc: int) -> Optional[Dict]:
|
||||||
|
|
@ -134,7 +134,7 @@ class SageGatewayClient:
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json().get("data", {})
|
return r.json().get("data", {})
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logger.error(f"❌ Erreur transformation: {e}")
|
logger.error(f" Erreur transformation: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def mettre_a_jour_champ_libre(
|
def mettre_a_jour_champ_libre(
|
||||||
|
|
@ -352,7 +352,7 @@ class SageGatewayClient:
|
||||||
|
|
||||||
pdf_bytes = base64.b64decode(pdf_base64)
|
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
|
return pdf_bytes
|
||||||
|
|
||||||
|
|
@ -364,11 +364,11 @@ class SageGatewayClient:
|
||||||
)
|
)
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
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)}")
|
raise RuntimeError(f"Erreur de communication avec la gateway: {str(e)}")
|
||||||
|
|
||||||
except Exception as 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
|
raise
|
||||||
|
|
||||||
def creer_article(self, article_data: Dict) -> Dict:
|
def creer_article(self, article_data: Dict) -> Dict:
|
||||||
|
|
@ -420,7 +420,7 @@ class SageGatewayClient:
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json().get("data", {})
|
return r.json().get("data", {})
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
logger.error(f"❌ Erreur listage modèles: {e}")
|
logger.error(f" Erreur listage modèles: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def generer_pdf_document(
|
def generer_pdf_document(
|
||||||
|
|
@ -446,7 +446,7 @@ class SageGatewayClient:
|
||||||
return r.content
|
return r.content
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
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
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -476,16 +476,6 @@ class SageGatewayClient:
|
||||||
|
|
||||||
|
|
||||||
def supprimer_contact(self, numero: str, contact_numero: int) -> Dict:
|
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", {
|
return self._post("/sage/contacts/delete", {
|
||||||
"numero": numero,
|
"numero": numero,
|
||||||
"contact_numero": contact_numero
|
"contact_numero": contact_numero
|
||||||
|
|
@ -493,19 +483,22 @@ class SageGatewayClient:
|
||||||
|
|
||||||
|
|
||||||
def definir_contact_defaut(self, numero: str, contact_numero: int) -> Dict:
|
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", {
|
return self._post("/sage/contacts/set-default", {
|
||||||
"numero": numero,
|
"numero": numero,
|
||||||
"contact_numero": contact_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()
|
sage_client = SageGatewayClient()
|
||||||
|
|
|
||||||
9
schemas/__init__.py
Normal file
9
schemas/__init__.py
Normal file
|
|
@ -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",
|
||||||
|
]
|
||||||
41
schemas/tiers/contact.py
Normal file
41
schemas/tiers/contact.py
Normal file
|
|
@ -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
|
||||||
104
schemas/tiers/tiers.py
Normal file
104
schemas/tiers/tiers.py
Normal file
|
|
@ -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"
|
||||||
|
)
|
||||||
6
schemas/tiers/type_tiers.py
Normal file
6
schemas/tiers/type_tiers.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
class TypeTiers(str, Enum):
|
||||||
|
"""Types de tiers possibles"""
|
||||||
|
ALL = "all"
|
||||||
|
CLIENT = "client"
|
||||||
|
FOURNISSEUR = "fournisseur"
|
||||||
|
PROSPECT = "prospect"
|
||||||
|
|
@ -32,11 +32,11 @@ class AuthEmailService:
|
||||||
|
|
||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
|
|
||||||
logger.info(f"✅ Email envoyé: {subject} → {to}")
|
logger.info(f" Email envoyé: {subject} → {to}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Erreur envoi email: {e}")
|
logger.error(f" Erreur envoi email: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -91,7 +91,7 @@ class AuthEmailService:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin-top: 30px; color: #ef4444;">
|
<p style="margin-top: 30px; color: #ef4444;">
|
||||||
⚠️ Ce lien expire dans <strong>24 heures</strong>
|
Ce lien expire dans <strong>24 heures</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin-top: 30px; font-size: 14px; color: #6b7280;">
|
<p style="margin-top: 30px; font-size: 14px; color: #6b7280;">
|
||||||
|
|
@ -107,7 +107,7 @@ class AuthEmailService:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return AuthEmailService._send_email(
|
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
|
@staticmethod
|
||||||
|
|
@ -146,7 +146,7 @@ class AuthEmailService:
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>🔑 Réinitialisation de mot de passe</h1>
|
<h1> Réinitialisation de mot de passe</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h2>Demande de réinitialisation</h2>
|
<h2>Demande de réinitialisation</h2>
|
||||||
|
|
@ -162,7 +162,7 @@ class AuthEmailService:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin-top: 30px; color: #ef4444;">
|
<p style="margin-top: 30px; color: #ef4444;">
|
||||||
⚠️ Ce lien expire dans <strong>1 heure</strong>
|
Ce lien expire dans <strong>1 heure</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin-top: 30px; font-size: 14px; color: #6b7280;">
|
<p style="margin-top: 30px; font-size: 14px; color: #6b7280;">
|
||||||
|
|
@ -178,7 +178,7 @@ class AuthEmailService:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return AuthEmailService._send_email(
|
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
|
@staticmethod
|
||||||
|
|
@ -199,14 +199,14 @@ class AuthEmailService:
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>✅ Mot de passe modifié</h1>
|
<h1> Mot de passe modifié</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h2>Votre mot de passe a été changé avec succès</h2>
|
<h2>Votre mot de passe a été changé avec succès</h2>
|
||||||
<p>Ce message confirme que le mot de passe de votre compte Sage Dataven a été modifié.</p>
|
<p>Ce message confirme que le mot de passe de votre compte Sage Dataven a été modifié.</p>
|
||||||
|
|
||||||
<p style="margin-top: 30px; padding: 15px; background: #FEF3C7; border-left: 4px solid #F59E0B; border-radius: 4px;">
|
<p style="margin-top: 30px; padding: 15px; background: #FEF3C7; border-left: 4px solid #F59E0B; border-radius: 4px;">
|
||||||
⚠️ 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.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
|
|
@ -218,5 +218,5 @@ class AuthEmailService:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return AuthEmailService._send_email(
|
return AuthEmailService._send_email(
|
||||||
email, "✅ Votre mot de passe a été modifié - Sage Dataven", html_body
|
email, " Votre mot de passe a été modifié - Sage Dataven", html_body
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue