Dynamic PDF Generation of a Google Doc

Our team frequently needs to send members various types of pdfs. From receipts, to email attachments, to confirmation letters, to everything in between - there are a lot of materials we need to dynamically generate pdfs for.

The google docs api is one way to allow content editors to use familiar technology like google docs for document formatting (and not having to ask a developer to update and html template), while still being able to generate dynamic variants of documents.

We'll study the following receipt we'd want to dynamically generate as an example of this process thegoodocs.com.

Invoice Template from thegoodocs.com

Some important fields we'd want to customize here include:

  • Flyer / Card / Invoice Design Rates / Quantities / Price
  • Subtotal / Discount / Tax / Total Due
  • Invoice Number / Date / Account Number
  • Name of Invoice Contact / Phone
  • Payment Remarks

We'll start by creating a new google document with the appropriate fields in a format similar to jinja: docs.google.com/document/d/1Yrb4fW-xpJ8kXB9..

Invoice with templated variables

Then we'll go in google cloud console and create a project and a corresponding service account that has the Google Docs and Google Drive apis enabled. Once we have the service account setup we download the service key and can run the following script to clone our document, plug in the proper variables and generate a pdf:

from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
import io
from apiclient.http import MediaIoBaseDownload

SCOPES = ['https://www.googleapis.com/auth/drive']

from apiclient.discovery import build
from oauth2client.service_account import ServiceAccountCredentials
credentials = ServiceAccountCredentials.from_json_keyfile_name(
      "pdfgenerator-300503-b52f564816eb.json", scopes=SCOPES)

docs_service = build('docs', 'v1', credentials=credentials)
drive_service = build('drive', 'v3', credentials=credentials)


file_id = '1Yrb4fW-xpJ8kXB9PnieKoAYBYzc780XoPhEoWrINZuY'
res = docs_service.documents().get(documentId=file_id).execute()


copy_title = 'Copy Title'
body = {'name': copy_title}
drive_response = drive_service.files()
    .copy(fileId=file_id, body=body)
    .execute()
document_copy_id = drive_response.get('id')

replacements = {
    "{{manager_director}}": "Anil V.",
    "{{manager_director_phone}}": "1-111-111-1111",
    "{{invoice_number}}": "#1322211",
    "{{invoice_date}}": "1 January 2021",
    "{{account_number}}": "CAT - 34311",

    "{{flyer_rate}}": "$10.00", "{{card_rate}}": "$5.00", "{{invoice_rate}}": "$15.00",
    "{{flyer_count}}": "2", "{{card_count}}": "4", "{{invoice_count}}": "8",
    "{{flyer_price}}": "$20.00", "{{card_price}}": "$20.00", "{{invoice_price}}": "$120.00",

    "{{subtotal_price}}": "$160.00",
    "{{discount_price}}": "$8.00",
    "{{total_due}}": "$152.00",

    "{{payment_method}}": "ACH Transfer to 314322",
    "{{remarks}}": "Happy Holidays!",
}

requests = [{
    'replaceAllText': {
        'containsText': {
            'text': r, 'matchCase': 'true'
        },
        'replaceText': v,
       }
} for r, v in replacements.items()]

result = docs_service.documents()
    .batchUpdate(documentId=document_copy_id, 
                  body={'requests': requests})
    .execute()


request = drive_service.files()
    .export_media(fileId=document_copy_id, 
                  mimeType='application/pdf')

with open("invoice_generated.pdf", "wb") as outfile:
    downloader = MediaIoBaseDownload(outfile, request)
    done = False
    while done is False:
        status, done = downloader.next_chunk()
        print("Download %d%%." % int(status.progress() * 100))

After running the script we can view the generated pdf:

Screen Shot 2021-01-01 at 10.12.55 PM.png

To wrap up, you can also delete the generated google doc and do other fun things here: developers.google.com/docs/api

Next time we'll discuss doing loops and dynamic generation of tabular components where we may not always know the number of rows when creating the google doc itself. Stay Tuned!