Search code examples
pythondatatablepdf-generationreportlab

Python Reportlab divide table to fit into different pages


I am trying to build a schedule planner, in a PDF file generated with ReportLab. The schedule will have a different rows depending on the hour of the day: starting with 8:00 a.m., 8:15 a.m., 8:30 a.m., and so on.

I made a loop in which the hours will be calculated automatically and the schedule will be filled. However, since my table is too long, it doesn't fit completely in the page. (Although the schedule should end on 7:30 p.m., it is cutted at 2:00 p.m.)

The desired result is to have a PageBreak when the table is at around 20 activities. On the next page, the header should be exactly the same as in the first page and below, the continuation of the table. The process should repeat every time it is necessary, until the end of the table.

enter image description here

The Python code is the following:

from reportlab.pdfgen.canvas import Canvas
from datetime import datetime, timedelta
from reportlab.platypus import Table, TableStyle
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter, landscape


class Vendedor:
    """
    Información del Vendedor: Nombre, sucursal, meta de venta
    """
    def __init__(self, nombre_vendedor, sucursal, dia_reporte):
        self.nombre_vendedor = nombre_vendedor
        self.sucursal = sucursal
        self.dia_reporte = dia_reporte


class Actividades:
    """
    Información de las Actividades realizadas: Hora de actividad y duración, cliente atendido,
    tipo de actividad, resultado, monto venta (mxn) + (usd), monto cotización (mxn) + (usd),
    solicitud de apoyo y comentarios adicionales
    """
    def __init__(self, hora_actividad, duracion_actividad, cliente, tipo_actividad, resultado,
                 monto_venta_mxn, monto_venta_usd, monto_cot_mxn, monto_cot_usd, requiero_apoyo, comentarios_extra):
        self.hora_actividad = hora_actividad
        self.duracion_actividad = duracion_actividad
        self.cliente = cliente
        self.tipo_actividad = tipo_actividad
        self.resultado = resultado
        self.monto_venta_mxn = monto_venta_mxn
        self.monto_venta_usd = monto_venta_usd
        self.monto_cot_mxn = monto_cot_mxn
        self.monto_cot_usd = monto_cot_usd
        self.requiero_apoyo = requiero_apoyo
        self.comentarios_extra = comentarios_extra


class PDFReport:
    """
    Crea el Reporte de Actividades diarias en archivo de formato PDF
    """
    def __init__(self, filename):
        self.filename = filename


vendedor = Vendedor('John Doe', 'Stack Overflow', datetime.now().strftime('%d/%m/%Y'))

file_name = 'cronograma_actividades.pdf'
document_title = 'Cronograma Diario de Actividades'
title = 'Cronograma Diario de Actividades'
nombre_colaborador = vendedor.nombre_vendedor
sucursal_colaborador = vendedor.sucursal
fecha_actual = vendedor.dia_reporte


canvas = Canvas(file_name)
canvas.setPageSize(landscape(letter))
canvas.setTitle(document_title)


canvas.setFont("Helvetica-Bold", 20)
canvas.drawCentredString(385+100, 805-250, title)
canvas.setFont("Helvetica", 16)
canvas.drawCentredString(385+100, 785-250, nombre_colaborador + ' - ' + sucursal_colaborador)
canvas.setFont("Helvetica", 14)
canvas.drawCentredString(385+100, 765-250, fecha_actual)

title_background = colors.fidblue
hour = 8
minute = 0
hour_list = []

data_actividades = [
    {'Hora', 'Cliente', 'Resultado de \nActividad', 'Monto Venta \n(MXN)', 'Monto Venta \n(USD)',
     'Monto Cotización \n(MXN)', 'Monto Cotización \n(USD)', 'Comentarios \nAdicionales'},
]

i = 0
for i in range(47):

    if minute == 0:
        if hour <= 12:
            time = str(hour) + ':' + str(minute) + '0 a.m.'
        else:
            time = str(hour-12) + ':' + str(minute) + '0 p.m.'
    else:
        if hour <= 12:
            time = str(hour) + ':' + str(minute) + ' a.m.'
        else:
            time = str(hour-12) + ':' + str(minute) + ' p.m.'

    if minute != 45:
        minute += 15
    else:
        hour += 1
        minute = 0
    hour_list.append(time)

    # I TRIED THIS SOLUTION BUT THIS DIDN'T WORK
    # if i % 20 == 0:
    #     canvas.showPage()

    data_actividades.append([hour_list[i], i, i, i, i, i, i, i])

    i += 1

    table_actividades = Table(data_actividades, colWidths=85, rowHeights=30, repeatRows=1)
    tblStyle = TableStyle([
        ('BACKGROUND', (0, 0), (-1, 0), title_background),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
        ('ALIGN', (1, 0), (1, -1), 'CENTER'),
        ('GRID', (0, 0), (-1, -1), 1, colors.black)
    ])

    rowNumb = len(data_actividades)
    for row in range(1, rowNumb):
        if row % 2 == 0:
            table_background = colors.lightblue
        else:
            table_background = colors.aliceblue

        tblStyle.add('BACKGROUND', (0, row), (-1, row), table_background)

    table_actividades.setStyle(tblStyle)

    width = 150
    height = 150
    table_actividades.wrapOn(canvas, width, height)
    table_actividades.drawOn(canvas, 65, (0 - height) - 240)

canvas.save()

I tried by adding:

if i % 20 == 0:
    canvas.showPage()

However this failed to achieve the desired result.

Other quick note: Although I specifically coded the column titles of the table. Once I run the program, the order of the column titles is modified for some reason (see the pasted image). Any idea of why this is happening?

data_actividades = [
    {'Hora', 'Cliente', 'Resultado de \nActividad', 'Monto Venta \n(MXN)', 'Monto Venta \n(USD)',
     'Monto Cotización \n(MXN)', 'Monto Cotización \n(USD)', 'Comentarios \nAdicionales'},
]

Thank you very much in advance, have a great day!


Solution

  • You should use templates, as suggested in the Chapter 5 "PLATYPUS - Page Layout and TypographyUsing Scripts" of the official documentation.

    The basic idea is to use frames, and add to a list element all the information you want to add. In my case I call it "contents", with the command "contents.append(FrameBreak())" you leave the frame and work on the next one, on the other hand if you want to change the type of template you use the command " contents.append(NextPageTemplate('<template_name>'))"

    My proposal:

    For your case I used two templates, the first one is the one that contains the header with the sheet information and the first part of the table, and the other template corresponds to the rest of the content. The name of these templates is firstpage and laterpage.The code is as follows:

    from reportlab.pdfgen.canvas import Canvas
    from datetime import datetime, timedelta
    from reportlab.platypus import Table, TableStyle
    from reportlab.lib import colors
    from reportlab.lib.pagesizes import letter, landscape
    from reportlab.platypus import BaseDocTemplate, Frame, Paragraph, PageBreak, \
        PageTemplate, Spacer, FrameBreak, NextPageTemplate, Image
    from reportlab.lib.pagesizes import letter,A4
    from reportlab.lib.units import inch, cm
    from reportlab.lib.styles import getSampleStyleSheet
    from reportlab.lib.enums import TA_JUSTIFY, TA_CENTER,TA_LEFT,TA_RIGHT
    class Vendedor:
        """
        Información del Vendedor: Nombre, sucursal, meta de venta
        """
        def __init__(self, nombre_vendedor, sucursal, dia_reporte):
            self.nombre_vendedor = nombre_vendedor
            self.sucursal = sucursal
            self.dia_reporte = dia_reporte
    
    
    class Actividades:
        """
        Información de las Actividades realizadas: Hora de actividad y duración, cliente atendido,
        tipo de actividad, resultado, monto venta (mxn) + (usd), monto cotización (mxn) + (usd),
        solicitud de apoyo y comentarios adicionales
        """
        def __init__(self, hora_actividad, duracion_actividad, cliente, tipo_actividad, resultado,
                     monto_venta_mxn, monto_venta_usd, monto_cot_mxn, monto_cot_usd, requiero_apoyo, comentarios_extra):
            self.hora_actividad = hora_actividad
            self.duracion_actividad = duracion_actividad
            self.cliente = cliente
            self.tipo_actividad = tipo_actividad
            self.resultado = resultado
            self.monto_venta_mxn = monto_venta_mxn
            self.monto_venta_usd = monto_venta_usd
            self.monto_cot_mxn = monto_cot_mxn
            self.monto_cot_usd = monto_cot_usd
            self.requiero_apoyo = requiero_apoyo
            self.comentarios_extra = comentarios_extra
    
    class PDFReport:
        """
        Crea el Reporte de Actividades diarias en archivo de formato PDF
        """
        def __init__(self, filename):
            self.filename = filename
    
    
    vendedor = Vendedor('John Doe', 'Stack Overflow', datetime.now().strftime('%d/%m/%Y'))
    
    file_name = 'cronograma_actividades.pdf'
    document_title = 'Cronograma Diario de Actividades'
    title = 'Cronograma Diario de Actividades'
    nombre_colaborador = vendedor.nombre_vendedor
    sucursal_colaborador = vendedor.sucursal
    fecha_actual = vendedor.dia_reporte
    
    
    canvas = Canvas(file_name, pagesize=landscape(letter))
    
    doc = BaseDocTemplate(file_name)
    contents =[]
    width,height = A4
    
    left_header_frame = Frame(
        0.2*inch, 
        height-1.2*inch, 
        2*inch, 
        1*inch
        )
    
    right_header_frame = Frame(
        2.2*inch, 
        height-1.2*inch, 
        width-2.5*inch, 
        1*inch,id='normal'
        )
    
    frame_later = Frame(
        0.2*inch, 
        0.6*inch, 
        (width-0.6*inch)+0.17*inch, 
        height-1*inch,
        leftPadding = 0, 
        topPadding=0, 
        showBoundary = 1,
        id='col'
        )
    
    frame_table= Frame(
        0.2*inch, 
        0.7*inch, 
        (width-0.6*inch)+0.17*inch, 
        height-2*inch,
        leftPadding = 0, 
        topPadding=0, 
        showBoundary = 1,
        id='col'
        )
    laterpages = PageTemplate(id='laterpages',frames=[frame_later])
    
    firstpage = PageTemplate(id='firstpage',frames=[left_header_frame, right_header_frame,frame_table],)
    
    contents.append(NextPageTemplate('firstpage'))
    logoleft = Image('logo_power.png')
    logoleft._restrictSize(1.5*inch, 1.5*inch)
    logoleft.hAlign = 'CENTER'
    logoleft.vAlign = 'CENTER'
    
    contents.append(logoleft)
    contents.append(FrameBreak())
    styleSheet = getSampleStyleSheet()
    style_title = styleSheet['Heading1']
    style_title.fontSize = 20 
    style_title.fontName = 'Helvetica-Bold'
    style_title.alignment=TA_CENTER
    
    style_data = styleSheet['Normal']
    style_data.fontSize = 16 
    style_data.fontName = 'Helvetica'
    style_data.alignment=TA_CENTER
    
    style_date = styleSheet['Normal']
    style_date.fontSize = 14
    style_date.fontName = 'Helvetica'
    style_date.alignment=TA_CENTER
    
    canvas.setTitle(document_title)
    
    contents.append(Paragraph(title, style_title))
    contents.append(Paragraph(nombre_colaborador + ' - ' + sucursal_colaborador, style_data))
    contents.append(Paragraph(fecha_actual, style_date))
    contents.append(FrameBreak())
    
    title_background = colors.fidblue
    hour = 8
    minute = 0
    hour_list = []
    
    data_actividades = [
        {'Hora', 'Cliente', 'Resultado de \nActividad', 'Monto Venta \n(MXN)', 'Monto Venta \n(USD)',
         'Monto Cotización \n(MXN)', 'Monto Cotización \n(USD)', 'Comentarios \nAdicionales'},
    ]
    
    i = 0
    for i in range(300):
    
        if minute == 0:
            if hour <= 12:
                time = str(hour) + ':' + str(minute) + '0 a.m.'
            else:
                time = str(hour-12) + ':' + str(minute) + '0 p.m.'
        else:
            if hour <= 12:
                time = str(hour) + ':' + str(minute) + ' a.m.'
            else:
                time = str(hour-12) + ':' + str(minute) + ' p.m.'
    
        if minute != 45:
            minute += 15
        else:
            hour += 1
            minute = 0
        hour_list.append(time)
    
        # I TRIED THIS SOLUTION BUT THIS DIDN'T WORK
        # if i % 20 == 0:
        
    
        data_actividades.append([hour_list[i], i, i, i, i, i, i, i])
    
        i += 1
    
        table_actividades = Table(data_actividades, colWidths=85, rowHeights=30, repeatRows=1)
        tblStyle = TableStyle([
            ('BACKGROUND', (0, 0), (-1, 0), title_background),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
            ('ALIGN', (1, 0), (1, -1), 'CENTER'),
            ('GRID', (0, 0), (-1, -1), 1, colors.black)
        ])
    
        rowNumb = len(data_actividades)
        for row in range(1, rowNumb):
            if row % 2 == 0:
                table_background = colors.lightblue
            else:
                table_background = colors.aliceblue
    
            tblStyle.add('BACKGROUND', (0, row), (-1, row), table_background)
    
        table_actividades.setStyle(tblStyle)
    
        width = 150
        height = 150
        
    contents.append(NextPageTemplate('laterpages'))
    contents.append(table_actividades)
    
    
    contents.append(PageBreak())
    
    
    doc.addPageTemplates([firstpage,laterpages])
    doc.build(contents)
    

    Results

    With this you can add as many records as you want, I tried with 300. The table is not fully visible because for my convenience I made an A4 size pdf. However, the principle is the same for any size so you must play with the size of the frames and the size of the pdf page.

    enter image description here enter image description here

    EXTRA, add header on each page

    since only one template will be needed now, the "first_page" template should be removed since it will be the same for all pages. In the same way that you proposed in the beginning I cut the table every 21 records (to include the header of the table) and it is grouped in a list that then iterates adding the header with the logo in each cycle. Also it is included in the logical cutting sentence, the case when the number of records does not reach 21 but the number of records is going to end. The code is as follows:

    canvas = Canvas(file_name, pagesize=landscape(letter))
    
    doc = BaseDocTemplate(file_name)
    contents =[]
    width,height = A4
    
    left_header_frame = Frame(
        0.2*inch, 
        height-1.2*inch, 
        2*inch, 
        1*inch
        )
    
    right_header_frame = Frame(
        2.2*inch, 
        height-1.2*inch, 
        width-2.5*inch, 
        1*inch,id='normal'
        )
    
    frame_table= Frame(
        0.2*inch, 
        0.7*inch, 
        (width-0.6*inch)+0.17*inch, 
        height-2*inch,
        leftPadding = 0, 
        topPadding=0, 
        showBoundary = 1,
        id='col'
        )
    
    laterpages = PageTemplate(id='laterpages',frames=[left_header_frame, right_header_frame,frame_table],)
    
    logoleft = Image('logo_power.png')
    logoleft._restrictSize(1.5*inch, 1.5*inch)
    logoleft.hAlign = 'CENTER'
    logoleft.vAlign = 'CENTER'
    
    
    styleSheet = getSampleStyleSheet()
    style_title = styleSheet['Heading1']
    style_title.fontSize = 20 
    style_title.fontName = 'Helvetica-Bold'
    style_title.alignment=TA_CENTER
    
    style_data = styleSheet['Normal']
    style_data.fontSize = 16 
    style_data.fontName = 'Helvetica'
    style_data.alignment=TA_CENTER
    
    style_date = styleSheet['Normal']
    style_date.fontSize = 14
    style_date.fontName = 'Helvetica'
    style_date.alignment=TA_CENTER
    
    canvas.setTitle(document_title)
    
    
    title_background = colors.fidblue
    hour = 8
    minute = 0
    hour_list = []
    
    data_actividades = [
        {'Hora', 'Cliente', 'Resultado de \nActividad', 'Monto Venta \n(MXN)', 'Monto Venta \n(USD)',
         'Monto Cotización \n(MXN)', 'Monto Cotización \n(USD)', 'Comentarios \nAdicionales'},
    ]
    
    i = 0
    table_group= []
    size = 304
    
    count = 0
    for i in range(size):
    
        if minute == 0:
            if hour <= 12:
                time = str(hour) + ':' + str(minute) + '0 a.m.'
            else:
                time = str(hour-12) + ':' + str(minute) + '0 p.m.'
        else:
            if hour <= 12:
                time = str(hour) + ':' + str(minute) + ' a.m.'
            else:
                time = str(hour-12) + ':' + str(minute) + ' p.m.'
    
        if minute != 45:
            minute += 15
        else:
            hour += 1
            minute = 0
        hour_list.append(time)    
    
        data_actividades.append([hour_list[i], i, i, i, i, i, i, i])
    
        i += 1
    
        table_actividades = Table(data_actividades, colWidths=85, rowHeights=30, repeatRows=1)
        tblStyle = TableStyle([
            ('BACKGROUND', (0, 0), (-1, 0), title_background),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
            ('ALIGN', (1, 0), (1, -1), 'CENTER'),
            ('GRID', (0, 0), (-1, -1), 1, colors.black)
        ])
    
        rowNumb = len(data_actividades)
        for row in range(1, rowNumb):
            if row % 2 == 0:
                table_background = colors.lightblue
            else:
                table_background = colors.aliceblue
    
            tblStyle.add('BACKGROUND', (0, row), (-1, row), table_background)
    
        table_actividades.setStyle(tblStyle)
    
        if ((count >= 20) or (i== size) ):
            count = 0
            table_group.append(table_actividades)
            data_actividades = [
                {'Hora', 'Cliente', 'Resultado de \nActividad', 'Monto Venta \n(MXN)', 'Monto Venta \n(USD)',
                'Monto Cotización \n(MXN)', 'Monto Cotización \n(USD)', 'Comentarios \nAdicionales'},]
        width = 150
        height = 150
        count += 1
        if i > size:
    
            break
    
    contents.append(NextPageTemplate('laterpages'))
    
    for table in table_group:
    
        contents.append(logoleft)
        contents.append(FrameBreak())
        contents.append(Paragraph(title, style_title))
        contents.append(Paragraph(nombre_colaborador + ' - ' + sucursal_colaborador, style_data))
        contents.append(Paragraph(fecha_actual, style_date))
        contents.append(FrameBreak()) 
        contents.append(table)
        contents.append(FrameBreak())
    
    doc.addPageTemplates([laterpages,])
    doc.build(contents)
    

    Extra - result:

    enter image description here