DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Why and How to Transition to SaaS Cloud Enterprise Applications
  • Data Governance – Data Privacy and Security – Part 1
  • What Is API-First?
  • Designing AI-Assisted Integration Pipelines for Enterprise SaaS

Trending

  • Building AI-Powered Java Applications With Jakarta EE and LangChain4j
  • Building Threat Intelligence Pipelines Using Python, APIs, and Elasticsearch
  • 5 AI Security Incidents That Broke Things in Production (and What They Have in Common)
  • Alternative Structured Concurrency
  1. DZone
  2. Software Design and Architecture
  3. Integration
  4. How To Integrate Quickbooks With Your SaaS Platform

How To Integrate Quickbooks With Your SaaS Platform

Follow a recent task to build a Quickbooks Online (QBO) integration to process the Profit and Loss report.

By 
Hassan Syyid user avatar
Hassan Syyid
·
Mar. 31, 21 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
11.3K Views

Join the DZone community and get the full member experience.

Join For Free

Recently I was tasked with building a Quickbooks Online (QBO) integration to process the Profit and Loss (P&L) report and determine the revenues, cost of goods sold (COGS), and expenses of departments  —  here's how I did it.

This article is not about building an OAuth flow or pulling data from the Quickbooks API using a cron job. That process is relatively boilerplate and has been solved by tools such as the one I'll be using. If building that yourself interests you, check out my article on using Singer to do just that.

Instead, this article will walk you through the harder part  —  how to make sense of the data.

Get the Data

As I mentioned above, for this article, I'll be using hotglue to handle all the boilerplate of building a QBO integration for a SaaS platform. This includes:

  • Creating an OAuth authorization flow
  • Enables users to connect Quickbooks in your UI and monitor the connection
  • Pulling the necessary data from the Quickbooks API

Create the Authorization Flow

In hotglue, I will create a new flow and add Quickbooks sandbox as a source. To do this, I merely provide my OAuth credentials and login to my Quickbooks sandbox account. If you do not have OAuth credentials already, you can just select 'Use testing tokens.'

Create the Quickbooks OAuth flow

Create the Quickbooks OAuth flow

Select the Data We Need

Now we can select what data we need from Quickbooks. We will select Account (Chart of Accounts), Item (Products and Services), Purchase (Expenses), Invoice, and Profit and Loss Report. 

This will provide us with the reference data we'll need to accurately categorize transactions.

Quickbooks selected data

Quickbooks selected data

Add the Integration To Your UI

With some basic JavaScript, I was able to embed the hotglue widget, which enables users to connect their Quickbooks account, refresh the synced data, and monitor when it was last refreshed.

Embedded integration

Embedded integration

Tag the Transactions

Quickbooks does not natively support tagging transactions under a specific department or project. To accomplish this, I used the Quickbooks class field to tag transactions under a Department/Project. For example, the Invoice below has been tagged Pest Control:Project X meaning Pest Control department, Project X.

Tagged Quickbooks Invoice using Class

Tagged Quickbooks Invoice using Class

Process the Data

Now for the fun part. We'll use a Python script using pandas and gluestick to convert the data from Quickbooks into a JSON file that looks something like below.

What We’re Trying To Produce

As you can see below, the JSON file takes every transaction in our P&L report and categorizes it under Project Xin the Pest Control department. Further, it categorizes each transaction as revenue , cogs (Cost of Goods Sold), labor , and costs (Expenses).

{
  "Pest Control": {
    "Project X": {
      "revenues": {
        "2021-11": {
          "Pest Control Services": {
            "Pest Control Services": {
              "Bed Bug Removal": {
                "total": 350,
                "qty": "10",
                "rate": 35
              }
            }
          }
        }
      },
      "cogs": {
        "2021-11": {
          "Supplies & Materials - COGS": {
            "Pest Control Services": {
              "Bed Bug Removal": {
                "total": 100,
                "qty": 1,
                "rate": 100
              }
            }
          }
        }
      },
      "labor": {
        "2021-11": {
          "Employee": {
            "Inspector": {
              "Employee Benefits": 10.8,
              "Payroll Tax": 61.2,
              "Payroll Wage Expenses": 800
            }
          }
        }
      },
      "costs": {
        "2021-11": {
          "Project Expenses": {
            "Automobile": {
              "Fuel": 100
            }
          }
        }
      }
    }
  }
}


Write the Script

hotglue has built-in functionality to run a Python script every time new data is synced from the Quickbooks API and will avoid syncing old data again (incremental sync).

You can go through each step of the script on Gist or below:

Python
 




