assemblerpdf 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assemblerpdf/__init__.py +16 -0
- assemblerpdf/assemblers/__init__.py +9 -0
- assemblerpdf/assemblers/base.py +120 -0
- assemblerpdf/assemblers/form.py +257 -0
- assemblerpdf/assemblers/report.py +44 -0
- assemblerpdf/core.py +193 -0
- assemblerpdf/fonts/Inter-Bold.ttf +0 -0
- assemblerpdf/fonts/Inter-Medium.ttf +0 -0
- assemblerpdf/fonts/Inter-Regular.ttf +0 -0
- assemblerpdf/img/Logo-unp-rojo.png +0 -0
- assemblerpdf/img/logo-mininterior-rojo.jpg +0 -0
- assemblerpdf/templates/assemblerpdf/checkbox.html +16 -0
- assemblerpdf/templates/assemblerpdf/footer.html +16 -0
- assemblerpdf/templates/assemblerpdf/grid_row.html +7 -0
- assemblerpdf/templates/assemblerpdf/header.html +19 -0
- assemblerpdf/templates/assemblerpdf/input.html +12 -0
- assemblerpdf/templates/assemblerpdf/radio.html +14 -0
- assemblerpdf/templates/assemblerpdf/section.html +6 -0
- assemblerpdf/templates/assemblerpdf/signature.html +14 -0
- assemblerpdf/templates/assemblerpdf/style.html +318 -0
- assemblerpdf/templates/assemblerpdf/textarea.html +12 -0
- assemblerpdf/templates/sistema/anexos.html +38 -0
- assemblerpdf/templates/sistema/anexos_limpio.html +52 -0
- assemblerpdf/templates/sistema/archivese.html +18 -0
- assemblerpdf/templates/sistema/autoriza_envio.html +33 -0
- assemblerpdf/templates/sistema/checkbox.html +41 -0
- assemblerpdf/templates/sistema/control_cambios.html +43 -0
- assemblerpdf/templates/sistema/datos_basicosinf.html +16 -0
- assemblerpdf/templates/sistema/datos_ot.html +33 -0
- assemblerpdf/templates/sistema/encaso_de.html +5 -0
- assemblerpdf/templates/sistema/fiila_dos_check.html +54 -0
- assemblerpdf/templates/sistema/fila_cinco_zona.html +54 -0
- assemblerpdf/templates/sistema/fila_cuatro.html +20 -0
- assemblerpdf/templates/sistema/fila_cuatro_si_no.html +46 -0
- assemblerpdf/templates/sistema/fila_cuatro_zona.html +48 -0
- assemblerpdf/templates/sistema/fila_dos.html +12 -0
- assemblerpdf/templates/sistema/fila_dos_zona.html +40 -0
- assemblerpdf/templates/sistema/fila_instructivo.html +16 -0
- assemblerpdf/templates/sistema/fila_tres.html +16 -0
- assemblerpdf/templates/sistema/fila_tres_checkbox.html +66 -0
- assemblerpdf/templates/sistema/fila_tres_zona.html +45 -0
- assemblerpdf/templates/sistema/fila_uno.html +8 -0
- assemblerpdf/templates/sistema/firma.html +10 -0
- assemblerpdf/templates/sistema/firma_doble.html +32 -0
- assemblerpdf/templates/sistema/firmaop2.html +17 -0
- assemblerpdf/templates/sistema/firmas_oficializacion.html +66 -0
- assemblerpdf/templates/sistema/hechos_sobrevinientes.html +261 -0
- assemblerpdf/templates/sistema/hechos_sobrevinientes_vacio.html +209 -0
- assemblerpdf/templates/sistema/iformfilacuatro.html +20 -0
- assemblerpdf/templates/sistema/informacion_doble.html +0 -0
- assemblerpdf/templates/sistema/input_2checks.html +98 -0
- assemblerpdf/templates/sistema/input_check.html +40 -0
- assemblerpdf/templates/sistema/input_checkbox.html +53 -0
- assemblerpdf/templates/sistema/input_solitario.html +9 -0
- assemblerpdf/templates/sistema/label_input_dos.html +24 -0
- assemblerpdf/templates/sistema/leyenda_autoriza_correo.html +8 -0
- assemblerpdf/templates/sistema/leyenda_superiol.html +5 -0
- assemblerpdf/templates/sistema/logos.html +4 -0
- assemblerpdf/templates/sistema/manifiesto.html +41 -0
- assemblerpdf/templates/sistema/notificacion_correo.html +14 -0
- assemblerpdf/templates/sistema/oficializacion.html +21 -0
- assemblerpdf/templates/sistema/pregunta_si_no.html +34 -0
- assemblerpdf/templates/sistema/pregunta_tresopciones.html +47 -0
- assemblerpdf/templates/sistema/qr_oficializacion.html +7 -0
- assemblerpdf/templates/sistema/situacion_amenaza.html +82 -0
- assemblerpdf/templates/sistema/style.html +228 -0
- assemblerpdf/templates/sistema/subtitulo_icono.html +6 -0
- assemblerpdf/templates/sistema/subtitulo_icono_largo.html +6 -0
- assemblerpdf/templates/sistema/tabla_doble.html +15 -0
- assemblerpdf/templates/sistema/tabla_instructivo.html +12 -0
- assemblerpdf/templates/sistema/tabla_matriz_riesgo.html +135 -0
- assemblerpdf/templates/sistema/tabla_proyeccion.html +37 -0
- assemblerpdf/templates/sistema/tabla_simple.html +187 -0
- assemblerpdf/templates/sistema/texarea.html +9 -0
- assemblerpdf/templates/sistema/tipo_identificacion.html +55 -0
- assemblerpdf/templates/sistema/tipos_checkbox.html +47 -0
- assemblerpdf/templates/sistema/titulo.html +6 -0
- assemblerpdf/templates/sistema/titulo_logo.html +22 -0
- assemblerpdf/templates/sistema/titulo_principal.html +5 -0
- assemblerpdf/utils/__init__.py +1 -0
- assemblerpdf/utils/template_loader.py +48 -0
- assemblerpdf-1.0.0.dist-info/METADATA +86 -0
- assemblerpdf-1.0.0.dist-info/RECORD +85 -0
- assemblerpdf-1.0.0.dist-info/WHEEL +5 -0
- assemblerpdf-1.0.0.dist-info/top_level.txt +1 -0
assemblerpdf/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .core import (
|
|
2
|
+
generate_pdf,
|
|
3
|
+
generate_pdf_from_string,
|
|
4
|
+
install_dependencies,
|
|
5
|
+
download_inter_font,
|
|
6
|
+
check_weasyprint_installed
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__version__ = "1.0.0"
|
|
10
|
+
__all__ = [
|
|
11
|
+
"generate_pdf",
|
|
12
|
+
"generate_pdf_from_string",
|
|
13
|
+
"install_dependencies",
|
|
14
|
+
"download_inter_font",
|
|
15
|
+
"check_weasyprint_installed",
|
|
16
|
+
]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from assemblerpdf.core import generate_pdf_from_string
|
|
3
|
+
|
|
4
|
+
class BaseFormAssembler:
|
|
5
|
+
def __init__(self, context=None):
|
|
6
|
+
self.context = context or {}
|
|
7
|
+
self.components = []
|
|
8
|
+
|
|
9
|
+
def _get_img_base(self):
|
|
10
|
+
import assemblerpdf
|
|
11
|
+
package_path = os.path.dirname(assemblerpdf.__file__)
|
|
12
|
+
img_dir = os.path.join(package_path, 'img')
|
|
13
|
+
img_dir = img_dir.replace('\\', '/')
|
|
14
|
+
return f'file:///{img_dir}'
|
|
15
|
+
|
|
16
|
+
def _render(self, template_name, extra_context=None):
|
|
17
|
+
"""Renderiza un componente usando Django (si está disponible) o Jinja2."""
|
|
18
|
+
# Unificar contexto
|
|
19
|
+
full_context = {
|
|
20
|
+
'img_base': self._get_img_base(),
|
|
21
|
+
**self.context,
|
|
22
|
+
**(extra_context or {})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Intentar usar Django primero si está configurado
|
|
26
|
+
try:
|
|
27
|
+
from django.template.loader import render_to_string
|
|
28
|
+
# Asegurar prefijo de carpeta
|
|
29
|
+
if not template_name.startswith("assemblerpdf/"):
|
|
30
|
+
django_path = f"assemblerpdf/{template_name}"
|
|
31
|
+
else:
|
|
32
|
+
django_path = template_name
|
|
33
|
+
return render_to_string(django_path, full_context)
|
|
34
|
+
except Exception:
|
|
35
|
+
# Fallback a Jinja2
|
|
36
|
+
from assemblerpdf.utils.template_loader import render_template
|
|
37
|
+
return render_template(template_name, full_context)
|
|
38
|
+
|
|
39
|
+
def add_component(self, template_name, extra_context=None, wrapper_html=None):
|
|
40
|
+
"""
|
|
41
|
+
Agrega un componente individual al ensamblador.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
template_name (str): Nombre del template (ej: 'input.html')
|
|
45
|
+
extra_context (dict): Contexto adicional para el template
|
|
46
|
+
wrapper_html (str): HTML envolvente opcional
|
|
47
|
+
"""
|
|
48
|
+
rendered = self._render(template_name, extra_context)
|
|
49
|
+
|
|
50
|
+
if wrapper_html:
|
|
51
|
+
rendered = wrapper_html.replace('{{component}}', rendered)
|
|
52
|
+
|
|
53
|
+
self.components.append(rendered)
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def add_grid_row(self, fields):
|
|
57
|
+
"""
|
|
58
|
+
Agrega una fila (grid_row) con múltiples campos alineados horizontalmente.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
fields (list): Lista de diccionarios, cada uno representa un campo:
|
|
62
|
+
[
|
|
63
|
+
{"template": "input.html", "context": {"label": "Nombre", "valor": "Juan"}, "ancho": "50%"},
|
|
64
|
+
{"template": "input.html", "context": {"label": "Cédula", "valor": "1234"}, "ancho": "50%"}
|
|
65
|
+
]
|
|
66
|
+
"""
|
|
67
|
+
columns = []
|
|
68
|
+
for field in fields:
|
|
69
|
+
tpl = field.get("template")
|
|
70
|
+
ctx = field.get("context", {})
|
|
71
|
+
ancho = field.get("ancho", "100%")
|
|
72
|
+
flex = field.get("flex", "none")
|
|
73
|
+
|
|
74
|
+
# Renderizar el campo
|
|
75
|
+
rendered_field = self._render(tpl, ctx)
|
|
76
|
+
columns.append({
|
|
77
|
+
"ancho": ancho,
|
|
78
|
+
"flex": flex,
|
|
79
|
+
"content": rendered_field
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
# Renderizar la fila contenedora grid_row.html
|
|
83
|
+
row_html = self._render("grid_row.html", {"columns": columns})
|
|
84
|
+
self.components.append(row_html)
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def add_raw_html(self, html):
|
|
88
|
+
"""Agrega HTML crudo al documento."""
|
|
89
|
+
self.components.append(html)
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def add_page_break(self):
|
|
93
|
+
"""Inserta un salto de página."""
|
|
94
|
+
self.components.append('<div class="page-break"></div>')
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def build(self):
|
|
98
|
+
"""Une todos los componentes renderizados y compila el PDF."""
|
|
99
|
+
# 1. Obtener estilos globales (style.html)
|
|
100
|
+
head = self._render("style.html")
|
|
101
|
+
|
|
102
|
+
# 2. Combinar componentes
|
|
103
|
+
html_completo = "\n".join(self.components)
|
|
104
|
+
|
|
105
|
+
# 3. Construir estructura HTML final
|
|
106
|
+
html_final = f"""
|
|
107
|
+
<!DOCTYPE html>
|
|
108
|
+
<html lang="es">
|
|
109
|
+
<head>
|
|
110
|
+
<meta charset="UTF-8">
|
|
111
|
+
<title>Documento PDF</title>
|
|
112
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
|
113
|
+
{head}
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
{html_completo}
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
119
|
+
"""
|
|
120
|
+
return generate_pdf_from_string(html_final)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from .base import BaseFormAssembler
|
|
2
|
+
|
|
3
|
+
class FormularioReposicionAssembler(BaseFormAssembler):
|
|
4
|
+
def __init__(self, context=None):
|
|
5
|
+
super().__init__(context)
|
|
6
|
+
self.page_number = 1
|
|
7
|
+
self.total_pages = 4
|
|
8
|
+
|
|
9
|
+
def build(self):
|
|
10
|
+
# ==========================================
|
|
11
|
+
# === PÁGINA 1: Datos y Radicado =========
|
|
12
|
+
# ==========================================
|
|
13
|
+
self.add_component('header.html', {
|
|
14
|
+
'titulo1': "FORMULARIO DE RECURSO DE REPOSICIÓN",
|
|
15
|
+
'titulo2': "CONTRA RESOLUCIONES DEL COMITÉ CERREM INDIVIDUAL Y COLECTIVO",
|
|
16
|
+
'titulo3': "UNIDAD NACIONAL DE PROTECCIÓN"
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
self.add_component('section.html', {'titulo': "Datos del radicado (Espacio para uso exclusivo de la UNP)"})
|
|
20
|
+
self.add_grid_row([
|
|
21
|
+
{"template": "input.html", "context": {"label": "1. Número del radicado *", "valor": self.context.get('numRadicado', '')}, "ancho": "33%"},
|
|
22
|
+
{"template": "input.html", "context": {"label": "2. Fecha de radicado *", "valor": self.context.get('fechaRadicado', '')}, "ancho": "33%"},
|
|
23
|
+
{"template": "input.html", "context": {"label": "3. Sede/Oficina", "valor": self.context.get('sedeRadicado', '')}, "ancho": "34%"}
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
self.add_component('section.html', {'titulo': "Fecha y lugar de diligenciamiento"})
|
|
27
|
+
self.add_grid_row([
|
|
28
|
+
{"template": "input.html", "context": {"label": "4. Fecha de diligenciamiento *", "valor": self.context.get('fechaDiligenciamiento', '')}, "ancho": "30%"},
|
|
29
|
+
{"template": "input.html", "context": {"label": "5. País *", "valor": self.context.get('paisDiligenciamiento', 'Colombia')}, "ancho": "20%"},
|
|
30
|
+
{"template": "input.html", "context": {"label": "6. Departamento *", "valor": self.context.get('dptoDiligenciamiento', '')}, "ancho": "25%"},
|
|
31
|
+
{"template": "input.html", "context": {"label": "7. Municipio *", "valor": self.context.get('mnpioDiligenciamiento', '')}, "ancho": "25%"}
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
self.add_component('section.html', {'titulo': "Resolución CERREM", 'icono': "fa-file-alt"})
|
|
35
|
+
self.add_grid_row([
|
|
36
|
+
{"template": "input.html", "context": {"label": "8. Número del radicado", "valor": self.context.get('numResolucion', '')}, "ancho": "50%"},
|
|
37
|
+
{"template": "input.html", "context": {"label": "9. Fecha de radicado", "valor": self.context.get('fechaResolucion', '')}, "ancho": "50%"}
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
self.add_component('section.html', {'titulo': "Datos Básicos", 'icono': "fa-user"})
|
|
41
|
+
self.add_grid_row([
|
|
42
|
+
{"template": "input.html", "context": {"label": "10. Primer nombre *", "valor": self.context.get('primerNombre', '')}, "ancho": "25%"},
|
|
43
|
+
{"template": "input.html", "context": {"label": "11. Segundo nombre", "valor": self.context.get('segundoNombre', '')}, "ancho": "25%"},
|
|
44
|
+
{"template": "input.html", "context": {"label": "12. Primer apellido *", "valor": self.context.get('primerApellido', '')}, "ancho": "25%"},
|
|
45
|
+
{"template": "input.html", "context": {"label": "13. Segundo apellido", "valor": self.context.get('segundoApellido', '')}, "ancho": "25%"}
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
self.add_component('checkbox.html', {
|
|
49
|
+
'label': "15. Género *",
|
|
50
|
+
'opciones': [
|
|
51
|
+
{'label': "Femenino", 'valor': "Femenino"},
|
|
52
|
+
{'label': "Masculino", 'valor': "Masculino"},
|
|
53
|
+
{'label': "No binario", 'valor': "No binario"}
|
|
54
|
+
],
|
|
55
|
+
'seleccionado': self.context.get('genero', '')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
self.add_component('section.html', {'titulo': "Número único de identificación personal (NUIP)", 'icono': "fa-id-card"})
|
|
59
|
+
self.add_grid_row([
|
|
60
|
+
{
|
|
61
|
+
"template": "checkbox.html",
|
|
62
|
+
"context": {
|
|
63
|
+
"label": "16. Tipo de Identificación *",
|
|
64
|
+
"opciones": [
|
|
65
|
+
{"label": "C.C.", "valor": "Cédula de ciudadanía"},
|
|
66
|
+
{"label": "C.E.", "valor": "Cédula de extranjería"},
|
|
67
|
+
{"label": "P.S.", "valor": "Pasaporte"},
|
|
68
|
+
{"label": "Otro", "valor": "Otro"}
|
|
69
|
+
],
|
|
70
|
+
"seleccionado": self.context.get('tipoIdentificacion', '')
|
|
71
|
+
},
|
|
72
|
+
"ancho": "45%"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"template": "input.html",
|
|
76
|
+
"context": {"label": "17. Número de identificación *", "valor": self.context.get('NumeroIdentificacion', '')},
|
|
77
|
+
"ancho": "27%"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"template": "input.html",
|
|
81
|
+
"context": {"label": "18. Fecha de expedición *", "valor": self.context.get('fechaExpedicion', '')},
|
|
82
|
+
"ancho": "28%"
|
|
83
|
+
}
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
self.add_component('section.html', {'titulo': "Datos de contacto", 'icono': "fa-envelope"})
|
|
87
|
+
self.add_grid_row([
|
|
88
|
+
{"template": "input.html", "context": {"label": "19. Celular uno *", "valor": self.context.get('celularUno', '')}, "ancho": "20%"},
|
|
89
|
+
{"template": "input.html", "context": {"label": "20. Celular dos", "valor": self.context.get('celularDos', '')}, "ancho": "20%"},
|
|
90
|
+
{"template": "input.html", "context": {"label": "21. Teléfono", "valor": self.context.get('telefono', '')}, "ancho": "20%"},
|
|
91
|
+
{"template": "input.html", "context": {"label": "22. Correo electrónico *", "valor": self.context.get('correo', '')}, "ancho": "40%"}
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
self.add_component('radio.html', {
|
|
95
|
+
'label': "23. ¿Autoriza el envío de notificaciones a través del correo electrónico inscrito?",
|
|
96
|
+
'opciones': [{'label': "Sí", 'valor': "Sí"}, {'label': "No", 'valor': "No"}],
|
|
97
|
+
'seleccionado': self.context.get('autoriza', '')
|
|
98
|
+
})
|
|
99
|
+
self.add_raw_html('<p style="font-size: 8px; color: #64748b; margin-top: -4px; margin-bottom: 12px;">* De conformidad con el artículo 56 de la Ley 1437 de 2011.</p>')
|
|
100
|
+
|
|
101
|
+
self.add_grid_row([
|
|
102
|
+
{
|
|
103
|
+
"template": "radio.html",
|
|
104
|
+
"context": {
|
|
105
|
+
"label": "24. ¿Presenta hechos sobrevinientes?",
|
|
106
|
+
"opciones": [{"label": "Sí", "valor": "Sí"}, {"label": "No", "valor": "No"}],
|
|
107
|
+
"seleccionado": self.context.get('presenta_hechos', '')
|
|
108
|
+
},
|
|
109
|
+
"ancho": "50%"
|
|
110
|
+
}
|
|
111
|
+
])
|
|
112
|
+
|
|
113
|
+
self.add_component('footer.html', {
|
|
114
|
+
'codigo': "GJU-FT-52-VX", 'fecha': "Oficialización: 00/00/0000", 'pagina': "Pág. 1 de 4"
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
# ==========================================
|
|
118
|
+
# === PÁGINA 2: Hechos Sobrevinientes =====
|
|
119
|
+
# ==========================================
|
|
120
|
+
self.add_page_break()
|
|
121
|
+
self.add_raw_html('<div class="bordered-page-container">')
|
|
122
|
+
self.add_component('section.html', {'titulo': "Hechos sobrevinientes que fundamentan el recurso", 'icono': "fa-edit"})
|
|
123
|
+
self.add_component('textarea.html', {
|
|
124
|
+
'alto': "780px",
|
|
125
|
+
'valor': self.context.get('hechosSobrevinientes', '')
|
|
126
|
+
})
|
|
127
|
+
self.add_raw_html('</div>')
|
|
128
|
+
self.add_component('footer.html', {
|
|
129
|
+
'codigo': "GJU-FT-52-VX", 'fecha': "Oficialización: 00/00/0000", 'pagina': "Pág. 2 de 4"
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
# ==========================================
|
|
133
|
+
# === PÁGINA 3: Medidas y Apoderado =======
|
|
134
|
+
# ==========================================
|
|
135
|
+
self.add_page_break()
|
|
136
|
+
self.add_raw_html('<div class="bordered-page-container">')
|
|
137
|
+
|
|
138
|
+
self.add_component('section.html', {'titulo': "Medidas de Protección", 'icono': "fa-shield-alt"})
|
|
139
|
+
self.add_component('radio.html', {
|
|
140
|
+
'label': "32. ¿Es beneficiario de medidas de protección por la UNP?",
|
|
141
|
+
'opciones': [{'label': "Sí", 'valor': "Sí"}, {'label': "No", 'valor': "No"}],
|
|
142
|
+
'seleccionado': self.context.get('tiene_medidas', '')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
self.add_component('section.html', {'titulo': "Datos apoderado"})
|
|
146
|
+
self.add_component('radio.html', {
|
|
147
|
+
'label': "33. ¿Presenta recurso mediante apoderado?",
|
|
148
|
+
'opciones': [{'label': "Sí", 'valor': "Sí"}, {'label': "No", 'valor': "No"}],
|
|
149
|
+
'seleccionado': self.context.get('presentaApoderado', '')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
self.add_component('section.html', {'titulo': "Nombre(s) y apellido(s) del apoderado", 'icono': "fa-user-tie"})
|
|
153
|
+
self.add_grid_row([
|
|
154
|
+
{"template": "input.html", "context": {"label": "34. Primer nombre *", "valor": self.context.get('primerNombreApoderado', '')}, "ancho": "25%"},
|
|
155
|
+
{"template": "input.html", "context": {"label": "35. Segundo nombre", "valor": self.context.get('segundoNombreApoderado', '')}, "ancho": "25%"},
|
|
156
|
+
{"template": "input.html", "context": {"label": "36. Primer apellido *", "valor": self.context.get('primerApellidoApoderado', '')}, "ancho": "25%"},
|
|
157
|
+
{"template": "input.html", "context": {"label": "37. Segundo apellido", "valor": self.context.get('segundoApellidoApoderado', '')}, "ancho": "25%"}
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
self.add_grid_row([
|
|
161
|
+
{
|
|
162
|
+
"template": "checkbox.html",
|
|
163
|
+
"context": {
|
|
164
|
+
"label": "38. Tipo de Identificación",
|
|
165
|
+
"opciones": [
|
|
166
|
+
{"label": "C.C.", "valor": "Cédula de ciudadanía"},
|
|
167
|
+
{"label": "C.E.", "valor": "Cédula de extranjería"},
|
|
168
|
+
{"label": "Pasaporte", "valor": "Pasaporte"}
|
|
169
|
+
],
|
|
170
|
+
"seleccionado": self.context.get('tipoIdentificacionApoderado', '')
|
|
171
|
+
},
|
|
172
|
+
"ancho": "40%"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"template": "input.html",
|
|
176
|
+
"context": {"label": "39. Número de identificación *", "valor": self.context.get('numeroIdentificacionApoderado', '')},
|
|
177
|
+
"ancho": "30%"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"template": "input.html",
|
|
181
|
+
"context": {"label": "40. Tarjeta Profesional *", "valor": self.context.get('tarjetaProfesional', '')},
|
|
182
|
+
"ancho": "30%"
|
|
183
|
+
}
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
self.add_component('section.html', {'titulo': "Inconformidad", 'icono': "fa-exclamation-circle"})
|
|
187
|
+
self.add_component('textarea.html', {
|
|
188
|
+
'label': "41. Descripción clara de los motivos de inconformidad",
|
|
189
|
+
'alto': "380px",
|
|
190
|
+
'valor': self.context.get('motivoInconformidad', '')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
self.add_raw_html('</div>')
|
|
194
|
+
self.add_component('footer.html', {
|
|
195
|
+
'codigo': "GJU-FT-52-VX", 'fecha': "Oficialización: 00/00/0000", 'pagina': "Pág. 3 de 4"
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
# ==========================================
|
|
199
|
+
# === PÁGINA 4: Anexos y Firma ============
|
|
200
|
+
# ==========================================
|
|
201
|
+
self.add_page_break()
|
|
202
|
+
self.add_raw_html('<div class="bordered-page-container">')
|
|
203
|
+
|
|
204
|
+
self.add_component('section.html', {'titulo': "Anexos adjuntos", 'icono': "fa-paperclip"})
|
|
205
|
+
self.add_component('checkbox.html', {
|
|
206
|
+
'label': "Marque los anexos que adjunta a este recurso:",
|
|
207
|
+
'opciones': [
|
|
208
|
+
{'label': "Copia del documento de identidad", 'valor': "identidad"},
|
|
209
|
+
{'label': "Poder debidamente otorgado (si aplica)", 'valor': "poder"},
|
|
210
|
+
{'label': "Pruebas documentales que sustentan la inconformidad", 'valor': "pruebas"},
|
|
211
|
+
{'label': "Otros documentos de interés", 'valor': "otros"}
|
|
212
|
+
],
|
|
213
|
+
'seleccionados': self.context.get('anexos_seleccionados', [])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
self.add_raw_html('''
|
|
217
|
+
<div style="margin-top: 25px; margin-bottom: 25px; padding: 12px; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 10px; color: #334155; background-color: #f8fafc; line-height: 1.4;">
|
|
218
|
+
<strong>42. MANIFIESTO BAJO LA GRAVEDAD DEL JURAMENTO *</strong><br>
|
|
219
|
+
Manifiesto bajo la gravedad del juramento que la información suministrada en este formulario es verídica y que los documentos adjuntos son copia fiel de sus originales.
|
|
220
|
+
</div>
|
|
221
|
+
''')
|
|
222
|
+
|
|
223
|
+
self.add_component('section.html', {'titulo': "Firma del solicitante / apoderado"})
|
|
224
|
+
|
|
225
|
+
firmas = [
|
|
226
|
+
{
|
|
227
|
+
'nombre': self.context.get('nombreCompleto', 'Firma del Solicitante'),
|
|
228
|
+
'cargo': "Solicitante",
|
|
229
|
+
'documento': self.context.get('NumeroIdentificacion', '')
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
if self.context.get('presentaApoderado') == 'Sí':
|
|
234
|
+
firmas.append({
|
|
235
|
+
'nombre': self.context.get('nombreCompletoApoderado', 'Firma del Apoderado'),
|
|
236
|
+
'cargo': "Apoderado",
|
|
237
|
+
'documento': self.context.get('numeroIdentificacionApoderado', '')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
self.add_component('signature.html', {
|
|
241
|
+
'firmas': firmas,
|
|
242
|
+
'margin_top': "60px"
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
self.add_raw_html('</div>')
|
|
246
|
+
|
|
247
|
+
self.add_raw_html('''
|
|
248
|
+
<div style="font-size: 8px; color: #64748b; text-align: center; margin-top: 10px;">
|
|
249
|
+
Archívese en: Carpeta única del solicitante - Unidad Nacional de Protección.
|
|
250
|
+
</div>
|
|
251
|
+
''')
|
|
252
|
+
|
|
253
|
+
self.add_component('footer.html', {
|
|
254
|
+
'codigo': "GJU-FT-52-VX", 'fecha': "Oficialización: 00/00/0000", 'pagina': "Pág. 4 de 4"
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
return super().build()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from .base import BaseFormAssembler
|
|
2
|
+
from assemblerpdf.core import generate_pdf_from_string
|
|
3
|
+
|
|
4
|
+
class TwoColumnReportAssembler(BaseFormAssembler):
|
|
5
|
+
def __init__(self, context=None):
|
|
6
|
+
super().__init__(context)
|
|
7
|
+
|
|
8
|
+
def add_section(self, title, content_html):
|
|
9
|
+
"""Agrega una sección con título y contenido en dos columnas."""
|
|
10
|
+
section_html = f"""
|
|
11
|
+
<div style="margin: 20px 0; padding: 15px; border: 1px solid #cbd5e1; border-radius: 8px; background-color: #ffffff;">
|
|
12
|
+
<h3 style="color: #002857; margin-top: 0; margin-bottom: 10px; font-size: 14px; font-weight: bold; border-bottom: 1px solid #cbd5e1; padding-bottom: 5px;">{title}</h3>
|
|
13
|
+
<div style="display: flex; gap: 20px;">
|
|
14
|
+
{content_html}
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
"""
|
|
18
|
+
self.components.append(section_html)
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def build(self):
|
|
22
|
+
# Renderizar head global (style.html)
|
|
23
|
+
head = self._render("style.html")
|
|
24
|
+
|
|
25
|
+
# Combinar todos los componentes
|
|
26
|
+
html_completo = "\n".join(self.components)
|
|
27
|
+
|
|
28
|
+
# Plantilla base del informe
|
|
29
|
+
html_final = f"""
|
|
30
|
+
<!DOCTYPE html>
|
|
31
|
+
<html lang="es">
|
|
32
|
+
<head>
|
|
33
|
+
<meta charset="UTF-8">
|
|
34
|
+
<title>Informe</title>
|
|
35
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
|
36
|
+
{head}
|
|
37
|
+
</head>
|
|
38
|
+
<body style="padding: 20px; background-color: #f8fafc;">
|
|
39
|
+
<h1 style="color: #002857; text-align: center; margin-bottom: 20px; font-size: 18px; font-weight: bold; border-bottom: 2px solid #002857; padding-bottom: 10px;">Informe</h1>
|
|
40
|
+
{html_completo}
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
43
|
+
"""
|
|
44
|
+
return generate_pdf_from_string(html_final)
|
assemblerpdf/core.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
import urllib.request
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
# Configurar logging básico
|
|
8
|
+
logging.basicConfig(level=logging.INFO)
|
|
9
|
+
logger = logging.getLogger("assemblerpdf")
|
|
10
|
+
|
|
11
|
+
# Intentar agregar MSYS2 en Windows al PATH de DLLs al importar el módulo
|
|
12
|
+
def _setup_windows_dll_path():
|
|
13
|
+
if sys.platform.startswith('win'):
|
|
14
|
+
# Buscar en ubicaciones típicas de MSYS2
|
|
15
|
+
msys_bin = r"C:\msys64\mingw64\bin"
|
|
16
|
+
if os.path.exists(msys_bin):
|
|
17
|
+
if msys_bin not in os.environ["PATH"]:
|
|
18
|
+
os.environ["PATH"] = msys_bin + os.path.pathsep + os.environ["PATH"]
|
|
19
|
+
try:
|
|
20
|
+
# Requiere Python >= 3.8
|
|
21
|
+
os.add_dll_directory(msys_bin)
|
|
22
|
+
logger.info(f"✅ DLL directory added: {msys_bin}")
|
|
23
|
+
except AttributeError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
_setup_windows_dll_path()
|
|
27
|
+
|
|
28
|
+
def check_weasyprint_installed():
|
|
29
|
+
"""Verifica si weasyprint y sus dependencias de C (Cairo/Pango) están listos."""
|
|
30
|
+
try:
|
|
31
|
+
import weasyprint
|
|
32
|
+
# Intentar una compilación mínima para verificar que Pango/Cairo carguen bien
|
|
33
|
+
weasyprint.HTML(string="<p>test</p>").write_pdf()
|
|
34
|
+
return True
|
|
35
|
+
except Exception as e:
|
|
36
|
+
logger.warning(f"❌ WeasyPrint no está completamente funcional: {e}")
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
def download_inter_font():
|
|
40
|
+
"""Descarga la fuente 'Inter' desde Google Fonts/GitHub a la carpeta de fuentes del paquete."""
|
|
41
|
+
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
42
|
+
fonts_dir = os.path.join(package_dir, "fonts")
|
|
43
|
+
os.makedirs(fonts_dir, exist_ok=True)
|
|
44
|
+
|
|
45
|
+
font_files = {
|
|
46
|
+
"Inter-Regular.ttf": "https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-400-normal.ttf",
|
|
47
|
+
"Inter-Medium.ttf": "https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-500-normal.ttf",
|
|
48
|
+
"Inter-Bold.ttf": "https://cdn.jsdelivr.net/fontsource/fonts/inter@latest/latin-700-normal.ttf"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for font_name, url in font_files.items():
|
|
52
|
+
dest_path = os.path.join(fonts_dir, font_name)
|
|
53
|
+
if not os.path.exists(dest_path):
|
|
54
|
+
logger.info(f"📥 Descargando fuente {font_name} desde {url}...")
|
|
55
|
+
try:
|
|
56
|
+
urllib.request.urlretrieve(url, dest_path)
|
|
57
|
+
logger.info(f"✅ Fuente {font_name} guardada en {dest_path}")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"❌ Error descargando {font_name}: {e}")
|
|
60
|
+
else:
|
|
61
|
+
logger.info(f"👍 Fuente {font_name} ya existe localmente.")
|
|
62
|
+
|
|
63
|
+
def install_dependencies():
|
|
64
|
+
"""Detecta el sistema operativo e instala dependencias del sistema y de Python."""
|
|
65
|
+
logger.info("🔍 Detectando sistema operativo y dependencias...")
|
|
66
|
+
|
|
67
|
+
# 1. Asegurar dependencias de Python
|
|
68
|
+
try:
|
|
69
|
+
import weasyprint
|
|
70
|
+
import requests
|
|
71
|
+
except ImportError:
|
|
72
|
+
logger.info("📦 Instalando dependencias de Python (weasyprint, requests)...")
|
|
73
|
+
subprocess.run([sys.executable, "-m", "pip", "install", "weasyprint", "requests"], check=True)
|
|
74
|
+
|
|
75
|
+
# 2. Asegurar dependencias del sistema operativo
|
|
76
|
+
if sys.platform.startswith('win'):
|
|
77
|
+
msys_bash = r"C:\msys64\usr\bin\bash.exe"
|
|
78
|
+
if not os.path.exists(msys_bash):
|
|
79
|
+
logger.info("⚙️ MSYS2 no detectado. Intentando instalar vía winget...")
|
|
80
|
+
try:
|
|
81
|
+
subprocess.run(["winget", "install", "MSYS2.MSYS2", "--silent", "--accept-source-agreements", "--accept-package-agreements"], check=True)
|
|
82
|
+
logger.info("✅ MSYS2 instalado. Por favor, reinicia la terminal si no se detecta de inmediato.")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"❌ Error al instalar MSYS2 vía winget: {e}")
|
|
85
|
+
logger.error("Por favor, instala MSYS2 manualmente desde https://www.msys2.org/")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
# Si MSYS2 está instalado, asegurar mingw-w64-x86_64-pango
|
|
89
|
+
if os.path.exists(msys_bash):
|
|
90
|
+
logger.info("⚙️ Instalando pango en MSYS2...")
|
|
91
|
+
try:
|
|
92
|
+
subprocess.run([msys_bash, "-lc", "pacman -S --noconfirm --needed mingw-w64-x86_64-pango"], check=True)
|
|
93
|
+
_setup_windows_dll_path()
|
|
94
|
+
logger.info("✅ Pango y Cairo configurados en MSYS2.")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"❌ Error al ejecutar pacman en MSYS2: {e}")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
elif sys.platform.startswith('linux'):
|
|
100
|
+
# Detectar el gestor de paquetes de Linux
|
|
101
|
+
if os.path.exists("/usr/bin/apt-get"):
|
|
102
|
+
logger.info("⚙️ Sistema Debian/Ubuntu detectado. Instalando librerías requeridas...")
|
|
103
|
+
try:
|
|
104
|
+
subprocess.run(["sudo", "apt-get", "update"], check=True)
|
|
105
|
+
subprocess.run(["sudo", "apt-get", "install", "-y", "libpango-1.0-0", "libpangocairo-1.0-0", "libgdk-pixbuf2.0-0", "libffi-dev", "shared-mime-info"], check=True)
|
|
106
|
+
logger.info("✅ Librerías del sistema instaladas con éxito.")
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"❌ Error al instalar dependencias con apt-get: {e}")
|
|
109
|
+
logger.error("Ejecuta manualmente: sudo apt-get install -y libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info")
|
|
110
|
+
return False
|
|
111
|
+
elif os.path.exists("/usr/bin/dnf"):
|
|
112
|
+
logger.info("⚙️ Sistema Fedora/RHEL detectado. Instalando librerías...")
|
|
113
|
+
try:
|
|
114
|
+
subprocess.run(["sudo", "dnf", "install", "-y", "pango", "cairo", "gdk-pixbuf2", "libffi-devel"], check=True)
|
|
115
|
+
logger.info("✅ Librerías del sistema instaladas con éxito.")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"❌ Error al instalar dependencias con dnf: {e}")
|
|
118
|
+
return False
|
|
119
|
+
else:
|
|
120
|
+
logger.warning("⚠️ No se pudo determinar el gestor de paquetes. Asegúrate de instalar pango, cairo y gdk-pixbuf2 manualmente.")
|
|
121
|
+
|
|
122
|
+
elif sys.platform.startswith('darwin'):
|
|
123
|
+
logger.info("⚙️ macOS detectado. Intentando instalar vía Homebrew...")
|
|
124
|
+
try:
|
|
125
|
+
subprocess.run(["brew", "install", "weasyprint"], check=True)
|
|
126
|
+
logger.info("✅ WeasyPrint y dependencias de macOS instaladas con Homebrew.")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"❌ Error al instalar con Homebrew: {e}")
|
|
129
|
+
logger.error("Ejecuta manualmente: brew install weasyprint")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
# 3. Descargar la fuente Inter
|
|
133
|
+
download_inter_font()
|
|
134
|
+
|
|
135
|
+
# 4. Verificar resultado final
|
|
136
|
+
if check_weasyprint_installed():
|
|
137
|
+
logger.info("🎉 WeasyPrint está completamente listo para usarse.")
|
|
138
|
+
return True
|
|
139
|
+
else:
|
|
140
|
+
logger.error("❌ La instalación de dependencias falló o WeasyPrint no se cargó correctamente.")
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
def generate_pdf(template_name, context=None, output_path=None):
|
|
144
|
+
"""Genera PDF usando Django (si está configurado) o Jinja2."""
|
|
145
|
+
if context is None:
|
|
146
|
+
context = {}
|
|
147
|
+
|
|
148
|
+
# Intentar usar Django primero si está configurado en el proyecto
|
|
149
|
+
try:
|
|
150
|
+
from django.template.loader import render_to_string
|
|
151
|
+
html = render_to_string(f'assemblerpdf/{template_name}', context)
|
|
152
|
+
except Exception:
|
|
153
|
+
# Fallback a Jinja2 local
|
|
154
|
+
from assemblerpdf.utils.template_loader import render_template
|
|
155
|
+
html = render_template(template_name, context)
|
|
156
|
+
|
|
157
|
+
return _generate_pdf_from_html(html, output_path)
|
|
158
|
+
|
|
159
|
+
def generate_pdf_from_string(html_content, output_path=None):
|
|
160
|
+
"""Genera PDF directamente desde un string HTML."""
|
|
161
|
+
return _generate_pdf_from_html(html_content, output_path)
|
|
162
|
+
|
|
163
|
+
def _generate_pdf_from_html(html, output_path=None):
|
|
164
|
+
"""Función interna que utiliza WeasyPrint para compilar HTML a PDF."""
|
|
165
|
+
# Verificar disponibilidad de WeasyPrint
|
|
166
|
+
if not check_weasyprint_installed():
|
|
167
|
+
logger.warning("⚠️ WeasyPrint no está completamente funcional. Intentando auto-configurar dependencias...")
|
|
168
|
+
# Intentar inicializar DLLs si el usuario instaló pero no se activó la DLL de Windows
|
|
169
|
+
_setup_windows_dll_path()
|
|
170
|
+
if not check_weasyprint_installed():
|
|
171
|
+
raise RuntimeError(
|
|
172
|
+
"WeasyPrint no tiene instaladas sus dependencias del sistema. "
|
|
173
|
+
"Ejecuta assemblerpdf.install_dependencies() en tu entorno."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
from weasyprint import HTML
|
|
177
|
+
|
|
178
|
+
# base_url apunta a la carpeta del paquete para poder cargar imágenes y fuentes usando rutas relativas
|
|
179
|
+
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
180
|
+
base_url = "file:///" + package_dir.replace('\\', '/').rstrip('/') + '/'
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Renderizar a PDF
|
|
184
|
+
pdf_data = HTML(string=html, base_url=base_url).write_pdf()
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise RuntimeError(f"Error al generar PDF con WeasyPrint: {e}")
|
|
187
|
+
|
|
188
|
+
if output_path:
|
|
189
|
+
with open(output_path, 'wb') as f:
|
|
190
|
+
f.write(pdf_data)
|
|
191
|
+
return output_path
|
|
192
|
+
|
|
193
|
+
return pdf_data
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|