DEV Community

Cover image for Building a 🧾 PDF Invoice Generator with πŸ“¨ Twilio SendGrid, 🐍 Python, Flask, and πŸ–ŒοΈ Bootstrap
Kumar Kalyan
Kumar Kalyan

Posted on

Building a 🧾 PDF Invoice Generator with πŸ“¨ Twilio SendGrid, 🐍 Python, Flask, and πŸ–ŒοΈ Bootstrap

save the article

Introduction 🌟

In the current digital era, virtually all online purchases, whether for goods or services, include an invoice. This document helps businesses retain records and customers keep track of their transactions in addition to serving as proof of purchase. Automating the creation of invoices can streamline processes for developers and enhance client satisfaction. In this post you will learn how to use Twilio SendGrid, Python ,Flask and Bootstrap to create a straightforward yet effective invoice generation application.

Tutorial Requirements πŸ› οΈπŸ“‹

Before you start, there are a few prerequisites for this tutorial that need to be addressed.

  • A free Twilio Sendgrid account. If you are new to Twilio Sendgrid, signup for a free account. Using free account you can send 100 emails per day
  • Python 3.10 or latest version . If your machine does not have this version or latest python version you can download it from here
  • ReportLab Toolkit. It is a free and open source python library for generating PDFs and graphics
  • Bootstrap version 4.6. It is a free and open-source framework for building responsive websites. You will use it for building the invoice UI

Configure Your Twilio SendGrid Account πŸ”§πŸ“€

Before you start building your app, you will need a Twilio SendGrid API key to use the Twilio SendGrid API and send emails using your Flask application.

To get an API key, first log in to your Twilio SendGrid account using your credentials. Then, on the left side, navigate to Settings, select API Keys, and click on Create API Key.

Create Twilio Sendgrig API Key

