2

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!

Diego Gc
  • 175
  • 2
  • 15
  • Check out this answer: https://stackoverflow.com/a/9287835/42346 – mechanical_meat Feb 24 '21 at 22:24
  • Regarding the order of the columns, the rearrangement is happening because you have a `set` inside of a list. Sets in Python are unordered. You can just use a `list` inside of a `list` instead. – mechanical_meat Feb 24 '21 at 22:25
  • Hello @mechanical_meat thanks for your reply. Although I added repeatRows=1. As suggested on the answer you suggested, I still can't get the pdf to add a new page. I edited my code to show this. On the other hand, I'm not quite sure of what you mean by "you have a ser inside of a list". Can you please give me further explanation? Thanks a lot in advance! – Diego Gc Feb 24 '21 at 23:38

1 Answers1

6

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

Andrewgmz
  • 355
  • 3
  • 11
  • Thanks for your response @Andrewgmz I have one last question for you. How can I add the header of the page (Logo, Title, nombre_colaborador, sucural_colaborador, etc. on every page? (So it is repeated at the top of every page in the pfd.) Thanks a lot in advance! – Diego Gc Feb 25 '21 at 15:58
  • 1
    @DiegoGc I already edited the answer, with the question you asked me. – Andrewgmz Feb 25 '21 at 19:28
  • thanks a lot, your answer made exactly what I needed. Nevertheless, the column titles are drawn in disorder on the tables. Any idea of how to solve this? Thanks a lot again, I'm sorry for any inconvenience. – Diego Gc Feb 25 '21 at 22:24
  • 1
    You must order the variable 'data_activities' in the way you want and also take into account how you add the records data_actividades = [ ['Monto Cotización \n(USD)', 'Cliente','Comentarios \nAdicionales', 'Monto Cotización \n(MXN)', 'Monto Venta \n(USD)', 'Hora', 'Monto Venta \n(MXN)', 'Resultado de \nActividad'], ] – Andrewgmz Feb 26 '21 at 12:37