├── .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 | [][ss1][][ss2][][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 |
{{ company_info }}
31 | {% endif %} 32 | {% with messages = get_flashed_messages(with_categories=true) %} 33 | {% if messages %} 34 |