Building an Online Invoice Application : Part 3

Finally we have reached our last chapter on Building an online invoice app using Bootstrap5. If you have not been following, read the previous 2 articles. This whole series is mainly based on building an online invoice generator. Directly from the web app, users can create invoices in pdf format and send emails.

Previous Articles

Part one — Python Django Invoice App

Part two — Setup Django app, models or invoicing app

Main Topics for this last chapter:

  • View function – build & view an invoice instance
  • Use crispy forms layout
  • Displaying the invoice in a PDF format
  • Send the PDF invoice in an email directly from the app
  • Full YouTube tutorial

View Function

We have built a model list page in the previous articles. This page shows the model instances in a list. An instance of the model class can be created by clicking a “create-page” button on the list page. The list page should be like this:

We are going to create views that are responsible for creating a new invoice and to view an invoice that already exists.

Building an Invoice

This will be devided into 2 parts. Normally creating an invoice should be done in one step, but we decided not to create it like that. We want to add product instances at once while we are creating an invoice. Basically this tells us that we must know what invoice to add the products to:

The initial step is to create a blank invoice instance:

@login_required
def createInvoice(request):
    #create a blank invoice ....
    number = 'INV-'+str(uuid4()).split('-')[1]
    newInvoice = Invoice.objects.create(number=number)
    newInvoice.save()

    inv = Invoice.objects.get(number=number)
    return redirect('create-build-invoice', slug=inv.slug)

The view code snippet above will create a blank invoice instance and redirect the end user to another view named ‘create-build-invoice’. This ‘create-build-invoice’ can be used by the user to generate this invoice. We can use this method to create a blank instance on which to add products, edit the invoice, save it and even view it later. To access the building view, we will need to use the slug. It should be like:

@login_required
def createBuildInvoice(request, slug):
    #fetch that invoice
    try:
        invoice = Invoice.objects.get(slug=slug)
        pass
    except:
        messages.error(request, 'Something went wrong')
        return redirect('invoices')

    #fetch all the products - related to this invoice
    products = Product.objects.filter(invoice=invoice)


    context = {}
    context['invoice'] = invoice
    context['products'] = products

    if request.method == 'GET':
        prod_form  = ProductForm()
        inv_form = InvoiceForm(instance=invoice)
        client_form = ClientSelectForm(initial_client=invoice.client)
        context['prod_form'] = prod_form
        context['inv_form'] = inv_form
        context['client_form'] = client_form
        return render(request, 'invoice/create-invoice.html', context)

    if request.method == 'POST':
        prod_form  = ProductForm(request.POST)
        inv_form = InvoiceForm(request.POST, instance=invoice)
        client_form = ClientSelectForm(request.POST, initial_client=invoice.client, instance=invoice)

        if prod_form.is_valid():
            obj = prod_form.save(commit=False)
            obj.invoice = invoice
            obj.save()

            messages.success(request, "Invoice product added succesfully")
            return redirect('create-build-invoice', slug=slug)
        elif inv_form.is_valid and 'paymentTerms' in request.POST:
            inv_form.save()

            messages.success(request, "Invoice updated succesfully")
            return redirect('create-build-invoice', slug=slug)
        elif client_form.is_valid() and 'client' in request.POST:

            client_form.save()
            messages.success(request, "Client added to invoice succesfully")
            return redirect('create-build-invoice', slug=slug)
        else:
            context['prod_form'] = prod_form
            context['inv_form'] = inv_form
            context['client_form'] = client_form
            messages.error(request,"Problem processing your request")
            return render(request, 'invoice/create-invoice.html', context)


    return render(request, 'invoice/create-invoice.html', context)

This is the main view of our app. The view is written in 3 forms: (1) Add products to the invoice using the product form, (2) The invoice itself and (3) Form for adding clients to an invoice that allows the user to select a client from those available.

For more clarification on the views, visit our youtube channel and listen to the 3 hour tutorial video. We cannot write everything in an article.

Crispy forms layouts

With Django crispy forms, forms can be displayed neatly and quickly on the frontend to save time and effort. The crispy form layout is just one column. If your form is huge and contains many fields , just make changes in the crispy form layout in the form class.

We’ve adjusted the invoice form like this:

from django import forms
from django.forms import widgets
from .models import *

#Form Layout from Crispy Forms
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column


class DateInput(forms.DateInput):
    input_type = 'date'

class InvoiceForm(forms.ModelForm):
    THE_OPTIONS = [
    ('14 days', '14 days'),
    ('30 days', '30 days'),
    ('60 days', '60 days'),
    ]
    STATUS_OPTIONS = [
    ('CURRENT', 'CURRENT'),
    ('OVERDUE', 'OVERDUE'),
    ('PAID', 'PAID'),
    ]

    title = forms.CharField(
                    required = True,
                    label='Invoice Name or Title',
                    widget=forms.TextInput(attrs={'class': 'form-control mb-3', 'placeholder': 'Enter Invoice Title'}),)
    paymentTerms = forms.ChoiceField(
                    choices = THE_OPTIONS,
                    required = True,
                    label='Select Payment Terms',
                    widget=forms.Select(attrs={'class': 'form-control mb-3'}),)
    status = forms.ChoiceField(
                    choices = STATUS_OPTIONS,
                    required = True,
                    label='Change Invoice Status',
                    widget=forms.Select(attrs={'class': 'form-control mb-3'}),)
    notes = forms.CharField(
                    required = True,
                    label='Enter any notes for the client',
                    widget=forms.Textarea(attrs={'class': 'form-control mb-3'}))

    dueDate = forms.DateField(
                        required = True,
                        label='Invoice Due',
                        widget=DateInput(attrs={'class': 'form-control mb-3'}),)


    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Row(
                Column('title', css_class='form-group col-md-6'),
                Column('dueDate', css_class='form-group col-md-6'),
                css_class='form-row'),
            Row(
                Column('paymentTerms', css_class='form-group col-md-6'),
                Column('status', css_class='form-group col-md-6'),
                css_class='form-row'),
            'notes',

            Submit('submit', ' EDIT INVOICE '))

    class Meta:
        model = Invoice
        fields = ['title', 'dueDate', 'paymentTerms', 'status', 'notes']

We started by instantiating form-helper at the top then used the layout class to do the rest. You can replicate the form anywhere you want within your code, with the same form layout you designed in your form class.

Displaying the invoice in a PDF form

Our goal is to generate an invoice that can be saved, or emailed to the client so that the client can act upon it. Our database contains the data, but it is not yet available in a format which can be sent by email.

To be able to create pdf documents in Django we need: wkhtmltopdf and pdfkit. On Skolo Online Documentation, you can find written instructions on how to set up your environment so you can use pdfkit.

This code will be combined with emailing the generated PDF.

#Dont forget the view imports
def emailDocumentInvoice(request, slug):
    #fetch that invoice
    try:
        invoice = Invoice.objects.get(slug=slug)
        pass
    except:
        messages.error(request, 'Something went wrong')
        return redirect('invoices')

    #fetch all the products - related to this invoice
    products = Product.objects.filter(invoice=invoice)

    #Get Client Settings
    p_settings = Settings.objects.get(clientName='Skolo Online Learning')

    #Calculate the Invoice Total
    invoiceTotal = 0.0
    if len(products) > 0:
        for x in products:
            y = float(x.quantity) * float(x.price)
            invoiceTotal += y


    context = {}
    context['invoice'] = invoice
    context['products'] = products
    context['p_settings'] = p_settings
    context['invoiceTotal'] = "{:.2f}".format(invoiceTotal)

    #The name of your PDF file
    filename = '{}.pdf'.format(invoice.uniqueId)

    #HTML FIle to be converted to PDF - inside your Django directory
    template = get_template('invoice/pdf-template.html')


    #Render the HTML
    html = template.render(context)

    #Options - Very Important [Don't forget this]
    options = {
          'encoding': 'UTF-8',
          'javascript-delay':'1000', #Optional
          'enable-local-file-access': None, #To be able to access CSS
          'page-size': 'A4',
          'custom-header' : [
              ('Accept-Encoding', 'gzip')
          ],
      }
      #Javascript delay is optional

    #Remember that location to wkhtmltopdf
    config = pdfkit.configuration(wkhtmltopdf='/usr/bin/wkhtmltopdf')

    #Saving the File
    filepath = os.path.join(settings.MEDIA_ROOT, 'client_invoices')
    os.makedirs(filepath, exist_ok=True)
    pdf_save_path = filepath+filename
    #Save the PDF
    pdfkit.from_string(html, pdf_save_path, configuration=config, options=options)


    #send the emails to client
    to_email = invoice.client.emailAddress
    from_client = p_settings.clientName
    emailInvoiceClient(to_email, from_client, pdf_save_path)

    invoice.status = 'EMAIL_SENT'
    invoice.save()

    #Email was send, redirect back to view - invoice
    messages.success(request, "Email sent to the client succesfully")
    return redirect('create-build-invoice', slug=slug)
from django.core.mail import EmailMessage
from django.conf import settings

def emailInvoiceClient(to_email, from_client, filepath):
    from_email = settings.EMAIL_HOST_USER
    subject = '[Skolo] Invoice Notification'
    body = """
    Good day,
    Please find attached invoice from {} for your immediate attention.
    regards,
    Skolo Online Learning
    """.format(from_client)

    message = EmailMessage(subject, body, from_email, [to_email])
    message.attach_file(filepath)
    message.send()

The above code will not work unless your settings.py file has SMTP settings configured.

Complete Youtube tutorial — Final tutorial of Invoice App

Our Django Web Development code snippets can be found here Skolo Online Documentation page.

Leave a Reply

Your email address will not be published.

Subscribe to our mailing list

And get this 💃💃 Free😀 eBook !

Holler Box