My favorites | Sign in
Project Home Downloads Wiki Issues Source
Search
for
Web2Py  
web framework integration for PDF report and templates support
Featured, Phase-Support
Updated Sep 15, 2010 by reingart

Introduction

If you use web2py, you can make complex reports that can be viewed in a browser, or downloaded as PDF (taking advantage of web2py HTML helper objects to easily diagram a report). See WriteHTML for more information, supported tags and attributes, etc.

Also, using web2py DAL, you can easily set up a templating engine for PDF documents. See Templates for more information.

The following examples are packaged in a ready to run application: web2py.app.fpdf.w2p

PyFPDF is included in web2py since release 1.85.2

Also, you can download the latest versión pyfpdf and uncompress it in the web2py, gluon, contrib folder.

Note about images: this sample images are small so they may look like low quality ones. For better results, use bigger images: more DPI (screen is often 72/96DPI, printers are often 300/600DPI). As a rule of thumb, use at least half of the image size when rendering to PDF, ie. if image is 500x200px, use 250x100px as width/height attributes of IMG tag.

Sample Report

You could make a "professional looking" bussiness report just using web2py HTML helpers, mixin headers, logos, charts, text and tables.

Te main advantage of this method is that the same report can be rendered in a HTML view, or can be downloaded as PDF, with a minimal effort:

Sample: report.pdf

Updated Live Demo (HTML and PDF version):

def report():
    response.title = "web2py sample report"
    
    # include a google chart (download it dynamically!)
    url = "http://chart.apis.google.com/chart?cht=p3&chd=t:60,40&chs=500x200&chl=Hello|World&.png"
    chart = IMG(_src=url, _width="250",_height="100")

    # create a small table with some data:
    rows = [THEAD(TR(TH("Key",_width="70%"), TH("Value",_width="30%"))),
            TBODY(TR(TD("Hello"),TD("60")), 
                  TR(TD("World"),TD("40")))]
    table = TABLE(*rows, _border="0", _align="center", _width="50%")

    if request.extension=="pdf":
        from gluon.contrib.pyfpdf import FPDF, HTMLMixin

        # create a custom class with the required functionalities 
        class MyFPDF(FPDF, HTMLMixin):
            def header(self): 
                "hook to draw custom page header (logo and title)"
                logo=os.path.join(request.env.web2py_path,"gluon","contrib","pyfpdf","tutorial","logo_pb.png")
                self.image(logo,10,8,33)
                self.set_font('Arial','B',15)
                self.cell(65) # padding
                self.cell(60,10,response.title,1,0,'C')
                self.ln(20)
                
            def footer(self):
                "hook to draw custom page footer (printing page numbers)"
                self.set_y(-15)
                self.set_font('Arial','I',8)
                txt = 'Page %s of %s' % (self.page_no(), self.alias_nb_pages())
                self.cell(0,10,txt,0,0,'C')
                    
        pdf=MyFPDF()
        # create a page and serialize/render HTML objects
        pdf.add_page()
        pdf.write_html(str(XML(table, sanitize=False)))
        pdf.write_html(str(XML(CENTER(chart), sanitize=False)))
        # prepare PDF to download:
        response.headers['Content-Type']='application/pdf'
        return pdf.output(dest='S')
    else:
        # normal html view:
        return dict(chart=chart, table=table)

Sample Table Listing

Also, you can make nice tables that automatically spreads over several pages, with header/footers, column/row highlight, etc., in a very pythonic way:

Sample: listing.pdf

Updated Live Demo (HTML and PDF version):

def listing():
    response.title = "web2py sample listing"
    
    # define header and footers:
    head = THEAD(TR(TH("Header 1",_width="50%"), 
                    TH("Header 2",_width="30%"),
                    TH("Header 3",_width="20%"), 
                    _bgcolor="#A0A0A0"))
    foot = TFOOT(TR(TH("Footer 1",_width="50%"), 
                    TH("Footer 2",_width="30%"),
                    TH("Footer 3",_width="20%"),
                    _bgcolor="#E0E0E0"))
    
    # create several rows:
    rows = []
    for i in range(1000):
        col = i % 2 and "#F0F0F0" or "#FFFFFF"
        rows.append(TR(TD("Row %s" %i),
                       TD("something", _align="center"),
                       TD("%s" % i, _align="right"),
                       _bgcolor=col)) 

    # make the table object
    body = TBODY(*rows)
    table = TABLE(*[head,foot, body], 
                  _border="1", _align="center", _width="100%")

    if request.extension=="pdf":
        from gluon.contrib.pyfpdf import FPDF, HTMLMixin

        # define our FPDF class (move to modules if it is reused  frequently)
        class MyFPDF(FPDF, HTMLMixin):
            def header(self):
                self.set_font('Arial','B',15)
                self.cell(0,10, response.title ,1,0,'C')
                self.ln(20)
                
            def footer(self):
                self.set_y(-15)
                self.set_font('Arial','I',8)
                txt = 'Page %s of %s' % (self.page_no(), self.alias_nb_pages())
                self.cell(0,10,txt,0,0,'C')
                    
        pdf=MyFPDF()
        # first page:
        pdf.add_page()
        pdf.write_html(str(XML(table, sanitize=False)))
        response.headers['Content-Type']='application/pdf'
        return pdf.output(dest='S')
    else:
        # normal html view:
        return dict(table=table)}}}

Sample Templating Engine

PyFPDF and web2py can be used to make PDF documents using templates like invoices, badges, certificates, etc.:

Sample: invoice.pdf

Updated Live Demo: http://www.web2py.com.ar/fpdf/default/invoice.pdf

To handle multiples templates, we can define two tables in web2py:

  • pdf_template: document general information (name, paper size, etc.)
  • pdf_element: several rows for each document, describing graphics primitives and placeholders.

In db.py write:

db.define_table("pdf_template",
    Field("pdf_template_id","id"),
    Field("title"),
    Field("format", requires=IS_IN_SET(["A4","legal","letter"])),
)

db.define_table("pdf_element",
    Field("pdf_template_id", db.pdf_template, requires=IS_IN_DB(db,'pdf_template.pdf_template_id', 'pdf_template.title')),
    Field("name", requires=IS_NOT_EMPTY()),
    Field("type", length=2, requires=IS_IN_SET(['T', 'L', 'I', 'B', 'BC'])),
    Field("x1", "double", requires=IS_NOT_EMPTY()),
    Field("y1", "double", requires=IS_NOT_EMPTY()),
    Field("x2", "double", requires=IS_NOT_EMPTY()),
    Field("y2", "double", requires=IS_NOT_EMPTY()),
    Field("font", default="Arial", requires=IS_IN_SET(['Courier','Arial','Times','Symbol','Zapfdingbats'])),
    Field("size", "double", default="10", requires=IS_NOT_EMPTY()),
    Field("bold", "boolean"),
    Field("italic", "boolean"),
    Field("underline", "boolean"),
    Field("foreground", "integer", default=0x000000, comment="Color text"),
    Field("background", "integer", default=0xFFFFFF, comment="Fill color"),
    Field("align", "string", length=1, default="L", requires=IS_IN_SET(['L', 'R', 'C', 'J'])),
    Field("text", "text", comment="Default text"),
    Field("priority", "integer", default=0, comment="Z-Order"),
    )

At this point you could go to web2py AppAdmin and start to define your document templates, or use import/export functions to reuse your already defined formats!

Then, you can use PyFPDF Templates directly reading rows elements from the web2py database:

For example, for an invoice, in a controller you could write:

