Skip to content

Commit

Permalink
Merge branch 'master' into sync-upstream
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredthecoder authored Aug 19, 2024
2 parents b252a73 + 045cded commit f4e1010
Show file tree
Hide file tree
Showing 31 changed files with 308 additions and 699 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3
Expand All @@ -27,7 +27,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
python -m pip install flake8 pytest coverage pytest-cov
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
Expand All @@ -37,4 +37,8 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest tests/unit
pytest tests/unit --cov
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
30 changes: 30 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
Changelog
=========

* 0.9.10 (August 7, 2024)
* Update intuit-oauth dependency
* Fix issues with Invoice Sharable Link
* Added optional params to get

* 0.9.9 (July 9, 2024)
* Removed simplejson
* Added use_decimal option (See PR: https://github.com/ej2/python-quickbooks/pull/356 for details)

* 0.9.8 (May 20, 2024)
* Added ItemAccountRef to SalesItemLineDetail
* Updated from_json example in readme

* 0.9.7 (March 12, 2024)
* Update intuit-oauth dependency
* Updated CompanyCurrency to ref to use Code instead of Id
* Added missing CurrentRef property from customer object
* Made improvements to file attachment handling

* 0.9.6 (January 2, 2024)
* Replace RAuth with requests_oauthlib
* Removed python 2 code from client.py
* Removed unused dependencies from Pipfile
* Added new fields to Employee object
* Added VendorAddr to Bill object
* Added new fields to Estimate object
* Fix TaxInclusiveAmt and vendor setting 1099 creation
* Updated readme and contributing

* 0.9.5 (November 1, 2023)
* Added the ability to void all voidable QB types
* Added to_ref to CreditMemo object
Expand Down
16 changes: 8 additions & 8 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]
coverage = "*"
twine = "*"
pytest = "*"
pytest-cov = "*"

[packages]
urllib3 = ">=1.26.5"
bleach = ">=3.3.0"
# Not needed as installed by Uncat core
# intuit-oauth = {git = "git+https://github.com/uncategorizedexpense/oauth-pythonclient.git@master#egg=intuit-oauth"}
rauth = ">=0.7.3"
urllib3 = ">=2.1.0"
# intuit-oauth = "==1.2.6"
requests = ">=2.31.0"
simplejson = ">=3.19.1"
nose = "*"
coverage = "*"
twine = "*"
requests_oauthlib = ">=1.3.1"
setuptools = "*"
578 changes: 0 additions & 578 deletions Pipfile.lock

Large diffs are not rendered by default.

34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ python-quickbooks
=================

