import os
from reportlab.lib.pagesizes import letter
from reportlab.lib import colors
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.pdfgen import canvas
class NumberedCanvas(canvas.Canvas):
"""Canvas personalizado para añadir numeración dinámica de páginas (Página X de Y)"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._saved_page_states = []
def showPage(self):
self._saved_page_states.append(dict(self.__dict__))
self._startPage()
def save(self):
num_pages = len(self._saved_page_states)
for state in self._saved_page_states:
self.__dict__.update(state)
self.draw_page_number(num_pages)
canvas.Canvas.showPage(self)
canvas.Canvas.save(self)
def draw_page_number(self, page_count):
self.saveState()
self.setFont("Helvetica", 9)
self.setFillColor(colors.HexColor("#666666"))
# Encabezado (se omite en la primera página si es portada, aquí se aplica a todo el reporte)
self.drawString(54, 11 * inch - 36, "World Pos Solutions - Reporte Financiero Confidencial")
self.setStrokeColor(colors.HexColor("#cccccc"))
self.setLineWidth(0.5)
self.line(54, 11 * inch - 42, 8.5 * inch - 54, 11 * inch - 42)
# Pie de página
page_text = f"Página {self._pageNumber} de {page_count}"
self.drawRightString(8.5 * inch - 54, 36, page_text)
self.drawString(54, 36, "Reporte generado automáticamente - Estricta Reserva Corporativa")
self.line(54, 48, 8.5 * inch - 54, 48)
self.restoreState()
def generar_reporte_pdf(filename="Reporte_Financiero_World_Pos_Solutions.pdf"):
# Configuración del documento (Márgenes de 0.75 pulgadas / 54 puntos)
doc = SimpleDocTemplate(
filename,
pagesize=letter,
leftMargin=54,
rightMargin=54,
topMargin=54,
bottomMargin=54
)
styles = getSampleStyleSheet()
# Paleta de Colores Corporativa
PRIMARY = colors.HexColor("#1A5F7A")
SECONDARY = colors.HexColor("#C84B31")
TEXT_DARK = colors.HexColor("#222222")
BG_LIGHT = colors.HexColor("#F9F9F9")
# Estilos de Texto Personalizados
title_style = ParagraphStyle(
'DocTitle',
parent=styles['Heading1'],
fontName='Helvetica-Bold',
fontSize=24,
leading=28,
textColor=PRIMARY,
spaceAfter=15
)
h1_style = ParagraphStyle(
'Heading1_Custom',
parent=styles['Heading2'],
fontName='Helvetica-Bold',
fontSize=14,
leading=18,
textColor=PRIMARY,
spaceBefore=12,
spaceAfter=8,
keepWithNext=True
)
body_style = ParagraphStyle(
'Body_Custom',
parent=styles['BodyText'],
fontName='Helvetica',
fontSize=10,
leading=14,
textColor=TEXT_DARK,
spaceAfter=8
)
table_text = ParagraphStyle(
'TableText',
parent=styles['Normal'],
fontName='Helvetica',
fontSize=8,
leading=10,
textColor=TEXT_DARK
)
table_header = ParagraphStyle(
'TableHeader',
parent=styles['Normal'],
fontName='Helvetica-Bold',
fontSize=8,
leading=10,
textColor=colors.white
)
story = []
# --- ENCABEZADO DEL INFORME ---
story.append(Spacer(1, 15))
story.append(Paragraph("REPORTE FINANCIERO INTEGRAL", title_style))
story.append(Paragraph("<b>Empresa:</b> World Pos Solutions S.A.S.<br/><b>Periodo analizado:</b> Abril - Mayo 2026<br/><b>Moneda:</b> Pesos Colombianos (COP)", body_style))
story.append(Spacer(1, 10))
# --- SECCIÓN 1: ESTADO DE RESULTADOS ---
story.append(Paragraph("1. Estado de Resultados Comparativo y Análisis de Variaciones", h1_style))
story.append(Paragraph("A continuación se presenta la estructura de ingresos, costos y gastos con sus respectivos análisis verticales (participación sobre el ingreso total de cada mes) y horizontales (variación temporal entre periodos):", body_style))
# Datos de la tabla (Estructurada para evitar desbordes de texto)
raw_data = [
["Cuenta del Estado de Resultados", "Abril 2026 (COP)", "AV Abr", "Mayo 2026 (COP)", "AV May", "Var. Absoluta", "Var. Rel (%)"],
["INGRESOS OPERACIONALES", "287.453.573", "50,00%", "231.254.790", "89,52%", "-56.198.783", "-19,55%"],
[" Comercio al por Mayor y Menor", "38.585.706", "6,71%", "25.235.714", "9,77%", "-13.349.992", "-34,60%"],
[" Consultoría en Equipo y Prog.", "248.867.867", "43,29%", "217.265.140", "84,10%", "-31.602.727", "-12,70%"],
[" Devoluciones, Rebajas y Desc.", "0", "0,00%", "-10.976.064", "-4,25%", "-10.976.064", "0,00%"],
["INGRESOS NO OPERACIONALES", "287.408.232", "50,00%", "26.810.434", "10,38%", "-260.597.798", "-90,67%"],
[" Financieros", "285.287.023", "49,63%", "26.524.714", "10,27%", "-258.762.309", "-90,70%"],
["TOTAL INGRESOS", "574.861.805", "100,00%", "258.335.224", "100,00%", "-316.526.581", "-55,06%"],
["GASTOS OPERACIONALES ADM.", "127.904.325", "22,25%", "123.566.152", "47,83%", "-4.338.173", "-3,39%"],
[" Gastos de Personal", "40.650.874", "7,07%", "36.907.766", "14,29%", "-3.743.108", "-9,21%"],
["GASTOS OPERACIONALES VENTAS", "189.015.743", "32,88%", "186.856.485", "72,33%", "-2.159.258", "-1,14%"],
[" Gastos de Personal (Ventas)", "188.154.743", "32,73%", "185.914.840", "71,97%", "-2.239.903", "-1,19%"],
["GASTOS NO OPERACIONALES", "184.784.045", "32,14%", "51.690.001", "20,01%", "-133.094.044", "-72,03%"],
["TOTAL GASTOS", "501.074.113", "87,16%", "362.112.639", "140,17%", "-138.961.474", "-27,73%"],
["TOTAL COSTOS DE VENTAS", "3.701.440", "0,64%", "7.810.937", "3,02%", "4.109.497", "111,02%"],
["UTILIDAD / PÉRDIDA NETAS", "69.456.252", "12,08%", "-111.588.352", "-43,20%", "-181.044.604", "-260,66%"]
]
# Procesar celdas con párrafos para asegurar empaquetado correcto
table_data = []
for i, row in enumerate(raw_data):
row_cells = []
for cell in row:
style = table_header if i == 0 else table_text
row_cells.append(Paragraph(str(cell), style))
table_data.append(row_cells)
# Definición de anchos de columna (Suma exacta del ancho disponible: 504 puntos)
col_widths = [154, 65, 40, 65, 40, 70, 70]
fin_table = Table(table_data, colWidths=col_widths, repeatRows=1)
fin_table.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), PRIMARY),
('ALIGN', (0,0), (-1,-1), 'LEFT'),
('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
('BOTTOMPADDING', (0,0), (-1,-1), 4),
('TOPPADDING', (0,0), (-1,-1), 4),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BG_LIGHT]),
('GRID', (0,0), (-1,-1), 0.5, colors.HexColor("#dddddd")),
('LINEBELOW', (0,-1), (-1,-1), 1.5, PRIMARY),
]))
story.append(fin_table)
story.append(Spacer(1, 15))
# --- SECCIÓN 2: BALANCES Y RATIOS ---
story.append(Paragraph("2. Hallazgos Patrimoniales y Ratios de Rendimiento", h1_style))
story.append(Paragraph("• <b>Erosión del Patrimonio:</b> La transición de una utilidad neta positiva a una pérdida de COP -111,58M debilita directamente los fondos propios de la compañía, reduciendo su capacidad de autofinanciación.", body_style))
story.append(Paragraph("• <b>Margen de Utilidad Neto:</b> Cayó críticamente del <b>+12,08%</b> al <b>-43,20%</b>. La alta dependencia previa de ingresos no operativos apalancó artificialmente el mes de abril.", body_style))
story.append(Paragraph("• <b>Inelasticidad de Costos Fixos:</b> Los gastos totales de personal equivalen al 86,25% de los ingresos en mayo, mostrando una falta de flexibilidad estructural ante caídas de ventas.", body_style))
story.append(Spacer(1, 10))
story.append(PageBreak()) # Saltamos de página para mantener orden limpio
# --- SECCIÓN 3: PROYECCIONES Y RECOMENDACIONES ---
story.append(Paragraph("3. Proyección Inercial (Junio 2026) y Plan de Acción", h1_style))
story.append(Paragraph("Si la compañía mantiene el comportamiento actual sin intervenciones operacionales directas, se estima el siguiente escenario base:", body_style))
proj_data = [
[Paragraph("<b>Indicador Proyectado</b>", table_header), Paragraph("<b>Valor Estimado (COP)</b>", table_header)],
[Paragraph("Ingresos Operacionales Netos", table_text), Paragraph("255.000.000", table_text)],
[Paragraph("Ingresos No Operacionales Mínimos", table_text), Paragraph("28.000.000", table_text)],
[Paragraph("Egresos Totales Consolidados", table_text), Paragraph("340.000.000", table_text)],
[Paragraph("<b>Resultado Neto Estimado (Déficit)</b>", table_text), Paragraph("<b>-57.000.000</b>", table_text)]
]
proj_table = Table(proj_data, colWidths=[250, 150])
proj_table.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), SECONDARY),
('GRID', (0,0), (-1,-1), 0.5, colors.HexColor("#dddddd")),
('ROWBACKGROUNDS', (0,1), (-1,-1), [colors.white, BG_LIGHT]),
('PADDING', (0,0), (-1,-1), 6),
]))
story.append(proj_table)
story.append(Spacer(1, 15))
story.append(Paragraph("Recomendaciones Estratégicas del Analista:", h1_style))
story.append(Paragraph("1. <b>Flexibilización de la Nómina Comercial:</b> Es imperativo migrar el costo de personal de ventas hacia un modelo variable atado estrictamente al volumen de consultorías cerradas.", body_style))
To embed this project on your website, copy the following code and paste it into your website's HTML: