Invoicing App (Part 2)

This is a part 2 of our tutorial series on Building an invoicing app. We are discussing how to use Python, Django, and Bootstrap5 to build an invoicing app from the ground up. Find part 1 here: Building an Invoicing Application.

Part 2 source code is available on GitHub: Invoicing Django App Part 2

YouTube full video tutorial: Create Invoicing App with Django and Bootstrap Tutorial

Invoicing Application
Invoicing App


Part 2 of Building an Invoicing Application will cover:

  • We’ll pick up where we left off with the models
  • Use custom CSS to create a form for our model
  • Django templating
  • Decorators for Django paths that require logging in and invisibility
  • Displaying a list of clients using the view function
 Online Invoice Generator
Online Invoice Generator

Django Invoicing Application – Continuing with the models

In this article, we are on part 2 of our series on building the Invoicing Application. We have already started the clients model in part 1. We will proceed with creating the models for Products and Invoice.

from django.db import models
from django.template.defaultfilters import slugify
from django.utils import timezone
from uuid import uuid4
from django.contrib.auth.models import User
class Client(models.Model):

    PROVINCES = [
    ('Gauteng', 'Gauteng'),
    ('Free State', 'Free State'),
    ('Limpopo', 'Limpopo'),
    ]

    #Basic Fields.
    clientName = models.CharField(null=True, blank=True, max_length=200)
    addressLine1 = models.CharField(null=True, blank=True, max_length=200)
    clientLogo  = models.ImageField(default='default_logo.jpg', upload_to='company_logos')
    province = models.CharField(choices=PROVINCES, blank=True, max_length=100)
    postalCode = models.CharField(null=True, blank=True, max_length=10)
    phoneNumber = models.CharField(null=True, blank=True, max_length=100)
    emailAddress = models.CharField(null=True, blank=True, max_length=100)
    taxNumber = models.CharField(null=True, blank=True, max_length=100)


    #Utility fields
    uniqueId = models.CharField(null=True, blank=True, max_length=100)
    slug = models.SlugField(max_length=500, unique=True, blank=True, null=True)
    date_created = models.DateTimeField(blank=True, null=True)
    last_updated = models.DateTimeField(blank=True, null=True)


    def __str__(self):
        return '{} {} {}'.format(self.clientName, self.province, self.uniqueId)


    def get_absolute_url(self):
        return reverse('client-detail', kwargs={'slug': self.slug})


    def save(self, *args, **kwargs):
        if self.date_created is None:
            self.date_created = timezone.localtime(timezone.now())
        if self.uniqueId is None:
            self.uniqueId = str(uuid4()).split('-')[4]
            self.slug = slugify('{} {} {}'.format(self.clientName, self.province, self.uniqueId))

        self.slug = slugify('{} {} {}'.format(self.clientName, self.province, self.uniqueId))
        self.last_updated = timezone.localtime(timezone.now())

        super(Client, self).save(*args, **kwargs)

class Product(models.Model):
    CURRENCY = [
    ('R', 'ZAR'),
    ('$', 'USD'),
    ]

    title = models.CharField(null=True, blank=True, max_length=100)
    description = models.TextField(null=True, blank=True)
    quantity = models.FloatField(null=True, blank=True)
    price = models.FloatField(null=True, blank=True)
    currency = models.CharField(choices=CURRENCY, default='R', max_length=100)

    #Utility fields
    uniqueId = models.CharField(null=True, blank=True, max_length=100)
    slug = models.SlugField(max_length=500, unique=True, blank=True, null=True)
    date_created = models.DateTimeField(blank=True, null=True)
    last_updated = models.DateTimeField(blank=True, null=True)


    def __str__(self):
        return '{} {}'.format(self.title, self.uniqueId)


    def get_absolute_url(self):
        return reverse('product-detail', kwargs={'slug': self.slug})


    def save(self, *args, **kwargs):
        if self.date_created is None:
            self.date_created = timezone.localtime(timezone.now())
        if self.uniqueId is None:
            self.uniqueId = str(uuid4()).split('-')[4]
            self.slug = slugify('{} {}'.format(self.title, self.uniqueId))

        self.slug = slugify('{} {}'.format(self.title, self.uniqueId))
        self.last_updated = timezone.localtime(timezone.now())

        super(Product, self).save(*args, **kwargs)
class Invoice(models.Model):
    TERMS = [
    ('14 days', '14 days'),
    ('30 days', '30 days'),
    ('60 days', '60 days'),
    ]

    STATUS = [
    ('CURRENT', 'CURRENT'),
    ('OVERDUE', 'OVERDUE'),
    ('PAID', 'PAID'),
    ]

    title = models.CharField(null=True, blank=True, max_length=100)
    number = models.CharField(null=True, blank=True, max_length=100)
    dueDate = models.DateField(null=True, blank=True)
    paymentTerms = models.CharField(choices=TERMS, default='14 days', max_length=100)
    status = models.CharField(choices=STATUS, default='CURRENT', max_length=100)
    notes = models.TextField(null=True, blank=True)

    #RELATED fields
    client = models.ForeignKey(Client, blank=True, null=True, on_delete=models.SET_NULL)
    product = models.ForeignKey(Product, blank=True, null=True, on_delete=models.SET_NULL)

    #Utility fields
    uniqueId = models.CharField(null=True, blank=True, max_length=100)
    slug = models.SlugField(max_length=500, unique=True, blank=True, null=True)
    date_created = models.DateTimeField(blank=True, null=True)
    last_updated = models.DateTimeField(blank=True, null=True)


    def __str__(self):
        return '{} {}'.format(self.title, self.uniqueId)


    def get_absolute_url(self):
        return reverse('invoice-detail', kwargs={'slug': self.slug})


    def save(self, *args, **kwargs):
        if self.date_created is None:
            self.date_created = timezone.localtime(timezone.now())
        if self.uniqueId is None:
            self.uniqueId = str(uuid4()).split('-')[4]
            self.slug = slugify()

        self.slug = slugify('{} {}'.format(self.title, self.uniqueId))
        self.last_updated = timezone.localtime(timezone.now())

        super(Invoice, self).save(*args, **kwargs)
class Settings(models.Model):

    PROVINCES = [
    ('Gauteng', 'Gauteng'),
    ('Free State', 'Free State'),
    ('Limpopo', 'Limpopo'),
    ]

    #Basic Fields
    clientName = models.CharField(null=True, blank=True, max_length=200)
    clientLogo = models.ImageField(default='default_logo.jpg', upload_to='company_logos')
    addressLine1 = models.CharField(null=True, blank=True, max_length=200)
    province = models.CharField(choices=PROVINCES, blank=True, max_length=100)
    postalCode = models.CharField(null=True, blank=True, max_length=10)
    phoneNumber = models.CharField(null=True, blank=True, max_length=100)
    emailAddress = models.CharField(null=True, blank=True, max_length=100)
    taxNumber = models.CharField(null=True, blank=True, max_length=100)


    #Utility fields
    uniqueId = models.CharField(null=True, blank=True, max_length=100)
    slug = models.SlugField(max_length=500, unique=True, blank=True, null=True)
    date_created = models.DateTimeField(blank=True, null=True)
    last_updated = models.DateTimeField(blank=True, null=True)


    def __str__(self):
        return '{} {} {}'.format(self.clientName, self.province, self.uniqueId)


    def get_absolute_url(self):
        return reverse('settings-detail', kwargs={'slug': self.slug})


    def save(self, *args, **kwargs):
        if self.date_created is None:
            self.date_created = timezone.localtime(timezone.now())
        if self.uniqueId is None:
            self.uniqueId = str(uuid4()).split('-')[4]
            self.slug = slugify('{} {} {}'.format(self.clientName, self.province, self.uniqueId))

        self.slug = slugify('{} {} {}'.format(self.clientName, self.province, self.uniqueId))
        self.last_updated = timezone.localtime(timezone.now())

        super(Settings, self).save(*args, **kwargs)

Basic and Utility Fields

We have basic fields and utility fields in our models. Because I am a person of habit, I usually create utility fields. So you don’t have to do them.

So these comes in handy later on when dealing with instances of the class and identifying, displaying, and interpreting them.

When you create model instances using UniqueId as the identifier, you help with routing towards a detailed page. So this is great for SEO and it looks nice to use Slugs to route towards the detailed page.

The purpose of having a date_created and last_updated field on every model is obvious.

Custom CSS to create a form for our model

Our first attempt was to create a model form in the Django format, and to display it as a table or as_p, but the styling looked bad. We decided to create the form using form-widgets, as we could specify the CSS for each field.

With this feature, even if your template isn’t bootstrap, you can copy the CSS and your form will look the way it should.

from django import forms
from django.contrib.auth.models import User
from django.forms import widgets
from .models import *
import json

class UserLoginForm(forms.ModelForm):
    username = forms.CharField(
                            widget=forms.TextInput(attrs={'id': 'floatingInput', 'class': 'form-control mb-3'}),
                            required=True)
    password = forms.CharField(
                            widget=forms.PasswordInput(attrs={'id': 'floatingPassword', 'class': 'form-control mb-3'}),
                            required=True)

    class Meta:
        model=User
        fields=['username','password']

Check out our Github link for more information about the file. The gist shows how we add custom CSS to the form widgets for both username and password fields.

Django Templating

Templates for the site were easy to create. The HTML code we had built into Bootstrap5 was extended to work with Django. I won’t go into too much detail here because it is pretty standard stuff.

Decorators for Django paths that require logging in and anonymous

For the HTML template to be rendered, we must create the view functions first. In Django, the view function performs the tasks like:

  • Preparing and displaying data retrieved from the database
  • Push the HTML template with our form to render it
  • Authentication for users
  • Manage access rights so you can manage who can see what in your application

We describe the view as the server where most of the logic of the application goes. Our first topic will be path decorators.

Like a kind of a helper for our views, path decorators deliver code before the view is executed. They filter requests and act as gatekeepers by allowing through only those requests that pass “tests that we create”.

Benefits of using the ‘test that we create”
  1. Access to a certain route is restricted to logged-in users
  2. There are also some nifty features like editing a document is restricted to the creator (or, specifically, using that documentId, that individual can go to the edit route).

However, at this point we will have to import 2 decorators. This is how we import login_required:

from django.contrib.auth.decorators import login_required
Anonymous_required

We also created anonymous required because it does the total opposite of login required. This decorator should appear on routes like the login page or registration page. It would be inconvenient to ask a user who is already logged in to log in again.

Above all, logging in twice is not a problem, but definitely not good practice. Users who have already logged in do not expect to be prompted again. I am not a fan of applications that allow this.

from django.contrib.auth.decorators import user_passes_test


#Anonymous required
def anonymous_required(function=None, redirect_url=None):

   if not redirect_url:
       redirect_url = 'dashboard'

   actual_decorator = user_passes_test(
       lambda u: u.is_anonymous,
       login_url=redirect_url
   )

   if function:
       return actual_decorator(function)
   return actual_decorator


def index(request):
    context = {}
    return render(request, 'invoice/index.html', context)

  

@anonymous_required
def login(request):
  #Rest of login function goes here
user_passes_test function

We are using the user_passes_test function to test that the user can pass the test: anonymous — which returns a boolean (true or false). You can customise this for many user-related functions like is_staff, superuser, etc.

To check that the user can pass the test, we are using the user_passes_test_function. Anonymous – returns a boolean(true or false). But these functions can be tailored for many aspects of the user relationship, for instance is_staff, superuser, etc.

Displaying a list of clients using the view function

If you wish to see the complete code, with all imports: view the code on GitHub. The link is above or play the attached YouTube video. So for the purpose of this article, these code gists are patches from sections of the code-base.

@login_required
def clients(request):
    context = {}
    clients = Client.objects.all()
    context['clients'] = clients

    if request.method == 'GET':
        form = ClientForm()
        context['form'] = form
        return render(request, 'invoice/clients.html', context)

    if request.method == 'POST':
        form = ClientForm(request.POST, request.FILES)

        if form.is_valid():
            form.save()

            messages.success(request, 'New Client Added')
            return redirect('clients')
        else:
            messages.error(request, 'Problem processing your request')

Part 2 YouTube Tutorial video: Building an Invoicing App

Leave a Reply

Your email address will not be published.

Subscribe to our mailing list

And get this 💃💃 Free😀 eBook !

Holler Box