File size: 25,537 Bytes
3d04910
6f397f7
3307ddd
0c7b15d
3307ddd
 
 
5b0dd39
3307ddd
5b0dd39
3307ddd
3d04910
3307ddd
 
 
3d04910
3307ddd
 
0c7b15d
 
 
 
 
 
3d04910
 
 
 
 
 
 
 
3307ddd
 
3d04910
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de5e25f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db7f3f2
de5e25f
 
 
db7f3f2
de5e25f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f6c2b9
 
db7f3f2
de5e25f
db7f3f2
de5e25f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3307ddd
3d04910
3307ddd
3d04910
db7f3f2
3307ddd
3d04910
 
de5e25f
 
3307ddd
3d04910
 
 
3307ddd
3d04910
3307ddd
3d04910
 
495d60d
3d04910
 
3307ddd
3d04910
 
3307ddd
 
3d04910
 
3307ddd
3d04910
 
 
 
 
3307ddd
 
 
 
 
3d04910
 
3307ddd
3d04910
 
 
 
 
 
 
 
 
3307ddd
 
 
 
3d04910
3307ddd
 
 
 
3d04910
 
 
 
3307ddd
 
3d04910
3307ddd
495d60d
3d04910
 
 
 
 
 
3307ddd
 
3d04910
 
 
 
 
 
3307ddd
 
 
 
 
 
3d04910
 
cb2d349
3d04910
 
cb2d349
 
3d04910
 
3307ddd
 
3d04910
 
 
 
 
 
3307ddd
 
 
 
3d04910
 
 
 
 
3307ddd
 
 
3d04910
 
 
3307ddd
 
3d04910
 
 
 
 
3307ddd
 
3d04910
 
 
 
 
3307ddd
 
3d04910
 
 
 
3307ddd
3d04910
3307ddd
3d04910
 
 
3307ddd
 
de5e25f
3307ddd
3d04910
de5e25f
 
 
db7f3f2
de5e25f
 
db7f3f2
 
 
de5e25f
 
 
 
 
 
 
 
 
 
3307ddd
 
 
 
 
3d04910
3307ddd
 
 
3d04910
6122be7
 
 
 
 
3307ddd
3d04910
 
de5e25f
 
3307ddd
3d04910
 
 
3307ddd
 
3d04910
495d60d
3d04910
 
3307ddd
3d04910
 
 
3307ddd
3d04910
 
 
 
 
 
 
 
3307ddd
 
 
 
3d04910
 
 
 
 
 
3307ddd
3d04910
 
 
 
495d60d
3d04910
6122be7
 
 
 
 
3307ddd
 
3d04910
 
 
 
3307ddd
3d04910
 
 
3307ddd
3d04910
 
 
 
3307ddd
3d04910
 
 
 
3307ddd
3d04910
 
 
3307ddd
3d04910
de5e25f
3307ddd
3d04910
 
3307ddd
6122be7
 
 
 
 
 
 
 
 
 
 
3307ddd
de5e25f
 
 
 
3307ddd
 
 
 
 
3d04910
3307ddd
 
 
3d04910
db7f3f2
3307ddd
3d04910
 
de5e25f
 
3307ddd
3d04910
 
 
3307ddd
 
3d04910
 
 
 
495d60d
3d04910
 
3307ddd
3d04910
 
 
 
 
 
 
 
 
3307ddd
 
 
 
3d04910
 
 
 
 
3307ddd
 
 
 
 
3d04910
 
 
495d60d
3d04910
 
 
 
 
 
3307ddd
 
3d04910
 
 
 
3307ddd
3d04910
 
 
 
3307ddd
3d04910
 
 
 
 
3307ddd
 
de5e25f
 
 
 
db7f3f2
de5e25f
 
db7f3f2
 
 
de5e25f
 
 
 
 
 
 
 
 
 
3307ddd
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
#Ce fichier est utiliser pour generer les docs sur les contrats de dette client et garant.

from io import BytesIO
import pandas as pd
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
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY


