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, TA_LEFT, TA_RIGHT from io import BytesIO from datetime import datetime 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 besoin de try ici) === print("=== DÉBUT GÉNÉRATION REÇU ===") print(f"Données reçues : {data.keys()}") print(f"Numéro reçu : {data.get('numero_recu')}") print(f"Transaction ID : {data.get('trans_id')}") # 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 (Protégé par try/except) === 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 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'] 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:', client.get('Adresse', 'N/A')] ] 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')} | Document officiel" footer_style = ParagraphStyle( 'Footer', parent=styles['Normal'], fontSize=8, textColor=colors.grey, alignment=TA_CENTER ) elements.append(Paragraph(footer_text, footer_style)) # === SIGNATURE === elements.append(Spacer(1, 1*cm)) signature_data = [ ['Signature du Prêteur:', ''], ['', ''], ['', '_________________________'] ] signature_table = Table(signature_data, colWidths=[8*cm, 8*cm]) signature_table.setStyle(TableStyle([ ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), ('FONTSIZE', (0, 0), (-1, -1), 10), ('VALIGN', (0, 0), (-1, -1), 'TOP'), ('ALIGN', (1, 0), (1, -1), 'CENTER'), ])) elements.append(signature_table) # 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"❌ ERREUR LORS DE LA GÉNÉRATION : {e}") import traceback traceback.print_exc() return None