├── .gitignore ├── Leads.xlsx ├── NOTICE.txt ├── QBOService.py ├── README.md ├── app.py ├── config.py ├── requirements.txt ├── static ├── C2QB_green_btn_lg_default.png └── C2QB_green_btn_lg_hover.png ├── templates └── index.html ├── utils ├── APICallService.py ├── OAuth2Helper.py ├── __init__.py ├── context.py └── excel.py └── views ├── Callout.png ├── Ratesample.png ├── Thumbdown.png └── Thumbup.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .flaskenv 4 | *.pyc 5 | *.pyo 6 | env/ 7 | env* 8 | dist/ 9 | .cache/ 10 | .pytest_cache/ 11 | -------------------------------------------------------------------------------- /Leads.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-QuickBooksV3API-Python/1710efe79cff0d71128837ea04d7418d86c6d04e/Leads.xlsx -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017 Intuit, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 8 | -------------------------------------------------------------------------------- /QBOService.py: -------------------------------------------------------------------------------- 1 | from flask import session 2 | from utils import context, APICallService 3 | import json 4 | import config 5 | 6 | def create_customer(excel_customer, req_context): 7 | """Create a customer object with customer data from a working dictionary""" 8 | full_name = excel_customer['Full Name'] 9 | name_list = full_name.split(' ') 10 | first_name = name_list[0] 11 | last_name = name_list[-1] 12 | if len(name_list) > 2: 13 | middle_name = str(name_list[1:len(name_list) - 1]) 14 | else: 15 | middle_name = '' 16 | 17 | # Create customer object 18 | customer = { 19 | 'GivenName': first_name, 20 | 'MiddleName': middle_name, 21 | 'FamilyName': last_name, 22 | 'PrimaryPhone': { 23 | 'FreeFormNumber': excel_customer['Phone'] 24 | }, 25 | 'PrimaryEmailAddr': { 26 | 'Address': excel_customer['Email'] 27 | } 28 | } 29 | 30 | uri = '/customer?minorversion=' + config.API_MINORVERSION 31 | response = APICallService.post_request(req_context, uri, customer) 32 | return response 33 | 34 | def get_companyInfo(req_context): 35 | """Get CompanyInfo of connected QBO company""" 36 | uri = "/companyinfo/" + req_context.realm_id + "?minorversion=" + config.API_MINORVERSION 37 | response = APICallService.get_request(req_context, uri) 38 | return response 39 | 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Rate your Sample](views/Ratesample.png)][ss1][![Yes](views/Thumbup.png)][ss2][![No](views/Thumbdown.png)][ss3] 2 | 3 | # Data import from Excel to QBO 4 | #### Sample App in Python that implements Connect to Quickbooks button and imports customer data from Excel to QBO company. 5 | 6 | This sample app is meant to provide working example of how to make API calls to Quickbooks. Specifically, this sample application demonstrates the following: 7 | 8 | - OAuth2 sample app for a QuickBooks Online company. 9 | - Creating a QB customer that are added from Excel file using Customer API. 10 | - Gets company data using CompanyInfo API 11 | 12 | Please note that while these examples work, features not called out above are not intended to be taken and used in production business applications. In other words, this is not a seed project to be taken cart blanche and deployed to your production environment. For example, certain concerns are not addressed at all in our samples (e.g. security, privacy, scalability). In our sample apps, we strive to strike a balance between clarity, maintainability, and performance where we can. However, clarity is ultimately the most important quality in a sample app. 13 | 14 | ## Requirements 15 | 1. Python 3.6 16 | 2. A [developer.intuit.com](https://developer.intuit.com/) account 17 | 3. An app on [developer.intuit.com](https://developer.intuit.com/) and the associated app keys: 18 | - Client Id and Client Secret for OAuth2 apps; Configure the RedirectUri[http://localhost:5000/callback] in your app's Keys tab on the Intuit developer account, only Accounting scope needed 19 | 4. This sample app uses several libraries listed in [requirements.txt](requirements.txt) which need to be installed including flask, openpyxl, requests_oauthlib 20 | 21 | ## First Time Instructions 22 | 1. Clone the GitHub repo to your computer 23 | 2. Fill in your [config.py](config.py) file values by copying over from the keys section for your app 24 | 25 | ## Running the code 26 | 1. cd to the project directory 27 | 2. ```pip install -r requirements.txt``` 28 | 3. Run the command: ```python app.py``` for MacOS/Linux 29 | 4. open a browser and enter ```http://localhost:5000``` 30 | 31 | ## High Level Project Overview 32 | 33 | 1. [app.py](app.py) module contains all routes for the Flask web app 34 | 2. [QBOService.py](QBOService.py) class creates a Customer in QBO and gets QBO company info 35 | 36 | ### Utility modules 37 | 3. [excel.py](utils/excel.py) module deals with importing data from [Leads.xlsx](Leads.xlsx) and editing it 38 | 4. [context.py](utils/context.py) class for request context object which has all tokens and realm required to make an API call 39 | 5. [APICallService.py](utils/APICallService.py) module has POST and GET methods for QBO API 40 | 6. [OAuth2Helper.py](utils/OAuth2Helper.py) module has the methos required for OAuth2 flow 41 | 42 | #### Note: For other OAuth2 services like Refresh token, Revoke token, etc, refer to [this](https://github.com/IntuitDeveloper/OAuth2PythonSampleApp) app 43 | 44 | [ss1]: # 45 | [ss2]: https://customersurveys.intuit.com/jfe/form/SV_9LWgJBcyy3NAwHc?check=Yes&checkpoint=SampleApp-QuickBooksV3API-Python&pageUrl=github 46 | [ss3]: https://customersurveys.intuit.com/jfe/form/SV_9LWgJBcyy3NAwHc?check=No&checkpoint=SampleApp-QuickBooksV3API-Python&pageUrl=github 47 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, redirect, url_for, session, g, flash, render_template 2 | # from flask_oauth import OAuth 3 | import requests 4 | import urllib 5 | from werkzeug.exceptions import BadRequest 6 | from QBOService import create_customer, get_companyInfo 7 | from utils import excel, context, OAuth2Helper 8 | import config 9 | 10 | # configuration 11 | SECRET_KEY = 'dev key' 12 | DEBUG = True 13 | 14 | # setup flask 15 | app = Flask(__name__) 16 | app.debug = DEBUG 17 | app.secret_key = SECRET_KEY 18 | 19 | @app.route('/') 20 | def index(): 21 | """Index route""" 22 | global customer_list 23 | customer_list = excel.load_excel() 24 | return render_template( 25 | 'index.html', 26 | customer_dict=customer_list, 27 | title="QB Customer Leads", 28 | ) 29 | 30 | @app.route('/', methods=['POST']) 31 | def update_table(): 32 | """Update Excel file after customer is added in QBO""" 33 | customer_id = request.form['id'] 34 | 35 | request_context = context.RequestContext(session['realm_id'], session['access_token'], session['refresh_token']) 36 | 37 | for customer in customer_list: 38 | if customer['Id'] == customer_id: 39 | # Create customer object 40 | response = create_customer(customer, request_context) 41 | 42 | # If customer added successfully, remove them from html and excel file 43 | if (response.status_code == 200): 44 | font_color = 'green' 45 | new_customer_list = excel.remove_lead(customer_list, customer_id) 46 | flash('Customer successfully added!') 47 | return render_template( 48 | 'index.html', 49 | customer_dict=new_customer_list, 50 | title='QB Customer Leads', 51 | text_color=font_color 52 | ) 53 | else: 54 | font_color = 'red' 55 | flash('Something went wrong: ' + response.text) 56 | return redirect(url_for('index')) 57 | 58 | @app.route('/company-info') 59 | def company_info(): 60 | """Gets CompanyInfo of the connected QBO account""" 61 | request_context = context.RequestContext(session['realm_id'], session['access_token'], session['refresh_token']) 62 | 63 | response = get_companyInfo(request_context) 64 | if (response.status_code == 200): 65 | return render_template( 66 | 'index.html', 67 | customer_dict=customer_list, 68 | company_info='Company Name: ' + response.json()['CompanyInfo']['CompanyName'], 69 | title='QB Customer Leads', 70 | ) 71 | else: 72 | return render_template( 73 | 'index.html', 74 | customer_dict=customer_list, 75 | company_info=response.text, 76 | title='QB Customer Leads', 77 | ) 78 | 79 | @app.route('/auth') 80 | def auth(): 81 | """Initiates the Authorization flow after getting the right config value""" 82 | params = { 83 | 'scope': 'com.intuit.quickbooks.accounting', 84 | 'redirect_uri': config.REDIRECT_URI, 85 | 'response_type': 'code', 86 | 'client_id': config.CLIENT_ID, 87 | 'state': csrf_token() 88 | } 89 | url = OAuth2Helper.get_discovery_doc()['authorization_endpoint'] + '?' + urllib.parse.urlencode(params) 90 | return redirect(url) 91 | 92 | @app.route('/reset-session') 93 | def reset_session(): 94 | """Resets session""" 95 | session.pop('qbo_token', None) 96 | session['is_authorized'] = False 97 | return redirect(request.referrer or url_for('index')) 98 | 99 | @app.route('/callback') 100 | def callback(): 101 | """Handles callback only for OAuth2""" 102 | #session['realmid'] = str(request.args.get('realmId')) 103 | state = str(request.args.get('state')) 104 | error = str(request.args.get('error')) 105 | if error == 'access_denied': 106 | return redirect(index) 107 | if state is None: 108 | return BadRequest() 109 | elif state != csrf_token(): # validate against CSRF attacks 110 | return BadRequest('unauthorized') 111 | 112 | auth_code = str(request.args.get('code')) 113 | if auth_code is None: 114 | return BadRequest() 115 | 116 | bearer = OAuth2Helper.get_bearer_token(auth_code) 117 | realmId = str(request.args.get('realmId')) 118 | 119 | # update session here 120 | session['is_authorized'] = True 121 | session['realm_id'] = realmId 122 | session['access_token'] = bearer['access_token'] 123 | session['refresh_token'] = bearer['refresh_token'] 124 | 125 | return redirect(url_for('index')) 126 | 127 | def csrf_token(): 128 | token = session.get('csrfToken', None) 129 | if token is None: 130 | token = OAuth2Helper.secret_key() 131 | session['csrfToken'] = token 132 | return token 133 | 134 | if __name__ == '__main__': 135 | app.run() -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | DEBUG = False 2 | SQLALCHEMY_ECHO = False 3 | 4 | # OAuth2 credentials 5 | CLIENT_ID= 'EnterClientIDHere' 6 | CLIENT_SECRET = 'EnterClientSecretHere' 7 | REDIRECT_URI = 'http://localhost:5000/callback' 8 | 9 | # Choose environment; default is sandbox 10 | ENVIRONMENT = 'Sandbox' 11 | # ENVIRONMENT = 'Production' 12 | 13 | # Set to latest at the time of updating this app, can be be configured to any minor version 14 | API_MINORVERSION = '23' 15 | 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.13.0 2 | Flask==0.12 3 | # Flask_OAuth==0.12 4 | Werkzeug==0.11.15 5 | openpyxl==2.4.4 6 | requests_oauthlib==0.8.0 7 | -------------------------------------------------------------------------------- /static/C2QB_green_btn_lg_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-QuickBooksV3API-Python/1710efe79cff0d71128837ea04d7418d86c6d04e/static/C2QB_green_btn_lg_default.png -------------------------------------------------------------------------------- /static/C2QB_green_btn_lg_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-QuickBooksV3API-Python/1710efe79cff0d71128837ea04d7418d86c6d04e/static/C2QB_green_btn_lg_hover.png -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% block body %} 2 |