def format_currency(amount):
    return f"{int(amount):,}".replace(",", " ")


def get_common_styles():
    styles = getSampleStyleSheet()
    if 'TitleCenter' not in styles:
        styles.add(ParagraphStyle(name='TitleCenter', parent=styles['Heading1'], alignment=TA_CENTER, fontSize=16, spaceAfter=20))
    if 'NormalJustify' not in styles:
        styles.add(ParagraphStyle(name='NormalJustify', parent=styles['Normal'], alignment=TA_JUSTIFY, fontSize=11, leading=14))
    if 'BoldLabel' not in styles:
        styles.add(ParagraphStyle(name='BoldLabel', parent=styles['Normal'], fontName='Helvetica-Bold', fontSize=11))
    if 'OffreTag' not in styles:
        styles.add(ParagraphStyle(
            name='OffreTag', parent=styles['Normal'],
            fontName='Helvetica-Bold', fontSize=9,
            textColor=colors.HexColor('#444444'),
            spaceAfter=4, spaceBefore=4,
            alignment=TA_LEFT
        ))
    return styles


def _offre_table(offre, styles):
    """
    Retourne un bloc Table affichant l'offre commerciale selectionnee,
    a inserer dans le preambule des contrats.
    """
    offre_val = offre if offre else "N/A"
    data = [["OFFRE COMMERCIALE", offre_val]]
    t = Table(data, colWidths=[7*cm, 8*cm])
    t.setStyle(TableStyle([
        ('GRID',        (0, 0), (-1, -1), 0.5, colors.HexColor('#cccccc')),
        ('BACKGROUND',  (0, 0), (0, 0),   colors.HexColor('#f0f0f0')),
        ('FONTNAME',    (0, 0), (0, 0),   'Helvetica-Bold'),
        ('FONTNAME',    (1, 0), (1, 0),   'Helvetica-Bold'),
        ('TEXTCOLOR',   (1, 0), (1, 0),   colors.HexColor('#1a2a4a')),
        ('FONTSIZE',    (0, 0), (-1, -1), 10),
        ('PADDING',     (0, 0), (-1, -1), 6),
    ]))
    return t


# ============================================================================
# SIGNATURE ELECTRONIQUE (bloc place en fin de document)
# ============================================================================
# Chaque generateur accepte `signature=None`. Sans signature : bloc manuscrit
# historique. Avec signature (dict) : bloc de signature electronique. Les PDF sont
# generes avec invariant=1 (aucun horodatage ni identifiant aleatoire) : memes
# donnees = memes octets, ce qui permet de recalculer l'empreinte d'un contrat.
#
# Cles attendues dans `signature` : id_signature, nom, role, date_utc (JJ/MM/AAAA HH:MM:SS),
# ip, id_masque, hash_document, scelle, url_verification.

def _ligne_fait_a(signature):
    if signature and signature.get("date_utc"):
        return f"FAIT A DAKAR, LE : {str(signature['date_utc'])[:10]}"
    return "FAIT A DAKAR, LE : __________________"


def _table_parties_signature(libelle_preteur, nom_preteur, libelle_signataire, nom_signataire, preteur_signe=False):
    data = [
        [libelle_preteur, libelle_signataire],
        [nom_preteur, nom_signataire],
        ["Signature electronique ci-dessous" if preteur_signe else "", "Signature electronique ci-dessous"],
    ]
    t = Table(data, colWidths=[8*cm, 8*cm])
    t.setStyle(TableStyle([('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold')]))
    return t


def _bloc_signature_electronique(signature, styles):
    """Flowables du bloc de signature electronique (fin de document)."""
    from xml.sax.saxutils import escape

    petit = ParagraphStyle(name='SigPetit', parent=styles['Normal'], fontSize=8, leading=10)
    mono = ParagraphStyle(name='SigMono', parent=styles['Normal'], fontName='Courier', fontSize=7, leading=9)

    def p(texte, style=petit):
        return Paragraph(escape(str(texte)), style)

    lignes = [
        ["SIGNATURE ELECTRONIQUE", ""],
        ["Signataire", p(f"{signature.get('nom', '')} ({signature.get('role', '')})")],
        ["Mention", p("« Lu et approuve » - acceptation par case a cocher")],
        ["Date et heure (UTC)", p(signature.get("date_utc", ""))],
        ["Adresse IP", p(signature.get("ip") or "N/A")],
        # Absente des signatures anterieures : leurs PDF restent identiques octet pour octet.
        *([["Localisation (GPS)", p(signature["localisation"])]] if signature.get("localisation") else []),
        ["Verification d'identite", p(signature.get("verification") or (
            f"4 derniers caracteres de la piece d'identite ({signature.get('id_masque', '')}) "
            "et code a usage unique envoye par email : valides"))],
        ["Identifiant de signature", p(signature.get("id_signature", ""))],
        ["Empreinte SHA-256 du document", p(signature.get("hash_document", ""), mono)],
        ["Sceau d'authenticite (HMAC)", p(signature.get("scelle", ""), mono)],
        ["Verification", p(signature.get("url_verification", ""))],
    ]
    t = Table(lignes, colWidths=[5*cm, 11*cm])
    t.setStyle(TableStyle([
        ('GRID',       (0, 0), (-1, -1), 0.5, colors.grey),
        ('SPAN',       (0, 0), (-1, 0)),
        ('BACKGROUND', (0, 0), (-1, 0),  colors.HexColor('#1a2a4a')),
        ('TEXTCOLOR',  (0, 0), (-1, 0),  colors.white),
        ('FONTNAME',   (0, 0), (-1, 0),  'Helvetica-Bold'),
        ('BACKGROUND', (0, 1), (0, -1),  colors.whitesmoke),
        ('FONTNAME',   (0, 1), (0, -1),  'Helvetica-Bold'),
        ('FONTSIZE',   (0, 0), (-1, -1), 8),
        ('VALIGN',     (0, 0), (-1, -1), 'TOP'),
        ('PADDING',    (0, 0), (-1, -1), 4),
    ]))
    return [t]


# ============================================================================
# 1. CONTRAT DE PRET
# ============================================================================