[![Python package](https://github.com/ej2/python-quickbooks/actions/workflows/python-package.yml/badge.svg)](https://github.com/ej2/python-quickbooks/actions/workflows/python-package.yml)
[![Coverage Status](https://coveralls.io/repos/github/ej2/python-quickbooks/badge.svg?branch=master)](https://coveralls.io/github/ej2/python-quickbooks?branch=master)
[![codecov](https://codecov.io/gh/ej2/python-quickbooks/graph/badge.svg?token=AKXS2F7wvP)](https://codecov.io/gh/ej2/python-quickbooks)
[![](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ej2/python-quickbooks/blob/master/LICENSE)
[![PyPI](https://img.shields.io/pypi/v/python-quickbooks)](https://pypi.org/project/python-quickbooks/)

Expand Down Expand Up @@ -54,14 +54,14 @@ Then create a QuickBooks client object passing in the AuthClient, refresh token,
company_id='COMPANY_ID',
)

If you need to access a minor version (See [Minor versions](https://developer.intuit.com/docs/0100_quickbooks_online/0200_dev_guides/accounting/minor_versions) for
If you need to access a minor version (See [Minor versions](https://developer.intuit.com/app/developer/qbo/docs/learn/explore-the-quickbooks-online-api/minor-versions#working-with-minor-versions) for
details) pass in minorversion when setting up the client:

client = QuickBooks(
auth_client=auth_client,
refresh_token='REFRESH_TOKEN',
company_id='COMPANY_ID',
minorversion=59
minorversion=69
)

Object Operations
Expand All @@ -74,7 +74,9 @@ List of objects:

**Note:** The maximum number of entities that can be returned in a
response is 1000. If the result size is not specified, the default
number is 100. (See [Intuit developer guide](https://developer.intuit.com/docs/0100_accounting/0300_developer_guides/querying_data) for details)
number is 100. (See [Query operations and syntax](https://developer.intuit.com/app/developer/qbo/docs/learn/explore-the-quickbooks-online-api/data-queries) for details)

**Warning:** You should never allow user input to pass into a query without sanitizing it first! This library DOES NOT sanitize user input!

Filtered list of objects:

Expand Down Expand Up @@ -104,6 +106,8 @@ List with custom Where Clause (do not include the `"WHERE"`):

customers = Customer.where("Active = True AND CompanyName LIKE 'S%'", qb=client)



List with custom Where and ordering

customers = Customer.where("Active = True AND CompanyName LIKE 'S%'", order_by='DisplayName', qb=client)
Expand All @@ -112,7 +116,7 @@ List with custom Where Clause and paging:

customers = Customer.where("CompanyName LIKE 'S%'", start_position=1, max_results=25, qb=client)

Filtering a list with a custom query (See [Intuit developer guide](https://developer.intuit.com/docs/0100_accounting/0300_developer_guides/querying_data) for
Filtering a list with a custom query (See [Query operations and syntax](https://developer.intuit.com/app/developer/qbo/docs/learn/explore-the-quickbooks-online-api/data-queries) for
supported SQL statements):

customers = Customer.query("SELECT * FROM Customer WHERE Active = True", qb=client)
Expand Down Expand Up @@ -248,15 +252,23 @@ One example is `include=allowduplicatedocnum` on the Purchase object. You can ad

purchase.save(qb=self.qb_client, params={'include': 'allowduplicatedocnum'})

Other operations
Sharable Invoice Link
----------------
Add Sharable link for an invoice sent to external customers (minorversion must be set to 36 or greater):
To add a sharable link for an invoice, make sure the AllowOnlineCreditCardPayment is set to True and BillEmail is set to a invalid email address:

invoice.AllowOnlineCreditCardPayment = True
invoice.BillEmail = EmailAddress()
invoice.BillEmail.Address = '[email protected]'

invoice.invoice_link = true
When you query the invoice include the following params (minorversion must be set to 36 or greater):

invoice = Invoice.get(id, qb=self.qb_client, params={'include': 'invoiceLink'})

Void an invoice:

Void an invoice
----------------
Call `void` on any invoice with an Id:

invoice = Invoice()
invoice.Id = 7
invoice.void(qb=client)
Expand All @@ -273,10 +285,10 @@ Converting an object to JSON data:

Loading JSON data into a quickbooks object:

account = Account()
account.from_json(
account = Account.from_json(
{
"AccountType": "Accounts Receivable",
"AcctNum": "123123",
"Name": "MyJobs"
}
)
Expand Down
23 changes: 13 additions & 10 deletions contributing.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
# Contributing

I am accepting pull requests. Sometimes life gets busy and it takes me a little while to get everything merged in. To help speed up the process, please write tests to cover your changes. I will review/merge them as soon as possible.
I am accepting pull requests. Sometimes life gets busy and it takes me a little while to get everything reviewed and merged in. To help speed up the process, please write tests to cover your changes. I will review/merge them as soon as possible.

# Testing

I use [nose](https://nose.readthedocs.io/en/latest/index.html) and [Coverage](https://coverage.readthedocs.io/en/latest/) to run the test suite.
I use [pytest](https://docs.pytest.org/en/7.4.x/contents.html), [Coverage](https://coverage.readthedocs.io/en/latest/), and [pytest-cov](https://pytest-cov.readthedocs.io/en/latest/) to run the test suite.

*WARNING*: The Tests connect to the QBO API and create/modify/delete data. DO NOT USE A PRODUCTION ACCOUNT!

## Testing setup:

1. Create/login into your [Intuit Developer account](https://developer.intuit.com).
2. On your Intuit Developer account, create a Sandbox company and an App.
3. Go to the Intuit Developer OAuth 2.0 Playground and fill out the form to get an **access token** and **refresh token**. You will need to copy the following values into your enviroment variables:
3. Go to the Intuit Developer OAuth 2.0 Playground and fill out the form to get a **refresh token**. You will need to copy the following values into your enviroment variables:
```
export CLIENT_ID="<Client ID>"
export CLIENT_SECRET="<Client Secret>"
export COMPANY_ID="<Realm ID>"
export ACCESS_TOKEN="<Access token>"
export COMPANY_ID="<Realm ID>"
export REFRESH_TOKEN="<Refresh token>"
```

*Note*: You will need to update the access token when it expires.
*Note*: You will need to update the refresh token when it expires.

5. Install *nose* and *coverage*. Using Pip:
`pip install nose coverage`
5. Install *pytest*, *coverage*, and *pytest-cov*. Using Pip (or whatever):
`pip install pytest coverage pytest-cov`

6. Run `nosetests . --with-coverage --cover-package=quickbooks`
6. Run all tests: ```pytest --cov```
Run only unit tests: ```pytest tests/unit --cov```
Run only integration tests: ```pytest tests/integration --cov```



## Creating new tests
Normal Unit tests that do not connect to the QBO API should be located under `test/unit` Test that connect to QBO API should go under `tests/integration`. Inheriting from `QuickbooksTestCase` will automatically setup `self.qb_client` to use when connecting to QBO.
Normal Unit tests that do not connect to the QBO API should be located under `test/unit`. Tests that connect to QBO API should go under `tests/integration`. Inheriting from `QuickbooksTestCase` will automatically setup `self.qb_client` to use when connecting to QBO.

Example:
```
Expand Down
57 changes: 25 additions & 32 deletions quickbooks/client.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,16 @@
import warnings

try: # Python 3
import http.client as httplib
from urllib.parse import parse_qsl
from functools import partial
to_bytes = lambda value, *args, **kwargs: bytes(value, "utf-8", *args, **kwargs)
except ImportError: # Python 2
import httplib
from urlparse import parse_qsl
to_bytes = str

import http.client as httplib
import textwrap
import codecs
import json

from . import exceptions
import base64
import hashlib
import hmac
import decimal

from . import exceptions
from requests_oauthlib import OAuth2Session

try:
from rauth import OAuth1Session, OAuth1Service, OAuth2Session
except ImportError:
print("Please import Rauth:\n\n")
print("http://rauth.readthedocs.org/en/latest/\n")
raise
def to_bytes(value, *args, **kwargs):
return bytes(value, "utf-8", *args, **kwargs)


class Environments(object):
Expand All @@ -40,6 +26,7 @@ class QuickBooks(object):
minorversion = None
verifier_token = None
invoice_link = False
use_decimal = False

sandbox_api_url_v3 = "https://sandbox-quickbooks.api.intuit.com/v3"
api_url_v3 = "https://quickbooks.api.intuit.com/v3"
Expand Down Expand Up @@ -95,17 +82,23 @@ def __new__(cls, **kwargs):
if 'verifier_token' in kwargs:
instance.verifier_token = kwargs.get('verifier_token')

if 'use_decimal' in kwargs:
instance.use_decimal = kwargs.get('use_decimal')

return instance

def _start_session(self):
if self.auth_client.access_token is None:
self.auth_client.refresh(refresh_token=self.refresh_token)

self.session = OAuth2Session(
client_id=self.auth_client.client_id,
client_secret=self.auth_client.client_secret,
access_token=self.auth_client.access_token,
self.auth_client.client_id,
token={
'access_token': self.auth_client.access_token,
'refresh_token': self.auth_client.refresh_token,
}
)

return self.auth_client.refresh_token

def _drop(self):
Expand Down Expand Up @@ -162,9 +155,6 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
if request_id:
params['requestid'] = request_id

if self.invoice_link:
params['include'] = 'invoiceLink'

if not request_body:
request_body = {}

Expand All @@ -175,7 +165,6 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
}

if file_path:
attachment = open(file_path, 'rb')
url = url.replace('attachable', 'upload')
boundary = '-------------PythonMultipartPost'
headers.update({
Expand All @@ -186,7 +175,8 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
'Connection': 'close'
})

binary_data = str(base64.b64encode(attachment.read()).decode('ascii'))
with open(file_path, 'rb') as attachment:
binary_data = str(base64.b64encode(attachment.read()).decode('ascii'))

content_type = json.loads(request_body)['ContentType']

Expand Down Expand Up @@ -219,7 +209,10 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
"Application authentication failed", error_code=req.status_code, detail=req.text)

try:
result = req.json()
if (self.use_decimal):
result = json.loads(req.text, parse_float=decimal.Decimal)
else:
result = json.loads(req.text)
except:
raise exceptions.QuickbooksException("Error reading json response: {0}".format(req.text), 10000)

Expand All @@ -246,9 +239,9 @@ def process_request(self, request_type, url, headers="", params="", data=""):
return self.session.request(
request_type, url, headers=headers, params=params, data=data)

def get_single_object(self, qbbo, pk):
def get_single_object(self, qbbo, pk, params=None):
url = "{0}/company/{1}/{2}/{3}/".format(self.api_url, self.company_id, qbbo.lower(), pk)
result = self.get(url, {})
result = self.get(url, {}, params=params)

return result

Expand Down
Loading

0 comments on commit f4e1010

Please sign in to comment.