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.
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
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.
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:
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:
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.