Generating PDF invoices dynamically

Now that we have a complete checkout and payment system, we can generate a PDF invoice for each order. There are several Python libraries to generate PDF files. One popular library to generate PDFs with Python code is Reportlab. You can find information about how to output PDF files with Reportlab at https://docs.djangoproject.com/en/1.8/howto/outputting-pdf/.

In most cases, you will have to add custom styles and formatting to your PDF files. You will find it more convenient to render an HTML template and convert it into a PDF file, keeping Python away from the presentation layer. We are going to follow this approach and use a module to generate PDF files with Django. We will use WeasyPrint, which is a Python library that can generate PDF files from HTML templates.

Installing WeasyPrint

First, install WeasyPrint's dependencies for your OS, which you will find at http://weasyprint.org/docs/install/#platforms.

Then, install WeasyPrint via pip using the following command:

pip install WeasyPrint==0.24

Creating a PDF template

We need an HTML document as input for WeasyPrint. We are going to create an HTML template, render it using Django, and pass it to WeasyPrint to generate the PDF file.

Create a new template file inside the templates/orders/order/ directory of the orders application and name it pdf.html. Add the following code to it:

<html>
<body>
  <h1>My Shop</h1>
  <p>
    Invoice no. {{ order.id }}</br>
    <span class="secondary">
      {{ order.created|date:"M d, Y" }}
    </span>
  </p>

  <h3>Bill to</h3>
  <p>
    {{ order.first_name }} {{ order.last_name }}<br>
    {{ order.email }}<br>
    {{ order.address }}<br>
    {{ order.postal_code }}, {{ order.city }}
  </p>

  <h3>Items bought</h3>
  <table>
    <thead>
      <tr>
        <th>Product</th>
        <th>Price</th>
        <th>Quantity</th>
        <th>Cost</th>
      </tr>
    </thead>
    <tbody>
      {% for item in order.items.all %}
        <tr class="row{% cycle "1" "2" %}">
          <td>{{ item.product.name }}</td>
          <td class="num">${{ item.price }}</td>
          <td class="num">{{ item.quantity }}</td>
          <td class="num">${{ item.get_cost }}</td>
        </tr>
      {% endfor %}
      <tr class="total">
        <td colspan="3">Total</td>
        <td class="num">${{ order.get_total_cost }}</td>
      </tr>
    </tbody>
  </table>
    
  <span class="{% if order.paid %}paid{% else %}pending{% endif %}">
    {% if order.paid %}Paid{% else %}Pending payment{% endif %}
  </span>
</body>
</html>

This is the template for the PDF invoice. In this template, we display all order details and an HTML <table> element including the products. We also include a message to display if the order has been paid or the payment is still pending.

Rendering PDF files

We are going to create a view to generate PDF invoices for existing orders using the administration site. Edit the views.py file inside the orders application directory and add the following code to it:

from django.conf import settings 
from django.http import HttpResponse
from django.template.loader import render_to_string
import weasyprint

@staff_member_required
def admin_order_pdf(request, order_id):
    order = get_object_or_404(Order, id=order_id)
    html = render_to_string('orders/order/pdf.html',
                            {'order': order})
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = 'filename=
        "order_{}.pdf"'.format(order.id)
    weasyprint.HTML(string=html).write_pdf(response,
        stylesheets=[weasyprint.CSS(
            settings.STATIC_ROOT + 'css/pdf.css')])
    return response

This is the view to generate a PDF invoice for an order. We use the staff_member_required decorator to make sure only staff users can access this view. We get the Order object with the given ID and we use the render_to_string() function provided by Django to render orders/order/pdf.html. The rendered HTML is saved in the html variable. Then, we generate a new HttpResponse object specifying the application/pdf content type and including the Content-Disposition header to specify the file name. We use WeasyPrint to generate a PDF file from the rendered HTML code and write the file to the HttpResponse object. We use the static file css/pdf.css to add CSS styles to the generated PDF file. We load it from the local path by using the STATIC_ROOT setting. Finally, we return the generated response.

