2

I tried to generate PDF invoices in Python using reportlab.

The invoices will be one page only and there will never be more details than there is space on that single page; my code checks max number of details before generating the PDF.

Right now I'm using SimpleDocTemplate to add everything to the page, and to structure the details I am using a Table. Here is a reduced code sample:

from reportlab.lib.units import mm
from reportlab.platypus import Paragraph, Spacer, Table, TableStyle
from reportlab.platypus import SimpleDocTemplate

# add invoice header
flowable_list = [
    Spacer(1, 5*mm),
    Paragraph('Date: ...', pg_style_1),
    Spacer(1, 5*mm),
]

# add invoice details
detail_list = [
    ('Item 1', 8, 45),
    ('Item 2', 1, 14),
]
row_list = [
    [
        Paragraph(desc, pg_style_1),
        quantity,
        amount,
    ]
    for desc, quantity, amount in detail_list]
story.append(
    Table(
        data=row_list,
        colWidths=[100*mm, 40*mm, 40*mm],
        style=TableStyle([
            ('VALIGN', (0, 0), (-1, -1), 'TOP'),
            ... some other options ...
        ])))

# add invoice footer; this should be at a specific position on the page
flowable_list.append(Spacer(1, 5*mm))
flowable_list.append(Paragraph('Total: 0', pg_style_1))

# build PDF
buffer = io.BytesIO()
doc = SimpleDocTemplate(buffer)
doc.build(flowable_list)

My problem: the total amounts at the bottom have to be at a specific location every time (something like x*mm from the bottom), but there can be a variable number of details which causes the details table to have a non-fixed height.

My current solution: adding a Spacer after the table; the height of this spacer has to be calculated based on the number of rows in the table (more rows mean that the spacer will be smaller; less rows produce a bigger spacer). But this fails if one of the rows wraps around and takes up more space than a single row.

My question: is there a way to set a fixed height for the details table, no matter how many rows there will be, but still keep using SimpleDocTemplate?

This similar question shows a solution that draws everything manually onto the canvas, but I would like a way to keep using the SimpleDocTemplate, if it is possible.

Ralf
  • 16,086
  • 4
  • 44
  • 68
  • What about invoice details (between header and footer) is split in two frames, a larger frame for the row list and a smaller frame for the total? Each frame contains a table, one for the details and another table for the total. Given frames, you can place them exact. However, I have asked a similar question though, what if I want to work from bottom and up in a frame instead of going from top to bottom. https://stackoverflow.com/questions/54836889/reportlab-align-valign-table-to-the-bottom-of-a-frame – Jaco Feb 23 '19 at 02:42
  • @Jaco how would that work with frames? I'm not entirely sure I understand. – Ralf Feb 25 '19 at 09:23
  • Hi Ralf, I thought of frames on a page and within frames you place tables. Instead of having the whole table in one frame, you split the table in two, one part with details and another part with the sum only (as you said you wanted the sum to be in the same pos on every page, unless misunderstood. Your answer yourself below looks on the surface similar to page 4 in this document, it is a class within Platypus.py called Top Padder. https://www.reportlab.com/examples/rml/test/test_008_tables.pdf. Maybe you can take a look at my question also, and see if you might have a solution to it, please. – Jaco Feb 25 '19 at 09:32
  • sorry, within flowables.py – Jaco Feb 25 '19 at 09:45

2 Answers2

2

Try this workable example utilizing the "TopPadder", (The result is that the Total is pushed to the bottom of the Frame, and I assume you can add an cushion below it, to control the height from the bottom):

########################################################################
from reportlab.lib.units import mm
from reportlab.platypus import Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import SimpleDocTemplate
from reportlab.platypus.flowables import TopPadder
import io

def create_pdf():

    styles=getSampleStyleSheet()

    # add invoice header
    flowable_list = [
        Spacer(1, 5 * mm),
        Paragraph(f'Date: ...', styles['Normal']),
        Spacer(1, 5 * mm),
    ]

    # add invoice details
    detail_list = [
        ('Item 1', 8, 45),
        ('Item 2', 1, 14),
    ]
    row_list = [
        [
            Paragraph(f'desc', styles['Normal']),
            # quantity,
            # amount,
        ]
        for desc, quantity, amount in detail_list]

    story = []
    story.append(
        Table(
            data=row_list,
            colWidths=[100 * mm, 40 * mm, 40 * mm],
            style=TableStyle([
                ('VALIGN', (0, 0), (-1, -1), 'TOP'),
                # ... some other options...
            ])))

    # add invoice footer; this should be at a specific position on the page
    flowable_list.append(Spacer(1, 5 * mm))
    flowable_list.append(TopPadder(Paragraph(f'Total: 0', styles['Normal'])))

    # build PDF
    buffer = io.BytesIO()
    doc = SimpleDocTemplate("test2.pdf")
    doc.build(flowable_list)

    # ----------------------------------------------------------------------
if __name__ == "__main__":
    create_pdf()  # Printing the pdf
Jaco
  • 1,564
  • 2
  • 9
  • 33
  • But coupled with your comments to the question, I think I understand what you are referring to to solve my problem. I might give it a try. Thanks. – Ralf Feb 25 '19 at 10:11
  • thanks for the update. This does seem possible this way. – Ralf Feb 25 '19 at 13:09
0

I have not found a better way yet, so I will add my current solution; maybe it will help some future reader with a similar problem.

...
story.append(
    Table(
        data=row_list,
        colWidths=[100*mm, 40*mm, 40*mm],
        style=TableStyle([
            ('VALIGN', (0, 0), (-1, -1), 'TOP'),
            ... some other options ...
        ])))

# calculate real height of details table, so we can add a 'Spacer' for the missing
# part to have the invoice totals at the correct height at the bottom of the page
_, real_height = story[-1].wrap(doc_width, doc_height)
height_reserved_for_details = 100*mm
remaining_height = max(0, height_reserved_for_details - real_height)
story.append(Spacer(1, remaining_height))

flowable_list.append(Paragraph('Total: 0', pg_style_1))
Ralf
  • 16,086
  • 4
  • 44
  • 68