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.
Files changed (85) hide show
  1. assemblerpdf/__init__.py +16 -0
  2. assemblerpdf/assemblers/__init__.py +9 -0
  3. assemblerpdf/assemblers/base.py +120 -0
  4. assemblerpdf/assemblers/form.py +257 -0
  5. assemblerpdf/assemblers/report.py +44 -0
  6. assemblerpdf/core.py +193 -0
  7. assemblerpdf/fonts/Inter-Bold.ttf +0 -0
  8. assemblerpdf/fonts/Inter-Medium.ttf +0 -0
  9. assemblerpdf/fonts/Inter-Regular.ttf +0 -0
  10. assemblerpdf/img/Logo-unp-rojo.png +0 -0
  11. assemblerpdf/img/logo-mininterior-rojo.jpg +0 -0
  12. assemblerpdf/templates/assemblerpdf/checkbox.html +16 -0
  13. assemblerpdf/templates/assemblerpdf/footer.html +16 -0
  14. assemblerpdf/templates/assemblerpdf/grid_row.html +7 -0
  15. assemblerpdf/templates/assemblerpdf/header.html +19 -0
  16. assemblerpdf/templates/assemblerpdf/input.html +12 -0
  17. assemblerpdf/templates/assemblerpdf/radio.html +14 -0
  18. assemblerpdf/templates/assemblerpdf/section.html +6 -0
  19. assemblerpdf/templates/assemblerpdf/signature.html +14 -0
  20. assemblerpdf/templates/assemblerpdf/style.html +318 -0
  21. assemblerpdf/templates/assemblerpdf/textarea.html +12 -0
  22. assemblerpdf/templates/sistema/anexos.html +38 -0
  23. assemblerpdf/templates/sistema/anexos_limpio.html +52 -0
  24. assemblerpdf/templates/sistema/archivese.html +18 -0
  25. assemblerpdf/templates/sistema/autoriza_envio.html +33 -0
  26. assemblerpdf/templates/sistema/checkbox.html +41 -0
  27. assemblerpdf/templates/sistema/control_cambios.html +43 -0
  28. assemblerpdf/templates/sistema/datos_basicosinf.html +16 -0
  29. assemblerpdf/templates/sistema/datos_ot.html +33 -0
  30. assemblerpdf/templates/sistema/encaso_de.html +5 -0
  31. assemblerpdf/templates/sistema/fiila_dos_check.html +54 -0
  32. assemblerpdf/templates/sistema/fila_cinco_zona.html +54 -0
  33. assemblerpdf/templates/sistema/fila_cuatro.html +20 -0
  34. assemblerpdf/templates/sistema/fila_cuatro_si_no.html +46 -0
  35. assemblerpdf/templates/sistema/fila_cuatro_zona.html +48 -0
  36. assemblerpdf/templates/sistema/fila_dos.html +12 -0
  37. assemblerpdf/templates/sistema/fila_dos_zona.html +40 -0
  38. assemblerpdf/templates/sistema/fila_instructivo.html +16 -0
  39. assemblerpdf/templates/sistema/fila_tres.html +16 -0
  40. assemblerpdf/templates/sistema/fila_tres_checkbox.html +66 -0
  41. assemblerpdf/templates/sistema/fila_tres_zona.html +45 -0
  42. assemblerpdf/templates/sistema/fila_uno.html +8 -0
  43. assemblerpdf/templates/sistema/firma.html +10 -0
  44. assemblerpdf/templates/sistema/firma_doble.html +32 -0
  45. assemblerpdf/templates/sistema/firmaop2.html +17 -0
  46. assemblerpdf/templates/sistema/firmas_oficializacion.html +66 -0
  47. assemblerpdf/templates/sistema/hechos_sobrevinientes.html +261 -0
  48. assemblerpdf/templates/sistema/hechos_sobrevinientes_vacio.html +209 -0
  49. assemblerpdf/templates/sistema/iformfilacuatro.html +20 -0
  50. assemblerpdf/templates/sistema/informacion_doble.html +0 -0
  51. assemblerpdf/templates/sistema/input_2checks.html +98 -0
  52. assemblerpdf/templates/sistema/input_check.html +40 -0
  53. assemblerpdf/templates/sistema/input_checkbox.html +53 -0
  54. assemblerpdf/templates/sistema/input_solitario.html +9 -0
  55. assemblerpdf/templates/sistema/label_input_dos.html +24 -0
  56. assemblerpdf/templates/sistema/leyenda_autoriza_correo.html +8 -0
  57. assemblerpdf/templates/sistema/leyenda_superiol.html +5 -0
  58. assemblerpdf/templates/sistema/logos.html +4 -0
  59. assemblerpdf/templates/sistema/manifiesto.html +41 -0
  60. assemblerpdf/templates/sistema/notificacion_correo.html +14 -0
  61. assemblerpdf/templates/sistema/oficializacion.html +21 -0
  62. assemblerpdf/templates/sistema/pregunta_si_no.html +34 -0
  63. assemblerpdf/templates/sistema/pregunta_tresopciones.html +47 -0
  64. assemblerpdf/templates/sistema/qr_oficializacion.html +7 -0
  65. assemblerpdf/templates/sistema/situacion_amenaza.html +82 -0
  66. assemblerpdf/templates/sistema/style.html +228 -0
  67. assemblerpdf/templates/sistema/subtitulo_icono.html +6 -0
  68. assemblerpdf/templates/sistema/subtitulo_icono_largo.html +6 -0
  69. assemblerpdf/templates/sistema/tabla_doble.html +15 -0
  70. assemblerpdf/templates/sistema/tabla_instructivo.html +12 -0
  71. assemblerpdf/templates/sistema/tabla_matriz_riesgo.html +135 -0
  72. assemblerpdf/templates/sistema/tabla_proyeccion.html +37 -0
  73. assemblerpdf/templates/sistema/tabla_simple.html +187 -0
  74. assemblerpdf/templates/sistema/texarea.html +9 -0
  75. assemblerpdf/templates/sistema/tipo_identificacion.html +55 -0
  76. assemblerpdf/templates/sistema/tipos_checkbox.html +47 -0
  77. assemblerpdf/templates/sistema/titulo.html +6 -0
  78. assemblerpdf/templates/sistema/titulo_logo.html +22 -0
  79. assemblerpdf/templates/sistema/titulo_principal.html +5 -0
  80. assemblerpdf/utils/__init__.py +1 -0
  81. assemblerpdf/utils/template_loader.py +48 -0
  82. assemblerpdf-1.0.0.dist-info/METADATA +86 -0
  83. assemblerpdf-1.0.0.dist-info/RECORD +85 -0
  84. assemblerpdf-1.0.0.dist-info/WHEEL +5 -0
  85. assemblerpdf-1.0.0.dist-info/top_level.txt +1 -0
@@ -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,9 @@
1
+ from .base import BaseFormAssembler
2
+ from .form import FormularioReposicionAssembler
3
+ from .report import TwoColumnReportAssembler
4
+
5
+ __all__ = [
6
+ "BaseFormAssembler",
7
+ "FormularioReposicionAssembler",
8
+ "TwoColumnReportAssembler"
9
+ ]
@@ -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