Since we need to use the STATIC_ROOT setting, we have to add it to our project. This is the project's path for static files to reside. Edit the settings.py file of the myshop project and add the following setting:

STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

Then, run the command python manage.py collectstatic. You should see an output that ends as follows:

You have requested to collect static files at the destination
location as specified in your settings:

    code/myshop/static

This will overwrite existing files!
Are you sure you want to do this?

Write yes and press Enter. You should get a message indicating that the static files have been copied to the STATIC_ROOT directory.

The collectstatic command copies all static files from your applications into the directory defined in the STATIC_ROOT setting. This allows each application to provide its own static files using a static/ directory that contains them. You can also provide additional static files sources in the STATICFILES_DIRS setting. All of the directories specified in the STATICFILES_DIRS list will also be copied to the STATIC_ROOT directory when collectstatic is executed.

Edit the urls.py file inside the orders application directory and add the following URL pattern to it:

url(r'^admin/order/(?P<order_id>d+)/pdf/$',     
    views.admin_order_pdf,
    name='admin_order_pdf'),

Now, we can edit the admin list display page for the Order model to add a link to the PDF file for each result. Edit the admin.py file inside the orders application and add the following code above the OrderAdmin class:

def order_pdf(obj):
    return '<a href="{}">PDF</a>'.format(
        reverse('orders:admin_order_pdf', args=[obj.id]))
order_pdf.allow_tags = True
order_pdf.short_description = 'PDF bill'

Add order_pdf to the list_display attribute of the OrderAdmin class as follows:

class OrderAdmin(admin.ModelAdmin):
    list_display = ['id',
                    # ...
                    order_detail,
                    order_pdf]

If you specify a short_description attribute for your callable, Django will use it for the name of the column.

Open http://127.0.0.1:8000/admin/orders/order/ in your browser. Each row should now include a PDF link like this:

Rendering PDF files

Click the PDF link for any order. You should see a generated PDF file like the following one for orders that have not been paid yet:

Rendering PDF files

For paid orders, you will see the following PDF file:

Rendering PDF files

Sending PDF files by e-mail

Let's send an e-mail to our customers including the generated PDF invoice when a payment is received. Edit the signals.py file of the payment application and add the following imports:

from django.template.loader import render_to_string
from django.core.mail import EmailMessage
from django.conf import settings
import weasyprint
from io import BytesIO

Then add the following code after the order.save() line, with the same indentation level:

# create invoice e-mail
subject = 'My Shop - Invoice no. {}'.format(order.id)
message = 'Please, find attached the invoice for your recent purchase.'
email = EmailMessage(subject,
                     message,
                     '[email protected]',
                     [order.email])

# generate PDF
html = render_to_string('orders/order/pdf.html', {'order': order})
out = BytesIO()
stylesheets=[weasyprint.CSS(settings.STATIC_ROOT + 'css/pdf.css')]
weasyprint.HTML(string=html).write_pdf(out,
                                       stylesheets=stylesheets)
# attach PDF file
email.attach('order_{}.pdf'.format(order.id),
             out.getvalue(),
             'application/pdf')
# send e-mail
email.send()

In this signal, we use the EmailMessage class provided by Django to create an e-mail object. Then we render the template into the html variable. We generate the PDF file from the rendered template, and we output it to a BytesIO instance, which is a in-memory bytes buffer. Then we attach the generated PDF file to the EmailMessage object using its attach() method, including the contents of the out buffer.

Remember to set up your SMTP settings in the settings.py file of the project to send e-mails. You can refer to Chapter 2, Enhancing Your Blog with Advanced Features to see a working example for an SMTP configuration.

Now you can open the URL for your application provided by Ngrok and complete a new payment process in order to receive the PDF invoice into your e-mail.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset