Skip to main content

Overview

The PDF Report Generation feature creates professional, downloadable attendance reports containing complete employee check-in history, tardiness tracking, and cumulative statistics. Built with ReportLab, the system generates structured PDF documents that can be printed, archived, or shared with stakeholders.
Reports are generated on-demand and include all historical attendance data from the system, not just current day records.

Key Features

Complete History

Every attendance record with username, date, time, and status

Automatic Status Detection

Late arrivals (after 8:30 AM) automatically flagged as “RETARDO”

Statistical Summary

Cumulative tardiness count by employee at end of report

Instant Download

PDF generated in-memory and streamed directly to browser

How to Generate Reports

1

Access Admin Dashboard

Log in with admin credentials and navigate to the dashboard at /monitor
2

Click PDF Button

Click the ”📥 Reporte PDF” button in the dashboard header
3

Automatic Generation

System generates PDF in-memory (typically takes 1-3 seconds)
4

Browser Download

PDF automatically downloads as reporte_asistencia.pdf to your default downloads folder

Direct URL Access

Reports can also be generated directly via URL:
GET /reporte-pdf
This endpoint requires authentication. Users must be logged in or will be redirected to the login page.

Technical Implementation

Backend Route Handler

The complete PDF generation logic in app.py:184-231:
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from io import BytesIO

