Skip to content

Commit 3b103d4

Browse files
authored
Merge pull request #386 from ej2/0.9.12
0.9.12
2 parents 67141c2 + 71693d0 commit 3b103d4

24 files changed

+276
-117
lines changed

CHANGELOG.rst

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
* 0.9.12 (April 15, 2025)
5+
* Add CostRate to TimeActivity
6+
* Fix retrieval of Item SKU field
7+
* Added support for attaching files using byte streams
8+
* Default minor version to minimum supported version
9+
* Fix incompatibility issues with setuptools
10+
411
* 0.9.11 (February 10, 2025)
512
* Add warning for unsupported minorversion
613
* Fix issue with new versions of jsonEncoder

Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
publish: clean
2-
python setup.py sdist
2+
python -m build
33
twine upload dist/*
44

55
clean:
66
rm -vrf ./build ./dist ./*.egg-info
77
find . -name '*.pyc' -delete
8-
find . -name '*.tgz' -delete
8+
find . -name '*.tgz' -delete

Pipfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ urllib3 = ">=2.1.0"
1414
intuit-oauth = "==1.2.6"
1515
requests = ">=2.31.0"
1616
requests_oauthlib = ">=1.3.1"
17-
setuptools = "*"
17+
build = "*"

Pipfile.lock

+26-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A Python 3 library for accessing the Quickbooks API. Complete rework of
1010
[quickbooks-python](https://github.com/troolee/quickbooks-python).
1111

1212
These instructions were written for a Django application. Make sure to
13-
change it to whatever framework/method youre using.
13+
change it to whatever framework/method you're using.
1414
You can find additional examples of usage in [Integration tests folder](https://github.com/ej2/python-quickbooks/tree/master/tests/integration).
1515

1616
For information about contributing, see the [Contributing Page](https://github.com/ej2/python-quickbooks/blob/master/contributing.md).
@@ -247,6 +247,22 @@ Attaching a file to customer:
247247
attachment.ContentType = 'application/pdf'
248248
attachment.save(qb=client)
249249

250+
Attaching file bytes to customer:
251+
252+
attachment = Attachable()
253+
254+
attachable_ref = AttachableRef()
255+
attachable_ref.EntityRef = customer.to_ref()
256+
257+
attachment.AttachableRef.append(attachable_ref)
258+
259+
attachment.FileName = 'Filename'
260+
attachment._FileBytes = pdf_bytes # bytes object containing the file content
261+
attachment.ContentType = 'application/pdf'
262+
attachment.save(qb=client)
263+
264+
**Note:** You can use either `_FilePath` or `_FileBytes` to attach a file, but not both at the same time.
265+
250266
Passing in optional params
251267
----------------
252268
Some QBO objects have options that need to be set on the query string of an API call.

dev_requirements.txt

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
coverage==7.3.0
22
ipdb==0.13.13
3-
mock==5.1.0
43
nose==1.3.7

quickbooks/client.py

+40-23
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,19 @@ def __new__(cls, **kwargs):
7676
if 'company_id' in kwargs:
7777
instance.company_id = kwargs['company_id']
7878

79-
if 'minorversion' in kwargs:
80-
instance.minorversion = kwargs['minorversion']
81-
82-
if instance.minorversion < instance.MINIMUM_MINOR_VERSION:
83-
warnings.warn(
84-
'Minor Version no longer supported.'
85-
'See: https://blogs.intuit.com/2025/01/21/changes-to-our-accounting-api-that-may-impact-your-application/',
86-
DeprecationWarning)
79+
# Handle minorversion with default
80+
instance.minorversion = kwargs.get('minorversion', instance.MINIMUM_MINOR_VERSION)
81+
if 'minorversion' not in kwargs:
82+
warnings.warn(
83+
'No minor version specified. Defaulting to minimum supported version (75). '
84+
'Please specify minorversion explicitly when initializing QuickBooks. '
85+
'See: https://blogs.intuit.com/2025/01/21/changes-to-our-accounting-api-that-may-impact-your-application/',
86+
DeprecationWarning)
87+
elif instance.minorversion < instance.MINIMUM_MINOR_VERSION:
88+
warnings.warn(
89+
f'Minor Version {instance.minorversion} is no longer supported. Minimum supported version is {instance.MINIMUM_MINOR_VERSION}. '
90+
'See: https://blogs.intuit.com/2025/01/21/changes-to-our-accounting-api-that-may-impact-your-application/',
91+
DeprecationWarning)
8792

8893
instance.invoice_link = kwargs.get('invoice_link', False)
8994

@@ -152,13 +157,12 @@ def change_data_capture(self, entity_string, changed_since):
152157
return result
153158

154159
def make_request(self, request_type, url, request_body=None, content_type='application/json',
155-
params=None, file_path=None, request_id=None):
160+
params=None, file_path=None, file_bytes=None, request_id=None):
156161

157162
if not params:
158163
params = {}
159164

160-
if self.minorversion:
161-
params['minorversion'] = self.minorversion
165+
params['minorversion'] = self.minorversion
162166

163167
if request_id:
164168
params['requestid'] = request_id
@@ -172,7 +176,7 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
172176
'User-Agent': 'python-quickbooks V3 library'
173177
}
174178

175-
if file_path:
179+
if file_path or file_bytes:
176180
url = url.replace('attachable', 'upload')
177181
boundary = '-------------PythonMultipartPost'
178182
headers.update({
@@ -183,8 +187,11 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
183187
'Connection': 'close'
184188
})
185189

186-
with open(file_path, 'rb') as attachment:
187-
binary_data = str(base64.b64encode(attachment.read()).decode('ascii'))
190+
if file_path:
191+
with open(file_path, 'rb') as attachment:
192+
binary_data = str(base64.b64encode(attachment.read()).decode('ascii'))
193+
else:
194+
binary_data = str(base64.b64encode(file_bytes).decode('ascii'))
188195

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

@@ -233,10 +240,16 @@ def make_request(self, request_type, url, request_body=None, content_type='appli
233240
return result
234241

235242
def get(self, *args, **kwargs):
236-
return self.make_request("GET", *args, **kwargs)
243+
if 'params' not in kwargs:
244+
kwargs['params'] = {}
245+
246+
return self.make_request('GET', *args, **kwargs)
237247

238248
def post(self, *args, **kwargs):
239-
return self.make_request("POST", *args, **kwargs)
249+
if 'params' not in kwargs:
250+
kwargs['params'] = {}
251+
252+
return self.make_request('POST', *args, **kwargs)
240253

241254
def process_request(self, request_type, url, headers="", params="", data=""):
242255
if self.session is None:
@@ -248,10 +261,11 @@ def process_request(self, request_type, url, headers="", params="", data=""):
248261
request_type, url, headers=headers, params=params, data=data)
249262

250263
def get_single_object(self, qbbo, pk, params=None):
251-
url = "{0}/company/{1}/{2}/{3}/".format(self.api_url, self.company_id, qbbo.lower(), pk)
252-
result = self.get(url, {}, params=params)
264+
url = "{0}/company/{1}/{2}/{3}".format(self.api_url, self.company_id, qbbo.lower(), pk)
265+
if params is None:
266+
params = {}
253267

254-
return result
268+
return self.get(url, {}, params=params)
255269

256270
@staticmethod
257271
def handle_exceptions(results):
@@ -287,11 +301,11 @@ def handle_exceptions(results):
287301
else:
288302
raise exceptions.QuickbooksException(message, code, detail)
289303

290-
def create_object(self, qbbo, request_body, _file_path=None, request_id=None, params=None):
304+
def create_object(self, qbbo, request_body, _file_path=None, _file_bytes=None, request_id=None, params=None):
291305
self.isvalid_object_name(qbbo)
292306

293307
url = "{0}/company/{1}/{2}".format(self.api_url, self.company_id, qbbo.lower())
294-
results = self.post(url, request_body, file_path=_file_path, request_id=request_id, params=params)
308+
results = self.post(url, request_body, file_path=_file_path, file_bytes=_file_bytes, request_id=request_id, params=params)
295309

296310
return results
297311

@@ -307,9 +321,12 @@ def isvalid_object_name(self, object_name):
307321

308322
return True
309323

310-
def update_object(self, qbbo, request_body, _file_path=None, request_id=None, params=None):
324+
def update_object(self, qbbo, request_body, _file_path=None, _file_bytes=None, request_id=None, params=None):
311325
url = "{0}/company/{1}/{2}".format(self.api_url, self.company_id, qbbo.lower())
312-
result = self.post(url, request_body, file_path=_file_path, request_id=request_id, params=params)
326+
if params is None:
327+
params = {}
328+
329+
result = self.post(url, request_body, file_path=_file_path, file_bytes=_file_bytes, request_id=request_id, params=params)
313330

314331
return result
315332

quickbooks/mixins.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,26 @@ class ListMixin(object):
255255

256256
@classmethod
257257
def all(cls, order_by="", start_position="", max_results=100, qb=None):
258-
"""
259-
:param start_position:
260-
:param max_results: The max number of entities that can be returned in a response is 1000.
261-
:param qb:
262-
:return: Returns list
263-
"""
264-
return cls.where("", order_by=order_by, start_position=start_position,
265-
max_results=max_results, qb=qb)
258+
"""Returns list of objects containing all objects in the QuickBooks database"""
259+
if qb is None:
260+
qb = QuickBooks()
261+
262+
# For Item objects, we need to explicitly request the SKU field
263+
if cls.qbo_object_name == "Item":
264+
select = "SELECT *, Sku FROM {0}".format(cls.qbo_object_name)
265+
else:
266+
select = "SELECT * FROM {0}".format(cls.qbo_object_name)
267+
268+
if order_by:
269+
select += " ORDER BY {0}".format(order_by)
270+
271+
if start_position:
272+
select += " STARTPOSITION {0}".format(start_position)
273+
274+
if max_results:
275+
select += " MAXRESULTS {0}".format(max_results)
276+
277+
return cls.query(select, qb=qb)
266278

267279
@classmethod
268280
def filter(cls, order_by="", start_position="", max_results="", qb=None, **kwargs):

quickbooks/objects/attachable.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def __init__(self):
2727
self.AttachableRef = []
2828
self.FileName = None
2929
self._FilePath = ''
30+
self._FileBytes = None
3031
self.Note = ""
3132
self.FileAccessUri = None
3233
self.TempDownloadUri = None
@@ -53,10 +54,18 @@ def save(self, qb=None):
5354
if not qb:
5455
qb = QuickBooks()
5556

57+
# Validate that we have either file path or bytes, but not both
58+
if self._FilePath and self._FileBytes:
59+
raise ValueError("Cannot specify both _FilePath and _FileBytes")
60+
5661
if self.Id and int(self.Id) > 0:
57-
json_data = qb.update_object(self.qbo_object_name, self.to_json(), _file_path=self._FilePath)
62+
json_data = qb.update_object(self.qbo_object_name, self.to_json(),
63+
_file_path=self._FilePath,
64+
_file_bytes=self._FileBytes)
5865
else:
59-
json_data = qb.create_object(self.qbo_object_name, self.to_json(), _file_path=self._FilePath)
66+
json_data = qb.create_object(self.qbo_object_name, self.to_json(),
67+
_file_path=self._FilePath,
68+
_file_bytes=self._FileBytes)
6069

6170
if self.Id is None and self.FileName:
6271
obj = type(self).from_json(json_data['AttachableResponse'][0]['Attachable'])

quickbooks/objects/employee.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Employee(QuickbooksManagedObject, QuickbooksTransactionEntity):
1717

1818
def __init__(self):
1919
super(Employee, self).__init__()
20-
self.SSN = ""
20+
self.SSN = None
2121

2222
self.GivenName = ""
2323
self.FamilyName = ""
@@ -29,9 +29,9 @@ def __init__(self):
2929
self.Title = ""
3030
self.BillRate = 0
3131
self.CostRate = 0
32-
self.BirthDate = ""
32+
self.BirthDate = None
3333
self.Gender = None
34-
self.HiredDate = ""
34+
self.HiredDate = None
3535
self.ReleasedDate = ""
3636
self.Active = True
3737
self.Organization = False

quickbooks/objects/timeactivity.py

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def __init__(self):
3232
self.StartTime = None
3333
self.EndTime = None
3434
self.Description = None
35+
self.CostRate = None
3536

3637
self.VendorRef = None
3738
self.CustomerRef = None

setup.cfg

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
[metadata]
2-
description-file = README.md
3-
41
[flake8]
5-
max-line-length = 100
6-
max-complexity = 10
2+
max_line_length = 100
3+
max_complexity = 10
74
filename = *.py
85
format = default
96
exclude =/quickbooks/objects/__init__.py

setup.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def read(*parts):
1010
return fp.read()
1111

1212

13-
VERSION = (0, 9, 11)
13+
VERSION = (0, 9, 12)
1414
version = '.'.join(map(str, VERSION))
1515

1616
setup(
@@ -30,7 +30,6 @@ def read(*parts):
3030
},
3131

3232
install_requires=[
33-
'setuptools',
3433
'intuit-oauth==1.2.6',
3534
'requests_oauthlib>=1.3.1',
3635
'requests>=2.31.0',

0 commit comments

Comments
 (0)