def invoice():
    from gluon.contrib.pyfpdf import Template
    import os.path
    
    # generate sample invoice (according Argentina's regulations)

    import random
    from decimal import Decimal

    # read elements from db 
    
    elements = db(db.pdf_element.pdf_template_id==1).select(orderby=db.pdf_element.priority)

    f = Template(format="A4",
             elements = elements,
             title="Sample Invoice", author="Sample Company",
             subject="Sample Customer", keywords="Electronic TAX Invoice")
    
    # create some random invoice line items and detail data
    detail = "Lorem ipsum dolor sit amet, consectetur. " * 5
    items = []
    for i in range(1, 30):
        ds = "Sample product %s" % i
        qty = random.randint(1,10)
        price = round(random.random()*100,3)
        code = "%s%s%02d" % (chr(random.randint(65,90)), chr(random.randint(65,90)),i)
        items.append(dict(code=code, unit='u',
                          qty=qty, price=price, 
                          amount=qty*price,
                          ds="%s: %s" % (i,ds)))

    # divide and count lines
    lines = 0
    li_items = []
    for it in items:
        qty = it['qty']
        code = it['code']
        unit = it['unit']
        for ds in f.split_multicell(it['ds'], 'item_description01'):
            # add item description line (without price nor amount)
            li_items.append(dict(code=code, ds=ds, qty=qty, unit=unit, price=None, amount=None))
            # clean qty and code (show only at first)
            unit = qty = code = None
        # set last item line price and amount
        li_items[-1].update(amount = it['amount'],
                            price = it['price'])

    # split detail into each line description
    obs="\n<U>Detail:</U>\n\n" + detail
    for ds in f.split_multicell(obs, 'item_description01'):
        li_items.append(dict(code=code, ds=ds, qty=qty, unit=unit, price=None, amount=None))

    # calculate pages:
    lines = len(li_items)
    max_lines_per_page = 24
    pages = lines / (max_lines_per_page - 1)
    if lines % (max_lines_per_page - 1): pages = pages + 1

    # fill placeholders for each page
    for page in range(1, pages+1):
        f.add_page()
        f['page'] = 'Page %s of %s' % (page, pages)
        if pages>1 and page<pages:
            s = 'Continues on page %s' % (page+1)
        else:
            s = ''
        f['item_description%02d' % (max_lines_per_page+1)] = s

        f["company_name"] = "Sample Company"
        f["company_logo"] = os.path.join(request.env.web2py_path,"gluon","contrib","pyfpdf","tutorial","logo.png")
        f["company_header1"] = "Some Address - somewhere -"
        f["company_header2"] = "http://www.example.com"        
        f["company_footer1"] = "Tax Code ..."
        f["company_footer2"] = "Tax/VAT ID ..."
        f['number'] = '0001-00001234'
        f['issue_date'] = '2010-09-10'
        f['due_date'] = '2099-09-10'
        f['customer_name'] = "Sample Client"
        f['customer_address'] = "Siempreviva 1234"
       
        # print line item...
        li = 0 
        k = 0
        total = Decimal("0.00")
        for it in li_items:
            k = k + 1
            if k > page * (max_lines_per_page - 1):
                break
            if it['amount']:
                total += Decimal("%.6f" % it['amount'])
            if k > (page - 1) * (max_lines_per_page - 1):
                li += 1
                if it['qty'] is not None:
                    f['item_quantity%02d' % li] = it['qty']
                if it['code'] is not None:
                    f['item_code%02d' % li] = it['code']
                if it['unit'] is not None:
                    f['item_unit%02d' % li] = it['unit']
                f['item_description%02d' % li] = it['ds']
                if it['price'] is not None:
                    f['item_price%02d' % li] = "%0.3f" % it['price']
                if it['amount'] is not None:
                    f['item_amount%02d' % li] = "%0.2f" % it['amount']

        # last page? print totals:
        if pages == page:
            f['net'] = "%0.2f" % (total/Decimal("1.21"))
            f['vat'] = "%0.2f" % (total*(1-1/Decimal("1.21")))
            f['total_label'] = 'Total:'
        else:
            f['total_label'] = 'SubTotal:'
        f['total'] = "%0.2f" % total

    response.headers['Content-Type']='application/pdf'
    return f.render('invoice.pdf', dest='S')

Of course, this is a hardcoded example, you can use the database to store invoices or any other data, there is no rigid class hierachy to follow, just fill your template like a dict!

Comment by Ovidio...@gmail.com, Sep 13, 2010

well, I am not able to understand, how to call the report for my application. If I understand what is explained is that we implement the dual tables, and pdf_template pdf_element. But within my application how do I call the report?

Comment by project member reingart, Sep 14, 2010

I've added "live demos" showing how to call web2py controllers (html and pdf views, when available). In downloads there is an sample application.

PS: can you fill an issue, as googlecode project updates seems broken... (and there we can attach code, examples and so on)

Comment by Ovidio...@gmail.com, Sep 18, 2010

Thank you for answering the previous post. But if I need to do a report by filtering by date, as do the data entry???

Comment by yamandu.costa, Sep 27, 2010

I´ve put some usable thing in a slice: http://web2pyslices.com/main/slices/take_slice/99

It does a simple report by letting the user pick a date range, filtering a table and providing a PDF button to get the PDF version.

I think it can get better, so please test and comment!

Comment by ankita.s...@gmail.com, Mar 1, 2012

I'm looking for php.... how can i implement that please tell me... i want such class to be more implemented than existing on on fpdf for writeHTML.. that doesnot accept table tag.

Comment by adesanto...@gmail.com, Mar 21, 2012

Hello, nice work and share.

I experienced odd output, the header() seems not handled the linefeed at the next page properly as the example

class MyFPDF(...):


    def header(self):
        ...
        self.ln(20)

I would like to suggest

class MyFPDF(...):


    def header(self):
        ln = 20
        first_page = 1
        ...
        if self.page_no > first_page:
            self.ln(ln*2)
        else:
            self.ln(ln)

Hope it helps


Sign in to add a comment
Powered by Google Project Hosting