60 Contrato de Préstamo en Laravel 12: Generación de Documentos Legales con DomPDF y Estilos CSS 📄✍️
Duración: 13 minDescripción
📄 Lección 60: Generación de Contratos de Préstamo en Laravel 12
En esta sesión de Benji V2, implementamos el módulo de Contratos. Aprendemos a generar documentos PDF de tamaño carta (Letter) que sirven como respaldo legal, detallando todas las cláusulas y la tabla de amortización para la firma de conformidad entre el prestamista y el cliente.
🛠️ Configuración del Documento Legal
A diferencia de los tickets térmicos, el contrato requiere un formato de oficina estándar:
- 📐 Formato de Papel: Configuramos DomPDF para utilizar tamaño Letter (Carta) en orientación vertical (Portrait) [08:46].
- ✒️ Tipografía Profesional: Establecimos fuentes claras y tamaños adecuados para asegurar que las cláusulas legales sean legibles y profesionales [07:56].
🧬 Lógica y Estructura del Contrato
El documento se genera de forma 100% dinámica extrayendo información en tiempo real:
- 🏢 Datos Institucionales: Cabecera con logo, nombre, dirección, teléfono y correo de la empresa (desde ajustes) [09:44].
- 👤 Identificación Completa: Detalle del cliente (nombre, documento, dirección, teléfono) y del préstamo (monto, tasa de interés, modalidad de pago y fechas) [10:00].
- 📊 Tabla de Amortización Integrada: El contrato incluye el desglose completo de todas las cuotas (capital, interés y saldos), adaptándose automáticamente si son 12, 36 o más cuotas [10:42].
- ⚖️ Cláusulas y Firmas: Añadimos secciones de cláusulas legales y un pie de página con espacios para el sello de la empresa y la firma del cliente [10:58].
🚀 Experiencia de Usuario (UX)
- 🖱️ Acceso Directo: Añadimos un botón de "Contrato" (color amarillo con icono de impresora) directamente en la lista de préstamos [04:38].
- 📂 Apertura Inteligente: Configuramos el enlace con target="_blank" para que el PDF se abra en una nueva pestaña, permitiendo al administrador seguir trabajando en el sistema mientras se imprime el documento [11:26].
✅ Resultado de la Lección
Al finalizar, el sistema no solo gestiona números, sino que produce documentación legal válida. Ya sea un préstamo pequeño o uno de gran capital (ej. 70,000 unidades), el sistema genera instantáneamente un contrato profesional de múltiples páginas con todo el historial de pagos proyectado, listo para ser impreso y firmado.
Contenido
Código fuente de la lección
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Contrato de Préstamo #{{ $prestamo->id }}</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: DejaVu Sans, Arial, sans-serif;
color: #1f2937;
margin: 28px;
font-size: 12px;
line-height: 1.45;
}
.header {
border-bottom: 2px solid #111827;
padding-bottom: 12px;
margin-bottom: 18px;
}
.company {
font-size: 18px;
font-weight: 700;
color: #111827;
margin-bottom: 4px;
}
.muted {
color: #6b7280;
font-size: 11px;
}
.title {
margin-top: 14px;
padding: 10px 12px;
background: #f3f4f6;
border: 1px solid #d1d5db;
font-size: 15px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .4px;
}
.meta {
margin-top: 10px;
font-size: 11px;
color: #374151;
}
.section {
margin-top: 18px;
}
.section h3 {
margin: 0 0 8px 0;
font-size: 13px;
color: #111827;
border-left: 4px solid #4f46e5;
padding-left: 8px;
}
.box {
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 10px 12px;
background: #ffffff;
}
.grid-2 {
width: 100%;
border-collapse: separate;
border-spacing: 10px 0;
margin-left: -10px;
margin-right: -10px;
}
.grid-2 td {
width: 50%;
vertical-align: top;
}
.row {
margin: 2px 0;
}
.row .label {
color: #6b7280;
font-size: 11px;
display: inline-block;
min-width: 150px;
}
.amount-highlight {
margin-top: 10px;
padding: 10px;
border: 1px dashed #9ca3af;
background: #f9fafb;
font-weight: 700;
}
table.schedule {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
font-size: 10.5px;
}
.schedule thead th {
background: #eef2ff;
color: #1e1b4b;
border: 1px solid #c7d2fe;
padding: 6px;
text-align: center;
font-weight: 700;
}
.schedule tbody td {
border: 1px solid #d1d5db;
padding: 5px 6px;
text-align: right;
}
.schedule tbody td.left {
text-align: left;
}
.schedule tfoot td {
border: 1px solid #9ca3af;
background: #f3f4f6;
font-weight: 700;
padding: 6px;
text-align: right;
}
.clauses {
margin-top: 12px;
padding-left: 16px;
}
.clauses li {
margin-bottom: 6px;
text-align: justify;
}
.signatures {
width: 100%;
margin-top: 34px;
}
.signatures td {
width: 50%;
text-align: center;
vertical-align: top;
padding: 0 10px;
}
.line {
margin-top: 42px;
border-top: 1px solid #111827;
padding-top: 6px;
font-weight: 600;
font-size: 11px;
}
.footer {
margin-top: 14px;
font-size: 10px;
color: #6b7280;
text-align: center;
}
.page-break {
page-break-after: always;
}
</style>
</head>
<body>
@php
$divisa = $ajuste->divisa ?? '$';
$fechaContrato = optional($prestamo->created_at)->format('d/m/Y');
@endphp
<div class="header">
<div class="company">{{ $ajuste->nombre ?? 'Empresa de Préstamos' }}</div>
<div class="muted">
{{ $ajuste->direccion ?? '-' }}
| Tel: {{ $ajuste->telefono ?? '-' }}
| Email: {{ $ajuste->email ?? '-' }}
@if (!empty($ajuste->web))
| Web: {{ $ajuste->web }}
@endif
</div>
<div class="title">Contrato de préstamo N° {{ $prestamo->id }}</div>
<div class="meta">
Fecha de emisión: {{ $fechaContrato ?: now()->format('d/m/Y') }}
</div>
</div>
<table class="grid-2">
<tr>
<td>
<div class="section">
<h3>Datos del cliente</h3>
<div class="box">
<div class="row"><span class="label">Nombre completo:</span>
{{ $cliente->apellidos }} {{ $cliente->nombres }}</div>
<div class="row"><span class="label">Documento:</span>
{{ $cliente->tipo_documento }} {{ $cliente->numero_documento }}</div>
<div class="row"><span class="label">Teléfono:</span> {{ $cliente->celular ?: '-' }}</div>
<div class="row"><span class="label">Dirección:</span> {{ $cliente->direccion ?: '-' }}
</div>
</div>
</div>
</td>
<td>
<div class="section">
<h3>Datos del préstamo</h3>
<div class="box">
<div class="row"><span class="label">Categoría:</span>
{{ optional($prestamo->categoria)->nombre ?: '-' }}</div>
<div class="row"><span class="label">Modalidad de pago:</span>
{{ $prestamo->modalidad_pago }}</div>
<div class="row"><span class="label">Amortización:</span>
{{ $prestamo->modalidad_amortizacion }}</div>
<div class="row"><span class="label">Fecha de inicio:</span>
{{ $prestamo->fecha_inicio ? \Carbon\Carbon::parse($prestamo->fecha_inicio)->format('d/m/Y') : '-' }}
</div>
<div class="row"><span class="label">Número de cuotas:</span> {{ $prestamo->nro_cuotas }}
</div>
<div class="amount-highlight">
Monto prestado: {{ $divisa }}
{{ number_format($prestamo->monto_prestado ?? 0, 2) }}<br>
Interés total: {{ $divisa }}
{{ number_format($prestamo->monto_interes_total ?? 0, 2) }}<br>
Total a pagar: {{ $divisa }}
{{ number_format($prestamo->monto_total_a_pagar ?? 0, 2) }}
</div>
</div>
</div>
</td>
</tr>
</table>
<div class="section">
<h3>Cláusulas principales</h3>
<div class="box">
<ol class="clauses">
<li>La parte prestataria reconoce haber recibido el monto indicado y se compromete a devolverlo según el
cronograma de pagos detallado en este contrato.</li>
<li>Los pagos se efectuarán conforme a la modalidad pactada, en las fechas de vencimiento señaladas en
la tabla de amortización.</li>
<li>El incumplimiento de pago podrá generar mora diaria conforme a la política vigente de la entidad
acreedora.</li>
<li>La parte prestataria declara que la información personal proporcionada es veraz y autoriza su
tratamiento para fines de gestión del crédito.</li>
<li>Este documento constituye constancia formal de aceptación de las condiciones del préstamo.</li>
</ol>
</div>
</div>
<div class="section">
<h3>Tabla de amortización</h3>
<table class="schedule">
<thead>
<tr>
<th>#</th>
<th>Referencia</th>
<th>Vencimiento</th>
<th>Saldo capital</th>
<th>Capital</th>
<th>Interés</th>
<th>Cuota</th>
</tr>
</thead>
<tbody>
@forelse ($pagos as $index => $pago)
<tr>
<td class="left">{{ $index + 1 }}</td>
<td class="left">{{ $pago->referencia_pago }}</td>
<td>{{ $pago->fecha_vencimiento ? \Carbon\Carbon::parse($pago->fecha_vencimiento)->format('d/m/Y') : '-' }}
</td>
<td>{{ $divisa }} {{ number_format($pago->saldo_capital ?? 0, 2) }}</td>
<td>{{ $divisa }} {{ number_format($pago->monto_capital ?? 0, 2) }}</td>
<td>{{ $divisa }} {{ number_format($pago->monto_interes ?? 0, 2) }}</td>
<td>{{ $divisa }} {{ number_format($pago->monto_cuota ?? 0, 2) }}</td>
</tr>
@empty
<tr>
<td colspan="7" class="left">No existen cuotas registradas para este préstamo.</td>
</tr>
@endforelse
</tbody>
<tfoot>
<tr>
<td colspan="4" class="left">Totales</td>
<td>{{ $divisa }} {{ number_format($totalCapital ?? 0, 2) }}</td>
<td>{{ $divisa }} {{ number_format($totalInteres ?? 0, 2) }}</td>
<td>{{ $divisa }} {{ number_format($totalCuotas ?? 0, 2) }}</td>
</tr>
</tfoot>
</table>
</div>
<table class="signatures">
<tr>
<td>
<div class="line">Firma y sello de la empresa</div>
</td>
<td>
<div class="line">Firma del cliente</div>
</td>
</tr>
</table>
<div class="footer">
Documento generado por el sistema el {{ now()->format('d/m/Y H:i') }}.
</div>
</body>
</html>