Spaces:
Running
Running
| 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 |