Vortex-Flux / src /DocumentGen /InvoiceRepayment.py
jaimalklyde's picture
Claude Haiku 5.5
style(ui): emojis remplaces par des balises tactiques sobres
495d60d
Raw History Blame Contribute Delete
13.5 kB
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, HRFlowable
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER
from io import BytesIO
from datetime import datetime
from xml.sax.saxutils import escape
def generer_recu(data):
"""
Génère un reçu de remboursement au format PDF
Args:
data: dict contenant toutes les informations nécessaires
Returns:
BytesIO: Buffer contenant le PDF (ou None en cas d'erreur)
"""
# === 1. LOGS ET VALIDATION ===
# Pas de numero_recu/trans_id/montants en clair ici : ces logs finissent
# dans les logs du Space Hugging Face, visibles par quiconque y a acces.
print("=== DÉBUT GÉNÉRATION REÇU ===")
# Vérification des données minimales
if not data.get('numero_recu'):
print("ERREUR : numero_recu manquant")
return None
if not data.get('client'):
print("ERREUR : données client manquantes")
return None
if not data.get('paiement'):
print("ERREUR : données paiement manquantes")
return None
# === 2. GÉNÉRATION PDF ===
try:
# Création du buffer
buffer = BytesIO()
# Configuration du document
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
rightMargin=2*cm,
leftMargin=2*cm,
topMargin=2*cm,
bottomMargin=2*cm
)
# Container pour les éléments
elements = []
# Styles
styles = getSampleStyleSheet()
# Style personnalisé pour le titre
title_style = ParagraphStyle(
'CustomTitle',
parent=styles['Heading1'],
fontSize=20,
textColor=colors.HexColor('#58a6ff'),
spaceAfter=12,
alignment=TA_CENTER,
fontName='Helvetica-Bold'
)
# Style pour les sous-titres
subtitle_style = ParagraphStyle(
'CustomSubtitle',
parent=styles['Heading2'],
fontSize=14,
textColor=colors.HexColor('#8b949e'),
spaceAfter=10,
spaceBefore=15,
fontName='Helvetica-Bold'
)
# Style pour le texte normal
normal_style = ParagraphStyle(
'CustomNormal',
parent=styles['Normal'],
fontSize=10,
textColor=colors.black,
spaceAfter=6
)
# Style spécifique pour forcer le retour à la ligne dans les cellules du tableau
table_val_style = ParagraphStyle(
'TableVal',
parent=styles['Normal'],
fontName='Helvetica',
fontSize=10,
textColor=colors.black,
leading=12
)
# Style pour les montants importants
amount_style = ParagraphStyle(
'AmountStyle',
parent=styles['Normal'],
fontSize=16,
textColor=colors.HexColor('#54bd4b'),
alignment=TA_CENTER,
fontName='Helvetica-Bold',
spaceAfter=10
)
# === EN-TÊTE ===
elements.append(Paragraph("REÇU DE REMBOURSEMENT", title_style))
elements.append(Spacer(1, 0.3*cm))
# Numéro de reçu et date
date_str = data['date_paiement'].strftime('%d/%m/%Y') if hasattr(data['date_paiement'], 'strftime') else str(data['date_paiement'])
header_data = [
[f"N° {data['numero_recu']}", f"Date: {date_str}"]
]
header_table = Table(header_data, colWidths=[8*cm, 8*cm])
header_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (-1, -1), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 11),
('TEXTCOLOR', (0, 0), (0, 0), colors.HexColor('#58a6ff')),
('TEXTCOLOR', (1, 0), (1, 0), colors.HexColor('#8b949e')),
('ALIGN', (0, 0), (0, 0), 'LEFT'),
('ALIGN', (1, 0), (1, 0), 'RIGHT'),
]))
elements.append(header_table)
elements.append(Spacer(1, 0.5*cm))
# Ligne de séparation
elements.append(HRFlowable(width="100%", thickness=2, color=colors.HexColor('#58a6ff')))
elements.append(Spacer(1, 0.5*cm))
# === INFORMATIONS CLIENT ===
elements.append(Paragraph("INFORMATIONS CLIENT", subtitle_style))
client = data['client']
# On enveloppe l'adresse dans un objet Paragraph pour forcer le retour à la ligne
# Paragraph interprete un mini-balisage XML : une adresse saisie par le client
# contenant <B>, </B>, <BR>... ferait echouer toute la generation du recu.
adresse_formatee = Paragraph(escape(str(client.get('Adresse') or 'N/A')), table_val_style)
client_data = [
['Nom Complet:', client.get('Nom_Complet', 'N/A')],
['N° Client:', client.get('ID_Client', 'N/A')],
['Téléphone:', str(client.get('Telephone', 'N/A'))],
['Adresse:', adresse_formatee]
]
client_table = Table(client_data, colWidths=[4*cm, 12*cm])
client_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#8b949e')),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('RIGHTPADDING', (0, 0), (-1, -1), 0),
]))
elements.append(client_table)
elements.append(Spacer(1, 0.5*cm))
# === INFORMATIONS PRÊT ===
elements.append(Paragraph("INFORMATIONS PRÊT", subtitle_style))
loan = data['loan']
loan_data = [
['N° Prêt:', loan.get('ID_Pret', 'N/A')],
['Type de Prêt:', loan.get('Type_Pret', 'N/A')],
['Capital Initial:', f"{loan.get('Montant_Capital', 0):,.0f} XOF"],
['Montant Total Dû:', f"{loan.get('Montant_Total', 0):,.0f} XOF"]
]
loan_table = Table(loan_data, colWidths=[4*cm, 12*cm])
loan_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#8b949e')),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('RIGHTPADDING', (0, 0), (-1, -1), 0),
]))
elements.append(loan_table)
elements.append(Spacer(1, 0.5*cm))
# === DÉTAILS DU PAIEMENT ===
elements.append(Paragraph("DÉTAILS DU PAIEMENT", subtitle_style))
paiement = data['paiement']
# Gestion sûre de la date d'échéance
date_echeance = paiement.get('date_echeance_prevue', 'N/A')
if hasattr(date_echeance, 'strftime'):
date_echeance_str = date_echeance.strftime('%d/%m/%Y')
else:
date_echeance_str = str(date_echeance)
payment_data = [
['Échéance:', str(paiement.get('numero_echeance', 'N/A'))],
['Date Échéance Prévue:', date_echeance_str],
['Retard (jours):', str(paiement.get('jours_retard', 0))],
['Statut:', paiement.get('statut_paiement', 'N/A')]
]
payment_table = Table(payment_data, colWidths=[4*cm, 12*cm])
payment_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#8b949e')),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('RIGHTPADDING', (0, 0), (-1, -1), 0),
]))
elements.append(payment_table)
elements.append(Spacer(1, 0.5*cm))
# === DÉCOMPOSITION DU MONTANT ===
elements.append(Paragraph("DÉCOMPOSITION DU MONTANT", subtitle_style))
decomp_data = [
['Principal remboursé', f"{paiement.get('montant_principal', 0):,.0f} XOF"],
['Intérêts payés', f"{paiement.get('montant_interets', 0):,.0f} XOF"],
['Pénalités de retard', f"{paiement.get('penalites_retard', 0):,.0f} XOF"]
]
decomp_table = Table(decomp_data, colWidths=[10*cm, 6*cm])
decomp_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#8b949e')),
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('LINEBELOW', (0, 0), (-1, -2), 0.5, colors.grey),
('LEFTPADDING', (0, 0), (-1, -1), 5),
('RIGHTPADDING', (0, 0), (-1, -1), 5),
]))
elements.append(decomp_table)
elements.append(Spacer(1, 0.3*cm))
# === MONTANT TOTAL VERSÉ ===
montant_verse = paiement.get('montant_verse',
paiement.get('montant_principal', 0) +
paiement.get('montant_interets', 0) +
paiement.get('penalites_retard', 0))
total_data = [
['MONTANT TOTAL VERSÉ', f"{montant_verse:,.0f} XOF"]
]
total_table = Table(total_data, colWidths=[10*cm, 6*cm])
total_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (-1, -1), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('TEXTCOLOR', (0, 0), (0, -1), colors.black),
('TEXTCOLOR', (1, 0), (1, -1), colors.HexColor('#54bd4b')),
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#e8f5e9')),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('LINEABOVE', (0, 0), (-1, 0), 2, colors.HexColor('#54bd4b')),
('LEFTPADDING', (0, 0), (-1, -1), 5),
('RIGHTPADDING', (0, 0), (-1, -1), 5),
('TOPPADDING', (0, 0), (-1, -1), 8),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
]))
elements.append(total_table)
elements.append(Spacer(1, 0.5*cm))
# === SOLDES ===
elements.append(Paragraph("SOLDES", subtitle_style))
soldes_data = [
['Solde avant paiement', f"{paiement.get('solde_avant', 0):,.0f} XOF"],
['Solde après paiement', f"{paiement.get('solde_apres', 0):,.0f} XOF"]
]
soldes_table = Table(soldes_data, colWidths=[10*cm, 6*cm])
soldes_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#8b949e')),
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('LEFTPADDING', (0, 0), (-1, -1), 5),
('RIGHTPADDING', (0, 0), (-1, -1), 5),
]))
elements.append(soldes_table)
elements.append(Spacer(1, 0.5*cm))
# === MOYEN DE PAIEMENT ===
elements.append(Paragraph("MOYEN DE PAIEMENT", subtitle_style))
moyen_data = [
['Mode de paiement:', data.get('moyen', 'N/A')],
['Référence externe:', data.get('reference', 'N/A')],
['Transaction ID:', data.get('trans_id', 'N/A')]
]
moyen_table = Table(moyen_data, colWidths=[4*cm, 12*cm])
moyen_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#8b949e')),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('LEFTPADDING', (0, 0), (-1, -1), 0),
('RIGHTPADDING', (0, 0), (-1, -1), 0),
]))
elements.append(moyen_table)
elements.append(Spacer(1, 1*cm))
# === FOOTER ===
elements.append(HRFlowable(width="100%", thickness=1, color=colors.grey))
elements.append(Spacer(1, 0.3*cm))
footer_text = f"Reçu généré le {datetime.now().strftime('%d/%m/%Y à %H:%M')}"
footer_style = ParagraphStyle(
'Footer',
parent=styles['Normal'],
fontSize=8,
textColor=colors.grey,
alignment=TA_CENTER
)
elements.append(Paragraph(footer_text, footer_style))
# Construction du PDF
doc.build(elements)
# Retour du buffer
buffer.seek(0)
print("=== PDF GÉNÉRÉ AVEC SUCCÈS ===")
return buffer
except Exception as e:
print(f"[ECHEC] ERREUR LORS DE LA GÉNÉRATION : {e}")
import traceback
traceback.print_exc()
return None