style(api): improve email template readability and clean up code formatting

This commit is contained in:
Fanilo-Nantenaina 2025-12-18 10:59:40 +03:00
parent 282ffe4898
commit 5cb9015ab5

306
api.py
View file

@ -1050,8 +1050,6 @@ class MouvementStockResponse(BaseModel):
nb_lignes: int = Field(..., description="Nombre de lignes") nb_lignes: int = Field(..., description="Nombre de lignes")
templates_signature_email = { templates_signature_email = {
"demande_signature": { "demande_signature": {
"id": "demande_signature", "id": "demande_signature",
@ -1073,7 +1071,7 @@ templates_signature_email = {
<!-- Header --> <!-- Header -->
<tr> <tr>
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 8px 8px 0 0;"> <td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 600;"> <h1 style="color: #000; margin: 0; font-size: 24px; font-weight: 600;">
📝 Signature Électronique Requise 📝 Signature Électronique Requise
</h1> </h1>
</td> </td>
@ -1124,7 +1122,7 @@ templates_signature_email = {
<table width="100%" cellpadding="0" cellspacing="0"> <table width="100%" cellpadding="0" cellspacing="0">
<tr> <tr>
<td align="center" style="padding: 10px 0 30px;"> <td align="center" style="padding: 10px 0 30px;">
<a href="{{SIGNER_URL}}" style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 16px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);"> <a href="{{SIGNER_URL}}" style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #000; text-decoration: none; padding: 16px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);">
Signer le document Signer le document
</a> </a>
</td> </td>
@ -1179,10 +1177,9 @@ templates_signature_email = {
"DATE", "DATE",
"MONTANT_TTC", "MONTANT_TTC",
"SIGNER_URL", "SIGNER_URL",
"CONTACT_EMAIL" "CONTACT_EMAIL",
] ],
}, },
"signature_confirmee": { "signature_confirmee": {
"id": "signature_confirmee", "id": "signature_confirmee",
"nom": "Confirmation de Signature", "nom": "Confirmation de Signature",
@ -1290,10 +1287,9 @@ templates_signature_email = {
"NUMERO", "NUMERO",
"DATE_SIGNATURE", "DATE_SIGNATURE",
"TRANSACTION_ID", "TRANSACTION_ID",
"CONTACT_EMAIL" "CONTACT_EMAIL",
] ],
}, },
"relance_signature": { "relance_signature": {
"id": "relance_signature", "id": "relance_signature",
"nom": "Relance Signature en Attente", "nom": "Relance Signature en Attente",
@ -1393,22 +1389,22 @@ templates_signature_email = {
"NB_JOURS", "NB_JOURS",
"JOURS_RESTANTS", "JOURS_RESTANTS",
"SIGNER_URL", "SIGNER_URL",
"CONTACT_EMAIL" "CONTACT_EMAIL",
] ],
} },
} }
async def universign_envoyer_avec_email( async def universign_envoyer_avec_email(
doc_id: str, doc_id: str,
pdf_bytes: bytes, pdf_bytes: bytes,
email: str, email: str,
nom: str, nom: str,
doc_data: Dict, # Données du document (type, montant, date, etc.) doc_data: Dict, # Données du document (type, montant, date, etc.)
session: AsyncSession session: AsyncSession,
) -> Dict: ) -> Dict:
import requests import requests
try: try:
api_key = settings.universign_api_key api_key = settings.universign_api_key
api_url = settings.universign_api_url api_url = settings.universign_api_url
@ -1419,20 +1415,26 @@ async def universign_envoyer_avec_email(
auth=auth, auth=auth,
json={ json={
"name": f"{doc_data.get('type_label', 'Document')} {doc_id}", "name": f"{doc_data.get('type_label', 'Document')} {doc_id}",
"language": "fr" "language": "fr",
}, },
timeout=30, timeout=30,
) )
response.raise_for_status() response.raise_for_status()
transaction_id = response.json().get("id") transaction_id = response.json().get("id")
logger.info(f"✅ Transaction Universign créée: {transaction_id}") logger.info(f"✅ Transaction Universign créée: {transaction_id}")
files = {"file": (f"{doc_data.get('type_label', 'Document')}_{doc_id}.pdf", pdf_bytes, "application/pdf")} files = {
"file": (
f"{doc_data.get('type_label', 'Document')}_{doc_id}.pdf",
pdf_bytes,
"application/pdf",
)
}
response = requests.post(f"{api_url}/files", auth=auth, files=files, timeout=30) response = requests.post(f"{api_url}/files", auth=auth, files=files, timeout=30)
response.raise_for_status() response.raise_for_status()
file_id = response.json().get("id") file_id = response.json().get("id")
response = requests.post( response = requests.post(
f"{api_url}/transactions/{transaction_id}/documents", f"{api_url}/transactions/{transaction_id}/documents",
auth=auth, auth=auth,
@ -1450,7 +1452,7 @@ async def universign_envoyer_avec_email(
) )
response.raise_for_status() response.raise_for_status()
field_id = response.json().get("id") field_id = response.json().get("id")
response = requests.post( response = requests.post(
f"{api_url}/transactions/{transaction_id}/signatures", f"{api_url}/transactions/{transaction_id}/signatures",
auth=auth, auth=auth,
@ -1460,35 +1462,33 @@ async def universign_envoyer_avec_email(
response.raise_for_status() response.raise_for_status()
response = requests.post( response = requests.post(
f"{api_url}/transactions/{transaction_id}/start", f"{api_url}/transactions/{transaction_id}/start", auth=auth, timeout=30
auth=auth,
timeout=30
) )
response.raise_for_status() response.raise_for_status()
final_data = response.json() final_data = response.json()
signer_url = ( signer_url = (
final_data.get("actions", [{}])[0].get("url", "") final_data.get("actions", [{}])[0].get("url", "")
if final_data.get("actions") if final_data.get("actions")
else "" else ""
) )
if not signer_url: if not signer_url:
raise ValueError("URL de signature non retournée par Universign") raise ValueError("URL de signature non retournée par Universign")
logger.info(f"✅ Signature Universign démarrée: {transaction_id}") logger.info(f"✅ Signature Universign démarrée: {transaction_id}")
template = templates_signature_email["demande_signature"] template = templates_signature_email["demande_signature"]
# Préparer les variables # Préparer les variables
type_labels = { type_labels = {
0: "Devis", 0: "Devis",
10: "Commande", 10: "Commande",
30: "Bon de Livraison", 30: "Bon de Livraison",
60: "Facture", 60: "Facture",
50: "Avoir" 50: "Avoir",
} }
variables = { variables = {
"NOM_SIGNATAIRE": nom, "NOM_SIGNATAIRE": nom,
"TYPE_DOC": type_labels.get(doc_data.get("type_doc", 0), "Document"), "TYPE_DOC": type_labels.get(doc_data.get("type_doc", 0), "Document"),
@ -1496,17 +1496,17 @@ async def universign_envoyer_avec_email(
"DATE": doc_data.get("date", datetime.now().strftime("%d/%m/%Y")), "DATE": doc_data.get("date", datetime.now().strftime("%d/%m/%Y")),
"MONTANT_TTC": f"{doc_data.get('montant_ttc', 0):.2f}", "MONTANT_TTC": f"{doc_data.get('montant_ttc', 0):.2f}",
"SIGNER_URL": signer_url, "SIGNER_URL": signer_url,
"CONTACT_EMAIL": settings.smtp_from "CONTACT_EMAIL": settings.smtp_from,
} }
# Remplacer les variables dans le template # Remplacer les variables dans le template
sujet = template["sujet"] sujet = template["sujet"]
corps = template["corps_html"] corps = template["corps_html"]
for var, valeur in variables.items(): for var, valeur in variables.items():
sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur)) sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur))
corps = corps.replace(f"{{{{{var}}}}}", str(valeur)) corps = corps.replace(f"{{{{{var}}}}}", str(valeur))
# Créer log email # Créer log email
email_log = EmailLog( email_log = EmailLog(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@ -1519,30 +1519,27 @@ async def universign_envoyer_avec_email(
date_creation=datetime.now(), date_creation=datetime.now(),
nb_tentatives=0, nb_tentatives=0,
) )
session.add(email_log) session.add(email_log)
await session.flush() await session.flush()
# Enqueue l'email # Enqueue l'email
email_queue.enqueue(email_log.id) email_queue.enqueue(email_log.id)
logger.info(f"📧 Email de signature envoyé en file: {email}") logger.info(f"📧 Email de signature envoyé en file: {email}")
return { return {
"transaction_id": transaction_id, "transaction_id": transaction_id,
"signer_url": signer_url, "signer_url": signer_url,
"statut": "ENVOYE", "statut": "ENVOYE",
"email_log_id": email_log.id, "email_log_id": email_log.id,
"email_sent": True "email_sent": True,
} }
except Exception as e: except Exception as e:
logger.error(f"❌ Erreur Universign+Email: {e}") logger.error(f"❌ Erreur Universign+Email: {e}")
return { return {"error": str(e), "statut": "ERREUR", "email_sent": False}
"error": str(e),
"statut": "ERREUR",
"email_sent": False
}
async def universign_statut(transaction_id: str) -> Dict: async def universign_statut(transaction_id: str) -> Dict:
"""Récupération statut signature""" """Récupération statut signature"""
@ -2398,6 +2395,7 @@ async def commande_vers_facture(id: str, session: AsyncSession = Depends(get_ses
logger.error(f"Erreur transformation: {e}") logger.error(f"Erreur transformation: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
def normaliser_type_doc(type_doc: int) -> int: def normaliser_type_doc(type_doc: int) -> int:
TYPES_AUTORISES = {0, 10, 30, 50, 60} TYPES_AUTORISES = {0, 10, 30, 50, 60}
@ -2411,32 +2409,35 @@ def normaliser_type_doc(type_doc: int) -> int:
@app.post("/signature/universign/send", tags=["Signatures"]) @app.post("/signature/universign/send", tags=["Signatures"])
async def envoyer_signature_optimise( async def envoyer_signature_optimise(
demande: SignatureRequest, demande: SignatureRequest, session: AsyncSession = Depends(get_session)
session: AsyncSession = Depends(get_session)
): ):
try: try:
# Récupérer le document depuis Sage # Récupérer le document depuis Sage
doc = sage_client.lire_document(demande.doc_id, normaliser_type_doc(demande.type_doc)) doc = sage_client.lire_document(
demande.doc_id, normaliser_type_doc(demande.type_doc)
)
if not doc: if not doc:
raise HTTPException(404, f"Document {demande.doc_id} introuvable") raise HTTPException(404, f"Document {demande.doc_id} introuvable")
# Générer PDF # Générer PDF
pdf_bytes = email_queue._generate_pdf(demande.doc_id, normaliser_type_doc(demande.type_doc)) pdf_bytes = email_queue._generate_pdf(
demande.doc_id, normaliser_type_doc(demande.type_doc)
)
# Préparer les données du document pour l'email # Préparer les données du document pour l'email
doc_data = { doc_data = {
"type_doc": demande.type_doc, "type_doc": demande.type_doc,
"type_label": { "type_label": {
0: "Devis", 0: "Devis",
10: "Commande", 10: "Commande",
30: "Bon de Livraison", 30: "Bon de Livraison",
60: "Facture", 60: "Facture",
50: "Avoir" 50: "Avoir",
}.get(demande.type_doc, "Document"), }.get(demande.type_doc, "Document"),
"montant_ttc": doc.get("total_ttc", 0), "montant_ttc": doc.get("total_ttc", 0),
"date": doc.get("date", datetime.now().strftime("%d/%m/%Y")) "date": doc.get("date", datetime.now().strftime("%d/%m/%Y")),
} }
# Envoi Universign + Email automatique # Envoi Universign + Email automatique
resultat = await universign_envoyer_avec_email( resultat = await universign_envoyer_avec_email(
doc_id=demande.doc_id, doc_id=demande.doc_id,
@ -2444,12 +2445,12 @@ async def envoyer_signature_optimise(
email=demande.email_signataire, email=demande.email_signataire,
nom=demande.nom_signataire, nom=demande.nom_signataire,
doc_data=doc_data, doc_data=doc_data,
session=session session=session,
) )
if "error" in resultat: if "error" in resultat:
raise HTTPException(500, resultat["error"]) raise HTTPException(500, resultat["error"])
# Logger en DB # Logger en DB
signature_log = SignatureLog( signature_log = SignatureLog(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@ -2462,97 +2463,98 @@ async def envoyer_signature_optimise(
statut=StatutSignatureEnum.ENVOYE, statut=StatutSignatureEnum.ENVOYE,
date_envoi=datetime.now(), date_envoi=datetime.now(),
) )
session.add(signature_log) session.add(signature_log)
await session.commit() await session.commit()
# MAJ champ libre Sage # MAJ champ libre Sage
sage_client.mettre_a_jour_champ_libre( sage_client.mettre_a_jour_champ_libre(
demande.doc_id, demande.doc_id, demande.type_doc, "UniversignID", resultat["transaction_id"]
demande.type_doc,
"UniversignID",
resultat["transaction_id"]
) )
logger.info(f"✅ Signature envoyée: {demande.doc_id} (Email: {resultat['email_sent']})") logger.info(
f"✅ Signature envoyée: {demande.doc_id} (Email: {resultat['email_sent']})"
)
return { return {
"success": True, "success": True,
"transaction_id": resultat["transaction_id"], "transaction_id": resultat["transaction_id"],
"signer_url": resultat["signer_url"], "signer_url": resultat["signer_url"],
"email_sent": resultat["email_sent"], "email_sent": resultat["email_sent"],
"email_log_id": resultat.get("email_log_id"), "email_log_id": resultat.get("email_log_id"),
"message": f"Demande de signature envoyée à {demande.email_signataire}" "message": f"Demande de signature envoyée à {demande.email_signataire}",
} }
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error(f"❌ Erreur signature: {e}") logger.error(f"❌ Erreur signature: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/webhooks/universign", tags=["Signatures"]) @app.post("/webhooks/universign", tags=["Signatures"])
async def webhook_universign( async def webhook_universign(
request: Request, request: Request, session: AsyncSession = Depends(get_session)
session: AsyncSession = Depends(get_session)
): ):
try: try:
payload = await request.json() payload = await request.json()
event_type = payload.get("event") event_type = payload.get("event")
transaction_id = payload.get("transaction_id") transaction_id = payload.get("transaction_id")
if not transaction_id: if not transaction_id:
logger.warning("⚠️ Webhook sans transaction_id") logger.warning("⚠️ Webhook sans transaction_id")
return {"status": "ignored"} return {"status": "ignored"}
# Chercher la signature dans la DB # Chercher la signature dans la DB
query = select(SignatureLog).where( query = select(SignatureLog).where(
SignatureLog.transaction_id == transaction_id SignatureLog.transaction_id == transaction_id
) )
result = await session.execute(query) result = await session.execute(query)
signature_log = result.scalar_one_or_none() signature_log = result.scalar_one_or_none()
if not signature_log: if not signature_log:
logger.warning(f"⚠️ Transaction {transaction_id} introuvable en DB") logger.warning(f"⚠️ Transaction {transaction_id} introuvable en DB")
return {"status": "not_found"} return {"status": "not_found"}
# ============================================= # =============================================
# TRAITER L'EVENT SELON LE TYPE # TRAITER L'EVENT SELON LE TYPE
# ============================================= # =============================================
if event_type == "transaction.completed": if event_type == "transaction.completed":
# ✅ SIGNATURE RÉUSSIE # ✅ SIGNATURE RÉUSSIE
signature_log.statut = StatutSignatureEnum.SIGNE signature_log.statut = StatutSignatureEnum.SIGNE
signature_log.date_signature = datetime.now() signature_log.date_signature = datetime.now()
logger.info(f"✅ Signature confirmée: {signature_log.document_id}") logger.info(f"✅ Signature confirmée: {signature_log.document_id}")
# ENVOYER EMAIL DE CONFIRMATION # ENVOYER EMAIL DE CONFIRMATION
template = templates_signature_email["signature_confirmee"] template = templates_signature_email["signature_confirmee"]
type_labels = { type_labels = {
0: "Devis", 10: "Commande", 30: "Bon de Livraison", 0: "Devis",
60: "Facture", 50: "Avoir" 10: "Commande",
30: "Bon de Livraison",
60: "Facture",
50: "Avoir",
} }
variables = { variables = {
"NOM_SIGNATAIRE": signature_log.nom_signataire, "NOM_SIGNATAIRE": signature_log.nom_signataire,
"TYPE_DOC": type_labels.get(signature_log.type_document, "Document"), "TYPE_DOC": type_labels.get(signature_log.type_document, "Document"),
"NUMERO": signature_log.document_id, "NUMERO": signature_log.document_id,
"DATE_SIGNATURE": datetime.now().strftime("%d/%m/%Y à %H:%M"), "DATE_SIGNATURE": datetime.now().strftime("%d/%m/%Y à %H:%M"),
"TRANSACTION_ID": transaction_id, "TRANSACTION_ID": transaction_id,
"CONTACT_EMAIL": settings.smtp_from "CONTACT_EMAIL": settings.smtp_from,
} }
sujet = template["sujet"] sujet = template["sujet"]
corps = template["corps_html"] corps = template["corps_html"]
for var, valeur in variables.items(): for var, valeur in variables.items():
sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur)) sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur))
corps = corps.replace(f"{{{{{var}}}}}", str(valeur)) corps = corps.replace(f"{{{{{var}}}}}", str(valeur))
# Créer email de confirmation # Créer email de confirmation
email_log = EmailLog( email_log = EmailLog(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@ -2565,77 +2567,80 @@ async def webhook_universign(
date_creation=datetime.now(), date_creation=datetime.now(),
nb_tentatives=0, nb_tentatives=0,
) )
session.add(email_log) session.add(email_log)
email_queue.enqueue(email_log.id) email_queue.enqueue(email_log.id)
logger.info(f"📧 Email de confirmation envoyé: {signature_log.email_signataire}") logger.info(
f"📧 Email de confirmation envoyé: {signature_log.email_signataire}"
)
elif event_type == "transaction.refused": elif event_type == "transaction.refused":
# ❌ SIGNATURE REFUSÉE # ❌ SIGNATURE REFUSÉE
signature_log.statut = StatutSignatureEnum.REFUSE signature_log.statut = StatutSignatureEnum.REFUSE
logger.warning(f"❌ Signature refusée: {signature_log.document_id}") logger.warning(f"❌ Signature refusée: {signature_log.document_id}")
elif event_type == "transaction.expired": elif event_type == "transaction.expired":
# ⏰ TRANSACTION EXPIRÉE # ⏰ TRANSACTION EXPIRÉE
signature_log.statut = StatutSignatureEnum.EXPIRE signature_log.statut = StatutSignatureEnum.EXPIRE
logger.warning(f"⏰ Transaction expirée: {signature_log.document_id}") logger.warning(f"⏰ Transaction expirée: {signature_log.document_id}")
await session.commit() await session.commit()
return { return {
"status": "processed", "status": "processed",
"event": event_type, "event": event_type,
"transaction_id": transaction_id "transaction_id": transaction_id,
} }
except Exception as e: except Exception as e:
logger.error(f"❌ Erreur webhook Universign: {e}") logger.error(f"❌ Erreur webhook Universign: {e}")
return {"status": "error", "message": str(e)} return {"status": "error", "message": str(e)}
@app.get("/admin/signatures/relances-auto", tags=["Admin"]) @app.get("/admin/signatures/relances-auto", tags=["Admin"])
async def relancer_signatures_automatique( async def relancer_signatures_automatique(session: AsyncSession = Depends(get_session)):
session: AsyncSession = Depends(get_session)
):
try: try:
from datetime import timedelta from datetime import timedelta
# Chercher signatures en attente depuis > 7 jours # Chercher signatures en attente depuis > 7 jours
date_limite = datetime.now() - timedelta(days=7) date_limite = datetime.now() - timedelta(days=7)
query = select(SignatureLog).where( query = select(SignatureLog).where(
SignatureLog.statut.in_([ SignatureLog.statut.in_(
StatutSignatureEnum.EN_ATTENTE, [StatutSignatureEnum.EN_ATTENTE, StatutSignatureEnum.ENVOYE]
StatutSignatureEnum.ENVOYE ),
]),
SignatureLog.date_envoi < date_limite, SignatureLog.date_envoi < date_limite,
SignatureLog.nb_relances < 3 # Max 3 relances SignatureLog.nb_relances < 3, # Max 3 relances
) )
result = await session.execute(query) result = await session.execute(query)
signatures_a_relancer = result.scalars().all() signatures_a_relancer = result.scalars().all()
nb_relances = 0 nb_relances = 0
for signature in signatures_a_relancer: for signature in signatures_a_relancer:
try: try:
# Calculer jours écoulés # Calculer jours écoulés
nb_jours = (datetime.now() - signature.date_envoi).days nb_jours = (datetime.now() - signature.date_envoi).days
jours_restants = 30 - nb_jours # Lien expire après 30 jours jours_restants = 30 - nb_jours # Lien expire après 30 jours
if jours_restants <= 0: if jours_restants <= 0:
# Transaction expirée # Transaction expirée
signature.statut = StatutSignatureEnum.EXPIRE signature.statut = StatutSignatureEnum.EXPIRE
continue continue
# Préparer email de relance # Préparer email de relance
template = templates_signature_email["relance_signature"] template = templates_signature_email["relance_signature"]
type_labels = { type_labels = {
0: "Devis", 10: "Commande", 30: "Bon de Livraison", 0: "Devis",
60: "Facture", 50: "Avoir" 10: "Commande",
30: "Bon de Livraison",
60: "Facture",
50: "Avoir",
} }
variables = { variables = {
"NOM_SIGNATAIRE": signature.nom_signataire, "NOM_SIGNATAIRE": signature.nom_signataire,
"TYPE_DOC": type_labels.get(signature.type_document, "Document"), "TYPE_DOC": type_labels.get(signature.type_document, "Document"),
@ -2643,16 +2648,16 @@ async def relancer_signatures_automatique(
"NB_JOURS": str(nb_jours), "NB_JOURS": str(nb_jours),
"JOURS_RESTANTS": str(jours_restants), "JOURS_RESTANTS": str(jours_restants),
"SIGNER_URL": signature.signer_url, "SIGNER_URL": signature.signer_url,
"CONTACT_EMAIL": settings.smtp_from "CONTACT_EMAIL": settings.smtp_from,
} }
sujet = template["sujet"] sujet = template["sujet"]
corps = template["corps_html"] corps = template["corps_html"]
for var, valeur in variables.items(): for var, valeur in variables.items():
sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur)) sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur))
corps = corps.replace(f"{{{{{var}}}}}", str(valeur)) corps = corps.replace(f"{{{{{var}}}}}", str(valeur))
# Créer email de relance # Créer email de relance
email_log = EmailLog( email_log = EmailLog(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@ -2665,35 +2670,37 @@ async def relancer_signatures_automatique(
date_creation=datetime.now(), date_creation=datetime.now(),
nb_tentatives=0, nb_tentatives=0,
) )
session.add(email_log) session.add(email_log)
email_queue.enqueue(email_log.id) email_queue.enqueue(email_log.id)
# Incrémenter compteur de relances # Incrémenter compteur de relances
signature.est_relance = True signature.est_relance = True
signature.nb_relances = (signature.nb_relances or 0) + 1 signature.nb_relances = (signature.nb_relances or 0) + 1
nb_relances += 1 nb_relances += 1
logger.info(f"📧 Relance envoyée: {signature.document_id} ({signature.nb_relances}/3)") logger.info(
f"📧 Relance envoyée: {signature.document_id} ({signature.nb_relances}/3)"
)
except Exception as e: except Exception as e:
logger.error(f"❌ Erreur relance signature {signature.id}: {e}") logger.error(f"❌ Erreur relance signature {signature.id}: {e}")
continue continue
await session.commit() await session.commit()
return { return {
"success": True, "success": True,
"signatures_verifiees": len(signatures_a_relancer), "signatures_verifiees": len(signatures_a_relancer),
"relances_envoyees": nb_relances, "relances_envoyees": nb_relances,
"message": f"{nb_relances} email(s) de relance envoyé(s)" "message": f"{nb_relances} email(s) de relance envoyé(s)",
} }
except Exception as e: except Exception as e:
logger.error(f"❌ Erreur relances automatiques: {e}") logger.error(f"❌ Erreur relances automatiques: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/signature/universign/status", tags=["Signatures"]) @app.get("/signature/universign/status", tags=["Signatures"])
async def statut_signature(docId: str = Query(...)): async def statut_signature(docId: str = Query(...)):
@ -4166,9 +4173,7 @@ async def commande_vers_livraison(
): ):
try: try:
# Étape 1: Vérifier que la commande existe # Étape 1: Vérifier que la commande existe
commande_existante = sage_client.lire_document( commande_existante = sage_client.lire_document(id, TypeDocumentSQL.BON_COMMANDE)
id, TypeDocumentSQL.BON_COMMANDE
)
if not commande_existante: if not commande_existante:
raise HTTPException(404, f"Commande {id} introuvable") raise HTTPException(404, f"Commande {id} introuvable")
@ -4549,6 +4554,7 @@ async def statistiques_utilisateurs(session: AsyncSession = Depends(get_session)
logger.error(f"❌ Erreur stats utilisateurs: {e}") logger.error(f"❌ Erreur stats utilisateurs: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/modeles", tags=["PDF Sage-Like"]) @app.get("/modeles", tags=["PDF Sage-Like"])
async def get_modeles_disponibles(): async def get_modeles_disponibles():
"""Liste tous les modèles PDF disponibles""" """Liste tous les modèles PDF disponibles"""
@ -4564,8 +4570,10 @@ async def get_modeles_disponibles():
async def get_document_pdf( async def get_document_pdf(
numero: str, numero: str,
type_doc: int = Query(..., description="0=devis, 60=facture, etc."), type_doc: int = Query(..., description="0=devis, 60=facture, etc."),
modele: str = Query(None, description="Nom du modèle (ex: 'Facture client logo.bgc')"), modele: str = Query(
download: bool = Query(False, description="Télécharger au lieu d'afficher") None, description="Nom du modèle (ex: 'Facture client logo.bgc')"
),
download: bool = Query(False, description="Télécharger au lieu d'afficher"),
): ):
try: try:
# Récupérer le PDF (en bytes) # Récupérer le PDF (en bytes)
@ -4573,28 +4581,26 @@ async def get_document_pdf(
numero=numero, numero=numero,
type_doc=type_doc, type_doc=type_doc,
modele=modele, modele=modele,
base64_encode=False # On veut les bytes bruts base64_encode=False, # On veut les bytes bruts
) )
# Retourner le PDF # Retourner le PDF
from fastapi.responses import Response from fastapi.responses import Response
disposition = "attachment" if download else "inline" disposition = "attachment" if download else "inline"
filename = f"{numero}.pdf" filename = f"{numero}.pdf"
return Response( return Response(
content=pdf_bytes, content=pdf_bytes,
media_type="application/pdf", media_type="application/pdf",
headers={ headers={"Content-Disposition": f'{disposition}; filename="{filename}"'},
"Content-Disposition": f'{disposition}; filename="{filename}"'
}
) )
except Exception as e: except Exception as e:
logger.error(f"Erreur génération PDF: {e}") logger.error(f"Erreur génération PDF: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# LANCEMENT # LANCEMENT
# ===================================================== # =====================================================