def generer_contrat_pret(loan_data, client_info, df_amortissement, signature=None, signature_preteur=None):
    buffer = BytesIO()
    doc    = SimpleDocTemplate(buffer, pagesize=A4,
                               rightMargin=2*cm, leftMargin=2*cm,
                               topMargin=2*cm, bottomMargin=2*cm,
                               invariant=1)
    styles = get_common_styles()
    story  = []

    offre = loan_data.get('Offre', '')

    # En-tete
    story.append(Paragraph(f"N° {loan_data['ID_Pret']}", styles['Normal']))
    story.append(Paragraph("CONTRAT DE PRET A INTERETS", styles['TitleCenter']))

    # [OK] Bloc offre commerciale juste apres le titre
    story.append(Spacer(1, 6))
    story.append(_offre_table(offre, styles))
    story.append(Spacer(1, 10))

    story.append(Paragraph("Entre les soussignes:", styles['Normal']))
    story.append(Spacer(1, 10))

    # I. LE PRETEUR
    story.append(Paragraph("<b>I. LE PRETEUR</b>", styles['Heading3']))
    lender_text = """
    <b>Nom complet :</b> MOUSSIPI YA NDEME KLYDE JAIMAL<br/>
    <b>Type de piece d'identite :</b> PASSEPORT<br/>
    <b>Numero de la piece d'identite :</b> AA287935<br/>
    <b>Pays de residence :</b> FRANCE<br/>
    Ci-apres designe « le Preteur »,<br/>
    D'une part,
    """
    story.append(Paragraph(lender_text, styles['Normal']))
    story.append(Spacer(1, 10))

    # II. LE BENEFICIAIRE
    story.append(Paragraph("<b>II. LE BENEFICIAIRE (EMPRUNTEUR)</b>", styles['Heading3']))
    borrower_text = f"""
    <b>Nom complet :</b> {client_info['Nom_Complet']}<br/>
    <b>Date de naissance :</b> {client_info.get('Date_Naissance', 'N/A')}<br/>
    <b>Statut professionnel :</b> {client_info['Statut_Pro']}<br/>
    <b>Adresse :</b> {client_info.get('Adresse', 'N/A')}<br/>
    <b>Ville de residence :</b> {client_info['Ville']}<br/>
    <b>Numero de telephone :</b> {client_info.get('Telephone', 'N/A')}<br/>
    <b>Type de piece d'identite :</b> {client_info.get('Type_Piece_Identite', 'N/A')}<br/>
    <b>Numero de la piece d'identite :</b> {client_info.get('ID_Officiel', 'N/A')}<br/>
    Ci-apres designe « l'Emprunteur »,<br/>
    D'autre part,
    """
    story.append(Paragraph(borrower_text, styles['Normal']))
    story.append(Spacer(1, 10))
    story.append(Paragraph("Il a ete convenu et arrete ce qui suit :", styles['Normal']))
    story.append(Spacer(1, 15))

    # ARTICLES
    story.append(Paragraph("<b>ARTICLE 1 - OBJET DU CONTRAT</b>", styles['Heading4']))
    story.append(Paragraph(
        "Le present contrat a pour objet l'octroi par le Preteur a l'Emprunteur d'un pret d'argent "
        "a interets, conformement aux dispositions du Code des Obligations Civiles et Commerciales "
        "du Senegal (COCC).", styles['NormalJustify']))
    story.append(Spacer(1, 10))

    story.append(Paragraph("<b>ARTICLE 2 - CARACTERISTIQUES DU PRET</b>", styles['Heading4']))
    data_carac = [
        ["Offre commerciale",         offre if offre else "N/A"],   # [OK] Offre dans le tableau
        ["Montant du capital",         f"{format_currency(loan_data['Montant_Capital'])} XOF"],
        ["Motif du pret",              loan_data.get('Motif', 'Personnel')],
        ["Taux hebdomadaire",          f"{loan_data['Taux_Hebdo']} %"],
        ["Duree",                      f"{loan_data['Duree_Semaines']} semaines"],
        ["Date de deblocage",          loan_data['Date_Deblocage']],
        ["Date d'echeance finale",     loan_data['Date_Fin']],
    ]
    t_carac = Table(data_carac, colWidths=[7*cm, 8*cm])
    t_carac.setStyle(TableStyle([
        ('GRID',       (0, 0), (-1, -1), 0.5, colors.grey),
        ('BACKGROUND', (0, 0), (0, -1),  colors.whitesmoke),
        ('BACKGROUND', (0, 0), (-1, 0),  colors.HexColor('#e8eef4')),
        ('FONTNAME',   (0, 0), (-1, 0),  'Helvetica-Bold'),
    ]))
    story.append(t_carac)
    story.append(Spacer(1, 10))

    story.append(Paragraph("<b>ARTICLE 3 - TABLEAU D'AMORTISSEMENT</b>", styles['Heading4']))
    data_amort = [["Date", "Versement", "Solde Restant"]]
    for _, row in df_amortissement.iterrows():
        versement = int(row['Versement'])    if pd.notna(row['Versement'])    else 0
        solde     = int(row['Solde_Restant']) if pd.notna(row['Solde_Restant']) else 0
        data_amort.append([
            row['Date'],
            f"{versement:,}".replace(",", " "),
            f"{solde:,}".replace(",", " ")
        ])
    if len(data_amort) > 20:
        data_amort = data_amort[:20]
        data_amort.append(["...", "...", "..."])
    t_amort = Table(data_amort, colWidths=[4*cm, 5*cm, 5*cm])
    t_amort.setStyle(TableStyle([
        ('GRID',        (0, 0), (-1, -1), 0.5, colors.grey),
        ('BACKGROUND',  (0, 0), (-1, 0),  colors.navy),
        ('TEXTCOLOR',   (0, 0), (-1, 0),  colors.white),
        ('FONTNAME',    (0, 0), (-1, 0),  'Helvetica-Bold'),
    ]))
    story.append(t_amort)
    story.append(Spacer(1, 10))

    story.append(Paragraph("<b>ARTICLE 4 - OBLIGATION DE REMBOURSEMENT</b>", styles['Heading4']))
    story.append(Paragraph(
        "L'Emprunteur s'engage de maniere ferme, irrevocable et inconditionnelle a rembourser "
        "l'integralite des sommes dues, meme en cas de perte d'emploi, difficultes financieres, "
        "maladie ou tout autre evenement personnel. AUCUNE EXCUSE OU CIRCONSTANCE NE POURRA "
        "JUSTIFIER UN NON-REMBOURSEMENT.", styles['NormalJustify']))
    story.append(Spacer(1, 10))

    story.append(Paragraph("<b>ARTICLE 5 - RETARD DE REMBOURSEMENT</b>", styles['Heading4']))
    story.append(Paragraph("Tout retard de paiement entraine immediatement, sans mise en demeure prealable :", styles['Normal']))
    story.append(Paragraph("Des penalites de retard fixees a 5% du montant de l'echeance impayee par semaine de retard.", styles['Normal'], bulletText="•"))
    story.append(Paragraph("La possibilite pour le Preteur d'exiger le paiement immediat de la totalite du solde restant du.", styles['Normal'], bulletText="•"))
    story.append(Spacer(1, 10))

    story.append(Paragraph("<b>ARTICLE 6 - DECHEANCE DU TERME</b>", styles['Heading4']))
    story.append(Paragraph(
        "Le Preteur pourra prononcer la decheance du terme et exiger le remboursement immediat "
        "de la totalite de la dette en cas de : non-paiement d'une seule echeance, fausse "
        "declaration de revenus ou d'identite, refus de cooperation de l'Emprunteur.", styles['NormalJustify']))
    story.append(Spacer(1, 10))

    story.append(Paragraph("<b>ARTICLE 7 - DROIT DE RETRACTATION</b>", styles['Heading4']))
    story.append(Paragraph(
        "L'Emprunteur dispose d'un delai de retractation de sept (7) jours calendaires a compter "
        "de la date de deblocage effectif des fonds. La retractation doit etre notifiee par ecrit. "
        "L'Emprunteur devra restituer l'integralite des fonds recus.", styles['NormalJustify']))
    story.append(Spacer(1, 10))

    story.append(Paragraph("<b>ARTICLE 8 - DROIT APPLICABLE ET JURIDICTION COMPETENTE</b>", styles['Heading4']))
    story.append(Paragraph(
        "Le present contrat est regi par le droit Senegalais. Tout litige releve de la competence "
        "exclusive des tribunaux de Dakar.", styles['NormalJustify']))
    story.append(Spacer(1, 10))

    story.append(Paragraph("<b>ARTICLE 9 - ACCEPTATION</b>", styles['Heading4']))
    story.append(Paragraph(
        "L'Emprunteur reconnait avoir lu, compris et accepte toutes les clauses du present contrat "
        "sans contrainte ni reserve.", styles['NormalJustify']))
    story.append(Spacer(1, 20))

    story.append(Paragraph(_ligne_fait_a(signature), styles['Normal']))
    story.append(Spacer(1, 20))

    if signature:
        story.append(_table_parties_signature(
            "LE PRETEUR", "MOUSSIPI YA NDEME KLYDE JAIMAL",
            "LE BENEFICIAIRE", f"{client_info['Nom_Complet']}", preteur_signe=bool(signature_preteur)))
        story.append(Spacer(1, 10))
        story.extend(_bloc_signature_electronique(signature, styles))
        if signature_preteur:
            story.append(Spacer(1, 10))
            story.extend(_bloc_signature_electronique(signature_preteur, styles))
    else:
        data_sign = [
            ["LE PRETEUR",                         "LE BENEFICIAIRE"],
            ["MOUSSIPI YA NDEME KLYDE JAIMAL",     f"{client_info['Nom_Complet']}"],
            ["",                                   "Signature precedee de la mention manuscrite :"],
            ["Signature :",                        "« Lu et approuve »"],
            ["\n\n\n",                             "\n\n\n"]
        ]
        t_sign = Table(data_sign, colWidths=[8*cm, 8*cm])
        story.append(t_sign)

    doc.build(story)
    buffer.seek(0)
    return buffer