x


 
1
import gluestick as gs
2
import pandas as pd
3
import os
4
import json
5
import ast
6
from datetime import datetime
7
from lib import util
8

          
9
# Let's establish the standard hotglue input/output directories
10

          
11
# standard directory for hotglue
12
ROOT_DIR = os.environ.get("ROOT_DIR", ".")
13
INPUT_DIR = f"{ROOT_DIR}/sync-output"
14
OUTPUT_DIR = f"{ROOT_DIR}/etl-output"
15
SNAPSHOT_DIR = f"{ROOT_DIR}/snapshots"
16

          
17
# Let's start by reading the data.
18
# We will use the gluestick package to read the raw data in the input folder into a dictionary of pandas dataframes using the read_csv_folder function.
19

          
20
# Read input data
21
input_data = gs.read_csv_folder(INPUT_DIR)
22
Process the snapshots
23

          
24
# Now we'll do some basic snapshotting to persist the data (you can skip this)
25
# Accounts
26
if input_data.get("Account") is not None:
27
    util.update_snapshot(
28
        SNAPSHOT_DIR, 
29
        "Account",
30
        ['Id'], 
31
        input_data["Account"]
32
     )
33

          
34
# Invoices
35
if input_data.get("Invoice") is not None:
36
    util.update_snapshot(
37
        SNAPSHOT_DIR, 
38
        "Invoice",
39
        ['Id'], 
40
        input_data["Invoice"]
41
     )
42

          
43
# Item
44
if input_data.get("Item") is not None:
45
    util.update_snapshot(
46
        SNAPSHOT_DIR, 
47
        "Item",
48
        ['Id'], 
49
        input_data["Item"]
50
     )
51

          
52
# Purchase
53
if input_data.get("Purchase") is not None:
54
    util.update_snapshot(
55
        SNAPSHOT_DIR, 
56
        "Purchase",
57
        ['Id'], 
58
        input_data["Purchase"]
59
     )
60

          
61
# Update the input_data with the snapshots, if they exist
62
input_data["Account"] = util.get_snapshot(SNAPSHOT_DIR, "Account")
63
input_data["Invoice"] = util.get_snapshot(SNAPSHOT_DIR, "Invoice")
64
input_data["Item"] = util.get_snapshot(SNAPSHOT_DIR, "Item")
65
input_data["Purchase"] = util.get_snapshot(SNAPSHOT_DIR, "Purchase")
66

          
67
# Process the P&L report
68
pl_df = input_data['ProfitAndLossDetailReport']
69
Split the class names into Department, Project, Worker Type, and Worker Role, if applicable
70

          
71
# Extract the class names into new columns
72
split_pl = pl_df.Class.str.split(pat=":", expand=True)
73

          
74
if len(split_pl.columns) == 4:
75
    pl_df[['Department', 'Project', 'Worker Type', 'Worker Role']] = split_pl
76
else:
77
    pl_df[['Department', 'Project']] = split_pl
78
Create the dictionary where all new data is saved (actuals)
79

          
80
# Process the actual data into a Python dict
81
actuals = {}
82

          
83
def handle_defaults(obj, args):
84
    if len(args) == 0:
85
        return
86

          
87
    arg = args[0]
88
    obj.setdefault(arg, {})
89
    args.pop(0)
90
    handle_defaults(obj[arg], args)
91

          
92
# Revenues
93
invoices_df = pl_df[pl_df['TransactionType'] == 'Invoice']
94
invoices = input_data['Invoice']
95
items = input_data['Item']
96

          
97
for index, row in invoices_df.iterrows():
98
    categories = ast.literal_eval(row['Categories'])
99
    department = row['Department']
100
    project = row['Project']
101
    docno = row['Num']
102
    date = datetime.strptime(row['Date'], '%Y-%m-%d')
103
    date = date.isocalendar()
104
    period = f"{date[0]}-{date[1]}"
105
    # Get item name
106
    cat = categories[2]
107

          
108
    invoice = invoices[invoices['DocNumber'] == docno].iloc[0]
109
    invoice_line = ast.literal_eval(invoice['Line'])
110
    
111
    for l in invoice_line:
112
        if l['DetailType'] == 'SalesItemLineDetail':
113
            item = ast.literal_eval(l['SalesItemLineDetail']['ItemRef'])
114
            item_name = item['name']
115
            item_data = items[items['FullyQualifiedName'] == item_name].iloc[0]
116
            item_name = item_name.split(":")
117
            item_category = item_name[0]
118
            item_name = item_name[1]
119
        
120
            handle_defaults(actuals, [department, project, 'revenues', period, cat, item_category, item_name])
121

          
122
            actuals[department][project]['revenues'][period][cat][item_category][item_name] = {
123
                'total': l['Amount'],
124
                'qty': item['value'],
125
                'rate': item_data['UnitPrice']
126
            }
127

          
128
# Get all the expenses and extract the Product/Service + price
129
purchases = input_data['Purchase']
130
purchases = gs.explode_json_to_cols(purchases, "Line", max_level=1)
131
purchases = purchases[purchases['Line.ItemBasedExpenseLineDetail'].notnull()]
132
purchases = gs.explode_json_to_cols(purchases, 'Line.ItemBasedExpenseLineDetail', max_level=1)
133

          
134
purchases = gs.explode_json_to_cols(purchases, 'Line.ItemBasedExpenseLineDetail.ClassRef', max_level=1)
135
purchases = gs.explode_json_to_cols(purchases, 'Line.ItemBasedExpenseLineDetail.ItemRef', max_level=1)
136

          
137
purchases = purchases.rename(columns={
138
    'Line.ItemBasedExpenseLineDetail.ClassRef.name': 'ClassName',
139
    'Line.ItemBasedExpenseLineDetail.UnitPrice': 'ItemRate',
140
    'Line.ItemBasedExpenseLineDetail.Qty': 'ItemQty',
141
    'Line.ItemBasedExpenseLineDetail.ItemRef.name': 'ItemName'
142
})
143

          
144
purchases = purchases[purchases['ClassName'].notnull()]
145

          
146
# Process each expense
147
expenses_df = pl_df[pl_df['TransactionType'] == 'Expense']
148

          
149
for index, row in expenses_df.iterrows():
150
    categories = ast.literal_eval(row['Categories'])
151
    department = row['Department']
152
    project = row['Project']
153
    date = datetime.strptime(row['Date'], '%Y-%m-%d')
154
    date = date.isocalendar()
155
    period = f"{date[0]}-{date[1]}"
156
    
157
    # COGS
158
    if categories[1] == 'Cost of Goods Sold':
159
        item = categories[2]
160
        
161
        # Get items
162
        relevant = purchases[purchases['ClassName'] == f'{department}:{project}']
163

          
164
        for index, item_row in relevant.iterrows():            
165
            item_name = item_row['ItemName']
166
            item_name = item_name.split(":")
167
            item_category = item_name[0]
168
            item_name = item_name[1]
169
            
170
            handle_defaults(actuals, [department, project, 'cogs', period, item, item_category, item_name])
171
            actuals[department][project]['cogs'][period][item][item_category][item_name] = {
172
                'total': item_row['Line.Amount'],
173
                'qty': item_row['ItemQty'],
174
                'rate': item_row['ItemRate']
175
            }
176

          
177
    # Costs
178
    if categories[1] == 'Expenses' and row['Worker Type'] is None:
179
        print(categories)
180
        expense_type = categories[2]
181
        item_cat = categories[3]
182
        item = categories[4]
183
        handle_defaults(actuals, [department, project, 'costs', period, expense_type, item_cat, item])
184
        actuals[department][project]['costs'][period][expense_type][item_cat][item] = row['Amount']
185
    
186
    # Labor
187
    if categories[1] == 'Expenses' and row['Worker Type'] is not None:
188
        worker_type = row['Worker Type']
189
        worker_role = row['Worker Role']
190
        item_cat = categories[2]
191

          
192
        handle_defaults(actuals, [department, project, 'labor', period, worker_type, worker_role, item_cat])
193
        actuals[department][project]['labor'][period][worker_type][worker_role][item_cat] = row['Amount']
194

          
195
# Preview the result
196
print(json.dumps(actuals, indent=4))
197

          
198
# Export the data to a JSON file
199
with open(f'{OUTPUT_DIR}/actuals.json', 'w') as outfile:
200
    json.dump(actuals, outfile)
201

          



If you're using hotglue to create your integration, you can learn how to write and deploy scripts in the docs.

Run It All Together

Choose Where the Data Should Go

If you're using hotglue, you need to specify a place for the final data to be delivered (a target). By default, hotglue will email new data to you using Mailgun. I used Google Cloud Storage for our Quickbooks integration.

Configure Google Cloud Storage target

Configure Google Cloud Storage target

Run a Job

Now we can run a sync job. This will connect to the Quickbooks API, query any new data, save it as a CSV, run our transformation script, and upload the resulting data to our target (Google Cloud Storage).

Quickbooks job completed

Quickbooks job completed

Now we can see the final actuals.json file in Google Cloud Storage!

Resulting data in Google Cloud Storage bucket

Resulting data in the Google Cloud Storage bucket

Conclusion

In this article, we went through creating a Quickbooks integration and processing the resulting CSV data into a JSON payload that can be used by a SaaS application.

Hopefully, this helps you consider how to build your own SaaS integrations. I'd highly recommend using a tool like hotglue to handle the boilerplate integration steps so you can focus on extracting the data you need rather than authorization and orchestration issues.

Thanks for reading!

Integration Data (computing) SaaS Google Cloud Storage authentication

Opinions expressed by DZone contributors are their own.

Related

  • Why and How to Transition to SaaS Cloud Enterprise Applications
  • Data Governance – Data Privacy and Security – Part 1
  • What Is API-First?
  • Designing AI-Assisted Integration Pipelines for Enterprise SaaS

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook