Skip to content

Commit f4e1010

Browse files
Merge branch 'master' into sync-upstream
2 parents b252a73 + 045cded commit f4e1010

31 files changed

+308
-699
lines changed

.github/workflows/pylint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
runs-on: ubuntu-latest
88
strategy:
99
matrix:
10-
python-version: ["3.8", "3.9", "3.10"]
10+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
1111
steps:
1212
- uses: actions/checkout@v3
1313
- name: Set up Python ${{ matrix.python-version }}

.github/workflows/python-package.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
19+
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
2020

2121
steps:
2222
- uses: actions/checkout@v3
@@ -27,7 +27,7 @@ jobs:
2727
- name: Install dependencies
2828
run: |
2929
python -m pip install --upgrade pip
30-
python -m pip install flake8 pytest
30+
python -m pip install flake8 pytest coverage pytest-cov
3131
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
3232
- name: Lint with flake8
3333
run: |
@@ -37,4 +37,8 @@ jobs:
3737
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
3838
- name: Test with pytest
3939
run: |
40-
pytest tests/unit
40+
pytest tests/unit --cov
41+
- name: Upload coverage reports to Codecov
42+
uses: codecov/codecov-action@v3
43+
env:
44+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

CHANGELOG.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
Changelog
22
=========
3+
4+
* 0.9.10 (August 7, 2024)
5+
* Update intuit-oauth dependency
6+
* Fix issues with Invoice Sharable Link
7+
* Added optional params to get
8+
9+
* 0.9.9 (July 9, 2024)
10+
* Removed simplejson
11+
* Added use_decimal option (See PR: https://github.com/ej2/python-quickbooks/pull/356 for details)
12+
13+
* 0.9.8 (May 20, 2024)
14+
* Added ItemAccountRef to SalesItemLineDetail
15+
* Updated from_json example in readme
16+
17+
* 0.9.7 (March 12, 2024)
18+
* Update intuit-oauth dependency
19+
* Updated CompanyCurrency to ref to use Code instead of Id
20+
* Added missing CurrentRef property from customer object
21+
* Made improvements to file attachment handling
22+
23+
* 0.9.6 (January 2, 2024)
24+
* Replace RAuth with requests_oauthlib
25+
* Removed python 2 code from client.py
26+
* Removed unused dependencies from Pipfile
27+
* Added new fields to Employee object
28+
* Added VendorAddr to Bill object
29+
* Added new fields to Estimate object
30+
* Fix TaxInclusiveAmt and vendor setting 1099 creation
31+
* Updated readme and contributing
32+
333
* 0.9.5 (November 1, 2023)
434
* Added the ability to void all voidable QB types
535
* Added to_ref to CreditMemo object

Pipfile

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ url = "https://pypi.org/simple"
44
verify_ssl = true
55

66
[dev-packages]
7+
coverage = "*"
8+
twine = "*"
9+
pytest = "*"
10+
pytest-cov = "*"
711

812
[packages]
9-
urllib3 = ">=1.26.5"
1013
bleach = ">=3.3.0"
11-
# Not needed as installed by Uncat core
12-
# intuit-oauth = {git = "git+https://github.com/uncategorizedexpense/oauth-pythonclient.git@master#egg=intuit-oauth"}
13-
rauth = ">=0.7.3"
14+
urllib3 = ">=2.1.0"
15+
# intuit-oauth = "==1.2.6"
1416
requests = ">=2.31.0"
15-
simplejson = ">=3.19.1"
16-
nose = "*"
17-
coverage = "*"
18-
twine = "*"
17+
requests_oauthlib = ">=1.3.1"
18+
setuptools = "*"

Pipfile.lock

Lines changed: 0 additions & 578 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ python-quickbooks
22
=================
33

44
[![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)
5-
[![Coverage Status](https://coveralls.io/repos/github/ej2/python-quickbooks/badge.svg?branch=master)](https://coveralls.io/github/ej2/python-quickbooks?branch=master)
5+
[![codecov](https://codecov.io/gh/ej2/python-quickbooks/graph/badge.svg?token=AKXS2F7wvP)](https://codecov.io/gh/ej2/python-quickbooks)
66
[![](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ej2/python-quickbooks/blob/master/LICENSE)
77
[![PyPI](https://img.shields.io/pypi/v/python-quickbooks)](https://pypi.org/project/python-quickbooks/)
88

@@ -54,14 +54,14 @@ Then create a QuickBooks client object passing in the AuthClient, refresh token,
5454
company_id='COMPANY_ID',
5555
)
5656

57-
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
57+
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
5858
details) pass in minorversion when setting up the client:
5959

6060
client = QuickBooks(
6161
auth_client=auth_client,
6262
refresh_token='REFRESH_TOKEN',
6363
company_id='COMPANY_ID',
64-
minorversion=59
64+
minorversion=69
6565
)
6666

6767
Object Operations
@@ -74,7 +74,9 @@ List of objects:
7474

7575
**Note:** The maximum number of entities that can be returned in a
7676
response is 1000. If the result size is not specified, the default
77-
number is 100. (See [Intuit developer guide](https://developer.intuit.com/docs/0100_accounting/0300_developer_guides/querying_data) for details)
77+
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)
78+
79+
**Warning:** You should never allow user input to pass into a query without sanitizing it first! This library DOES NOT sanitize user input!
7880

7981
Filtered list of objects:
8082

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

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

109+
110+
107111
List with custom Where and ordering
108112

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

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

115-
Filtering a list with a custom query (See [Intuit developer guide](https://developer.intuit.com/docs/0100_accounting/0300_developer_guides/querying_data) for
119+
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
116120
supported SQL statements):
117121

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

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

251-
Other operations
255+
Sharable Invoice Link
252256
----------------
253-
Add Sharable link for an invoice sent to external customers (minorversion must be set to 36 or greater):
257+
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:
258+
259+
invoice.AllowOnlineCreditCardPayment = True
260+
invoice.BillEmail = EmailAddress()
261+
invoice.BillEmail.Address = '[email protected]'
254262

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

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

258-
Void an invoice:
259267

268+
Void an invoice
269+
----------------
270+
Call `void` on any invoice with an Id:
271+
260272
invoice = Invoice()
261273
invoice.Id = 7
262274
invoice.void(qb=client)
@@ -273,10 +285,10 @@ Converting an object to JSON data:
273285

274286
Loading JSON data into a quickbooks object:
275287

276-
account = Account()
277-
account.from_json(
288+
account = Account.from_json(
278289
{
279290
"AccountType": "Accounts Receivable",
291+
"AcctNum": "123123",
280292
"Name": "MyJobs"
281293
}
282294
)

contributing.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,38 @@
11
# Contributing
22

3-
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.
3+
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.
44

55
# Testing
66

7-
I use [nose](https://nose.readthedocs.io/en/latest/index.html) and [Coverage](https://coverage.readthedocs.io/en/latest/) to run the test suite.
7+
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.
88

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

1111
## Testing setup:
1212

1313
1. Create/login into your [Intuit Developer account](https://developer.intuit.com).
1414
2. On your Intuit Developer account, create a Sandbox company and an App.
15-
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:
15+
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:
1616
```
1717
export CLIENT_ID="<Client ID>"
1818
export CLIENT_SECRET="<Client Secret>"
19-
export COMPANY_ID="<Realm ID>"
20-
export ACCESS_TOKEN="<Access token>"
19+
export COMPANY_ID="<Realm ID>"
2120
export REFRESH_TOKEN="<Refresh token>"
2221
```
2322

24-
*Note*: You will need to update the access token when it expires.
23+
*Note*: You will need to update the refresh token when it expires.
2524

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

29-
6. Run `nosetests . --with-coverage --cover-package=quickbooks`
28+
6. Run all tests: ```pytest --cov```
29+
Run only unit tests: ```pytest tests/unit --cov```
30+
Run only integration tests: ```pytest tests/integration --cov```
31+
32+
3033

3134
## Creating new tests
32-
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.
35+
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.
3336

3437
Example:
3538
```

quickbooks/client.py

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,16 @@
1-
import warnings
2-
3-
try: # Python 3
4-
import http.client as httplib
5-
from urllib.parse import parse_qsl
6-
from functools import partial
7-
to_bytes = lambda value, *args, **kwargs: bytes(value, "utf-8", *args, **kwargs)
8-
except ImportError: # Python 2
9-
import httplib
10-
from urlparse import parse_qsl
11-
to_bytes = str
12-
1+
import http.client as httplib
132
import textwrap
14-
import codecs
153
import json
16-
17-
from . import exceptions
184
import base64
195
import hashlib
206
import hmac
7+
import decimal
8+
9+
from . import exceptions
10+
from requests_oauthlib import OAuth2Session
2111

22-
try:
23-
from rauth import OAuth1Session, OAuth1Service, OAuth2Session
24-
except ImportError:
25-
print("Please import Rauth:\n\n")
26-
print("http://rauth.readthedocs.org/en/latest/\n")
27-
raise
12+
def to_bytes(value, *args, **kwargs):
13+
return bytes(value, "utf-8", *args, **kwargs)
2814

2915

3016
class Environments(object):
@@ -40,6 +26,7 @@ class QuickBooks(object):
4026
minorversion = None
4127
verifier_token = None
4228
invoice_link = False
29+
use_decimal = False
4330

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

85+
if 'use_decimal' in kwargs:
86+
instance.use_decimal = kwargs.get('use_decimal')
87+
9888
return instance
9989

10090
def _start_session(self):
10191
if self.auth_client.access_token is None:
10292
self.auth_client.refresh(refresh_token=self.refresh_token)
10393

10494
self.session = OAuth2Session(
105-
client_id=self.auth_client.client_id,
106-
client_secret=self.auth_client.client_secret,
107-
access_token=self.auth_client.access_token,
95+
self.auth_client.client_id,
96+
token={
97+
'access_token': self.auth_client.access_token,
98+
'refresh_token': self.auth_client.refresh_token,
99+
}
108100
)
101+
109102
return self.auth_client.refresh_token
110103

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

165-
if self.invoice_link:
166-
params['include'] = 'invoiceLink'
167-
168158
if not request_body:
169159
request_body = {}
170160

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

177167
if file_path:
178-
attachment = open(file_path, 'rb')
179168
url = url.replace('attachable', 'upload')
180169
boundary = '-------------PythonMultipartPost'
181170
headers.update({
@@ -186,7 +175,8 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
186175
'Connection': 'close'
187176
})
188177

189-
binary_data = str(base64.b64encode(attachment.read()).decode('ascii'))
178+
with open(file_path, 'rb') as attachment:
179+
binary_data = str(base64.b64encode(attachment.read()).decode('ascii'))
190180

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

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

221211
try:
222-
result = req.json()
212+
if (self.use_decimal):
213+
result = json.loads(req.text, parse_float=decimal.Decimal)
214+
else:
215+
result = json.loads(req.text)
223216
except:
224217
raise exceptions.QuickbooksException("Error reading json response: {0}".format(req.text), 10000)
225218

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

249-
def get_single_object(self, qbbo, pk):
242+
def get_single_object(self, qbbo, pk, params=None):
250243
url = "{0}/company/{1}/{2}/{3}/".format(self.api_url, self.company_id, qbbo.lower(), pk)
251-
result = self.get(url, {})
244+
result = self.get(url, {}, params=params)
252245

253246
return result
254247

0 commit comments

Comments
 (0)