# ============================================================================
# 2. RECONNAISSANCE DE DETTE
# ============================================================================

def generer_reconnaissance_dette(loan_data, client_info, signature=None, montant_en_lettres_auto=False):
    """`montant_en_lettres_auto` : remplit le montant en lettres sous le montant en chiffres
    (snapshots version >= 2 ; les anciens restent identiques octet pour octet).
    La mention « Bon pour... » n'est jamais remplie par le systeme : elle est saisie par le
    signataire lui-meme sur la page de signature (signature["mention_bon_pour"])."""
    buffer = BytesIO()
    doc    = SimpleDocTemplate(buffer, pagesize=A4,
                               rightMargin=2*cm, leftMargin=2*cm,
                               topMargin=2*cm, bottomMargin=2*cm,
                               invariant=1)
    styles = get_common_styles()
    story  = []

    offre = loan_data.get('Offre', '')

    story.append(Paragraph("LETTRE DE RECONNAISSANCE DE DETTE", styles['TitleCenter']))

    # [OK] Offre sous le titre
    story.append(Spacer(1, 6))
    story.append(_offre_table(offre, styles))
    story.append(Spacer(1, 15))

    story.append(Paragraph("Je soussigne(e) :", styles['Normal']))

    info_debtor = f"""
    <b>Nom et prenoms :</b> {client_info['Nom_Complet']}<br/>
    <b>Date de naissance :</b> {client_info.get('Date_Naissance', '................')}<br/>
    <b>Statut professionnel :</b> {client_info['Statut_Pro']}<br/>
    <b>Adresse :</b> {client_info.get('Adresse', '................')}<br/>
    <b>Ville de residence :</b> {client_info['Ville']}<br/>
    <b>Numero de telephone :</b> {client_info.get('Telephone', '................')}<br/>
    <b>Type de piece d'identite :</b> {client_info.get('Type_Piece_Identite', '................')}<br/>
    <b>Numero de la piece d'identite :</b> {client_info.get('ID_Officiel', '................')}<br/>
    """
    story.append(Paragraph(info_debtor, styles['Normal']))
    story.append(Spacer(1, 10))

    story.append(Paragraph(
        "reconnais, librement, expressement et sans aucune contrainte, devoir la somme "
        "ci-apres indiquee a :", styles['NormalJustify']))
    story.append(Paragraph(
        "<b>Monsieur MOUSSIPI YA NDEME KLYDE JAIMAL</b>, titulaire du passeport n° AA287935, "
        "residant en France, ci-apres designe « le Preteur ».", styles['Normal']))
    story.append(Spacer(1, 10))

    story.append(Paragraph(
        f"Je reconnais devoir au Preteur la somme totale de "
        f"<b>{format_currency(loan_data['Montant_Total'])} FCFA</b> "
        f"au titre de l'offre <b>{offre if offre else 'N/A'}</b>.",   # [OK] Offre dans le corps
        styles['Normal']))
    if montant_en_lettres_auto:
        from DocumentGen.nombres_fr import montant_en_lettres
        story.append(Paragraph(f"({montant_en_lettres(loan_data['Montant_Total'])} Francs CFA).", styles['Normal']))
    else:
        story.append(Paragraph("(......................................................................... Francs CFA).", styles['Normal']))
    story.append(Spacer(1, 10))

    story.append(Paragraph(
        f"Cette somme correspond au montant total a rembourser au titre du Contrat de pret a "
        f"interets n° {loan_data['ID_Pret']}, incluant le capital, les interets, les penalites "
        f"eventuelles et tous frais prevus audit contrat.", styles['NormalJustify']))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "Je m'engage de maniere ferme, irrevocable et inconditionnelle a rembourser integralement "
        "ladite somme selon les modalites prevues dans le contrat de pret.", styles['NormalJustify']))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "Je reconnais expressement qu'aucune difficulte personnelle, professionnelle, financiere "
        "ou medicale, aucune perte d'emploi ou baisse de revenus, ne saurait en aucun cas justifier "
        "un retard ou un refus de paiement.", styles['NormalJustify']))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "En cas de non-respect de mes obligations, le Preteur pourra exiger le paiement immediat "
        "de la totalite des sommes restant dues, appliquer les penalites de retard prevues au "
        "contrat de pret et engager toute procedure de recouvrement.", styles['NormalJustify']))
    story.append(Spacer(1, 5))
    story.append(Paragraph(
        "Je reconnais avoir lu, compris et accepte l'integralite des engagements resultant de la "
        "presente reconnaissance de dette, que je signe en toute connaissance de cause.", styles['NormalJustify']))
    story.append(Spacer(1, 20))

    story.append(Paragraph(_ligne_fait_a(signature), styles['Normal']))
    story.append(Spacer(1, 10))
    story.append(Paragraph("<b>L'EMPRUNTEUR (DEBITEUR)</b>", styles['Normal']))
    story.append(Paragraph(f"Nom : {client_info['Nom_Complet']}", styles['Normal']))
    story.append(Spacer(1, 5))
    mention = str((signature or {}).get("mention_bon_pour", "") or "").strip()
    if mention:
        from xml.sax.saxutils import escape
        story.append(Paragraph(f"<i>« {escape(mention)} »</i>", styles['Normal']))
        story.append(Paragraph("<font size=7>(mention saisie par le signataire lui-meme lors de la "
                               "signature electronique)</font>", styles['Normal']))
    else:
        story.append(Paragraph(
            "<i>« Bon pour reconnaissance de dette de la somme de "
            "......................... Francs CFA, que je m'engage a rembourser integralement »</i>",
            styles['Normal']))
    story.append(Spacer(1, 15))
    if signature:
        story.extend(_bloc_signature_electronique(signature, styles))
    else:
        story.append(Paragraph("Signature :", styles['Normal']))

    doc.build(story)
    buffer.seek(0)
    return buffer