Python Sample App

3 | 4 | 13 | 14 | 15 | 26 | {% if session['is_authorized'] == True %} 27 | 28 | 29 | {% if company_info %} 30 |

{{ company_info }}

31 | {% endif %} 32 | {% with messages = get_flashed_messages(with_categories=true) %} 33 | {% if messages %} 34 | 39 | {% endif %} 40 | {% endwith %} 41 |

Potential Customers from Leads.xlsx

42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {% for customer in customer_dict %} 54 | 55 | 56 | 58 | 59 | 60 | 61 | 62 | 63 | {% endfor %} 64 |
No.IDFull NamePhoneEmail
{{ loop.index }}{{ customer['Id'] }} 57 | {% set req_id = customer['Id'] %}{{ customer['Full Name'] }}{{ customer['Phone'] }}{{ customer['Email'] }}
65 |
66 | {% else %} 67 | 68 |

Hi, welcome to Intuit Developer's Python Flask app. This app adds customer from Excel file to QBO and also shows how to get CompanyInfo of the connected QBO account. This app can be used with both OAuth1 and OAuth2. Don't forget to add your app's token in config file.

69 | 70 | 71 | 72 | {% endif %} 73 | 74 | {% endblock %} 75 | 76 | -------------------------------------------------------------------------------- /utils/APICallService.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests_oauthlib import OAuth1 3 | import config 4 | import json 5 | 6 | def get_request(req_context, uri): 7 | """HTTP GET request for QBO API""" 8 | headers = { 'Accept': "application/json", 9 | 'User-Agent': "PythonSampleApp1" 10 | } 11 | if config.ENVIRONMENT == "Sandbox": 12 | base_url = "https://sandbox-quickbooks.api.intuit.com/v3/company/" 13 | else: 14 | base_url = "https://quickbooks.api.intuit.com/v3/company/" 15 | url = base_url + req_context.realm_id + uri 16 | print(url) 17 | if config.AUTH_TYPE == "OAuth2": 18 | headers['Authorization'] = "Bearer " + req_context.access_token 19 | req = requests.get(url, headers=headers) 20 | else: 21 | auth = OAuth1(req_context.consumer_key, req_context.consumer_secret, req_context.access_key, req_context.access_secret) 22 | req = requests.get(url, auth=auth, headers=headers) 23 | return req 24 | 25 | def post_request(req_context, uri, payload): 26 | """HTTP POST request for QBO API""" 27 | headers = { 'Accept': "application/json", 28 | 'content-type': "application/json; charset=utf-8", 29 | 'User-Agent': "PythonSampleApp1" 30 | } 31 | 32 | if config.ENVIRONMENT == "Sandbox": 33 | base_url = "https://sandbox-quickbooks.api.intuit.com/v3/company/" 34 | else: 35 | base_url = "https://quickbooks.api.intuit.com/v3/company/" 36 | url = base_url + req_context.realm_id + uri 37 | 38 | if config.AUTH_TYPE == "OAuth2": 39 | headers['Authorization'] = "Bearer " + req_context.access_token 40 | req = requests.post(url, headers=headers, data=json.dumps(payload)) 41 | else: 42 | auth = OAuth1(req_context.consumer_key, req_context.consumer_secret, req_context.access_key, req_context.access_secret) 43 | req = requests.post(url, auth=auth, headers=headers, data=json.dumps(payload)) 44 | return req 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /utils/OAuth2Helper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import base64 3 | import json 4 | import random 5 | import config 6 | 7 | def get_bearer_token(auth_code): 8 | """Gets bearer token from authorization code""" 9 | token_endpoint = get_discovery_doc()['token_endpoint'] 10 | auth_header = 'Basic ' + to_base64(config.CLIENT_ID + ':' + config.CLIENT_SECRET) 11 | headers = { 12 | 'Accept': 'application/json', 13 | 'content-type': 'application/x-www-form-urlencoded', 14 | 'Authorization': auth_header 15 | } 16 | payload = { 17 | 'code': auth_code, 18 | 'redirect_uri': config.REDIRECT_URI, 19 | 'grant_type': 'authorization_code' 20 | } 21 | r = requests.post(token_endpoint, data=payload, headers=headers) 22 | if r.status_code != 200: 23 | return r.text 24 | bearer = json.loads(r.text) 25 | return bearer 26 | 27 | def get_discovery_doc(): 28 | """Gets OAuth2 discover document based on configured environment""" 29 | if config.ENVIRONMENT == "Sandbox": 30 | req = requests.get("https://developer.intuit.com/.well-known/openid_sandbox_configuration/") 31 | else: 32 | req = requests.get("https://developer.intuit.com/.well-known/openid_configuration/") 33 | if req.status_code >= 400: 34 | return '' 35 | discovery_doc = req.json() 36 | return discovery_doc 37 | 38 | def to_base64(s): 39 | """String to Base64""" 40 | return base64.b64encode(bytes(s, 'utf-8')).decode() 41 | 42 | def random_string(length, allowed_chars='abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): 43 | return ''.join(random.choice(allowed_chars) for i in range(length)) 44 | 45 | def secret_key(): 46 | chars = 'abcdefghijklmnopqrstuvwxyz0123456789' 47 | return random_string(40, chars) -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-QuickBooksV3API-Python/1710efe79cff0d71128837ea04d7418d86c6d04e/utils/__init__.py -------------------------------------------------------------------------------- /utils/context.py: -------------------------------------------------------------------------------- 1 | import config 2 | 3 | class RequestContext(object): 4 | """The context class sets the realm id with the app's Client tokens every time user authorizes an app for their QB company""" 5 | def __init__(self, realm_id, access_token, refresh_token): 6 | self.client_id = config.CLIENT_ID 7 | self.client_secret = config.CLIENT_SECRET 8 | self.realm_id = realm_id 9 | self.access_token = access_token 10 | self.refresh_token = refresh_token 11 | 12 | def __str__(self): 13 | return self.realm_id 14 | 15 | class RequestContextOAuth1(object): 16 | """The context class sets the realm id with the app's Consumer tokens every time user authorizes an app for their QB company""" 17 | def __init__(self, realm_id, access_key, access_secret): 18 | self.consumer_key = config.CONSUMER_KEY 19 | self.consumer_secret = config.CONSUMER_SECRET 20 | self.realm_id = realm_id 21 | self.access_key = access_key 22 | self.access_secret = access_secret 23 | 24 | def __str__(self): 25 | return self.realm_id 26 | 27 | -------------------------------------------------------------------------------- /utils/excel.py: -------------------------------------------------------------------------------- 1 | import os 2 | import openpyxl 3 | 4 | APP_ROOT = os.path.dirname(os.path.abspath(__file__)) # refers to application_top 5 | APP_STATIC = os.path.join(APP_ROOT, '../Leads.xlsx') 6 | 7 | excel_file = APP_STATIC 8 | 9 | def load_excel(): 10 | """Excel is loaded into a list of dictionaries""" 11 | wb = openpyxl.load_workbook(filename=excel_file) 12 | ws = wb['data'] 13 | 14 | # Parse data from Excel file 15 | dict_list = [] 16 | keys = {'A': 'Id', 'B': 'Full Name', 'C': 'Email', 'D': 'Phone'} 17 | for row in ws.iter_rows(min_row=2, max_col=4): 18 | d = {} 19 | for cell in row: 20 | if cell.column == 'A' and cell.value == None: 21 | break 22 | elif cell.column != 'A' and cell.value == None: 23 | d[keys[cell.column]] = '' 24 | else: 25 | d[keys[cell.column]] = str(cell.value) 26 | 27 | if len(d.keys()) != 0: 28 | dict_list.append(d) 29 | return dict_list 30 | 31 | def remove_lead(customer_list, customer_id): 32 | """Update lead in excel worksheet and working dictionary; Removing from excel leaves a blank row in the excel""" 33 | wb = openpyxl.load_workbook(filename=excel_file) 34 | ws = wb['data'] 35 | for row in ws.iter_rows(min_row=2, max_col=4): 36 | for cell in row: 37 | if cell.value == None or cell.column == 'B' or cell.column == 'C' or cell.column == 'D': 38 | continue 39 | elif cell.column == 'A' and str(cell.value) == customer_id: 40 | cell_row = str(cell.row) 41 | cell.value = None 42 | ws['B' + cell_row].value = None 43 | ws['C' + cell_row].value = None 44 | ws['D' + cell_row].value = None 45 | break 46 | wb.save(filename=excel_file) 47 | 48 | # Remove selected lead from excel sheet 49 | for customer in customer_list: 50 | if customer['Id'] == customer_id: 51 | customer_list.pop(customer_list.index(customer)) 52 | return customer_list 53 | -------------------------------------------------------------------------------- /views/Callout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-QuickBooksV3API-Python/1710efe79cff0d71128837ea04d7418d86c6d04e/views/Callout.png -------------------------------------------------------------------------------- /views/Ratesample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-QuickBooksV3API-Python/1710efe79cff0d71128837ea04d7418d86c6d04e/views/Ratesample.png -------------------------------------------------------------------------------- /views/Thumbdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-QuickBooksV3API-Python/1710efe79cff0d71128837ea04d7418d86c6d04e/views/Thumbdown.png -------------------------------------------------------------------------------- /views/Thumbup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-QuickBooksV3API-Python/1710efe79cff0d71128837ea04d7418d86c6d04e/views/Thumbup.png --------------------------------------------------------------------------------