Python Google Document Generator

Using Python to Generate Google Docs for Real Estate Documents

Back to All Articles

Using Python to Generate Google Docs for Real Estate Documents

A detailed tutorial on using Python to build a real estate document generator. Learn how to automate document creation for your real estate transactions with this flexible script.

Last updated: July 31, 2024

In the fast-paced world of real estate, efficiency is key. I remember a time when I was swamped with deals and spent hours manually creating documents. Automating document generation not only saved me significant time but also reduced errors that often crept in due to repetitive manual tasks. In this tutorial, I'll walk you through how I used Python to create Google Docs for my real estate business. You'll be able to pass a Google Doc template and any data you want to it. This guide assumes you have a basic understanding of Python and Google APIs.

What You'll Need

Before we dive in, make sure you have the following:

  • A Google account
  • Google Cloud project with Google Docs API enabled
  • Python installed on your computer
  • google-auth, google-auth-oauthlib, google-auth-httplib2, and google-api-python-client libraries installed

Step 1: Setting Up Your Google Cloud Project

  1. Create a New Project: Go to the Google Cloud Console and create a new project.
  2. Enable the Google Docs/Drive APIs: Navigate to the API library and enable the Google Docs and Drive APIs for your project. Make sure it says API enabled like this: Google Docs API GeneratorGoogle Drive API Generator
  3. Create a Service Account:
    • Navigate to "IAM & Admin" and click "Service Accounts". At the top of the screen, you will see "Create Service Account". It should look something like this: Google Cloud Console Doc Automation Screenshot
    • Fill in the account details, give it whatever name you'd like Google Cloud Console
    • Grant the service account the appropriate roles. You will need to give it 'Editor' and 'Viewer' access at the minimum. Google Cloud Console
    • Give yourself access to the service account. I put my personal email in both of these roles, you can add others who may be working on the project here as well. Google Service Account Access
  4. Create Credentials for the Service Account:
    • Click on the service account you just created. Click the top where it says "Keys". There is a button that says "Add Key": Google Service Account Key
    • There is an option for "JSON". It will download a file, save it securely.

I still remember the first time I navigated the Google Cloud Console, it felt like deciphering a treasure map. Trust me, it gets easier with practice.

Step 2: Installing the Necessary Python Libraries

Open your terminal inside you project and install the required libraries. I do this inside a virtual environment, which you can read more about here:

pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client

Step 3: Authenticating and Authorizing Your Script

Create a Python script and add the following code to authenticate and authorize your script:

import json
import logging
from googleapiclient.discovery import build
from google.oauth2.service_account import Credentials

# Set up logging
logging.basicConfig(level=logging.INFO)

# Set up Google Docs and Drive API
SCOPES = [
    "https://www.googleapis.com/auth/documents",
    "https://www.googleapis.com/auth/drive",
]
SERVICE_ACCOUNT_FILE = "service-account.json"
TEMPLATE_DOCUMENT_ID = "1M251-WfuC5yDubqfUiK95A-R3y0yGqlf1X-QXvKfe2I"
EMAIL = "your-email@example.com"

credentials = Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
delegated_credentials = credentials.with_subject(EMAIL)

# Build the service clients with the delegated credentials
docs_service = build("docs", "v1", credentials=delegated_credentials)
drive_service = build("drive", "v3", credentials=delegated_credentials)

Delegating your credentials allows the service user to add the created documents to your google account. You can also share the document from the service account but you may run into sharing limits on the Google Docs API.

Step 4: Creating and Copying a Google Doc Template

Create functions to copy a document, and share it with a user if your use case needs to:

#This allows us to keep the template intact and work off a copy
def create_document_copy(template_id, new_title):
    body = {"name": new_title}
    copied_file = drive_service.files().copy(fileId=template_id, body=body).execute()
    return copied_file["id"]

#In some cases you may want to share the document with other people. Be aware in testing you may run into limits set by Google Docs API sharing quot
def share_document_with_user(file_id, user_email):
    permissions = {"type": "user", "role": "writer", "emailAddress": user_email}
    drive_service.permissions().create(
        fileId=file_id, body=permissions, fields="id"
    ).execute()

Step 5: Replacing Placeholders and Updating Document Content

Functions to replace placeholders and find text/table indexes:

def replace_placeholder(document_id, placeholder, replacement_text):
    requests = [
        {
            "replaceAllText": {
                "containsText": {"text": placeholder, "matchCase": True},
                "replaceText": replacement_text,
            }
        }
    ]
    docs_service.documents().batchUpdate(
        documentId=document_id, body={"requests": requests}
    ).execute()


def find_table_index(document, placeholder):
    for index, element in enumerate(document["body"]["content"]):
        if "table" in element:
            for row in element["table"]["tableRows"]:
                for cell in row["tableCells"]:
                    for content in cell["content"]:
                        if "paragraph" in content:
                            for run in content["paragraph"]["elements"]:
                                if (
                                    "textRun" in run
                                    and placeholder in run["textRun"]["content"]
                                ):
                                    location_dict = dict()
                                    location_dict["table_index"] = document["body"][
                                        "content"
                                    ][index]["startIndex"]
                                    location_dict["content_index"] = run["startIndex"]
                                    return location_dict
    return None


def find_text_index(document, placeholder):
    for index, element in enumerate(document["body"]["content"]):
        if "paragraph" in element:
            for run in element["paragraph"]["elements"]:
                if "textRun" in run and placeholder in run["textRun"]["content"]:
                    location_dict = {
                        "type": "paragraph",
                        "paragraph_index": element["startIndex"],
                        "content_index": run["startIndex"],
                    }
                    return location_dict
    return None

Step 6: Processing the Template

Function to process the template and insert data into the document:

def insert_table_rows(requests, table_index, num_rows):
    for _ in range(num_rows):
        requests.append(
            {
                "insertTableRow": {
                    "tableCellLocation": {
                        "tableStartLocation": {"index": table_index},
                        "rowIndex": 0,
                        "columnIndex": 1,
                    },
                    "insertBelow": True,
                }
            }
        )


def process_template(config):
    templates = config["templates"] 
    for template in templates:
        new_doc_id = create_document_copy(template["id"], template["title"])
        document = docs_service.documents().get(documentId=new_doc_id).execute()
        requests = []

        for section in template["sections"]:
            placeholder = section["placeholder"]
            content_type = section["type"]
            data = section["data"]
            # data = section

            if not data:
                continue

            requests.append(
                {
                    "replaceAllText": {
                        "containsText": {"text": f"{placeholder}", "matchCase": True},
                        "replaceText": "",
                    }
                }
            )

            if content_type == "table":
                indices = find_table_index(document, placeholder)
                if not indices:
                    print("couldn't find table index")
                    continue

                table_index = indices["table_index"]
                content_index = indices["content_index"]
                columns = section["columns"]
                padding = 3 # This allows us to go to the next table cell
                records = data
                insert_table_rows(requests, table_index, len(records))
                for column in columns:
                    print(content_index)
                    requests.append(
                        {
                            "insertText": {
                                "location": {"index": content_index},
                                "text": f"{column}",
                            }
                        }
                    )
                    content_index += len(column) + padding
                for record in records:
                    for column in columns:
                        print(f"column is {record[column]}")
                        requests.append(
                            {
                                "insertText": {
                                    "location": {"index": content_index},
                                    "text": f"\n{record[column]}",
                                }
                            }
                        )
                        content_index += len(record[column]) + padding
                    content_index += 1 #This is to move to the next row in the table

            if content_type == "text":

                indices = find_text_index(document, placeholder)
                paragraph_index = indices["paragraph_index"]
                content_index = indices["content_index"]
                data_rows = section["data"]
                for data in data_rows:
                    requests.append(
                        {
                            "insertText": {
                                "location": {"index": paragraph_index + 1},
                                "text": f"\n{data}",
                            }
                        }
                    )
            

        docs_service.documents().batchUpdate(
            documentId=new_doc_id, body={"requests": requests}
        ).execute()

Step 7: Main Function to Execute the Script

The main function to execute the script, make sure you put the right template name here from your files:

def main():
    with open("template_project_proposal.json", "r") as config_file:
        config = json.load(config_file)
        process_template(config)


if __name__ == "__main__":
    main()

Example JSON and Document Template

Here's what your templates should look like, we pass your Google Doc's template ID, title, and sections with their respective data/types and columns where needed:

{
 "templates": [
        {
            "id": "1708SM-ym9C8BsdqNrkfQfAcdreS2hXmdoXqiKbQycaaQ",
            "title": "Site Tour Proposal",
            "sections": [
                {
                    "placeholder": "{{REQUIREMENTS}}",
                    "type": "table",
                    "data": [
                        {
                            "neighborhood": "Crotona",
                            "property_types": "educational, community facility, office",
                            "square feet": "35,000",
                            "lot_size": "40,000",
                            "use groups": "3,6"
                        }
                    ],
                    "columns": [
                        "neighborhood",
                        "property_types",
                        "square feet",
                        "lot_size",
                        "use groups"
                    ]
                },
                {
                    "placeholder": "{{SHORT_DESCRIPTION}}",
                    "type": "text",
                    "data": [
                        "Site Selection Proposal for Crotona section of the Bronx"
                    ]
                },
                {
                    "placeholder": "{{PROJECT_NAME}}",
                    "type": "text",
                    "data": [
                        "Client Tour Proposal"
                    ]
                }
            ]
        }
    ]
}

And here's what my Doc template looks like with the placeholders:

Google Document Automation Template

Google Document Automation Template

Some things to consider:

  • Google Doc's text indexing is a bit complicated. The official suggestion is to work backwards, because adding text affects the index number of the text that follows. My template is set up to fill the bottom placeholders first.
  • Table content - I haven't quite figured this part out, I'm currently structuring the {PLACEHOLDER} with no spaces before or after in its column. Every other column needs to have exactly one space for the script to work. If you're reading this, I'm still working on a better solution. But if you follow this rule for tables, you should be fine.

The final result:

Google Document Automation Template

Google Document Automation Template

Conclusion

By following these steps, you can automate the generation of documents using Python and Google Docs. I've created Apple Shortcuts that call this script with Contact data to create contact sheets, made tour templates for tenant representation assignments, and much more. This automation will help you streamline your transactions, save time, and reduce errors. Subscribe to RETech.tips for more tutorials and insights on leveraging technology in real estate.



Join the newsletter to be notified when we post new content.


We value your privacy

We use cookies to ensure you get the best experience on our website. By continuing, you consent to the use of cookies.