# ============================================================================
# 3. CONTRAT DE CAUTION
# ============================================================================

def generer_contrat_caution(loan_data, garant_info, signature=None, signature_preteur=None):
    buffer = BytesIO()
    doc    = SimpleDocTemplate(buffer, pagesize=A4,
                               rightMargin=2*cm, leftMargin=2*cm,
                               topMargin=2*cm, bottomMargin=2*cm,
                               invariant=1)
    styles = get_common_styles()
    story  = []

    offre = loan_data.get('Offre', '')

    story.append(Paragraph("CONTRAT DE CAUTION SOLIDAIRE", styles['TitleCenter']))
    story.append(Paragraph(
        f"Relatif au Contrat de pret a interets n° {loan_data['ID_Pret']}",
        styles['TitleCenter']))

    # [OK] Offre sous le titre
    story.append(Spacer(1, 6))
    story.append(_offre_table(offre, styles))
    story.append(Spacer(1, 10))

    story.append(Paragraph("Entre les soussignes :", styles['Normal']))

    story.append(Paragraph("<b>I. LE PRETEUR</b>", styles['Heading3']))
    story.append(Paragraph(
        "Nom complet : MOUSSIPI YA NDEME KLYDE JAIMAL<br/>"
        "Numero piece : AA287935 (Passeport)<br/>"
        "Pays : FRANCE<br/>"
        "Ci-apres designe « le Preteur »", styles['Normal']))
    story.append(Spacer(1, 10))

    story.append(Paragraph("<b>II. LE GARANT (CAUTION)</b>", styles['Heading3']))
    garant_txt = f"""
    Nom complet : {garant_info['Nom_Complet']}<br/>
    Statut professionnel : {garant_info.get('Statut_Pro', 'N/A')}<br/>
    Ville : {garant_info.get('Ville', 'N/A')}<br/>
    Telephone : {garant_info.get('Telephone', 'N/A')}<br/>
    Ci-apres designe « le Garant »,
    """
    story.append(Paragraph(garant_txt, styles['Normal']))
    story.append(Spacer(1, 15))

    story.append(Paragraph("<b>ARTICLE 1 - OBJET DU CONTRAT</b>", styles['Heading4']))
    story.append(Paragraph(
        f"Le Garant s'engage a garantir l'execution integrale des obligations de remboursement "
        f"de l'Emprunteur, beneficiaire du Contrat de pret n° {loan_data['ID_Pret']} "
        f"souscrit dans le cadre de l'offre <b>{offre if offre else 'N/A'}</b>.",  # [OK] Offre dans l'objet
        styles['NormalJustify']))

    story.append(Paragraph("<b>ARTICLE 2 - MONTANT ET ETENDUE</b>", styles['Heading4']))
    story.append(Paragraph(
        "La garantie couvre l'integralite des sommes dues : capital, interets, penalites de retard. "
        "L'engagement est illimite jusqu'au paiement total.", styles['NormalJustify']))

    story.append(Paragraph("<b>ARTICLE 3 - NATURE DE L'ENGAGEMENT</b>", styles['Heading4']))
    story.append(Paragraph(
        "Le Garant se porte caution solidaire et renonce expressement au benefice de discussion "
        "et au benefice de division. Le Preteur pourra reclamer directement au Garant le paiement "
        "sans mise en demeure prealable de l'Emprunteur.", styles['NormalJustify']))

    story.append(Paragraph("<b>ARTICLE 4 - EXIGIBILITE IMMEDIATE</b>", styles['Heading4']))
    story.append(Paragraph(
        "En cas de non-remboursement d'une seule echeance par l'Emprunteur, le Preteur pourra "
        "exiger immediatement du Garant le paiement de la totalite des sommes dues.", styles['NormalJustify']))

    story.append(Paragraph("<b>ARTICLES 5 A 9</b>", styles['Heading4']))
    story.append(Paragraph(
        "L'engagement demeure valable jusqu'au remboursement integral. Le present contrat est "
        "regi par le droit senegalais (Tribunaux de Dakar). Le Garant reconnait avoir lu et "
        "accepte toutes les clauses.", styles['NormalJustify']))
    story.append(Spacer(1, 20))

    story.append(Paragraph(_ligne_fait_a(signature), styles['Normal']))
    if signature:
        story.append(_table_parties_signature(
            "LE PRETEUR", "MOUSSIPI YA NDEME K.J.",
            "LE GARANT (CAUTION)", f"{garant_info['Nom_Complet']}", preteur_signe=bool(signature_preteur)))
        story.append(Spacer(1, 10))
        story.extend(_bloc_signature_electronique(signature, styles))
        if signature_preteur:
            story.append(Spacer(1, 10))
            story.extend(_bloc_signature_electronique(signature_preteur, styles))
    else:
        data_sign = [
            ["LE PRETEUR",                     "LE GARANT (CAUTION)"],
            ["MOUSSIPI YA NDEME K.J.",         f"{garant_info['Nom_Complet']}"],
            ["",                               "Signature precedee de la mention manuscrite :"],
            ["Signature :",                    "« Bon pour caution solidaire,\nrenonciation aux benefices de discussion »"],
            ["\n\n",                           "\n\n"]
        ]
        t_sign = Table(data_sign, colWidths=[8*cm, 8*cm])
        story.append(t_sign)

    doc.build(story)
    buffer.seek(0)
    return buffer