@app.route('/reporte-pdf')
@login_required
def reporte_pdf():
    registros = leer_json(REGISTROS_FILE)
    buffer = BytesIO()
    p = canvas.Canvas(buffer, pagesize=letter)
    p.setTitle("Reporte de Asistencia Maestros")
    
    # Header
    p.drawString(100, 750, f"REPORTE DE ASISTENCIA - GENERADO: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    p.line(100, 745, 520, 745)
    
    # Table headers
    y = 710
    p.drawString(100, y, "Maestro")
    p.drawString(200, y, "Fecha")
    p.drawString(300, y, "Hora")
    p.drawString(400, y, "Estado")
    y -= 25
    
    # Track tardiness statistics
    stats = {}
    
    # Iterate through all records
    for r in registros:
        hora = r.get('hora', '00:00:00')
        estado = "RETARDO" if hora > "08:30:00" else "PUNTUAL"
        p.drawString(100, y, str(r.get('usuario', 'S/N')))
        p.drawString(200, y, str(r.get('fecha', 'S/F')))
        p.drawString(300, y, str(hora))
        p.drawString(400, y, estado)
        
        # Count tardiness
        if estado == "RETARDO":
            usr = r.get('usuario', 'S/N')
            stats[usr] = stats.get(usr, 0) + 1
        
        y -= 15
        if y < 80:  # Page break if near bottom
            p.showPage()
            y = 750

    # Summary section
    y -= 40
    p.drawString(100, y, "RESUMEN TOTAL DE RETARDOS ACUMULADOS:")
    p.line(100, y-5, 380, y-5)
    y -= 25
    for user, count in stats.items():
        p.drawString(120, y, f"• {user}: {count} retardos.")
        y -= 15

    p.save()
    buffer.seek(0)
    return send_file(buffer, as_attachment=True, download_name="reporte_asistencia.pdf", mimetype='application/pdf')

Report Structure

Page Layout

The PDF uses standard US Letter page size (8.5” x 11”):
Position: Top of page (y=750)
p.drawString(100, 750, f"REPORTE DE ASISTENCIA - GENERADO: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
p.line(100, 745, 520, 745)  # Horizontal line separator
Includes:
  • Report title: “REPORTE DE ASISTENCIA”
  • Generation timestamp in format: YYYY-MM-DD HH:MM:SS
  • Horizontal separator line

What’s Included

Complete Attendance Records

Every entry from registros.json appears in the report:
[
  {
    "usuario": "empleado1",
    "lat": 19.432608,
    "lon": -99.133209,
    "fecha": "2026-03-05",
    "hora": "08:45:00"
  },
  {
    "usuario": "empleado2",
    "lat": 19.411234,
    "lon": -99.145678,
    "fecha": "2026-03-05",
    "hora": "08:15:00"
  }
]
Renders as:
MaestroFechaHoraEstado
empleado12026-03-0508:45:00RETARDO
empleado22026-03-0508:15:00PUNTUAL
GPS coordinates (lat/lon) are not included in the PDF report. Only username, date, time, and status appear.

Tardiness Detection

The same 8:30 AM threshold used in the Admin Dashboard:
hora = r.get('hora', '00:00:00')
estado = "RETARDO" if hora > "08:30:00" else "PUNTUAL"

PUNTUAL

Check-in at or before 08:30:00

RETARDO

Check-in after 08:30:00 (late arrival)

Statistical Summary

The report concludes with cumulative tardiness by employee:
stats = {}
for r in registros:
    hora = r.get('hora', '00:00:00')
    estado = "RETARDO" if hora > "08:30:00" else "PUNTUAL"
    
    if estado == "RETARDO":
        usr = r.get('usuario', 'S/N')
        stats[usr] = stats.get(usr, 0) + 1

# Print summary
for user, count in stats.items():
    p.drawString(120, y, f"• {user}: {count} retardos.")
    y -= 15
Example output:
RESUMEN TOTAL DE RETARDOS ACUMULADOS:
─────────────────────────────────────
• empleado1: 5 retardos.
• empleado3: 2 retardos.
Employees with zero tardiness do not appear in the summary section.

Pagination

Automatic Page Breaks

The system prevents content from running off the page:
y -= 15  # Move down 15 points for next row
if y < 80:  # If near bottom margin
    p.showPage()  # Create new page
    y = 750  # Reset to top of new page
Approximate records per page: 40-45 entriesCalculation:
  • Starting y: 685 (after headers)
  • Ending y: 80 (bottom margin)
  • Row height: 15 points
  • Capacity: (685 - 80) / 15 ≈ 40 rows
Limitation: Table headers do not repeat on new pages. Continuation pages show data rows without column labels.Consider adding repeating headers for better readability:
if y < 80:
    p.showPage()
    y = 750
    # Redraw headers
    p.drawString(100, y, "Maestro")
    p.drawString(200, y, "Fecha")
    # ... etc
    y -= 25

Download Behavior

In-Memory Generation

The PDF is generated entirely in memory without creating temporary files:
from io import BytesIO

buffer = BytesIO()  # Create in-memory file-like object
p = canvas.Canvas(buffer, pagesize=letter)
# ... generate PDF content ...
p.save()  # Write to buffer
buffer.seek(0)  # Reset read position to start
Benefits:
  • No disk I/O overhead
  • No cleanup required
  • Thread-safe for concurrent requests
  • Works in containerized environments without persistent storage

File Download

Flask’s send_file() streams the PDF to the browser:
return send_file(
    buffer, 
    as_attachment=True,  # Force download instead of inline display
    download_name="reporte_asistencia.pdf",  # Default filename
    mimetype='application/pdf'
)

as_attachment=True

Browser downloads the file instead of opening in a new tab

download_name

Sets default filename in download dialog (user can rename)

mimetype

Tells browser this is a PDF document for proper handling

buffer

In-memory BytesIO object streamed directly to response

Use Cases

Scenario: End-of-day attendance archivingWorkflow:
  1. Admin reviews dashboard at 5 PM
  2. Generates PDF report
  3. Saves to network drive or prints for physical filing
  4. Shares via email with HR department

Customization Examples

Add Page Numbers

page_number = 1

# In main loop
if y < 80:
    p.drawString(500, 50, f"Página {page_number}")  # Bottom right
    p.showPage()
    page_number += 1
    y = 750

# After final page
p.drawString(500, 50, f"Página {page_number}")
from reportlab.lib.utils import ImageReader

# At top of report
logo = ImageReader('static/logo.png')
p.drawImage(logo, 50, 730, width=40, height=40, mask='auto')
p.drawString(100, 750, "REPORTE DE ASISTENCIA")

Color-Coded Status

from reportlab.lib.colors import green, red

if estado == "RETARDO":
    p.setFillColor(red)
else:
    p.setFillColor(green)
p.drawString(400, y, estado)
p.setFillColor(black)  # Reset for next row

Filter by Date Range

from datetime import datetime, timedelta

# Get records from last 7 days
hoy = datetime.now()
inicio = (hoy - timedelta(days=7)).strftime("%Y-%m-%d")

registros_filtrados = [
    r for r in registros 
    if r.get('fecha', '') >= inicio
]

# Use registros_filtrados instead of registros

Add GPS Coordinates

# Adjust table headers
p.drawString(100, y, "Maestro")
p.drawString(200, y, "Fecha")
p.drawString(280, y, "Hora")
p.drawString(350, y, "Ubicación")  # New column
p.drawString(480, y, "Estado")

# In data rows
for r in registros:
    # ... existing code ...
    ubicacion = f"{r.get('lat', 0):.4f}, {r.get('lon', 0):.4f}"
    p.drawString(350, y, ubicacion)
    # ... existing code ...

Performance Considerations

Challenge: Reports with thousands of recordsImpact:
  • Generation time increases (5-10 seconds for 1000+ records)
  • Memory usage grows (10-20 MB in memory)
  • Browser may show “file downloading” for longer
Optimization strategies:
  • Add date range filters to limit records
  • Implement pagination (separate reports by month)
  • Consider switching to Excel format for very large exports
  • Add loading indicator on admin dashboard
Scenario: Multiple admins generating reports simultaneouslyBehavior:
  • Each request creates independent BytesIO buffer
  • No shared state between requests
  • Thread-safe by design
Potential issues:
  • High memory usage if many concurrent reports
  • CPU spikes during ReportLab rendering
Mitigation:
  • Implement rate limiting on /reporte-pdf endpoint
  • Cache recently generated reports (5-minute TTL)
  • Use background task queue for report generation

Error Handling

Missing Data Fields

The code uses .get() with fallback values:
usuario = r.get('usuario', 'S/N')    # "S/N" = Sin Nombre (No Name)
fecha = r.get('fecha', 'S/F')        # "S/F" = Sin Fecha (No Date)
hora = r.get('hora', '00:00:00')
Malformed records will appear in the PDF with placeholder values instead of causing errors.

Empty Records File

If registros.json is empty or missing:
registros = leer_json(REGISTROS_FILE)  # Returns [] if file empty/missing
Result: PDF generates successfully with:
  • Header and table columns
  • No data rows
  • Empty summary section
  • Message: “No records found” can be added

Integration with Other Features

The PDF reporting system leverages data from:

Best Practices

For Effective Report Usage:✅ Generate reports at consistent intervals (daily, weekly, monthly)
✅ Store PDFs in organized folder structure by date
✅ Review tardiness summary section for trends
✅ Cross-reference with payroll and HR systems
✅ Print and file reports for compliance requirements
✅ Backup digital copies to cloud storage
❌ Don’t rely solely on screen viewing (download for archives)
❌ Don’t share reports containing sensitive employee data insecurely
❌ Don’t generate excessive reports unnecessarily (wastes resources)

Troubleshooting

Symptoms: File downloads successfully but shows “corrupted” error in PDF readerPossible causes:
  • p.save() not called before buffer.seek(0)
  • Exception occurred during generation (partial write)
  • ReportLab not installed or outdated
Solutions:
  • Check server logs for Python exceptions
  • Verify ReportLab version: pip show reportlab
  • Test PDF generation in Python shell
  • Ensure buffer.seek(0) is called before send_file()
Symptoms: Long delay before download startsPossible causes:
  • Thousands of attendance records
  • Multiple page breaks and pagination
  • Slow disk I/O when reading registros.json
Solutions:
  • Add date filters to limit records
  • Optimize JSON file reading (consider database migration)
  • Implement caching for recently generated reports
  • Add loading spinner on frontend
Symptoms: Dashboard shows more records than appear in PDFPossible causes:
  • registros.json not synchronized (cached old version)
  • Records added after PDF generation started
  • Pagination bug causing data loss
Solutions:
  • Refresh dashboard before generating report
  • Check file modification timestamp of registros.json
  • Review pagination logic for off-by-one errors
  • Add record count verification in PDF footer

Future Enhancements

Date Range Filters

Allow admins to specify start/end dates for custom report periods

Excel Export

Alternative format with formulas, pivot tables, and charts

Email Delivery

Automatically email reports to stakeholders on schedule

Visual Charts

Embed Chart.js graphs directly in PDF (requires additional library)

Build docs developers (and LLMs) love