Now, add your preferred API key name (in my case, I am using Flask-invoice-new, then choose Full Access under API Key Permissions, and finally, click on Create & View.

Now, you will see that your API key has been generated. Copy the key and store it in a secure place, as Twilio SendGrid will only allow you to view the key once for security reasons.

Now that your API key has been generated successfully, you need to verify your email with Twilio SendGrid, which you will use to send emails.

Navigate to Settings, select Sender Authentication, then click on the Verify a Single Sender button. Next, click on Create New Sender, fill in the required details, and click Create.

Create a Sender

Twilio SendGrid will now send a verification email to this address with a link. Once you click on the link and complete the verification, you’re good to go.

Configure your Python Virtual Environment 🌟🐍

Now that you have generated your API keys in the previous step, you need to set up your Python virtual environment to create a Flask application and send emails using the Twilio SendGrid API.

Create a directory for your app and set up the Python virtual environment using the commands provided below. (For this tutorial, I am using the directory name 'Twilio-Sendgrid-Flask-invoice-app').

For unix or mac users

$ mkdir Twilio-Sendgrid-Flask-invoice-app
$ cd Twilio-Sendgrid-Flask-invoice-app
$ python3 -m venv venv
$source venv/bin/activate
(venv) $ _
Enter fullscreen mode Exit fullscreen mode

For Windows users

mkdir Twilio-Sendgrid-Flask-invoice-app
cd Twilio-Sendgrid-Flask-invoice-app
python3 -m venv venv
venv\Scripts\activate

Enter fullscreen mode Exit fullscreen mode

Once you have configured your Python virtual environment, install the required Python dependencies using the command below

pip install flask sendgrid reportlab python-dotenv
Enter fullscreen mode Exit fullscreen mode

Create your Flask application πŸπŸ§ΎπŸ“§

Create a file named β€œapp.py” in the root directory and add the starter code for creating a Flask application

import os
from flask import Flask, render_template, request, send_file
from reportlab.lib.pagesizes import letter
from reportlab.platypus import Table, TableStyle, Paragraph, SimpleDocTemplate
from reportlab.lib import colors
from reportlab.lib.units import inch
from reportlab.lib.styles import getSampleStyleSheet
from io import BytesIO
import base64
from datetime import datetime
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail, Attachment, Disposition, FileContent, FileName, FileType
app = Flask(__name__)
API_KEY=os.environ.get('TWILIO_SENDGRID_API_KEY')
EMAIL_ADDRESS = os.environ.get('MY_VERIFIED_EMAIL_ADDRESS')

Enter fullscreen mode Exit fullscreen mode

In the above code I have imported all the necessary modules required to create a Flask application and build the invoice generator app. For security reasons, I am using environment variables to store my Twilio SendGrid API key and my email address, which will be used to send PDF invoices to clients' email addresses

Now, create a file named '.env' in the root directory and store your Twilio SendGrid API key and your verified email address. For this tutorial, I am using the variable names TWILIO_SENDGRID_API_KEY and MY_VERIFIED_EMAIL_ADDRESS. Flask will automatically import these environment variables using the Python dotenv package.

TWILIO_SENDGRID_API_KEY="YOUR TWILIO SENDGRID API KEY "
MY_VERIFIED_EMAIL_ADDRESS="YOUR EMAIL ADDRESS"

Enter fullscreen mode Exit fullscreen mode

Create the home page for your Flask application πŸ πŸ’»

In Flask, you can render dynamic HTML content by combining HTML templates and data, a process called template rendering. This means that when you open a specific route in your Flask application, you can render a predefined HTML file there. For this tutorial, you will create an HTML file for the invoice UI so that whenever anyone opens the home route of your Flask application, they will see a beautiful invoice UI where they can enter invoice data.

Now, create a folder named templates in the root directory and add an index.html file inside it. In Flask you can render any HTML file present within this folder

Building the invoice form using bootstrap 🧾✨

Inside the index.html file paste the code below

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Invoice Generator</title>
    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
        integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
</head>

<body class="bg-light">
    <div class="container mt-5">
        <div class="card">
            <div class="card-header bg-primary text-white">
                <h3 class="mb-0">Invoice Generator</h3>
            </div>
            <div class="card-body">
                <form id="invoice-form" action="/generate-invoice" method="POST" onsubmit="alertSubmit()">
                    <div class="form-group">
                        <label for="client_name">Client Name:</label>
                        <input type="text" class="form-control" id="client_name" name="client_name" required>
                    </div>
                    <div class="form-group">
                        <label for="email">Client Email:</label>
                        <input type="email" class="form-control" id="email" name="email" required>
                    </div>

                    <h4 class="mt-4">Invoice Items</h4>
                    <div id="items">
                        <div class="form-row align-items-center mb-3 item">
                            <div class="col-md-4">
                                <label>Description</label>
                                <input type="text" class="form-control" name="description[]" required>
                            </div>
                            <div class="col-md-2">
                                <label>Quantity</label>
                                <input type="number" class="form-control" name="quantity[]" oninput="calculateTotal()"
                                    required>
                            </div>
                            <div class="col-md-2">
                                <label>Price</label>
                                <input type="number" step="0.01" class="form-control" name="price[]"
                                    oninput="calculateTotal()" required>
                            </div>
                            <div class="col-md-2">
                                <button type="button" class="btn btn-danger btn-block mt-4"
                                    onclick="removeItem(this)">Remove</button>
                            </div>
                        </div>
                    </div>
                    <button type="button" class="btn btn-secondary" onclick="addItem()">Add Another Item</button>

                    <div class="form-group mt-4">
                        <label for="total_amount">Total Amount:</label>
                        <input type="number" step="0.01" class="form-control" id="total_amount" name="amount" readonly>
                    </div>

                    <button type="submit" class="btn btn-primary btn-block">Generate and Send Invoice</button>
                    <button onclick="resetForm()" class="btn btn-danger btn-block">Reset Invoice Form </button>
                </form>
            </div>
        </div>
    </div>


</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Invoice UI

For this tutorial, I am using Bootstrap version 4.5, so make sure to add the correct CDN link for this Bootstrap version; otherwise, some features may not work properly.

In the code above, I have created a form with a simple UI that submits data to the /generate-invoice route. As of now, it is just a basic HTML form without any actions, as I haven't added functionalities to the buttons yet and for that JavaScript is needed

Building the functionalities for buttons πŸ”˜βš™οΈ

To add functionalities to the buttons, include additional invoice fields, and calculate the total invoice amount, you will need to use JavaScript. Paste the JavaScript code just above the ending <div> tag to add functionalities to the form.

  <script>
        function alertSubmit() {
            alert("Email Sent Successfully");
        }
        function resetForm() {
            document.getElementById("invoice-form").reset();
        }

        // Function to add a new item row
        function addItem() {
            const itemDiv = document.createElement('div');
            itemDiv.className = 'form-row align-items-center mb-3 item';
            itemDiv.innerHTML = `
                <div class="col-md-4">
                    <label>Description</label>
                    <input type="text" class="form-control" name="description" required>
                </div>
                <div class="col-md-2">
                    <label>Quantity</label>
                    <input type="number" class="form-control" name="quantity" oninput="calculateTotal()" required>
                </div>
                <div class="col-md-2">
                    <label>Price</label>
                    <input type="number" step="0.01" class="form-control" name="price" oninput="calculateTotal()" required>
                </div>
                <div class="col-md-2">
                    <button type="button" class="btn btn-danger btn-block mt-4" onclick="removeItem(this)">Remove</button>
                </div>
            `;
            document.getElementById('items').appendChild(itemDiv);
            calculateTotal();
        }

        // Function to calculate total amount
        function calculateTotal() {
            let total = 0;
            const quantities = document.querySelectorAll('input[name="quantity"]');
            const prices = document.querySelectorAll('input[name="price"]');

            quantities.forEach((quantity, index) => {
                const price = prices[index];
                const qty = parseFloat(quantity.value) || 0;
                const prc = parseFloat(price.value) || 0;
                total += qty * prc;
            });

            // Update the total amount field
            document.getElementById('total_amount').value = total.toFixed(2);
        }

        // Function to remove an item row
        function removeItem(button) {
            const itemDiv = button.parentNode.parentNode;
            document.getElementById('items').removeChild(itemDiv);
            calculateTotal();
        }
    </script>
Enter fullscreen mode Exit fullscreen mode

In the above code

  • alertSubmit() - Displays an alert confirming that the email was sent successfully.
  • resetForm() - Resets the values in the invoice form.
  • addItem() - Adds new form fields to include more items in the invoice form.
  • calculateTotal() - Automatically updates the Total Amount field by calculating the total whenever a new item is added to the invoice.
  • removeItem() - Removes a specific item from the invoice form.

Write the Python code that would generate invoices in PDF format and send emails.

Once your UI for the invoice generator app is ready, it’s time to write Python functions for the backend.

In this part of the tutorial, you will create the /generate-invoice route to handle the POST request and the send_invoice_via_email() function to send emails using the free Twilio SendGrid API.

Let’s code the genenate_invoice function πŸ’»πŸ§Ύ

Open the app.py file and write a function to fetch data from the form and generate PDF invoices.


@app.route('/generate-invoice', methods=['POST'])
def generate_invoice():
    data = request.form
    client_name = data.get('client_name')
    email = data.get('email')
    total_amount = data.get('amount')

    # Retrieve item details
    descriptions = data.getlist('description')
    quantities = data.getlist('quantity')
    prices = data.getlist('price')

    # Generate PDF using ReportLab
    pdf_buffer = BytesIO()
    pdf = SimpleDocTemplate(pdf_buffer, pagesize=letter)

    # Styles and layout
    styles = getSampleStyleSheet() 
    title_style = styles["Title"]
    normal_style = styles["Normal"]

    elements = []

    # Invoice Title
    elements.append(Paragraph("INVOICE", title_style))

    # Client and date details
    elements.append(Paragraph(f"Date: {datetime.now().strftime('%Y-%m-%d')}", normal_style))
    elements.append(Paragraph(f"Client: {client_name}", normal_style))
    elements.append(Paragraph(" ", normal_style))  # Add space before the table

    # Table for invoice items
    items = [["Description", "Quantity", "Price", "Total"]]
    for description, quantity, price in zip(descriptions, quantities, prices):
        total = float(quantity) * float(price)
        items.append([description, quantity, f"${float(price):.2f}", f"${total:.2f}"])

    # Adding the Total row with only two columns
    items.append(["", "", "Total", f"${float(total_amount):.2f}"])

    # Table style
    table = Table(items, colWidths=[3 * inch, inch, inch, inch])
    table.setStyle(TableStyle([
        ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
        ('BACKGROUND', (0, 1), (-1, -2), colors.beige),
        ('GRID', (0, 0), (-1, -1), 1, colors.black),
        ('SPAN', (0, -1), (2, -1)),  # Span the first three cells in the last row
        ('ALIGN', (2, -1), (3, -1), 'CENTER'),
        ('FONTNAME', (2, -1), (3, -1), 'Helvetica-Bold'),
    ]))

    elements.append(table)

    # Adding space and final message
    elements.append(Paragraph(" ", normal_style))  # Add space after the table
    elements.append(Paragraph("Thank you for your business!", title_style))

    # Build PDF
    pdf.build(elements)

    # Rewind the buffer
    pdf_buffer.seek(0)

    # Send the PDF via email
    try:
        if email:
            if(send_invoice_via_email(pdf_buffer, email)):
                return send_file(pdf_buffer, as_attachment=True, download_name="invoice.pdf", mimetype='application/pdf')
    except Exception as e:
        return f"Error sending email: {e}"

Enter fullscreen mode Exit fullscreen mode

In the above code the important points are as follows:

  • I created a new route, /generate-invoice, of type POST, meaning this route will only handle POST requests.
  • I am using the request.form method in Flask to process data coming from the invoice form and store the values in respective variables.
  • I am using the getlist() method to retrieve multiple data entries using a single key. For this tutorial, there can be multiple invoice items with the same key. Using getlist() allows me to store item descriptions, quantities, and prices in separate lists.
  • I am using the BytesIO object from Python's io module and the SimpleDocTemplate function from ReportLab to set up a PDF template for the invoice. Note that this BytesIO class will be used to write invoice data into the PDF file.
  • I am using getSampleStyleSheet() from ReportLab to add some styles to the invoice data.
  • I am using Python lists and zip to organize the invoice items and calculate the net total amount for each item by multiplying the quantity by the price to get the final amount.
  • I am using the Table object from ReportLab to style the invoice table. Once all data processing and arrangement are complete, I use the append method to add each invoice element to the item list, and the build() method from ReportLab to generate the invoice as a PDF document.
  • I am using seek() to reset the file handler pointer to 0.
  • Finally, I check if an email address is provided and, if so, whether the email is successfully sent using the send_invoice_via_email(pdf_buffer, email) function. Once all conditions are met, I use the send_file() function from Flask to send the invoice PDF for download. All these steps are wrapped in try and except blocks for exception handling.

Let’s code the send_invoice_via_email function πŸ“§βœ¨

As of now, the /generate-invoice route is set up but not yet functional. This means that if you try running your Flask application now, it will show an error. The reason for this is that you still need to add the send_invoice_via_email() function to handle sending emails. So, let’s create this function.

def send_invoice_via_email(pdf_buffer, recipient_email):
    # Convert PDF to base64 for email attachment
    pdf_data = base64.b64encode(pdf_buffer.getvalue()).decode()

    # Create SendGrid Mail object
    message = Mail(
        from_email='kum9748ar@gmail.com',  # Replace with your email
        to_emails=recipient_email,
        subject='Your Invoice',
        html_content='<p>Please find attached your invoice.</p>'
    )

    # Create attachment for the email
    attachment = Attachment(
        FileContent(pdf_data),
        FileName("invoice.pdf"),
        FileType("application/pdf"),
        Disposition("attachment")
    )
    message.attachment = attachment

    # Send email using SendGrid
    try:
        sg = SendGridAPIClient(API_KEY)
        response = sg.send(message)
        print(f"Email sent! Status code: {response.status_code}")
        return True
    except Exception as e:
        print(f"Error sending email: {e}")
        return False

Enter fullscreen mode Exit fullscreen mode

In the above function the important points are as follows:

  • The function send_invoice_via_email(pdf_buffer, recipient_email) takes two arguments: pdf_buffer, which is your generated invoice PDF, and recipient_email, which is the email address where the invoice will be sent to the client.
  • I am using the base64 module to convert the PDF into an email attachment.
  • I am using the Mail object from sendgrid.helpers.mail to add essential details required to send an email via SendGrid, and the Attachment object to include the generated PDF as an attachment with this email message.
  • Finally, I am using the SendGridAPIClient() object, which takes the SendGrid API key as an argument, to send the email. This portion of code is also wrapped within try and except blocks for exception handling.

Run your application πŸš€πŸ’»

To run your Flask application, you need to add a final piece of code.

if __name__ == "__main__":
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

The above code ensures that your app.py file will run as a script and not as a module when it is imported.

Now, open your terminal, navigate to your project directory (for this tutorial, it should be Twilio-Sendgrid-Flask-invoice-app), and use the command below to start your Flask application. Then, open your browser and go to http://127.0.0.1:5000, and you will see your fully functional invoice app running successfully

python app.py 
Enter fullscreen mode Exit fullscreen mode

Final Invoice App

The image above shows that my Flask application is working properly.
Now, let’s check the mailbox.

Sample email image

You can see that I have received an email via SendGrid containing my invoice attached as a PDF.

Some key points to remember for deployment πŸŒπŸ“

Now that your Flask invoice generator app is ready, and you’re excited to share it with others as a live app, keep these key points in mind:

  • Generate a requirements.txt file using pip install -r requirements.txt which contains a list of python packages required to run this app

Python package list

The above image shows the requirements.txt file for the Twilio-Sendgrid-Flask-invoice-app

  • Make sure to add the .env file to your .gitignore file so that nobody else can see your API keys.
  • Keep in mind that if you are using a Twilio SendGrid trial account, you can only send 100 emails per day. If you need to send more, I recommend upgrading your plan, as they offer a very flexible pricing structure.
  • If you want to update the application UI, make sure to check out the documentation for Bootstrap version 4.6.2, which I have used for this tutorial. Otherwise, you may need to rewrite the entire UI code.

Conclusion πŸŽ―βœ…

So far in this tutorial, you have learned how to send emails using Twilio SendGrid, create a fully functional Flask app, work with Python's io module, render static HTML files, create PDFs in Python, and use Bootstrap and I hope you enjoyed it!

Check out Twilio’s developer docs and blogs to learn how to build interesting apps using various Twilio products. They also offer a $15 trial credit to new users that you can use for free.

The entire code for this tutorial is hosted on GitLab, and you can check it using this link.

Top comments (0)